标题:[Linux]信号(signal)详解(三):信号总结、信号在实操编码中如何应用
(图片来源于文心一言)
目录
正文开始:
一、信号相关常见库函数的使用
在前两次对信号的讲解中,我们已知信号的重要作用以及意义,信号的本质等。接下来我会列举一些常用的库函数,并讲解它们的作用以及意义。
(1)sigset_t类型
这个类型本质就是一个位图,这个位图的每一bit位表示一个状态(非0即1),由于这个位图是库函数维护的,所以不允许用户直接修改这个位图的内容,想要修改,需要用到对应的库函数。
头文件:<signal.h>
//表示初始化set所指向的信号集,使其中所有信号的对应bit清零,表示这个信号集不包含任何有效信号 int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对应bit置1,表示 该信号集的有效信号包括系统支持的所有信号 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);
注意:
i,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
ii,初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
iii,sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
(2)signal函数与sigaction函数
signal()函数原型:
参数:
signum:用户规定的信号标号;
handler:函数指针,返回值void ,参数为int;
作用:
捕捉用户规定的signum信号,并在捕捉到对应的信号的时候,跳转执行handler函数。
sigaction函数原型:
参数:
signum:用户规定的信号标号;
第二个、第三个参数是结构体类型,这个结构体类型包含有与信号相关的各种信息,定义这个结构体的意义就是为了方便维护信号管理。
结构体原型:
act:结构体类型,使用之前需要设置各种参数,具体参数设置见实例;
oldact:与act相同的结构体类型,传递这个参数的意义就是保存设置act之前的参数设置,如果想要恢复oldact,可以直接使用oldact。
作用:
捕捉用户规定的signum信号,并在捕捉到对应的信号的时候,跳转执行结构体内部的handler函数。(这里的handler函数被设置在结构体内部)
综合两个函数的应用实例:
#include <signal.h>
#include <unistd.h>
#include <iostream>
using std::cout;
using std::endl;
bool loop = true;
void Print(sigset_t &pending)
{
for (int sig = 31; sig > 0; sig--) // 没有0号信号,信号的范围1——31,两闭
{
if (sigismember(&pending, sig))
{
cout << 1;
}
else
{
cout << 0;
}
}
cout << endl;
}
/*一旦检测到3号信号,会走这里的处理逻辑,此时吧loop置false,使得对2号信号的处理逻辑结束。*/
void donesig2(int sig)
{
cout << "get sig 3" << endl;
cout << "loop = false, done sig2" << endl;
loop = false;
}
void sigcb(int sig)
{
loop = true;
cout << "get a sig:" << sig << endl;
while (loop)
{
sigset_t pending;
sigpending(&pending);//函数接口:获取目前的pending位图
Print(pending);//打印出pending位图,便于观察
sleep(1);
signal(3, donesig2);//检测3号信号——在处理信号的同时依然可以接受并处理信号
}
}
int main()
{
struct sigaction ac, oac;//一个结构体类型,内部存储有维护信号系统的一系列变量
ac.sa_flags = 0;//暂时设为0
/*sa_mask是一个sigset_t类型的位图(sigset_t是一个专门用于维护31个信号位的类型)
此处这个函数的作用是把这个位图的所有位置全置0(初始化)
但是这个位图目前还没有被设置进操作系统的信号位图*/
sigemptyset(&ac.sa_mask);
/*sa_handler是结构体内部的一个成员,是一个函数指针类型,需要用户自定义实现,
也就是当进程接受到特定信号之后需要做的处理动作*/
ac.sa_handler = sigcb;
while (true)
{
//这是一个和上述的结构体类型同名称的一个函数
//参数:(需要屏蔽的信号,需要设置结构体类型,老的结构体类型,目的是为了保存设置之前的数据,防止用户想要撤回操作)
sigaction(2, &ac, &oac);//对2号信号进行特殊处理
sleep(1);
cout << "I am process:" << getpid() << endl;
}
return 0;
}
二、常见信号补充
1、SIGCHID
父进程创建子进程,在子进程退出时候,会给父进程发送SIGCHID信号,但是OS对此信号的处理动作是SIG_IGN,所以一般父进程对子进程退出这一动作不做任何响应。
子进程退出一般需要被回收,回收的动作一般需要通过父进程来进行。回收既可以阻塞等待,也可以轮训检测.轮训检测就需要给waitpid函数设置WNOHANG参数。除此之外,可以利用SIGCHID信号:父进程收到SIGCHID信号,说明子进程退出,在信号处理动作中回收子进程。对一个子进程而言,这种方法十分好用。对多个子进程而言,在信号处理函数中,是阻塞等待,还是轮训检测?
采用轮训检测,同时要一次回收尽可能多的局部子进程,同时不会影响执行流的正常推进。
void HandlerChld(int sig)
{
while(true)
{
pid_t rid = waitpid(-1,nullptr,WNOHANG);
if(rid > 0)
{
cout<<"wait success,rid:"<<rid<<endl;
}
else if(rid < 0)
{
cout<<"wait err"<<endl;
break;
}
else
{//局部子进程等待完了
break;
}
}
}
因为采用阻塞等待,如果有一些子进程是长期存在的,那么执行流就被困在信号响应逻辑中了。但是你有没有想过为什么不一次回收一个子进程?这种方法也是行不通的。在处理一个信号A的时候,A信号会被自动屏蔽。如果在回收子进程的时候,同时又出现了多次SIGCHLD信号,那么在SIGCHLD信号被处理完后,由于OS的pending位图只能记录出现了信号,却不能记录信号出现的次数,于是一定有一些信号丢失了,注定会造成一些子进程退出后一直没被回收,造成僵尸进程。
父进程能不能把回收这件事拜托给其他进程?
可以;父进程在创建子进程的时候,让子进程创建孙子进程,子进程退出,孙子进程执行后续代码。这样孙子进程的父进程退出,成为孤儿进程,会被1号init进程领养,通过1号init进程回收。
父进程也可以宣言自己不回收子进程,可以设置SIGCHLD为SIG_IGN,这样子进程退出的时候,不再进入Z状态,直接被系统回收资源。
要注意我们用户手动设置的SIG_IGN和系统默认设置的SIG_IGN不相同。系统设置,父进程要等待子进程,并回收。用户手动设置,子进程退出后直接回收资源,不进入Z状态。
三、信号总结
综合关于操作系统的三篇文章,关于信号的讨论,无非围绕一个时间轴:
信号的产生和发送——>信号的保存——>信号的处理
信号的产生:本质是由OS产生与发送,本质就是OS向pending位图中写入数据,改变bit位的状态。
信号的保存,需要注意的就是三张位图的理解:
block位图:是否需要屏蔽信号;
pending位图:是否收到信号;
handler位图:如何处理信号;
信号的处理,包括默认(SIG_DFL),忽略(SIG_IGN),用户自定义处理(用户定义的函数指针)。
除此之外,需要熟悉进程的两态转变。
完~