系列文章目录
C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程
期待你的关注哦!!!
现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.
Linux信号编程、signal函数范例详解
- 系列文章目录
- 一、signal 函数初识
- 二、引申出的思考问题 - 可重入函数概念
- 三、信号集(信号屏蔽字)
- 四、信号相关函数
- 五、sigprocmask等信号函数的范例演示
- 六、小结
一、signal 函数初识
收到一个信号之后,可以使用signal函数来忽略或者捕捉,看如下范例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sig_usr(int signo)
{
if(signo == SIGUSR1)
{
printf("收到了SIGUSR1信号!\n");
}else if(signo == SIGUSR2){
printf("收到了SIGUSR2信号!\n");
}else{
printf("收到了未捕捉的信号%d!\n", signo);
}
}
int main(int argc, char *const *argv)
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR1信号!\n");
}
//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR2信号!\n");
}
for(;;)
{
sleep(1); //休息1s
printf("休息1s\n");
}
return 0;
}
通过两次调用signal函数,分别注册信号SIGUSR1和信号SIGUSR2对应的信号处理函数(sig_usr),当收到这两个信号时,sig_usr就会被调用。在sig_usr信号处理函数过程中,只是做了一些信息输出的工作。
编译运行,然后用kill命令发送两个信号:
kill -usr1 4155
kill -usr2 4155
查看进程:
可以看出,nginx进程收到两个信号,并且还能继续运行不受影响。还可以看到kill命令的另外一种形式:直接用信号名的方式向进程发送信号。
通过这个范例,应该认识到两个问题:
(1)signal函数捕捉了系统的SIGUSR1和SIGUSR2信号,并用自己的函数来处理,获得了成功。
如果程序中不捕捉SIGUSR1或者SIGUSR2信号,用kill向改进程发送SIGUSR1或者SIGUSR2信号,进程会有什么表现呢?
答案:当然是终止进程,因为这两个信号的系统默认动作是终止进程。(可以自行测试下)
(2)信号可能是某个进程发出的,也可能是内核发出的,但不管是怎么发出的,总之目标进程(nginx)收到了这个信号。目标进程收到信号这件事,会被内核注意到,这时内核就有动作了。内核动作是什么呢?
如图:
二、引申出的思考问题 - 可重入函数概念
我们看下如下代码,会出现什么问题?
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int g_mysign = 0;
void muNEfunc(int value)
{
//...其他处理
g_mysign = value;//函数muNEfunc能够修改全局变量g_mysign的值
//...其他处理
}
void sig_usr(int signo)
{
muNEfunc(22);
if(signo == SIGUSR1)
{
printf("收到了SIGUSR1信号!\n");
}else if(signo == SIGUSR2){
printf("收到了SIGUSR2信号!\n");
}else{
printf("收到了未捕捉的信号%d!\n", signo);
}
}
int main(int argc, char *const *argv)
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR1信号!\n");
}
//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR2信号!\n");
}
for(;;)
{
sleep(1); //休息1s
printf("休息1s\n");
muNEfunc(15);
printf("g_mysign = %d\n", g_mysign);
}
return 0;
}
请思考一下,这样写代码会出现什么问题?(当然这种问题是在极端的情况下出现,平时不出现,不太容易看出)
本来期望每次输出的g_mysign的值是15,但偏偏收到一个信号,信号处理程序中改变了g_mysign的值,导致printf输出的g_mysign变成了22。这个结果是不是出乎意料呢!
所以,引出一个概念,叫做“可重入函数”。
可重入函数又称可重入的函数或异步信号安全的函数,指在信号处理函数中调用是安全的函数。(显然muNEfunc是不安全的)
⚠️有些周知的函数是不可重入的(在信号处理函数中不要调用的)如malloc分配内存的函数、printf屏幕输出函数等。(实际商业代码中避免在信号处理函数中调用printf函数)。
根据分析,得到一些结论和处理方法:
(1)在信号处理函数中,应尽量使用简单的语句做简单的事情,尽量不要调用系统函数,以免引起麻烦。
(2)如果必须在信号处理函数中调用系统函数,只调用可重入函数,不要调用不可重入的系统函数。
(3)如果必须在信号处理函数中调用那可能修改errno的值的可重入系统函数,应考虑事先备份errno的值,事后再从信号处理函数返回之前恢复errno的值。(errno的值的系统函数被认为是可重入的系统函数
)
#include <errno.h> //用到errno则需要包含此头文件
void sig_usr(int signo)
{
int myerrno = errno; //备份errno值
//......进行一系列处理,如调用可重入函数
//......
errno = myerrno; //还原errno值
}
三、信号集(信号屏蔽字)
思考一个问题:收到一个SIGUSR1信号,开始执行信号处理函数sig_usr,尚未执行完成时,突然又收到一个SIGUSR1信号,系统会不会再次触发sig_usr函数开始执行呢?
一般不会,也就是说,当收到某个信号,启动执行信号处理函数的时候,通常会”屏蔽/阻塞“其后相同的信号,直到信号处理函数执行结束(系统自动处理)。
一个进程必须记住当前阻塞了哪些信号。如收到信号SIGUSR1时,系统将标记正在处理的该信号的标志设置为1,然后去执行信号处理函数,如果信号处理函数未执行完成时再次收到该信号,系统检测到该信号的标志已经为1,后来的SIGUSR1信号就需要排队等候(等待调用信号处理函数来处理)或直接被忽略(丢失)。当信号处理函数执行完毕,再把信号SIGUSR1信号对应的标志设置回0,此时如果有排队的SIGUSR信号或者新收到的SIGUSR1信号,就可以继续调用信号处理函数处理了。
这时候引入了信号集的概念,一种叫做信号集的数据类型,这种数据类型能把60个信号的状态(0或者1)都保存下来。用0表示没收到某个信号,用1表示收到某个信号并正在处理中。
1、例如如果约定第五个位置表示信号SIGUSR1,程序开始执行后,收到一个SIGUSR1信号,就立即把第5个位置标记1:
0000100000,0000000000,0000000000,.....
2、然后,等待调用信号处理函数处理这个到来的信号;
3、此时,如果再收到一个SIGUSR1信号,因为第5个位置已经被标记为1,后面的这个SIGUSR1信号就会排队等候或者忽略;
4、调用完处理函数后,把信号集的第5个位置标记回0:
0000000000,0000000000,0000000000,......
5、此时,如果有排队等候或者新收到的SIGUSR1信号,就会又可以继续调用信号处理函数来处理了。
信号集这种数据类型用 sigset_t 来表示。
sigset_t 结构大概这样:
typedef struct{
unsigned long sig[2]; //long是4字节32位,两个就是64位,代表64个信号
}sigset_t;
四、信号相关函数
有了这些信号集类型就可以介绍 sigempty、sigfillset、sigaddset、sigdest、sigprocmask、sigismember
等几个函数了。
(1)sigemptyset。把信号集中所有的信号清零,表示这60多个信号都没来。
0000000000,0000000000,000000000,......
(2)sigfillset。把信号集中的所有信号都设置为1,与sigemptyset功能正好相反。会导致到来任何信号都会排队等候或者被忽略。
111111111,1111111111,111111111, ......
(3)信号集中支持60多个信号,可以向信号集中增加(信号标志设置为1)或删除(信号标志设置为0)特定的信号,用sigaddset和sigdelset就可以做到。
sigaddset用于将某个信号设置为1,sigdelset用于将某个信号设置为0。
(4)sigprocmask、sigismember。
sigpromask函数用于设置进程所对应的信号集(进程有默认的信号集,但可以用sigpromask函数设置其他信号集)。
sigismember函数用于检测信号集的特定信号是否被置位。
五、sigprocmask等信号函数的范例演示
范例如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sig_quit(int signo)
{
printf("收到了SIGQUIT信号!\n");
}
int main(int argc, char *const *argv)
{
//定义新的信号集和原有的信号集
sigset_t newmask, oldmask, pendmask;
//注册信号对应的处理函数
if(signal(SIGQUIT, sig_quit) == SIG_ERR)
{
printf("无法捕捉SIGUSR1信号!\n");
//退出程序,参数是错误代码,0表示正常退出,非0表示错误,但具体什么错误,没有特别的规定
exit(1);
}
//newmask信号集中所有的信号都清零(表示这些信号都没有来)
sigemptyset(&newmask);
//设置newmask信号集中的SIGQUIT信号位为1,再来SIGQUIT信号时进程就收不到
sigaddset(&newmask, SIGQUIT);
//设置该进程所对应的信号集
//第1个参数用了SIG_BLOCK,表明设置进程新的信号屏蔽字为当前信号屏蔽字和第2个参数指向的信号集的并集。
//一个进程的当前信号屏蔽字,开始全部为0,相当于把当前信号屏蔽字设置成newmask(屏蔽了SIGQUIT)。
//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第3个参数里,以备后续恢复用
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0);
{
printf("sigprocmask(SIG_BLOCK)失败!\n");
exit(1);
}
printf("我要开始休息10s了-------begin----,此时我无法接受SIGQUIT信号!\n");
sleep(10);
printf("我已经休息10s了-------end----!\n");
//测试一个指定的信号位是否被置位,测试的是newmask
if(sigismember(&newmask, SIGQUIT))
{
printf("SIGQUIT信号被屏蔽了!\n");
}else{
printf("SIGQUIT信号没有被屏蔽了!\n");
}
//测试一个指定的信号位是否被置位,测试的是newmask
if(sigimember(&newmask,SIGHUP))
{
printf("SIGQUIT信号被屏蔽了!\n");
}else{
printf("SIGQUIT信号没有被屏蔽了!\n");
}
//现在取消SIGQUIT信号的屏蔽(阻塞)-- 把信号集还原回去
//第一个参数用了SIG_SETMASK表明设置进程新的信号屏蔽字为第2个参数指向的信号集,第3个参数没用
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
{
printf("sigprocmask(SIG_SETMASK)失败!\n");
}else{
printf("sigprocmask(SIG_SETMASK)成功!\n");
}
//测试一个指定的信号位是否被置位,这里测试的是oldmask
if(sigismember(&oldmask, SIGQUIT))
{
printf("SIGQUIT信号被屏蔽了!\n");
}else{
printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要睡10s!!!!!!!\n");
int mysl = sleep(10);
if(mysl > 0)
{
printf("sleep还没睡够,剩余%d\n", mysl);
}
}
printf("再见了!\n");
return 0;
}
运行结果如下:
六、小结
还有一个sigaction
函数,用来取代signal
函数。商业代码中只用sigaction
。可以了解下。