有朋友用 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;} ;
其中:
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
稍微改动一下,移除输出语句:
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 )