信号 (signal) 是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令 ( 即信号 ) 。应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。进程收到一个信号后,会检查对该信号的处理机制。如果是 SIG_IGN ,就忽略该信号;如果是 SIG_DFT ,则会采用系统默认的处理动作,通常是终止进程或忽略该信号;如果给该信号指定了一个处理函数 ( 捕捉 ) ,则会中断当前进程正在执行的任务,转而去执行该信号的处理函数,返回后再继续执行被中断的任务。
下面就来说说与信号有关的函数吧。
最简单 signal 函数
typedef void (*sighandler_t) (int)
sighandler_t signal(int signum, sighandler_t handler);
返回原信号处理函数,或 SIG_ERR
signal() 是最简单的给进程安装信号处理器的函数,第一个参数指定信号,第二个参数为该信号指定一个处理函数。
如下是一个最简单的处理信号的程序,它捕捉 SIGUSR1 ,忽略 SIGUSR2 ,按系统默认处理 SIGINT , SIGUSR1 和 SIGUSR2 是 Linux 提供的用户定义信号,可用于任何应用程序。主程序什么都不干,只用 pause() 循环等待信号。
例程 1 最简单的信号处理
- static void pr_mask( const char * string) {
- sigset_t procmask;
- sigprocmask(SIG_SETMASK, NULL, &procmask);
- printf( "%s: " , string);
- if (sigismember(&procmask, SIGINT))
- printf( "SIGINT " );
- if (sigismember(&procmask, SIGUSR1))
- printf( "SIGUSR1 " );
- if (sigismember(&procmask, SIGUSR2))
- printf( "SIGUSR2 " );
- if (sigismember(&procmask, SIGTERM))
- printf( "SIGTERM " );
- if (sigismember(&procmask, SIGQUIT))
- printf( "SIGQUIT " );
- printf( "\n" );
- }
- static void sigusr( int signum)
- {
- pr_mask(“ int sigusr”);
- if (signum == SIGUSR1)
- printf(“SIGUSR1 received\n”);
- else if (signum == SIGUSR2)
- printf(“SIGUSR2 received\n”);
- else
- printf(“signal %d received\n”, signum);
- }
- int main( void )
- {
- if (signal(SIGUSR1, sig_usr) == SIG_ERR) {
- printf(“error catching SIGUSR1\n”);
- exit(1);
- }
- if (signal(SIGUSR2, SIG_IGN) == SIG_ERR) {
- printf(“error ignoring SIGUSR2\n”);
- exit(1);
- }
- if (signal(SIGINT, SIG_DFT) == SIG_ERR) {
- printf(“error setting SIGINT to default \n”);
- exit(1);
- }
- while (1)
- pause();
- exit(0);
- }
后台运行该程序,并用 kill 发送信号给它。
$ ./a.out &
[1] 3725
$ kill -USR1 3725
in sigusr: SIGUSR1
SIGUSR1 received
$ kill -USR2 3725
[1]+ User defined signal 2 ./a.out
我们可以看到, Linux 系统对 SIGUSR2 的默认动作是终止进程。
中断与自动重启动
前面说过,信号是一种软件中断机制,这就产生了一个问题:如果信号到来时进城正在执行某个低速系统调用,系统应该怎么处理?是暂时阻塞系统调用返回,在信号处理程序完成后继续没完成的系统调用呢,还是让系统调用出错返回,同时把 errno 设置为 EINTR ,让调用者去做进一步的出错检查呢?用事实说话,让我们做一个试验先吧。
下面的程序读取标准输入并把它输出到标准输出,在此期间,我们给进程发送 SIGUSR1 信号,以此来确定 Linux 在收到信号后是如何对处理系统调用的。
例程 2 信号与自动重启动的 signal 版本
- int main( void )
- {
- char buf[BUFSIZ];
- int n;
- signal(SIGUSR1, sig_usr);
- while (1) {
- if ((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) {
- if (errno == EINTR)
- printf(“read is interrupted\n”);
- }
- else {
- write(STDOUT_FILENO, buf, n);
- }
- }
- exit(0);
- }
运行该程序,并从另一个终端给该进程发送信号 SIGUSR1 。
$./a.out
first line
first line
in sigusr: SIGUSR1
SIGUSR1 received
second line
second line
in sigusr: SIGUSR1
SIGUSR1 received
^C
可见对由 signal() 函数安装的信号处理程序,系统默认会自动重启动被中断的系统调用,而不是让它出错返回,所以应用程序不必针对慢速系统调用的 errno ,做 EINTR 检查,这就是自动重启动机制。
我们再来看另外一个例子,它使用另一个函数 sigaction() 来安装信号处理程序。 sigaction() 允许进程对信号进行更多的控制:
例程 3 信号与自动重启动的 sigaction 版本
- int main( void )
- {
- char buf[BUFSIZ];
- int n;
- struct sigaction sa_usr;
- sa_usr.flags = 0; //SA_RESART
- sa_usr.sa_handler = sig_usr;
- sigaction(SIGUSR1, &sa_usr, NULL);
- //signal(SIGUSR1, sig_usr);
- while (1) {
- if ((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) {
- if (errno == EINTR)
- printf(“read is interrupted\n”);
- }
- else {
- write(STDOUT_FILENO, buf, n);
- }
- }
- exit(0);
- }
此时再运行这个程序,并从另一终端给该进程发送信号 SIGUSR1 ,我们会得到如下结果。
$./a.out
first line
first line
in sigusr: SIGUSR1
SIGUSR1 received
read is interrupted
second line
second line
in sigusr: SIGUSR1
SIGUSR1 received
read is interrupted
^C
由此我们可以得出, Linux 对 sigaction() 的默认动作是不自动重启动被中断的系统调用,因此如果我们在使用 sigaction() 时需要自动重启动被中断的系统调用,就需要使用 sigaction 的 SA_RESTART 选项,见上例注释,关于 sigaction() ,下文会有更多的描述。这和《 UNIX 环境高级编程》中对 Linux 信号处理的描述是一致的。
可重入函数
如前所述,进程在收到信号并对其进行处理时,会暂时中断当前正在执行的指令序列,转而去执行信号处理程序。但是信号的到来,往往是无法预测的,我们无法确定进程会在何时收到信号。如果进程在收到信号时正在执行 malloc() 调用,而此时捕捉到信号,进城就会转而去执行信号处理程序,而信号处理程序中又再次调用了 malloc() 函数,那结果将会怎样呢?进程的栈空间很可能就会受到破坏,从而产生无法预料的结果。所以有些函数是不能在信号处理程序中调用的,这些函数被称为不可重入函数,而那些允许在信号处理函数中调用的函数,则称为可重入函数。下表列出了 Linux 系统中的可重入函数 ( 摘自《 UNIX 环境高级编程》 ) ,对不在该表中的函数,信号处理函数中要慎用。
表 1 可重入函数
accept access aio_error aio_return aio_suspend alarm bind cfgetispeed cfgetospeed cfsetispeed cfsetospeed chdir chmod chown clock_gettime close connect creat dup dup2 execle execve _Exit & _exit | fchmod fchown fcntl fdatasync fork fpathconf fstat fsync ftruncate getegid geteuid getgid getgroups getpeername getpgrp getpid getppid getsockname getsockopt getuid kill link listen | lseek lstat mkdir mkfifo open pathconf pause pipe poll posix_trace_event pselect raise read readlink recv recvfrom recvmsg rename rmdir select sem_post send sendmsg | sendto setgid setpgid setsid setsockopt setuid shutdown sigaction sigaddset sigdelset sigemptyset sigfillset sigismember signal sigpause sigpending sigprocmask sigqueue sigset sigsuspend sleep socket socketpair | stat symlink sysconf tcdrain tcflow tcflush tcgetattr tcgetpgrp tcsendbreak tcsetattr tcsetpgrp time timer_getoverrun timer_gettime timer_settime times umask uname unlink utime wait waitpid write |
发送信号的 kill 和 raise 函数
int kill(pid_t pid, int sig);
int raise(int sig);
kill() 发送信号给指定进程, raise() 发送信号给进程本身。对 kill() 的 pid ,有如下描述:
pid > 0 将信号发送给 ID 为 pid 的进程
pid == 0 将信号发送给与发送进程属于同意个进程组的所有进程
pid < 0 将信号发送给进程组 ID 等于 pid 绝对值的所有进程
pid == -1 将信号发送给该进程有权限发送的系统里的所有进程
所有信号的发送都要先经过权限检查,如果进程没有相应发送的权限, kill() 会出错返回,并把 errno 设为 EPERM 。但也有一个例外,对 SIGCONT ,进程可以将它发送给当前会话的所有进程。
产生时钟信号 SIGALRM 的 alarm 函数
unsigned int alarm(unsigned int seconds);
alarm() 函数可设置一个计时器,计时器超时就产生 SIGALRM 信号。由于每个进程只能有一个 SIGALRM 处理程序,所以只能为一个进程设置一个计时器,所以 alarm() 和 setitimer() 会共享同一个 SIGALRM 信号和该信号的处理函数。也就是说, alarm() 和 setitimer() 彼此会互相影响。调用 alarm() ,会使先前设置的计时器失效,并把没有超时的时间作为当前 alarm 的返回值。如先前设置的时钟为 100 秒,当前调用 alarm() 时才经过 30 秒,剩余的 70 秒就作为 alarm() 的返回值,并用 alarm() 中指定的秒数重新设置计时器。如果 seconds 为 0 ,则会取消先前设置的计时器,并将其余留值作为 alarm() 的返回值。
等待信号的 pause 函数
int pause(void);
pause() 会使当前进程挂起,直到捕捉到一个信号,对指定为忽略的信号, pause() 不会返回。只有执行了一个信号处理函数,并从其返回, puase() 才返回 -1 ,并将 errno 设为 EINTR 。详见前面的第一个例子。
信号屏蔽字 (process signal mask)
每个进程都会有一个信号屏蔽字,它规定了当前进程要阻塞的信号集。对于每种可能的信号,信号屏蔽字中都会有一位与之对应,如果该位被设置,该信号当前就是阻塞的。进程可以通过 sigprocmask() 来获得和修改当前进程的信号屏蔽字。
信号集 (signal set)
信号集是一种特殊的数据类型,由于无法确定信号的多少,所以不能用简单数据类型来包含所有可能的信号,所以系统就定义了一个 sigset_t 的数据类型专门用于信号集。同时还定义了一族用于处理信号集的函数。这样用户可以不必关心信号集的实现,只要使用这组函数来处理信号集就可以了。
信号集函数
int sigemptyset(sigset_t * set);
int sigfillset(sigset_t * set);
int sigaddset(sigset_t * set, int signum);
int sigdelset(sigset_t * set, int signum);
int sigismember(sigset_t * set, int signum);
sigemptyset() 和 sigfillset() 都用于初始化一个信号集,前者用于清空信号集中所有的信号,后者则用于设置信号集中所有的信号;信号集在使用前必须要经过初始化,初始化后,就可以用 sigaddset() 和 sigdelset() 往信号集里添加删除信号了。 sigismember() 用于判断指定信号是否在信号集中。
修改信号屏蔽字的 sigprocmask 函数
int sigprocmask(int how, const sigset_t * set, sigset_t * oldset);
sigpromask() 根据 how 指定的方式,设置进程的当前信号屏蔽字为 set ,并将旧的信号屏蔽字保存在 oldset 中返回。如果 set 为 NULL ,则不修改当前信号屏蔽字,而将其通过 oldset 返回;如果 oldset 为 NULL ,则不会返回旧的信号屏蔽字。 how 支持 三种方式,见下表。
表 2 设置信号屏蔽字的方式
how | 说明 |
SIG_BLOCK SIG_UNBLOCK SIG_SETMASK | 设置阻塞 set 指定的信号集 设置解除阻塞 set 指定的信号集 设置当前信号屏蔽字为 set ,在 set 中的信号都会被阻塞,不在 set 中的信号会被递送 |
如果我们想阻塞 SIGUSR1 ,有两种方式。
- // using SIG_BLOCK
- sigset_t sigset;
- sigemptyset(&sigset);
- sigaddset(&sigset, SIGUSR1);
- sigprocmask(SIG_BLOCK, &sigset, NULL);
- // or using SIG_SETMASK
- sigset_t set, oldset;
- // get current signal mask
- sigprocmask(SIG_SETMASK, NULL, &set);
- // add SIGUSR1 into the signal mask
- sigaddset(&set, SIGUSR1);
- sigprocmask(SIG_SETMASK, &set, &oldset);
同样,如果要解除阻塞 SIGUSR1 ,也有两种方式。
- // using SIG_UNBLOCK
- sigset_t sigset;
- sigemptyset(&sigset);
- sigaddset(&sigset, SIGUSR1);
- sigprocmask(SIG_UNBLOCK, &sigset, NULL);
- // or using SIG_SETMASK
- sigset_t set, oldset;
- // get current signal mask
- sigprocmask(SIG_SETMASK, NULL, &set);
- // delete SIGUSR1 from the signal mask
- sigdelset(&set, SIGUSR1);
- sigprocmask(SIG_SETMASK, &set, &oldset);
信号未决 (pending)
信号是由某些事件产生的,这些事件可能是硬件异常 ( 如被零除 ) ,软件条件 ( 如计时器超时 ) ,终端信号或调用 kill()/raise() 函数。信号产生时,内核通常会在进程表中设置某种标志,表示当前信号的状态。当内核对信号采取某种动作时,我们说向进程递送 (deliver) 了一个信号,而在信号产生和递送之间的间隔内,该信号的状态是未决的 (pending) 。
获得未决的信号 sigpending
int sigpending(sigset_t * set);
该函数在 set 中返回进程中当前尚未递送的信号集。
功能更全的 sigaction 函数
int sigaction(int signum, const struct sigaction * act, struct sigaction * oldact);
struct sigaction {
void (*sa_handler) (int);
void (*sa_sigaction) (int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
siginfo_t {
int si_signo; // Signal number
int si_errno; // An errno value
int si_code; // signal code
pid_t si_pid; // sending process ID
pid_t si_uid; // Real user ID of sending process
int si_status; // Exit value or signal
...
};
sigaction() 的功能是为信号指定相关的处理程序,但是它在执行信号处理程序时,会把当前信号加入到进程的信号屏蔽字中,从而防止在进行信号处理期间信号丢失。从前面的例子我们可以看到,简单的 signal() 函数也具有同样的功能,这是由于 signal() 已经被重新实现的缘故,所以如果不在乎对信号的更多的控制,我们尽可放心大胆的使用简单的 signal() 函数。 signum 指定将要改变处理行为的信号; act 指定该信号的处理动作, oldact 用于返回该信号先前的处理动作。
在 sigaction 结构中, sa_handler 和 sa_sigaction 用于指定信号处理函数,但要注意,二者只能用其一,因为它们在内部可能会实现为 union 结构。除了在为 sa_flags 指定 SA_SIGINFO 标志时,会使用 sa_sigaction 字段外,其他情况下都应该只用 sa_handler 字段。
sa_mask 用于指定在当前信号处理程序执行期间,需要阻塞的信号集。如在处理 SIGUSR1 期间,我们希望暂时阻塞 SIGUSR2 ,就应该把 SIGUSR2 加到 SIGUSR1 的 sa_mask 中。信号处理程序返回后,会自动解除对 SIGUSR2 的阻塞,详见例程 4 。
sa_flags 用于指定信号处理动的选项标志,详见手册。这里我想说的是 SA_RESTART 和 SA_SIGINFO 。 SA_RESTART 用于控制信号的自动重启动机制,如前面例子所示,对 signal() , Linux 默认会自动重启动被中断的系统调用;而对于 sigaction() , Linux 默认并不会自动重启动,所以如果希望执行信号处理后自动重启动先前中断的系统调用,就需要为 sa_flags 指定 SA_RESTART 标志。对于 SA_SIGINFO ,手册上说此标志可能会导致对信号的可靠排队,但是从下面的例子我们将会看到, Linux 并没有对信号进行排队。
例程 4 sigaction 函数
- int main( void )
- {
- struct sigaction act_usr;
- act_usr.sa_flags = 0;
- act_usr.sa_handler = sigusr;
- sigemptyset(&act_usr.sa_mask);
- // add the signal you want to block while SIGUSR1 is processing here
- sigaddset(&act_usr.sa_mask, SIGUSR2);
- // we dont care about the old action of SIGUSR1
- sigaction (SIGUSR1, &act_usr, NULL);
- while (1)
- pause();
- }
运行结果如下:
$./a.out &
[1] 16385
$kill -USR1 16385
in sig_usr1: SIGUSR1 SIGUSR2
SIGUSR1 recieved
可见在 SIGUSR1 处理期间, SIGUSR2 已经被加入到进程的屏蔽字中了,所以在此期间, SIGUSR2 是被暂时阻塞的。
信号排队
如果进程阻塞了一个信号,在没有对其解除阻塞之前,该信号产生了多次,将会如何处理呢? Linux 并不会对信号排队,当信号解除阻塞后,内核只向进程递送一个信号,而不管在其阻塞期间有多少个信号产生。
下面是上例的改进版。首先我们阻塞 SIGUSR1 ,然后在 SIGUSR2 的处理函数里解除对 SIGUSR1 的阻塞,这样我们就有机会在 SIGUSR1 阻塞期间,多发送几个 SIGUSR1 来确定 Linux 内核是怎样处理的。我们期望能看到 Linux 对信号的排队。
例程 5 信号排队
- static void sig_usr2( int sig)
- {
- sigset_t set;
- printf( "SIGUSR2 recieved\n" );
- // unblock SIGUSR1
- sigprocmask(SIG_SETMASK, NULL, &set);
- sigdelset(&set, SIGUSR1);
- sigprocmask(SIG_SETMASK, &set, NULL);
- }
- static void handler( int signum, siginfo_t * info, void * context)
- {
- // dump signal information
- printf( "si_signo: %d\n" , info->si_signo);
- printf( "si_errno: %d\n" , info->si_errno);
- printf( "si_code: %d\n" , info->si_code);
- printf( "si_pid: %d\n" , info->si_pid);
- printf( "si_uid: %d\n" , info->si_uid);
- }
- int main( void )
- {
- struct sigaction act_usr1;
- struct sigaction act_usr2;
- sigset_t mask;
- act_usr1.sa_flags = SA_SIGINFO;
- //act_usr1.sa_handler = sigusr;
- sigemptyset(&act_usr1.sa_mask);
- act_usr1.sa_sigaction = handler;
- sigaction(SIGUSR1, &act_usr1, NULL);
- act_usr2.sa_flags = 0;
- act_usr2.sa_handler = sig_usr2;
- sigemptyset(&act_usr2.sa_mask);
- sigaction(SIGUSR2, &act_usr2, NULL);
- // block SIGUSR1
- sigprocmask(SIG_SETMASK, NULL, &mask);
- sigaddset(&mask, SIGUSR1);
- sigprocmask(SIG_SETMASK, &cmask, NULL);
- while (1)
- pause();
- exit(0);
- }
$ ./a.out &
[1] 17165
$ kill -USR1 17165
$ kill -USR1 17165
$ kill -USR1 17165
$ kill -USR1 17165
$ kill -USR1 17165
$ kill -USR2 17165
SIGUSR2 recieved
si_signo: 10
si_errno: 0
si_code: 0
si_pid: 3945
si_uid: 500
$ ps
PID TTY TIME CMD
3945 pts/1 00:00:00 bash
在 SIGUSR1 阻塞期间,我们向进程发送了 5 个 SIGUSR1 ,而解除阻塞后,内核只递送了一个 SIGUSR1 ,说明 Linux 并不支持信号排队。另外我们还可以看到, si_signo 是收到的信号的数值; si_pid 是发送进程的进程 ID , ps 输出我的终端进程 ID 正是 3945 ; si_uid 是发送进程的有效用户 ID ,而我的用户 ID 也正是 500 。对于 siginfo 结构中的其它成员,我没有打印,有兴趣的可以自己研究。
信号跳转函数 sigsetjump 和 siglongjump
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);
sigsetjmp() 有多次返回,对于直接调用者 ( 一般是主程序 ) ,它返回 0 ;若从 siglongjmp() 调用 ( 一般是信号处理程序 ) ,则返回返回 siglongjmp() 中的 val 值。所以为了避免混淆,最好不要在调用 siglongjmp() 时,让 val=0 。
另外需要说明的是 sigsetjmp() 的第二个参数,它用于告诉内核,要不要保存进程的信号屏蔽字。当 savesigs 为非 0 时,调用 sigsetjmp() 会在 env 中保存当前的信号屏蔽字,然后在调用 siglongjmp() 时恢复之前保存的信号屏字。由于信号处理函数使用 siglongjmp() 跳转时不属于正常返回,所以在进入信号处理函数时被阻塞的当前信号就没有机会在返回时恢复。 sigsetjmp() 的 savesigs 参数就用于是告诉系统,在调用 siglongjmp 时,是否需要恢复先前的信号屏蔽字。
下例向你展示了如何使用 sigsetjmp() 和 siglongjmp() ,注意这里引入了一个全局变量 canjmp ,它是一种同步保护机制,用于告诉信号处理程序,在进程环境没有准备好之前,不要跳转,否则可能会导致混乱。
例程 6 信号跳转
- static sigjmp_buf jmpbuf;
- // for synchronizing
- static volatile sig_atomic_t canjmp;
- static void sigusr1( int signum)
- {
- printf(“SIGUSR1 reveived\n”);
- // main process initialization is not completed
- if (canjmp == 0)
- return ;
- siglongjmp(jmpbuf, 1);
- }
- satic void sigusr2( int signum)
- {
- printf(“SIGUSR2 reveived\n”);
- if (canjmp == 0)
- return ;
- siglongjmp(jmpbuf, 2);
- }
- int main( void )
- {
- int n;
- int savemask = 1;
- signal(SIGUSR1, sigusr1);
- signal(SIGUSR2, sigusr2);
- // need to save the procmask, otherwise, u have to reset the procmask
- n = sigsetjmp(jmpbuf, savemask);
- if (n == 1) {
- // jump from SIGUSR1
- printf(“Jump to here from SIGUSR1\n”);
- if (savemask == 0) {
- // prevent from long jumping
- canjmp = 0;
- // reset the procmask, unblock SIGUSR1
- sigset_t set;
- sigprocmask(SIG_SETMASK, NULL, &set);
- sigdelset(&set, SIGUSR1);
- sigprocmask(SIG_SETMASK, &set, NULL);
- canjmp = 1;
- }
- }
- else if (n == 2) {
- printf(“Jump to here from SIGUSR2\n”);
- if (savemask == 0) {
- canjmp = 0;
- sigset_t set;
- sigprocmask(SIG_SETMASK, NULL, &set);
- sigdelset(&set, SIGUSR1);
- sigprocmask(SIG_SETMASK, &set, NULL);
- canjmp = 1;
- }
- }
- canjmp = 1;
- while (1)
- pause();
- exit(0);
- }
$ ./a.out &
[1] 5485
$ kill -USR1 5485
SIGUSR1 recieved
Jump to here from SIGUSR1
$ kill -USR2 5485
SIGUSR2 recieved
Jump to here from SIGUSR2
例程 6 告诉我们,根据 sigsetjmp() 的返回值,我们也可以通过信号实现程序的多分支控制。另外如果没有在 sigsetjmp() 时设置了 savesigs ,那么在 siglongjmp() 返回后,就要重新设置进程的信号屏蔽字,否则该信号在一次 siglongjmp() 之后将被永久阻塞。
难以捉摸的 sigsuspend 函数
int sigsuspend(const sigset_t * sigmask);
对于这个函数,我始终无法清晰的理解,关于它的用法,它的作用,它的语义,都让我一头雾水。《 UNIX 环境高级编程》, Linux 手册,看了几遍,都无法开塞,真是愚钝至极啊!
从《 UNIX 环境高级编程》对 sigsuspend() 的引言看,该函数的出现是为了解决早期不可靠信号,即信号丢失的问题的。在早期的信号机制中,对信号解除阻塞和等待信号需要两步进行:
sigprocmask(SIG_SETMASK, &unblockmask, NULL);
pause();
在对信号解除阻塞之后和调用 pause() 之前有一个时间窗口,所以在这之间产生的信号就可能会丢失,从而是本该返回的 pause() 没有返回。
sigsuspend() 能让解除信号阻塞和等待信号成为一个原子操作,这样就避免了上述的问题。它会把当前进程的信号屏蔽字设定为 sigmask 指定的值,所以在等待信号期间, sigmask 中的信号会被暂时阻塞,而 sigmask 之外的信号都会被暂时解除阻塞。然后 sigsuspend() 挂起当前进程,等待,直到捕捉到一个信号或发生了一个会终止该进程的信号。如果是捕捉到一个信号并从出来程序中返回,则 sigsuspend() 返回 -1 ,把进程信号屏蔽字设回调用 sigsuspend() 之前的值,并将 errno 设为 EINTR 。注意,指定为忽略的信号,并不会导致 sissuspend() 返回。
注意, sigsuspend() 只是暂时解除对不在 sigmask 中的信号的阻塞,在捕捉到一个信号后,以前阻塞的信号还会被重新阻塞,所以如果你要对一个以前阻塞的信号解除阻塞的话,在 sigsuspend() 返回之后,还要重新用 sigprocmask 来解除对该信号的阻塞。这就是我的疑问了,如果我无意让进程等待任何信号的话,那这个 sigsuspend() 不是对我几乎毫无用处吗?
sigsuspend() 的另一个用途就是让进程等待一个信号处理程序来设置一个全局变量。这个似乎还比较有用,在进程等待某一信号时,可让进程先挂起,直到收到该信号,设置的全局变量并导致 sigsuspend() 返回,进程才处理相应的任务,避免了 CPU 的无谓等待。如下例程就展示了如何让进程等待 SIGUSR1 ,并在每收到一次 SIGUSR1 后去执行一组相同的操作。
例程 7 sigsuspend 的一个应用
- static volatile sig_atomic_t ok;
- static void sigusr( int signum)
- {
- // do nothing, set the flag only
- ok = 1;
- }
- int main( void )
- {
- sigset_t emptymask, waitmask, oldmask;
- // block SIGUSR1 first
- sigemptyset(&waitmask);
- sigaddset(&waitmask, SIGUSR1);
- sigprocmask(SIG_BLOCK, &waitmask, &oldmask);
- // set SIGUSR1 handler
- signal(SIGUSR1, sigusr);
- sigemptyset(&emptymask);
- while (1) {
- // waiting for SIGUSR1 to set ok
- while (ok == 0)
- sigsuspend(&emptymask);
- // sigsuspend return, SIGUSR1 has come, and now SIGUSR1 is blocked
- // reset the flag, so the process will keep waiting after the
- // following things are done
- ok = 0;
- //
- // other things need to do here
- //
- }
- exit(0);
- }
我对 sigsuspend() 的理解仅限于此,不敢多卖弄,就此打住吧 :)
发送 SIGABRT 的专用函数 abort
int abort(void);
abort 会向当前进程发送 SIGABRT 信号,让进程做一些“善后”处理,如刷新流缓冲区,关闭文件等,然后终止进程。
其它几个有用的小函数:
extern char * sys_siglist[];
这是一个以信号为索引的字符串数组,通过它可以很容易的找到信号的字符串名称。
void psignal(int signum, const char * msg);
此函数类似于 perror() ,输出对指定信号的字符串描述。
char * strsignal(int signum);
返回指定信号的字符串描述。
例程 8 其它函数
- int main( void ) {
- printf( "sys_siglist:\n" );
- printf( "SIGUSR1: %s\n" , sys_siglist[SIGUSR1]);
- printf( "SIGSEGV: %s\n" , sys_siglist[SIGSEGV]);
- printf( "SIGHUP: %s\n\n" , sys_siglist[SIGHUP]);
- printf( "strsignal:\n" );
- printf( "SIGUSR1: %s\n" , strsignal(SIGUSR1));
- printf( "SIGSEGV: %s\n" , strsignal(SIGSEGV));
- printf( "SIGHUP: %s\n\n" , strsignal(SIGHUP));
- printf( "psignal:\n" );
- psignal(SIGUSR1, "SIGUSR1" );
- psignal(SIGSEGV, "SIGSEGV" );
- psignal(SIGHUP, "SIGHUP" );
- exit(0);
- }
输出形式如下:
sys_siglist:
SIGUSR1: User defined signal 1
SIGSEGV: Segmentation fault
SIGHUP: Hangup
strsignal:
SIGUSR1: User defined signal 1
SIGSEGV: Segmentation fault
SIGHUP: Hangup
psignal:
SIGUSR1: User defined signal 1
SIGSEGV: Segmentation fault
SIGHUP: Hangup
最后的忠告:千万不要去写一个复杂的信号处理程序,那是最出力不讨好的事情,信号处理程序每多一行,你的程序莫名崩溃的可能性就增大一分,谁能保证你在信号处理函数里调用的都是可重入函数呢?本文中的很多例程使用的 printf() 就不是可重入的……信号处理程序应该尽量简单,对稍微复杂的任务,应该想办法 ( 如 siglongjmp() ,设置全局标志等 ) 交给主程序处理。