进程信号
信号时一种从软件层面上对中断的模拟,很多重要的程序都需要处理信号,信号提供了一种处理异步事件的方法。比如,用户在终端按下
ctrl C
会终止一个进程,或者通过kill
命令来给特定的进程发送信号。
信号基本概念
每个信号都有一个名字,这些名字以SIG
开头,在头文件 <signal.h>
中,这些信号名被定义为正整数常量(信号编号)。我们可以通过 kill -l
查看系统定义的信号列表。如下:
如上:ctrl C
对应 SIGINT
信号,信号编号是2。
产生信号的条件:
- 当用户按下某些终端按键时,会产生终端信号。如上面提到的
ctrl C
; - 硬件异常产生信号:如除 0 异常, 无效的地址访问(对NULL指针解引用、内存越界访问),这些信号通常有计算机硬件检测到,并通知操作系统内核,内核再给触发异常的进程发送合适的信号,如:对无效内存的解引用的进程将受到
SIGSEGV
信号; - 通过系统调用函数:
int kill(pid_t id, int sig);
,下面是一个例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
int id = fork();
int count = 0;
if(id < 0){
perror("fork");
return -1;
}else if(id == 0){//child
for(;;){
printf("%d\n", count++);
usleep(1000*200);
}
}else{//father
sleep(3);
kill(id, SIGKILL);
wait(0);
}
return 0;
}
上面这段代码中,子进程做死循环打印动作,父进程睡眠 3 秒后给子进程发送信号,将其终止。
- 命令行中通过
kill [opt] <pid>
命令给指定进程发送 指定命令; - 当检测到某些软件条件产生时,应将其通知有关进程并产生信号。比如:
SIGALRM
(进程设置的定时器超时),SIGPIPE
(管道的读进程终止后,一个进程写管道)。
信号的处理方式:
- 忽略此信号。有两种信号不能忽略,它们是
SIGKILL
和SIGSTOP
,原因是:它们像内核和超级用户提供了是进程终止的可靠方法。 - 扑捉信号。通知内核在某种信号发生时,调用一个用户函数,在该函数中,执行用户希望对该信号处理的动作。如 一个子进程终止时,会想其父进程发送
SIGCHLD
信号,所以我们可以自定义信号的捕捉函数,在该函数中调用waitpid
以获取子进程的 ID 和退出状态; - 执行默认动作,大多数系统默认动作是终止该进程。
信号的底层机制
我们来看看,当我们在键盘上敲下 ctrl C
时操作系统都发生了什么。
可以看到,对某一个进程发送信号实际上是,将该进程 PCB 的某个字段设置一个值,那么这一点在 PCB 中更是怎样展现的呢?看下面这张图:
执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
可以将信号在 PCB 中的标识看为上图中的三张表:
- block 又称为信号屏蔽状态字,它是个 64bit 的位图,每个位代表一个信号,对应位为1,则表示信号被阻塞;
- pending 又称为信号未决状态字, 它也是个 64bit 的位图,每个位代表一个信号,对应位为1代表未决,0代表信号可以递达了;
- handler 表中记录了每种信号的处理方式,对于用户自定义的处理函数,该表对应位上为信号处理函数的地址。
注意:
- 比如向进程发送进程
SIGINT
信号,内核首先判断信号屏蔽状态字是否阻塞,如果该信号被设为为了阻塞的,那么信号未决状态字(pending)相应位制成1;若该信号阻塞解除,信号未决状态字(pending)相应位制成0;表示信号此时可以抵达了,也就是可以接收该信号了; - 信号设计的机制是:用户可以读写信号屏蔽状态字 ,但只能读信号未决状态字。
信号的捕捉:
信号的处理动作为用户自定义函数,在信号递达是就调用该函数,这个动作称为信号的捕捉。用户自定义的信号处理函数在用户空间内,所以其相应的处理就要涉及到 “用户态——内核态” 之间的相互切换,过程比较复杂。比如用户注册了 SIGINT
信号的处理函数 myHandler()
,那么当进程发生中断或者异常后,就切换到内核态,在处理完中断异常,在返回用户态之前,内核检查到有信号 SIGINT
递达,那么内核就返回至用户态执行 myHandler()
信号处理函数(并没有返回到中断是的上下文),main
函数和信号吹函数 myHandler()
不存在调用与被调用的关系,它们是两个独立的控制流。在执行完 myHandler()
后,通过特殊的系统调用 sigreturn
返回值内核态。再次检查是否有信号递达,如果没有,这次就返回至原来终端处上下文开始执行。这整个过程对应着下图:
信号相关 API 函数
- signal
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
该函数看上去优点复杂,我们来分解分析它:
- 函数名:signal
- 参数:int signo, void (*func)(int),可以看到第二个参数是一个函数指针,它有三种取值:
SIG_IGN
>代表忽略该信号;SIGDFL
>代表采取默认处理动作;用户自定义信号处理函数的地址>采取用户的处理动作。- 返回值: void (*)(int),函数的返回值是之前该信号的处理函数的指针。
下面通过例子来展示该函数的功能:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void myHandler(int sig)
{
printf("sig: %d\n", sig);
}
int main()
{
signal(SIGINT, myHandler);
while(1){
}
return 0;
}
让该程序跑起来,并在终端按下 ctrl C
:
此时我们发现 ctrl C
无法终止该进程,原因是:进程执行了我们自定义的对 SIGINT
信号的处理函数,而在我们自定义的函数中执行的代码是打印信号编号。
- 信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);//将信号集清空,共64bits
int sigfillset(sigset_t *set);//将信号集置1
int sigaddset(sigset_t *set, int signum);//将signum对应的位置为1
int sigdelset(sigset_t *set, int signum);//将signum对应的位置为0
int sigismember(const sigset_t *set, int signum);//判断signum是否在该信号集合中,如果集合中该位为1,则返回1,表示位于在集合中
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);读取和更改屏蔽状态字
int sigpending(sigset_t *set);
-
sigemptyset()
函数初始化 set 所指向的信号集,将所有信号对应的 bit 位设置为 0,表示该信号集不包含任何有效信号; - 与第一个函数类似,将所有信号对应的 bit 位设置为 1;
-
sigaddset
和sigdelset
函数往信号集中添加或删除某种有效信号; -
sigprocmask
用于读取和更改信号屏蔽字(即:阻塞信号集)。若果
oldset
非空,则读取当前信号屏蔽字并通过该参数传出,如果set
非空,则更改进程的信号屏蔽字,how
参数指示如何更改。如果oldset
和set
都非空,则将原来的信号屏蔽字备份到oldset
中,然后通过how
指示的方式更改信号屏蔽字。how
参数的可选值如下:
选项 | 描述 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask = mask | set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除的信号,相当于 mask = mask & ~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于 mask = set |
-
sigpending
用于读取当前进程的未决信号集,通过set
参数传出。
上面的函数除了 sigismember
之外的,它们的执行成功返回 0,出错返回 -1,而 sigismember
判断一个信号集中是否包含某信号,包含返回 1,不包含返回 0, 出错返回 -1。
下面是一个例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void print_pending(sigset_t *p)
{
int i = 1;
for(; i <= 32; ++i){
if(sigismember(p, i))
putchar('1');
else
putchar('0');
}
putchar('\n');
}
int main()
{
sigset_t s,p;
sigemptyset(&s);
sigemptyset(&p);
sigaddset(&s, SIGINT); // ctrl C
sigprocmask(SIG_BLOCK, &s, NULL); //阻塞 SIGINT(2号)信号
while(1){
sigpending(&p);//获取阻塞信号集
print_pending(&p);//打印阻塞信号集
sleep(1);
}
return 0;
}
当程序跑起来后,每隔一秒打印一次阻塞信号集,当我们在终端按下 ctrl C
时,可以看到打印出的阻塞信号集发生了变换,2 号信号被阻塞。
sigaction
在前面我们学习了信号的捕捉,我们可以使用 signal
注册信号处理函数。但 signal
函数有很多缺陷,现今很多地方已经不用它,下面介绍另一种信号处理函数 sigaction
:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
函数调用成功返回 0, 出错返回 -1, signo是要处理的信号的编号,struct sigaction
是个结构体:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler:原型是一个参数为int,返回类型为void的函数指针。参数即为信号值,所以信号不能传递除信号值之外的任何信息。
- sa_sigaction:原型是一个带三个参数,类型分别为int,struct siginfo ,void ,返回类型为void的函数指针。第一个参数为信号值;第二个参数是一个指向struct siginfo结构的指针,此结构中包含信号携带的数据值;第三个参数没有使用, 若果将
sa_sigaction
设置为SIGIGN
则表示忽略此信号, 设置为SIGDFL
表示采用默认信号处理方式。- sa_mask:指定在信号处理程序执行过程中,哪些信号应当被阻塞。默认当前信号本身被阻塞。
注意:sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。- sa_flags:包含了许多标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中。即sa_sigaction指定信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。
下面是一个该函数的使用实例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void myHandler(int sig)
{
printf("sig: %d\n", sig);
}
int main()
{
struct sigaction newH, oldH;
newH.sa_handler = myHandler;//注册信号处理函数
sigemptyset(&newH.sa_mask);//清空信号屏蔽字
newH.sa_flags = 0;
sigaction(SIGINT, &newH, &oldH);
while(1){
}
return 0;
}
运行结果和我们开始测试的另一个函数一样:
那么signal
和 sigaction
有什么区别呢?
- signal在调用handler之前先把信号的handler指针恢复;sigaction调用之后不会恢复handler指针,直到再次调用sigaction修改handler指针。这样,signal就会丢失信号,而且不能处理重复的信号,而sigaction就可以。因为signal在得到信号和调用handler之间有个时间把handler恢复了,这样再次接收到此信号就会执行默认的handler。(虽然有些调用,在handler的以开头再次置handler,这样只能保证丢信号的概率降低,但是不能保证所有的信号都能正确处理)
- signal在调用过程不支持信号block;sigaction调用后在handler调用之前会把屏蔽信号(屏蔽信号中自动默认包含传送的该信号)加入信号中,handler调用后会自动恢复信号到原先的值。signal处理过程中就不能提供阻塞某些信号的功能,sigaction就可以阻指定的信号和本身处理的信号,直到handler处理结束。这样就可以阻塞本身处理的信号,到handler结束就可以再次接受重复的信号。
自定义睡眠函数和竞态条件
pause
函数
#include <unistd.h>
int pause(void);
pause 使进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,此时 pause 不返回,如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回,如果信号的处理动作是捕捉,则在调用了信号处理函数后pause返回-1,errno设为EINTR。
alarm
函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm
也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM
信号。可以设置忽略或者不捕获此信号,如果采用默认方式其动作是终止调用该alarm函数的进程。如果调用 alarm
前已经设置了闹钟,则返回剩余时间,否则返回 0;出错返回 -1。
mySleep
函数
下面是我们实现的 mySleep
函数的思路:
- 注册
SIGALRM
的处理函数,该函数什么也不干;- 调用
alarm
设定闹钟;- 调用
pause
等待;alarm
设定的时间到,内核发送SIGALRM
信号给该进程;- 进程从内核切换到用户态之前发现有未决信号,则调用用户注册的
SIGALRM
信号处理函数;- 进入
SIGALRM
处理函数前SIGALRM
信号被屏蔽, 执行完该函数又解除对SIGALRM
信号的屏蔽;- 通过系统调用
sigreturn
返回内核态,再次检查未决信号表,如果此时没有未决信号,则返回到用户态,接着主控制流程执行。pause
函数返回-1,调用alarm(0)
取消闹钟,恢复SIGALRM
信号的处理动作。
下面是代码:
void myHandler(int sig){
(void) sig;
}
unsigned int mySleep(unsigned int scon)
{
struct sigaction s, oldHandler;
unsigned int ret;
//s.sa_handler = myHandler;
sigemptyset(&s.sa_mask);
s.sa_flags = 0;
sigaction(SIGALRM, &s, &oldHandler);//注册信号处理函数
alarm(scon);//调用alarm设定闹钟
pause();//调用pause等待
ret = alarm(0);//取消闹钟
sigaction(SIGALRM, &oldHandler, NULL);//恢复之前的信号处理函数
return ret;
}
我们在main
函数中调用一下它:
int main()
while(1){
mySleep(2);
printf("hello world\n");
}
return 0;
}
运行结果每隔两秒打印一次。
几个问题:
信号
SIGALRM
处理函数myHandler
什么都没干,为什么还要注册它,不注册行不行?
信号SGALRM
的默认处理动作是终止调用它的进程,如果不注册它的处理函数的话,当我们设定的时间一到,则整个进程就会终止掉,这大大超出了我们的预期;为什么在
mySleep
函数返回前要恢复SIGALRM
信号的处理动作?mySleep
函数应该应该仅仅完成睡眠对应时间的动作,而不应该带副作用的,而如果不恢复SIGALRM
信号的处理动作,则等于在函数内部修改了SIGALRM
信号的处理动作,更严重的后果是,在该程序的接下来的代码中,如果调用alarm
函数,则不会得到期望的结果。mySleep
函数的返回值代表什么?
先看看alarm
函数的返回值含义:如果调用alarm
前已经设置了闹钟,则返回剩余时间,否则返回 0;出错返回 -1。 稍加分析,我们就可以知道,若进程挂起到参数所指定的时间则返回0,若有信号中断则返回剩余秒数。
重新审视一下我们的 mySleep
,如果在第一次调用 alarm
之后,cpu还没有来得及调用 pause
, 该进程就被切换出去(),cpu 去执行优先更高的进程,而这些优先级更高的进程执行的时间可能超出我们设定的闹钟时间,所以当 alarm
超时后,内核发送 SIGALRM
信号给该进程,等到其它优先级更高的进程执行完后,内核在切换到本进程之前,检测到有未递达的未觉信号,于是去处理该信号,处理完成之后,才执行 pause
函数,可是此时,alarm
函数已经执行完,并且发送给本进程的 SIGALRM
信号已经被处理,pause
函数还在等什么呢?
由于异步时间任何时候都有可能发生,像上面这种由于程序时序产生的问题叫做竞态条件。
如何解决上面的问题呢?一种好的方法是,将 “调用alarm设定闹钟” 和 “调用pause挂起等待” 这两步合并为一个原子操作,而 sigsuspend
函数正提供了这种功能。下面是它的原型:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
该函数没有成功返回值,只有在执行了信号处理函数后,才返回 -1, 并将errno设置为:ENITR
。调用 sigsuspend
函数后,该函数先将进程信号屏蔽字设为 sigmask
的所指定的值,然后挂起等待,当有信号递达,该函数返回时,再回复指定信号原来的屏蔽字。
下面是重新实现的 mySleep
:
unsigned int mysleep(unsigned int scon)
{
int ret = 0;
struct sigaction newH, oldH;
sigset_t newMask, oldMask, susMask;
newH.sa_handler = myHandler;
sigemptyset(&newH.sa_mask);
newH.sa_flags = 0;
sigaction(SIGALRM, &newH, &oldH);
sigemptyset(&newMask);
sigaddset(&newMask, SIGALRM);
sigprocmask(SIG_BLOCK, &newMask, &oldMask);// 屏蔽SIGALRM
alarm(scon);//睡眠等待
susMask = oldMask;
sigdelset(&susMask, SIGALRM);
//将信号屏蔽字设置为susMask并调用pause
sigsuspend(&susMask);
//sigsuspend返回时再回复屏蔽字,即又屏蔽SIGALRM
ret = alarm(0);
//用户主动还原最开始的信号屏蔽字
sigaction(SIGALRM, &oldH, NULL);
sigprocmask(SIG_SETMASK, &oldMask, NULL);
return ret;
}
调用结果:和我们预期一样。
借助SIGCHLD信号处理僵尸进程
为了处理僵尸进程,我们需要再父进程中调用 wait
或 waitpid
来等待子进程,获取子进程的退出状态,这种方法要么父进程阻塞式等待,要么轮巡视等待,即没有效率上的优势,轮训的代码实现也较复杂。
实际上,在子进程退出时,会给其父进程发送一个 SIGCHLD
信号,我们可以自己注册该信号的处理函数,在信号处理函数中做一些善后的事情。代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void myHandler(int sig)
{
(void) sig;
pid_t id;
while((id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait success: %d\n", id);
}
printf("child quit: %d\n", getpid());
}
int main()
{
signal(SIGCHLD, myHandler);
int id = fork();
if(id < 0){
perror("fork");
return -1;
}else if(id == 0){//child
printf("child start...\n");
sleep(3);
printf("child finish...\n");
exit(0);
}
while(1){
printf("father runing!\n");
sleep(1);
}
return 0;
}
运行结果:
—— 完!
【作者:果冻:http://blog.csdn.net/jelly_9】
上一篇: Java 中的“==”和equals()方法区别 java==equals
下一篇: 进程间通信介绍