1. 程式人生 > >Subprocess Popen管道阻塞問題分析解決

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管道限制,為什麼會阻塞呢?

子程序產生一些資料,他們會被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
# [email protected]/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範