Unix环境高级编程学习笔记(六) 信号机制

信号机制概述

UNIX 系统的信号 ( signa l) , 是通知进程发生了异步事件的一种机制, 很多重要的应用进程都需要处理信 号。在 U N IX 系统中, 信号的主要用途是当系统出现 突发情况, 比如硬件异常、 用户键入了某些终端键都 会使系统给相关进程发送信号。 除了系统默认的信号处理和忽略以外, 用户还可以自定义信号处理函数, 以便执行一些必要操作, 比如程序执行异常应终止时, 可 以关闭数据库、 将缓存中的数据写入磁盘、 断开共享内存的连接等一些清理工作; 信号的另一个主要作用是协调进程的执行, U N IX 父子进程之间协同工作 ( 需要同步) 时经常依靠信号实现。
Unix的每一个信号数字都是以大写字母SIG开头的宏,它们被定义在<signal.h>头文件中。当进程有信号发生时,如果该信号没有被阻塞,那么该执行进程将立即中断当前程序的执行(非慢系统调用除外),转而去处理信号(可能忽略,默认处理或调用处理函数)。如果程序没有被结束或中止,那么进程处理完信号后将返回继续执行。
对于用户代码,在处理信号后的返回继续执行的方式如上所述,但对于系统调用,则得分情况考虑。首先,我们得明确慢系统调用的概念,当程序执行一个系统调用可能永远被阻塞时(例如对中断设备执行读操作),这时就称该系统调用为一个慢系统调用,只有慢系统调用才有可能被信号中断。当它被系统调用中断时,可能会有两种情况发生,中止返回(失败返回),或是自动重启,这取决于系统的实现,同时我们也可以对其进行设置(后面我会提到如何对这进行控制)。

下表罗列了Unix的系统信号:
Unix环境高级编程学习笔记(六) 信号机制_第1张图片


信号捕获函数

从上面的表可以看出系统对各信号发生时的默认处理方式,但许多时候我们的程序处理逻辑需要另一种更适应于我们应用的处理方式,这时候我们就需用利用信号捕获函数捕获信号,然后执行我们自己的逻辑。这时候就需要用到注册函数对信号捕获函数进行注册。

signal函数

该函数是最简单的注册函数,先来看其声明:
void (*signal(int signo, void (*func)(int)))(int);
其声明原型稍显复杂,我们换一种方式来看:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
这样就清晰多了,signal函数的第一个参数时信号序号,第二个参数是需要注册的捕获函数,其返回值是之前的捕获函数。另外func参数还可以是SIG_IGN或SIG_DFL常量,前者表示忽略该信号,而后者表示默认处理方式。当该函数执行失败时(比如指定的信号值无效),其返回SIG_ERR常量。
还有一点需要注意的是,虽然我们可以指定进程忽略某些信号,但SIGKILL和SIGSTOP这两个信号是不可能忽略的。
我们在编写信号捕获函数的时候有一点需要注意的是,不要在信号捕获函数中调用不可重入的函数,例如malloc函数。该函数通常为它所分配的存储区维护一个链表,如果进程在执行该函数时被信号中断,那么链表就可能处于不完整的状态,而如果在信号捕获函数中再次调用该malloc函数,就会造成程序的执行错误,其后果是灾难性的。

当一个程序启动时,我们知道实际上时通过exec系列函数,那么信号捕获函数在这个过程中会怎样呢?通常,在执行一个新进程时,原进程的捕获函数是不适用的,而且在新进程中其函数地址也失去了意义。所以exec的处理方式是,将所有关联了捕获函数的信号量都更改为默认操作,而其他不变。也就是说,实际上执行exec之前我们可以指定下面的进程忽略一些信号量的。

sigaction函数

该函数提供了更加灵活而强大的信号捕获函数的注册方式,还是先看其声明:
int sigaction(int signo, const struct sigaction *restrict act,
	struct sigaction *restrict oact);
首先我们来看一下struct sigaction结构体:
struct sigaction {
	void (*sa_handler)(int); /* addr of signal handler, */
	/* or SIG_IGN, or SIG_DFL */
	sigset_t sa_mask; /* additional signals to block */
	int sa_flags; /* signal options, Figure 10.16 */
	/* alternate handler */
	void (*sa_sigaction)(int, siginfo_t *, void *);
};
第一个是指向信号捕获函数的指针。第二个成员是信号集,它说明了当该捕获函数执行时会被阻塞(加入已有的阻塞信号集)的信号有哪些(关于信号阻塞在后面会详细解释)。第三个参数指定对信号处理的各个选项,其意义如下:
Unix环境高级编程学习笔记(六) 信号机制_第2张图片
这些选项可以使用或进行组织。第四个成员sa_sigaction是一个替代的信号捕获函数,当sa_flags成员中包含SA_SIGINFO标识时,该函数将被使用。

信号阻塞

许多情况下,我们希望在执行某些代码时,一些特定的信号被阻塞调用,此时我们可以使用sigprocmask函数,其声明形式如下:
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
sigset_t是信号集类型,如果系统的信号数量少于32个,他可能用一个4字节的整形去实现,其中的每一个位代表了一个信号,该函数用于设置信号的阻塞方式。第二个参数是新的阻塞信号集,第三个参数是O参数,当其不为空时,它将返回改变前的阻塞信号集。当第二参数不为空时,参数一将制定该信号集的处理方式,其枚举值如下:
SIG_BLOCK:阻塞信号集是旧信号集与新信号集的并集
SIG_UNBLOCK:阻塞信号集是旧信号集与新信号集的交集
SIG_SETMASK:新信号集将直接替换掉原信号集成为阻塞信号集。

现在说一下信号集的操作方式,看如下五个函数的声明:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
第一个函数用于将信号集置空,第二个函数则是将信号集填满。
第三个函数和第四个函数则是向信号集中添加指定的信号或删除指定的信号。
第五个函数则是检查信号集中是否已经包含了指定的信号。

pause函数用于挂起一个进程,直到一个非阻塞信号发生。

kill和raise函数

这两个函数用于手动产生信号,其函数声明新式如下:
int kill(pid_t pid, int signo);
int raise(int signo);
调用raise(signo)等价于调用kill(getpid(), signo);
kill函数向指定进程或进程组发送signo信号,其参数形式如下:
pid>0 将信号发送该进程ID=pid的进程;
Pid==0 将信号发送给与该发送进程属于同一进程组的所有进程;
pid==-1 将信号发送给所有进程;
pid<-1 将信号发送给进程组ID=-pid的进程;
发送信号有一个限制是它只能向它具有权限发送信号的进程发送信号,其权限机制如下:
超级用户可将信号发送给任意进程,而对于非超级用户,发送进程的实际或有效ID必须等于接收进程的实际或有效ID。对于权限问题也有一个特例,如果发送的信号是SIGCONT,则进程可将它发送给属于同一个会话(session)的任一进程。
对与kill函数还有一个限制是不能向系统进程集发送信号,这里的系统进程集包括内核进程以及init进程。

我们可能会熟悉一个命令kill,它的作用于该函数类似,向指定进程发送信号,在默认情况下,它发送的信号是SIGKILL。

参考文献

《UNIX信号机制》 王娟娣   徐敏丽

你可能感兴趣的:(Unix环境高级编程学习笔记(六) 信号机制)