Linux信号与信号处理

信号 (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 最简单的信号处理

  1. static void pr_mask( const char * string) {
  2.     sigset_t procmask;
  3.     sigprocmask(SIG_SETMASK, NULL, &procmask);
  4.     printf( "%s: " , string);
  5.     if (sigismember(&procmask, SIGINT))
  6.         printf( "SIGINT " );
  7.     if (sigismember(&procmask, SIGUSR1))
  8.         printf( "SIGUSR1 " );
  9.     if (sigismember(&procmask, SIGUSR2))
  10.         printf( "SIGUSR2 " );
  11.     if (sigismember(&procmask, SIGTERM))
  12.         printf( "SIGTERM " );
  13.     if (sigismember(&procmask, SIGQUIT))
  14.         printf( "SIGQUIT " );
  15.     printf( "\n" );
  16. }
  17. static void sigusr( int signum)
  18. {
  19.     pr_mask(“ int sigusr”);
  20.     if (signum == SIGUSR1)
  21.         printf(“SIGUSR1 received\n”);
  22.     else if (signum == SIGUSR2)
  23.         printf(“SIGUSR2 received\n”);
  24.     else
  25.         printf(“signal %d received\n”, signum);
  26. }
  27. int main( void )
  28. {
  29.     if (signal(SIGUSR1, sig_usr) == SIG_ERR) {
  30.         printf(“error catching SIGUSR1\n”);
  31.         exit(1);
  32.     }
  33.     if (signal(SIGUSR2, SIG_IGN) == SIG_ERR) {
  34.         printf(“error ignoring SIGUSR2\n”);
  35.         exit(1);
  36.     }
  37.     if (signal(SIGINT, SIG_DFT) == SIG_ERR) {
  38.         printf(“error setting SIGINT to default \n”);
  39.         exit(1);
  40.     }
  41.     while (1)
  42.         pause();
  43.     exit(0);
  44. }

后台运行该程序,并用 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 版本

  1. int main( void )
  2. {
  3.     char buf[BUFSIZ];
  4.     int n;
  5.     signal(SIGUSR1, sig_usr);
  6.     while (1) {
  7.         if ((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) {
  8.             if (errno == EINTR)
  9.                 printf(“read is interrupted\n”);
  10.         }
  11.         else {
  12.             write(STDOUT_FILENO, buf, n);
  13.         }
  14.     }
  15.     exit(0);
  16. }

运行该程序,并从另一个终端给该进程发送信号 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 版本

  1. int main( void )
  2. {
  3.     char buf[BUFSIZ];
  4.     int n;
  5.     struct sigaction sa_usr;
  6.     sa_usr.flags = 0; //SA_RESART
  7.     sa_usr.sa_handler = sig_usr;
  8.     sigaction(SIGUSR1, &sa_usr, NULL);
  9.     //signal(SIGUSR1, sig_usr);
  10.     while (1) {
  11.         if ((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) {
  12.             if (errno == EINTR)
  13.                 printf(“read is interrupted\n”);
  14.         }
  15.         else {
  16.             write(STDOUT_FILENO, buf, n);
  17.       }
  18.     }
  19.     exit(0);
  20. }

此时再运行这个程序,并从另一终端给该进程发送信号 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 ,有两种方式。

  1. // using SIG_BLOCK
  2. sigset_t sigset;
  3. sigemptyset(&sigset);
  4. sigaddset(&sigset, SIGUSR1);
  5. sigprocmask(SIG_BLOCK, &sigset, NULL);
  6. // or using SIG_SETMASK
  7. sigset_t set, oldset;
  8. // get current signal mask
  9. sigprocmask(SIG_SETMASK, NULL, &set);
  10. // add SIGUSR1 into the signal mask
  11. sigaddset(&set, SIGUSR1);
  12. sigprocmask(SIG_SETMASK, &set, &oldset);

同样,如果要解除阻塞 SIGUSR1 ,也有两种方式。

  1. // using SIG_UNBLOCK
  2. sigset_t sigset;
  3. sigemptyset(&sigset);
  4. sigaddset(&sigset, SIGUSR1);
  5. sigprocmask(SIG_UNBLOCK, &sigset, NULL);
  6. // or using SIG_SETMASK
  7. sigset_t set, oldset;
  8. // get current signal mask
  9. sigprocmask(SIG_SETMASK, NULL, &set);
  10. // delete SIGUSR1 from the signal mask
  11. sigdelset(&set, SIGUSR1);
  12. 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 函数

  1. int main( void )
  2. {
  3.     struct sigaction act_usr;
  4.     act_usr.sa_flags = 0;
  5.     act_usr.sa_handler = sigusr;
  6.     sigemptyset(&act_usr.sa_mask);
  7.     // add the signal you want to block while SIGUSR1 is processing here
  8.     sigaddset(&act_usr.sa_mask, SIGUSR2);
  9.     // we dont care about the old action of SIGUSR1
  10.     sigaction   (SIGUSR1, &act_usr, NULL);
  11.     while (1)
  12.         pause();
  13. }

运行结果如下:

$./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 信号排队

  1. static void sig_usr2( int sig)
  2. {
  3.     sigset_t set;
  4.     printf( "SIGUSR2 recieved\n" );
  5.     // unblock SIGUSR1
  6.     sigprocmask(SIG_SETMASK, NULL, &set);
  7.     sigdelset(&set, SIGUSR1);
  8.     sigprocmask(SIG_SETMASK, &set, NULL);
  9. }
  10. static void handler( int signum, siginfo_t * info, void * context)
  11. {
  12.     // dump signal information
  13.     printf( "si_signo: %d\n" , info->si_signo);
  14.     printf( "si_errno: %d\n" , info->si_errno);
  15.     printf( "si_code: %d\n" , info->si_code);
  16.     printf( "si_pid: %d\n" , info->si_pid);
  17.     printf( "si_uid: %d\n" , info->si_uid);
  18. }
  19. int main( void )
  20. {
  21.     struct sigaction act_usr1;
  22.     struct sigaction act_usr2;
  23.     sigset_t mask;
  24.     act_usr1.sa_flags = SA_SIGINFO;
  25.     //act_usr1.sa_handler = sigusr;
  26.     sigemptyset(&act_usr1.sa_mask);
  27.     act_usr1.sa_sigaction = handler;
  28.     sigaction(SIGUSR1, &act_usr1, NULL);
  29.     act_usr2.sa_flags = 0;
  30.     act_usr2.sa_handler = sig_usr2;
  31.     sigemptyset(&act_usr2.sa_mask);
  32.     sigaction(SIGUSR2, &act_usr2, NULL);
  33.     // block SIGUSR1
  34.     sigprocmask(SIG_SETMASK, NULL, &mask);
  35.     sigaddset(&mask, SIGUSR1);
  36.     sigprocmask(SIG_SETMASK, &cmask, NULL);
  37.     while (1)
  38.         pause();
  39.     exit(0);
  40. }

$ ./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 信号跳转

  1. static sigjmp_buf jmpbuf;
  2. // for synchronizing
  3. static volatile sig_atomic_t canjmp;
  4. static void sigusr1( int signum)
  5. {
  6.     printf(“SIGUSR1 reveived\n”);
  7.     // main process initialization is not completed
  8.     if (canjmp == 0)
  9.         return ;
  10.     siglongjmp(jmpbuf, 1);
  11. }
  12. satic void sigusr2( int signum)
  13. {
  14.     printf(“SIGUSR2 reveived\n”);
  15.     if (canjmp == 0)
  16.         return ;
  17.     siglongjmp(jmpbuf, 2);
  18. }
  19. int main( void )
  20. {
  21.     int n;
  22.     int savemask = 1;
  23.     signal(SIGUSR1, sigusr1);
  24.     signal(SIGUSR2, sigusr2);
  25.     // need to save the procmask, otherwise, u have to reset the procmask
  26.     n = sigsetjmp(jmpbuf, savemask);
  27.     if (n == 1) {
  28.         // jump from SIGUSR1
  29.         printf(“Jump to here from SIGUSR1\n”);
  30.         if (savemask == 0) {
  31.             // prevent from long jumping
  32.             canjmp = 0;
  33.             // reset the procmask, unblock SIGUSR1
  34.             sigset_t set;
  35.             sigprocmask(SIG_SETMASK, NULL, &set);
  36.             sigdelset(&set, SIGUSR1);
  37.             sigprocmask(SIG_SETMASK, &set, NULL);
  38.             canjmp = 1;
  39.         }
  40.     }
  41.     else if (n == 2) {
  42.         printf(“Jump to here from SIGUSR2\n”);
  43.         if (savemask == 0) {
  44.             canjmp = 0;
  45.             sigset_t set;
  46.             sigprocmask(SIG_SETMASK, NULL, &set);
  47.             sigdelset(&set, SIGUSR1);
  48.             sigprocmask(SIG_SETMASK, &set, NULL);
  49.             canjmp = 1;
  50.         }
  51.     }
  52.     canjmp = 1;
  53.     while (1)
  54.         pause();
  55.     exit(0);
  56. }

$ ./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 的一个应用

  1. static volatile sig_atomic_t ok;
  2. static void sigusr( int signum)
  3. {
  4.     // do nothing, set the flag only
  5.     ok = 1;
  6. }
  7. int main( void )
  8. {
  9.     sigset_t emptymask, waitmask, oldmask;
  10.     // block SIGUSR1 first
  11.     sigemptyset(&waitmask);
  12.     sigaddset(&waitmask, SIGUSR1);
  13.     sigprocmask(SIG_BLOCK, &waitmask, &oldmask);
  14.     // set SIGUSR1 handler
  15.     signal(SIGUSR1, sigusr);
  16.     sigemptyset(&emptymask);
  17.     while (1) {
  18.         // waiting for SIGUSR1 to set ok
  19.         while (ok == 0)
  20.             sigsuspend(&emptymask);
  21.         // sigsuspend return, SIGUSR1 has come, and now SIGUSR1 is blocked
  22.         // reset the flag, so the process will keep waiting after the
  23.         // following things are done
  24.         ok = 0;
  25.         //
  26.         // other things need to do here
  27.         //
  28.     }
  29.     exit(0);
  30. }

我对 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 其它函数

  1. int main( void ) {
  2.     printf( "sys_siglist:\n" );
  3.     printf( "SIGUSR1: %s\n" , sys_siglist[SIGUSR1]);
  4.     printf( "SIGSEGV: %s\n" , sys_siglist[SIGSEGV]);
  5.     printf( "SIGHUP: %s\n\n" , sys_siglist[SIGHUP]);
  6.     printf( "strsignal:\n" );
  7.     printf( "SIGUSR1: %s\n" , strsignal(SIGUSR1));
  8.     printf( "SIGSEGV: %s\n" , strsignal(SIGSEGV));
  9.     printf( "SIGHUP: %s\n\n" , strsignal(SIGHUP));
  10.     printf( "psignal:\n" );
  11.     psignal(SIGUSR1, "SIGUSR1" );
  12.     psignal(SIGSEGV, "SIGSEGV" );
  13.     psignal(SIGHUP, "SIGHUP" );
  14.     exit(0);
  15. }

输出形式如下:

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() ,设置全局标志等 ) 交给主程序处理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值