1,sigaction原型
前面我们已经使用signal函数去注册信号。
但是,该方法有两个缺点:
- 使用c99标准编译的话,该方法注册的信号只能被触发一次。想要再次触发就必须再次注册;
- 如果在执行信号处理程序的时候,又捕获到了信号,则该信号会丢失;
所以现在推荐使用的是一个更健壮的函数sigaction来实现相同的功能。不过它的调用要比signal更复杂。
其原型如下:
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
//执行成功返回0;失败返回-1
signo代表信号,不用多说。比较麻烦的是,这里引入了一个新的结构体sigaction,其定义如下:
struct sigaction {
void (*sa_handler)(int); //信号处理程序
sigset_t sa_mask; //信号屏蔽字
int sa_flags; //信号处理选项
void (*sa_sigaction)(int, siginfo_t*, void*);//先别管吧。。。
}
act和oact分别代表修改后和修改前的。oact可以为空,为空就不赋值。
在使用中,sa_mask直接调用sigemptyset()初始化为空就行了。sa_handler也直接赋值句柄就OK。比较麻烦的是sa_flags。针对这一项,有多个宏定义,代表不同的含义。同时,不同的系统对他们的支持还不同。
列举两个最常用的吧:
- SA_INTERRUPT,由此信号终止的系统调用不自动重启(还记得17节里面的那个带限时功能的read函数吧?)
- SA_RESTART,由此信号终止的系统调用会自动重启;
- SA_RESETHAND,捕捉到该信号后,将信号的处理方式重置为默认(SIG_DFL),也就是C99里signal的处理方式,信号注册一次就只能触发一次;
主要我们的sa_flags不取SA_RESETHAND,我们的信号就可以一次注册,多次触发;
同时,sigaction与signal最大的不同是,在调用信号处理程序之前,我们会屏蔽当前信号。这样一来,在执行信号处理程序的时候如果又发来了信号,则这个信号是未决的,等信号处理程序返回,我们会恢复信号屏蔽字。如此,则之前pending的信号这回会被捕获、触发。所以,不用担心在执行信号处理程序的时候发生信号丢失。
2,例子
我们用sigaction代替signal,注册SIGUSR1信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <setjmp.h>
typedef void Sigfunc(int); //信号处理程序的函数类型
void sig_usr_new(int signo);//声明信号处理程序
int main(){
//10.14 使用sigaction注册信号
//代替signal(SIGUSR1, sig_usr_new)
struct sigaction act, oact;
act.sa_handler = sig_usr_new;//设置信号处理程序
sigemptyset(&act.sa_mask); //初始化信号屏蔽字
//设置信号处理选项
act.sa_flags = 0;
act.sa_flags |= SA_INTERRUPT;
//调用sigaction
if (sigaction(SIGUSR1, &act, &oact) < 0)
{
printf("sigaction error!\n");
exit(1);
}
//死循环,为了防止进程退出
while(1){
pause();
}
}
//定义信号处理程序
void sig_usr_new(int signo)
{
printf("recived SIGUSR, signo = %d\n",signo);
sleep(7);
printf("return form sig_usr_new\n");
}
我们故意在信号处理程序里休眠了7秒中,在这7秒种内我们可以不停地发送信号,看看它是怎么处理的:
➜ code g++ -g -W -o study_Linux study_Linux.c
➜ code ./study_Linux &
[2] 169
➜ code kill -USR1 169 #第一次发送信号触发回调
recived SIGUSR, signo = 10 #信号处理程序开始执行,接下来休眠7秒
➜ code kill -USR1 169 #在7秒内连续发送3次信号
➜ code kill -USR1 169
➜ code kill -USR1 169
➜ code return form sig_usr_new #7秒结束,信号处理程序返回,返回前取消屏蔽信号
recived SIGUSR, signo = 10 #休眠期发送的信号触发了回调
➜ code return form sig_usr_new #7秒后,信号处理程序返回
我们可以看到,休眠期内发送的信号并没有丢失,也没有被马上触发,而是等到之前的信号处理程序执行完之后才被触发;这是因为在执行信号处理程序时,操作系统建立的新信号屏蔽字自动包括当前正在处理的信号。换句话说,用sigaction或signal注册信号的时候,操作系统自动屏蔽当前正在触发的信号;等信号处理函数返回前,操作系统再取消对其的屏蔽,准备捕获下一个同样的信号。所以在休眠期再发送相同的信号是不会有问题的。
不过,当前的系统也没有对信号排队,只触发了一次,而不是三次。
如果我们把main函数代码调整一下,变成如下:
int main(){
//改为调用signal
if(signal(SIGUSR1, sig_usr_new) == SIG_ERR)
{
printf("sigaction error!\n");
exit(1);
}
if(signal(SIGUSR2, sig_usr_new) == SIG_ERR)
{
printf("sigaction error!\n");
exit(1);
}
//死循环,为了防止进程退出
while(1){
pause();
}
}
这时我们会发现,在休眠时,不管是发送SIGUSR1还是SIGUSR2,都是立马执行信号处理程序,根本不会等待原先的信号处理程序执行完。
结果如下:
➜ code kill -USR1 388 #发送USR1
recived SIGUSR, signo = 10 #执行USR1的回调
➜ code kill -USR2 388 #发送USR2
recived SIGUSR, signo = 12 #立刻执行USR2的回调
➜ code return form sig_usr_new, signo = 12 #USR2的回调返回
return form sig_usr_new, signo = 10 #USR1的回调返回
我们可以看到USR2的回调甚至在USR1的回调之前返回。
我们可以把代码再修改下,改成执行USR1的回调的时候,屏蔽USR2.这样做的好处是,确保USR1的回调不会因为受到USR2信号而被打断,在执行完USR1的回调后再继续执行USR2的回调。
int main()
{
//10.14 使用sigaction注册信号
//代替signal(SIGUSR1, sig_usr_new)
struct sigaction act, oact;
act.sa_handler = sig_usr_new;//设置信号处理程序
sigemptyset(&act.sa_mask); //初始化信号屏蔽字
sigaddset(&act.sa_mask, SIGUSR2);//屏蔽SIGUSR2
//设置信号处理选项
act.sa_flags = 0;
act.sa_flags |= SA_INTERRUPT;
//调用sigaction,注册SIGUSR1
if (sigaction(SIGUSR1, &act, &oact) < 0)
{
printf("sigaction error!\n");
exit(1);
}
//调用signal,注册SIGUSR2
if(signal(SIGUSR2, sig_usr_new) == SIG_ERR)
{
printf("sigaction error!\n");
exit(1);
}
//死循环,为了防止进程退出
while(1){
pause();
}
}
执行结果如下:
➜ code kill -USR1 448 #发送USR1
recived SIGUSR, signo = 10 #执行USR1回调,屏蔽了USR2
➜ code kill -USR2 448 #休眠期发送USR2回调
➜ code return form sig_usr_new, signo = 10 #USR1的回调返回,取消屏蔽USR2
recived SIGUSR, signo = 12 #开始执行USR2的回调
return form sig_usr_new, signo = 12 #USR2的回调返回
现在不用担心在执行USR1回调的期间开始执行USR2的回调了。