文章目录
一、信号概念
我们之前提到过System V 信号量,请注意:信号和信号量完全没有关系。
信号,是外部或其他人或硬件给进程发送的一种异步的事件通知机制。
- 异步:指多种事件,彼此不影响,同时发生
- 事件:包括进程终止,异常,指令退出...
通过指令kill -l,可以查到系统中所有的信号种类:

在内核中,信号编号的和名字是用宏定义实现的,所以代码中使用时2和SIGINT是等价的。1 ~ 31信号是普通信号,34 ~ 64是实时信号,本文所谈内容只针对普通信号
最简单的信号产生方式是命令kill -信号编号 进程pid,给指定进程发送一个信号。
进程识别信号,是系统内置的。信号的处理方法,在信号产生之前一定是准备好了的!
信号的完整使用过程是:信号产生、信号保存、信号处理
二、信号处理
我们首先来讲信号处理的方法。
一个信号到来,进程有三种方法处理他:
- 使用默认动作(大部分信号的默认是终止进程)
- 忽略这个信号
- 使用自定义动作
这些信号各自在什么条件下产生,默认的处理动作是什么,在man 7 signal中都能查到。
前两种方式很好理解,如2号信号的默认动作是终止进程,到来时,进程采用他的默认动作:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
while (1)
{
cout << getpid() << endl;
sleep(1);
}
return 0;
}

重点是自定义动作:
系统调用signal,可以更改当前进程对一个信号的处理动作!

他的第一个参数是想要改变的信号的编号,第二个参数是一个回调函数,即自定义动作!
这个自定义动作函数,必须有一个int参数,返回值为空。被调用时,参数就会自动传入这个信号编号!
第二个参数除了可以自己传,还有两个额外选项:SIG_IGN表示忽视这个信号,SIG_DFL表示采用默认动作。
signal函数的返回值也是一个函数指针,是这个信号的老的处理动作。
值得注意的是,signal函数只要在代码的开头调用一次就行了,整个进程都有效。
演示:
c
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
printf("接收到了%d号信号\n", sig);
}
int main()
{
signal(2, handler);
while (1)
{
printf("%d\n", getpid());
sleep(1);
}
return 0;
}

可以发现,现在发送2号信号无法终止进程了,而是进程执行了自定义动作函数!
如果所有的信号都被自定义动作了,那我们该如何退出进程呢?
其实,9号和19号信号是无法被自定义动作的,称之为"管理员信号"。作用分别是强制终止进程、强制暂停进程。进程无法忽略这两个信号!
还有一个函数
sigaction,和signal作用类似,它的操作粒度更细,用法也更多样。
三、信号产生
信号,有五种产生方式:
- 命令kill产生:命令
kill -信号编号 进程pid,给指定进程发送一个信号。最简单的一种。 - 用键盘产生:按下
ctrl c,表示发送2号信号;按下ctrl \,表示发送3号信号;按下ctrl z,表示发送19号信号,等等。
2号和3号信号的默认动作是终止进程,19号信号的默认动作是暂停进程。
当然,这种方式只能控制前台进程,只有前台进程能获取键盘的输入。
除了9号和19号,bash不对自己的任何信号响应,因为都自动忽略了。
- 程序异常崩溃:如果你写下了除0、野指针等非法操作,就会发现程序运行到这里时就会崩溃终止。这是因为程序异常,导致收到了相关信号,如除0会发出8号信号,访问野指针会发出11号信号。
本质原因是,硬件出错了!OS作为硬件的管理者,他就一定知道。而产生信号的方式有很多种,但最后只有OS能目标进程"写入"信号!
进程的task_struct中,会维护信号位图,OS向其发送信号本质就是写入信号位图!
-
函数产生信号:
-
函数
kill:

这个系统调用很简单,第一个参数是目标进程,第二个参数是发送的信号。
-
函数
raise:

这个函数是向当前调用它的进程发送一个信号。其实等价于
kill(getpid(), sig)。kill可以向任意进程发送信号,但raise是向自己发送信号。 -
函数
abort:

函数
abort会向当前进程发送6号信号而使进程强制终止,6号信号可以被signal修改动作,但是执行自定义动作后还是会终止程序!abort通常用于终止进程,类似于exit,区别在于abort会进行核心转储(Core Dump),核心转储是程序异常终止时,OS将进程内存内容保存到磁盘的文件,用于事后调试分析崩溃原因。
-
-
软件条件产生信号:软件条件产生的信号分类和举例有很多,如管道读端关闭时进程终止、alarm。
alarm是一个用于设置闹钟的函数。它可以让内核在指定的秒数后,向当前进程发14号信号。

它的参数是想要设置的秒数,返回值是上一个闹钟剩余时间。
alarm(0)可以取消闹钟
四、信号保存
在这里,我们需要补充几个概念:
- 实际处理信号的动作称为"信号递达"(Deliver)
- 从信号产生到信号递达之间的状态,称为"信号未决"(Pending)
- 信号可以被进程"阻塞"(Block)
- 被阻塞的信号产生后将保持在未决状态,直到进程解除对其的阻塞,信号就会立刻递达!
- 阻塞不等于忽略,只要信号被阻塞就不会递达,而忽略是信号递达后的一种处理动作!
在内核中,信号机制的核心是靠task_struct的三张表完成组织的!

- block表,是一张31位的位图。位的编号是信号编号,位的内容表示该信号是否阻塞!
- pending表,是一张31位的位图。位的编号是信号编号,位的内容表示是否收到该信号!
- handler表,是一个函数指针数组,为信号动作表。数组下标+1为信号编号,SIG_DFL表示执行默认动作,SIG_IGN表示忽略动作!
假设2号信号的block位为1,即被阻塞。当2号信号产生时,pending表第2位会置为1,然后检查block表,如果被阻塞,此时就不能处理2号信号,只有解除阻塞后,才能完成信号递达,pending表第2位恢复为0。
可以在内核源码中看到,用于存储信号的位图是sigset_t类型,而本质是一个unsigned long 类型,我们称sigset_t为信号集,这个类型可以表示每个信号的"有效"和"无效"状态。



block表也叫做阻塞信号集,阻塞信号集也叫做信号屏蔽字(Signal Mask)
系统提供了一些函数用于操作sigset_t变量:
c
#include <signal.h>
// 清空信号集set,信号全初始化为0
int sigemptyset(sigset_t *set);
// 填满信号集set,信号全设为为1
int sigfillset(sigset_t *set);
// 向信号集set中添加一个指定信号signum,将这一位信号设为1
int sigaddset(sigset_t *set, int signum);
// 从信号集set中删除一个指定信号signum,将这一位信号设为0
int sigdelset(sigset_t *set, int signum);
// 检测指定信号signum是否存在于信号集set中。
int sigismember(const sigset_t *set, int signum);
前四个函数都是成功返回0,失败返回-1。sigismember是布尔函数,若包含返回1,不包含返回0,失败返回-1。
在使用sigset_t变量前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
当然,也有相关的函数操作pending表和block表:
-
sigpending函数,用来读取当前进程的pending表:

读取的结果会记录到参数set中,很简单
-
sigprocmask函数,可以读取或更改当前进程的block表:

第一个参数为更改的方式选项,第二个参数是我们传入的set,第三个参数会记录老的block表。
第一个参数有如下选项:
SIG_BLOCK:当设置为这个值时,表示新增信号屏蔽。set中的信号会添加到block表中SIG_UNBLOCK:当设置为这个值时,表示解除信号屏蔽。set中的信号会从block表中删除SIG_SETMASK:当设置为这个值时,block表会复制set中的全部信号状态
演示:
c
#include <bits/types/sigset_t.h>
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
printf("接收到了%d号信号\n", sig);
}
int main()
{
signal(2, handler);
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, 2);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
int cnt = 20;
while (cnt--)
{
sigset_t pending_set;
sigpending(&pending_set);
// 打印出pending表的内容
printf("pid:%d ", getpid());
printf("当前pending表: ");
for(int i = 31;i>=1;i--)
{
if(sigismember(&pending_set, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
if(cnt == 10)
{
// 这时我们解除2号信号的屏蔽
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
printf("2号信号已解除屏蔽\n");
}
sleep(1);
}
return 0;
}
