一.信号预备知识
1、生活中的信号
提起Linux中的信号,大家可能有些许陌生,甚至有些许恐惧,其实信号在我们日常生活中十分常见,就例如:
早晨起床时的闹钟,十字路口的红路灯,上课铃声,电话铃声,甚至有一些更为抽象的信号:看到某人的脸色似乎不太好......
我们可以发现有一个共同点,这些事会中断 我们当前正在做 的事情:这是一种事件的异步通知机制
对于进程也如此,信号是一种给进程发送的,用来进行事件异步通知的机制。
什么是异步通知?
就比如闹钟,它计时,我们继续睡觉,两者互不干扰,只有它像闹铃时才会通知我们,进程也是如此:信号的产生,相对于进程的运行是异步的。
2.结论
1.信号处理,进程在信号产生之前,就知道信号该如何处理了。就比如:我们还未听到闹钟响起,但我们心中知道闹钟响了我们就该去上班。
2.信号处理,不是立即 就必须处理的,可以在合适的时候进行信号处理。就比如:外卖员打电话叫我们取餐时,我们可以让外卖员把餐放在某个地点 ,到合适的时间我们再去取(注意这句话解释了后续信号的一些工作机制)。
3.进程可以识别信号,是因为信号已经被定义,OS程序员设计进程就已经内置了对于信号的识别和处理方式。
4.产生信号的信号源,也很多。就像生活中有各种各样的信号,它们来自不同的对象。
二.信号的产生
信号产生,跟信号源有关。
1、键盘产生信号
比如我们现在有一个正在运行的代码,键盘输入Ctrl+C会给目标进程发送信号,相当一部分信号的处理动作都是让自己终止。
信号有哪些?
kill -l
可以看到,信号的种类有许多。我们之后研究的信号主要集中于1-31号的普通信号,而34号之后的实时信号不纳入讨论范围。
bash
wujiahao@VM-12-14-ubuntu:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不难看出,这些信号实际上都是常量的宏定义。
进程收到信号后,到合适的时间会处理信号:
1.默认处理方式
2.自定义信号处理方式
3.忽略处理
Ctrl+C就是向目标信号发送2号信号。如何证明?我们需要看到信号的处理过程
我们可以改变进程的默认处理信号动作,使用系统调用signal:参数,信号的宏,函数指针
cpp
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数指针handler就是我们自定义的信号处理方式,我们具体的信号处理方式可以自己实现。注意,这个函数指针的返回值应该是int。
接着我们写代码进行验证:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
signal(SIGINT, handlerSig);
int cnt = 0;
while (1)
{
std::cout << "cnt: " << cnt++ << std::endl;
sleep(1);
}
}
效果如下:当我们按下Ctrl+c时,进程接收到键盘的信号,然后执行我们自定义的处理方法handlerSig。
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
cnt: 0
cnt: 1
cnt: 2
^C获得了一个信号: 2
cnt: 3
cnt: 4
^C获得了一个信号: 2
cnt: 5
cnt: 6
cnt: 7
我们也可以把自定义信号动作叫做自定义捕捉。
此时要终止进程,可以使用Ctrl+\。
我们可以使用指令查看具体的信号处理方式
man 7 signal
bash
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
SIGABRT P1990 Core Abort signal from abort(3)
SIGALRM P1990 Term Timer signal from alarm(2)
SIGBUS P2001 Core Bus error (bad memory access)
SIGCHLD P1990 Ign Child stopped or terminated
SIGCLD - Ign A synonym for SIGCHLD
SIGCONT P1990 Cont Continue if stopped
SIGEMT - Term Emulator trap
SIGFPE P1990 Core Floating-point exception
SIGHUP P1990 Term Hangup detected on controlling terminal
or death of controlling process
SIGILL P1990 Core Illegal Instruction
SIGINFO - A synonym for SIGPWR
SIGINT P1990 Term Interrupt from keyboard
SIGIO - Term I/O now possible (4.2BSD)
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGKILL P1990 Term Kill signal
......
现在我们抠一个之前的小小细节:什么是目标进程?它和前后台进程有什么关系?
./XXX ------前台进程
./YYY & ------后台进程
命令行shell进程------前台进程
如果把进程放到后台,后台进程无法识别 键盘信号。键盘产生的信号只能发给前台进程。
后台进程无法从标准输入中获取内容,前台进程能从键盘获取标准输入,但都可向标准输出上打印数据。
为什么?
因为键盘只有一个,输入数据一定是给一个确定进程的,而前台进程只能有一个,而后台可以有多个后台进程。键盘组合键也是键盘输入的信号。
前台进程的本质就是为了从键盘获取数据。
这也就能解释为什么前台数据在运行代码时不能执行其他指令如ls,因为前台进程只能有一个,bash进程会被自动放到后台,而后台可以做上述的操作。
以前台进程运行,无法执行其他指令
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
cnt: 0
cnt: 1
ls
cnt: 2
cnt: 3
ps
cnt: 4
cnt: 5
pwdcnt: 6
^C获得了一个信号: 2
cnt: 7
cnt: 8
^\Quit (core dumped)
而以后台进程运行,可以执行其他指令
bash
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig &
[1] 2101874
wujiahao@VM-12-14-ubuntu:~/TestSig$ cnt: 0
cnt: 1
cnt: 2
ls
Makefile testsig testSig.cc
wujiahao@VM-12-14-ubuntu:~/TestSig$ cnt: 3
cnt: 4
cnt: 5
pwd
/home/wujiahao/TestSig
同时我们发现,当一个代码以后台进程运行时,我们需要发送kill -9信号才能杀掉后台进程。
bash
wujiahao@VM-12-14-ubuntu:~/TestSig$ kill -9 2101874
[1]+ Killed ./testsig
我们可以用指令查看前后台信息
jobs
bash
wujiahao@VM-12-14-ubuntu:~/TestSig$ cnt: 3
cnt: 4
jobscnt: 5
[1]+ Running ./testsig &
但此时输出消息混在了一起,此时的显示器文件就是共享资源,这种现象也叫做做数据不一致问题
同样地道理:当我们调用fork()接口创建子进程时,若父进程先退出,子进程会自动提到后台成为僵尸进程,Ctrl+c无法杀掉这个进程。
如何将一个进程切换到前台或后台?
fg 1命令,切换到前台可以正常响应键盘信号
ctrl+Z切换到后台,bg 1将该进程继续运行
什么叫做给进程发送信号?
信号并不是要求产生之后就立即处理的,所以要把信号记录下来。
比如一个延时处理的信号,进程如何把要处理的信号记录下来?
task_struct中设置一个位图结构:

发送信号的本质,就是向目标进程写信号 ------修改位图,一个是进程pid,一个是信号编号
修改位图,本质是修改内核的数据(task_struct是内核的进程PCB数据结构),所以不管信号产生多少种,发送信号在底层都必须由操作系统发送。
因此操作系统必须提供发送信号的系统调用:kill接口。
信号 vs 通信
信号虽然不以传递数据为目的,但是可以通知某种事件。可以看作广义上的通信。
2、系统调用
1.kill函数:给任意进程发任意信号
cpp
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
我们来写一段代码来模拟向任意进程发送信号
tips:
我们对信号进行自定义捕捉时,为了防止某些进程恶意屏蔽中断信号,有部分信号是无法进行自定义捕捉的,例如kill -9.
cpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
// ./mykill signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cout << "./mykill signumber pid" << std::endl;
return 1;
}
int signum = std::stoi(argv[1]);
pid_t target = std::stoi(argv[2]);
int n = kill(target, signum);
if(n == 0)
{
std::cout << "send " << signum << " to " << target << " success.";
}
return 0;
}
此时我们运行上面的testSig,然后运行mykill发送信号。

2.raise函数:自己给自己发信号
cpp
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
我们可以写一段代码验证它的作用。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
for(int i=1;i<32;i++){
signal(i,handlerSig);
}
for(int i=1;i<32;i++){
sleep(1);
raise(i);
}
int cnt=0;
while(1){
std::cout<<"hello world:"<<cnt++<<"PID: "<<getpid()<<std::endl;
sleep(1);
}
}
可以看到,当捕捉到9号信号时,testsig就自动把自己的进程杀死了。
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
获得了一个信号: 1
获得了一个信号: 2
获得了一个信号: 3
获得了一个信号: 4
获得了一个信号: 5
获得了一个信号: 6
获得了一个信号: 7
^C获得了一个信号: 2
获得了一个信号: 8
Killed
3.abort:这里abort会捕捉六号信号并引起进程终止。
6号信号的特殊情况:虽然我们对所有信号进行捕捉并自定义了处理信号方法,但在abort调用时就把对六号信号的自定义捕捉恢复默认。
cpp
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
效果如下:
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
获得了一个信号: 6
Aborted (core dumped)
3、异常产生信号
当我们的程序出现问题时,例如除0错误,引用野指针等等都会引起程序崩溃。我们可以模拟一下除0错误和野指针错误。
1.除0错误
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
for(int i=1;i<32;i++){
signal(i,handlerSig);
}
int cnt=0;
while(1){
std::cout<<"hello world"<<cnt++<<" ,pid:"<<getpid()<<std::endl;
int a=10;
a/=0;
sleep(1);
}
}
我们可以看到报出一个八号信号,并终止了程序。可以看到这是一个浮点数错误。
bash
8) SIGFPE
问题:信号谁发送的?操作系统!
2.野指针错误
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handlerSig);
int cnt = 0;
while (true)
{
std::cout << "hello world, " << cnt++ << " ,pid: " << getpid() << std::endl;
sleep(1);
int *p = nullptr;
*p = 100; // 野指针
}
}
现象如下:
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
hello world, 0 ,pid: 2120073
获得了一个信号: 11
SIGSEGV P1990 Core Invalid memory reference
3.问题:操作系统怎么知道哪个进程犯错?
用除0错误举例:
操作系统是软硬件资源的管理者,它可以协同软硬件进行工作。CPU中有状态寄存器EFLAGS,当除0错误发生时,状态寄存器会溢出,发生硬件报错。
而对于野指针,我们对于一个为0的指针p进行寻址,页表是没有映射信息的,CPU的MMU寄存器转化虚拟地址和物理地址失败,发生硬件报错。

4、软件产生信号
例如在管道通信中,当关闭了读端,操作系统不做任何浪费时间空间的事,这种情况会直接把进程杀掉。
1.alarm接口
alarm
是一个系统调用,用于设置一个定时器,在指定的时间(秒)后,内核会向进程发送一个SIGALRM
信号。该信号的默认处理动作是终止当前进程。
参数
seconds
:指定定时器的时间,单位为秒。如果为0,则取消之前设置的定时器。
返回值
- 返回之前设置的定时器还剩余的时间(秒)。如果之前没有设置定时器,则返回0。
cpp
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
我们来举例查看alarm接口的作用。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
alarm(1);
//设定1s闹钟,1s后向进程发送SIGALRM信号,默认终止进程
//打印出cnt看看:
int cnt=0;
while(1){
std::cout << "count: " << cnt++ << std::endl;
}
}
结果发现,count居然只计数到四万多!
cpp
count: 41607
count: 41608Alarm clock
这是因为,对于操作系统来说,IO操作实际上是一个比较浪费时间的操作,一秒之内几乎大部分时间都浪费在了输出cnt值上。我们可以修改一下代码,执行一秒后,只进行一次输出cnt。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
long long cnt=0;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig <<"cnt:" << cnt<<std::endl;
exit(13);
}
int main()
{
//捕捉到SIGALRM信号后,进行响应处理
signal(SIGALRM, handlerSig);
alarm(1);
//设定1s闹钟,1s后向进程发送SIGALRM信号,默认终止进程
//打印出cnt看看:
while(1){
cnt++;
}
}
可以发现,此时得到的cnt比之前高许多数量级。
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
获得了一个信号: 14cnt:1553639757
2.pause函数:等待一个信号。
cpp
NAME
pause - wait for signal
SYNOPSIS
#include <unistd.h>
int pause(void);
我们写一个代码:让进程一直pause,然后设置一个闹钟,每隔一秒向进程发送一次信号,并唤醒进程,然后进程继续进入睡眠...
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
long long cnt=0;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig <<"pid:"<<getpid()<<std::endl;
//当收到SIGALRM,执行的方法就是再设置一个alarm.
alarm(1);
}
int main()
{
//捕捉到SIGALRM信号后,进行响应处理
signal(SIGALRM, handlerSig);
alarm(1);
//设定1s闹钟,1s后向进程发送SIGALRM信号,默认终止进程
//打印出cnt看看:
while(1){
pause();
}
return 0;
}
可以看到效果如下:其实,外部信号驱动任务执行,这十分类似操作系统的工作原理(下章详细介绍)。
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
获得了一个信号: 14pid:2476851
获得了一个信号: 14pid:2476851
获得了一个信号: 14pid:2476851
我们可以向一个vector中注册方法,然后根据上面的原理每隔一秒唤醒一次进程,让它执行vector中的方法。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
struct task_struct
{
pid_t id;
int count = 10; // 时间片,本质就是一个计数器!
void(*code)();
}t;
// //////////func////////////
void Sched()
{
std::cout << "我是进程调度" << std::endl;
t.count--;
if(t.count <= 0){}
// 切换其他进程
}
void MemManger()
{
std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
std::cout << "我是刷新程序,我在定期刷新内存数据,到磁盘" << std::endl;
}
/////////////////////////
using func_t = std::function<void()>;
std::vector<func_t> funcs;
int timestamp = 0;
// 每隔一秒,完成一些任务
void handlerSig(int sig)
{
timestamp++; //10000
std::cout << "##############################" << std::endl;
for(auto f : funcs)
f();
std::cout << "##############################" << std::endl;
int n = alarm(1);
}
int main() {
funcs.push_back(Sched);
funcs.push_back(MemManger);
funcs.push_back(Fflush);
signal(SIGALRM, handlerSig);
alarm(1); // 1S, 0.0000000001s
while(true) // 这就是操作系统!也是牛马!
{
pause();
}
return 0;
}
效果如下:
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
##############################
我是进程调度
我是周期性的内存管理,正在检查有没有内存问题
我是刷新程序,我在定期刷新内存数据,到磁盘
##############################
##############################
我是进程调度
我是周期性的内存管理,正在检查有没有内存问题
我是刷新程序,我在定期刷新内存数据,到磁盘
如何理解闹钟
闹钟本质就是一个计数器,时间片也是计数器。
cpp
struct task_struct
{
pid_t id;
int count = 10; // 时间片,本质就是一个计数器!
void(*code)();
}t;
// //////////func////////////
void Sched()
{
std::cout << "我是进程调度" << std::endl;
t.count--;
if(t.count <= 0){}
// 切换其他进程
}
操作系统中会同时存在许多闹钟,就需要对闹钟进行管理------先描述,再组织!
一个alarm,内部会设置过期时间和执行方法。假设当时系统时间戳为1000,并且设置了一个alarm(5),操作系统会只访问最小堆的堆顶------也就是最短超时的闹钟,如果当前时间戳小于堆顶,闹钟一个都不会响;如果大于等于,闹钟出队,执行alarm------像目标进程发送SIGALRM信号,最小堆自动调整。

三.信号的保存
1、信号保存的具体组织
因为进程在接收到信号时不一定会立即处理,所以需要对信号进行保存
• 实际执⾏信号的处理动作称为信号递达(Delivery)
• 信号从产⽣到递达之间的状态,称为信号未决 (Pending)。
• 进程可以选择阻塞 (Block )某个信号。
• 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
• 注意,阻塞和忽略 是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。
我们可以把信号看作老师,对信号的处理就相当于这个老师布置作业,我们如何处理。
信号未决:记作业的过程
信号递达:写作业的过程
我们可以把信号看作老师,如果我们讨厌它,就阻塞他。注意阻塞和忽略不同,忽略是抵达之后的一种可选处理动作
接下来我们来了解这些概念在内核中是如何落实的:虽然是三张表,但实际上是三十一组描述信号的关系

根据接收到的信号类型来执行不同的处理方法
对一个信号是否block与它是否pending没有直接关系。

细节:
• 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
• SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞
• SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数
sighandler。如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。
2、验证信号保存
1.信号的保存,一定是围绕上面的三张表展开。我们可以定义一个struct bits:

三张表的内核结构:
cpp
struct task_struct {
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked
struct sigpending pending;
...
}
1 2 3 4 5 6 7 8 9struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
struct __new_sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
struct k_sigaction {
struct __new_sigaction sa;
void __user *ka_restorer;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending {
struct list_head list;
sigset_t signal;
}
2.用于定义位图的结构sigset_t:信号集
从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, , 这个类型可以表⽰每个信号的"有效"或"⽆效"状态, 在阻塞信号集中"有效"和"⽆效"的含义是该信号是否被阻塞, ⽽在未决信号集中"有 效"和"⽆效"的含义是该信号是否处于未决状态。下⼀节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的 这⾥的"屏蔽"应该理解为阻塞⽽不是忽略。
sigset_t类型对于每种信号⽤⼀个bit表⽰"有效"或"⽆效"状态, ⾄于这个类型内部如何存储这些bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量, ⽽不应该对它的内部数据做任何解释, ⽐如⽤printf直接打印sigset_t变量是没有意义的。
3.信号集操作函数
cpp
块#i nclude <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在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask:谁调用这个函数,就更新自己的block表
cpp
SYNOPSIS
#include <signal.h>
/* Prototype for the legacy system call (deprecated) */
int sigprocmask(int how, const old_kernel_sigset_t *set,
old_kernel_sigset_t *oldset);
其中的set:最常用第三个

oldset:将旧表导出,如果想再次按照这个标准设置可直接使用
signalpending:用于获取pending信号集,也属于输出型参数
cpp
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
为什么没有提供修改pending的接口?因为上述的信号产生方法就会修改pending表。
3、整合接口
我们接下来写一个demo,屏蔽二号信号,获取pending表。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void PrintPending(sigset_t &pending)
{
printf("我是一个进程(%d), pending: ", getpid());
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int sig)
{
std::cout << "#######################" << std::endl;
std::cout << "递达" << sig << "信号!" << std::endl;
sigset_t pending;
int m = sigpending(&pending);
PrintPending(pending);
// 0000 0010(处理完,2号才回被设置为0),0000 0000(执行handler方法之前,2对应的pending已经被清理了)
std::cout << "#######################" << std::endl;
}
int main()
{
signal(SIGINT, handler);
// 1. 屏蔽2号信号
sigset_t block, oblock;
//对表示信号集的变量进行初始化0
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, SIGINT); // 已经对2号信号进行屏蔽了吗?没有!
int n = sigprocmask(SIG_SETMASK, &block, &oblock);
(void)n;
// 4. 重复获取打印过程
while (true)
{
// 2. 获取pending信号集合
sigset_t pending;
int m = sigpending(&pending);
// 3. 打印
PrintPending(pending);
}
return 0;
}
效果如下:当我们发送2号信号时,可以看到pending表中表示2号的bit位已经从0变1

九号信号,不可被捕捉,不可被阻塞
当我们取消对2号信号的屏蔽,pending对应比特位会变0,问题是:在递达前变0.还是在处理信号后变0?
验证:此时可以看到准备抵达的时候就会首先清空pending信号集中的对应信号位图,修改上面代码如下
cpp
sigaddset(&block, NULL); // 取消对2号信号的屏蔽
int n = sigprocmask(SIG_SETMASK, &block, NULL);
可以看到,此时位图已经将对应的bit位归零。

4.一些细节
1.Core和Term
终止方式有两种,一种是Term,一种是Core,它们有什么区别?
Core:会在当前路径下形成一个文件,进程异常退出时,进程在内存中的核心数据从内存拷贝到磁盘,形成一个文件------核心转储,最后进程退出。
Term:进程退出。
Core方式的动作主要目的是方便debug,因为核心转储的数据大多跟代码中的逻辑错误有关,如果实在找不到代码中的bug可以通过cgdb调试代码时输入命令:查看出现异常的代码位置。
core-file core
然而在云服务器上,core dump的功能是被禁止的。因为云服务器作为一个可以部署服务的生产环境,如果发生异常会在服务器内核中生成大量dump文件,我们只需要在开发和测试环境上开放这个功能即可。不过我们也可以自己临时打开这个功能:设置core file size。
ulimit -a
cpp
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7250
max locked memory (kbytes, -l) 247244
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7250
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
2.core dump
了解完上面的知识,我们就可以用一张老图来解释什么是core dump了。

正常终止时,status首8位标识退出码
异常终止时,status后7位为终止信号,第八位是core dump标志
进行验证:我们先手动写一个除0错误
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2);
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
int a = 10;
a /= 0;
printf("hello bit\n");
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
printf("signal: %d, exit code: %d, core dump: %d\n",
(status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}
结果如下:
cpp
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./testsig
hello bit
hello bit
hello bit
hello bit
hello bit
signal: 8, exit code: 0, core dump: 1
关于信号捕捉,我们放在下章跟一个更重要的话题一起讲解。