Linux信号丢失问题分析

来源:互联网 发布:c语言怎么求素数的和 编辑:程序博客网 时间:2024/04/30 08:36

转载自http://hi.baidu.com/qiupingwu/item/f8ff6d3856c051b8633aff0c


1. 问题引入


  我们想实现这样的一个功能:通过使用~SIGUSR1~信号实现对守护进程的重启。

  我们编写了如下代码:

/*restart1.c*/
#include<signal.h>
#include<stdio.h>

static void sig_usr(int signo)
{
    if (SIGUSR1 == signo)
     {
         printf("received SIGUSR1\n");
         system("restart.sh");
     }
    else
     {
         printf("received signal%d\n", signo);
     }
}

int main()
{
    if (SIG_ERR == signal(SIGUSR1, sig_usr))
     {
         printf("can't catch SIGUSR1\n");
     }

    for (;;)
     {
         pause();
     }
}

#!/bin/bash
# restart.sh
pkill -9 daemon
sleep 1
./daemon &

  经测试,发现的问题是当~daemon~程序第一次启动时,使用~kill -USR1 <pid>~命令可以令其重启。但是对于此后重启的~daemon~进程再也接收不到~kill~发送的~USR1~信号。

2. 原因分析

  先是怀疑是不是使用~signal~存在不可靠信号丢失问题,于是改为~sigaction~进行测试。

  修改后的代码为:

/*restart2.c*/
#include<signal.h>
#include<stdio.h>

static void sig_usr(int signo)
{
    if (SIGUSR1 == signo)
     {
         printf("received SIGUSR1\n");
         system("restart.sh");
     }
    else
     {
         printf("received signal%d\n", signo);
     }
}

int main()
{
    struct sigaction action;

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

    for (;;)
     {
         pause();
     }
}

测试结果表明,问题依旧。

  开始怀疑会不会是前后创建的进程不属于相同用户(或者是用户组、TTY、父进程)所致,但是通过使用~ps -ajx~命令查看,发现这些属性是相同的。

  在测试过程还发现一个现象就是~daemon~在上一次接收过的信号,在其重启后都不能接收;但还没接收过的信号,仍然可以正常接收。

  在阅读~LINUX kernel~源码的时候,发现~LINUX~在派生进程时,存在继承父进程信号屏蔽标识的现象。于是怀疑是不是由于子~daemon~进程不断继承父进程的屏蔽标识,致使用不能接收~USR1~信号?但是父进程又为何要屏蔽~USR1~信号呢?

  查看~restart1.c~得知,我们在~sig\_usr~中执行~system~操作,从而派生了~daemon~进程。根据~LINUX~的信号 处理机制,我们知道为妨止在处理信号的过程中又来重复信号造成信号丢失,会采取屏蔽正在处理的信号标志位,以让重复信号排队;在处理完上一个信号后,再打 开标志位,接着处理重复信号。

  于是在执行~sig\_usr~时,USR1~标志位是屏蔽的,而此时派生的进程该标志位估计也是屏蔽的(后面我们采用例子验证)。因此当~daemon~进程起来后,就不能接收~USR1~信号了。

  一般~LINUX~编程建议,不要在中断中处理过于复杂的事情,因此首先简化~sig\_usr~进行测试。修改后的代码如下:

/*restart3.c*/
#include<signal.h>
#include<stdio.h>

static int resetflag = 0;

static void sig_usr(int signo)
{
    if (SIGUSR1 == signo)
     {
         printf("received SIGUSR1\n");
         resetflag = 1;
     }
    else
     {
         printf("received signal%d\n", signo);
     }
}

int main()
{
    if (SIG_ERR == signal(SIGUSR1, sig_usr))
     {
         printf("can't catch SIGUSR1\n");
     }

    for (;;)
     {
         pause();

        if (1 == resetflag)
         {
             system("restart.sh");
             exit(0);
         }
     }
}

测试通过。

  再来验证一下重启的~daemon~子进程是否继承了父进程的屏蔽标志位,我们尝试强行清除子进程的~USR1~信号屏蔽标志位即可。

  修改后的代码如下:

/*restart4.c*/
#include<signal.h>
#include<stdio.h>

static void sig_usr(int signo)
{
    if (SIGUSR1 == signo)
     {
         printf("received SIGUSR1\n");
         system("restart.sh");
     }
    else
     {
         printf("received signal%d\n", signo);
     }
}

int main()
{
    struct sigaction action;
     sigset_t set;

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

     sigaddset(&set, SIGUSR1);
    if (0 > sigprocmask(SIG_UNBLOCK, &set, NULL))
     {
         printf("sigprocmask error\n");
        return 1;
     }

    for (;;)
     {
         pause();
     }
}

测试仍然正常通过。

  另外,还可以通过查看~/proc/<pid>/status~文件更直观地查看到指定进程的信息屏蔽情况。
+--------+-------------------------------+------------------+
| 段名 |        含义       |    例子    |
+--------+-------------------------------+------------------+
| SigPnd | The bitmap of pending signals | 0000000000000000|
|    | (usually 0)           |          |
+--------+-------------------------------+------------------+
| SigBlk | The bitmap of blocked signals |0000000000000000|
|    | (usually 0, 2 for shells)   |          |
+--------+-------------------------------+------------------+
| SigIgn | The bitmap of ignored signals |0000000000000027|
+--------+-------------------------------+------------------+
| SigCgt | The bitmap of catched signals | 0000000180000a00 |
+--------+-------------------------------+------------------+

3. 结论
  1) 不要在中断函数中执行过于复杂的处理流程;
  2) 在信号处理过程返回前,进程会对相同信号的标志进行屏蔽;
  3) 父进程会把当前的信号屏蔽标志位信息传递给它派生的子进程。
原创粉丝点击