信号基础知识

信号(signal)是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。进程收到一个信号后,会检查对该信号的处理机制。如果是SIG_IGN,就忽略该信号;如果是SIG_DFT,则会采用系统默认的处理动作,通常是终止进程或忽略该信号;如果给该信号指定了一个处理函数(捕捉),则会中断当前进程正在执行的任务,转而去执行该信号的处理函数,返回后再继续执行被中断的任务。

使用kill -l就会显示出linux支持的信号列表。

图片

常见信号分类:

SIGHUP     终止进程     终端线路挂断
SIGINT     终止进程     中断进程
SIGQUIT   建立CORE文件终止进程,并且生成core文件

SIGILL   建立CORE文件       非法指令
SIGTRAP   建立CORE文件       跟踪自陷
SIGBUS   建立CORE文件       总线错误
SIGSEGV   建立CORE文件       段非法错误
SIGFPE   建立CORE文件       浮点异常
SIGIOT   建立CORE文件       执行I/O自陷
SIGKILL   终止进程     杀死进程
SIGPIPE   终止进程     向一个没有读进程的管道写数据
SIGALARM   终止进程     计时器到时
SIGTERM   终止进程     软件终止信号
SIGSTOP   停止进程     非终端来的停止信号
SIGTSTP   停止进程     终端来的停止信号
SIGCONT   忽略信号     继续执行一个停止的进程
SIGURG   忽略信号     I/O紧急信号
SIGIO     忽略信号     描述符上可以进行I/O
SIGCHLD   忽略信号     当子进程停止或退出时通知父进程
SIGTTOU   停止进程     后台进程写终端
SIGTTIN   停止进程     后台进程读终端
SIGXGPU   终止进程     CPU时限超时
SIGXFSZ   终止进程     文件长度过长
SIGWINCH   忽略信号     窗口大小发生变化
SIGPROF   终止进程     统计分布图用计时器到时
SIGUSR1   终止进程     用户定义信号1
SIGUSR2   终止进程     用户定义信号2

SIGVTALRM 终止进程     虚拟计时器到时

1) SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控 制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联. 
2) SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出 
3) SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是ctrl+\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号. 
4) SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号. 
5) SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用. 
6) SIGABRT 程序自己发现错误并调用abort时产生. 
6) SIGIOT 在PDP-11上由iot指令产生, 在其它机器上和SIGABRT一样. 
7) SIGBUS 非法地址, 包括内存地址对齐(alignment)出错. eg: 访问一个四个字长的整数, 但其地址不是4的倍数. 
8) SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误. 
9) SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞, 处理和忽略. 
10) SIGUSR1 留给用户使用 
11) SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据. 
12) SIGUSR2 留给用户使用 
13) SIGPIPE Broken pipe 
14) SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号. 
15) SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这个信号. 
17) SIGCHLD 子进程结束时, 父进程会收到这个信号. 
18) SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符 
19) SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别: 该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略. 
20) SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 
21) SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行. 
22) SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到. 
23) SIGURG 有"紧急"数据或out-of-band数据到达socket时产生. 
24) SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变 
25) SIGXFSZ 超过文件大小资源限制. 
26) SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. 
27) SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间. 
28) SIGWINCH 窗口大小改变时发出. 
29) SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作. 
30) SIGPWR Power failure 

有两个信号可以停止进程:SIGTERM和SIGKILL。 SIGTERM比较友好,进程能捕捉这个信号,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。在某些情况下,假如进程正在进行作业而且不能中断,那么进程可以忽略这个SIGTERM信号。

对于SIGKILL信号,进程是不能忽略的。这是一个 “我不管您在做什么,立刻停止”的信号。假如您发送SIGKILL信号给进程,Linux就将进程停止在那里。 


信号常见函数:

1.signal函数
typedef void (*sighandler_t) (int)

sighandler_t signal(int signum, sighandler_t handler);
signal()是最简单的给进程安装信号处理器的函数,第一个参数指定信号,第二个参数为该信号指定一个处理函数。

如下是一个最简单的处理信号的程序,它捕捉SIGUSR1,忽略SIGUSR2,按系统默认处理SIGINT,SIGUSR1和SIGUSR2是Linux提供的用户定义信号,可用于任何应用程序。主程序什么都不干,只用pause()循环等待信号。
例程1 最简单的信号处理

static void pr_mask(const char * string) 
{
    sigset_t procmask;

    sigprocmask(SIG_SETMASK, NULL, &procmask);

    printf("%s: ", string);
    if(sigismember(&procmask, SIGINT))
        printf("SIGINT ");
    if(sigismember(&procmask, SIGUSR1))
       printf("SIGUSR1 ");
   if(sigismember(&procmask, SIGUSR2))
        printf("SIGUSR2 ");
    if(sigismember(&procmask, SIGTERM))
        printf("SIGTERM ");
    if(sigismember(&procmask, SIGQUIT))
        printf("SIGQUIT ");
    printf("/n");
}

static void sigusr(int signum)
{
    pr_mask(“int sigusr”);

    if(signum == SIGUSR1)
        printf(“SIGUSR1 received/n”);
    else if(signum == SIGUSR2)
        printf(“SIGUSR2 received/n”);
    else
        printf(“signal %d received/n”, signum);
}

int main(void)
{
    if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
        printf(“error catching SIGUSR1/n”);
        exit(1);
    }

    if(signal(SIGUSR2, SIG_IGN) == SIG_ERR) {
        printf(“error ignoring SIGUSR2/n”);
        exit(1);
    }

    if(signal(SIGINT, SIG_DFT) == SIG_ERR) {
        printf(“error setting SIGINT to default/n”);
        exit(1);
    }

    while(1)
       pause();

    exit(0);
}

用kill发送信号给后台运行程序,结果如下:

$./a.out &

[1] 3725

$kill -USR1 3725

in sigusr: SIGUSR1

SIGUSR1 received

$kill -USR2 3725

[1]+ User defined signal 2 ./a.out

我们可以看到,Linux系统对SIGUSR2的默认动作是终止进程。

中断与自动重启动
信号是一种软件中断机制,这就产生了一个问题:如果信号到来时进程正在执行某个低速系统调用,系统应该怎么处理?

是暂时阻塞系统调用返回,在信号处理程序完成后继续没完成的系统调用呢?

还是让系统调用出错返回,同时把errno设置为EINTR,让调用者去做进一步的出错检查呢?

用事实说话,让我们做一个试验先吧。


下面的程序读取标准输入并把它输出到标准输出,在此期间,给进程发送SIGUSR1信号,以此来确定Linux在收到信号后是如何对处理系统调用的。

例程2 信号与自动重启动的signal版本

static void pr_mask(const char * string) 
{
    sigset_t procmask;

    sigprocmask(SIG_SETMASK, NULL, &procmask);

    printf("%s: ", string);
    if(sigismember(&procmask, SIGINT))
        printf("SIGINT ");
    if(sigismember(&procmask, SIGUSR1))
       printf("SIGUSR1 ");
   if(sigismember(&procmask, SIGUSR2))
        printf("SIGUSR2 ");
    if(sigismember(&procmask, SIGTERM))
        printf("SIGTERM ");
    if(sigismember(&procmask, SIGQUIT))
        printf("SIGQUIT ");
    printf("/n");
}

static void sigusr(int signum)
{
    pr_mask(“int sigusr”);

    if(signum == SIGUSR1)
        printf(“SIGUSR1 received/n”);
    else if(signum == SIGUSR2)
        printf(“SIGUSR2 received/n”);
    else
        printf(“signal %d received/n”, signum);
}

int main(void)
{
    char buf[BUFSIZ];
    int n;

    signal(SIGUSR1, sig_usr);


    while(1)
    {
        //从键盘输入字符串,并输出到屏幕
        //read 函数的返回值为实际读取到的字节数,如果返回值为 0,表示读取到文件末尾,如果返回值为 -1,表示读取错误
        if((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) 
        {
            if(errno == EINTR)
            {
                // read()被信号等异常中断
                printf(“read is interrupted/n”);
            }
            else if (errno == EAGAIN)
            {
             // read()以非阻塞方式读, 但没读到数据

             }
        }
        else 
        {
            write(STDOUT_FILENO, buf, n);
        }
    }

    exit(0);
}

运行该程序,从另一个终端给该进程发送信号SIGUSR1,结果如下:

$./a.out

first line

first line

in sigusr: SIGUSR1

SIGUSR1 received

second line

second line

in sigusr: SIGUSR1

SIGUSR1 received

^C


可见对由signal()函数安装的信号处理程序,系统默认会自动重启动被中断的系统调用(即重新执行read和write函数),而不是让它出错返回(出错会打印read is interrupted,并没有打印),所以应用程序不必针对慢速系统调用的errno,做EINTR检查,这就是自动重启动机制。


我们再来看另外一个例子,它使用另一个函数sigaction()来安装信号处理程序。sigaction()允许进程对信号进行更多的控制:

例程3 信号与自动重启动的sigaction版本

static void pr_mask(const char * string) 
{
    sigset_t procmask;

    sigprocmask(SIG_SETMASK, NULL, &procmask);

    printf("%s: ", string);
    if(sigismember(&procmask, SIGINT))
        printf("SIGINT ");
    if(sigismember(&procmask, SIGUSR1))
       printf("SIGUSR1 ");
   if(sigismember(&procmask, SIGUSR2))
        printf("SIGUSR2 ");
    if(sigismember(&procmask, SIGTERM))
        printf("SIGTERM ");
    if(sigismember(&procmask, SIGQUIT))
        printf("SIGQUIT ");
    printf("/n");
}

static void sigusr(int signum)
{
    pr_mask(“int sigusr”);

    if(signum == SIGUSR1)
        printf(“SIGUSR1 received/n”);
    else if(signum == SIGUSR2)
        printf(“SIGUSR2 received/n”);
    else
        printf(“signal %d received/n”, signum);
}


int main(void)
{
    char buf[BUFSIZ];
    int n;

    struct sigaction sa_usr;
    sa_usr.flags = 0; //SA_RESART

    sa_usr.sa_handler = sig_usr;
    sigaction(SIGUSR1, &sa_usr, NULL);

    //signal(SIGUSR1, sig_usr);

    while(1)
    {
        if((n = read(STDIN_FILENO, buf, BUFSIZ)) == -1) 
        {
            if(errno == EINTR)
                printf(“read is interrupted/n”);
        }
        else 
        {
            write(STDOUT_FILENO, buf, n);
        }
    }

    exit(0);
}

此时再运行这个程序,并从另一终端给该进程发送信号SIGUSR1,我们会得到如下结果。

$./a.out

first line

first line

in sigusr: SIGUSR1    //这里说明开始发送SIGUSER1信号了

SIGUSR1 received

read is interrupted    //打印了发送信号产生的报错信息了 

second line

second line

in sigusr: SIGUSR1

SIGUSR1 received

read is interrupted

^C

由此我们可以得出,Linux对sigaction()的默认动作是不自动重启动被中断的系统调用,因此如果我们在使用sigaction()时需要自动重启动被中断的系统调用,就需要使用sigaction的SA_RESTART选项。


可重入函数
如前所述,进程在收到信号并对其进行处理时,会暂时中断当前正在执行的指令序列,转而去执行信号处理程序。但是信号的到来,往往是无法预测的,我们无法确定进程会在何时收到信号。如果进程在收到信号时正在执行malloc()调用,而此时捕捉到信号,进城就会转而去执行信号处理程序,而信号处理程序中又再次调用了malloc()函数,那结果将会怎样呢?进程的栈空间很可能就会受到破坏,从而产生无法预料的结果。所以有些函数是不能在信号处理程序中调用的,这些函数被称为不可重入函数,而那些允许在信号处理函数中调用的函数,则称为可重入函数。

下表列出了Linux系统中的可重入函数(摘自《UNIX环境高级编程》),对不在该表中的函数,信号处理函数中要慎用。
表1 可重入函数

发送信号的kill和raise函数

int kill(pid_t pid, int sig);

int raise(int sig);

函数功能:

kill()发送信号给指定进程,raise()发送信号给进程本身。

对kill()的pid,有如下描述:

pid > 0 将信号发送给ID为pid的进程

pid == 0 将信号发送给与发送进程属于同意个进程组的所有进程

pid < 0 将信号发送给进程组ID等于pid绝对值的所有进程

pid == -1 将信号发送给该进程有权限发送的系统里的所有进程

 
所有信号的发送都要先经过权限检查,如果进程没有相应发送的权限,kill()会出错返回,并把errno设为EPERM。但也有一个例外,对SIGCONT,进程可以将它发送给当前会话的所有进程。

 
产生时钟信号SIGALRM的alarm函数
unsigned int alarm(unsigned int seconds);

alarm()函数可设置一个计时器,计时器超时就产生SIGALRM信号。由于每个进程只能有一个SIGALRM处理程序,所以只能为一个进程设置一个计时器,所以alarm()和setitimer()会共享同一个SIGALRM信号和该信号的处理函数。也就是说,alarm()和 setitimer()彼此会互相影响。调用alarm(),会使先前设置的计时器失效,并把没有超时的时间作为当前alarm的返回值。如先前设置的时钟为100秒,当前调用alarm()时才经过30秒,剩余的70秒就作为alarm()的返回值,并用alarm()中指定的秒数重新设置计时器。如果 seconds为0,则会取消先前设置的计时器,并将其余留值作为alarm()的返回值。


等待信号的pause函数

int pause(void);

pause()会使当前进程挂起,直到捕捉到一个信号,对指定为忽略的信号,pause()不会返回。只有执行了一个信号处理函数,并从其返回,puase()才返回-1,并将errno设为EINTR。详见前面的第一个例子。

 
信号屏蔽字(process signal mask)

每个进程都会有一个信号屏蔽字,它规定了当前进程要阻塞的信号集。对于每种可能的信号,信号屏蔽字中都会有一位与之对应,如果该位被设置,该信号当前就是阻塞的。

进程可以通过sigprocmask()来获得和修改当前进程的信号屏蔽字。


信号集函数(signal set)
信号集是一种特殊的数据类型,由于无法确定信号的多少,所以不能用简单数据类型来包含所有可能的信号,所以系统就定义了一个 sigset_t的数据类型专门用于信号集

同时还定义了一组用于处理信号集的函数。这样用户可以不必关心信号集的实现,只要使用这组函数来处理信号集就可以了。


信号集函数
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(sigset_t * set, int signum);

sigemptyset()和sigfillset()都用于初始化一个信号集,前者用于清空信号集中所有的信号,后者则用于设置信号集中所有的信号;

信号集在使用前必须要经过初始化,初始化后,就可以用sigaddset()和sigdelset()往信号集里添加删除信号了。

sigismember()用于判断指定信号是否在信号集中。

修改信号屏蔽字的sigprocmask函数

int sigprocmask(int how, const sigset_t * set, sigset_t * oldset);

sigpromask()根据how指定的方式,设置进程的当前信号屏蔽字为set,并将旧的信号屏蔽字保存在oldset中返回。如果set为 NULL,则不修改当前信号屏蔽字,而将其通过oldset返回;如果oldset为NULL,则不会返回旧的信号屏蔽字。

how支持三种方式,见下表。
表2 设置信号屏蔽字的方式

 如果我们想阻塞SIGUSR1,有两种方式。

// using SIG_BLOCK 2.sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigset, NULL);

// or using SIG_SETMASK

sigset_t set, oldset;
// get current signal mask

sigprocmask(SIG_SETMASK, NULL, &set);
// add SIGUSR1 into the signal mask

sigaddset(&set, SIGUSR1);
sigprocmask(SIG_SETMASK, &set, &oldset);

同样,如果要解除阻塞SIGUSR1,也有两种方式。

// using SIG_UNBLOCK

sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_UNBLOCK, &sigset, NULL);

// or using SIG_SETMASK

sigset_t set, oldset;
// get current signal mask

sigprocmask(SIG_SETMASK, NULL, &set);
// delete SIGUSR1 from the signal mask

sigdelset(&set, SIGUSR1);
sigprocmask(SIG_SETMASK, &set, &oldset);


信号未决(pending)
信号是由某些事件产生的,这些事件可能是硬件异常(如被零除),软件条件(如计时器超时),终端信号或调用kill()/raise()函数。信号产生时,内核通常会在进程表中设置某种标志,表示当前信号的状态。当内核对信号采取某种动作时,我们说向进程递送(deliver)了一个信号,而在信号产生和递送之间的间隔内,该信号的状态是未决的(pending)。


获得未决的信号sigpending

int sigpending(sigset_t * set);
该函数在set中返回进程中当前尚未递送的信号集。


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

 sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oldact指针非空,则通过oldact传出该信号原来的处理动作。act和oldact指向sigaction结构体: 

struct sigaction {

void (*sa_handler) (int);

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

sigset_t sa_mask;

int sa_flags;

};

 
siginfo_t {

int si_signo; // Signal number

int si_errno; // An errno value

int si_code; // signal code

pid_t si_pid; // sending process ID

pid_t si_uid; // Real user ID of sending process

int si_status; // Exit value or signal

...

};

 signum是指定信号的编号

sa_handler:信号处理器函数的地址,亦或是常量SIG_IGN、SIG_DFL之一。仅当sa_handler是信号处理程序的地址时,亦即sa_handler的
取值在SIG_IGN和SIG_DFL之外,才会对sa_mask和sa_flags字段加以处理。
sa_sigaction:如果设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。
sa_mask:定义一组信号,在调用由sa_handler所定义的处理器程序时将阻塞该组信号,不允许它们中断此处理器程序的执行。
sa_flags:位掩码,指定用于控制信号处理过程的各种选项。
SA_NODEFER:捕获该信号时,不会在执行处理器程序时将该信号自动添加到进程掩码中。
SA_ONSTACK:针对此信号调用处理器函数时,使用了由sigaltstack()安装的备选栈。
SA_RESETHAND:当捕获该信号时,会在调用处理器函数之前将信号处置重置为默认值(即SIG_IGN)。
SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息

将sa_handler/sa_sigaction赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号.

在sigaction结构中,sa_handler和sa_sigaction用于指定信号处理函数,但要注意,二者只能用其一,因为它们在内部可能会实现为union结构。除了在为sa_flags指定SA_SIGINFO标志时,会使用sa_sigaction字段外,其他情况下都应该只用sa_handler字段。

sa_mask用于指定在当前信号处理程序执行期间,需要阻塞的信号集。如在处理SIGUSR1期间,我们希望暂时阻塞SIGUSR2,就应该把SIGUSR2加到SIGUSR1的sa_mask中。信号处理程序返回后,会自动解除对SIGUSR2的阻塞,详见例程4。

sa_flags用于指定信号处理动的选项标志,详见手册。这里我想说的是SA_RESTART和SA_SIGINFO。

SA_RESTART用于控制信号的自动重启动机制,如前面例子所示,对signal(),Linux默认会自动重启动被中断的系统调用;而对于 sigaction(),Linux默认并不会自动重启动,所以如果希望执行信号处理后自动重启动先前中断的系统调用,就需要为sa_flags指定 SA_RESTART标志。

SA_SIGINFO,手册上说此标志可能会导致对信号的可靠排队,但是从下面的例子我们将会看到,Linux并没有对信号进行排队。


例程4 sigaction函数

static void pr_mask(const char * string) 
{
    sigset_t procmask;

    sigprocmask(SIG_SETMASK, NULL, &procmask);

    printf("%s: ", string);
    if(sigismember(&procmask, SIGINT))
        printf("SIGINT ");
    if(sigismember(&procmask, SIGUSR1))
       printf("SIGUSR1 ");
   if(sigismember(&procmask, SIGUSR2))
        printf("SIGUSR2 ");
    if(sigismember(&procmask, SIGTERM))
        printf("SIGTERM ");
    if(sigismember(&procmask, SIGQUIT))
        printf("SIGQUIT ");
    printf("/n");
}

static void sigusr(int signum)
{
    pr_mask(“int sigusr”);

    if(signum == SIGUSR1)
        printf(“SIGUSR1 received/n”);
    else if(signum == SIGUSR2)
        printf(“SIGUSR2 received/n”);
    else
        printf(“signal %d received/n”, signum);
}

int main(void)
{
    struct sigaction act_usr;

    act_usr.sa_flags = 0;
    act_usr.sa_handler = sigusr;
    sigemptyset(&act_usr.sa_mask);
    
    // add the signal you want to block while SIGUSR1 is processing here
    sigaddset(&act_usr.sa_mask, SIGUSR2);

    sigaction(SIGUSR1, &act_usr, NULL);

    while(1)
        pause();
}

运行结果如下:

$./a.out &

[1] 16385

$kill -USR1 16385

in sig_usr1: SIGUSR1 SIGUSR2

SIGUSR1 recieved

可见在SIGUSR1处理期间,SIGUSR2已经被加入到进程的屏蔽字中了,所以在此期间,SIGUSR2是被暂时阻塞的。

 
信号排队
如果进程阻塞了一个信号,在没有对其解除阻塞之前,该信号产生了多次,将会如何处理呢?Linux并不会对信号排队,当信号解除阻塞后,内核只向进程递送一个信号,而不管在其阻塞期间有多少个信号产生。
下面是上例的改进版。首先我们阻塞SIGUSR1,然后在SIGUSR2的处理函数里解除对SIGUSR1的阻塞,这样我们就有机会在SIGUSR1阻塞期间,多发送几个SIGUSR1来确定Linux内核是怎样处理的。我们期望能看到Linux对信号的排队。


例程5 信号排队

static void sig_usr2(int sig)
{
    sigset_t set;
    printf("SIGUSR2 recieved/n");
    // unblock SIGUSR1

    sigprocmask(SIG_SETMASK, NULL, &set);
    sigdelset(&set, SIGUSR1);
    sigprocmask(SIG_SETMASK, &set, NULL);
}

static void handler(int signum, siginfo_t * info, void * context)
{
    // dump signal information

    printf("si_signo: %d/n", info->si_signo);
    printf("si_errno: %d/n", info->si_errno);
    printf("si_code: %d/n", info->si_code);
    printf("si_pid: %d/n", info->si_pid);
    printf("si_uid: %d/n", info->si_uid);
}

int main(void)
{
    struct sigaction act_usr1;
    struct sigaction act_usr2;
    sigset_t mask;

    act_usr1.sa_flags = SA_SIGINFO;
    //act_usr1.sa_handler = sigusr; 

    sigemptyset(&act_usr1.sa_mask);
    act_usr1.sa_sigaction = handler;
    sigaction(SIGUSR1, &act_usr1, NULL);

    act_usr2.sa_flags = 0;
    act_usr2.sa_handler = sig_usr2;
    sigemptyset(&act_usr2.sa_mask);
    sigaction(SIGUSR2, &act_usr2, NULL);

    // block SIGUSR1

    sigprocmask(SIG_SETMASK, NULL, &mask);
    sigaddset(&mask, SIGUSR1);
    sigprocmask(SIG_SETMASK, &cmask, NULL);

    while(1)
        pause();
    exit(0);
}

测试结果:

$ ./a.out &

[1] 17165

$ kill -USR1 17165

$ kill -USR1 17165

$ kill -USR1 17165

$ kill -USR1 17165

$ kill -USR1 17165

$ kill -USR2 17165

SIGUSR2 recieved

si_signo: 10

si_errno: 0

si_code: 0

si_pid: 3945

si_uid: 500


$ ps

PID TTY TIME CMD

3945 pts/1 00:00:00 bash

 在SIGUSR1阻塞期间,我们向进程发送了5个SIGUSR1,而解除阻塞后,内核只递送了一个SIGUSR1,说明Linux并不支持信号排队。另外我们还可以看到,si_signo是收到的信号的数值;si_pid是发送进程的进程ID,ps输出我的终端进程ID正是 3945;si_uid是发送进程的有效用户ID,而我的用户ID也正是500。对于siginfo结构中的其它成员,我没有打印,有兴趣的可以自己研究。


信号跳转函数sigsetjump和siglongjump
int sigsetjmp(sigjmp_buf env, int savesigs);

void siglongjmp(sigjmp_buf env, int val);

sigsetjmp()有多次返回,对于直接调用者(一般是主程序),它返回0;若从siglongjmp()调用(一般是信号处理程序),则返回返回siglongjmp()中的val值。所以为了避免混淆,最好不要在调用siglongjmp()时,让val=0。


另外需要说明的是sigsetjmp()的第二个参数,它用于告诉内核,要不要保存进程的信号屏蔽字。当savesigs为非0时,调用 sigsetjmp()会在env中保存当前的信号屏蔽字,然后在调用siglongjmp()时恢复之前保存的信号屏字。由于信号处理函数使用 siglongjmp()跳转时不属于正常返回,所以在进入信号处理函数时被阻塞的当前信号就没有机会在返回时恢复。sigsetjmp()的 savesigs参数就用于是告诉系统,在调用siglongjmp时,是否需要恢复先前的信号屏蔽字。

下例向你展示了如何使用sigsetjmp()和siglongjmp(),注意这里引入了一个全局变量canjmp,它是一种同步保护机制,用于告诉信号处理程序,在进程环境没有准备好之前,不要跳转,否则可能会导致混乱。

 
例程6 信号跳转

static sigjmp_buf jmpbuf;
// for synchronizing

static volatile sig_atomic_t canjmp;

static void sigusr1(int signum)
{
    printf(“SIGUSR1 reveived/n”);

    // main process initialization is not completed

    if(canjmp == 0)
        return;

    siglongjmp(jmpbuf, 1);
}

satic void sigusr2(int signum)
{
    printf(“SIGUSR2 reveived/n”);

    if(canjmp == 0)
        return;

    siglongjmp(jmpbuf, 2);
}

int main(void)
{
    int n;
    int savemask = 1;

    signal(SIGUSR1, sigusr1);
    signal(SIGUSR2, sigusr2);

    // need to save the procmask, otherwise, u have to reset the procmask

    n = sigsetjmp(jmpbuf, savemask);

    if(n == 1) 
    {
        // jump from SIGUSR1

        printf(“Jump to here from SIGUSR1/n”);
        if(savemask == 0) 
        {
            // prevent from long jumping

            canjmp = 0;
            // reset the procmask, unblock SIGUSR1

            sigset_t set;
            sigprocmask(SIG_SETMASK, NULL, &set);
            sigdelset(&set, SIGUSR1);
            sigprocmask(SIG_SETMASK, &set, NULL);
            canjmp = 1;
        }
    }
    else if(n == 2) 
    {
        printf(“Jump to here from SIGUSR2/n”);
        if(savemask == 0) 
        {
            canjmp = 0;
            sigset_t set;
            sigprocmask(SIG_SETMASK, NULL, &set);
            sigdelset(&set, SIGUSR1);
            sigprocmask(SIG_SETMASK, &set, NULL);
            canjmp = 1;
        }
    }

    canjmp = 1;

    while(1)
        pause();
    exit(0);
}

测试结果:

$ ./a.out &

[1] 5485

$ kill -USR1 5485

SIGUSR1 recieved

Jump to here from SIGUSR1

$ kill -USR2 5485

SIGUSR2 recieved

Jump to here from SIGUSR2

例程6告诉我们,根据sigsetjmp()的返回值,我们也可以通过信号实现程序的多分支控制。另外如果没有在sigsetjmp()时设置了savesigs,那么在siglongjmp()返回后,就要重新设置进程的信号屏蔽字,否则该信号在一次siglongjmp()之后将被永久阻塞。

 
难以捉摸的sigsuspend函数
int sigsuspend(const sigset_t * sigmask);

对于这个函数,我始终无法清晰的理解,关于它的用法,它的作用,它的语义,都让我一头雾水。《UNIX环境高级编程》,Linux手册,看了几遍,都无法开塞,真是愚钝至极啊!

从《UNIX环境高级编程》对sigsuspend()的引言看,该函数的出现是为了解决早期不可靠信号,即信号丢失的问题的。在早期的信号机制中,对信号解除阻塞和等待信号需要两步进行: 
sigprocmask(SIG_SETMASK, &unblockmask, NULL);

pause();
在对信号解除阻塞之后和调用pause()之前有一个时间窗口,所以在这之间产生的信号就可能会丢失,从而是本该返回的pause()没有返回。

 
sigsuspend()能让解除信号阻塞和等待信号成为一个原子操作,这样就避免了上述的问题。它会把当前进程的信号屏蔽字设定为 sigmask指定的值,所以在等待信号期间,sigmask中的信号会被暂时阻塞,而sigmask之外的信号都会被暂时解除阻塞。然后 sigsuspend()挂起当前进程,等待,直到捕捉到一个信号或发生了一个会终止该进程的信号。如果是捕捉到一个信号并从出来程序中返回,则 sigsuspend()返回-1,把进程信号屏蔽字设回调用sigsuspend()之前的值,并将errno设为EINTR。注意,指定为忽略的信号,并不会导致sissuspend()返回。

 
注意,sigsuspend()只是暂时解除对不在sigmask中的信号的阻塞,在捕捉到一个信号后,以前阻塞的信号还会被重新阻塞,所以如果你要对一个以前阻塞的信号解除阻塞的话,在sigsuspend()返回之后,还要重新用sigprocmask来解除对该信号的阻塞。

sigsuspend()的另一个用途就是让进程等待一个信号处理程序来设置一个全局变量。这个似乎还比较有用,在进程等待某一信号时,可让进程先挂起,直到收到该信号,设置的全局变量并导致sigsuspend()返回,进程才处理相应的任务,避免了CPU的无谓等待。如下例程就展示了如何让进程等待SIGUSR1,并在每收到一次SIGUSR1后去执行一组相同的操作。


发送SIGABRT的专用函数abort
int abort(void); 
abort会向当前进程发送SIGABRT信号,让进程做一些“善后”处理,如刷新流缓冲区,关闭文件等,然后终止进程。


最后的忠告:千万不要去写一个复杂的信号处理程序,那是最出力不讨好的事情,信号处理程序每多一行,你的程序莫名崩溃的可能性就增大一分,谁能保证你在信号处理函数里调用的都是可重入函数呢?本文中的很多例程使用的printf()就不是可重入的……信号处理程序应该尽量简单,对稍微复杂的任务,应该想办法(如siglongjmp(),设置全局标志等)交给主程序处理。

参考:

Linux信号signal介绍,signal()函数,sigaction()函数_signal函数sigusr1-CSDN博客

Linux signal、sigaction的使用总结_linux sigaction-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值