前面的两篇文章主要讲了信号的发送和信号的内核处理,本文将介绍目标进程对信号的接收。
信号的接收
默认操作
很多信号都有默认的处理方式,也就是说,如果目标进程不注册(install)某个信号的处理函数,那么当它收到这个信号后,就会执行信号默认的操作。
有些信号天生存在感就很弱,比如表示子进程终止的SIGCHLD,它们的默认行为就是被进程忽略,除非进程注册处理函数来修改信号的默认行为,否则这些信号发了就跟没发一下,目标进程根本感觉不到。
当然,大部分信号都不是等闲之辈,比如SIGSTOP的默认操作就是停止进程,SIGTERM的默认操作就是终止进程。SIGSTOP只是要求进程暂时停下手头的工作,休息一下,直到听到SIGCONT的召唤,SIGTERM是直接要求进程结束生命啊。
执行信号
不过,目标进程可以通过设置SIGTERM的信号处理函数来改变信号的默认行为,这样进程在终止时,可以先执行一些清理操作,比如断开网络,或删除临时文件。你可以理解为这是“白绫赐死”吧,在临死之前,还可以嘱咐交代一下,说些临终遗言什么的。
在使用kill()发送信号的时代,信号的处理函数是由signal()来注册的。自从有了sigqueue(),我们就开始使用它的黄金搭档sigaction()了。
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction()在signal()的基础上进行了扩展,提供了sigaction结构体(对,它们俩的名字是一样的),可以实现对信号执行更精确的控制。
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
...
};
其中"sa_handler"指向注册的信号处理函数,同signal()中的"handler"是一样的。"sa_flags"用于标识信号处理的特性,比如SA_ONESHOT就是表示信号处理函数注册后只被执行一次,执行完后信号又恢复默认行为。至于"sa_mask",则是和接下来要讲到的信号屏蔽有关。
sigaction()中第二个参数"act"和第三个参数"oldact"都是指向struct sigaction的指针,但它们的用途是不相同的。"act"用于设定新的信号控制信息,而"oldact"则用于返回之前设定的信号控制信息,也就是说,sigaction()是兼具设定和查询功能的二合一形式的函数。
信号处理函数虽然会改变一个信号默认的行为,但如果你想恢复信号的默认行为,也是很容易的,只需要将"handler"设为SIG_DFL,再调用一下signal()就可以了。
但不是所有信号的默认行为都可以被目标进程更改,比如SIGKILL就不可以,只能直接受死,不容许片刻挣扎,相当于“斩立决”。试想一下,如果进程可以自行设计所有信号的处理函数,那操作系统可能就无法控制某些进程,如果一些进程无法无天,胡作非为,那操作系统这个终极boss也只能两手一摊:我能怎么办。
当然,SIGKILL这种残暴的武器也不能滥用,它应该作为从系统中删除失控进程的最后手段。一般情况下,还是尽量对进程温柔一点,使用SIGTERM就可以了。如果进程胆敢抗旨逃走(修改SIGTERM的处理方式为继续运行),那再祭出SIGKILL追杀,普天之下,莫非王土,想逃是逃不掉的。不过,面对大杀器SIGKILL,有一个进程是可以被豁免的,那就是init进程。Init进程作为开国功臣,位高权重,内核是不允许它被kill掉的。
忽略信号
发不发信号,发什么信号,那都是你的自由,但是否接收,如何接受,则是我的自由。如果不想处理这个信号,直接在signal()里将"handler"设为SIG_IGN就可以了。因为内核在将信号enqueue之前,会首先查看目标进程设置的信号处理函数,如果是SIG_IGN,或者这个信号的默认操作就是被忽略且"handler"是SIG_DFL,那么内核会直接丢弃这一信号。
当然,这种忽略方式虽然简单高效,但未免显得有些粗鲁,就像你不想看快递员送来的一封信件,直接门都不开,把快递员挡外面了。一个更友(虚)善(伪)的做法是把笑嘻嘻的把门打开,把信件接过来,然后关上门,马上把信件扔到垃圾桶里,也就是假装注册一个信号处理函数,但在这个函数里啥也不干,相当于间接忽略。
屏蔽信号
在目标进程执行信号处理函数期间,有可能收到内核递送来的其他信号。如果新到来的信号和当前正在处理的信号是同样的信号(信号值相同的信号就是同样的信号),目标进程这时跳转去执行新的信号,将会形成信号处理函数的嵌套,这就要求信号处理函数必须是可重入的(reentrant)。即便不是同样的信号,当目标进程正在执行某些关键操作时,它可能也是不想被打断的。就像CPU可以屏蔽硬件中断一样,进程也可以在某段时期阻塞/屏蔽(block)某个特定的信号。那进程是如何实现对信号的屏蔽呢?
同信号的pending机制类似,这里也是用一个sigset_t表示的bitmap来记录目标进程对信号的屏蔽情况,bitmap存放在进程对应的task_struct中。前面讲到的struct sigaction的"sa_mask"可用于在注册信号处理函数的时候设置这个bitmap,之后也可以通过sigprocmask()函数来动态的更改进程对应的bitmap。
int sigprocmask(int how, sigset_t *set, sigset_t *oldset)
{
struct task_struct *tsk = current;
switch (how) {
case SIG_BLOCK:
sigorsets(&newset, &tsk->blocked, set);
break;
case SIG_UNBLOCK:
sigandnsets(&newset, &tsk->blocked, set);
break;
case SIG_SETMASK:
newset = *set;
break;
}
}
同sigaction()一样,sigprocmask()也是一个设定和查询二合一的函数。在Linux的实现中,和当前处理信号相同的信号是默认被屏蔽的,不需要用户单独设置。
表示pending状态的bitmap和表示block状态的bitmap不光实现方式类似,而且在功能上是有关联的。只有被pending,且没有被block的信号,才会得到目标进程的执行。

对于一个被屏蔽的信号,即便目标进程将它的处理方式设置为了SIG_IGN,内核也不会忽略并丢弃这个信号。这是因为啊,在这个信号被屏蔽的这段时间里,进程是有可能将SIG_IGN改成其他的处理函数的。进程可是以它解除对这个信号的屏蔽时,设置的处理函数为依据的,在信号阻塞期间的改动通通不算数。要是内核按照它之前的设定把信号忽略了,它反倒是要怪罪内核了,内核可不想背这个锅。
对于System V信号机制,在信号处理期间,不会屏蔽
进程唤醒
通常,进程是在执行过程中被信号打断的,其实信号机制还有另一个用途,它可以用来唤醒一个正在睡眠的进程。进程可以调用sigsuspend()将自己置于TASK_INTERRUPTIBLE的睡眠态,等待被信号唤醒。Linux中有两种睡眠:不可中断的睡眠(TASK_UNINTERRUPTIBLE)和可中断的睡眠(TASK_INTERRUPTIBLE)。前者通常用于相对短期的事件,比如等待磁盘I/O的完成;后者则用于可能需要较长时间才会发生的事件,比如等待终端(terminal)的用户输入。
int sigsuspend(sigset_t *set)
{
set_current_blocked(set);
while (!signal_pending(current)) {
__set_current_state(TASK_INTERRUPTIBLE);
schedule();
}
}
这里的参数"set"是一个信号屏蔽的bitmap,凡是不在这个"set"里的信号,都是进程敞开大门欢迎的,都可以用来唤醒进程。
以上的讨论都是基于进程的,事实上,信号也可以被发送给进程中的某个特定线程,这将在下文中介绍。




