目录
1. 信号前期知识
1.1 生活中的信号
1.2 Linux中的信号
1.3 信号概念
1.4 信号处理方法的注册
2. 产生信号
2.1 通过终端按键产生信号
2.2 调用系统调用向进程发信号
2.3 软件条件产生信号
2.4 硬件异常产生信号
3. 核心转储Core Dump
4. 保存信号
4.1 信号在内核中的表示
4.2 信号集操作
4.2.1 信号集sigset_t:
4.2.2 信号集操作函数
4.3 代码使用实验
5. 所有测试代码
本篇完。
1. 信号前期知识
1.1 生活中的信号
从生活中入手,例如闹钟,红绿灯,狼烟,LOL游戏信号等等,这些都是信号。信号必须都是动态的,像路标就不能称之为信号。
以红绿灯为例,一看到红绿灯我们就知道红灯行,绿灯停,我们不仅能认识它是一个红绿灯,而且还知道应该产生什么样的行为,这样才算是能够识别红绿灯。识别 = 认识 + 行为产生。
对于红绿等这个信号,我们需要有如下几个共识:
- 我们之所以能识别红绿灯,是因为我们受到过教育(手段),让我们在大脑中记住了不同颜色对应的行为(属性)。
- 当绿灯亮了以后,不一定要立刻过马路,比如有其他的车闯红灯,需要进行避让,所以说我们不一定要立刻产生相应的行为。
- 红灯亮了以后,正好来了一个电话,在接电话这个期间我们会记住此时是红灯,不会将这个状态忘记。
- 红绿灯默认的行为是红灯行,绿灯停,但是也可以产生其他行为,还可以忽略。
现在将生活中红绿灯的例子迁移到进程中:信号是发给进程的。
进程之所以能够识别信号,是因为程序员将对应的信号种类和逻辑已经写好了的。
当信号发给进程后,进程不一定要立刻去处理,可能有更加紧急的任务,会在合适的时候去处理
进程收到信号到处理信号之前会有一个窗口期,这个期间要将收到的信号进行保存。
处理信号的方式有三种:默认动作,自定义动作,忽略。
再举个例子:
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1.执行默认动作(幸福的打开快递,使用商品)
2.执行自定义动作(快递是零食,你要开始吃了)
3.忽略快递(快递拿上来之后,扔到床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
我们学习信号是学习它的整个生命周期,分为产生信号,保存信号,处理信号。但是在这之前先需要学习一些预备知识。
1.2 Linux中的信号
用户输入命令, 在Shell下启动一个前台进程。
用户按下Ctrl C, 这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
前台进程因为收到信号,进而引起进程退出:
这里进程就是你,操作系统就是快递员,信号就是快递。
注意:
① Ctrl C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行, 这样Shell不必等待进程结束就可以接受新的命令, 启动新的进程。
② Shell可以同时运行一个前台进程和任意多个后台进程, 只有前台进程才能接到像 Ctrl - C 这种控制键产生的信号。
③ 前台进程在运行过程中用户随时可能按下 Ctrl - C 而产生一个信号, 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止, 所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
再看什么是Linux信号?
本质是一种通知机制,用户 or 操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。
结合进程得出的信号结论
① 进程要处理信号,必须具备信号“识别”的能力 (看到 + 处理动作作)
② 凭什么进程能够“识别”信号呢? 程序员
③ 信号产生是随机的,进程可能正在忙自己的事情,信号的后续处理,可能不是立即处理的④ 信号会临时的记录下对应的信号,方便后续进行处理
⑤ 在什么时候处理呢? 合适的时候
⑥ 一般而言,信号的产生相对于进程而言是异步的
1.3 信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。
用kill - l命令可以察看系统定义的信号列表:
白色区域的是普通信号,编号从1-31。
其它区域的是实时信号,编号从34-64。
这其中没有32号和33号信号,所以一共有62个信号。而且这里我们只学习普通信号,对实时信号暂不做研究。
在使用这些信号时,可以用信号名,也可以用信号编号,它是一样的,都是宏定义后的结果。
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,
例如其中有定义 #define SIGINT 2
这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明:
man 7 signal然后下滑:
根据我们对Linux的了解,信号存放在哪里呢?既然信号是给进程的,而进程是通过内核数据结构来管理的,所以我们可以推断出,信号放在进程的task_struct结构体中。
既然它是在PCB中,而且数量是31个,task_struct中必定不会设置31个变量来存放信号,数组还有可能,但是信号的状态只分为有和没有两种,所以再次推断,31个信号放在一个32位的整形变量中,每个比特位代表一个信号。写一段伪代码来示意一下:
struct task_struct
{
// 进程属性
unsigned int signal;
// .......
}
就像在学习基础IO和进程间通信的时候,那些flags标志中的不同的比特位代表着不同的意义,这31个信号量也是这种方式:
问题来了,内核数据结构的修改,这个工作是由谁来完成的?毫无疑问是操作系统,因为task_struct就是它维护的,而且是存在于内存中的,只有操作系统才有权力去修改它,用户是无法直接操作的,因为操作系统不相信任何人。
所以说,无论哪个信号,最后的本质都是由操作系统发生给进程的,这里的发送本质就是在修改task_struct中存放信号哪个变量的比特位。
信号发送的本质就是在修改PCB中的信号位图。
无论未来我们学习了多少中发送信号的方式,本质都是通过操作系统向目标进程发送信号。
所以操作系统一定会提供相关的系统调用,比如我们之前使用过的各自信号:
kill -9 pid值 //停止某个进程
kill -19 pid值 //暂停某个进程
它们的底层一定是在调用相关的系统调用,来让操作系统修改PCB中的信号位图。
信号处理常见方式:
① 忽略此信号。
② 执行该信号的默认处理动作。
③ 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。
1.4 信号处理方法的注册
所谓的注册,就是告诉操作系统,当某个进程接收到某个信号后的处理方式。
既然是告诉操作系统,那么肯定会用到系统调用,该系统调用的名字是signal,man 2 signal:
- int signal:要注册的信号编号
- sighandler_t handler:自定义的函数指针
可以将信号的处理方式写成一个函数,然后将函数名传递个signal,此时当进程接收到signum指定的信号编号时,就会执行我们定义的函数。
2. 产生信号
有了上面的知识以后,就可以正式来研究信号了,先来看看产生信号的几种方式。
2.1 通过终端按键产生信号
也就是在键盘上按一些热键,来给进程发送相应的信号,比如上面讲的ctrl c,它产生的是2号信号SIGINT,还有常用按键ctrl \,它产生的是3号信号SIGQUIT。怎么验证呢?
写个正经点的代码了:
Makefile
mykill:mykill.cc
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f mykill
mykill.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}
int main()
{
signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
signal(SIGQUIT, catchSig);
// signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作
// 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用
while(1)
{
cout << "I am a process,my pid: " << getpid()<< endl;
sleep(1);
}
return 0;
}
如图,得证,也演示了还可以用其它信号终止进程。
2.2 调用系统调用向进程发信号
和命令一样名字的系统调用kill(),man 2 kill:
- pid_t pid:要给发信号的pid
- int sig:要发送的信号编号
- 返回值:发送成功返回0,失败返回-1
该系统调用是一个进程给另一个进程发送指定信号,可以向任意进程发送任意信号。
mykill.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
// void catchSig(int signum)
// {
// cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
// }
static void Usage(string proc)
{
cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}
int main(int argc, char *argv[])
{
if(argc != 3) // ./mykill 9 pid
{
Usage(argv[0]);
exit(1);
}
int signumber = atoi(argv[1]);
int procid = atoi(argv[2]); // 获取两个命令行参数并转化
int ret = kill(procid, signumber);
if(ret != 0)
{
cerr << errno << ": " << strerror(errno) << endl;
}
return 0;
}
首先创建了一个睡眠进程,然后用自己写的mykill给其发了9号信号。
给自己发信号的系统调用raise(),man raise:
编译运行:
可以发现并没有执行raise后面的代码
给自己发送6号信号的系统调用abort(),man abort:
编译运行:
虽然有3个系统调用来产生信号,但是归根到底都是在使用kill系统调用。
- kill()可以给任意进程发送任意信号。
- raise()可以给自己发送任意信号。
- abort()可以给自己发送6号SIGABRT信号。
2.3 软件条件产生信号
验证一下管道当读端关闭的时候,写端所在进程就会收到编号为13的SIGPIPE信号结束进程:
pipe.cc
#include <iostream>
#include <cerrno> // C++包C语言头文件常用的方法,和.h效果一样
#include <cstring>
#include <cassert>
#include <unistd.h> // pipe + close + read + write
#include <sys/types.h> // waitpid两个头文件
#include <sys/wait.h>
#include <signal.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}
int main()
{
int pipefd[2];
int ret = pipe(pipefd); // 一.创建管道
if(ret < 0)
{
cerr << errno << ": " << strerror(errno) << endl;
}
// cout << "pipefd[0]: " << pipefd[0] << endl; // 3
// cout << "pipefd[1]: " << pipefd[1] << endl; // 4
pid_t id = fork(); // 二.创建子进程
assert(id != -1);
if (id == 0) // 子进程,读,关闭写
{
close(pipefd[1]);
// 三. 子进程读
while (true)
{
int cnt = 5;
char buffer[1024 * 8];
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // read读
cout << "我是子进程,我的pid: " << getpid()<< endl;
if(cnt--) // 5秒后关闭读端
{
close(pipefd[0]);
exit(0);
}
sleep(1);
}
}
signal(SIGPIPE, catchSig); // 特定信号的处理动作,一般只有一个
close(pipefd[0]); // 父进程,写,关闭读
// 四. 父进程写
char send_buffer[1024 * 8];
while (true)
{
cout << "我是父进程,我的pid: " << getpid()<< endl;
ssize_t s = write(pipefd[1], send_buffer, strlen(send_buffer));
sleep(1); // 父进程一直写,不关闭写端
}
return 0;
}
编译运行;
在读端关闭以后,写端的自定义处理方式中就接收到了系统发给的SIGPIPE信号,编号为13。
(管道,读端不光不读,而且还关闭了,写端一直在写,会发生什么问题?写没有意义,OS会自动终止对应的写端进程,通过发送13信号SIGPIPE的方式,这就是软件条件产生信号)
- 读端是否关闭是软件中的条件。
- 当条件达成以后,产生信号。
下面介绍一下alarm函数和SIGALRM信号
闹钟触发的信号:
闹钟就是系统中的定时器,使用的时候同样需要通过系统调用实现:
- 参数:要定的时长。
- 返回值:距离定的时间还差多少。
验证1s之内,一共会进行多少次count++:定时1秒钟,在循环中进行疯狂加1,设置自定义处理方式,打印定时到后收到的信号编号,并且统计这一秒中内进行了多少次加1操作。
编译运行:
有点延迟的打印了四万多次,这是包括IO的,如果单纯向计算算力呢?:
五亿多,这就发现我们计算机还是计数得很快的。
上面代码也写了闹钟自动触发就移除了,我们在任务里重设一个闹钟,就实现了定时器的功能:
编译运行:
成功实现,你也可以把int cnt改成无符号的。
如何理解软件条件给进程发送信号?:
OS先识别到某种软件条件触发或者不满足,然后构建信号,发送给指定进程。
2.4 硬件异常产生信号
除0操作导致的硬件异常:
编译运行:
- 在运行的时候,直接出错,没有再执行下去,是因为接收到了信号。
- 接收到的信号是SIGFPE信号,编号为8号。
这其实就是一种硬件异常产生的信号。
CPU中有很多的寄存器,例如eax,ebx,eip等等。CPU会从内存中将代码中的变量拿到寄存器中进行运算,如果有必要,还会将运算的结果放回到内存中。
还有一个状态寄存器,如果CPU在运算的时候发现了除0操作,就会将状态寄存器的溢出标志位置一。此时就意味着硬件产生了异常。而操作系统是一个进行软硬件资源管理的软件,CPU的中状态寄存器的溢出标志位置一后,操作系统可以第一时间拿到。除0导致硬件异常以后,操作系统会给对应的进程发送SIGFPE信号。
当进程接收到SIGFPE信号以后,默认的处理方式就是结束进程。
现在我们对这个SIGFPE信号注册一个自定义处理方式:
编译运行:
怎么这个信号被操作系统不停的发送给这个进程?
进程收到信号后进程不退出,随着CPU时间片的轮转就会再次被调到。
CPU中只有一份寄存器,但是寄存器中的内容属于当前进程的上下文。
当进程被切换的时候,就有无数次的状态寄存器被保存和恢复的过程。
而除0操作导致的溢出标志位置一的数据还会被恢复到CPU中。
所以每一次恢复的时候,操作系统就会识别到,并且给对应进程发送SIGFPE信号。
所以就会导致上面不停调用自定义处理函数,不停打印接收到的信号编号。
如何理解除0呢?:
① 进行计算的是CPU,这个硬件。
② CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位, 溢出标记位, OS会自动进行计算完毕之后的检测,如果溢出标记位是1, OS里面识别到有溢出问题,立即只要找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候,进行处理。
③ 一旦出现硬件异常,进程一定会退出吗?
不一定,一般默认是退出,但是我们即便不退出,我们也做不了什么
④ 为什么会死循环?寄存器中的异常一直没有被解决。
解引用空指针导致的硬件异常:
编译运行:
上面代码中存在对空指针的解引用操作,空指针的本质是(void*)0,而0地址处是不允许我们用户进行访问的,这部分属于内核空间。
- 运行的时候直接出错,没有再运行下去,也是因为接收到了信号。
- 接收到的信号是SIGSEGV,编号是11。
这同样是一种硬件异常产生的信号。
- 我们之前一直谈论的页表实际上是页表+MMU,而MMU是在CPU中的,为了简便,我们就只说页表。
- 进程地址空间和物理内存之间的映射关系实际上是有MMU去完成映射的。
- 当对空指针解引用的时候,MMU会拒绝这种操作,从而产生异常标志。
- 操作系统拿到MMU产生的异常以后就会给对应的进程发送SIGSEGV信号。
当进程接收到编号为11的SIGSEGV信号以后,默认的处理动作就是结束进程。
将这个信号注册自定义处理方式,同样打印接收到的信号编号,但是不结束进程,可以看到,和除0操作一样,不停的打印。
如何理解野指针或者越界问题?:
① 都必须通过地址,找到目标位置。
② 我们语言上面的地址,全部都是虚拟地址。
③ 将虚拟地址转成物理地址。
④ 页表+ MMU((Memory Manager Unit是硬件)
⑤ 野指针,越界, 使用非法地址,MMU转化的时候,一定会报错。
- 硬件异常所产生的信号,如果不结束这个进程,我们是没有能力去处理这个进程的。
- 随着时间片的轮转,这个导致硬件异常的进程还会不停的调到,所以操作系统会不停的向进程发送信号。
硬件异常产生的信号并不会显示发送,而是由操作系统自动发送的。
产生信号总结思考:
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者。
信号的处理是否是立即处理的?
在合适的时候才处理。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
是的,记录在PCB对应的信号位图当中。
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
能知道,因为这是程序员帮我们写好的。
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
OS去修改位图,根据信号编号修改特定比特位,由比特位由0置1。
如何理解系统调用接口?
系统调用接口执行OS对应的系统调用代码,然后OS提取参数,或者设置特定的数值,再然后OS向目标进程写信号,修改对应进程的信号标记位,进程后续会处理信号,最后执行对应的处理动作。
3. 核心转储Core Dump
以前在Linux_10_进程等待讲的:
学了上面信号的知识,是否有一个疑问,31个信号的默认处理方式都是结束进程,并且还可以自定义处理方式,那么为什么要这么多信号呢?一个信号不就行了吗?
- 重要的不是产生信号的结果,而是产生信号的原因。
- 所有出现异常的进程,必然是收到了某一个信号。
man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:
我们可以根据这个表找到不同信号产生所对应的不同原因。
以信号2和3为例,他们的默认处理方式一个是Term,一个是Core。
- Term和Core的结果都是结束进程。
那么这两个方式的区别在哪里呢?Term方式仅仅是结束进程,结束了以后就什么都不干了。但是Core不仅结束进程,而且还会保存一些信息。
比如刚才使用了野指针收到的11号信号的默认处理方式就是Core,退出了,但会保存一些信息。
在云服务器上,默认情况下是看不到Core退出的现象的,这是因为云服务器默认关闭了core file选项,ulimit -a:
看到第一行,core file size的大小是0,意味着这个选项是关闭的。
- 从这里还可以看到别的关于这个云服务器的信息,比如能够打开的最多文件个数,管道个数,以及栈的大小等等信息。
为了能够看到Core方式的明显现象,我们需要将core file选项打开,ulimit -c 1024:
此时该选项就打开了,表示的意思就是核心转储文件的大小是1024个数据块。
再运行使用野指针的程序,但是不捕捉信号了:
同样会收到11号信号停止。但是在当前目录下会多出一个文件,如下图。
- core.7607:被叫做核心转储文件,其中后缀7607是接收到该信号进程的pid值。
对于一个奔溃的程序,我们最关心的是它为什么崩溃,在哪里崩溃?
当进程出现异常的时候,将进程在对应的时刻,
在内存中的有效数据转储到磁盘中:核心转储。
核心转储的文件我们可以拿着它进行调试,快速定位到出现异常而崩溃的位置。
- 使用gdb调试我们的可执行程序。
- 调试开始后,输入core-file core.pid值,表明调试核心转储文件。
- 此时gdb就会直接定位到产生异常的位置。
这就是核心转储的重要意义,它相比Term方式,能够让我们快速定位出现异常的位置。
再看Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024
4. 保存信号
首先介绍几个新的概念:
- 信号递达(Delivery):实际执行信号的处理动作。
- 信号未决(Pending):信号从产生到递达之间的状态。
- 信号阻塞(Block):进程可以选择阻塞 (Block )某个信号,被阻塞的信号产生时将保持在未决状态,直达解除对该信号的阻塞,才执行递达动作。
注意: 阻塞和忽略是不同的,只要信号被阻塞就不会被递达,但是忽略是在递达之后进行的一种处理动作。
4.1 信号在内核中的表示
我们知道,信号是保存在内核数据结构中的,下面来看它具体的储存模型:
- pending表:用来存放接收到的信号,操作系统向进程发送信号时,都会修改pending表中对应编号处的比特位。
- block表:用来存放被阻塞的信号,当指定信号需要被阻塞时,操作系统会修改block表中对应编号处的比特位。
- handler表:这是是一个数组,用来存放不同信号的处理方法,保存的是函数指针。
当我们使用signal注册一个自定义处理方式时,
操作系统会将我们定义的函数指针放在handler表中,在信号递达后调用。
如果是默认处理方式,会调用handler默认的初始函数指针所对应的函数。
- 信号产生后,操作系统就会修改pending位图,使信号处于未决状态。
操作系统会按照一定的顺序来检查block表和pending表,然后去调用相应信号编号的处理方式来完成信号递达。大概逻辑(伪代码):
if(1<<(signo - 1) & pcb->block)
{
//signo信号被阻塞,不会被递达
}
else
{
if(1<<(signo - 1) & pcb->pending)
{
//信号递达,处理该信号
handler[signo - 1];
}
}
操作系统在对信号进行检测的时候,先检测的是信号的block位图,如果对应信号的比特位被置一,说明该信号被阻塞,就不再去检测pending位图。如果没有被阻塞,才会去检测pending位图,如果pending位图相应的位被置一,再去调用handler表中的处理函数。
所以如果一个信号没有产生,但是并不妨碍它被阻塞。被阻塞的信号,在产生之后就会一直处于未决状态,不会被递达,只有当阻塞被解除后才会被递达。
- 默认情况下,所有信号都是不被阻塞的,所有信号都没有产生,也就是block位图和pending位图都是0。
4.2 信号集操作
pending图,block图以及handler表是存放在内核数据结构中的,所以只能由操作系统来修改,我们用户如果要修改也能通过操作系统来实现,所以操作系统同样给我们提供了系统调用。
handler表中的函数指针可以通过系统调用signal来设置。
4.2.1 信号集sigset_t:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来表示,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
用户在设置pending位图和block位图的时候,并不能直接让系统调用将内核中对于的比特位置1或清0,而是需要预先在一个变量中表达出我们的意愿,然后将这个变量通过系统调用给到操作系统,再由操作系统去修改内核数据结构。
操作系统给我们提供了一个sigset_t的变量类型,用户只需要对这个变量进行预设置,然后再交给操作系统。
系统提供的信号集操作函数操作的也是也是这个域先处理的变量,之所以也用系统调用来处理这个变量,是因为这个变量不单单是一个32位的整形变量,它的结构和内核是对应的,所以操作也要按照相应的规则。从使用者的角度不必关心具体是如何操作的,只需要使用信号集操作函数来操作sigset_t变量即可。sigset_t变量用其他方式是无法操作的,比如用printf去打印,这是没有意义的。
4.2.2 信号集操作函数
对于block位图和pending位图的修改,操作系统提供了一族系统调用,称为信号集操作函数。
man sigemptyset:
- sigset_t set:信号集变量。
- int signum:信号编号。
- 返回值:成功返回0,失败返回-1。
sigemptyset:使所有信号对应的bit清零,表示该信号集不包含任何有效信号。
sigfillset:使所有信号对应的bit置位,表示该信号集的有效信号包括系统支持的所有信号。
sigaddset:使指定信号所对应的bit置位,表示该信号集中对应信号有效。
sigdetset:使指定信号所对应的bit清零,表示该信号集中对应信号无效。
sigismember:判断指定信号所对应的bit是否有效,返回类型是bool类型。
在使用sigset_t类型的变量之前,一定要调用sigemptyset进行初始化,使信号集处于确定状态。
此时我们已经对sigset_t变量预处理好了,下一步就是把这个变量交给操作系统了,操作系统同样提供了对应的系统调用。
sigprocmask()
该系统调用是专门用来修改内核数据结构中的block位图的。man sigprocmask:
- int how:修改方式,有三个选项:
- SIG_BLOCK:block在block原有位图基础上添加sigset_t变量中设置的比特位。
- SIG_UNBLICK:unblock在bolck原有位图解除上删除sigset_t变量中设置的比特位。
- SIG_SETMASK:setmask用sigset_t变量覆盖原有的block位图。一般使用这个。
- set:我们设置好的sigset_t变量。
- oldeset:这是一个输出型参数,将原本block位图输出到这个sigset_t变量中。
- 返回值:设置成功返回0,失败返回-1。
sigpending()
这是专门用来获取内核数据结构中的pending位图的。man sigpending:
- set:这是一个输出型参数,用来返回从内核中获取的pending位图情况。
- 返回值:成功返回0,失败返回-1。
4.3 代码使用实验
前面的:man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:
(注意到表格下面的一句话,SIGKILL and SIGSTOP不能被捕捉,阻塞,忽略,这里9号信号就是管理员信号,就是防止你把所有信号都设定自定义动作,导致进程不能退出的情况,可以自己做一个实验验证)
我们还可以利用上面的系统调用做一个小的实验,来验证某个信号被阻塞后,它的pengding位图会被置一,但是不会被递达。mykill.cc:
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号: " << signum << " Pid: " << getpid() << endl;
}
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig)) //在信号集里,输出1
cout << "1";
else
cout << "0";
}
cout << endl;
}
int main(int argc, char *argv[])
{
// 0. 方便测试,捕捉2号信号,不要退出
signal(2, catchSig);
// 1. 定义信号集对象
sigset_t bset, obset; // b是block,o是old
sigset_t pending;
// 2. 初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&bset, 2 /*SIGINT*/);
// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量
assert(n == 0); // sigprocmask成功了返回0
(void)n; // 强转一下,防止relese下出现变量未被使用的警告
cout << "block 2 号信号成功..., pid: " << getpid() << endl;
// 5. 重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中的没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 20) // 20秒后恢复2号信号的block,1->0
{
// 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
// 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步
cout << "开始解除对于2号信号的block" << endl;
int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复
assert(n == 0);
(void)n;
cout << "解除对于2号信号的block成功" << endl;
}
}
return 0;
}
5. 所有测试代码
这里放一些前面的所有测试代码,很多注释起来了:
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
// int cnt = 0;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
// cout << "final cnt: " << cnt << " 信号: " << signum << " Pid: " << getpid() << endl;
// alarm(1); // 重设闹钟 -> 定时器 -> 你可以实现任意任务
}
// static void Usage(string proc)
// {
// cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
// }
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig)) //在信号集里,输出1
cout << "1";
else
cout << "0";
}
cout << endl;
}
int main(int argc, char *argv[])
{
// 0. 方便测试,捕捉2号信号,不要退出
signal(2, catchSig);
// 1. 定义信号集对象
sigset_t bset, obset; // b是block,o是old
sigset_t pending;
// 2. 初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&bset, 2 /*SIGINT*/);
// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量
assert(n == 0); // sigprocmask成功了返回0
(void)n; // 强转一下,防止relese下出现变量未被使用的警告
cout << "block 2 号信号成功..., pid: " << getpid() << endl;
// 5. 重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中的没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 20) // 20秒后恢复2号信号的block,1->0
{
// 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
// 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步
cout << "开始解除对于2号信号的block" << endl;
int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复
assert(n == 0);
(void)n;
cout << "解除对于2号信号的block成功" << endl;
}
}
// cout << "my pid: " << getpid() << endl;
// for (int sig = 1; sig <= 31; sig++)
// {
// signal(sig, catchSig);
// }
// while (true)
// {
// sleep(1);
// }
// signal(SIGSEGV, catchSig);
// cout << "my pid: " << getpid() << endl;
// int *p = nullptr;
// *p = 100;
// while (true)
// {
// sleep(1);
// }
// signal(SIGFPE,catchSig);
// int cnt = 0;
// while(true)
// {
// cout << "正在运行的进程" << cnt++ << endl;
// int result = 7;
// result /= 0;
// sleep(1);
// }
// signal(SIGALRM,catchSig);
// alarm(1); // 先设定了一个闹钟,这个闹钟一旦触发,就自动移除了
// while(true)
// {
// ++cnt;
// }
// cout << "我开始运行咯" << endl;
// sleep(1);
// abort(); // 通常用来进行终止进程。等于raise(6) 等于kill(getpid(), 6)
// // raise(9); // 等于kill(getpid(), 8)
// cout << "运行结束咯" << endl;
// if(argc != 3) // ./mykill 9 pid
// {
// Usage(argv[0]);
// exit(1);
// }
// int signumber = atoi(argv[1]);
// int procid = atoi(argv[2]); // 获取两个命令行参数并转化
// int ret = kill(procid, signumber);
// if(ret != 0)
// {
// cerr << errno << ": " << strerror(errno) << endl;
// }
// signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
// signal(SIGQUIT, catchSig);
// // signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作
// // 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用
// while(1)
// {
// cout << "I am a process,my pid: " << getpid()<< endl;
// sleep(1);
// }
return 0;
}
本篇完。
下一篇继续进程信号的内容:处理信号部分和一些信号的笔试面试题,再就是多线程的内容了。
下一篇:零基础Linux_20(进程信号)内核态和用户态+处理信号+不可重入函数+volatile。