信号快速认识
生活角度的信号
• 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
• 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的为并不是⼀定要立即执行,可以理解成“在合适的时候去取”。
• 在收到通知,再到你拿到快递期间,是有⼀个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有⼀个快递已经来了。本质上是你“记住了有⼀个快递要去取”
• 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递⼀般方式有三种:
1.执行默认动作(幸福的打开快递,使用商品)
2.执行自定义动作(快递是零食,你要送给你的女朋友)
3.忽略快递(快递拿上来之后,扔掉床头,继续开⼀把游戏)
• 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话 ??
基本结论:
• 你怎么能识别信号呢?操作系统在设计的时候,进程就已经内置了对于信号的识别和处理方式
• 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?
知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
• 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?
合适的时候。
信号的产生
产生信号的方式
1.键盘产生信号的方式
使用ctrl+c的方式能给进程发送信号,能够终止进程的运行,有相当一部分信号的处理动作,就是让自己终止
而进程处理信号的方式一般有三种:1.默认处理动作,2.自定义处理动作,3.忽略信号
怎么证明进程收到信号并处理?
#include<iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
signal(2,handlerSig);
int cnt=0;
while(1)
{
cout<<"cnt:"<<cnt<<endl;
cnt++;
sleep(1);
}
return 0;
}
signal函数是 C/C++ 中用于处理信号(Signal)的系统调用,第一个参数传入第几个信号,第二个参数传入处理该信号的函数指针。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
使用该段代码可以发现我们使用的ctrl+c是发送的2号信号
要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
前后台进程
我们的进程分为前台进程和后台进程,当./xxx运行进程时,进程是前台进程,当./xxx &运行进程时,进程就会变成后台进程
当./xxx &,我们会发现后台进程收不到我们键盘上发出的信号,ctrl+c无法终止进程,这是由于只有前台进程才能接收来自键盘的信号,键盘产生的信号,只能发送给前台进程,这是由于键盘只有一个,输入的数据只能给一个确定的进程,所以前台进程只能有一个,前台进程就是一个确定的进程,而后台进程可以有多个。
前台进程的本质就是从键盘上获取数据。
当父进程退出,只剩子进程之后,子进程使用ctrl+c终止不掉,这是由于子进程变成了后台进程。
前后台移动
1.jobs能够查看当前的后台进程
2.fg + 任务号:能够将特定的后台进程提到前台
3.ctrl+z:暂停进程,将前台进程切换到后台进程,由于前台进程的特殊性不能被暂停,一旦停止就会变成后台进程
4.bg + 任务号:让暂停的后台进程恢复运行
查看信号
kill -l
通过kill -l可以查看信号列表
记录信号
信号产生之后,并不是需要立即处理的,而是可以记录下来,等待进程合适的时候进行处理,那么信号是记录在哪里?
task_struct中存在一个变量sigs,通过位图记录下32个信号,而sigs属于操作系统内的数据结构对象,而修改位图,本质上就是在修改内核的数据结构,修改内核的数据结构只有操作系统能进行修改,所以不管信号怎么产生,发送信号,在底层,必须让操作系统发送,所以操作系统就提供了kill的系统调用
2.系统调用产生信号
kill系统调用
pid:目标进程的 ID(PID),或特殊值(如-1表示所有进程)。
sig:要发送的信号编号(如SIGTERM=15,SIGKILL=9)。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
这就是一个简单的向进程发送信号的代码,使用时./xxx 信号编号 进程号,即可向对应的进程发送信号
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
using namespace std;
// ./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;
}
raise库函数
sig:信号编号(整数),常见信号如 SIGINT(2)、SIGTERM(15)、SIGKILL(9)
#include <signal.h>
int raise(int sig);
当cnt运行到3的时候,就会给自己给进程发送2号信号
#include<iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
//exit(13);
}
int main()
{
signal(2,handlerSig);
int cnt=0;
while(1)
{
cout<<"cnt:"<<cnt<<"这是我的pid:"<<getpid()<<endl;
cnt++;
if(cnt==3) raise(2);
sleep(1);
}
return 0;
}
3.硬件异常产生信号
类似/0,野指针的情况,在运行时就会产生报错
在/0的时候,进程会收到SIGFPE信号,在出现野指针的时候,进程会收到SIGSEGV信号
这些信号全是由操作系统发送,操作系统通过CPU,寄存器等等的设备发现错误,然后会去识别错误的类型,再去向进程发送对用的信号
像出现野指针时,CR3寄存器就会出现错误,操作系统就会去识别
4.软件条件产生信号
软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生
alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
• 调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程。
• 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某⼈要小睡⼀觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡⼀会儿,于是重新设定闹钟为15分钟之后响,“以 前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数 的返回值仍然是以前设定的闹钟时间还余下的秒数。
•第一个调用alarm(5),它的返回值是0。如果第一个调用alarm(5),经过3秒后,再次调用alarm(10),它的返回值是2
IO效率问题
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while(true)
{
std::cout << "count : " << count << std::endl;
count++;
}
return 0;
}
在一秒内只能够循环157337次
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
在缺少了io操作后,一秒的循环次数为580406578次
在有没有io操作的情况下,效率相差极大
alarm管理
在操作系统中,不止存在一个闹钟,所以会存在一个数据结构来管理闹钟,这个数据结构就是小堆,堆顶元素就是最小的闹钟。
操作系统会检测堆顶元素的超时时间,并与系统时间进行对比,判断是否超时,一旦超时,会让堆顶元素出去,并执行堆顶元素的函数指针
利用alarm模拟操作系统的软件产生信号
操作系统本质也是牛马,必须有人叫他做事情才会做
闹钟就是软件条件的一种,闹钟是软件,超时是条件,通过闹钟超时对进程发送信号
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using func_t = std::function<void()>;
std::vector<func_t> funcs;
int timestamp = 0;
// //func
void Sched()
{
std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{
std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
std::cout << "我是刷新程序,我在定期刷新内存数据,到磁盘" << std::endl;
}
// /
// // 每隔一秒,完成一些任务
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;
}
信号的保存
• 实际执行信号的处理动作称为信号递达(Delivery),而处理动作分为自定义,默认,忽略
• 信号从产生到递达之间的状态,称为信号未决(Pending),就是信号在位图中还未处理
• 进程可以选择阻塞(Block)某个信号。
• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的⼀种处理动 作。
信号在内核中的示意图
• 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动 作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上 图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
• block和pending都是位图,通过位图来判断信号是否存在,而handler实际上就是个函数指针数组,数组下标就是对应的信号,数组的内容就是处理该信号的动作,SIG_DFL就是默认处理动作,SIG_IGC就是忽略信号
• SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻 塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
• SIGQUIT信号未产生过,⼀旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数 sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1(当前的信号处理系统)允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计⼀次
sigset_t
从上图来看,每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储(就是位图),sigset_t被称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽” 应该理解为阻塞而不是忽略。
信号集操作函数
sigset_t 类型对于每种信号用一个 bit 表示 “有效” 或 “无效” 状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现,从使用者的角度是不必关心的。使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。
#include <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 置1,表示该信号集的有效信号包括系统支持的所有信号。
函数sigaddset将指定的信号 signum 添加到信号集 set 中。
函数sigdelset将指定的信号signum从信号集 set
中移除。
函数sigismember检查指定的信号 signum 是否存在于信号集 set 中。
注意,在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后,就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0,出错返回 -1。sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回 1,不包含则返回 0,出错返回 -1。
1.sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
set参数表示要操作的信号集,oset参数为输出型参数,存储调用前的信号屏蔽字
如果 oset 是非空指针,则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how 指示如何更改。如果 oset 和 set 都是非空指针,则先将原来的信号屏蔽字备份到 oset 里,然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。
2.sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
sigpending是位图,不能直接读取
测试
使用信号集操作函数修改信号的屏蔽区间,经过10秒后解除屏蔽,此时就可以收到2号信号
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
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);
sigset_t block,oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,SIGINT);
int n = sigprocmask(SIG_SETMASK,&block,&oblock);
int cnt=0;
while(1)
{
sigset_t pending;
int m=sigpending(&pending);
PrintPending(pending);
if(cnt==10)
{
std::cout << "解除对2号的屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
cnt++;
}
return 0;
}
core核心转储
core会在当前路径下形成一个文件,当进程异常退出的时候,进程在内存中的核心数据,会从内存中拷贝到磁盘,形成一个文件
但是云服务器上一般会禁用此功能,这是由于该功能在做项目时会花费大量的时间和空间,所以默认不会开启。
如何查看功能?
ulimit -a
如何开启功能?
ulimit -c unlimited
当我们开启core dump功能后,在gdb中使用core-file core,能够直接定位到错误位置
可以使用一个/0代码来进行验证
#include<iostream>
using namespace std;
int main()
{
int a=10;
a/=0;
return 0;
}
捕捉信号
信号捕捉的流程
当我们因为系统调用或某些方法进入内核态,执行完系统调用后准备返回用户态,此时操作系统会检查是否有信号需要执行,如果有,就会执行对应的方法,执行完毕之后,会返回到内核,再由内核返回到用户态
如果信号是忽略,就会将pending表对应位置置0,再返回用户态
如果信号是默认操作,会查找进程的PCB,根据信号将进程杀掉或者设置为停止状态
在执行用户自定义方法时,是以内核态执行还是以用户态执行?
是以用户态来执行方法的,如果用户的自定义方法存在非法操作的情况下,内核态运行会使非法操作实现
是怎么再次进入内核并执行函数sys_sigreturn()?
当main函数调用fun函数,会进行压栈,将返回地址放入栈中,当执行完func函数后,会释放func所对应的栈帧,然后返回地址(一般是返回func后面的代码),可以将其他函数的地址写入返回地址,当执行完返回地址后,就会执行该函数的调用
无论代码怎么样都会进入内核态吗?
int main()
{
while(1)
{}
return 0;
}
如果是上面那段代码,也会进入内核态吗?代码运行的时候在cpu中是以进程的方式进行管理的,只要是进程,都有时间片,一旦时间片耗尽,操作系统就会介入,进行进程调度,此时就进入了内核态
总结:进程一共会有4次机会进行内核态与用户台的切换
中断
硬件中断
操作系统是如何知道键盘上何时输入数据的?
这是通过硬件中断实现的,一般在电脑的主板上存在一个中断控制器,它的各个接口连接着各个外设,一旦外设做出操作,如:键盘的操作,鼠标的点击等等,它都会导致中断控制器向cpu发送一个高点平信号,让cpu直到有外设出现数据了
操作系统是如何知道是哪个外设发出的信息?
每个外设都有对应的中断号,可以将中断号简单理解成数组下标,而cpu上一般都会对中断号提前进行存储,一旦中断控制器向cpu发送高电平信号,同时也会将外设的中断号发送给cpu
所以之后操作系统不会再关注外设是否准备好,而是外设准备好会通知操作系统
操作系统是如何知道做出对应的回应?
操作系统中存在一张中断向量表,这张表是一个函数指针数组,下标就是中断号,里面存放了处理对应外设的方法
信号与中断
发中断---发信号 保存中断号---保存信号 中断号---信号编号 处理中断---处理信号
中断与信号存在相似之处
信号即是中断的软件模仿
当没有中断的时候,操作系统在做什么?
操作系统是基于中断而进行工作的软件,如果在没有硬件中断的情况下,操作系统什么都不做
for( ; ; )
pause();
操作系统的底层就是类似这样的操作
时钟中断
进程可以在操作系统的指挥下,被调度,被执行,那么操作系统被谁指挥,被谁推动执行呢?
电脑内部存在一个时钟源,时钟源会持续触发时钟中断(以固定频率),它的中断服务是进程调度,在进行进程调度的时候,如果有进程的时间片耗尽,他会进行管理,如果没有耗尽,就会return
软件中断
上面的两种中断都是硬件中断,还存在着软件中断
在大部分外设中都存在着寄存器,寄存器存放着各种数据,不只有cpu存在寄存器,当进程在执行自己的代码时,如果出现了/0错误,EFLAGS寄存器就会出现溢出情况,此时cpu就会发现不对劲,寄存器出现问题是一种在cpu内部触发的一种中断,此时就会进行处理异常的中断服务
像野指针,/0,指针重复释放,都是代码中出现的问题,在代码运行的时候,都会有相对应的寄存器记录代码的运行情况,一旦寄存器中的数据出现问题,就会触发中断,这种中断就是 软件中断
这就是为什么操作系统能够知道硬件出现问题的原因了
上面的情况是软件导致硬件出现问题而引发的中断,那软件自身能不能引起中断?
操作系统中存在两个汇编指令
x86下:int 0x80 x86_64:syscall
这两个汇编指令一旦写入cpu,就会引起cpu软中断,会跳转到对应的执行方法而他们的中断方法大致如下
{
//获取系统调用号
int n=0
move n eax
//获取系统调用方法
sys_call_table[n]
}
sys_call_table是系统调用表(函数指针数组),每一个系统调用,都有一个唯一的下标,这个下标叫做系统调用号
那么用户是如何将系统调用号给操作系统的呢?
在系统调用的实现中,比如open系统调用,会使用汇编语言将系统调用号存入寄存器中,然后触发软中断,进行对应的处理方法,系统调用最核心的片段只要做:将系统调用表中的下标移动到寄存器中,然后触发软中断
move eax 5;
int 0x80
所以操作系统不会提供任何的系统调用接口,只会提供系统调用号。
冯诺依曼体系结构
输入设备会与cpu相连,但不是以拷贝数据为目的,而是为了传递信息(硬件中断,也就是给cpu特定针脚触发高低电平,来告知cpu出现信息(一般是出现类似回车的情况)),然后会将中断号放在控制器的寄存器中,cpu得知有高电平信号,就会去寄存器中获取中断号找到相应设备,然后cpu会去中断向量表中查找对应的方法,进行处理中断的操作
用户态与内核态
进程的空间一共有4GB,[0,3GB]为用户区,[3,4GB]为内核区,用户和内核都在同一块地址上
在进程中不仅只有一张页表,一共有两张页表,一张用户页表,一张内核页表,那么内核页表与用户页表有什么区别?
用户页表在内存中有多份,每一个进程的代码和数据都各不相同,所以每个进程的用户页表都需要指向其他内容
而内核页表只有一份,内核页表常驻物理内存空间,他所指向的物理地址是固定的,不需要存在多份,所以所有进程共享同一份内核页表
所以用户态和内核态是什么意思?
用户态:以用户的身份,只能访问自己的[0,3GB]
内核态:以内核的身份,只能访问自己[3,4GB]
如果用户获取到内核区中的虚拟地址,是否可以在用户区访问该虚拟地址而访问内核的数据呢?
操作系统为了保护自己,必须采用系统调用的方式才能访问内核。
那么如何知道当前是用户态还是内核态?
cpu中存在一个cs寄存器,它的低2位代表了当前是处于什么状态,00是内核态,11是用户态,而剩下的比特位指向了当前状态下代码的地址
课外
volatile
它是C语言关键字之一,可以站在信号的角度上方便我们理解
Linux的编译器一共存在三种级别的编译器优化,分别是-O0,-O1,-O2;
在较高等级的编译器优化下,有些代码容易出现问题
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
这段代码在-O1级别的优化下,当我们发送2号信号,代码却不会终止,这是因为在高级别的优化下,编译器会将flag的值从物理内存中移动到寄存器中,当代码进行!flag操作时,操作系统会去寄存器中找flag,并不会去物理内存中找,而物理内存的值修改过后寄存器的值还是0,二者的数据不一致。
此时可以使用volatile来修饰flag,使其不会被优化到寄存器中
volatile int flag=0;
sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction的功能与signal相似,都是进行处理信号的函数,只不过sigaction的功能更加多样化
signum 要设置 / 查询的信号编号(如 SIGINT、SIGTERM 等)。例外:SIGKILL 和 SIGSTOP 无法修改(系统强制处理,不可捕捉 / 忽略)。
act 指向 struct sigaction 结构体的指针,描述新的信号处理配置。 若为 NULL:仅查询旧配置,不修改当前行为。
oldact 指向 struct sigaction 结构体的指针,用于保存旧的信号处理配置。- 若为 NULL:不保存旧配置。
其中传参的时候需要创建sigaction结构体,结构体的结构如下图所示
struct sigaction {
void (*sa_handler)(int); // 简单信号处理函数(二选一)
void (*sa_sigaction)(int, siginfo_t *, void *); // 带额外信息的处理函数(二选一)
sigset_t sa_mask; // 处理信号时,临时阻塞的信号集
int sa_flags; // 控制信号行为的标志(如 SA_RESTART、SA_SIGINFO 等)
void (*sa_restorer)(void); // 已废弃,无需使用
};
在创建sigaction结构体的时候,需要对sa_mask和sa_flags进行初始化操作,避免出现随机值的情况,前两个参数就是函数指针,我们只需要对sa_handler进行处理即可
• sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。 signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
• 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统 默认动作,赋值为⼀个函数指针表示用自定义函数捕捉信号,或者说向内核注册了⼀个信号处理函数,该函数返回值为void,可以带⼀个int参数,通过参数可以得知当前信号的编号,这样就可以用同⼀个函数处理多种信号。显然,这也是⼀个回调函数,不是被main函数调用,而是被系统所调用。
某个信号的处理当函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外⼀些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
SIGCHLD信号
wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第⼀种方式,父亲进程阻塞了就不能处理自己的工作了;采用第⼆种方式,父进程在处理自己的工作的同时还要记得时不时地轮询⼀下,程序实现复杂, 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外⼀种办法:父进程调用sigaction将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是⼀个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。