首页 > 代码库 > Linux 进程间通信系列之 信号

Linux 进程间通信系列之 信号

信号(Signal)

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号
语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又
能够统一对外接口,用sigaction函数重新实现了signal函数)

信号种类

技术分享

 

每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。大多数信号的默认操作是结束接收信号的进程;然而,一个进程通常可以请求系统采取某些代替的操作,各种代替操作是:

  • 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号  不可以被忽略:SIGKILL,它将结束进程;SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
  • 恢复信号的默认操作。
  • 执行一个预先安排的信号处理函数。进程可以登记特殊的信号处理函数。当进程收到信号时,信号处理函数将像中断服务例程一样被调用,当从该信号处理函数返回时,控制被返回给主程序,并且继续正常执行。

但是,信号和中断有所不同。中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间。

那么,什么时候检测和响应信号呢?通常发生在两种情况下:

  • 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前夕;
  • 当前进程在内核中进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间。

 

信号本质

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

 

关于信号处理机制的原理(内核角度)

内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要 看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。    

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态(上面的例子程序中,在步骤5中,解除阻塞后,先打印caught SIGQUIT,再打印SIGQUIT unblocked,即在sigprocmask返回前,信号处理程序先执行),进程在用户态下不会有未处理完的信号。    

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方,接着原来的地方继续运行。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

在信号的处理方法中有几点特别要引起注意。    

第一,在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程在调用signal之前又得 到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在 BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。    

第二个要引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上(若系统调用未睡眠而是在运行,根据上面的分 析,等该系统调用运行完毕后再处理信号),这时该信号引起进程作一次longjmp,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回 时,进程就象从系统调用返回一样,但返回了一个错误如-1,并将errno设置为EINTR,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内 核可以自动地重新开始系统调用,或者手如上面所述手动设置重启。    

第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不 到进程曾经被唤醒,而是象没有发生过该信号一样。所以能够使pause、sleep等函数从挂起态返回的信号必须要有信号处理函数,如果没有什么动作,可 以将处理函数设为空。    

第四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程正常或异常终止时,内核都向其父进程发一个SIGCLD 信号,缺省情况下,父进程忽略该信号,就象没有收到该信号似的,如果父进程希望获得子进程终止的状态,则应该事先用signal函数为SIGCLD信号设 置信号处理程序,在信号处理程序中调用wait。

SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,则 什么也不做。其实wait不一定放在信号处理函数中,但这样的话因为不知道子进程何时终止,在子进程终止前,wait将使父进程挂起休眠。

信号生命周期

技术分享

 

 

1.signal()

#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

 

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigroutine(int dunno)

{ /* 信号处理例程,其中dunno将会得到信号的值 */

    switch (dunno) {
    case 1:
        printf("Get a signal -- SIGHUP ");
        break;
    case 2:
        printf("Get a signal -- SIGINT ");
        break;
    case 3:
        printf("Get a signal -- SIGQUIT ");
        break;
    }
    return;
}

int main() {
    printf("process id is %d ", getpid());
    signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法
    signal(SIGINT, sigroutine);
    signal(SIGQUIT, sigroutine);

    for (;;);
}

 

其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出。该程序执行的结果如下:

 

localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的结果
Get a signal -SIGQUIT //按下Ctrl-得到的结果
//按下Ctrl-z将进程置于后台
 [1]+ Stopped ./sig_test
localhost:~$ bg
 [1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向进程发送SIGKILL信号,终止进程

 

2. 信号的发送

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

 

2.1    kill()

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid,int signo)

 

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

 

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

 

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

 

2.2    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指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

 

2.3    alarm()

#include <unistd.h>
unsigned int alarm(unsigned int seconds)

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

 

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

 

2.4    setitimer()

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:

#include <sys/time.h>

 

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

 

定时器中的参数value用来指明定时器的时间,其结构如下:

struct itimerval {
        struct timeval it_interval; /* 下一次的取值 */
        struct timeval it_value; /* 本次的设定值 */

};

该结构中timeval结构定义如下:

struct timeval {
       long tv_sec; /**/
      long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
int sec;
void sigroutine(int signo) {
    switch (signo) {
    case SIGALRM:
        printf("Catch a signal -- SIGALRM ");
        break;
    case SIGVTALRM:
        printf("Catch a signal -- SIGVTALRM ");
        break;
    }
    return;
}
int main()
{
    struct itimerval value, ovalue, value2;
    sec = 5;
    printf("process id is %d ", getpid());
    signal(SIGALRM, sigroutine);
    signal(SIGVTALRM, sigroutine);
    value.it_value.tv_sec = 1;
    value.it_value.tv_usec = 0;
    value.it_interval.tv_sec = 1;
    value.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &value, &ovalue);
    value2.it_value.tv_sec = 0;
    value2.it_value.tv_usec = 500000;
    value2.it_interval.tv_sec = 0;
    value2.it_interval.tv_usec = 500000;
    setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

    for (;;);
}

该例子的屏幕拷贝如下:

localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM

2.5    abort()

#include <stdlib.h>

void abort(void);

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

2.6    raise()

#include <signal.h>

int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

3.信号集及信号集操作函数:

  信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用

信号集被定义为一种数据类型:

typedef struct {
                       unsigned long sig[_NSIG_WORDS];
} sigset_t

下面是为信号集操作定义的相关函数:

#include <signal.h>
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(const sigset_t *set, int signum);

sigemptyset(sigset_t
*set) //初始化由set指定的信号集,信号集里面的所有信号被清空; sigfillset(sigset_t *set) //调用该函数后,set指向的信号集中将包含linux支持的64种信号; sigaddset(sigset_t *set, int signum) //在set指向的信号集中加入signum信号; sigdelset(sigset_t *set, int signum) //在set指向的信号集中删除signum信号; sigismember(const sigset_t *set, int signum) //判定信号signum是否在set指向的信号集中

4.信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。

下面是与信号阻塞相关的几个函数:

#include <signal.h>
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));
int  sigpending(sigset_t *set));
int  sigsuspend(const sigset_t *mask));

 

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号

SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞

SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

 

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

 

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。

    是阻塞函数,对参数信号屏蔽,对没有指定的参数不屏蔽,但当没有屏蔽信号处理函数调用sigsuspend 返回

sigsuspend 返回后将恢复调用之前的信号掩码。

sigsuspend 和sigprocmask是有区别的

sigprocmask的整个过程不能被中断,例如系统备份整个过程不能被中断

sigsuspend 是在某一时候不能被中断,例如拷贝一个文件夹下的电影,在考完一部之后,是可以中断的

sigsuspend返回条件

        1.信号发送,并且信号是非屏蔽信号

         2.信号必须要处理,而且处理函数放回后,sigsuspend才返回

信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

 

#include <stdio.h>     
#include <signal.h>     
void checkset();     

void func();     
void main()     
{     
     sigset_tblockset,oldblockset,pendmask;     
     printf("pid:%ld\n",(long)getpid());     

     signal(SIGINT,func); //信号量捕捉函数,捕捉到SIGINT,跳转到函数指针func处执行     

    sigemptyset(&blockset); //初始化信号量集     
    sigaddset(&blockset,SIGTSTP); //将SIGTSTP添加到信号量集中     
    sigaddset(&blockset,SIGINT);//将SIGINT添加到信号量集中     

   sigprocmask(SIG_SETMASK,&blockset,&oldblockset); //将blockset中的SIGINT,SIGTSTP阻塞掉,并保存当前信号屏蔽字     

     /*执行以下程序时,不会被信号打搅*/ 
    checkset();     
    sleep(5);     
     sigpending(&pendmask); //检查信号是悬而未决的     
     if(sigismember(&pendmask,SIGINT)) //SIGINT是悬而未决的。所谓悬而未决,是指SIGQUIT被阻塞还没有被处理     
         printf("SIGINTpending\n");     

     /*免打搅结束*/ 

     sigprocmask(SIG_SETMASK,&oldblockset,NULL); //恢复被屏蔽的信号SIGINT SIGTSTP     
     printf("SIGINTunblocked\n");     
     sleep(6);     
}     

void checkset()     
{     
     sigset_tset;     
     printf("checksetstart:\n");     
     if(sigprocmask(0,NULL,&set)<0)     
     {     
     printf("checksetsigprocmask error!!\n");     
     exit(0);     
     }     
     if(sigismember(&set,SIGINT))     
     printf("sigint\n");     
         
     if(sigismember(&set,SIGTSTP))     
     printf("sigtstp\n");     

     if(sigismember(&set,SIGTERM))     
     printf("sigterm\n");     

     printf("checksetend\n"); 

}     
void func()     
{     
     printf("hellofunc\n");     
}

 

Linux 进程间通信系列之 信号