Unix环境高级编程(第三版)
第10章 信号
信号是一种软中断。很多比较重要的应用程序都需要处理信号。信号提供了一种异步处理事件的方法,例如:终端用户输入中断键,会通过信号机制终止一个程序等。早期的信号存在丢失的风险,且执行在临界代码区时无法关闭所选择的信号,后来一些系统便增加了可靠信号机制。下面的章节提供详细的说明。
首先,每一个信号都有一个名字。这些名字都是以"SIG"开头的。Linux支持31种基本信号,不同的操作系统可能支持的信号数量略有不同。信号是在头文件
toney@ubantu:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不存在编号为0的信号。在后面的章节中会说明编号为0的信号的特殊用途。
产生信号的条件有很多:
信号是异步事件的典型示例。产生信号的事件对进程而言是随机出现的。进程不能通过测试一个简单的变量(如errno)来判断是否有信号发生,而是应该告诉内核:“当此信号发生时,应该执行如下操作”。这里一共有三种方式可供选择:
(1)忽略此信号。
(2)捕捉此信号。
(3)执行系统默认操作。
首先来说忽略信号的用法。大多数的信号都可以使用这种方式来处理信号,但是有两种信号是绝不能被忽略的,它们分别是SIGKILL和SIGSTOP信号。这有两种信号不能被忽略的原因是:它们向内核和用户提供了使进程终止或者停止的可靠方法。此外,如果忽略某些由硬件产生的信号(例如SIGSEG信号),会导致软件出现无法预料的问题。
为了实现捕捉信号的目的,我们必须通知内核在某种信号发生时,调用一个用户函数。在用户函数中,我们可以执行我们希望对该信号的处理方式。例如我们可以捕捉SIGALRM信号,当定时时间到时打印某些提示信息等。注意:不能捕捉SIGKILL和SIGSTOP信号。
对于大多数信号的系统默认操作都是终止该进程。
下表中列出了31中信号编号、信号名称,Linux系统的默认操作,并对其中常见或者常用到的信号做了一个简单的说明。如果以后用到再做详细补充说明。
序号
信号名称
说明
默认操作
1
SIGHUP
暂不介绍
terminate
2
SIGINT
当用户按中断键(一般是Ctrl+C或Delete键)时,驱动程序会产生此信号来终止进程。
terminate
3
SIGQUIT
当用户按退出键(一般是Ctrl+)时,中断驱动程序会产生该信号,发送给所有前台进程。
coredump
4
SIGILL
该信号表示已经执行一条非法硬件指令。
coredump
5
SIGTRAP
指示一个实现的硬件故障。
coredump
6
SIGABRT
调用abort()函数来终止进程时会产生该信号。
coredump
7
SIGBUS
指示一个已定义的硬件故障
coredump
8
SIGFPE
表示算数运算异常。例如除0操作,浮点溢出等。
coredump
9
SIGKILL
无法被忽略和捕捉的信号。它向系统提供一种可以杀死任意进程的可靠方法。
terminate
0
SIGUSR1
用户定义的信号,可用于应用程序
terminate
11
SIGSEGV
无效的内存访问。例如经典的“段错误”。
coredump
12
SIGUSR2
用户定义的另一个信号,可用于应用程序
terminate
13
SIGPIPE
管道的读进程已经终止时写管道会产生该信号。
terminate
14
SIGALRM
当使用alarm()函数,或者setitimer()设置的定时时间到时会产生此信号
terminate
15
SIGTERM
是由kill(1)命令发送的系统默认终止信号。该信号可被应用程序捕获,从而进行清理工作,完成优雅的终止(相对于SIGKILL而言,SIGKILL信号不能被捕获或者忽略)。
terminate
16
SIGSTKFLT
暂不介绍
17
SIGCHLD
一个进程终止或者停止时,SIGCHLD信号会发送给其父进程。按系统默认,将忽略此信号。如果父进程需要被告知该子进程退出状态,则需要捕捉此信号。一般在信号处理函数中调用wait()函数回收子进程的资源。
ignore
18
SIGCONT
作业控制信号。它用来发送给需要继续运行,但当前处于停止状态的进程。收到此信号后,挂起的进程继续运行。如果本来已经在运行则忽略该信号。
ignore
19
SIGSTOP
作业控制信号,它停止一个进程。不能被捕捉或忽略。
stop
20
SIGTSTP
交互停止信号。当用户按挂起键(一般是Ctrl+z)时,中断驱动程序产生此信号。
stop
21
SIGTTIN
暂不介绍
stop
22
SIGTTOU
暂不介绍
stop
23
SIGURG
暂不介绍
ignore
24
SIGXCPU
暂不介绍
coredump
25
SIGXFSZ
暂不介绍
coredump
26
SIGVTALRM
暂不介绍
terminate
27
SIGPROF
暂不介绍
terminate
28
SIGWINCH
暂不介绍
ignore
29
SIGIO
暂不介绍
terminate
30
SIGPWR
暂不介绍
terminate
31
SIGUNUSED / SIGSYS
一个无效的系统调用
coredump
Unix系统信号机制最简单的接口是signal函数。
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
返回值:若成功,返回之前的信号处理配置;若失败,返回SIG_ERR.
代码中:
signo是指信号名称(详情参见2.4常见的信号)
func是常量SIG_IGN、SIG_DFL或者接收到信号时自定义的信号处理函数地址。
说句心里话,signal的函数原型看起来有点看不懂:( 。下面我们也按前辈先人的说法再熟悉下(原话):
本节开头所示的signal函数原型太复杂了,如果使用下面的typedef,则可以使其简单些。
typedef void Sigfunc(int); (3-1)
然后,可将signal函数原型写成:
Sigfunc *signal(int, Sigfunc *); (3-2)
这样,signal的函数看起来就简单了很多:signal函数要求两个参数,并返回一个函数指针(如3-2所示),而该函数指针指向的函数有一个整型参数且无返回值(如3-1所示)。
用通俗一点的话描述:定义一个信号处理函数,它有一个整型参数signo, 无返回值;当调用signal函数设置信号处理程序时,signal函数的第二个参数是指向该信号处理函数的指针,signal函数的返回值是指向未修改之前的信号处理函数指针。
在上述的描述中,我们提到了三个宏定义: SIG_IGN、SIG_DFL、SIG_ERR。这三个宏Linux上的原型如下:
typedef void __sighandler_t(int);
#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__sighandler_t)-1) /* error return from signal */
这三个常量可用于表示“指向函数的指针”。
该实例中定义了两个信号处理函数,捕获了三个信号(SIGUSR1, SIGUSR2共用一个信号处理函数)。
/*************************************************************************
> File Name: signal_demo.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月27日 星期一 11时50分47秒
************************************************************************/
#include <stdio.h>
#include <signal.h>
static void sig_handler(int); /*自定义的信号处理函数*/
static void sig_usr(int); /*自定义的信号处理函数*/
int signal_install()
{
if(signal(SIGINT, sig_handler)==SIG_ERR){
printf("SIGINT handle function register error\n");
}
if(signal(SIGUSR1, sig_usr)==SIG_ERR){
printf("SIGUSR1 handle function register error\n");
}
if(signal(SIGUSR2, sig_usr)==SIG_ERR){
printf("SIGUSR2 handle function register error\n");
}
}
void sig_handler(int signo)
{
if(signo == SIGINT){
printf("Recieved SIGINT signal\n");
}else{
printf("sig_handler receieve Error signal\n");
}
}
void sig_usr(int signo)
{
if(signo == SIGUSR1){
printf("Recieved SIGUSR1 signal\n");
}else if(signo == SIGUSR2){
printf("Recieved SIGUSR2 signal\n");
}else{
printf("sig_usr receieve Error signal\n");
}
}
void main(int argc, char *argv[])
{
//signal_demo();
//exec_funcs();
signal_install();
while(1){
pause();
}
}
结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
^CRecieved SIGINT signal
^CRecieved SIGINT signal
^CRecieved SIGINT signal
^Z
[3]+ Stopped ./demo.out
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out &
[6] 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ kill -USR2 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ Recieved SIGUSR2 signal
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ kill -USR1 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ Recieved SIGUSR1 signal
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
如果想使用signal函数来获取当前进程对某一信号的处理方式,会修改当前的处理方式,否则无法确定当前的处理方式。常见的用法如下:
if(signal(SIGINT, SIG_IGN)!=SIG_IGN)
signal(SIGINT, sig_handler);
if(signal(SIGUSR1, SIG_IGN)!=SIG_IGN)
signal(SIGINT, sig_usr);
后面我们将使用另一种信号处理方式:sigaction()函数,此函数无需修改便可以查询当前的处理方式。
进程创建
当一个进程调用fork时,其子进程继承了父进程的信号处理方式。因为子进程在创建时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有效的。
在早期的Unix版本中,信号是不可靠的。这里的不可靠指的是:信号可能会丢失(一个信号已经发生了,但是该进程却不知道这一点)。除此之外,进程对信号的控制能力也特别差,它只能捕捉或者忽略信号。但是有时用户希望通知内核阻塞某个信号,不要忽略该信号;而在进程准备好处理该信号时在由内核重新通知该进程。
某些书籍提到signal函数每触发一次,得重新调用signal重新注册安装信号处理函数。这已经很很久以前的了,现在是一次signal注册,之后多次使用,无需每次在信号处理函数中重新调用signal函数(Linux便是如此)
正常情况下,信号的发生频率很低,因此信号丢失的情况比较少。但是如果信号发生的频率比较高,且信号处理函数费时的话就很容易发生信号丢失的情形。下面我们通过在信号处理函数中调用延时函数模拟实现:
/*************************************************************************
> File Name: signal_lost.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月27日 星期一 15时03分18秒
************************************************************************/
#include<stdio.h>
#include<signal.h>
extern void my_msleep(int mseconds);//自己实现的睡眠函数,与库函数并无区别
static void sig_handler(int signo)
{
static int flag=0;
printf("sig_handler finish: flag=%d\n", flag);
if(signo == SIGINT){
printf("Recieved SIGINT signal\n");
flag++;
my_msleep(5000);//延时5秒
}else{
printf("sig_handler receieve Error signal\n");
}
printf("sig_handler finish: flag=%d\n", flag);
}
int signal_lost_test()
{
if(signal(SIGINT, sig_handler)==SIG_ERR){
printf("SIGINT handle function register error\n");
}
}
执行结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
^Csig_handler finish: flag=0
Recieved SIGINT signal
^C^C^Csig_handler finish: flag=1
sig_handler finish: flag=1
Recieved SIGINT signal
sig_handler finish: flag=2
运行程序后我是通过连续按下四次“Ctrl+C”,产生四个SIGINT信号,但实际上只捕获了其中的两个信号,另外两个则**丢失**了。之所以说丢失,是因为程序只打印了两次信号处理函数中的信息,该进程也只知道发生了两次事件。出现这种情形的主要原因在于信号处理程序处理的太慢(程序中我们把她睡了会儿),而信号又发生的太频繁,CPU处理不过来导致的。
此外,上述结果中还有个奇怪的现象:信号处理程序不是一次性执行完毕的(专业点称为存在竞态),而是在第一个执行过程中又去相应下一个信号处理函数,等下一个处理完毕了再回来继续处理先前未执行完毕的信号处理函数。这个现像应该在编写程序中特别注意下。
在第4部分,我们简要的说明了下什么是不可靠的信号,这里我们再来简要的说明下什么是可靠的信号。
进程可以选择使用“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且对信号的动作是系统默认动作或捕捉该信号(非忽略状态),则为该进程将此信号保持为未决状态,直到该进程对此信号接触阻塞状态(或者修改为忽略此信号)。
内核在递送一个原来被阻塞的信号给进程时,才决定对它的处理方式(因此之前的状态称为未决的)。于是乎进程在信号递送之前是可以修改对该信号的动作。系统可以调用sigpending函数来判断哪些信号是设置为阻塞同时悬而未决的。
如果进程在解除多某个信号阻塞之前,该信号已经发生了多次(就是我们4.2的例子),那么该如何处理呢?目前大多数系统仍然是只递送一次该信号
,也就是说不支持信号排队。
每一个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每一种可能的信号,该屏蔽字中都有与之相对应的位。如果该位被设置,则当前进程会阻塞该信号。程序中可以使用sigprocmask(后面我会详细说明)来查询和设置当前进程的信号屏蔽字。
信号编号可能会超过一个整数所包含的二进制位数(32位系统是32位),因此POSIX.1专门定义了一个新的类型sigset_t,它可以容纳一个信号集。信号屏蔽字就存放在其中一个信号集中。后面我们对这部分进行详细说明。
kill函数用来将信号发送给进程或者进程组。
raise函数则是进程用来向本进程发送信号的。
#include
int kill(pid_t pid, int signo);
int raise(int signo);
返回值说明:成功返回0;失败返回-1
调用
raise(signo);
相当于调用
kill(getpid(), signo);
函数kill的pid参数有以下四种不同的情况
序号
pid范围
说明
1
pid > 0
将信号发送给进程ID为pid的进程
2
pid == 0
将信号发送给与当前进程属于同一进程组的所有进程(进程组ID相同的进程),当然这里不包括系统进程和内核进程
3
pid < 0
将信号发送给进程组ID等于|pid|的所有进程。同样不包括系统进程和内核进程
4
pid == -1
将信号发送给具有权限的其他所有进程
这里面有一个前提: 要么是超级用户,拥有所有的权限。要么是拥有相同进程ID的进程,否则无法发送信号给其他进程。
使用alarm函数用来设置一个定时器,在将来的某一时间该定时器会超时。当定时器超时时,产生SIGALRM信号。如果忽略或者不捕捉该信号,则执行默认的动作:终止当前进程
。
#include <signal.h>
unsigned int alarm(unsigned int seconds);
返回值说明: 0或者以前设置的闹钟时间剩余的秒数。
参数seconds的值是产生SIGALRM信号需要经过的秒数。当定时时间到时由内核产生,但是由于进程调度的延时,时间上有一定的延时。
每一个进程只允许有一个闹钟时间:
/*************************************************************************
> File Name: alarm.c
> Author: Toney Sun
> Mail: vip_13031075266@163.com
> Created Time: 2020年04月27日 星期一 17时35分42秒
************************************************************************/
#include <stdio.h>
#include <signal.h>
void alarm_test(void)
{
int ret;
ret=alarm(10);
printf("First alarm :ret = %d\n", ret);
sleep(2);
ret=alarm(2);
printf("Second alarm :ret = %d\n", ret);
}
验证结果如下:
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out
First alarm :ret = 0
Second alarm :ret = 8
Alarm clock
这会引入一个问题: 如果先前该进程已经注册了一个闹钟时间,但是还没有超时;如果重新设置定时器,先前剩余的时间会最为alarm()函数的返回值返回,与此同时,以前注册的闹钟时间被取代而不再生效。 如果有以前注册的尚未超时的闹钟时间,而且本地设置的seconds为0,则是取消该闹钟,剩余的时间仍然作为alarm函数的返回值。
pause函数使调用进程挂起,直至捕捉到某个信号。
#include <signal.h>
int pause(void);
返回值:-1, errno设置为EINTR
只有执行了一个信号处理程序并从其返回,pause函数才会返回。它的返回值一直为-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或者sigfillset对信号集进行初始化。一旦初始化了一个信号集,以后便可以对信号集进行增加、删除特定的信号:
对所有以信号集为参数的函数,总是以信号集的地址作为传递的参数。
这个原因在于:因为我们要在函数中修改信号集中的信号,因此需要地址传递方式(简单的值传递是行不通的),否则无法实现真正的修改。
我们在前面已经提到过:进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。进程的信号屏蔽字是通过函数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__);
}
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的方式重新设置修改之前信号屏蔽字。
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字段说明了一个信号集。 在调用该信号捕捉函数时,该信号屏蔽字要添加到进程的信号屏蔽字中,当信号处理函数处理完毕后,在重新将进程的信号屏蔽字恢复为原先的值。这样做最主要的目的在于:防止两个相同的信号同时(间隔很短)到来时产生竞态。也就是说应该在处理该信号的过程中阻塞该类型的后续信号。
当然,这个函数也是不支持信号入队的:如果阻塞过程中发生了多个该信号,那么只会递送一次该信号。
序号
选项
说明
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);
};
/*************************************************************************
> 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调度器$
首先看sigsuspend的函数原型:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
返回值:-1
sigsuspend函数的使用方法在下面的示例中进行说明。·
我们已经知道,修改进程的信号屏蔽字可以阻塞所选择的信号,或者解除对他们的阻塞。使用这种技术可以保护不希望被信号打断的临界代码区。如果希望对一个信号解除阻塞,然后调用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()函数的出现就是为了解决上述的问题。它的作用包括两部分:
上述这几步都是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;
}
/*
....
*/
}
手机扫一扫
移动阅读更方便
你可能感兴趣的文章