7.2.4 典型系统调用的实现

  sigaction()系统调用的实现较具代表性,它的主要功能为设置信号处理程序,其原型为:

 

     int sys_sigaction(int signum, const struct sigaction * action,

                       struct sigaction * oldaction)

 

     其中,sigaction数据结构在include/asm/signal.h中定义,其格式为:

 

struct sigaction {

       __sighandler_t  sa_handler;

       sigset_t       sa_mask;

          unsigned long  sa_flags;   

          void (*sa_restorer)(void);

             };

 

    其中__sighandler_t 定义为:

   

typedef void (*__sighandler_t) (int);

 

在这个结构中, sa_handler为指向处理函数的指针,sa_mask是信号掩码,当该信号signum出现时,这个掩码就被逻辑或到接收进程的信号掩码中。当信号处理程序执行时,这个掩码保持有效。Sa_flags域是几个位标志的逻辑或(OR)组合,其中两个主要的标志是:

 

SA_ONESHOT    当信号出现时,将信号操作置为默认操作;

SA_NOMASK     忽略sigaction结构的sa_mask域。

 

Linux中定义的信号处理的三种类型为:

 

    #define SIG_DFL    ((__sighandler_t)0)    /* 缺省的信号处理*/

    #define SIG_IGN    ((__sighandler_t)1)     /*忽略这个信号 */

    #define SIG_ERR    ((__sighandler_t)-1)  /*从信号返回错误 */

 

   下面是sigaction()系统调用在内核中实现的代码及解释。

 

   int sys_sigaction(int signum, const struct sigaction * action,

                     struct sigaction * oldaction)

   {  

       struct sigaction new_sa, *p;

 

       if (signum<1 || signum>32)

            return -EINVAL; 

         /* 信号的值不在132之间,则出错 */

       if (signum==SIGKILL || signum==SIGSTOP)

            return -EINVAL;

         /*SIGKILLSIGSTOP不能设置信号处理程序 */

        p = signum - 1 + current->sig->action;

             /*在当前进程中,指向信号signumaction的指针 */

       if (action) {

        int err = verify_area(VERIFY_READ, action, sizeof(*action));

            /* 验证给action在用户空间分配的地址的有效性 */

           if (err)

               return err;    

        memcpy_fromfs(&new_sa, action, sizeof(struct sigaction));

           /* actoin的内容从用户空间拷贝到内核空间*/

        new_sa.sa_mask |= _S(signum);

           /* 把信号signum加到掩码中 */

       if (new_sa.sa_flags & SA_NOMASK)

           new_sa.sa_mask &= ~_S(signum);

/* 如果标志为SA_NOMASK,当信号signum出现时,将它的操作置为默认操作 */

       new_sa.sa_mask &= _BLOCKABLE;

         /* 不能阻塞SIGKILLSIGSTOP  */

if (new_sa.sa_handler != SIG_DFL && new_sa.sa_handler !=SIG_IGN)                       {      

           err = verify_area(VERIFY_READ, new_sa.sa_handler, 1);

/* 当处理程序不是信号默认的处理操作,并且signum信号不能被忽略时, 验证给信号处理程序分配空间的有效性 */

       if (err)

               return err;     }

       }

   if (oldaction) {

int err = verify_area(VERIFY_WRITE, oldaction,   sizeof(*oldaction));

   if (err)

           return err;    

     memcpy_tofs(oldaction, p, sizeof(struct sigaction));

      /* 恢复原来的信号处理程序 */

                   } 

   if (action) {     

        *p = new_sa;      

        check_pending(signum);

               }

       return 0;

  }   

 

Linux可以将各种信号发送给程序,以表示程序故障、用户请求的中断、其它各种情况等。通过对sigaction()系统调用源代码的分析,有助于灵活应用信号的系统调用。7.2.5进程与信号的关系

   Linux 内核中不存在任何机制用来区分不同信号的优先级。也就是说,当同时有多个信号发出时,进程可能会以任意顺序接收到信号并进行处理。另外,如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。产生上述现象的原因与内核对信号的实现有关。

 系统在 task_struct 结构中利用两个域分别记录当前挂起的信号(signal)以及当前阻塞的信号(blocked)。挂起的信号指尚未进行处理的信号。阻塞的信号指进程当前不处理的信号,如果产生了某个当前被阻塞的信号,则该信号会一直保持挂起,直到该信号不再被阻塞为止。除了 SIGKILL SIGSTOP 信号外,所有的信号均可以被阻塞,信号的阻塞可通过系统调用sigprocmask()实现。每个进程的 task_struct 结构中还包含了一个指向 sigaction 结构数组的指针,该结构数组中的信息实际指定了进程处理所有信号的方式。如果某个 sigaction 结构中包含有处理信号的例程地址,则由该处理例程处理该信号;反之,则根据结构中的一个标志或者由内核进行默认处理,或者只是忽略该信号。通过系统调用sigaction(),进程可以修改 sigaction 结构数组的信息,从而指定进程处理信号的方式。

进程不能向系统中所有的进程发送信号,一般而言,除系统和超级用户外,普通进程只能向具有相同 uid gid 的进程,或者处于同一进程组的进程发送信号。当有信号产生时,内核将进程 task_struct signal 字中的相应位设置 1。系统不对置位之前该位已经为 1的情况进行处理,因而进程无法接收到前一次信号。如果进程当前没有阻塞该信号,并且进程正处于可中断的等待状态(INTERRUPTIBLE),则内核将该进程的状态改变为运行(RUNNING),并放置在运行队列中。这样,调度程序在进行调度时,就有可能选择该进程运行,从而可以让进程处理该信号。

发送给某个进程的信号并不会立即得到处理,相反,只有该进程再次运行时,才有机会处理该信号。每次进程从系统调用中退出时,内核会检查它的 signal block 字段,如果有任何一个未被阻塞的信号发出,内核就根据 sigaction 结构数组中的信息进行处理。处理过程如下:

 

1. 检查对应的 sigaction 结构,如果该信号不是 SIGKILL SIGSTOP 信号,且被忽略,则处理该信号。

 

2. 如果该信号利用默认的处理程序处理,则由内核处理该信号,否则转向第 3 步。

 

3. 该信号由进程自己的处理程序处理,内核将修改当前进程的调用堆栈,并将进程的程序计数寄存器修改为信号处理程序的入口地址。此后,指令将跳转到信号处理程序,当从信号处理程序中返回时,实际就返回了进程的用户模式部分。

 

 Linux POSIX 兼容的,因此,进程在处理某个信号时,还可以修改进程的 blocked 掩码。但是,当信号处理程序返回时,blocked 值必须恢复为原有的掩码值,这一任务由内核的sigaction()函数完成。Linux 在进程的调用堆栈帧中添加了对清理程序的调用,该清理程序可以恢复原有的 blocked 掩码值。当内核在处理信号时,可能同时有多个信号需要由用户处理程序处理,这时,Linux 内核可以将所有的信号处理程序地址推入堆栈中,而当所有的信号处理完毕后,调用清理程序恢复原先的 blocked 值。