信号
前言
1. 引入
- 在生活中处处有信号的身影。eg:红绿灯,闹钟,外卖信息提示等…。即便现在没有信号产生,但是我们知道信号产生之后,我们应该怎么做。—— 我们知道信号产生后的对应动作
- 信号产生了,我们可能不立即处理这个信号对应的动作,例如外卖信息提示了,但是现在正在和父母打电话,很明显和父母打电话更重要,等打完电话再去拿外卖。—— 而这期间就有个时间窗口
所以:—— 进程(用户)
- 信号处理功能在进程中内置: 进程必须识别并且可以处理信号,哪怕没有产生信号,也有具备相关信号处理的能力
- 时间窗口: 进程可能不会第一时间收到信号就去处理,可能他要先处理更重要的事情。—— 所以进程就要有保存信号的能力
- 信号的处理方式:例如,绿灯亮了默认就是过马路,也有可能我们没有过马路的需求,所以就是忽略这个绿灯,还有就是旁边有个年迈的老奶奶,我们扶老奶奶过马路。
- 默认动作
- 忽略
- 自定义动作
2. 概念
是一种异步的通知机制,用来提醒进程一个事件已经发生,属于软中断(等下介绍硬件中断,类似)。
异步: 接收者不知道发送者什么时候发送信息,也不需要知道,只需要跑自己的流程,在后续的某个时间点产生交集
Linux中的信号:
- [1 - 31]:这个范围的信号为普通信号,需要掌握
- [34 - 64]:这个范围的信号为实时信号,了解即可。
实时信号常用的是车载系统之类的,这边都要撞车了,那边还要听歌肯定不行,所以要及时处理。而我们学的Linux是基于时间片轮转的操作系统,是相对公平的。
3. 初步认识ctrl+c信号
代码:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "I am a process, pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
./mykill
的方式执行代码,使用ctrl+c,能终止该进程。本质是被进程解释成收到了信号——2号信号(可以在上面找一下2) SIGINT
(interrupt —— 中断))。在讲信号产生时会进行验证./mykill &
的方式执行的代码不能被ctrl+c,原因是因为ctrl+c只能被前台进程接收,而./mykill &
的方式是以后台进程的方式启动该程序。所以实际上ctrl+c被发送给了bash进程- bash既然又是进程又是前台进程为什么不受到ctrl+c的影响呢,很明显bash内部进行了设置,可能就是该信号接收的处理方式改为自定义动作
- 在Linux中,一次登录,一个终端一般会配上一个bash,每一次登陆,只允许一个前台进程,但是可以允许多个后台进程。(只有前台进程能获取键盘输入)
注: ll
在显示屏上,错乱显示,为什么都可以执行?
在一切皆文件部分,我们也说了外设也被映射成为文件。
简化图解:
认识:
-
pidof proc | xargs kill -9
查看proc(指定进程)的PID,然后杀死该进程。- pidof proc:pidof命令可以用来查找指定进程的PID。proc是要查找的进程名。pidof会返回一个或多个PID。如果使用proc生成多个后台进程,可以返回多个PID
- xargs: 传递命令行参数的工具。在这个命令中,xargs会将上一个命令(pidof)的输出作为参数传递给后面的命令(kill)
- kill -9:杀死指定进程
示例:
-
ps -axj | grep proc | awk 'print $2' | xargs kill -9
找到包含"proc"关键字的所有进程,并强制杀死这些进程。- ps -axj | grep proc:过滤proc的进程信息
- awk ‘print $2’:使用awk命令获取第二列(即PID)的信息
-
awk
文本处理工具。可以逐行读取文本文件、提取数据、对数据进行处理和格式化输出。awk 'pattern { action }' file
pattern表示匹配条件,若省略则适用于所有行;action表示对匹配行执行的操作。常见的操作包括打印特定列、计算求和、使用条件语句等- 打印列:
awk '{ print $1, $3 }' file.txt
4. 硬件中断
键盘的数据时如何输入给内核的,ctrl+c又是如何变成信号的?
OS怎么知道键盘缓冲区有数据了,难道是一次次遍历?——肯定不是,毕竟硬件那么多。
图解:
硬件发送的信号是高低电平,即高电平代表1,低电平代表0。这种信号在中断控制器中会被解释成二进制信号,然后发送给CPU。
我们学习的信号,就是模拟的硬件中断,所以称为软中断
注: 中断的处理是串性的,不会多个硬件同时发生中断
一、信号的产生
无论信号如何产生,最终都是OS发送给进程,OS是进程的管理者
先认识一个系统调用:方便做测试
signal:设置一个函数处理对应的信号
头文件
#include <signal.h>
函数声明:
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:要捕获信号的编号,是SIGINT这类也可以
handler:表示信号处理函数的指针。参数类型:typedef void (*sighandler_t)(int)。可以设置成:
SIG_IGN:忽略该信号
SIG_DFL:使用系统默认处理方式
自定义:用户自定义信号处理函数
返回值:
1. 成功,返回一个函数指针,指向先前的signum对应的信号处理函数
2. 错误,返回SIG_ERR
后续实验
1. 键盘组合键
- 测试:Ctrl + c是2号信号——中断进程
使用signal
测试
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << " process get a signal:" << signo << endl;
}
int main()
{
//对2号信号捕获,自定义函数处理该信号
signal(SIGINT, handler);
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
-
Ctrl+\是3号信号——退出进程
这就不测试了,和测试2号信号同样的方式 -
Ctrl+Z是19号信号——暂停进程
测试一下是所有进程都可以被捕获吗?
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
//signo收到的信号
cout << " process get a signal:" << signo << endl;
}
int main()
{
//普通信号就31个
for(int i = 0; i <= 31; i++)
{
//那个信号被触发,都会执行我们自定义的处理方式
signal(i, handler);
}
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:(测试方法:使用kill命令
一次次的给我们测试的进程发送信号)9号和19号进程没有被捕获 其余的都被捕获了。
2. kill命令
方式:kill -signo [PID]
在上面测试信号能不能被全部捕获时,就是通过另一个终端使用kill命令,向测试进程发送信号。
示例:
3. 系统调用
①kill
向指定进程发送信号
头文件:
#include <sys/types.h>
#include <signal.h>
函数声明:
int kill(pid_t pid, int sig);
参数:
1. pid:要发送信号目标进程的PID
pid=0,表示给调用进程同一个组的所有进程发送信号
pid为负数,表示给某个进程组的所有进程发送信号
2. sig:表示要发送的信号编号,可以是宏
返回值:
1. 成功,返回0
2. 失败,返回-1,错误码被设置
- 测试代码1:使用kill命令终止另一个启动的进程
#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(errno);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if(n == -1)
{
perror("kill");
exit(errno);
}
return 0;
}
被终止的进程:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "I am a pro, pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:终止成功
- 测试代码2:使用kill命令终止自己
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
int main()
{
int cnt = 10;
while(cnt--)
{
cout << "I am a proc, pid:" << getpid() << endl;
sleep(1);
if(cnt == 5)
{
kill(getpid(), 2);
}
}
return 0;
}
运行结果:
②raise
向调用者进程发送信号
头文件:
#include <signal.h>
函数声明:
int raise(int sig);
参数:
sig:要发送的信号,可以是宏
返回值:
1. 成功,返回0
2. 失败,返回非0
测试代码:
代码描述:五秒之后给自己发送2号终止信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
int cnt = 10;
while(cnt--)
{
cout << "I am a proc, pid:" << getpid() << endl;
sleep(1);
if(cnt == 5)
{
raise(2);
}
}
return 0;
}
运行结果:
注:也可以使用signal捕获一下看看是不是收到了2号信号
③ abort
使进程异常终止。实际就是给调用进程发送6号信号
头文件:
#include <stdlib.h>
函数声明:
void abort(void);
无参无返回值
- 测试代码1:正常调用
代码描述:5秒后给自己发送6号信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int main()
{
int cnt = 10;
while(cnt--)
{
cout << "I am a proc, pid:" << getpid() << endl;
sleep(1);
if(cnt == 5)
{
abort();
}
}
return 0;
}
运行结果:
- 测试代码2:捕获6号信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
using namespace std;
void handler(int signo)
{
//signo收到的信号
cout << " process get a signal:" << signo << endl;
}
int main()
{
//两种方式都可以
// signal(6, handler);
signal(SIGABRT, handler);
int cnt = 5;
while(cnt--)
{
cout << "I am a proc, pid:" << getpid() << endl;
sleep(1);
if(cnt == 2)
{
abort();
}
}
return 0;
}
运行结果:
发现:6号信号被我们捕获后,我们在自定义处理方法中并没有终止进程,但是调用abort还是进行了终止
注:使用kill -6 [PID]
可以执行正常的自定义
4. 异常
①异常产生信号
1. 除0异常——SIGFPE
- 测试代码1:
#include <iostream>
using namespace std;
int main()
{
cout << "div zero before!" << endl;
int a = 10;
a /= 0;
cout << "div zero after!" << endl;
return 0;
}
运行结果:打印出浮点数异常就退出了
解释结果:除0异常,其实就是向进程发送8号信号——8) SIGFPE
- 测试代码2:看看除0异常是不是进程收到了8号信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
解释结果:所以浮点数异常确实是因为收到了8号信号
- 测试代码3:如果对8号信号捕获会发送什么?
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << " process get a signal:" << signo << endl;
sleep(1);
}
int main()
{
// 对8号捕获,自定义函数处理该信号
signal(SIGFPE, handler);
cout << "div zero before!" << endl;
int a = 10;
a /= 0;
cout << "div zero after!" << endl;
return 0;
}
运行结果:
结果:一直执行这个自定义的8号信号处理方法。(下面原理讲原因)
2. 野指针异常——SIGSEGV
- 测试代码1:
#include <iostream>
using namespace std;
int main()
{
cout << "wild pointers before!" << endl;
int* ptr = nullptr;
*ptr = 10;
cout << "wild pointers after!" << endl;
return 0;
}
运行结果:段错误
解释结果:野指针本质就是向进程发送了11号信号——11) SIGSEGV
- 测试代码2:直接对11号信号捕获
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << " process get a signal:" << signo << endl;
sleep(1);
}
int main()
{
signal(SIGSEGV, handler);
cout << "wild pointers before!" << endl;
int* ptr = nullptr;
*ptr = 10;
cout << "wild pointers after!" << endl;
return 0;
}
运行结果:
结果:一直执行这个自定义的11号信号处理方法。(下面原理讲原因)
②原理
- 除0错误的原理:
一般理解:除0错误会让进程崩溃 ——> 因为OS给进程发送信号了——>进程出问题了,OS检测到了,所以要给进程发信号
问题:OS怎么知道进程出现异常了
简化的图解:主要是CPU,其它弱化
- 野指针错误的原理:
简化的图解:
注:我们没有资格修改寄存器的值,所以不要妄想修改寄存器的值,从而让进程继续跑,没有意义(毕竟已经出错了)
5. 软件条件
- 上述讲的除0和野指针这类异常时基于硬件的,然后触发相应处理机制。
- 在之前学习管道时,有一种情况读端关闭,写端继续写,进程会收到13号信号——
SIGPIPE
。OS认为这浪费资源的操作,所以直接终止正在写入的进程,这就是基于软件的异常。本节内容主要介绍的是alarm接口和SIGALRM——基于软件条件产生的信号
alarm接口和SIGALRM信号:
认识alarm:
作用:用于设置一个定时器,在指定时间后发送SIGALRM
信号给当前进程,该信号的默认处理动作时终止当前进程
头文件:
#include <unistd.h>
函数声明:
unsigned int alarm(unsigned int seconds);
参数:
要设置闹钟的秒数
返回值:
1. 上一个闹钟剩下的秒数
2. 没有前一个闹钟返回0
- 测试代码1:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
//设置一个两秒的闹钟
alarm(2);
while(true)
{
cout << "I am a proc, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
- 测试代码2:
代码描述:在开始定义一个15秒的闹钟,然后我在代码开始运行5秒后,使用kill命令直接向该进程发送14号信号,该信号的处理方法被我们自定义了,所以会执行我们自定义的信号处理方法。执行完之后直接退出,此时获取了上一个设置闹钟时间还余下的秒数。
注:sleep影响闹钟的触发,所以自定义方法中的闹钟没有被触发
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << " process get a signal:" << signo << endl;
sleep(1);
int val = alarm(2);
cout << "return val:" << val << endl;
sleep(5);
exit(0);
}
int main()
{
signal(14, handler);
alarm(15);
while(true)
{
cout << "I am a proc, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:和预期结果相符
注:alarm——先描述再组织
- 先描述:系统中不会就一个alarm闹钟,所以必然会被结构体描述起来,等alarm时间到达之后,再发送信号
- 再组织:将alarm对象用优先级队列维护,堆顶为最近闹钟触发时间,所以OS只需要判断堆顶元素即可
6. 小结
Core Dump:
在讲到进程控制的进程等待时status输出型参数,低16位中有一个core dump——核心转储标志位。
图解core dump:
测试core dump,代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// child
while (true)
{
cout << "I am a child process, pid:" << getpid() << endl;
sleep(1);
}
exit(0);
}
// father
int status = 0;
pid_t rid = waitpid(pid, &status, 0);
if(rid == pid)
{
cout << "wait child proc success!, rid:" << rid << endl;
cout << "exit code:" << ((status>>8)&0xFF) << " exit signal: "
<< (status&0x7F) << endl;
cout << "core dump: " << ((status>>7)&1) << endl;
}
return 0;
}
运行结果+分析:
core文件分析:
- 打开系统的core dump功能,一旦进程出现异常,OS会讲进程在内存中的运行信息,给我dump(转储)到进程的当前目录(磁盘),形成core.pid文件——核心转储(core dump)
- 运行时错误,可以直接定位到出错行,先运行然后
core-file
调试。
示例:
终端信号和内核信号:
在Linux系统中,信号可以分为两类:终端信号(Terminal Signal)和内核信号(Kernel Signal)
- 终端信号: 由进程之间交互或控制终端发出的信号,由用户或其他进程直接发送给目标进程。终端信号通常用来通知目标进程某种事件的发生
- 内核信号:由内核直接向进程发送的信号,表示某种系统事件的发生或错误的发生。由内核管理和发送的,用于处理系统级事件或错误情况。
二、信号的保存
1. 引入
信号保存的相关问题:
- 信号为什么要保存?
因为进程可能不会第一时间收到信号就去处理,可能他要先处理更重要的事情—— 所以进程就要有保存信号的能力- 怎么保存?
普通信号使用位图保存,比特位的内容是两态的,所以可以决定是否收到相对应的信号- OS在这里扮演什么作用?(信号的发送)
OS对进程发送信号,本质就是OS修改task_struct属性中信号位图对应的比特位。因为OS是进程的管理者,只有OS才有资格修改进程PCB的属性信息
为介绍下面内容,引入一些信号的常见概念:
- 信号递达(Delivery):实际执行信号的处理动作
- 信号未决(Pending):信号从产生到递达中间的状态
- 阻塞信号:进程可以选择指定信号阻塞,就是让该信号不能递达
被阻塞的信号会保持在未决状态,直到进程解除阻塞,才会执行递达。注:有些内容在信号处理中讲,这里主要是介绍三张表
在上面我们也说了,信号肯定是保存在位图中,实际上也确实这样(哈哈哈哈哈),但是(突如其来的转折——要注意了)内核里有三张相关的表。block、pending和handler,前两张都是位图,handler是函数指针数组。
画图了画图了,图里介绍。
2. 原理
图解:
文字描述:
- 对于每个信号=block和pending表都有一个标志位+handler表中的一个函数指针(表示处理动作)。
- 信号产生时,如果没有阻塞,内核会设置该信号的未决标志,直到该信号递达才清除(执行处理信号函数前,对应的未决标志就会被置0了,同时对应的阻塞信号也被置1,信号处理时再实验),递达时如果判断handler表中是0就执行默认动作,是1就执行忽略动作,此外是自定义动作
- 如果指定的信号被阻塞,暂时不能被递达,就算对应的处理动作是忽略,但是没有解除阻塞前不能忽略该信号,因为进程在这个过程中有机会改变处理动作然后解除阻塞。
- handler表中有一个SIG_IGN(默认处理动作是忽略),block表是阻塞,前者是处理动作,后者是对应阻塞的信号不能递达。
- 某个信号被阻塞了,也可以向该进程发送信号,也会pending只不过不会递达。
注:
问题:进程解除对某信号的阻塞之前,该信号产生多次的处理方法?
答:普通信号在递达前产生多次只记一次,实时信号在递达前产生多次可以一次放在队列中维护,后续依次执行
3. 接口
①信号集——sigset_t
sigset_t也称为信号集,该类型是给用户提供的。包含在头文件
#include <signal.h>
中
提供该类型的原因:
block和pending两张表都是用相同的数据类型sigset_t来存储,虽然也可以使用long这种类型,但是考虑到扩展性等方面,提供了sigset_t类型,本质就是位图,每个位代表一个信息,有效和无效。
注:阻塞信号集也叫做当前进程的信号屏蔽字(signal mask)
信号集操作函数:
与该类型一起出现的还有相关的位图操作的接口,用来操作sigset_t类型的变量
头文件:
#include <signal.h>
相关操作函数声明:
int sigemptyset(sigset_t *set); //用于清空信号集合
int sigfillset(sigset_t *set); //用于填满信号集合
int sigaddset(sigset_t *set, int signum); //向信号集合中添加指定的信号signum
int sigdelset(sigset_t *set, int signum); //向信号集合中删除指定的信号signum
int sigismember(const sigset_t *set, int signum); //判断signum信号是否在信号集合中
参数:
1. set:被操作的sigset_t类型对象
2. signum:向set对象中添加、删除几号信号,或者判断相应的信号是否在set指向的信号集中
返回值:
1. 前四个函数成功返回0,出错返回-1
2. sigismember函数的返回值,判断一个信号集中的有效信号是否包含指定的信号,包含返回1、不包含返回0,出错返回-1
注:
使用sigset_t类型的变量之前,要先调用sigemptyset或sigfillset做初始化,使信号集和处于确定的状态。
②sigprocmask
sigprocmask:用于修改当前进程信号屏蔽字(signal mask)的系统调用。可以在程序中动态地控制进程对信号的阻塞和解除阻塞
头文件:
#include <signal.h>
函数声明:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
1. how:指定信号屏蔽字的操作方式,取值有以下几种:
SIG_BLOCK:将set中的信号添加到进程的信号屏蔽字中,这些信号将被阻塞。相当于mask=mask|set
SIG_UNBLOCK:从进程的信号屏蔽字中移除set中的信号,这些信号不再被阻塞。相当于mask=mask&~set
SIG_SETMASK:将进程的信号屏蔽字设置为set中的值。相当于mask=set
2. set(输入型参数):表示要设置的信号集合。
3. oldset(输出型参数):用于存储之前的信号屏蔽字。如果不为NULL,则会把之前的信号屏蔽字存放在oldset中。
返回值:
成功返回0,出错返回-1,错误码被设置
③sigpending
sigpending:读取当前进程未决信号集。可以用来查询有哪些信号被阻塞而尚未处理,以便进一步处理。
头文件:
#include <signal.h>
函数声明:
int sigpending(sigset_t *set);
参数:
set:输出型参数,把pending表带出来
返回值:
成功返回0,出错返回-1,错误码被设置
④使用接口
- 测试代码1:
代码描述:对2号信号捕获,阻塞2号信号,然后10秒后解除阻塞,在这十秒钟向进程发送2号信号。结果可以发现2号信号处于未决状态
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t &pending)
{
//发送2号信号之后,我们想要看到的结果是000000...00000000010
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "catch a signo: " << signo << endl;
}
int main()
{
// 1. 对2号信号做自定义捕获
signal(2, handler);
// 2. 先对2号信号进行屏蔽
sigset_t set, oset;
sigemptyset(&set); // 初始化操作
sigemptyset(&oset);
sigaddset(&set, 2); // 对2号信号屏蔽
// 3. 系统调用,把set的数据设置到内核
sigprocmask(SIG_SETMASK, &set, &oset); // 屏蔽了2号信号
// 4. 打印当前进程pending
sigset_t pending;
int cnt = 0;
while (true)
{
// 获取pending表
int n = sigpending(&pending);
if (n < 0)
continue;
PrintPending(pending);
sleep(1);
cnt++;
if (cnt == 10)
{
cout << "unblock 2 signo" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
return 0;
}
运行结果:
- 测试代码2:
代码描述:试试把所有的信号都屏蔽
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintBlock(const sigset_t &block)
{
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&block, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
sigset_t set, oset;
sigfillset(&set); // 初始化操作
sigemptyset(&oset);
cout << "set before:";
PrintBlock(set);
// 设置进task_struct
sigprocmask(SIG_SETMASK, &set, nullptr);
// 看一看有哪些信号被屏蔽了
sigprocmask(0, nullptr, &oset);
cout << "set after:";
PrintBlock(oset);
return 0;
}
运行结果:
ok了牢底,9和19号信号不能被阻塞,之前也说过9和19号信号不能被捕获
三、信号的处理
信号递达:实际执行信号的处理动作。
在文章的开头也说过,信号的处理方式有三种:
- 默认动作:即系统对相应的信号默认的处理动作
- 忽略:处理动作就是忽略该信号
- 自定义动作:即用户自己实现的信号处理动作
1. 信号什么时候处理和怎么处理
在谈这个之前,就要再谈进程地址空间了
再分析信号是什么时候处理的和怎么处理的?
答:当我们的进程从内核态返回用户态的时候,进行信号检测和处理
- 用户态:允许访问用户自己的代码和数据
- 内核态:允许访问OS的代码和数据
图解:
注:
- 从③返回到用户态执行自定义信号处理方法,其实内核态也可以执行,但是群众里有坏人,使用内核执行内核的代码,用户执行用户的代码。
- sighandler和main函数使用不同的堆栈空间
- 所有的系统调用也是在一张表中,是函数指针数组,调用系统调用本质是把其对应的系统调用号(数组下标)写到寄存器中
- 用户层看是sigreturn,在内核看sys_sigreturn
2. 接口——sigaction
捕捉信号:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数。
捕捉信号在上面我们已经学习并使用了一个接口signal,在上面测试时,我们也发现9和19号信号是不能被捕捉的。
学习另一个捕捉方法:sigaction——用于设置信号处理方法
头文件:
#include <signal.h>
函数声明:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
1. signum:要设置信号处理函数的信号
2. act(输入型参数):一个结构体指针。包含了信号的处理方法和标志位
3. oldact(输出型参数):传出该信号原来的处理动作
struct sigaction
{
void (*sa_handler)(int); //向内核注册信号处理函数
sigset_t sa_mask;
};
sa_mask:当调用信号处理函数时,除了当前信号被自动屏蔽之外,屏蔽别的信号用sa_mask,信号处理函数返回时自动恢复原来的屏蔽字
返回值:
成功返回0,失败返回-1,错误码被设置
sigaction使用:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
void PrintPending()
{
sigset_t set;
sigpending(&set); //把pending位图带出来
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&set, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "catch a signal, signal num:" << signo << endl;
while(true)
{
PrintPending();
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
//对结构体初始化
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
//为信号设置处理方法
act.sa_handler = handler; //自定义
// act.sa_handler = SIG_IGN; //忽略
// act.sa_handler = SIG_DFL; //默认
//阻塞指定信号
sigemptyset(&act.sa_mask); //初始化工作
sigaddset(&act.sa_mask, 3); //阻塞3号信号
sigaddset(&act.sa_mask, 4); //阻塞4号信号
sigaddset(&act.sa_mask, 5); //阻塞5号信号
//捕获2号信号
sigaction(2, &act, &oact);
while(true)
{
cout << "I am a process! pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:
结论:
- pending位图由1置0的时间是:执行信号捕捉方法前,先清0,再调用
- 信号被处理时,对应的信号会被自动添加到block表中,防止嵌套调用
eg:正在处理自定义信号处理函数时,完全有可能在自定义函数中再次陷入内核,这时再收到一样的信号,就嵌套了。- 也证明了前面所说,普通信号在递达前产生多次只记一次
四、拓展
1. 可重入函数
1. 引入问题
不同执行流访问一个链表的插入函数:
图解:
信号可以理解为假执行流,可以影响到可重入函数问题:
- main函数调用insert插入节点,插入操作分为两步,刚执行完第一步,因为硬件中断,切换到内核,完成相应的处理之后,返回用户态
- 返回用户态要检测信号有没有可以递达的,有执行信号处理函数
- 信号处理函数也调用了同一个链表,执行了插入节点操作
- 执行完信号处理函数,返回从主控流程上次被中断的地方继续执行
- 执行插入操作第二步
结果:只有一个节点插入到链表,另一个节点丢失
2. 概念
上述问题中,insert函数被main和handler(假执行流)执行流重复进入,导致节点丢失,内存泄漏。
不可重入函数:如果一个函数被重复进入,导致可能出错。eg:insert
反之就是可重入函数
3. 局部变量
上述的insert函数访问的是一个全局的链表,有可能因为重入造成错乱,如果一个函数只访问自己的局部变量或参数就不会错乱。原因是:每个控制流程调用函数时都会创建一个新的栈帧,它们实际上操作的是各自栈帧中的不同副本,不会相互影响,从而避免了数据错乱。
2. volatile
volatile:保持内存的可见性。告诉编译器该关键字修饰的变量可以在程序的其他部分改变,不应该对其进行优化处理。每次访问“volatile”变量时,都应该从内存读取数据,而不是从寄存器中获取。
验证代码:
代码描述:在main函数中写个死循环,当收到2号信号时程序结束
#include <iostream>
#include <signal.h>
using namespace std;
int flag = 0;
void handler(int signo)
{
cout << "catch a signal, signal num:" << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
cout << "process quit normal" << endl;
return 0;
}
运行结果:
-
不进行优化编译:
g++ -o $@ $^ -std=c++11
符合我们的预期结果 -
进行优化编译:
g++ -o $@ $^ -O3 -std=c++11
进程不终止了
解释优化编译的结果:
代码:在代码中定义的flag变量,并被设置为循环条件。
优化:flag符合下面两个条件,所以被编译器优化到CPU寄存器中。
- flag在代码中没有被更改(如果信号不被触发,那将会一直不被修改)
- !flag是一种计算(逻辑计算)。(计算中有两种计算,一种算术计算,一种逻辑计算)
解决办法:
volatile int flag = 0; //在定义变量flag时,使用volatile关键字修饰
运行结果:
3. SIGCHLD——17
子进程在退出的时候,不是直接就退出了,而是会主动向父进程发送17号信号——SIGCHLD
测试代码1: 看看是不是子进程退出会给父进程发送17号信号
代码描述:父进程死循环,子进程执行五秒自动退出,对17号信号捕捉
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>
using namespace std;
void handler(int signo)
{
cout << "catch a signal, signum:" << signo << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork();
if(id == 0)
{
//child
while(true)
{
cout << "I am child proc, pid: "<< getpid() << " ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
while(true)
{
cout << "I am father proc, pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:符合预期,结果也是17号信号
所以,我们也就可以在信号处理方法哪里进行等待。
测试代码2: 多个子进程,分批退出,或者一起退出的情况
问题1:同时退出可能会出现多个信号同时发送给父进程,只执行一次
解决方式:采用循环的形式,直到这一批子进程全部退出
问题2:一部分一部分的退出
解决方式:非阻塞轮询的方式
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
using namespace std;
void handler(int signo)
{
cout << "catch a signal, signum:" << signo << endl;
int id = -1;
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) //非阻塞轮询的方式
{
cout << "I am proc, pid: " << getpid() << " "
<< "wait success! pid: " << id << endl;
}
}
int main()
{
signal(17, handler);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
while (true)
{
cout << "I am child proc, pid: " << getpid() << " ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
//创建的慢一点
sleep(1);
}
while (true)
{
cout << "I am father proc, pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
测试结果:所有子进程无论一起退出还是分批退出,都被正常等待成功。
测试代码3: 父进程必须要等待吗?不是
int main()
{
signal(17, SIG_IGN);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
while (true)
{
cout << "I am child proc, pid: " << getpid() << " ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
//创建的慢一点
//sleep(1);
}
while (true)
{
cout << "I am father proc, pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
结论:讲SIGCHLD信号设置为SIG_IGN(忽略),这样fork出来的子进程,在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程
注:
signal(17, SIG_DFL);
更改这一句代码的运行结果:信号默认处理动作还是会产生僵尸进程
系统默认的做法:
系统默认的忽略动作和用户用sigaction函数自定义的忽略一般没有区别,SIGCHLD这个信号属于特例。