【摘要】本文详细讲述了信号集的概念,并分别列举了与其有关的各种操作函数,必要时举出了相关例子进行进一步阐述,旨在帮助大家更好的理解信号集操作函数的内在涵义和具体使用场景。
1.基本概念
- Linux中通过数据结构——信号集(sigset_t)来按位表示系统中的64种信号的状态;例如阻塞信号集、未决信号集等。
- 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态.
- 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
- 信号集这个数据类型内部是如何存储这些 bit ,依赖于具体操作系统实现 , 我们是不能直接通过位运算进行操作 ,而只能调用信号集操作函数来操作 sigset_t变量。
- 系统对每个信号都有两个标志位来表示该信号所处的状态——阻塞(block)和未决(pending),另外每个信号还关联一个函数指针,表示该信号的处理动作。
- 阻塞:表示即使信号已经产生(信号对应的未决标志置位),也暂时不对该信号做出反应,但会根据信号是否为实时信号(1-31号为普通信号,34-64号为实时信号),做累计计数或不做累计计数。即Linux中,普通信号在递达之前,即使产生多次也只计⼀次,而实时信号在递达之前产生多次,会依次存放在一个队列⾥。
- 未决:表示该信号已经产生但目前还未被处理,即还未执行默认动作、忽略或是捕捉(用户自定义函数)。
- 当一个信号产生时,内核首先在进程控制块中将该信号的未决标志置位,直到信号递达(执行力相应的信号处理函数)才会清除该标志。
- 注意:假设有某个处理函数为“忽略”的信号A,且目前该信号是被阻塞的(在阻塞信号集中,它对应的位为1)。如果,此时该信号产生,我们不能因为它的处理动作是忽略,而在没有解除阻塞之前忽略这个信号。因为进程仍有可能再解除阻塞之前改变信号的处理动作。
2.信号集操作函数
2.0 一些无需多讲的简单函数
-
int sigemptyset(sigset_t *set);
- 该函数的作用是将信号集初始化为空。
-
int sigfillset(sigset_t *set);
- 该函数的作用是把信号集初始化包含所有已定义的信号。
-
int sigaddset(sigset_t *set, int signo);
- 该函数的作用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。
-
int sigdelset(sigset_t *set, int signo);
- 该函数的作用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1。
-
int sigismember(sigset_t *set, int signo);
- 该函数的作用是判断给定的信号signo是否是信号集中的一个成员,如果是返回1,如果不是,返回0,如果给定的信号无效,返回-1。
2.1 sigpromask()
-
int sigpromask(int how, const sigset_t *set, sigset_t *oset);
-
该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。
-
如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。
-
how的不同取值及操作如下所示:
how 涵义 SIG_BLOCK 把参数set中的信号添加到信号屏蔽字中,也就是将两者并集作为新的信号屏蔽字 SIG_UNBLOCK 从信号屏蔽字中删除参数set中的信号,也就是将两者差集作为新的信号屏蔽字 SIG_SETMASK 把信号屏蔽字设置为参数set,并将原信号屏蔽字保存到oset中(如果非空) -
如果sigpromask成功完成返回0,如果how取值无效返回-1,并设置errno为EINVAL。
-
注意:调用这个函数会改变进程的屏蔽字,而之前的函数都是为改变一个变量的值而已,并不会真正影响进程的屏蔽字。
-
在调用sigpromask后如果有任何未决的、不再阻塞的信号,则在sigpromask函数返回之前,至少会将其中的一个信号传递给该进程,这点和kill函数很相似。
-
如果想要得到当前的信号屏蔽字,可以这样:
sigpromask(0,NULL,&sigset)
,其中sigset是sigset_t类型的变量
-
2.2 sigpending()
进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。进程不会出现未决,除非进程中的sigprocmask函数设置了SIG_BLOCK,才会出现未决信号。
-
int sigpending(sigset_t *set);
- 该函数的作用是将被阻塞的信号(停留在待处理状态的一组信号)写到参数set指向的信号集中,成功调用返回0,否则返回-1,并设置errno表明错误原因。
-
编写测试程序,进程首先阻塞所有信号,而后睡眠10秒,此期间向进程发送的信号就是未决信号,并可以通过sigpending函数获取。
#include <stdio.h> #include <signal.h> #include <unistd.h> void term_hook(int signo) { printf("stop\n"); } int main(void) { printf("%d\n",getpid()); signal(SIGTERM,term_hook); sigset_t set, old_set, pending; sigfillset(&set); sigprocmask(SIG_BLOCK,&set,&old_set); sleep(10); //在此期间向进程发送的信号称为未决信号 sigpending(&pending); //获取睡眠时收到的未决信号 printf("%d\n",sigismember(&pending,SIGTERM)); while(1); return 0; }
- 在上述进程暂停10秒的期间,利用另一个终端执行
kill -15 进程pid
对着进程发送信号,就会发现信号出现在了pending这个信号集中(由sigismember验证),验证了sigpending是用来检测未决信号的。
- 在上述进程暂停10秒的期间,利用另一个终端执行
2.3 sigsuspend() VS pause()
-
int sigsuspend(const sigset_t *sigmask);
- 该函数通过将进程的屏蔽字替换为由参数sigmask给出的信号集,而后挂起进程,注意这两个操作是原子的,中间不会被信号打断。
- 当捕捉一个信号,首先执行信号处理程序,然后从sigsuspend返回,最后将信号屏蔽字恢复为调用sigsuspend之前的值。
- 如果接收到的信号终止了程序,sigsuspend就不会返回,如果接收到的信号没有终止程序,sigsuspend就返回-1,并将errno设置为EINTR。
-
int pause(void);
- 使调用它的进程被挂起,直到进程捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回-1,并且将erron设置为EINTR。
- 如果先用sigpromask函数阻塞信号,而后执行完特定代码后,再次执行sigpromask解除阻塞,接着执行pause函数等待信号。那么在sigpromask解除阻塞之后及pause执行之前的这段时间,有可能会有信号进来(或者有之前被阻塞的信号需要响应)。所以,相比sigsuspend它是非原子的。具体看一下例子代码:
-
举例对比sigsuspend的原子性和pause的特性
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <string.h> void func(int num) { printf("0\n"); } int main(void) { int i; sigset_t set, sigset_t empty; sigemptyset(&set); sigemptyset(&empty); sigaddset(&set, SIGINT); signal(SIGINT, func); while(1) { sigprocmask(SIG_BLOCK, &set, NULL); for(i = 0; i < 5 ; i++) { write(1, "* ", strlen("* ")); sleep(1); } printf("\n"); #if 1 sigsuspend(&empty); #else sigprocmask(SIG_UNBLOCK, &set, NULL); pause(); #endif } return 0; }
-
在未打印到第5 个星时,中间按 ctrl+c 键发送中断信号:
1)sigsuspend, 5个*到之后会自动换行继续执行。
2)pause, 5个*到之后,先进入信号处理func函数,之后执行pause(不会继续往下执行),等待再一次中断。
[root@localhost lee]# ./a.out //#if 1 时 * * * * * 0 * * * * * 0 * * 退出 [root@localhost lee]# ./a.out //#if 0 时 * * * * * 0 <——响应第一个中断信号 0 <——响应第二个中断信号(pause) * * * * *
-
特别提醒:如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。
2.4 sigaction()
- sigaction函数
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
该函数与signal函数一样,用于设置与信号sig关联的动作,而oact如果不是空指针的话,就用它来保存原先对该信号的动作的位置,act则用于设置指定信号的动作。
sigaction是比signal函数更加强大和稳健的一个系统函数,sigaction函数的功能是检查或者修改与置顶信号相关联的处理动作,一旦设置了sigaction,那么这个设置将会一直有效,推荐使用此函数处理信号,不要使用signal()。
-
sigaction结构体定义在signal.h中,但是它至少包括以下成员:
-
void (*) (int) sa_handler;
处理函数指针,相当于signal函数的func参数。- sa_handler的三种情况:
- SIG_DFL是系统默认的信号处理方式
- SIG_IGN 是 忽略信号的处理
- 我们定义的处理函数
- sa_handler的三种情况:
-
sigset_t sa_mask;
sa_mask是一个信号集,在调用信号处理函数之前,将其加到信号的屏蔽字当中去,仅当从信号处理函数返回之后再将信号的屏蔽字复位为原来的值.这样在调用信号处理函数时就可以阻塞某些信号,其中包括sigaction函数处理的信号。极端情况下,阻塞所有的除本信号之外的所有的信号,这样sigaction函数将不会被中断。 -
int sa_flags;
信号处理修改器;。该函数一旦对给定的信号设置了一个动作,那么除非再次调用sigaction显示的改变之前,该设置将一直有效,也就是说不会存在在处理完一个信号之后下一个相同的信号的处理改为系统默认的情况。除非sa_flags设置为SA_RESETHAND。后面有例子进行讲解。-
sa_flags的取值
-
-
结构体完整定义为:
struct sigaction { /* Signal handler. */ #if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED union { /* Used if SA_SIGINFO is not set. */ __sighandler_t sa_handler; /* Used if SA_SIGINFO is set. */ void (*sa_sigaction) (int, siginfo_t *, void *); } __sigaction_handler; # define sa_handler __sigaction_handler.sa_handler # define sa_sigaction __sigaction_handler.sa_sigaction #else __sighandler_t sa_handler; #endif /* Additional set of signals to be blocked. */ __sigset_t sa_mask; /* Special flags. */ int sa_flags; /* Restore handler. */ void (*sa_restorer) (void); };
-
-
应用举例:
-
(1)通过设置sa_flags为SA_SIGINFO来获取信号发送进程相关信息
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> void hook(int signo,siginfo_t* info,void* context) { printf("info signo:%d\n",info->si_signo); printf("info errno:%d\n",info->si_errno); printf("info code:%d\n",info->si_code); printf("info pid:%d\n",info->si_pid); printf("info uid:%d\n",info->si_uid); printf("info band:%ld\n",info->si_band); ucontext_t* sig_context = (ucontext_t*)context;//用来保存上下文的 //常见的ucontext库可以使用 } int main() { printf("%d\n",getpid()); struct sigaction action; struct sigaction action_info; action.sa_flags = SA_SIGINFO; action.sa_handler = (void*)hook; sigaction(SIGTERM,&action,NULL); while(1); }
-
以上程序涉及到了sigaction的两种用法:
- 一个是为特定信号绑定信号处理函数,
- 另一个是获取信号的action信息,即当我们设置了SA_SIGINFO以后,信号处理函数可以返回更多信息(siginfo_t* info 和 void *context这两个参数),我们可以通过这些参数获取有关发送该信号的进程的信息。
-
我们使用
kill -15 进程pid
,对进程发送信号,会得到如下输出:
-
-
(2)使用sa_mask字段消除竞态条件
#include <unistd.h> #include <stdio.h> #include <signal.h> void gotit(int sig) { printf("I got signal %d\n", sig); } int main() { struct sigaction act; act.sa_handler = gotit; sigemptyset(&act.sa_mask); //创建空的信号屏蔽字,即不屏蔽任何信息 act.sa_flags = SA_RESETHAND; //使对信号的处置动作重置为默认行为 sigaction(SIGINT, &act, 0); while(1) { printf("Hello World!\n"); sleep(1); } return 0; }
-
运行结果:
-
sigaction函数在默认情况下是不重置信号处置动作的,如果要想它重置,则sa_flags就要为SA_RESETHAND。
-
2.5 sigsetjmp()/siglongjmp() VS setjmp()/longjmp()
-
goto是我们在编程中用的最简单的函数内跳转关键字,但如果我们要实现跨函数的跳转就可以使用setjmp和longjmp,但是这两个函数在信号处理函数中使用会有一个问题——在信号处理函数内部调用longjmp的时候,会因为进程默认阻塞1-31号普通信号(即当前信号会自动加入屏蔽集中),即阻止了进程继续响应后来产生的普通信号。
#include <setjmp.h> int setjmp(jmp_buf env); 返回值:若直接调用则返回0,若从longjmp调用返回,则返回非0。 void longjmp(jmp_buf env, int val); 跳转到设置env的setjmp函数后继续执行
-
为了解决上述问题,我们可以使用
sigsetjmp()/siglongjmp()
函数对,通过添加非零的savemask参数,达到实时响应的目的。#include <setjmp.h> int sigsetjmp(jmp_buf env, int savemask); 返回值:若直接调用则返回0,若从longjmp调用返回,则返回非0。 savemask:非0,则在env中保存当前的信号屏蔽字,从而在调用siglongjmp时候, siglongjmp从其中回复他的信号屏蔽字(可继续响应该信号); 为0,则不保存,效果同setjmp/longjmp void siglongjmp(jmp_buf env, int val); 跳转到设置env的setjmp函数后继续执行
-
下面具体分析:
(1)setjmp()/longjmp()使用举例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <ucontext.h>
#include <setjmp.h>
jmp_buf context_buffer;
void hook(int signo)
{
printf("stop\n");
longjmp(context_buffer,1);
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,hook);
setjmp(context_buffer);
while(1);
}
-
运行上述程序,通过另一个终端连续向该进程发送15号信号时,进程只响应一次,后续信号被屏蔽。
(2)sigsetjmp()/siglongjmp()使用举例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <ucontext.h>
#include <setjmp.h>
jmp_buf context_buffer;
void hook(int signo)
{
printf("stop\n");
siglongjmp(context_buffer,1);
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,hook);
//sigsetjmp(context_buffer,0); //效果同setjmp()/longjmp()
sigsetjmp(context_buffer,1);
while(1);
}
-
运行上述程序,通过另一个终端连续向该进程发送15号信号时,进程连续响应。
2.6 sigwait()
int sigwait(const sigset_t *sigmask,int* signo);
-
sigwait函数一直阻塞,直到*sigmask指定的信号集中任何一个信号到来为止,然后从挂起(未决)信号集中删除那个信号,并解除对它的阻塞。当sigwait返回时,从挂起信号集中删除的信号的个数被存储在signo指定的那个位置中。
- 信号是向进程发送的软件通知,通知进程有事件发生。引发信号的事件发生时,信号就被生成了。进程根据信号采取行动时,信号就被传递了。信号的寿命就是信号的生成和传递之间的时间间隔。已经生成但还未被传递的信号被称为挂起的信号。在信号生成和信号传递之间可能会有相当长的时间。
-
sigwait() 提供了一种等待信号的到来,并以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:
-
多个相同的信号可不可以在信号队列中排队?
- 对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队
-
如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?实时信号和非实时信号在处理时有没有什么区别?
- 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。
-
sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait()函数相似的功能。
-
sigwait为我们提供了一种信号同步的处理方法,当然也可利用epoll+signalfd的方法进行多路复用形式的信号同步。下面用一小段测试程序来测试 sigwait 在信号处理时的一些用法:
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> int main() { sigset_t set,block_set; sigemptyset(&set); sigaddset(&set,SIGINT); //设置信号屏蔽 sigfillset(&block_set); sigprocmask(SIG_BLOCK,&block_set,NULL); //阻塞所有信号 int retval; for(;;) { int res = sigwait(&set, &retval); //仅等待SIGINT信号 if (res == 0) { printf("sigint\n"); } } exit(0); }
-
程序运行结果
-
3.信号集操作函数使用举例
下面以几个例子来说明上述函数的用法:
(1)综合例子
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
void handler(int sig)
{
printf("Handle the signal %d\\n", sig);
}
int main()
{
sigset_t sigset; //用于记录屏蔽字
sigset_t ign; //用于记录被阻塞的信号集
struct sigaction act;
sigemptyset(&sigset); //清空信号集
sigemptyset(&ign);
sigaddset(&sigset, SIGINT); //向信号集中添加信号SIGINT
act.sa_handler = handler; //设置处理函数
sigemptyset(&act.sa_mask); //设置信号集,不阻塞任何信号
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
printf("Wait the signal SIGINT...\n");
pause(); //挂起进程,等待信号,直到有信号被处理才会继续执行
sigprocmask(SIG_SETMASK, &sigset, 0); //设置进程屏蔽字,在本例中为屏蔽SIGINT
printf("Please press Ctrl+c in 10 seconds...\n");
sleep(10);
sigpending(&ign); //获取接收到的且被阻塞的信号,测试SIGINT是否被屏蔽
if(sigismember(&ign, SIGINT))
printf("The SIGINT signal has ignored\n");
sigdelset(&sigset, SIGINT); //在信号集中删除信号SIGINT
printf("Wait the signal SIGINT...\n");
sigsuspend(&sigset); //将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽,并挂起进程
printf("The app will exit in 5 seconds!\n");
sleep(5);
exit(0);
}
- 首先,我们能过sigaction函数改变了SIGINT信号的默认行为,使之执行指定的函数handler,所以输出了语句:Handle the signal 2。
- 然后,通过sigprocmask设置进程的信号屏蔽字,把SIGINT信号屏蔽起来,所以过了10秒之后,用sigpending函数去获取被阻塞的信号集时,检测到了被阻塞的信号SIGINT,输出The SIGINT signal has ignored。
- 最后,用函数sigdelset函数去除先前用sigaddset函数加在sigset上的信号SIGINT,再调用函数sigsuspend,把进程的屏蔽字再次修改为sigset(不包含SIGINT),并挂起进程。由于先前的SIGINT信号停留在待处理状态,而现在进程已经不再阻塞该信号,所以进程马上对该信号进行处理,从而在最后,你不用输入Ctrl+c也会出现后面的处理语句,最后过了5秒程序就成功退出了。
(2)将普通信号(1-31)改造为实时信号(34-64)
系统在处理接收到的普通信号时,一旦进入某信号处理函数后,就会默认的屏蔽(阻塞)该信号,即就算你再此阶段发送了多次信号,也会被阻塞在外,系统不会响应。但如果我们在某信号的处理函数中,解除对该信号的屏蔽,那该信号不就会在信号处理阶段被接收和处理了嘛。我们通过实验来进行验证!
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
void my_func(int signo)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_UNBLOCK,&set,NULL);
printf("hello\n");
sleep(3);
printf("leon!\n");
}
int main(void)
{
signal(SIGINT, my_func);
while(1);
return 0;
}
- 上图显示了,当在程序运行时,我不断向程序发送SIGINT信号时,进程能够实时多次响应,而不是向以前那样,在信号处理阶段仅响应一次。
【声明】本文参考 不少优秀作者发布的博客,由于时间仓促未一一列明出处,若涉及侵权,请原作者联系授权或删除。