Subprocess Popen管道阻塞问题分析解决

使用Subprocess Popen的类库困挠了我一个月的问题终于解决了。

一句话就是:等待命令返回不要使用wait(),而是使用communicate(),但注意内存,大输出使用文件。

错误的使用例子

之前的代码这样使用的。

# 不合适的代码
def run_it(self, cmd):
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True,
                         stderr=subprocess.PIPE, close_fds=True)
    log.debug('running:%s' % cmd)
    p.wait()
    if p.returncode != 0:
        log.critical("Non zero exit code:%s executing: %s" % (p.returncode, cmd))
    return p.stdout

这段代码之前用着一直没有问题的,后来不知道为何就不能用了(后面知道了,原来输出内容增加,输出的问题本太长,把管道给堵塞了)。

这样的代码也在之前的一个项目中使用,而且调用的次数有上亿次,也没什么问题。之前倒是也卡住了一次,不过有个大神把问题找到了,因为Python版本低于2.7.6,Python对close_fds的一些实现不太好导致的,没有把管道释放掉,一直卡住。设置close_fds=True。不过这个并没有解决我的问题。

解决了我的问题

当时想着既然卡住了,那我就看看是输出了什么才卡住的,结果现有的代码无法支持我的想法,就换了代码,没想到就不卡住了。

def run_it(cmd):
    # _PIPE = subprocess.PIPE
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True,
                         stderr=subprocess.PIPE) #, close_fds=True)

    log.debug('running:%s' % cmd)
    out, err = p.communicate()
    log.debg(out)
    if p.returncode != 0:
        log.critical("Non zero exit code:%s executing: %s" % (p.returncode, cmd))
    return p.stdout

看看Python文档信息

Warning

Use communicate() rather than .stdin.write, .stdout.read or .stderr.read to avoid deadlocks due to any of the other OS pipe buffers filling up and blocking the child process.

Popen.wait()
    Wait for child process to terminate. Set and return returncode attribute.

    Warning This will deadlock when using stdout=PIPE and/or stderr=PIPE and the child process generates enough output to a pipe such that it blocks waiting for the OS pipe buffer to accept more data. Use communicate() to avoid that.
Popen.communicate(input=None)
    Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child.

    communicate() returns a tuple (stdoutdata, stderrdata).

    Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too.

    Note The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

之前没注意,再细看一下文档,感觉豁然开朗。

Linux管道限制,为什么会阻塞呢?

下面来看看Can someone explain pipe buffer deadlock?的回答。

子进程产生一些数据,他们会被buffer起来,当buffer满了,会写到子进程的标准输出和标准错误输出,这些东西通过管道发送给父进程。当管道满了之后,子进程就停止写入,于是就卡住了。

及时取走管道的输出也没有问题

# 及时从管道中取走数据
def run_it(self, cmd):
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True,
                         stderr=subprocess.PIPE, close_fds=True)
    log.debug('running:%s' % cmd)
    for line in iter(p.stdout.readline, b''):
        print line,          # print to stdout immediately
    p.stdout.close()
    p.wait()
    if p.returncode != 0:
        log.critical("Non zero exit code:%s executing: %s" % (p.returncode, cmd))
    return p.stdout

看了Python的communicate()内部就是将stdout/stderr读取出来到一个list变量中的,最后函数结束时返回。

测试Linux管道阻塞问题

看到别人的例子,一直在想怎么测试输出64K的数据,发现dd这个思路很棒,是见过最优雅的例子了,精确控制输出的长度,其他都是从某些地方搞来大文件导入进来。

#!/usr/bin/env python
# coding: utf-8
# yc@2013/04/28

import subprocess

def test(size):
    print 'start'

    cmd = 'dd if=/dev/urandom bs=1 count=%d 2>/dev/null' % size
    p = subprocess.Popen(args=cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
    #p.communicate()
    p.wait()  # 这里超出管道限制,将会卡住子进程

    print 'end'

# 64KB
test(64 * 1024)

# 64KB + 1B
test(64 * 1024 + 1)

# output :
start
end
start   #  然后就阻塞了。

首先测试输出为 64KB 大小的情况。使用 dd 产生了正好 64KB 的标准输出,由 subprocess.Popen 调用,然后使用 wait() 等待 dd 调用结束。可以看到正确的 start 和 end 输出;然后测试比 64KB 多的情况,这种情况下只输出了 start,也就是说程序执行卡在了 p.wait() 上,程序死锁。

总结

那死锁问题如何避免呢?官方文档里推荐使用 Popen.communicate()。这个方法会把输出放在内存,而不是管道里,所以这时候上限就和内存大小有关了,一般不会有问题。而且如果要获得程序返回值,可以在调用 Popen.communicate() 之后取 Popen.returncode 的值。

但真的如果超过内存了,那么要考虑比如文件 stdout=open("process.out", "w") 的方式来解决了,不能使用管道了。

另外说一下。管道的要用清楚,不要随意的乱世用管道。比如没有input的时候,那么stdin就不要用管道了。

还有不要把简单的事情复杂化。比如echo 1 > /sys/linux/xxx修改文件,这么简单的功能就不要用Linux的shell调用了,使用Python自带的 open('file', 'w').write('1') 。尽量保持Python范

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
subprocess.Popen在读取子进程的标准输出时,如果子进程没有输出,使用stdout.read()会导致程序阻塞。 为了避免阻塞,可以采用两种方法。一种是在读取输出之前,将文件描述符设置为非阻塞模式,例如使用fcntl库的fcntl函数进行设置。具体实现可以参考下面的代码示例: ``` import select import os import subprocess import time import fcntl args = ['python','./fetch_file2.py',ip,path] proc = subprocess.Popen(args, stdout=subprocess.PIPE,stderr=subprocess.PIPE,close_fds=True) def non_block_read(output): # 避免阻塞 fd = output.fileno() fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) try: return output.read() except: return "" while proc.poll() is None: #fetch中rsync结束。但是fetch没有结束(怀疑输出过大) 导致这里一直是None pass print proc.poll() # 杀死fetch进程 返回-9 print proc.stderr.read() #阻塞#方法1: #non_block_read(proc.stderr) #防止阻塞#方法2: select_rfds = [ proc.stdout, proc.stderr] (rfds, wfds, efds) = select.select(select_rfds, [],[]) if proc.stderr in rfds: #不存在。若select_rfds=[stderr],则阻塞在select上 len = proc.stderr.read(10) if len == 0: print "empty" else: print "proc.stderr" if proc.stdout in rfds: print "proc.stdout" ``` 通过设置文件描述符为非阻塞模式,可以避免阻塞问题,并且使用select.select来检查子进程的标准输出是否可读。 这样即可解决subprocess.Popen阻塞问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [对Python subprocess.Popen子进程管道阻塞详解](https://download.csdn.net/download/weixin_38661128/12866393)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [python subprocess.Popen read阻塞问题解决](https://blog.csdn.net/weixin_41582874/article/details/127939185)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [python subprocess阻塞](https://blog.csdn.net/weixin_30855761/article/details/99389411)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值