Linux信号机制

Linux信号(signal) 机制分析php

【摘要】本文分析了Linux内核对于信号的实现机制和应用层的相关处理。首先介绍了软中断信号的本质及信号的两种不一样分类方法尤为是不可靠信号的原理。接着分析了内核对于信号的处理流程包括信号的触发/注册/执行及注销等。最后介绍了应用层的相关处理,主要包括信号处理函数的安装、信号的发送、屏蔽阻塞等,最后给了几个简单的应用实例。html

 

【关键字】软中断信号,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_tlinux

 

1       信号本质

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求能够说是同样的。信号是进程间通讯机制中惟一的异步通讯机制,一个进程没必要经过任何操做来等待信号的到达,事实上,进程也不知道信号到底何时到达。进程之间能够互相经过系统调用kill发送软中断信号。内核也能够由于内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还能够传递附加信息。程序员

 

收到信号的进程对各类信号有不一样的处理方法。处理方法能够分为三类:session

第一种是相似中断的处理程序,对于须要处理的信号,进程能够指定处理函数,由该函数来处理。数据结构

第二种方法是,忽略某个信号,对该信号不作任何处理,就象未发生过同样。多线程

第三种方法是,对该信号的处理保留系统的默认值,这种缺省操做,对大部分的信号的缺省操做是使得进程终止。进程经过系统调用signal来指定进程对某个信号的处理行为。架构

 

2       信号的种类

能够从两个不一样的分类角度对信号进行分类:异步

可靠性方面:可靠信号与不可靠信号;函数

与时间的关系上:实时信号与非实时信号。

 

2.1    可靠信号与不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

 

随着时间的发展,实践证实了有必要对信号的原始机制加以改进和扩充。因为原来定义的信号已有许多应用,很差再作改动,最终只好又新增长了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

 

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

 

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是经过sigation()函数实现的,所以,即便经过signal()安装的信号,在信号处理函数的结尾也没必要再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,一样不会丢失。

 

对于目前linux的两个信号安装函数:signal()及sigaction()来讲,它们都不能把SIGRTMIN之前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),并且对SIGRTMIN之后的信号都支持排队。这两个函数的最大区别在于,通过sigaction安装的信号都能传递信息给信号处理函数,而通过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来讲也是同样的。

 

2.2    实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预约义值,每一个信号有了肯定的用途及含义,而且每种信号都有各自的缺省动做。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

 

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

 

3       信号处理流程

 

 

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来讲,能够分为三个阶段:

信号诞生

信号在进程中注册

信号的执行和注销

 

 

3.1    信号诞生

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

 

这里按发出信号的缘由简单分类,以了解各类信号:

(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。

(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其余各类硬件错误。

(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。

(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。

(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其余进程发送信号。

(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等状况。

(7) 跟踪进程执行的信号。

 

Linux支持的信号列表以下。不少信号是与机器的体系结构相关的

信号值 默认处理动做 发出信号的缘由

SIGHUP 1 A 终端挂起或者控制进程终止

SIGINT 2 A 键盘中断(如break键被按下)

SIGQUIT 3 C 键盘的退出键被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)发出的退出指令

SIGFPE 8 C 浮点异常

SIGKILL 9 AEF Kill信号

SIGSEGV 11 C 无效的内存引用

SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道

SIGALRM 14 A 由alarm(2)发出的信号

SIGTERM 15 A 终止信号

SIGUSR1 30,10,16 A 用户自定义信号1

SIGUSR2 31,12,17 A 用户自定义信号2

SIGCHLD 20,17,18 B 子进程结束信号

SIGCONT 19,18,25 进程继续(曾被中止的进程)

SIGSTOP 17,19,23 DEF 终止进程

SIGTSTP 18,20,24 D 控制终端(tty)上按下中止键

SIGTTIN 21,21,26 D 后台进程企图从控制终端读

SIGTTOU 22,22,27 D 后台进程企图从控制终端写

 

处理动做一项中的字母含义以下

A 缺省的动做是终止进程

B 缺省的动做是忽略此信号,将该信号丢弃,不作处理

C 缺省的动做是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部份内容以必定格式转储到文件系统,而且进程退出执行,这样作的好处是为程序员提供了方便,使得他们能够获得进程当时执行时的数据值,容许他们肯定转储的缘由,而且能够调试他们的程序。

D 缺省的动做是中止进程,进入中止情况之后还能从新进行下去,通常是在调试的过程当中(例如ptrace系统调用)

E 信号不能被捕获

F 信号不能被忽略

 

3.2    信号在目标进程中注册

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。若是信号发送给一个正在睡眠的进程,若是进程睡眠在可被中断的优先级上,则唤醒进程;不然仅设置进程表中信号域相应的位,而不唤醒进程。若是发送给一个处于可运行状态的进程,则只置相应的域便可。

 

进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

 

第三个成员是进程中全部未决信号集,第1、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每一个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

 

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每一个信号占用一位)中,而且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,代表进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

 

当一个实时信号发送给一个进程时,无论该信号是否已经在进程中注册,都会被再注册一次,所以,信号不会丢失,所以,实时信号又叫作"可靠信号"。这意味着同一个实时信号能够在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即全部诞生的实时信号都会在目标进程中注册)。

 

当一个非实时信号发送给一个进程时,若是该信号已经在进程中注册(经过sigset_t signal指示),则该信号将被丢弃,形成信号丢失。所以,非实时信号又叫作"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

 

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

 

 

3.3    信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,所以,进程必须处于运行状态。当其因为被信号唤醒或者正常调度从新得到CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。若是存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

 

对于非实时信号来讲,因为在未决信号信息链中最多只占用一个sigqueue结构,所以该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来讲,可能在未决信号信息链中占用多个sigqueue结构,所以应该针对占用sigqueue结构的数目区别对待:若是只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。不然待该信号的全部sigqueue处理完毕后再在进程的未决信号集中删除该信号。

 

当全部未被屏蔽的信号都处理完毕后,便可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

 

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。因此,当一个进程在内核态下运行时,软中断信号并不当即起做用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

 

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。若是进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。并且执行用户定义的函数的方法很巧妙,内核是在用户栈上建立一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样作的缘由是用户定义的处理函数不能且不容许在内核态下执行(若是用户定义的函数在内核态下运行的话,用户就能够得到任何权限)。

 

4       信号的安装

若是进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来肯定信号值及进程针对该信号值的动做之间的映射关系,即进程将要处理哪一个信号;该信号被传递给进程时,将执行何种操做。

 

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,固然,sigaction()一样支持非实时信号的安装。sigaction()优于signal()主要体如今支持信号带有参数。

 

4.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信号,终止进程

localhost:~$

 

4.2    sigaction()

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,能够为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义本身的处理函数,将致使信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,能够为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。若是把第2、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

 

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程当中应屏蔽掉哪些信号等等。

sigaction结构定义以下:

struct sigaction {

                       union{

                               __sighandler_t _sa_handler;

                               void (*_sa_sigaction)(int,struct siginfo *, void *);

                       }_u

            sigset_t sa_mask;

            unsigned long sa_flags;

}

 

一、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了能够是用户自定义的处理函数外,还能够为SIG_DFL(采用缺省的处理方式),也能够为SIG_IGN(忽略信号)。

 

二、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(固然一样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构以下:

siginfo_t {

                  int      si_signo;  /* 信号值,对全部信号有意义*/

                  int      si_errno;  /* errno值,对全部信号有意义*/

                  int      si_code;   /* 信号产生的缘由,对全部信号有意义*/

                               union{                               /* 联合数据结构,不一样成员适应不一样信号 */

                                       //确保分配足够大的存储空间

                                       int _pad[SI_PAD_SIZE];

                                       //对SIGKILL有意义的结构

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

 

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可让信号传递一些附加信息。信号能够传递信息对程序开发是很是有意义的。

 

三、sa_mask指定在信号处理程序执行过程当中,哪些信号应当被阻塞。缺省状况下当前信号自己被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程当中由sa_mask指定的信号才被阻塞。

 

四、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另外一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数能够被传递到信号处理函数中,所以,应该为sigaction结构中的sa_sigaction指定处理函数,而不该该为sa_handler指定信号处理函数,不然,设置该标志变得毫无心义。即便为sa_sigaction指定了信号处理函数,若是不设置SA_SIGINFO,信号处理函数一样不能获得信号传递过来的数据,在信号处理函数中对这些信息的访问都将致使段错误(Segmentation fault)。

 

 

5       信号的发送

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

 

5.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,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

 

5.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()系统调用的功能要灵活和强大得多。

 

5.3    alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

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

 

注意,在使用时,alarm只设定为发送一次信号,若是要屡次发送,就要屡次使用alarm调用。

 

5.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

 

5.5    abort()

#include <stdlib.h>

void abort(void);

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

 

5.6    raise()

#include <signal.h>

int raise(int signo)

向进程自己发送信号,参数为即将发送的信号值。调用成功返回 0;不然,返回 -1。

 

6       信号集及信号集操做函数:

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

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

信号集用来描述信号的集合,每一个信号占用一位。Linux所支持的全部信号能够所有或部分的出如今信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操做定义的相关函数:

 

#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指向的信号集中。

 

7       信号阻塞与信号未决:

每一个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的全部信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

#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 返回后将恢复调用以前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

 

 

8       信号应用实例

linux下的信号应用并无想象的那么恐怖,程序员所要作的最多只有三件事情:

安装信号(推荐使用sigaction());

实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);

发送信号,推荐使用sigqueue()。

实际上,对有些信号来讲,只要安装信号就足够了(信号处理方式采用缺省或忽略)。其余可能要作的无非是与信号集相关的几种操做。

 

实例一:信号发送及处理

实现一个信号接收程序sigreceive(其中信号安装由sigaction())。

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        int sig;

        sig=atoi(argv[1]);

       

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=new_op;

       

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

       

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

 

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("receive signal %d", signum);

        sleep(5);

}

说明,命令行参数为信号值,后台运行sigreceive signo &,可得到该进程的ID,假设为pid,而后再另外一终端上运行kill -s signo pid验证信号的发送接收及处理。同时,可验证信号的排队问题。

 

实例二:信号传递附加信息

主要包括两个实例:

向进程自己发送信号,并传递指针参数

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        union sigval mysigval;

        int i;

        int sig;

        pid_t pid;         

        char data[10];

        memset(data,0,sizeof(data));

        for(i=0;i < 5;i++)

                data[i]='2';

        mysigval.sival_ptr=data;

       

        sig=atoi(argv[1]);

        pid=getpid();

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;//三参数信号处理函数

        act.sa_flags=SA_SIGINFO;//信息传递开关,容许传说参数信息给new_op

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

                sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息

        }

}

void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现

{

        int i;

        for(i=0;i<10;i++)

        {

                printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));

        }

        printf("handle signal %d over;",signum);

}

 

这个例子中,信号实现了附加信息的传递,信号究竟如何对这些信息进行处理则取决于具体的应用。

 

不一样进程间传递整型参数:

把1中的信号发送和接收放在两个程序中,而且在发送过程当中传递整型参数。

信号接收程序:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;

        int sig;

        pid_t pid;         

       

        pid=getpid();

        sig=atoi(argv[1]);     

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;

        act.sa_flags=SA_SIGINFO;

        if(sigaction(sig,&act,NULL)<0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("the int value is %d \n",info->si_int);

}

 

 

信号发送程序:

命令行第二个参数为信号值,第三个参数为接收进程ID。

 

#include <signal.h>

#include <sys/time.h>

#include <unistd.h>

#include <sys/types.h>

main(int argc,char**argv)

{

        pid_t pid;

        int signum;

        union sigval mysigval;

        signum=atoi(argv[1]);

        pid=(pid_t)atoi(argv[2]);

        mysigval.sival_int=8;//不表明具体含义,只用于说明问题

        if(sigqueue(pid,signum,mysigval)==-1)

                printf("send error\n");

        sleep(2);

}

 

 

注:实例2的两个例子侧重点在于用信号来传递信息,目前关于在linux下经过信号传递信息的实例很是少,却是Unix下有一些,但传递的基本上都是关于传递一个整数

 

实例三:信号阻塞及信号集操做

#include "signal.h"

#include "unistd.h"

static void my_op(int);

main()

{

        sigset_t new_mask,old_mask,pending_mask;

        struct sigaction act;

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=(void*)my_op;

        if(sigaction(SIGRTMIN+10,&act,NULL))

                printf("install signal SIGRTMIN+10 error\n");

        sigemptyset(&new_mask);

        sigaddset(&new_mask,SIGRTMIN+10);

        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))

                printf("block signal SIGRTMIN+10 error\n");

        sleep(10);

        printf("now begin to get pending mask and unblock SIGRTMIN+10\n");

        if(sigpending(&pending_mask)<0)

                printf("get pending mask error\n");

        if(sigismember(&pending_mask,SIGRTMIN+10))

                printf("signal SIGRTMIN+10 is pending\n");

        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)

                printf("unblock signal error\n");

        printf("signal unblocked\n");

        sleep(10);

}

 

static void my_op(int signum)

{

        printf("receive signal %d \n",signum);

}

 

编译该程序,并之后台方式运行。在另外一终端向该进程发送信号(运行kill -s 42 pid,SIGRTMIN+10为42),查看结果能够看出几个关键函数的运行机制,信号集相关操做比较简单。

 

9       参考鸣谢:

linux信号处理机制(详解),http://www.zxbc.cn/html/20080712/61613.html

Linux环境进程间通讯(二): 信号(上),郑彦兴 (mlinux@163.com)

signal、sigaction、kill等手册,最直接而可靠的参考资料。

http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了许多系统调用、库函数等的在线指南。

http://www.opengroup.org/onlinepubs/007904975/能够在这里对许多关键函数(包括系统调用)进行查询,很是好的一个网址

进程间通讯信号(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html

进程间通讯信号(下)http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html

 

 

 

Linux信号处理机制

在Linux中,信号是进程间通信的一种方式,它采用的是异步机制。当信号发送到某个进程中时,操做系统会中断该进程的正常流程,并进入相应的信号处理函数执行操做,完成后再回到中断的地方继续执行。

须要说明的是,信号只是用于通知进程发生了某个事件,除了信号自己的信息以外,并不具有传递用户数据的功能。

1 信号的响应动做

每一个信号都有本身的响应动做,当接收到信号时,进程会根据信号的响应动做执行相应的操做,信号的响应动做有如下几种:

  • 停止进程(Term)
  • 忽略信号(Ign)
  • 停止进程并保存内存信息(Core)
  • 中止进程(Stop)
  • 继续运行进程(Cont)

用户能够经过signalsigaction函数修改信号的响应动做(也就是常说的“注册信号”,在文章的后面会举例说明)。另外,在多线程中,各线程的信号响应动做都是相同的,不能对某个线程设置独立的响应动做。

2 信号类型

Linux支持的信号类型能够参考下面给出的列表。

2.1 在POSIX.1-1990标准中的信号列表

信号 动做 说明
SIGHUP 1 Term 终端控制进程结束(终端链接断开)
SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发
SIGQUIT 3 Core 用户发送QUIT字符(Ctrl+/)触发
SIGILL 4 Core 非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT 6 Core 调用abort函数触发
SIGFPE 8 Core 算术运行错误(浮点运算错误、除数为零等)
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV 11 Core 无效内存引用(试图访问不属于本身的内存空间、对只读内存空间进行写操做)
SIGPIPE 13 Term 消息管道损坏(FIFO/Socket通讯时,管道未打开而进行写操做)
SIGALRM 14 Term 时钟定时信号
SIGTERM 15 Term 结束程序(能够被捕获、阻塞或忽略)
SIGUSR1 30,10,16 Term 用户保留
SIGUSR2 31,12,17 Term 用户保留
SIGCHLD 20,17,18 Ign 子进程结束(由父进程接收)
SIGCONT 19,18,25 Cont 继续执行已经中止的进程(不能被阻塞)
SIGSTOP 17,19,23 Stop 中止进程(不能被捕获、阻塞或忽略)
SIGTSTP 18,20,24 Stop 中止进程(能够被捕获、阻塞或忽略)
SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发
SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

:其中SIGKILLSIGSTOP信号不能被捕获、阻塞或忽略。

2.2 在SUSv2和POSIX.1-2001标准中的信号列表

信号 动做 说明
SIGTRAP 5 Core Trap指令触发(如断点,在调试器中使用)
SIGBUS 0,7,10 Core 非法地址(内存地址对齐错误)
SIGPOLL   Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS 12,31,12 Core 无效的系统调用(SVr4)
SIGURG 16,23,21 Ign 有紧急数据到达Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU 24,24,30 Core 超过CPU时间资源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超过文件大小资源限制(4.2BSD)

:在Linux 2.2版本以前,SIGSYSSIGXCPUSIGXFSZ以及SIGBUS的默认响应动做为Term,Linux 2.4版本以后这三个信号的默认响应动做改成Core。

2.3 其它信号

信号 动做 说明
SIGIOT 6 Core IOT捕获信号(同SIGABRT信号)
SIGEMT 7,-,7 Term 实时硬件发生错误
SIGSTKFLT -,16,- Term 协同处理器栈错误(未使用)
SIGIO 23,29,22 Term 文件描述符准备就绪(能够开始进行输入/输出操做)(4.2BSD)
SIGCLD -,-,18 Ign 子进程结束(由父进程接收)(同SIGCHLD信号)
SIGPWR 29,30,19 Term 电源错误(System V)
SIGINFO 29,-,-   电源错误(同SIGPWR信号)
SIGLOST -,-,- Term 文件锁丢失(未使用)
SIGWINCH 28,28,20 Ign 窗口大小改变时触发(4.3BSD, Sun)
SIGUNUSED -,31,- Core 无效的系统调用(同SIGSYS信号)

注意:列表中有的信号有三个值,这是由于部分信号的值和CPU架构有关,这些信号的值在不一样架构的CPU中是不一样的,三个值的排列顺序为:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。

例如SIGSTOP这个信号,它有三种可能的值,分别是1七、1九、23,其中第一个值(17)是用在Alpha和Sparc架构中,第二个值(19)用在x8六、ARM等其它架构中,第三个值(23)则是用在MIPS架构中的。

3 信号机制

文章的前面提到过,信号是异步的,这就涉及信号什么时候接收、什么时候处理的问题。

咱们知道,函数运行在用户态,当遇到系统调用、中断或是异常的状况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程能够先看一下下面的示意图:

信号处理机制示意图

接下来围绕示意图,将信号分红接收、检测和处理三个部分,逐一讲解每一步的处理流程。

3.1 信号的接收

接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。

注意,此时信号还只是在队列中,对进程来讲暂时是不知道有信号到来的。

3.2 信号的检测

进程陷入内核态后,有两种场景会对信号进行检测:

  • 进程从内核态返回到用户态前进行信号检测
  • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测

当发现有新信号时,便会进入下一步,信号的处理。

3.3 信号的处理

信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,而且修改指令寄存器(eip)将其指向信号处理函数。

接下来进程返回到用户态中,执行相应的信号处理函数。

信号处理函数执行完成后,还须要返回内核态,检查是否还有其它信号未处理。若是全部信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

至此,一个完整的信号处理流程便结束了,若是同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。

4 信号的使用

4.1 发送信号

用于发送信号的函数有raisekillkillpgpthread_killtgkillsigqueue,这几个函数的含义和用法都大同小异,这里主要介绍一下经常使用的raisekill函数。

raise函数:向进程自己发送信号

函数声明以下:

#include <signal.h> int raise(int sig); 

函数功能是向当前程序(自身)发送信号,其中参数sig为信号值。

kill函数:向指定进程发送信号

函数声明以下:

#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); 

函数功能是向特定的进程发送信号,其中参数pid为进程号,sig为信号值。

在这里的参数pid,根据取值范围不一样,含义也不一样,具体说明以下:

  • pid > 0 :向进程号为pid的进程发送信号
  • pid = 0 :向当前进程所在的进程组发送信号
  • pid = -1 :向全部进程(除PID=1外)发送信号(权限范围内)
  • pid < -1 :向进程组号为-pid的全部进程发送信号

另外,当sig值为零时,实际不发送任何信号,但函数返回值依然有效,能够用于检查进程是否存在。

4.2 等待信号被捕获

等待信号的过程,其实就是将当前进程(线程)暂停,直到有信号发到当前进程(线程)上并被捕获,函数有pausesigsuspend

pause函数:将进程(或线程)转入睡眠状态,直到接收到信号

函数声明以下:

#include <unistd.h> int pause(void); 

该函数调用后,调用者(进程或线程)会进入睡眠(Sleep)状态,直到捕获到(任意)信号为止。该函数的返回值始终为-1,而且调用结束后,错误代码(errno)会被置为EINTR。

sigsuspend函数:将进程(或线程)转入睡眠状态,直到接收到特定信号

函数声明以下:

#include <signal.h> int sigsuspend(const sigset_t *mask); 

该函数调用后,会将进程的信号掩码临时修改(参数mask),而后暂停进程,直到收到符合条件的信号为止,函数返回前会将调用前的信号掩码恢复。该函数的返回值始终为-1,而且调用结束后,错误代码(errno)会被置为EINTR。

4.3 修改信号的响应动做

用户能够本身从新定义某个信号的处理方式,即前面提到的修改信号的默认响应动做,也能够理解为对信号的注册,能够经过signalsigaction函数进行,这里以signal函数举例说明。

首先看一下函数声明:

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

第一个参数signum是信号值,能够从前面的信号列表中查到,第二个参数handler为处理函数,经过回调方式在信号触发时调用。

下面为示例代码:

#include <stdio.h> #include <signal.h> #include <unistd.h> /* 信号处理函数 */ void sig_callback(int signum) { switch (signum) { case SIGINT: /* SIGINT: Ctrl+C 按下时触发 */ printf("Get signal SIGINT. \r\n"); break; /* 多个信号能够放到同一个函数中进行 经过信号值来区分 */ default: /* 其它信号 */ printf("Unknown signal %d. \r\n", signum); break; } return; } /* 主函数 */ int main(int argc, char *argv[]) { printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT); /* 注册SIGINT信号的处理函数 */ signal(SIGINT, sig_callback); printf("Waitting for Signal ... \r\n"); /* 等待信号触发 */ pause(); printf("Process Continue. \r\n"); return 0; } 

源文件下载:连接

例子中,将SIGINT信号(Ctrl+C触发)的动做接管(打印提示信息),程序运行后,按下Ctrl+C,命令行输出以下:

./linux_signal_example
Register SIGINT(2) Signal Action. 
Waitting for Signal ... 
^CGet signal SIGINT. 
Process Continue.

进程收到SIGINT信号后,触发响应动做,将提示信息打印出来,而后从暂停的地方继续运行。这里须要注意的是,由于咱们修改了SIGINT信号的响应动做(只打印信息,不作进程退出处理),因此咱们按下Ctrl+C后,程序并无直接退出,而是继续运行并将"Process Continue."打印出来,直至程序正常结束。

 

 

 

linux内核中异步通知机制--信号处理机制

什么是异步通知:很简单,一旦设备准备好,就主动通知应用程序,这种状况下应用程序就不须要查询设备状态, 特像硬件上常提的“中断的概念”。 比较准确的说法其实应该叫作“信号驱动的异步I/O”,信号是在软件层次上对中断机制的一种模拟。阻塞I/O意味着一直等待设备可访问再访问,非阻塞I/O意味着使用poll()来查询是否可访问,而异步通知则意味着设备通知应用程序自身可访问。(但愿用这么一句话能表达个人意思)

1、系统中存在的异步机制

我认为异步机制是一种理念,并非某一种具体实现,同步/异步的核心理解应该是如何获取消息的问题,你自身(在计算机中固然是进程自己了)亲自去获取消息,那么就是同步机制,可是若是别人使用某种方式通知你某一个消息,那么你采用的就是异步机制。内核中使用到异步机制的大概有:信号,这是一种进程间通讯的异步机制;epoll,这是一种高效处理IO的异步通讯机制。也就是从通讯和IO两个方面经过不一样的方式使用了异步机制。(可能还有别的,暂时不知道)

下面进入正题:

2、信号的基本概念

1)信号的本质

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求能够说是同样的。信号是进程间通讯机制中惟一的异步通讯机制,一个进程没必要经过任何操做来等待信号的到达,事实上,进程也不知道信号到底何时到达。进程之间能够互相经过系统调用kill发送软中断信号。内核也能够由于内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还能够传递附加信息。

收到信号的进程对各类信号有不一样的处理方法。处理方法能够分为三类:
第一种是相似中断的处理程序,对于须要处理的信号,进程能够指定处理函数,由该函数来处理。
第二种方法是,忽略某个信号,对该信号不作任何处理,就象未发生过同样。
第三种方法是,对该信号的处理保留系统的默认值,这种缺省操做,对大部分的信号的缺省操做是使得进程终止。进程经过系统调用signal来指定进程对某个信号的处理行为。

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此能够看出,进程对不一样的信号能够同时保留,但对于同一个信号,进程并不知道在处理以前来过多少个。

2)信号的种类

能够从两个不一样的分类角度对信号进行分类:
可靠性方面:可靠信号与不可靠信号;
与时间的关系上:实时信号与非实时信号。

3)可靠信号与不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

随着时间的发展,实践证实了有必要对信号的原始机制加以改进和扩充。因为原来定义的信号已有许多应用,很差再作改动,最终只好又新增长了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是经过sigation()函数实现的,所以,即便经过signal()安装的信号,在信号处理函数的结尾也没必要再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,一样不会丢失。

对于目前linux的两个信号安装函数:signal()及sigaction()来讲,它们都不能把SIGRTMIN之前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),并且对SIGRTMIN之后的信号都支持排队。这两个函数的最大区别在于,通过sigaction安装的信号都能传递信息给信号处理函数,而通过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来讲也是同样的。

4)实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预约义值,每一个信号有了肯定的用途及含义,而且每种信号都有各自的缺省动做。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

5)linux 下信号的生命周期以下:

在目的进程中安装该信号。便是设置捕获该信号时进程进程该执行的操做码。采用signal();sigaction()系统调用来实现。
信号被某个进程产生,同时设置该信号的目的进程(使用pid),以后交给操做系统进行管理。采用kill()、arise()、alarm()等系统调用来实现。
信号在目的进程被注册。信号被添加进进程的PCB(task_struct)中相关的数据结构里——未决信号的数据成员。信号在进程中注册就是把信号值加入到进程的未决信号集里。而且,信号携带的其余信息被保留到未决信的队列的某个sigqueue结构中。
信号在进程中注销。在执行信号处理函数前,要把信号在进程中注销。对于非实时信号(不可靠信号),其在信号未决信号信息链中最多只有一个sigqueue结构,所以该结构被释放后,相应的信号要在未决信号集删除。而实时信号(可靠信号),若是有多个sigqueue,则不会把信号从进程的未决信号集中删除。
信号生命的终结。进程终止当前的工做,保护上下文,执行信号处理函数,以后回复。若是内核是可抢占的,那么还须要调度。

3、信 号 机 制

上 一节中介绍了信号的基本概念,在这一节中,咱们将介绍内核如何实现信号机制。即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制本身对信 号的反应、内核在什么时机处理和怎样处理进程收到的信号。还要介绍一下setjmp和longjmp在信号中起到的做用。

一、内核对信号的基本处理方法

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

进程的task_struct结构中有关于本进程中未决信号的数据成员:struct sigpending pending:

struct sigpending{  struct sigqueue *head, *tail;  sigset_t signal; };

第三个成员是进程中全部未决信号集,第1、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每一个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{  struct sigqueue *next;  siginfo_t info; }

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每一个信号占用一位)中,而且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,代表进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,无论该信号是否已经在进程中注册,都会被再注册一次,所以,信号不会丢失,所以,实时信号又叫作"可靠信号"。这意味着同一个实时信号能够在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即全部诞生的实时信号都会在目标进程中注册)。

当一个非实时信号发送给一个进程时,若是该信号已经在进程中注册(经过sigset_t signal指示),则该信号将被丢弃,形成信号丢失。所以,非实时信号又叫作"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。因此,当一个进程在内核态下运行时,软中断信号并不当即起做用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

内核处理一个进程收到的软中断信号是在该进程的上下文中,所以,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退 出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似 的继续运行。若是进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。并且执行用户定义的函数的方法很巧妙,内核是在用户栈上创 建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时, 才返回原先进入内核的地方。这样作的缘由是用户定义的处理函数不能且不容许在内核态下执行(若是用户定义的函数在内核态下运行的话,用户就能够得到任何权 限)。

对于非实时信号来讲,因为在未决信号信息链中最多只占用一个sigqueue结构,所以该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来讲,可能在未决信号信息链中占用多个sigqueue结构,所以应该针对占用sigqueue结构的数目区别对待:若是只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。不然待该信号的全部sigqueue处理完毕后再在进程的未决信号集中删除该信号。

当全部未被屏蔽的信号都处理完毕后,便可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

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

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

第二个要 引发注意的是,若是要捕捉的信号发生于进程正在一个系统调用中时,而且该进程睡眠在可中断的优先级上,这时该信号引发进程做一次longjmp,跳出睡眠 状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回同样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注 意的是,BSD系统中内核能够自动地从新开始系统调用。

第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不作longjmp,通常是继续睡眠。但用户感受不到进程曾经被唤醒,而是象没有发生过该信号同样。

第四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其余信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省状况下,该进程 就象没有收到该信号似的,若是父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操做(找 出僵死的子进程,释放子进程的进程表项),而后从wait中返回。SIGCLD信号的做用是唤醒一个睡眠在可被中断优先级上的进程。若是该进程捕捉了这个 信号,就象普通讯号处理同样转处处理例程。若是进程忽略该信号,那么系统调用wait的动做就有所不一样,由于SIGCLD的做用仅仅是唤醒一个睡眠在可被 中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操做,而后等待其余的子进程。

若是一个进程调用signal系统调用,并设置了SIGCLD的处理方法,而且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。

二、setjmp和longjmp的做用

前面在介绍信号处理机制时,屡次提到了setjmp和longjmp,但没有仔细说明它们的做用和实现方法。这里就此做一个简单的介绍。 在 介绍信号的时候,咱们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程忽然改变其上下文的状况,就是 使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当由于资 源或其余缘由要去睡眠时,内核为进程做了一次setjmp,若是在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操做是内核 为进程将原先setjmp调用保存在进程用户区的上下文恢复成如今的上下文,这样就使得进程能够恢复等待资源前的状态,并且内核为setjmp返回1,使 得进程知道该次系统调用失败。这就是它们的做用。

相关文章
相关标签/搜索