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