一、信号的基本概念
1.1 信号生活中的例子:
- 信号在生活中,随时可以产生,信号的产生和我的认知是异步的
- 我能认识这个信号
- 我们知道信号产生了,信号该怎么处理,我可以识别并处理这个信号
- 我们可能证字啊左着更重要的事情,把到来的信号暂不处理。我要记得这件事情,在合适的时候进行处理
1.2 信号概念的基本存储
- 信号:Linux系统提供的一种向指定进程发送特定事件的方式:做识别和处理
- 信号的产生是异步的
1.3 我们可以看一看信号是什么?
信号分为两类:普通信号和实时信号。实时信号是不需要学习的,我们只需要学习普通信号。
每一个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define STCINT 2
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal,一般信号的处理动作为:终止进程;暂停;忽略
二、信号处理常见方式
- 忽略此动作
- 执行该信号的默认处理动作
- 自定义处理------提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
2.1 如何理解信号的发送和保存?(浅度理解)
信号的发送和保存类似于进程,进程有自己的PCB结构体,结构体中有自己的成员变量,成员变量中包含有一个数据结构------位图。我们可以使用位图来保存我们有几个信号。发送信号就是修改指定进程PCB中的信号的指定位图,将0置为1。
注意:谁可以修改进程PCB?只有操作系统可以进行修改。
如何理解信号的发送和保存??
信号的发送是进程,进程有对应的task_struct,在里面有成员函数,用位图来保存所需要收到的信号。
2.2 来介绍一下signal函数
函数的原型:
函数的功能:
设置某一信号的对应动作
函数的参数:
- signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
- handler:描述了与信号关联的动作,它可以取以下三种值:
cppSIG_IGN 这个符号表示忽略该信号。 SIG_DFL 这个符号表示恢复对信号的系统默认处理。不写此处理函数默认也是执行系统默认操作。 sighandler_t类型的函数指针 此函数必须在signal()被调用前申明,handler中为这个函数的名字。 当接收到一个类型为sig的信号时,就执行handler 所指定的函数。 (int)signum是传递给它的唯一参数。执行了signal()调用后, 进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分, 就立即执行func()函数。当func()函数执行结束后,控制权返回进程被中断的那一点继续执行。
函数的返回值:
返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
举个例子:
我们想要将kill -2 的指令进行捕捉。对于有的信号,其自定义捕捉只需要执行一次,我们只需要捕捉一次,后续一直有效。
cpp
void hander(int sig)
{
std::cout << "get a sig:" << sig << std::endl;
}
int main()
{
// 1. 一直不产生??
// 2. 可不可以对更多的信号进行捕捉呢??
// 3. 2号信号是终止进程
// 4. 2号信号是 ctrl + c ctrl + \ ---- 终止进程
signal(2, hander);
while (true)
{
std::cout << "hello world, pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
三、信号产生的五个方式(OS发送)
3.1 通过kill命令
我们可以直接通过kill命令向指定的进程发送信号。
通过上面的代码,我们可以看见当我们输入kill指令后,我们的signal函数会被触发进而执行hadler函数。
3.2 键盘产生信号
键盘可以产生信号,例如:ctrl + c(SIGINT) 或者是 ctrl + \(SIGQUIT) ,ctrl + z(SIGTSTP)。同理得:
3.3 系统调用函数
3.3.1 kill函数
函数的原型:
函数的功能:
kill
函数是一个操作系统提供的系统调用函数,它用于向目标进程发送一个信号,从而终止该进程的执行。除了终止进程外,kill
函数还可以用来向进程发送其他类型的信号,例如暂停进程、恢复进程等。注意事项和常见问题:
在使用kill函数时,有一些注意事项需要我们牢记:
- 权限要求:只能终止当前用户具备终止权限的进程。若需要终止其他用户的进程,需要以超级用户身份运行程序。
- 错误处理:在调用kill函数时,我们应该检查其返回值,以便及时发现并处理错误。
- 信号处理:被终止的进程可以选择捕获和处理信号。一些信号可以被忽略或阻塞,这取决于进程的信号处理方式。
函数的参数:
- pid:指定了目标进程的进程ID(PID)
cpp大于 0 的整数:表示发送信号给具有该 PID 的单个进程。 等于 0:表示发送信号给与调用进程属于同一进程组的所有进程。 也就是调用kill函数的这个进程组的进程都会接受到这个信号。 等于 -1:表示发送信号给除了调用进程和 init 进程(PID 为 1)以外的所有进程。 通常情况下,这需要调用进程拥有特定权限,例如 root 用户权限。 小于 -1 的整数:表示发送信号给进程组 ID 等于 pid 绝对值的所有进程。 换句话说,如果 pid 是 -N(N > 1),则信号将发送给进程组 ID 为 N 的所有进程。
- sig:指定了要发送的信号
cppSIGTERM(15): 默认信号,告诉进程终止运行。 SIGKILL(9): 强制终止进程,不可被捕获或忽略。 SIGSTOP(17): 暂停进程的执行。 SIGCONT(19): 恢复进程的执行。
cpp
// 实现一个mykill命令
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "User:" << argv[0] << "signum pid" << std::endl;
return 1;
}
//
size_t pid = std::stoi(argv[2]);
int sig = std::stoi(argv[1]);
kill(pid, sig);
}
3.3.2 raise函数
函数的原型:
函数的功能:
raise 是一个C语言标准库函数,它的作用是给当前进程发送信号。它属于信号处理库(signal.h),允许程序员通过代码控制信号的发送和处理。
函数的参数:
- sig:要发送的信号。Linux 支持多种信号,sig 参数可以是整数信号代码,也可以是预定义的信号常量。
函数的返回值:
- 如果函数成功发送信号,返回0。
- 如果出现错误,返回非0值。
3.3.3 abort函数
函数的原型:
函数的功能:
立即终止当前进程,产生异常程序终止
进程终止时不会销毁任何对象
当函数进行调用的时候,会直接将程序终止。
这个函数只能等待一次,之后就又会将程序进行终止。
说明一下: abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,因此使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的。
3.4 由软件条件产生信号
3.4.1 SIGPIPE信号
SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止。
例如,下面代码当中,创建匿名管道进行父子进程之间的通信,其中父进程是读端进程,子进程是写端进程,但是一开始通信父进程就将读端关闭了,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
3.4.2 SIGALRM信号
3.4.2.1 alarm函数
函数的原型:
函数的功能:
让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。
函数的参数:seconds:秒数,想要在几秒后激发alarm函数
如果是0,则取消闹钟
函数的返回值:
- 若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。
- 如果调用alarm函数前,进程没有设置闹钟,则返回值为0。
3.4.2.2 验证IO
我们可以用下面的代码,测试自己的云服务器一秒时间内可以将一个变量累加到多大。
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <algorithm>
#include <string>
#include <cstring>
#include <cstdlib>
#include <signal.h>
int cnt = 0;
void hander(int sig)
{
std::cout << "get a sig:" << sig << std::endl;
//std::cout << cnt << std::endl;
// exit(1);
}
int main()
{
//int cnt = 0;
signal(SIGALRM, hander);
alarm(1);
while (true)
{
//sleep(1);
//std::cout << "cnt: " << cnt << std::endl;
cnt++;
}
std::cout << cnt << std::endl;
return 0;
}
但实际上我当前的云服务器在一秒内可以执行的累加次数远大于两万,那为什么上述代码运行结果比实际结果要小呢?
主要原因有两个,首先,由于我们每进行一次累加就进行了一次打印操作,而与外设之间的IO操作所需的时间要比累加操作的时间更长,其次,由于我当前使用的是云服务器,因此在累加操作后还需要将累加结果通过网络传输将服务器上的数据发送过来,因此最终显示的结果要比实际一秒内可累加的次数小得多。
为了尽可能避免上述问题,我们可以先让count变量一直执行累加操作,直到一秒后进程收到SIGALRM信号后再打印累加后的数据。
此时可以看到,count变量在一秒内被累加的次数变成了五亿多,由此也证明了,与计算机单纯的计算相比较,计算机与外设进行IO时的速度是非常慢的。
3.5 由硬件异常产生信号
3.5.1 程序为什么会崩溃?
我们在程序中写出了一些的异常操作:除0、野指针......程序会进行退出,程序会进行退出,默认是终止进程,终止进程的本质是:释放硬件上下文的数据,包括溢出标志数据或者其他异常数据。程序也是可以不退出的,但是没有意义。程序收到了操作系统发送的异常访问的信号:野指针发送的是SIGSEGV,浮点数错误是SIGFPE。
3.5.2 异常捕捉的机制
如果我们把信号都进行捕捉,我们会发现还是有一些信号是不允许捕捉的,比如:9号信号。
3.5.3 如何理解上面的信号发送??
3.5.3.1 除0操作
我们的CPU会进行两种运算:算术运算和逻辑运算。在运算的时候,我们会发现在CPU中有一些寄存器,我们需要使用寄存器来进行存储运算的结果,在CPU中,也会有一个叫做状态寄存器。在状态寄存器中有一个溢出标志位,当我们进行运算时,出现了溢出现象,溢出标志位被标志。操作系统收到后,会向异常程序发送信号,这样我们就会进行程序的终止。
状态寄存器又名条件码寄存器(SR,Status register),它是计算机系统的核心部件------运算器的一部分,状态寄存器用来存放两类信息:一类是体现当前指令执行结果的各种状态信息(条件码),如有无进位(CF位)、有无溢出(OF位)、结果正负(SF位)、结果是否为零(ZF位)、奇偶标志位(P位)等;另一类是存放控制信息(PSW:程序状态字寄存器),如允许中断(IF位)、跟踪标志(TF位)等。有些机器中将PSW称为标志寄存器FR(Flag Register)。
3.5.3.2 野指针操作
在CPU中,我们会将虚拟地址空间中的地址和CPU中的MMU寄存器的地址进行结合,形成内存的地址去内存中访问数据,如果在内存中,这个新形成的地址找不到,我们需要将这个地址送到页故障线性地址寄存器中,然后操作系统将会给程序发送信号,使程序进行终止。
3.6 core和term
3.6.1 core和term的区别和共同点
共同点:
- core和term都是终止进程
区别:
- core:异常终止,会帮我们形成一个Debug文件,方便调试,core协助我们进行调试代码
- ulimit -a:来查看权限是否关闭,
- term:异常终止
3.6.2 历史遗留问题
core dump字段,在程序异常退出,会形成core文件,我们可以通过指令进行修改,但是修改次数是有一定的限制。如果出现错误,进程退出的时候的镜像数据,核心转储,会将数据存储到内存空间中。
如果需要使用需要通过ulimit进行设置,可以通过ulimit -c查看当前系统是否支持coredump。如果为0,则表示coredump被关闭。通过ulimit -c unlimited可以打开coredump。coredump文件默认存储位置与可执行文件在同一目录下,文件名为core。可以通过/proc/sys/kernel/core_pattern进行设置。
3.6.3 云服务器为什么要关闭核心转储?
因为如果不关闭,一直产生这种文件,会将磁盘中的空间打满。
3.7 如何理解核心转储
四、信号的阻塞
4.1 信号的一些常见概念
- 实际执行信号的处理动作称为信号递达(Deliver)
- 信号从产生到递达之间的状态称为信号未决
- 进程可以选择阻塞某个信号
- 被阻塞的信号产生时将保持在未决的状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
4.2 信号在内核中的表示
每一个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,知道信号递达才清除该标志。在上图的例子中,SIGHUP信号位阻塞,也未产生过,当它递达时,执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但是在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT此你好将被阻塞,她的处理动作用户自定义函数sighandler。如果在进程解除对
4.3 sigset_t
在上图中,每一个信号只有一个bit的未决标志:非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞信号可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每一个信号的有效或者无效状态,在阻塞信号集中,有效和无效的含义是该信号是否被阻塞,而在未决信号集中,有效和无效的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(SIgnal Mask),这里的屏蔽应该理解为阻塞而不是忽略。
4.4 信号集操作函数
sigset_t类型对于每一种信号用一个bit表示有效或无效状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者角度是不需要关心的,使用者只能调用下面的函数来操作sigset_t变量。而不是对它的内部数据做出解释,比如用printf直接打印sigset_t变量是没有意义的。
4.4.1 sigemptyset
函数的原型:
cppint sigemptyset(sigset_t* set);
函数的功能:
初始化set所指向的信号集,使其中所有信号对应bit清零,表示该信号集不包含任何有效信号
函数的参数:
- set:指向sigset_t类型的指针,sigset_t是一个信号集类型,用于表示一个信号集。我们会把用这个指针指向的信号集清空,也就是不阻塞任何信号。
函数的返回值:
- 成功时,sigemptyset()函数返回0;
- 失败时,返回-1,并设置errno表示错误原因。
4.4.2 sigfillset
函数的原型:
cppint sigfillset(sigset_t* set);
函数的功能:
初始化set所指向的信号集,使其中的所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号
函数的参数:
- set:指向sigset_t类型的指针,sigset_t是一个信号集类型,用于表示一个信号集。我们会把用这个指针指向的信号集清空,也就是不阻塞任何信号。
函数的返回值:
- 成功时,sigemptyset()函数返回0;
- 失败时,返回-1,并设置errno表示错误原因。
4.4.3 sigaddset
函数的原型:
cppint addset(sigset_t* set, int signo);
函数的功能:
该函数允许你将一个指定的信号添加到一个自定义的信号集中,也就是将该信号的标准位设为1,表示阻塞这个信号。当您需要创建或修改信号集,以便在信号处理、信号屏蔽等操作中使用时,可以使用此函数。
函数的参数:
- set:指向sigset_t类型的指针,sigset_t是一个信号集类型,表示一个信号集。
- signum:需要添加到信号集中的信号编号。
函数的返回值:
- 成功时,sigaddset()函数返回0;
- 失败时,返回-1,并设置errno表示错误原因。
4.4.4 sigdelset
函数的原型:
cppint sigdelset(sigset_t* set, int signo);
函数的功能:
该函数允许你从一个自定义信号集中删除一个指定的信号,也就是将该信号的标准位设为0,不阻塞这个信号。当您需要调整信号集,以便在信号处理、信号屏蔽等操作中使用时,可以使用此函数。
函数的参数:
- set:指向sigset_t类型的指针,sigset_t是一个信号集类型,表示一个信号集。
- signum:需要从信号集中删除的信号编号。
函数的返回值:
- 成功时,sigdelset()函数返回0;
- 失败时,返回-1,并设置errno表示错误原因。
4.4.5 sigismember
函数的原型:
cppint sigismember(const sigset_t* set, int signo);
函数的功能:
用于检查一个指定的信号是否在给定的信号集中,也就是检查该信号是否被阻塞。当您需要确定信号集中是否包含某个特定信号,以便进行相应的信号处理、信号屏蔽等操作时,可以使用此函数。
函数的参数:
- set:指向sigset_t类型的指针,sigset_t是一个信号集类型,表示一个信号集。
- signum:要检查的信号编号。
函数的返回值:
- 如果指定的信号在信号集中,sigismember()函数返回1;如果指定的信号不在信号集中,返回0;
- 失败时,返回-1,并设置errno表示错误原因。
4.4.6 sigprocmask
函数的原型:
函数的功能:
一个进程的信号屏蔽字规定了当前阻塞而给该进程的信号集 。调用函数sigprocmask可以检测或更改其信号屏蔽字 ,或者在一个步骤中同时执行这两个操作。
函数的参数:
- how:提供sigprocmask更改当前信号屏蔽字的方法
下面来介绍一下how可选的值注意,不能阻塞SIGKILL和SIGSTOP信号。
|-------------|------------------|
| 信号 | 含义 |
| SIG_BLOCK | 加入信号到进程屏蔽。 |
| SIG_UNBLOCK | 从进程屏蔽里将信号删除。 |
| SIG_SETMASK | 将set的值设定为新的进程屏蔽。 |
- set:为指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL。
- oldset:也是指向信号集的指针,在此存放原来的信号集。
注意:
如果oset 是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。函数的返回值:
- 成功执行时,返回0。
- 失败返回-1,errno被设为EINVAL。
4.4.7 sigpending
函数的原型:
函数的功能:
sigpending函数返回被阻塞而为调用进程待定的信号
函数的参数:
- set:为指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL。
函数的返回值:
- 成功执行时,返回0。
- 失败返回-1
4.4.8 案例
cpp
#include <iostream>
#include <algorithm>
#include <signal.h>
void signalprint(sigset_t &pending)
{
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending, i))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
int main()
{
// 首先,要将这个2号信号堵塞
// 然后再发送这个2号信号
sigset_t set, oldset;
sigemptyset(&set); // 先将这个信号集清空
sigemptyset(&oldset);
sigaddset(&set, 2); // 再将2号信号填充到信号集中
// 1.1 设置进入进程的Block
int n = sigprocmask(SIG_BLOCK, &set, &oldset);
int cnt = 5;
while (true)
{
// 获取当前进程的penging指令
sigset_t pending;
sigpending(&pending);
signalprint(pending);
cnt--;
if (cnt == 0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &oldset, &set);
}
sleep(1);
}
return 0;
}
五、信号的捕捉
5.1 信号捕捉的流程
5.2 内核时如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂。
用户程序注册了SIGQUIT 信号的处理函数 sighandler 。 当前正在执行main函数 , 这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复 main 函数的上下文继续执行 , 而是执行 sighandler 函数 ,sighandler和main 函数使用不同的堆栈空间 , 它们之间不存在调用和被调用的关系 , 是 两个独立的控制流程。 sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达 , 这次再返回用户态就是恢复main函数的上下文继续执行了。
5.3 内核态与用户态
以32位机器为例,在我们的地址空间中, 0~3GB的地址空间是我们的用户空间,3~4GB的地址空间是我们的内核空间。因此,我们一个进程中会有两张页表:用户级页表(管理用户空间与内存的映射),内核级页表(管理内核空间与内存的映射)。上图是以两个进程为例。
我们可以在3~4GB的空间中找到OS的所有代码和数据,在一个程序中有多个进程,每一个进程的内核空间和内核级页表是一样的。因此,无论进程如何进行切换,我们都可以找到OS。我们在访问OS,其实还是在进程的地址空间中和访问库函数是没有区别。
但是,OS不相信任何人,要求进程访问3~4GB地址空间中要受到一定的约束,只能进行系统调用。
5.4 键盘输入数据的过程
在这个过程中,我们会发现我们学习的信号就是模拟中断实现的,信号是:纯硬件,中断是:软件 + 硬件。
5.5 sigaction
5.5.1 知识点补充
5.5.1.1 操作系统是如何正常运行的
操作系统在开机到关机的这一段空间中,操作系统是一直在进行运转,因此操作系统的本质是一个死循环 + 时钟中断,不断进行调度系统的任务(在操作系统原码中:for(; ;) pause();)。
5.5.1.2 如何理解系统调用
系统调用函数指针列表中,用于系统调用中断处理程序作为跳转表。
操作系统不相信任何用户,用户无法直接跳转到3~4GB的内核空间,必须在特定的条件下,才能跳转过去。我们需要使用硬件进行配合。
5.5.2 sigaction函数
函数的原型:
cppstruct sigaction { void (*sa_handler)(int); // void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; // int sa_flags; // void (*sa_restorer)(void); }
函数的功能:
该函数可以读取和修改指定信号相关联的处理动作。
函数的参数:
- signum:指出要捕获的信号类型
- act:(输入型参数)指定新的信号处理方式
- oldset:(输出型参数)输出先前信号的处理方式(如果不为NULL的话)。
函数的返回值:
- 0 表示成功
- -1 表示有错误发生
函数中的结构体类型:
- sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
- sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
- sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
- SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
- SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
- SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
5.5.3 举例说明
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
// 捕捉后的自定义函数
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
exit(1);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(2, &act, &oact);
while (true)
{
std::cout << "I am process, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
六、可重入函数
main函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步 , 刚做完第一步的 时候 , 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 , 于是切换 到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的 两步都做完之后从sighandler返回内核态 , 再次回到用户态就从 main 函数调用的 insert 函数中继续 往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后 向链表中插入两个节点 , 而最后只有一个节点真正插入链表中了。
像上例这样,insert 函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数, 这称为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 ,如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。想一下 , 为什么两个不同的控制流程调用同一个函数, 访问它的同一个局部变量或参数就不会造成错乱 ?
如果一个函数符合以下条件之一则是不可重入的 :
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。