信号掩码
内核为每一个进程维护了一个信号掩码,本质为一组信号集。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
向信号掩码中添加一个信号,通常有如下几种方式:
1、当应用程序调用signal()或sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞(sigaction()需设置标志位);当信号处理函数返回后,会自动将该信号从信号掩码中移除。
2、使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过 sa_mask 参数进行设置。
3、除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加或移除信号。
sigprocmask()函数原型如下所示:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数参数和返回值含义如下:
how:参数 how 指定了调用函数时的一些行为。
set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为 NULL,则表示无需对当前信号掩码作出改动。
oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码, 存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
返回值:成功返回 0;失败将返回-1,并设置 errno。
参数 how 可以设置为以下宏:
SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。
SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig){
printf("执行信号处理函数...\n");
}
int main(void){
struct sigaction sig = {0};
sigset_t sig_set;
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 信号集初始化 */
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
exit(-1);
/* 向自己发送信号 */
raise(SIGINT);
/* 休眠 2 秒 */
sleep(2);
printf("休眠结束\n");
/* 从信号掩码中移除添加的信号 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
exit(-1);
exit(0);
}
上述代码中,我们为 SIGINT 信号注册了一个处理函数 sig_handler,当进程接收到该信号之后就会执行它;然后调用 sigprocmask 函数将 SIGINT 信号添加到信号掩码中,然后再调用 raise(SIGINT)向自己发送一 个 SIGINT 信号,如果信号掩码没有生效、也就意味着 SIGINT 信号不会被阻塞,那么调用 raise(SIGINT)之 后应该就会立马执行 sig_handler 函数,从而打印出"执行信号处理函数..."字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在 2 秒后才执行,因为程序中使用 sleep(2)休眠了 2 秒钟之后,才将 SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。
运行测试:
阻塞等待信号 sigsuspend()
上文已经说明,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望对一个信号解除阻塞后,然后调用 pause()以等待之前被阻塞的信号的传递,这将如何?譬如有如下代码段:
sigset_t new_set, old_set;
/* 信号集初始化 */
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))
exit(-1);
/* 受保护的关键代码段 */
......
/**********************/
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))
exit(-1);
pause();/* 等待信号唤醒 */
执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了pause()阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从 pause 返回继续执行。
考虑到这样一种情况,如果信号的传递恰好发生在第 二次调用 sigprocmask()之后、pause()之前,如果确实发生了这种情况,就会产生一个问题,信号传递过来就会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到 pause()被阻塞,知道下一次信号发生时才会被唤醒,这有违代码的本意。
虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷,要避免这个问题,需要将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这正是 sigsuspend()系统调用的目的所在,sigsuspend()函数原型如下所示:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
函数参数和返回值含义如下:
mask:参数 mask 指向一个信号集。
返回值:sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT。
sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进程的信号掩码恢复成调用前的值。 调用 sigsuspend()函数相当于以不可中断(原子操作)的方式执行以下操作:
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig){
printf("执行信号处理函数...\n");
}
int main(void){
struct sigaction sig = {0};
sigset_t new_mask, old_mask, wait_mask;
/* 信号集初始化 */
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigemptyset(&wait_mask);
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
exit(-1);
/* 执行保护代码段 */
puts("执行保护代码段");
/******************/
/* 挂起、等待信号唤醒 */
if (-1 != sigsuspend(&wait_mask))
exit(-1);
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
exit(-1);
exit(0);
}
运行结果:
在上述代码中,我们希望执行受保护代码段时不被 SIGINT 中断信号打断,所以在执行保护代码段之前将 SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段,调用 sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态。