你的位置:首页 > 操作系统

[操作系统]UNIX高级环境编程(13)信号


信号就是软中断。

信号提供了异步处理事件的一种方式。例如,用户在终端按下结束进程键,使一个进程提前终止。

 

1 信号的概念

每一个信号都有一个名字,它们的名字都以SIG打头。例如,每当进程调用了abort函数时,都会产生一个SIGABRT信号。

每一个信号对应一个正整数,定义在头文件<signal.h>中。

没有信号对应整数0,kill函数使用信号编号0表示一种特殊情况,所以信号编号0又叫做空信号(null signal)。

下面的各种情况会产生一个信号:

  • 当用户在终端按下特定的键时,会产生信号。例如,当用户按下DELETE按键(或Control-C)时,会产生一个中断信号(interrupt signal,SIGINIT),该信号使得一个运行中的程序终止。
  • 硬件异常可以产生信号。会引发硬件异常的情况如除以0,非法内存引用(invalid memory reference)等。这种情况会被硬件检测到,并通知内核,然后内核产生相应的信号通知对应的运行进程。例如,当一个进程执行了一个非法的内存引用,会触发SIGSEGV信号。 
  • kill函数允许当前进程向其他的进程或者进程组发送任意的信号。当然,这种方法存在限制:我们必须是信号接收进程的所有者,或者我们必须是超级用户(superuser)。
  • kill命令的作用和kill函数类似。这个命令多用户杀死后台进程。
  • 软件异常可以根据不同的条件产生不同的信号。例如:网络连接中接受的数据超出边界时,会触发SIGURG信号。

对于进程来说,信号是随机产生的,所以进程不能简单地根据检测某个变量是否改变来判断信号是否发生,而应该告诉内核“当这个信号发生时,做下面的这些事情”。

我们告诉内核当某个信号发生时做的事情叫做信号处理函数。信号处理函数有三种功能可供选择:

 

  • 忽略该信号。该行为适用于大部分的信号,除了两个信号不能被忽略:SIGKILL和SIGSTOP。这两个信号无能被忽略,是因为其作用是为内核和超级用户提供了一种杀死或者暂停进程的万无一失的方法(a surefire way)。
  • 捕获该信号。当某个信号发生时,我们告诉进程去执行我们的一段程序。在该程序中,我们可以做任何操作来处理该种情况。两个信号SIGKILL和SIGSTOP不可以被捕获。
  • 执行默认的信号处理程序。每个信号都有一个默认的处理程序,而大部分的信号默认处理程序都是终止该进程。

对于一些信号发生时,会造成进程终止,同时生成一个core文件,该core文件记录了该进程终止时的内存情况,可以帮助调试和调查进程的终止状态。

有几种情况不会生成core文件:

  • 如果进程设置了suid位(chmod u+s file),并且当前用户不是程序文件的所有者;
  • 如果进程设置了guid位(set-group-ID),并且当前用户不是程序文件的组所有者;
  • 如果过户没有当前工作目录的写权限;
  • 如果core文件已经存在,并且用户没有该文件的写权限;
  • 该core文件太大(由参数RLIMIT_CORE限制) 

2 signal函数

函数声明

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);

    Returns: previous disposition of signal if OK, SIG_ERR on err.

 函数声明解析:

void   (*signal(int   signr,   void   (*handler)(int)))(int); 
================================================ 
handler是一个函数指针,指向参数为单参数int,返回类型void的函数 
signal是一个函数指针、这个函数指针指向一个参数为一个int型和一个handler型的指针、返回值是一个指向参数为int、返回值是void的函数的指针的指针。

总结一下: 
    这个复杂的声明可以用下面2种比较简单的型式表达出来,如下: 
第一种型式如下:     
typedef   void   (*handler_pt)(int); 
handler_pt   signal1(int   signum,handler_pt   ahandler); 
第二种型式如下: 
typedef   void   handler_t(int); 
handler_t*   signal2(int   signum,   handler_t*   ahandler); 
------------------------------------------------------ 
以上这两种形式结果是等价的,但也有区别,第一种形式定义的是函数指针类型, 
sizeof(handler_pt)=4//borland   c++   5.6.4   for   win   32,windos   xp   32   platform 
第二种形式定义的是函数类型,如果对他使用sizeof(handler_t)会提示: 
sizeof   may   not   be   applied   to   a   function

参数说明:

  • signo:信号名
  • func:三种取值选择:常量SIG_IGN,常量SIG_DFL,或信号处理函数的地址。如果func的取值为SIG_IGN,则信号发生时忽略处理(除了两个信号SIGKILL和SIGSTOP)。如果func的取值为SIG_DFL,则调用信号的默认处理函数。

在上面的声明解析中我们可以看到,使用typedef可以简化signal函数的声明,后面对signal函数的调用也将使用简化后的声明:

typedef void Sigfunc(int);

Sigfunc *signal(int, Sigfunc *);

 

 Example

该例子的作用是捕获两个用户自定义的信号,并打印相关的信号信息。

使用函数pause来使程序挂起,知道接收到信号。

Code

#include "apue.h"

 

staticvoid sig_usr(int);   /* one handler for both signals */

          

int      

main(void)

{        

    if (signal(SIGUSR1, sig_usr) == SIG_ERR)

        err_sys("can't catch SIGUSR1");

    if (signal(SIGUSR2, sig_usr) == SIG_ERR)

        err_sys("can't catch SIGUSR2");

    for ( ; ; )

        pause();

}   

    

staticvoid

sig_usr(int signo)      /* argument is signal number */

{   

    if (signo == SIGUSR1)

        printf("received SIGUSR1\n");

    else if (signo == SIGUSR2)

        printf("received SIGUSR2\n");

    else

        err_dump("received signal %d\n", signo);

}

执行结果:

NewImage

执行时,我们先让该程序后台执行,然后调用kill命令向该进程发送信号。

kill并不真的会杀死进程,而只是发送信号。所以kill并不是很准确的描述了该命令的作用。

当我们调用kill 2081命令时,进程被终止,因为在信号处理函数中并没有处理该信号,而该信号的默认处理程序为终止进程。

 

程序启动态

程序执行时,所有信号的状态都为默认值或者被忽略。

如果程序调用了exec系函数,则会改变信号的自定义处理函数为它的默认处理程序,因为在原来的程序中的处理函数地址对于新的程序来说是没有意义的。

例如,在一个交互式的shell中,启动一个后台进程,会设置该进程的中断和退出信号的处理动作为忽略,这样,当用户在shell中键入中断命令时,只会中断前台进程,而不会影响后台进程。

这个例子也告诉我们了signal函数的一个限制:我们无法确认当前进程的一些信号的处理动作,除非我们现在改变它们。后面我们将学习sigaction函数来确认一个信号的处理动作,而不需要改变它们。

 

程序创建

当调用fork函数时,子进程继承了父进程的信号处理函数。因为子进程拷贝了父进程的内存,所以信号处理函数的地址对于子进程来说也是有意义的。

 

3 不可靠的信号(Unreliable Signals)

在早期的Unix系统中,信号是不可靠的。

不可靠的意思是,信号是有可能丢失的。即信号发生了,但是进程没有捕获它。

我们希望内核可以记住信号,当我们ready时,告诉我们该信号发生,让我们去处理。

早期的系统,对于信号机制的实现还有一个问题:当信号发生,执行了信号处理函数,该信号的处理函数就被置为默认的信号处理程序。因此,早期的关于信号的程序框架如下所示:

NewImage

这段代码的问题在于,在SIGINT信号发生后,且在对它的信号处理函数重置为sig_int前,有一个时间差,在这个时间差内,可能再发生一次SIGINT信号。

如果第二次SIGINT发生在信号处理函数重置前,则会执行它的默认处理动作,即终止进程。

早期实现还有一个问题,就是如果进程不希望某个信号发生,它只能选择忽略它,而无法将该信号关闭。

一种使用场景是:我们不希望被信号打断,但是希望记住它们发生过。代码可能如下:

NewImage

在这里,我们假设该信号只发生一次。

代码的目的在于:我们等待信号发生,信号发生之前,进程停止,等待。

代码的问题在于,有一个时间差,可能会发生异常情况,如果代码的执行序列如下:

1 信号发生

2 while (sig_int_flag == 0)

3 sig_int_flag= 1

4 pause()

这时,进程暂停挂起,等待信号发生,但是实际上该信号已经发生过了。这就导致了信号没有被捕获。

 

4 可中断系统调用

早期Unix操作系统的一个特性是:如果一个进程阻塞在一个“慢”系统调用,则该进程会收到一个信号,导致该进程被中断。该系统调用返回一个错误,并且errno设置为EINTR。

系统调用被分为两类:慢系统调用和其他系统调用。慢系统调用是那些可能永久阻塞的系统调用。慢系统调用包括:

  • 读数据函数可能阻塞调用者,如果数据不是特定的格式;
  • 写数据函数可能阻塞调用者,如果数据不能按照特定的格式立刻被接收。
  • 按照某种格式打开某个文件
  • pause函数和wait函数
  • 特定的ioctl操作
  • 一些进程间通信函数

对于可中断系统调用,我们需要在代码中处理errno EINTR:

NewImage

为了避免需要显式处理可中断系统调用,一些可中断系统调用在发生阻塞时会自动重启。

这些会自动重启的可中断系统调用包括:ioctl, read, readv, write, writev, wait和waitepid。

如果某些应用并不希望这些系统调用自动重启,可以该系统调用单独设置SA_RESTART。

 

5 可重入函数

信号的发生导致程序的指令执行顺序被打乱。

但是在信号处理函数中,无法知道原进程的执行情况。

如果原进程这个在分配内存或者释放内存,或者调用了修改static变量的函数,并在信号处理函数中再次调用该函数,会发生不可预期的结果。

在信号处理函数中可以安全调用的函数称为可重入函数,也叫做异步信号安全的函数。除了保证可重入,这些函数还会阻塞可能导致结果不一致的信号。

如果函数满足下面的一种或者几种条件,则说明是不可重入的函数:

  • 使用static数据结构
  • 调用malloc或free
  • 标准IO库中的函数,因为大部分的标准IO函数都使用了全局数据结构

 

6 SIGCLD语义

一直容易混淆的两个信号是SIGCLD和SIGCHLD。

SIGCLD来自System V,而SIGCHLD来自BSD和POSIX.1。

BSD SIGCHLD的语义:当该信号发生时,说明子进程的状态发生了改变,这时我们需要调用wait函数确认状态的变化。

对于System V系统中,对信号SIGCLD的处理说明如下:

  1. 如果进程设置信号SIGCLD的处理动作为SIG_IGN,该进程的子进程将不会变成僵尸进程。这和默认的处理动作(SIG_DFL)是不同的,虽然默认的动作也是忽略,但是对于SIG_IGN,如果随后调用了wait函数,调用进程会阻塞直到所有的子进程都终止,wait返回-1,并且设置errno为ECHILD。默认处理动作(SIG_DFL)是没有后面的动作的。
  2. 如果我们对信号SIGCLD设置了捕获,则内核会立刻检查是否有任何子进程被等待,如果有,调用信号处理函数。

 

7 可靠信号及其语义

 我们先定义几个信号相关的概念:

  • 信号产生:如果某一事件导致信号的发生,则叫做信号产生。该事件可能是硬件异常、软件条件、终端产生的信号或者kill函数传递的信号等。当信号产生,内核需要在进程表中设置某个标志位。
  • 信号送达:当信号处理函数被执行,则信号送达。
  • 信号挂起:在信号产生和送达之间的时间叫做信号挂起。
  • 信号阻塞:进程可以选择不接收某个信号的传达,叫做阻塞该信号。如果被阻塞的信号的处理动作为默认处理程序或者被捕获处理,则该信号会一直处于挂起状态,直到进程解除对该信号的阻塞或者改变该信号的处理动作为忽略。系统在信号传递时决定如何处理被阻塞信号,而不是信号产生时。函数sigpending的作用就是让进程决定哪些信号被阻塞和挂起。

 可靠机制,不同的标准对于异常情况有不同的处理:

  • 如果一个被阻塞的信号多次产生,内核简单地传递该信号一次。
  • 如果多个信号等待被传达,POSIX.1并不关注信号的传达顺序,而The Rationale for POSIX.1标准会保证和进程的当前状态相关的信号先传达。
  • 每个进程都有一个信号掩码来决定是否屏蔽某个信号,信号掩码的每一位都对应一个信号,如果要阻塞某个信号,则将对应的信号置为1。
  • 数据结构sigset_t被定义用来记录信号掩码(signal mask)

 

 参考资料:

《Advanced Programming in the UNIX Envinronment 3rd》