Chinaunix首页 | 论坛 | 博客
  • 博客访问: 40289
  • 博文数量: 17
  • 博客积分: 750
  • 博客等级: 军士长
  • 技术积分: 160
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-19 17:39
文章分类
文章存档

2009年(17)

我的朋友

分类: LINUX

2009-11-04 19:54:22

信号是一种软中断,提供了一种处理异步事件的方法。它的实质和使用很象中断,但与中断不同的是它运行在进程上下文中,如果进程终止了,即程序结束运行了,则程序不会在接收信号不会再运行起处理函数,而中断处理程序与进程无关。。
    信号就是在signal.h中定义的一系列以SIG开头的宏,实质是整数。信号可以通过进程(调用kill,raise,alarm,abort等,实质还是内核发信号)发出,也可以是内核(如用户按下ctl+c)发出。信号也叫软中断。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
    收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。
    在进程表的表项中(struct task_struct)有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个,只保留一次(见下面的例子)。

    系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:void (*signal(int signum, void (*handler)(int)))(int);
    第二个参数handler是一个处理函数的指针,该函数无返回值,需要一个整型参数,调用时该参数将被赋予接收的信号值signum。
    handler指针也可以是SIG_IGN:忽略参数signum所指的信号;或者赋值SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
    若成功,singal的返回值则是指向之前的信号处理函数的指针;若失败,则返回SIG_ERR.

    与signal函数类似的是sigaction函数,他是POSIX的信号接口,而signal()是标准C的信号接口(如果程序必须在非POSIX系统上运行,那么就应该使用这个接口)。
    int sigaction(int signo,const struct sigaction *restrict act,  struct sigaction *restrict oact);成功返回0,出错返回-1。
    结构sigaction定义如下:

struct sigaction{
          void (*sa_handler)(int);//信号捕捉函数的地址,也可以赋值为SIG_IGN或SIG_DFL

           sigset_t sa_mask; /*说明了一个信号集,在调用该信号捕捉函数sa_handler之前,阻塞该信号集中的信号,信号捕捉函数返回时再解除对该信号集中信号的阻塞,若这些信号在阻塞期间再次发生,内核保留此信号等阻塞结束后再递给进程处理,且同一信号无论阻塞期间发生几次,均递交处理一次。注意这里只是恢复进程原先的信号屏 蔽子,但如果调用之前信号集中的某些信号已经被阻塞,这时还是被阻塞,。*/
          int sa_flag; //sa_flag是一个选项,主要理解两个:SA_INTERRUPT 由此信号中断的系统调用不会自动重启;SA_RESTART 由此信号中断的系统调用会自动重启。默认不重起。
          void (*sa_sigaction)(int,siginfo_t *,void *);
    };

signal和sigaction函数均保证当前被捕获的信号在其信号处理函数运行时,该信号被阻塞,直到处理函数结束,若同一信号在阻塞期间再次发生,内核保留此信号等阻塞结束后再递给进程处理,且无论阻塞期间发生几次,均递交处理一次。对sigaction函数,不管信号本身是否被加到sa_mask中。一旦对给定的信号设定了一个动作,那么在调用signal或sigaction函数显式的改变它之前,该设置一直有效。从信号处理函数返回后,接着处理程序被调度运行前的位置继续执行,类似于函数返回后接着下面的语句继续运行。
   
很多平台都是用sigaction函数实现signal的。


#include "apue.h"
Sigfunc *
signal(int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) { //对SIGALRM信号,不重启被其中断的系统调用,这是因为我们希望某些系统调用被阻塞时能够被SIGALRM信号超时唤醒,而不是无限期阻塞。

#ifdef    SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else { //除SIGALRM外,由signal设置的信号,被这些信号中断的系统调用默认会重启。而sigaction默认不重启。

#ifdef    SA_RESTART
        act.sa_flags |= SA_RESTART;
#endif
    }
    if (sigaction(signo, &act, &oact) < 0)
        return(SIG_ERR);
    return(oact.sa_handler);
}
对任何信号不可重起的signal_intr()实现:
Sigfunc *signal_intr(int signo, Sigfunc *func)
{
       struct sigaction act, oact;

       act.sa_handler = func;
       sigemptyset(act.sa_mask);
       act.sa_flags = 0;
  
#ifdef SA_INTTERRUPT //SUNOS

          act.flags |= SA_INTTERRUPT;
#endif
    
        if (sigaction(signo, &act, &oac) < 0)
             return (SIG_ERR);

         return (oact.sa_handler);
}
signal由ISO C定义,他的语义与实现相关。


下面的程序为了防止read永远阻塞,设置了定时,注意signal必须对SIGALRM信号不可重启。

int
main(void)
{
        int n;
        char line[MAXLINE];

        if (signal(SIGALRM, sig_alrm) == SIG_ERR)
                perror("signal");

        alarm(10);
        if ( (n = read(STDIN_FILENO, line, MAXLINE)) < 0) //若read阻塞超过10s,则将被SIGALRM信号唤醒;若read为发生阻塞而返回,则取消定时。
                perror("read");
        alarm(0);
        write(STDOUT_FILENO, line, n);
        write(STDOUT_FILENO, "exit\n", 5);

        exit(0);
}

static void
sig_alrm(int signo)
{
                                                        //信号处理函数什么也不做,但若不定义信号处理函数,则不会唤醒阻塞的read
}


一个信号处理例程处理多个信号,如下:

#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 (;;) ;//让程序不断循环,否则程序终止,信号不再作用,因为信号工作在进程上下文中。

}


当程序执行某些任务时,不希望被外界信号中断,这时候不应该简单的忽略信号,而是阻塞信号,当进程处理完特定任务后,再解除阻塞处理未决信号。阻塞信号的典型用法:

#include "apue.h"
static void    sig_quit(int);
int main(void)
{
    sigset_t    newmask, oldmask, pendmask;

    if (signal(SIGQUIT, sig_quit) == SIG_ERR)                 //1.登记信号,设置信号处理程序,注意出错处理
        err_sys("can't catch SIGQUIT");

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);                            //2.初始化信号集,先清零,在将需要阻塞的信号置1

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)     //3.用2中的信号集设置进程的信号屏蔽字,保存原先的信号屏蔽字,然后进入下面的不希望被这些信号打扰的动作,
        err_sys("SIG_BLOCK error");

    sleep(5);    /* SIGQUIT here will remain pending */

    if (sigpending(&pendmask) < 0)
        err_sys("sigpending error");                        
    if (sigismember(&pendmask, SIGQUIT))                    //4.到此,是阻塞信号期间所作的动作,此处调用sigpending返回设为阻塞的处于未决状态的信号集并用sigismember
        printf("\nSIGQUIT pending\n");                             测试某特定信号(这里是SIGQUIT)是否处于阻塞未决状态(在阻塞期间发生的但尚未处理的信号)

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)        //5.动作执行完毕,撤销阻塞,恢复3之前的信号屏蔽字。若在上面的阻塞期间,SIGQUIT发生了,且不管发生了多少次,解除
        err_sys("SIG_SETMASK error");                            阻塞后将向进程传递SIGQUIT且仅传送一次,即sig_quit只运行一次。
    printf("SIGQUIT unblocked\n");

    sleep(5);    /* SIGQUIT here will terminate with core file */
    exit(0);
}

static void
sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)                //在信号处理程序中将信号的处理方式恢复为默认,此处理函数不再被执行,否则每次捕获到信号后将总被执行。
        err_sys("can't reset SIGQUIT");
}


下面看一下信号处理机制的原理:
    内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
    内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态(上面的例子程序中,在步骤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将使父进程挂起休眠。

当应用程序向特定进程发送信号时,调用kill或raise函数。
阅读(811) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~