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,按系统默认处理SIGINTSIGUSR1SIGUSR2Linux提供的用户定义信号,可用于任何应用程序。主程序什么都不干,只用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. static void sigusr(int signum)
  17. {
  18.     pr_mask(“int sigusr”);
  19.     if(signum == SIGUSR1)
  20.         printf(“SIGUSR1 received/n”);
  21.     else if(signum == SIGUSR2)
  22.         printf(“SIGUSR2 received/n”);
  23.     else
  24.         printf(“signal %d received/n”, signum);
  25. }
  26. int main(void)
  27. {
  28.     if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
  29.         printf(“error catching SIGUSR1/n”);
  30.         exit(1);
  31.     }
  32.     if(signal(SIGUSR2, SIG_IGN) == SIG_ERR) {
  33.         printf(“error ignoring SIGUSR2/n”);
  34.         exit(1);
  35.     }
  36.     if(signal(SIGINT, SIG_DFT) == SIG_ERR) {
  37.         printf(“error setting SIGINT to default/n”);
  38.         exit(1);
  39.     }
  40.     while(1)
  41.         pause();
  42.     exit(0);
  43. }


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


由此我们可以得出,Linuxsigaction()的默认动作是不自动重启动被中断的系统调用,因此如果我们在使用sigaction()时需要自动重启动被中断的系统调用,就需要使用sigactionSA_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


发送信号的killraise函数


int kill(pid_t pid, int sig);

int raise(int sig);


kill()发送信号给指定进程,raise()发送信号给进程本身。对kill()pid,有如下描述:


pid > 0 将信号发送给IDpid的进程

pid == 0 将信号发送给与发送进程属于同意个进程组的所有进程

pid < 0 将信号发送给进程组ID等于pid绝对值的所有进程

pid == -1 将信号发送给该进程有权限发送的系统里的所有进程


所有信号的发送都要先经过权限检查,如果进程没有相应发送的权限,kill()会出错返回,并把errno设为EPERM。但也有一个例外,对SIGCONT,进程可以将它发送给当前会话的所有进程。


产生时钟信号SIGALRMalarm函数


unsigned int alarm(unsigned int seconds);


alarm()函数可设置一个计时器,计时器超时就产生SIGALRM信号。由于每个进程只能有一个SIGALRM处理程序,所以只能为一个进程设置一个计时器,所以alarm()setitimer()会共享同一个SIGALRM信号和该信号的处理函数。也就是说,alarm()setitimer()彼此会互相影响。调用alarm(),会使先前设置的计时器失效,并把没有超时的时间作为当前alarm的返回值。如先前设置的时钟为100秒,当前调用alarm()时才经过30秒,剩余的70秒就作为alarm()的返回值,并用alarm()中指定的秒数重新设置计时器。如果seconds0,则会取消先前设置的计时器,并将其余留值作为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中返回。如果setNULL,则不修改当前信号屏蔽字,而将其通过oldset返回;如果oldsetNULL,则不会返回旧的信号屏蔽字。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_handlersa_sigaction用于指定信号处理函数,但要注意,二者只能用其一,因为它们在内部可能会实现为union结构。除了在为sa_flags指定SA_SIGINFO标志时,会使用sa_sigaction字段外,其他情况下都应该只用sa_handler字段。


sa_mask用于指定在当前信号处理程序执行期间,需要阻塞的信号集。如在处理SIGUSR1期间,我们希望暂时阻塞SIGUSR2,就应该把SIGUSR2加到SIGUSR1sa_mask中。信号处理程序返回后,会自动解除对SIGUSR2的阻塞,详见例程4


sa_flags用于指定信号处理动的选项标志,详见手册。这里我想说的是SA_RESTARTSA_SIGINFOSA_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.     sigset_t set; 
  3.     printf("SIGUSR2 recieved/n"); 
  4.     // unblock SIGUSR1
  5.     sigprocmask(SIG_SETMASK, NULL, &set); 
  6.     sigdelset(&set, SIGUSR1); 
  7.     sigprocmask(SIG_SETMASK, &set, NULL); 
  8. static void handler(int signum, siginfo_t * info, void * context)
  9.     // dump signal information
  10.     printf("si_signo: %d/n", info->si_signo); 
  11.     printf("si_errno: %d/n", info->si_errno); 
  12.     printf("si_code: %d/n", info->si_code); 
  13.     printf("si_pid: %d/n", info->si_pid); 
  14.     printf("si_uid: %d/n", info->si_uid); 
  15. int main(void)
  16.     struct sigaction act_usr1; 
  17.     struct sigaction act_usr2; 
  18.     sigset_t mask;
  19.     act_usr1.sa_flags = SA_SIGINFO; 
  20.     //act_usr1.sa_handler = sigusr; 
  21.     sigemptyset(&act_usr1.sa_mask); 
  22.     act_usr1.sa_sigaction = handler; 
  23.     sigaction(SIGUSR1, &act_usr1, NULL); 
  24.     act_usr2.sa_flags = 0; 
  25.     act_usr2.sa_handler = sig_usr2; 
  26.     sigemptyset(&act_usr2.sa_mask); 
  27.     sigaction(SIGUSR2, &act_usr2, NULL); 
  28.     // block SIGUSR1
  29.     sigprocmask(SIG_SETMASK, NULL, &mask); 
  30.     sigaddset(&mask, SIGUSR1); 
  31.     sigprocmask(SIG_SETMASK, &cmask, NULL); 
  32.     while(1) 
  33.         pause(); 
  34.     exit(0); 
  35. }

$ ./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阻塞期间,我们向进程发送了5SIGUSR1,而解除阻塞后,内核只递送了一个SIGUSR1,说明Linux并不支持信号排队。另外我们还可以看到,si_signo是收到的信号的数值;si_pid是发送进程的进程IDps输出我的终端进程ID正是3945si_uid是发送进程的有效用户ID,而我的用户ID也正是500。对于siginfo结构中的其它成员,我没有打印,有兴趣的可以自己研究。


信号跳转函数sigsetjumpsiglongjump


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


吼吼,花了一天时间,终于把它整理完了,乱七八糟,但会方便各位查阅吧!其中大部分内容参考了《UNIX环境高级编程》,有不对的地方大家尽管拍砖,但不要骂俺抄袭,会脸红的^_^。图灵教育说的好,站在巨人的肩膀上,Standing on shoulders of the giants, I think I can fly...


参考资料:

[1] []W. Rechard Stevens/Stephen A. Rago UNIX环境高级编程 (2). 人民邮电出版社. 2006

 

 

 

 

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
信号处理Linux系统中的一个重要概念,用于处理进程间通信和异常情况。信号是由操作系统或其他进程发送给进程的通知,用于通知进程发生了某个事件或异常情况。信号可以被进程捕获和处理,也可以被忽略或使用默认处理方式。 在Linux中,信号可以由多种情况触发,比如按下CTRL+C键产生的SIGINT信号,非法内存访问产生的信号,硬件故障产生的信号,以及环境切换等。进程可以通过调用signal函数来注册信号处理函数,以捕获和处理特定的信号signal函数的原型如下: ```c typedef void (*sighandler)(int); sighandler signal(int signum, sighandler handler); ``` 其中,signum是需要处理的信号编号,handler是信号的处理函数。处理函数可以是用户自定义的函数,也可以是预定义的常量SIG_IGN表示忽略该信号,或者SIG_DFL表示使用默认的信号处理方式。 在信号处理函数中,可以执行一些特定的操作来处理信号,比如打印日志、保存数据、发送信号给其他进程等。处理函数可以是空函数,表示仅仅捕获信号但不做任何处理。 需要注意的是,一个进程可以屏蔽掉大多数的信号,除了SIGSTOP和SIGKILL这两个信号是无法被屏蔽的。信号有优先级,当一个进程有多个未决信号时,内核将按照发送的顺序来递送信号。值越小的信号越先被递送。 在Linux中,可以通过编写信号处理程序来处理不同的信号,并根据需要执行特定的操作。通过信号处理,可以实现进程间通信、优雅地关闭进程或处理异常情况等功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值