【Linux】信号相关知识详细梳理
目录
1. 信号概念
2. 信号的产生
3. 信号处理
4. 信号原理
4.1 理解信号行为
4.2 重谈信号产生
4.3 信号保存
4.3.1 信号集
4.3.2 信号集操作函数
4.4 信号处理
4.4.1 信号捕捉和处理
1. 信号概念
在 Linux 系统中,信号是一种用于进程间通信 (IPC) 的机制,可以用来通知进程发生了某些事件。信号是由内核或进程发送的,目标是终止、暂停、恢复或触发特定行为。
- 信号 (Signal) 是一种软件中断,用于通知进程需要执行某种特定的处理。
 - 信号的来源可以是用户、内核或者其他进程。
 - 每个信号都有一个唯一的编号 和对应的符号名称(例如,
SIGKILL对应编号为 9)。 
以下是一张信号表,可用kill -l 命令查看:

2. 信号的产生
在讲信号的产生前,首先看一个信号的例子:
以下程序是一个捕捉信号 SIGINT (编号9),并执行对应的信号处理方法的程序,代码如下:
 #include <stdio.h>
    
 #include <signal.h>
    
  
    
 //信号处理方法
    
 void sigcb(int signum)
    
 {
    
     printf("signum is %d\n",signum);
    
 }
    
  
    
 int main()
    
 {
    
     //设置要捕捉的信号,和信号处理方法
    
     signal(SIGINT,sigcb);
    
     while(1)
    
     {}
    
     return 0;
    
 }
    
    
    
    
        我们都知道 按住 ctrl + c可用终止一个进程的运行,实际上ctrl + c的原理就是向进程发送SIGINT信号,该信号的默认处理方式就是终止程序,而我们设置了信号处理方法后,不再用默认方式处理信号,于是现象如下:

可以看到,对应进程在接收到SIGINT后,执行了我们设置的处理方法,其中ctrl + c就是一种产生信号的方式,接下来在看看其他方式。
kill命令:
    kill [选项] 信号 [PID...]
    
    
        - 信号 :可以是信号的名称(如 
SIGTERM)、编号(如15),或前缀省略的简写(如TERM)。- PID :目标进程的 ID(进程号),也可以是多个 PID。
 
 
例如:
 # 向进程1234发信号SIGKILL信号的不同方式:
    
  
    
 kill -SIGKILL 1234   # 使用信号名称
    
 kill -9 1234         # 使用信号编号
    
 kill -KILL 1234      # 使用简写
    
    
    
    
        现象示例:

3. 信号处理
对于一个信号的处理,有三种方式:
1. 忽略此信号
 #include <iostream>
    
  
    
 #include <unistd.h>
    
 #include <signal.h>
    
 void handler(int signumber)
    
 {
    
 std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber
    
 << std::endl;
    
 } i
    
 nt main()
    
 {
    
 std::cout << "我是进程: " << getpid() << std::endl;
    
 signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏
    
 while(true){
    
 std::cout << "I am a process, I am waiting signal!" << std::endl;
    
 sleep(1);
    
 }
    
 } 
    
 $ g++ sig.cc -o sig
    
 $ ./sig
    
 // 现象:
    
 // 我是进程: 212681
    
 // I am a process, I am waiting signal!
    
 // I am a process, I am waiting signal!
    
 // ^C^C^C^C^C^CI am a process, I am waiting signal! // 输⼊ctrl+c毫⽆反应
    
 // I am a process, I am waiting signal!
    
    
    
    
        2. 执行该信号的默认处理动作
 #include <iostream>
    
 #include <unistd.h>
    
 #include <signal.h>
    
 void handler(int signumber)
    
 {
    
     std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber<< std::endl;
    
 } 
    
 int main()
    
 {
    
     std::cout << "我是进程: " << getpid() << std::endl;
    
     signal(SIGINT/*2*/, SIG_DFL);
    
     while(true){
    
     std::cout << "I am a process, I am waiting signal!" << std::endl;
    
     sleep(1);
    
     }
    
 } 
    
 $ g++ sig.cc -o sig
    
 $ ./sig
    
 // 我是进程: 212791
    
 // I am a process, I am waiting signal!
    
 // I am a process, I am waiting signal!
    
 // ^C // 输⼊ctrl+c,进程退出,就是默认动作
    
    
    
    
        3. 提供⼀个信号处理函数,要求内核在处理该信号时切换到用户态 执行这个处理函数,这种方式称为自定义捕捉(Catch)⼀个信号。
 #include <stdio.h>
    
 #include <signal.h>
    
  
    
 //信号处理方法
    
 void sigcb(int signum)
    
 {
    
     printf("signum is %d\n",signum);
    
 }
    
  
    
 int main()
    
 {
    
     //设置要捕捉的信号,和信号处理方法
    
     signal(SIGINT,sigcb);
    
     while(1)
    
     {}
    
     return 0;
    
 }
    
    
    
    
        简易认识了信号的产生及处理后,接下来开始了解信号的原理。
4. 信号原理
4.1 理解信号行为
首先要知道,现代计算机是依靠硬件中断来执行各个IO设备的读取和输出的。
什么是硬件中断?
- 硬件中断是外部设备(如键盘、硬盘、网卡等)通过向 CPU 发出信号,通知其发生了特定事件的一种机制。
 - 中断的目的是让 CPU 停止当前执行的任务,去处理设备请求,例如读取数据、写入数据等。
 
而信号和中断具有相似性:
1.信号其实是从纯软件角度,模拟硬件中断的行为。
2. 只不过硬件中断是发给CPU,而信号是发给进程。
3. 两者有相似性,但是层级不同。
**** 为了更好的认识信号是对硬件中断的模拟,下面用一段程序来模拟计算机运行的过程:
下面会用到一个叫alarm的系统调用函数,作用是设定一个闹钟并向进程发送SIGALRM信号,我们将用这个函数来模拟操作系统中的时间片调度,模拟硬件中断。
 #include <iostream>
    
 #include <unistd.h>
    
 #include <signal.h>
    
 #include <vector>
    
 #include <functional>
    
  
    
 using func_t = std::function<void()>;
    
 //计数
    
 int gcount = 0;
    
 //模拟中断向量表
    
 std::vector<func_t> gfuncs;
    
 // 把信号 更换 成为 硬件中断
    
 void hanlder(int signo)
    
 {
    
     for (auto &f : gfuncs)
    
     {
    
     //模拟执行中断向量表中的预定任务
    
     f();
    
     }
    
     std::cout << "gcount : " << gcount << std::endl;
    
     int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间
    
     std::cout << "剩余时间 : " << n << std::endl;
    
 }
    
 int main()
    
 {
    
     //向中断向量表中添加预定任务
    
     gfuncs.push_back([](){ std::cout << "我是⼀个内核刷新操作" << std::endl; });
    
     gfuncs.push_back([](){ std::cout << "我是⼀个检测进程时间⽚的操作,如果时间⽚到了,我会切换进程 " << std::endl; });
    
     gfuncs.push_back([](){ std::cout << "我是⼀个内存管理操作,定期清理操作系统内部的内存碎⽚ " << std::endl; });
    
     alarm(1); // ⼀次性的闹钟,超时alarm会⾃动被取消
    
     signal(SIGALRM, hanlder);
    
     while (true)
    
     {
    
     pause();
    
     std::cout << "我醒来了..." << std::endl;
    
     gcount++;
    
     }
    
 }
    
    
    
    
        现象:

进一步理解了信号的行为后,我们继续来了解信号具体如何产生,保存以及处理。 4.1
4.2 重谈信号产生
信号产生的条件有两种,一种是 软件条件,一种是硬件异常:
1. 软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。 这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。 例如上面的alarm就是一种软件条件。
2. 硬件异常
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。 例如,当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如,当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
子进程退出时也可能会产生信号,他可能和软件条件,硬件异常都有关:
子进程退出有两种情况,依据情况不同,返回值也不同:

可以用程序来验证:
 #include <stdio.h>
    
 #include <sys/types.h>
    
 #include <sys/wait.h>
    
 #include <unistd.h>
    
 #include <stdlib.h>
    
  
    
 int main()
    
 {
    
     printf("测试1:\n");
    
     pid_t pid = fork();
    
     if (pid < 0)
    
     return -1;
    
     if (pid == 0)
    
     {
    
     // while (1)
    
     //{
    
     printf("i am child process\n");
    
     sleep(3);
    
     // }
    
     printf("child process quit!\n");
    
     exit(10);
    
     }
    
     else
    
     {
    
     int status;
    
     int ret = wait(&status);
    
     if (ret > 0 && (status & 0xFF) == 0)
    
     {
    
         // 子进程正常退出
    
         printf("子进程正常退出,子进程退出码:%d\n", (status >> 8) & 0xFF);
    
     }
    
     else if (ret > 0)
    
     {
    
         // 子进程异常退出
    
         printf("子进程异常退出,子进程退出信号:%d,coredump:%d", status & 0x7F, (status & 0xFF) >> 7);
    
     }
    
     }
    
     ///
    
     printf("测试2:\n");
    
     pid = fork();
    
     if (pid < 0)
    
     return -1;
    
     if (pid == 0)
    
     {
    
     while (1)
    
     {
    
         printf("i am child process\n");
    
         sleep(3);
    
     }
    
     printf("child process quit!\n");
    
     exit(10);
    
     }
    
     else
    
     {
    
     int status;
    
     int ret = wait(&status);
    
     if (ret > 0 && (status & 0xFF) == 0)
    
     {
    
         // 子进程正常退出
    
         printf("子进程正常退出,子进程退出码:%d\n", (status >> 8) & 0xFF);
    
     }
    
     else if (ret > 0)
    
     {
    
         // 子进程异常退出
    
         printf("子进程异常退出,子进程退出信号:%d,coredump:%d\n", status & 0x7F, (status & 0xFF) >> 7);
    
     }
    
     }
    
     return 0;
    
 }
    
    
    
    
        
 
其中的core dump是什么呢?
当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core ,这叫做CoreDump 。
SIGINT的默认处理动作是终⽌进程,SIGQUIT的默认处理动作是终止进程并且CoreDump,可以用来验证:

进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。
⼀个进程允许产生多大的 core 文件取决于进程的 Resource Limit (这个信息保存在PCB
中)。默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。
在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。首先用 ulimit 命令
改变 Shell 进程的 Resource Limit ,如允许 core 文件最大为 1024K: $ ulimit -c 1024
4.3 信号保存
前置概念:
实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞(Block)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动
作。
信号在内核中的形式:

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动作。 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志 。在上
图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,⼀旦产生SIGQUIT信号将被阻塞,它的处理动作是用户定义函数
sighandler。
那么如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
在Linux中:常规信号 在递达之前产生多次只计⼀次,而 实时信号 在递达之前产生多次可以依次放在⼀个队列里。
4.3.1 信号集
由上面的内容可知, 每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t 来存储,sigset_t 称为信号集。
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
4.3.2 信号集操作函数
 include <signal.h>
    
 int sigemptyset(sigset_t *set);
    
 int sigfillset(sigset_t *set);
    
 int sigaddset(sigset_t *set, int signo);
    
 int sigdelset(sigset_t *set, int signo);
    
 int sigismember(const sigset_t *set, int signo);
    
    
    
    
        函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_t类型的变量之前,⼀定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
sigismember是⼀个布尔函数,用于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
1. sigprocmask
sigprocmask 用于检查或更改当前进程的信号屏蔽字(阻塞信号)。
 #include <signal.h>
    
  
    
 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    
    
    
    
        参数说明
how:决定如何修改信号屏蔽字,可选值:SIG_BLOCK:将set中的信号加入当前屏蔽信号集合。SIG_UNBLOCK:从当前屏蔽信号集合中移除set中的信号。SIG_SETMASK:替换当前的屏蔽信号集合为set。
set:指向一个sigset_t类型的信号集合,表示要修改的信号。oldset:保存修改前的信号屏蔽字,如果不需要保存,可传入NULL。
2. sigaction
sigaction 是用于设置某个信号的处理方式的函数,比早期的 signal 函数更灵活和可靠。
 #include <signal.h>
    
  
    
 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    
    
    
    
        参数说明
signum:指定要处理的信号编号,例如SIGINT、SIGTERM。act:- 指向一个 
struct sigaction结构的指针,用于指定新的信号处理动作。 
- 指向一个 
 oldact:- 指向一个 
struct sigaction结构的指针,用于保存先前的信号处理设置。如果不需要保存,可传入NULL。 
- 指向一个 
 
其中 struct sigaction的结构:
 struct sigaction {
    
     void (*sa_handler)(int);          // 信号处理函数,或以下特殊值之一
    
                                    // SIG_DFL: 使用默认处理
    
                                    // SIG_IGN: 忽略信号
    
     void (*sa_sigaction)(int, siginfo_t *, void *); // 备用信号处理函数
    
     sigset_t sa_mask;                 // 在处理此信号时屏蔽的其他信号
    
     int sa_flags;                     // 标志位
    
     void (*sa_restorer)(void);        // 已废弃
    
 };
    
    
    
    
        常用标志(sa_flags)
SA_RESTART:使被信号中断的系统调用自动重新启动。SA_NOCLDSTOP:对SIGCHLD信号有效,不在子进程停止时产生信号。SA_SIGINFO:使用sa_sigaction而非sa_handler,可以获得更多信号信息。
3. sigpending
用于获取当前进程中处于挂起状态 的信号集合(即已触发但因屏蔽未处理的信号)。
 #include <signal.h>
    
  
    
 int sigpending(sigset_t *set);
    
    
    
    
        set:存储挂起的信号集合。
示例:
 sigset_t pending;
    
 sigpending(&pending);
    
 if (sigismember(&pending, SIGINT)) {
    
     printf("SIGINT is pending\n");
    
 }
    
    
    
    
        更多信号集操作函数请查表自行了解~。
4.4 信号处理
4.4.1 信号捕捉和处理
信号捕捉的流程图:

总结来说就是:

如上图的一个横着的 ‘8’,用户态和内核态每个循环会切换四次,同时注意检查pending表未决信号是在内核态进行的。
补充:内核态和用户态 可以理解为 一个是操作系统内核级别,一个是用户级别,权限以及可操作范围不同,用户态就是执行用户[0,3]GB时所处的状态,内核态就是执行内核[3,4]GB时所处的状态。
