信号是由内核(kernel)管理的。信号的产生方 式多种多样,它可以是内核自身产生的,比如出现硬件错误(比如出现分母为0的除法运算,或者出现segmentation fault),内核需要通知某一进程;也可以是其它进程产生的,发送给内核,再由内核传递给目标进程。

        内核中针对每一个进程都有一个表存储相关信息(房间 的信箱)。当内核需要将信号传递给某个进程时,就在该进程相对应的表中的适当位置写入信号(塞入纸条),这样,就生成(generate)了信号。当该进程执行系统调用时,在系统调用完成后退出内核时,都会顺便查看信箱里的信息。如果有信号,进程会执行对应该信号的操作(signal action, 也叫做信号处理signal disposition),此时叫做执行(deliver)信号。从信号的生成到信号的传递的时间,信号处于等待(pending)状态(纸条还没有被查看)。我们同样可以设计程序,让其生成的进程阻塞(block)某些信号,也就是让这些信号始终处于等待的状态,直到进程取消阻塞(unblock)或者无视信号。


信号所传递的每一个整数都被赋予了特殊的意义,并有一个信号名对应该整数。常见的信号有SIGINT, SIGQUIT, SIGCONT, SIGTSTP, SIGALRM等。这些都是信号的名字。可以通过

 

$man 7 signal来查阅更多信号。

SIGINT      当键盘按下ctrl+c从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是中断(INTERRUPT)该进程。

SIGQUIT    当键盘按下ctrl+\从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作时退出(QUIT)该进程。

SIGTSTP     当键盘按下ctrl+z从shell中发粗信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是暂停(STOP)该进程。

SIGCONT    用于通知暂停的进程继续。

SIGALRM     起到定时器的作用,通常是程序在一定时间之后才生成该信号。


信号的生命周期

  一个完整的信号生命周期可以分为3个重要阶段,这3个阶段由4个重要事件来刻画的;信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数。这里信号的产生、注册、注销等是指信号的内部实现机制,而不是信号的函数实现(不受我们的掌控)。因此信号注册与否与后面讲到的发送信号函数(如 kill()等)及信号安装函数(如 signal()等)无关,只与信号值有关。

相邻两个事件的时间间隔构成信号生命周期的一个阶段,如下图,注意这里的信号处理有多种方式,一般是由内核完成的,当然也可以由用户进程来完成。


信号的处理包括信号的发送、捕捉和处理,它们有各自相对应的常见函数:

发生信号的函数: kill()、raise()。

捕捉信号的函数: alarm()、pause()。

处理信号的函数: signal()、sigaction()。

 

信号函数

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

函数说明:

kill()函数可以发送信号给进程或进程组。

下表列出了kill()的格式


 

raise() 
#include <signal.h> 
int raise(int signo) 
向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

 

sigqueue() 
#include <sys/types.h> 
#include <signal.h> 
int sigqueue(pid_t pid, int sig, const union sigval val) 
调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数;sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

alarm() 
#include <unistd.h> 
unsigned int alarm(unsigned int seconds) 
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。 
返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

setitimer() 
#include <sys/time.h> 
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)); 
setitimer()比alarm功能强大,支持3种类型的定时器:

  • ITIMER_REAL:设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
  • ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
  • ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;

Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构itimerval的一个实例,结构itimerval形式见附录1。第三个参数可不做处理。

Setitimer()调用成功返回0,否则返回-1。

 

abort() 
#include <stdlib.h> 
void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

 

实例

该例首先使用 fork()创建了一个子进程,接着为了保证子进程不在父进程调用kill()之前退出,在子进程中使用raise()函数向自身发送 SIGSTOP信号,使子进程暂停。接下来在父进程中调用kill()向子进程发送信号,在该实验中使用的是SIGKILL。实验代码如下:

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t pid;
  int ret;
  /*创建一个子进程*/
  if((pid=fork())<0) /*出错处理*/
  {
    printf("Fork error\n");
    exit(1);
  }
  if(pid==0) /*子进程*/
  {
    /*在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停*/
    printf("I am child progress(pid:%d).I am waiting for any signal\n",getpid());
    raise(SIGSTOP);
    printf("I am child progress(pid:%d).I am killed by progress:%d\n",getpid(),getppid());
    exit(0);
  }
  else /*父进程*/
  {
    sleep(2); /*先让父进程休眠,让子进程执行,建议你把这句话去掉试一试*/
    /*在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作*/
    if((waitpid(pid,NULL,WNOHANG))==0)
    { /*若pid指向的子进程没有退出,则返回0,且父进程不阻塞,继续执行下边的语句*/
      if((ret=kill(pid,SIGKILL))==0)
      {
        printf("I am parent progress(pid:%d).I kill %d\n",getpid(),pid);
      }
    }
    waitpid(pid,NULL,0);/*等待子进程退出,否则就一直阻塞*/
    exit(0);
  }
}/* end */

编译运行

[wss@localhost sig2]$gcc signal.c
[wss@localhost sig2]$./a.out
I am child progress(pid:22021).I am waiting for any signal
I am parent progress(pid:22020).I kill 22021
[wss@localhost sig2]$

信号捕捉函数: alarm()、pause()

函数说明:

alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm()之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。

pause()函数用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到。


例子:

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
  int ret;
  ret = alarm(5);
  pause();
  printf("I have wake up!");
  return 0;
}

[wss@localhost test]$gcc test.c
[wss@localhost test]$./a.out
Alarm clock
[wss@localhost test]$

可以看到12行的语句根本就没执行,其实想想程序的执行流程就很清除,首先程序定时,执行到11行pause();时进程会被挂起,当计时到,发出信号SIGALARM,这时pause()捕捉到信号,进程直接被终止。