python执行cmd subprocess持续_如何在python的程序中调用subprocess?

本文分析了Python使用subprocess.Popen执行命令时遇到的并发问题,当命令启动守护进程时可能导致读管道卡住。问题源于子进程继承了不必要的文件描述符,造成资源未及时释放。解决方案是设置`close_fds=True`,关闭除标准输入、输出、错误外的所有文件描述符,防止并发调用时的管道持有问题。
摘要由CSDN通过智能技术生成

0. 结论

简单的结论:

subprocess.Popen永远要考虑将参数close_fds设置为True。

通用的结论

fork进程时要考虑到子进程会共享父进程的所有已打开文件,在某些场景下尤其需要考虑到这可能会造成资源未及时释放的问题。

1. 问题背景

偶尔会有流程超时问题出现,发现现网的母机上存在mount.ntfs-3g或qemu-nbd进程被Compute通过subprocess.Popen(类似于glibc里的popen)拉起后确没有获取到命令的返回结果。

一开始我并没有怀疑到是popen的问题,因为Compute通过subprocess.Popen调用外部命令是项目中最基本的代码,经验上也经受了数以亿次调用的考验,似乎不应该会是这段逻辑的问题。

通过strace分析发现,Compute卡在read管道,而管道被qemu-nbd或mount.ntfs-3g持有。

# strace -p 118693

Process 118693 attached - interrupt to quit

read(31,

# ls -l /proc/118693/fd/31

lr-x------ 1 root root 64 Aug 10 16:17 /proc/118693/fd/31 -> pipe:[881608557]

# lsof | grep 881608557

qemu-nbd 118823 root 31r FIFO 0,8 0t0 881608557 pipe

qemu-nbd 118823 root 32w FIFO 0,8 0t0 881608557 pipe

vstationd 200949 root 31r FIFO 0,8 0t0 881608557 pipe

2. 理论分析

不论是Python这样的基于字节码解释器的语言,还是裸用glibc,popen的原理都是相似的:父子进程之间通过pipe通信:

父进程创建pipe

父进程fork出子进程

父进程关闭pipe的写fd,子进程关闭pipe的读fd

子进程将pipe的写fd dup到标准输出(fd=2),即子进程的输出将重定向到管道

子进程退出(变成僵尸进程),父进程收到SIGCHLD信号,父进程的wait等调用返回,僵尸进程退出

父进程读管道,一般为了获取到所有输出,会循环读管道直到遇到EOF,读操作返回

标准错误的pipe方式也是类似的。

在这个过程中可以看到一个与现网现象非常相似的操作——读管道。

再反过来看在“数以亿计”的调用中出错的两类无返回命令: qemu-nbd和mount.ntfs-3g,它们有一个共同的特点:

都是daemon进程。

daemon进程从不退出,也就是说popen调用的命令如果是daemon,那这个命令的子进程退出,其子进程的子进程(daemon)却永远在运行。那假设有这么个过程,父进程的读管道永远不返回就得以构建:

父进程创建pipe

父进程fork出子进程

父进程关闭pipe的写fd,子进程关闭pipe的读fd

子进程将pipe的写fd dup到标准输出(fd=2),即子进程的输出将重定向到管道

子进程再fork子进程,并使其与当前会话解除,成为daemon进程,并持有管道

子进程退出(变成僵尸进程),父进程收到SIGCHLD信号,父进程的wait等调用返回

父进程读管道,但因为daemon持有管道,无法获取到EOF,卡住!

3. 测试程序

裸写一个daemon程序

#include

#include

#include

#include

#include

#include

#include

int main(){

pid_t daemon;

int i, fd;

daemon = fork();

if(daemon < 0){

printf("Fork Error!\n");

exit(1);

}

else if (daemon > 0 ) {

printf("Father process exited!\n");

exit(0);

}

setsid();

umask(0);

printf("in daemon!\n");

sleep(3600);

exit(0);

}

使用subprocess.Popen的包装如下:

def system(cmd, timeout=3):

data = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

wait_time = 0.2

use_time = 0.0

while use_time <= timeout:

retcode = data.poll()

if retcode is None:

use_time += wait_time

time.sleep(wait_time)

else:

out_msg = data.stdout.read()

err_msg = data.st

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值