前言
笔者在工作中常常使用subporcess
模块来执行shell命令,偶尔会遇到命令卡死的问题,这时需要利用到超时机制进行补救,防止某条命令进入卡死状态,导致整个程序卡顿。由于工作中用到的是python2.7
,没有超时设置的参数,因此需要笔者自行设置超时机制。
一、超时问题
threading模块的Timer类
就是一个定时器,可以很方便的使用。于是,笔者很快给出了第一版代码:
import subprocess
import threading
def shell_cmd(cmd, timeout=None):
process = subprocess.Popen(
cmd,
shell=True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
t = threading.Timer(timeout, lambda x: x.kill(), [process])
if timeout:
t.start()
stdout, stderr = process.communicate()
code = process.returncode
t.cancel()
return stdout, stderr, code
shell_cmd("sleep 100", timeout=10)
运行上述代码后,笔者很快发现根本无法实现所谓的超时退出的功能。笔者很快分析进程后发现,subprocess起的子进程的子进程没有被杀死
。
未设置超时退出机制:
host# ps -ef | grep sleep
root 37735 37730 0 09:58 pts/0 00:00:00 /bin/sh -c sleep 100
root 37736 37735 0 09:58 pts/0 00:00:00 sleep 100
root 44340 112591 0 09:58 pts/1 00:00:00 grep sleep
设置后:
host # ps -ef | grep sleep
root 74773 1 0 10:03 pts/0 00:00:00 sleep 100
root 81531 112591 0 10:03 pts/1 00:00:00 grep sleep
根据上述输出对比可知,父进程/bin/sh -c sleep 100确实已经被杀死,但是子进程sleep 100没有
,而且他的父进程id变成1,表明是init进程,就是说父进程被杀死后,子进程会被init进程接管
。
二、方法
经过查询资料可知,父进程死后,子进程会被init进程接管,不会退出。因此,很快就有两条路可以走:
(1)为什么subprocess起来的进程是一对父子进程,而非单个进程?如果是单个进程,那么上述代码可以生效
(2)如何一同杀死父进程和子进程?
1.subporcess的执行过程
经过查阅代码后可得知,subprocess的popen类中存在参数shell
:
shell=False
,表示在执行命令时不能直接执行命令字符串,而是执行命令数组。而且会将命令数组直接在subprocess起的子进程中执行。
shell=True
,表明会在popen开启的子进程中调用bash进程来执行命令字符串。
于是,我们做出如下调整即可:
import subprocess
import threading
def shell_cmd(cmd, timeout=None):
process = subprocess.Popen(
cmd,
shell=False,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
t = threading.Timer(timeout, lambda x: x.kill(), [process])
if timeout:
t.start()
stdout, stderr = process.communicate()
code = process.returncode
t.cancel()
return stdout, stderr, code
shell_cmd(["sleep, "100"], timeout=10)
但是上述代码也存在问题,如果执行的命令中存在子进程,那么还是无法杀死所有的进程。
2.杀死父进程及其子进程
经过查阅资料可知,我们可以利用psutil包来获取进程以及所有的子进程,然后全部杀死即可。
import psutil
import os
import subprocess
import threading
def kill_process_and_children(pid):
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
parent.kill()
def shell_cmd(cmd, timeout=None):
process = subprocess.Popen(
cmd,
shell=True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
t = threading.Timer(timeout, kill_process_and_children, [process.pid])
if timeout:
t.start()
stdout, stderr = process.communicate()
code = process.returncode
t.cancel()
return stdout, stderr, code
shell_cmd("sleep 100", timeout=10)
3.杀死进程组
另外,笔者也发现了popen的一个参数preexec_fn
(在python3中可以使用start_new_session
)可以达到杀死父进程,子进程也同时退出的效果。该参数的值是方法,是在popen类开启子进程之前执行的方法。
import subprocess
import threading
import signal
import os
def shell_cmd(cmd, timeout=None):
process = subprocess.Popen(
cmd,
shell=True,
preexec_fn=os.setsid
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
t = threading.Timer(timeout, lambda pid: os.killpg(pid, signal.SIGTERM), [process.pid])
if timeout:
t.start()
stdout, stderr = process.communicate()
code = process.returncode
t.cancel()
return stdout, stderr, code
shell_cmd("sleep 100", timeout=10)
setsid 就是linux中的同名函数:pid_t setsid(void)
;
查阅资料可知:如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:
(1) 此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期
的进程)。此进程是该新对话期中的唯一进程。
(2) 此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID
。
而我们可以通过进程组id向该组内所有进程发送kill信号,从而杀死所有进程
。当然如果子进程也开启一个新的组,那么这种方法也没法保证。
注意:preexec_fn参数无法再多线程中使用。
三、其他
当然,我们也可以根据python3的超时机制进行模仿。于是,翻阅源码后,笔者发现python3中也不能保证杀死子进程,而是通过线索的join超时机制来保证超市后不再读取stdout管道中内容,直接返回后再执行其他代码,但是总体上也可以保证防止程序卡死的意图了。
总结
本文简单介绍了subprocess 的popen开启的子进程情况以及如何杀死子进程等知识。