Xupeng's blog

圆外之大,心向往之

KILL 和 SIGPIPE

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

limit.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
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
<?php
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)

Comments