【技术篇】linux进程间通讯--信号

(一)信号的本质

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

(二)信号的种类

       从可靠性方面分为可靠信号不可靠信号;从与时间的关系上分为实时信号非实时信号

      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.1 处理信号的整个过程

      用户进程提供的信号处理函数是在用户态里的,而我们发现信号到找到信号处理函数是处于内核态中的,因此需要从内核态跑到用户态去执行信号处理函数,执行完毕后还要返回到内核态。过程如下图:

                             

    可以这样用语言描述:进程由于系统调用或者中断进入内核,完成相应的任务后返回用户口空间之前回检查信号队列,如果刚好有信号,则根据信号向量表找到信号处理函数,设置好“堆栈”后跳转到用户态执行信号处理函数。信号处理函数执行完毕后返回内核态设置“堆栈”,最后返回到用户态继续执行。

     3.2内核实现信号捕捉信号的步骤

      1、用户为某条信号注册一个信号处理函数-sighandler

      2、当前正在执行的主程序由于中断、异常、或者系统调用而进入内核态;

      3、在处理完异常返回用户态的主程序之前,检查是否有信号未处理,并找到该信号需要按照用户自定义的函数来处理;

      4、内核态决定返回用户态执行sidhandler函数,而不是恢复主程序的上下文继续执行,sighandler和main函数使用的是不同的堆栈空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程;

     5、sighandler函数返回后,执行特殊的系统调用sigreturn从用户态返回到内核态;

     6、检查是否有其他的信号想要传达,如果没有则返回到用户态并恢复主程序的上下文继续执行;

    注:进程收到一个信号后不会被立即处理,而是在恰当 时机进行处理!什么是适当的时候呢?比如说中断返回的时候,或者内核态返回用户态的时候(这个情况出现的比较多)。信号不一定会被立即处理,操作系统不会为了处理一个信号而把当前正在运行的进程挂起(切换进程),挂起(进程切换)的话消耗太大了,如果不是紧急信号,是不会立即处理的。操作系统多选择在内核态切换回用户态的时候处理信号,这样就利用两者的切换来处理了(不用单独进行进程切换以免浪费时间)。总归是不能避免的,因为很有可能在睡眠的进程就接收到信号,操作系统肯定不愿意切换当前正在运行的进程,于是就得把信号储存在进程唯一的PCB(task_struct)当中。

     3.3一个完整的信号生命周期

          3.3.1 信号的诞生

          信号事件的发生来源:硬件来源(我们按下的键盘键、硬件故障等),软件来源(发送信号的函数、非法云算法等);

          这里按发出信号的原因简单分类,以了解各种信号:

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

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

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

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

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

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

          7、跟踪进程执行的信号

          3.3.2 信号在目标进程中被注册

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

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

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

        第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个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.3信号的执行和注销      

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

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

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

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

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

(四)信号的安装

        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。

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

   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。如果把第二、第三个参数都设为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;
};

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

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

siginfo_t 
{
     int   si_signo;  /* 信号值,对所有信号有意义*/
     int   si_errno;  /* errno值,对所有信号有意义*/
     int   si_code;   /* 信号产生的原因,对所有信号有意义*/

     union            /* 联合数据结构,不同成员适应不同信号 */
     {                               
       
          int _pad[SI_PAD_SIZE];   //确保分配足够大的存储空间
          struct          //对SIGKILL有意义的结构
          {
             ...
          }...
          ... ...
          ... ...                                        
          struct //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
          {   
             ...
          }...
          ... ...
    }
}

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

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

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

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

(五)信号的发送

   发送信号的主要函数有: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的进程

Signo是信号值,当为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中的一个

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。

参考文章:https://www.cnblogs.com/subo_peng/p/5325326.html ; https://mp.csdn.net/postedit

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值