有朋友用 PHP 写了一个工具(limit.php
),用来限制另一个进程的执行时间,代码如下:
<?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;};'
做测试:
php limit.php "php -r 'while(1){sleep(1);echo PHP_OS;};'" 10
可以看到共有三个进程:
$ 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;};
其中:
- PID 为 21233 的进程 (
进程 A
) 是第一个启动的进程 - PID 为 21234 的进程 (
进程 B
) 是在 21233 中 fork 出来的子进程 - PID 为 21235 的进程 (
进程 C
) 是 21234 中使用exec
fork 并替换的孙子进程
在 10 秒钟之后,进程 A
向 进程 B
发 KILL 信号,之后 A
, B
, C
3 个进程都退出了,符合对 limit.php
预想功能的期望。
但是,将上面 进程 C
稍微改动一下,移除输出语句:
php limit.php "php -r 'while(1){sleep(1);};'" 10
在 10 秒钟之后,进程 A
和 进程 B
退出,而 进程 C
仍然在继续运行:
$ 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;};'
:
$ 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
掉了只是假象,歪打正着而已:
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
信号:
<?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
:
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 代码:
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)