在Linux系统中,信号是一种用于进程间通信和进程控制的机制,它允许系统内核和用户进程对其他进程进行通知、干预和控制。信号可以被用于各种用途,例如终止进程、暂停进程、捕捉异常以及处理用户自定义事件。
为了更好地理解进程信号,我们将从以下几个方面进行探讨:
- 信号的基本概念:什么是信号,信号的种类,以及信号的特性。
- 信号的发送与处理:如何向进程发送信号,进程如何捕捉和处理信号。
- 常用信号:例如 SIGINT、SIGTERM、SIGKILL、SIGHUP 等的用途和区别。
- 信号的实际应用:在实际编程中如何利用信号进行进程控制和通信。
信号的基本概念
信号是进程之间事件异步通知的一种方式,属于 软中断 。
使用kill -l
命令可以查看系统定义的信号列表:
每个命令前的编号就是它们的宏定义,比如二号信号,其宏定义就是:define SIGINT 2
,可以使用SIGINT
进行替换;
如果需要知道信号的详细信息,可以在man
手册加上-7
选项,可以显示信号的详细信息;
信号的产生
信号的产生有多种方式,下面分别介绍:
通过终端按键产生信号
SIGINT
的默认处理动作是终止进程,SIGQUIT
的默认处理动作是终止进程并且Core Dump
,现在我们来验证一下。
验证之前,先把一些名词解释一些: Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查 core 文件以查清错误原因,这叫做Post-mortem Debug
(事后调试)。
而一个进程允许产生多大的 core 文件,取决于进程的Resource Limit
(这个信息保存在PCB中),可以通过ulimit -a
命令查看相应的用户限制,比如:
默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit
命令改变这个限制,允许产生 core 文件。 首先用ulimit
命令改变 Shell 进程的Resource Limit
,允许core文件最大为1024K:
ulimit -c 1024
<br>
下面开始验证:
SIGINT
的默认处理动作是终止进程,SIGQUIT
的默认处理动作是终止进程并且Core Dump
注意这里的SIGINT
对应键盘ctrl + c
,SIGQUIT
对应ctrl + \
实验代码如下:
#include <iostream>
#include <unistd.h>
int main(){
while(1){
// 死循环,用于测试是否产生 core dump
std::cout << "pid : " << getpid() << std::endl;
sleep(1);
}
return 0;
}
实验结论:
当采用ctrl + c
(即SIGINT
)结束时,不会产生对应的core
文件,而使用ctrl + \
(对应SIGQUIT
)时,则会产生对应core
文件,由此便验证了上述结论;
而该core
文件可用于 _gdb_调试选项,可用于快速确定错误原因和错误位置;
通过系统函数调用向进程发送信号
kill 函数
kill 函数的使用:
kill [-信号] [进程id]
可用 kill 函数执行相应的系统调用,以此向进程发送信号;
通过在另一个终端调用系统调用kill
函数,杀死了相应进程;<br>
raise 函数
int raise(int sig);
向当前所在进程发送信号,就相当于kill -sig getpid()
,即作用对象始终为当前进程;<br>
abort 函数
直接终止当前进程,没有id
和信号
选项;
由软件信号产生信号
之前介绍过管道,其中当管道的读取端关闭后,若继续向写入端写入数据,操作系统会终止该行为,其中发送的终止信号即为SIGPIPE
,这是一种软件信号;
alarm 函数和 SIGALRM 信号
下面将再介绍另一种函数及其对应的软件信号:alarm
函数和SIGALRM
信号;
#include <unistd.h>
unsigned int alarm(unsigned int secends);
// 该函数用于设定一个闹钟,告诉内核在`seconds`秒后给当前进程发`SIGALRM`信号
// 该信号的默认处理动作是终止当前进程
该函数的返回值是0
或者以前设定的闹钟时间还余下的秒数
;如果seconds
值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数,下面写一个程序验证一下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void alarm_handler(int sig) {
printf("Alarm triggered!\n");
}
int main() {
// 注册 SIGALRM 信号处理函数
signal(SIGALRM, alarm_handler);
// 第一次设置闹钟为5秒
unsigned int remaining = alarm(5);
printf("First alarm set to 5 seconds, returned: %u seconds\n", remaining);
// 第二次设置闹钟为2秒,验证返回之前闹钟还剩余的时间
sleep(2);
remaining = alarm(2);
printf("Second alarm set to 2 seconds, returned: %u seconds\n", remaining);
// 取消闹钟,验证返回之前还剩余的时间
sleep(1);
remaining = alarm(0);
printf("Alarm canceled, returned: %u seconds\n", remaining);
// 睡眠3秒,等待看是否会有闹钟触发
sleep(3);
return 0;
}
硬件异常产生信号
硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送对应信号对进程作出处理;
常见的硬件异常有除0异常
,CPU的运算单元会产生异常,内核将该异常解释为SIGFPE
(浮点异常)信号发送给进程;还有非法内存地址访问异常
,MMU
会产生异常,内核将该异常解释为SIGSEGV
(段错误)信号发送给进程;
常见硬件异常包括:
- SIGFPE(浮点异常): 当程序执行了非法的算术操作(如除以0或浮点数异常)时,由硬件产生信号。
- SIGSEGV(段错误): 当进程访问了非法内存地址时产生,比如访问超出分配的内存区域。这通常涉及到内存管理单元(MMU)的检测。
- SIGILL(非法指令): 当CPU尝试执行非法或不支持的指令时产生,比如代码被破坏或执行了无效的机器指令。
- SIGBUS(总线错误): 当进程尝试访问未对齐的数据或无法访问的物理地址时产生。
MMU(Memory Management Unit,内存管理单元)是计算机硬件中负责虚拟内存管理和物理内存访问的一个硬件组件。它主要负责以下几个关键功能:
- 地址转换: MMU将CPU生成的虚拟地址转换为物理地址。当进程访问某个内存地址时,它实际上是访问一个虚拟地址,MMU负责将这个虚拟地址映射到真实的物理内存地址。
- 分页和分段: MMU支持分页或分段的内存管理模式。在分页系统中,内存被分成固定大小的页,而每一页都可以映射到不同的物理内存位置。分段系统中,内存分为逻辑段,MMU负责管理这些段的访问权限和边界。
- 内存保护: MMU允许对不同的内存区域设置访问权限(例如读、写、执行权限),从而防止进程访问未授权的内存区域。如果进程试图访问受保护的内存区域,MMU会产生一个异常(例如 SIGSEGV)。
- 页表管理: MMU使用页表来记录虚拟地址到物理地址的映射。页表通常由操作系统维护,但由MMU在硬件层面进行查找和使用。
信号的捕捉
signal
函数:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数之前的代码演示中已经出现,这里正式解释一下它的作用:该函数用于自定义对应信号的处理方法,下面进行实验演示:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int num){
int cnt = 10;
while(cnt--){
std::cout << "倒数 " << cnt << " 秒: " << std::endl;
std::cout << "捕捉到了" << num << "号信号,正在处理..." << std::endl;
sleep(1);
}
std::cout << "任务处理结束,即将退出..." << std::endl;
sleep(1);
exit(1);
}
int main(){
signal(SIGINT, handler);
while(1){
std::cout << "正在执行任务..." << std::endl;
sleep(1);
}
return 0;
}
当进程运行时,向对应进程pid
发送2号信号:
kill -2 [pid]
可以发现,调用signal
函数可以重置2号信号的处理方式;
但是注意,9号信号其设计初衷就是为了立即终止进程,所以无法对9号信号进行自定义操作,同时对于我们后面要学习的设置阻塞信号,9号信号依然不支持被用户自定义屏蔽或阻塞;
下面再测试一下上述的硬件异常问题导致的信号传递:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signo){
int cnt = 10;
while(cnt){
std::cout << "段错误信号,信号编号为 " << signo << " ,已被捕捉,正在处理... ";
std::cout << "倒计时:" << cnt << std::endl;
--cnt;
sleep(1);
}
std::cout << "信号处理完毕,即将退出..." << std::endl;
sleep(1);
exit(1);
}
// 模拟野指针错误
int main(){
signal(SIGSEGV, handler);
std::cout << "段错误即将发生,请注意..." << std::endl;
sleep(1);
{
int* p = nullptr;
*p = 10;
}
return 0;
}
由此可以确认,当我们在C/C++中发生除零异常、内存越界访问等异常时,在系统层面上是被当作信号处理的;
总结
综上,我们便完成了从信号的发送、接收到处理的大致流程的认识,下一篇文章我会进一步介绍信号的储存、传送和处理相关的细节问题,还会涉及到上面提到的阻塞信号的概念; 具体的代码集合可以到我的GitHub 主页查看下载,这一节的代码在signal/por_signal文件夹下。
标签:int,MMU,内存,信号,Linux,进程,include From: https://blog.51cto.com/u_16271511/12073719