8 信号集
8.1信号集的基本操作函数
前面我们已经知道,不同的信号的编号可能超过一个整型量所包含的位数,因此我们不能使用整型量中的一位来表示一种信号,也就是说我们不能使用整型变量来表示信号集。POSIX.1定义了一种新的数据类型:信号集(sigset_t), 并且还定义了5个处理信号的函数。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
上述四个函数返回值:若成功返回0;失败返回-1
int sigismember(const sigset_t *set, int signo);
返回值:为真返回1;为假返回0
- 函数sigemptyset初始化由set指向的信号集,清除其中所有的信号;
- 函数sigfillset初始化set指向的信号集,使其包含所有的信号;
所有的应用程序在使用信号集之前,都需要调用sigemptyset或者sigfillset对信号集进行初始化。一旦初始化了一个信号集,以后便可以对信号集进行增加、删除特定的信号:
- 函数sigaddset将一个信号添加到已有的信号集中;
- 函数sigdelset从一个现有的信号集中删除一个特定的信号;
对所有以信号集为参数的函数,总是以信号集的地址作为传递的参数。
这个原因在于:因为我们要在函数中修改信号集中的信号,因此需要地址传递方式(简单的值传递是行不通的),否则无法实现真正的修改。
8.2 函数sigprocmask: 进程屏蔽字
我们在前面已经提到过:进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。进程的信号屏蔽字是通过函数sigprocmask来进行检测或修改的,或者同时进行检测和修改。
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
返回值:成功返回0;失败返回-1;
-
首先,如果oset是非空指针,那么进程的当前信号屏蔽字通过oset返回
; -
其次,如果set为非空指针,则参数how只是如何修改当前进程的信号屏蔽字
;下表便是how的取值以及相关的说明。 -
如果set为空指针,则不修改该进程的信号屏蔽字,how值是无意义的
。
序号 | how | 说明 |
---|---|---|
1 | SIG_BLOCK | 将set中的信号添加到当前进程的信号屏蔽字中(两者取或运算,即并集) |
2 | SIG_UNBLOCK | 将set中的信号从当前进程信号屏蔽字中删除(set补集的交集) |
3 | SIG_SETMASK | 将set设置为当前进程的信号屏蔽字(不关心原进程的信号屏蔽字) |
在调用sigprocmask后如果有任何悬而未决、不再阻塞的信号(原来信号是阻塞,现在改为非阻塞),则在sigprocmask函数返回前至少将其中之一递送给该进程。
示例:获取当前进程屏蔽字
/*************************************************************************
> File Name: sigprocmask_demo.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月28日 星期二 08时15分06秒
************************************************************************/
#include <stdio.h>
#include <error.h>
#include <signal.h>
/*获取当前进程的信号屏蔽字,并测试包括哪些信号。*/
void getSigProcMask()
{
sigset_t set;
printf("Enter %s\n", __func__);
if(sigemptyset(&set)!=0){
perror("sigemptyset error:");
}else if(sigprocmask(0,NULL,&set)!=0){
perror("sigprocmask error:");
}else{
printf("Sigprocmask contains following signals:");
if(sigismember(&set, SIGINT))
printf(" SIGINT");
if(sigismember(&set, SIGQUIT))
printf(" SIGQUIT");
if(sigismember(&set, SIGALRM))
printf(" SIGALRM");
if(sigismember(&set, SIGCHLD))
printf(" SIGCHLD");
printf("\n");
}
printf("Exit %s\n", __func__);
}
8.3 函数sigpending
sigpending函数返回一个信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前悬而未决的。该信号集通过set参数返回。
#include <signal.h>
int sigpending(sigset_t *set);
返回值:成功返回0;失败返回-1;
示例:查询当前挂起的信号
/*************************************************************************
> File Name: sigpending_demo.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月28日 星期二 08时36分45秒
************************************************************************/
#include <stdio.h>
#include <signal.h>
/*初始化进程屏蔽字*/
void testPendingSignal()
{
/*定义三个信号集*/
sigset_t new_set; /*,用来设置新的屏蔽字*/
sigset_t old_set; /*,用来获取之前的信号屏蔽字*/
sigset_t pending_set; /*,获取正在挂起的信号屏蔽字*/
/*清空三个信号集*/
sigemptyset(&new_set);
sigemptyset(&old_set);
sigemptyset(&pending_set);
/*将SIGQUIT信号添加至信号集中*/
sigaddset(&new_set, SIGINT);
/*设置当前进程的信号屏蔽字*/
if(sigprocmask(SIG_BLOCK, &new_set, &old_set)!=0){
printf("%s error!!!\n", __func__);
return;
}
printf("sleeping.......\n");
sleep(5);
if(sigpending(&pending_set)!=0){
printf("%s error!!!\n", __func__);
return;
}
if(sigismember(&pending_set, SIGINT))
printf("SIGINT is in pending_set\n");
if(sigismember(&pending_set, SIGQUIT))
printf("SIGQUIT is in pending_set\n");
/*将进程的信号屏蔽字回复到修改之前的状态*/
if(sigprocmask(SIG_SETMASK, &old_set, NULL)!=0){
printf("%s error!!!\n", __func__);
return;
}
printf("%s...exit....\n", __func__);
}
void main(int argc, char *argv[])
{
testPendingSignal();
while(1){
pause();
}
}
程序的执行结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
sleeping.......
^C^C^C^C^CSIGINT is in pending_set
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
因为我们在程序中设置了进程屏蔽字,所以进程会阻塞SIGINT信号。在sleep(5)的睡眠等待的过程中,我连续按了5次"Ctrl+C“产生了五次SIGINT信号。但是由于进程暂时屏蔽该信号,不会讲该信号递送给进程,因此程序也不会响应(SIGINT的默认动作是结束当前进程),而当我重新恢复进程的信号屏蔽字后,先前产生的SIGINT信号被立即递送到进程(应该是在sigprocmask函数返回之前就递送了
),因此函数testPendingSignal()的最后一行并没有打印出来。
为了解除对该信号的阻塞,我们用先前的信号屏蔽字重新设置了(SIG_SETMASK)进程的信号屏蔽字。
这样做的原因在于: 可能先前的进程已经屏蔽了该信号(例如其他函数接口设置了,但是自己并不知道)。如果我们简单的使用SIG_UNBLOCK来将信号从进程屏蔽字中删除可能会影响其他程序正常的功能。因此这里推荐使用SIG_SETMASK的方式重新设置修改之前信号屏蔽字。
8.4 函数sigaction
sigaction函数的功能是检查或者修改(也可以是检查和修改)与指定信号相关联的动作。 该函数是对signal函数的改进。
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sinaction *restrict oact);
返回值:成功返回0;失败返回-1;
其中:
- 参数signo是要检测或者修改其具体动作的信号编号。如果act指针非空,则要修改信号的动作;如果oact指针非空,则是通过oact指针将返回该信号的上一个处理动作(如果act为空,即未修改,就是当前信号的处理动作)。struct sigaction的结构如下(Linux形式上可能有所不同):
struct sigaction{
void (*sa_handler)(int); /*addr of signal handler*/
/*or SIG_IGN, or SIG_DFL*/
sigset_t sa_mask; /*adddtional signals to block*/
int sa_flags; /*signal options, 详细信息见下表*/
/*alternate handler*/
void (*sa_sigaction)(int, siginfo_t *, void *);
};
当设置信号动作时,如果sa_handler指针指向一个信号捕捉函数的地址(不是常量SIG_IGN、SIG_DEL), 则sa_mask字段说明了一个信号集。 在调用该信号捕捉函数时,该信号屏蔽字要添加到进程的信号屏蔽字中,当信号处理函数处理完毕后,在重新将进程的信号屏蔽字恢复为原先的值。这样做最主要的目的在于:防止两个相同的信号同时(间隔很短)到来时产生竞态。也就是说应该在处理该信号的过程中阻塞该类型的后续信号。
当然,这个函数也是不支持信号入队的:如果阻塞过程中发生了多个该信号,那么只会递送一次该信号。
- sa_flags字段指明了对信号处理的各个选项:
序号 | 选项 | 说明 |
---|---|---|
1 | SA_INTERRUPT | 由此信号中断的系统调用不自动重启(sigaction默认处理方式 ) |
2 | SA_NOCLDSTOP | 若signo为SIGCHLD, 当子进程停止时不产生此信号;当子进程终止时产生此信号 |
3 | SA_NOCLDWAIT | 若signo为SIGCHLD, 当子进程终止时不产生僵尸进程;若调用进程随后调用wait,则阻塞到它所有进程都终止。 |
4 | SA_NODEFER | 捕捉到此函数时,在执行信号捕捉函数时系统不自动阻塞该信号(除非sa_mask中包括此信号) |
5 | SA_ONSTACK | 递交给替换栈上的进程 |
6 | SA_RESETHAND | 在信号捕捉函数入口处将此信号的处理函数改为SIG_DFL, 并清除SA_SIGINFO标志 |
7 | SA_RESTART | 由此信号中断的系统调用自动重启 |
8 | SA_SIGINFO | 此选项对信号处理程序提供一个附加信息。 |
- sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用SA_SIGINFO标志时,使用该信号处理程序。对于sa_handler和sa_sigaction,实现上一般是一个共用体,因此应用只能一次使用他们中的一个。下面是Linux2.6.12上的实现,从这里可以看出_sa_sigaction和sa_handler是一个共用体:
struct sigaction {
union {
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int, struct siginfo *, void *);
} _u;
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
};
示例:使用sigaction实现signal
/*************************************************************************
> File Name: sigaction2signal.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月28日 星期二 10时30分48秒
************************************************************************/
#include <stdio.h>
#include <signal.h>
/*
* 我们使用sigaction函数来实现signal的功能
*/
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
if(sigaction(signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}
static void alarm_handler(int signo)
{
printf("Catch SIGALRM signal!!!\n");
}
void sigaction2signal_demo()
{
signal(SIGALRM, alarm_handler);
alarm(1);
sleep(2);
alarm(1);
}
void main(int argc, char *argv[])
{
//signal_demo();
//exec_funcs();
//signal_lost_test();
sigaction2signal_demo();
while(1){
pause();
}
}
执行结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
Catch SIGALRM signal!!!
Catch SIGALRM signal!!!
^C
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
8.5 函数sigsuspend
首先看sigsuspend的函数原型:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
返回值:-1
sigsuspend函数的使用方法在下面的示例中进行说明。·
示例1:错误用法
我们已经知道,修改进程的信号屏蔽字可以阻塞所选择的信号,或者解除对他们的阻塞。使用这种技术可以保护不希望被信号打断的临界代码区。如果希望对一个信号解除阻塞,然后调用pause以等待以前被阻塞的信号发生,发生什么事情呢? 我们依然使用程序来说明:
/*************************************************************************
> File Name: sigsuspend_demo.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月28日 星期二 10时58分19秒
************************************************************************/
#include <stdio.h>
#include <signal.h>
static void handler(int signo)
{
printf("Catch SIGINT!!!\n");
}
void invalid_usages()
{
sigset_t newmask, oldmask;
sigfillset(&newmask);//阻塞所有信号
sigemptyset(&oldmask);
//sigaddset(&newmask, SIGINT);
signal(SIGINT, handler);
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask)<0){
printf("sigprocmask SIG_BLOCK error\n");
return;
}
/*临界代码区:critical region of code begin*/
sleep(2);
/*此区域的代码不会被信号打断*/
/*临界代码区:critical region of code end*/
/*解除对SIGINT的阻塞*/
if(sigprocmask(SIG_SETMASK, &oldmask, NULL)<0){
printf("sigprocmask SIG_SETMASK error\n");
return;
}
printf("Before pause\n");
pause();
printf("After pause\n");
}
void main(int argc, char *argv[])
{
//signal_demo();
//exec_funcs();
//signal_lost_test();
invalid_usages();
}
我在程序执行到sleep(2);
时连续按下“Ctrl+C”, 这里会被阻塞,没有什么问题。2秒过后,进程屏蔽字解除信号阻塞,然后调用pause()
等待信号的到来。那么问题来了,信号是在sigprocmask调用返回之前就递送到进程的,因此paus()是捕捉不到SIGINT信号的,之后程序会一直阻塞,直到下一个信号的到来(后面我又重新按下“Ctrl+C“退出程序的)。结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
^C^C^C^C^CCatch SIGINT!!!
Before pause
^CCatch SIGINT!!!
After pause
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
针对上述的问题,系统实现了一个原子操作:解除阻塞信号并等待。
之前,我对于这个sigsuspend()函数的功能总是不得要领,不明白它的真正目的。现在有一点清楚了: suspend()函数的出现就是为了解决上述的问题。它的作用包括两部分:
- 修改当前的进程屏蔽字(将其修改为sigmask)
- 阻塞当前进程
-
非sigmask中的信号到来时:
- 首先执行信号的处理函数
- 然后恢复进程的信号屏蔽字(调用suspend之前的信号屏蔽字)
- 最后进程继续运行
上述这几步都是suspend函数自动完成的。
示例2:suspend的用法
void sigsuspend_demo()
{
sigset_t newmask, oldmask, waitmask;
sigfillset(&newmask);//阻塞所有信号
sigemptyset(&oldmask);
sigfillset(&waitmask);//等待所有信号
sigdelset(&waitmask, SIGINT);//等待除了SIGINT之外的所有信号
signal(SIGINT, handler);
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask)<0){
printf("sigprocmask SIG_BLOCK error\n");
return;
}
if(sigsuspend(&waitmask) < 0){
printf("sigsuspend error\n");
return;
}
/*临界代码区:critical region of code begin*/
sleep(2);
/*此区域的代码不会被SIGINT信号打断*/
/*临界代码区:critical region of code end*/
/*阻塞等待SIGINT信号*/
if(sigsuspend(&waitmask) < 0){
printf("sigsuspend error\n");
return;
}
/*
....
*/
}