IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    KILL 和 SIGPIPE

    Xupeng发表于 2012-08-11 14:49:00
    love 0

    有朋友用 PHP 写了一个工具(limit.php),用来限制另一个进程的执行时间,代码如下:

    limit.php
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    
    declare(ticks = 1);
    
    if ($argc<2) die("Wrong parameter\n");
    $cmd = $argv[1];
    $tl = isset($argv[2]) ? intval($argv[2]) : 3;
    
    $pid = pcntl_fork();
    if (-1 == $pid) {
        die('FORK_FAILED');
    } elseif ($pid == 0) {
        exec($cmd);
        posix_kill(posix_getppid(), SIGALRM);
    } else {
        pcntl_signal(SIGALRM, create_function('$signo',"die('EXECUTE_ENDED\n');"));
        sleep($tl);
        posix_kill($pid, SIGKILL);
        die("TIMEOUT_KILLED : $pid\n");
    }
    

    使用这个工具对 php -r 'while(1){sleep(1);echo PHP_OS;};' 做测试:

    1
    
    php limit.php "php -r 'while(1){sleep(1);echo PHP_OS;};'" 10
    

    可以看到共有三个进程:

    1
    2
    3
    4
    
    $ ps -u $USER -opid,ppid,pgid,command|grep whil[e]
    21233 20858 21233 php limit.php php -r 'while(1){sleep(1);echo PHP_OS;};' 10
    21234 21233 21233 php limit.php php -r 'while(1){sleep(1);echo PHP_OS;};' 10
    21235 21234 21233 php -r while(1){sleep(1);echo PHP_OS;};
    

    其中:

    1. PID 为 21233 的进程 (进程 A) 是第一个启动的进程
    2. PID 为 21234 的进程 (进程 B) 是在 21233 中 fork 出来的子进程
    3. PID 为 21235 的进程 (进程 C) 是 21234 中使用 exec fork 并替换的孙子进程

    在 10 秒钟之后,进程 A 向 进程 B 发 KILL 信号,之后 A, B, C 3 个进程都退出了,符合对 limit.php 预想功能的期望。

    但是,将上面 进程 C 稍微改动一下,移除输出语句:

    1
    
    php limit.php "php -r 'while(1){sleep(1);};'" 10
    

    在 10 秒钟之后,进程 A 和 进程 B 退出,而 进程 C 仍然在继续运行:

    1
    2
    3
    4
    5
    6
    
    $ ps -u $USER -opid,ppid,pgid,command|grep whil[e]
    21372 20858 21372 php limit.php php -r 'while(1){sleep(1);};' 10
    21373 21372 21372 php limit.php php -r 'while(1){sleep(1);};' 10
    21374 21373 21372 php -r while(1){sleep(1);};
    $ ps -u $USER -opid,ppid,pgid,command|grep whil[e]
    21374     1 21372 php -r while(1){sleep(1);};
    

    至此,疑问产生了,标准输出会影响 KILL 么?

    而实际上,父进程退出之后,子进程作为孤儿进程继续运行,这才是 Linux 下预期的正常行为,那么在第一种情况下,进程 B 被杀死之后,进程 C (php -r 'while(1){sleep(1);echo PHP_OS;};') 为什么也退出了呢?

    使用 strace 跟踪 php -r 'while(1){sleep(1);echo PHP_OS;};':

    1
    2
    3
    4
    5
    6
    7
    8
    
    $ php limit.php "strace -ftt -o limit.strace php -r 'while(1){sleep(1);echo PHP_OS;};'" 10
    $ grep -C 2 Broken limit.strace
    22558 17:26:13.606751 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    22558 17:26:13.606823 nanosleep({1, 0}, 0x7fffcbe00a60) = 0
    22558 17:26:14.607080 write(1, "Linux", 5) = -1 EPIPE (Broken pipe)
    22558 17:26:14.607186 --- SIGPIPE (Broken pipe) @ 0 (0) ---
    22558 17:26:14.607321 close(2)          = 0
    22558 17:26:14.607403 close(1)          = 0
    

    可以看到 php -r 'while(1){sleep(1);echo PHP_OS;};' 在向标准输出写 Linux 时收到了 SIGPIPE 信号,它没有忽略或者处理 SIGPIPE 信号,于是就退出了。之所以会收到 SIGPIPE 信号,是因为随着它的父进程退出,它和父进程之间管道的读端被关闭了。因此,当有输出时进程看起来是被 KILL 掉了只是假象,歪打正着而已:

    man 2 write
    1
    2
    3
    4
    5
    
    EPIPE  fd  is  connected  to  a  pipe  or  socket whose reading end is
           closed.  When  this  happens  the  writing  process  will  also
           receive  a  SIGPIPE  signal.   (Thus, the write return value is
           seen only if the program catches, blocks or ignores  this  sig‐
           nal.)

    为了实现这里期望的行为:一个进程退出之后,它的子进程也随着退出,通常不能只是对单个的进程发 KILL 信号,而是要对整个进程组发 KILL 信号,仍然拿这个 PHP 脚本来做例子,则是需要在 fork 子进程之后,将子进程放进单独的进程组,之后向这个进程组发 KILL 信号:

    limit.php
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    
    declare(ticks = 1);
    
    if ($argc<2) die("Wrong parameter\n");
    $cmd = $argv[1];
    $tl = isset($argv[2]) ? intval($argv[2]) : 3;
    
    $pid = pcntl_fork();
    if (-1 == $pid) {
        die('FORK_FAILED');
    } elseif ($pid == 0) {
        // 创建新的进程组
        $_pid = posix_getpid();
        posix_setpgid($_pid, $_pid);
        exec($cmd);
        posix_kill(posix_getppid(), SIGALRM);
    } else {
        pcntl_signal(SIGALRM, create_function('$signo',"die('EXECUTE_ENDED\n');"));
        sleep($tl);
        // 向整个进程组发 KILL 信号
        posix_kill(-$pid, SIGKILL);
        die("TIMEOUT_KILLED : $pid\n");
    }
    

    另外 Linux 平台也提供了父进程退出时通知子进程的机制,方法是子进程设置 pdeath_signal 属性,这样当父进程退出时,检查到子进程设置了 pdeath_signal,就向子进程发送预设的信号,下面的代码来自 kernel/exit.c:forget_original_parent:

    kernel/exit.c:forget_original_parent
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    list_for_each_entry_safe(p, n, &father->children, sibling) {
        struct task_struct *t = p;
        do {
            t->real_parent = reaper;
            if (t->parent == father) {
                BUG_ON(task_ptrace(t));
                t->parent = t->real_parent;
            }
            if (t->pdeath_signal)
                group_send_sig_info(t->pdeath_signal,
                            SEND_SIG_NOINFO, t);
        } while_each_thread(p, t);
        reparent_leader(father, p, &dead_children);
    }
    

    最后,这是一段演示如何使用这个通知机制来让子进程优雅退出的 Python 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    import os
    import sys
    import time
    import signal
    
    import prctl
    
    pid = os.fork()
    if pid == 0:
        def sig_handler(sig, frame):
            if sig == signal.SIGTERM:
                print 'exit'
                sys.exit(1)
        prctl.set_pdeathsig(signal.SIGTERM)
        signal.signal(signal.SIGTERM, sig_handler)
        while True:
            time.sleep(1)
    else:
        while True:
            time.sleep(1)
    


沪ICP备19023445号-2号
友情链接