subporcess模块超时杀死子进程问题


前言

笔者在工作中常常使用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开启的子进程情况以及如何杀死子进程等知识。

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值