在C或Python中绕过popen的子进程输出缓冲

13 投票
3 回答
12302 浏览
提问于 2025-04-15 14:15

我有一个关于popen(以及所有相关函数)的一般性问题,这个问题适用于所有操作系统。当我写一个python脚本或一些C代码,并从控制台(无论是Windows还是Linux)运行生成的可执行文件时,我可以立即看到进程的输出。然而,如果我以一个分叉的进程运行同样的可执行文件,并将它的标准输出重定向到一个管道中,输出会在某个地方被缓冲,通常会缓冲到4096字节,然后再写入管道,这样父进程才能读取到。

下面的python脚本会以1024字节的块生成输出

import os, sys, time

if __name__ == "__main__":
     dye = '@'*1024
     for i in range (0,8):
        print dye
        time.sleep(1)

下面的python脚本会执行之前的脚本,并在输出到达管道时,逐字节读取输出

import os, sys, subprocess, time, thread

if __name__ == "__main__":
    execArgs = ["c:\\python25\\python.exe", "C:\\Scripts\\PythonScratch\\byte_stream.py"]

    p = subprocess.Popen(execArgs, bufsize=0, stdout=subprocess.PIPE)
    while p.returncode == None:
        data = p.stdout.read(1)
        sys.stdout.write(data)
        p.poll()

请根据你的操作系统调整路径。当以这种配置运行时,输出不会以1024字节的块出现,而是以4096字节的块出现,尽管popen命令的缓冲区大小设置为0(其实默认就是这样)。有没有人能告诉我如何改变这种行为?有没有办法强制操作系统以与从控制台运行时相同的方式处理分叉进程的输出?也就是说,直接传输数据而不进行缓冲?

3 个回答

0

在C/C++中,当你使用popen读取进程时,可以调用setvbuf这个函数:

#include <stdio.h>
...
int main(){
  setvbuf(stdout,NULL,_IONBF,0);
  ...
}

这个函数会把标准输出设置为不进行缓冲,这样你的输出就能正常工作。通常我会在程序的main()函数一开始就做这个设置。

我还没找到从主进程读取管道时能做到这一点的方法。可能有某个fcntl或stty的函数可以让子进程误以为它是在一个终端里。如果有人知道答案,我很想知道。

1

没错,这个说法适用于Windows和Linux系统(可能还有其他系统),涉及到popen()fopen()这两个函数。如果你想在输出缓冲区达到4096字节之前就把内容发送出去,可以使用fflush()(在C语言中)或者sys.stdout.flush()(在Python中)。

17

一般来说,标准的C运行时库(几乎每个系统上的每个程序都在使用它)会检测标准输出(stdout)是不是一个终端。如果不是,它就会把输出内容先存起来,这样可以提高效率,相比于直接输出来说,这种方式能节省很多时间。

如果你能控制正在写入的程序,你可以像其他答案提到的那样,持续刷新标准输出,或者(如果可行的话)更优雅地让标准输出不进行缓存,比如通过在命令行中加上 -u 这个参数来运行Python:

-u     : unbuffered binary stdout and stderr (also PYTHONUNBUFFERED=x)
         see man page for details on internal buffering relating to '-u'

(手册页中还提到了标准输入(stdin)和二进制模式的问题)。

如果你不能或者不想修改正在写入的程序,那么在只读的程序上加 -u 之类的参数可能没什么用(最重要的缓存是写入者的标准输出上的,而不是读取者的标准输入上的)。另一种方法是通过 pty 这个标准库模块,或者更高级的第三方模块 pexpect(在Windows上可以用它的移植版 wexpect),来欺骗写入者,让它以为自己是在写入一个终端(尽管实际上它是在写入另一个程序!)。

撰写回答