将子进程的stdout/stderr重定向到文件

4 投票
3 回答
2488 浏览
提问于 2025-04-18 12:52

我有一个Python脚本(popen.py),它会运行另一个Python脚本(counter.py),并把输出重定向到/tmp/counter.log。我使用的代码如下:

/tmp/counter.py

#!/usr/bin/env python2
import time

i = 0
while True:
    print i
    i +=1
    time.sleep(1)

/tmp/popen.py

#!/usr/bin/env python2
import subprocess

f = open("/tmp/counter.log", "a+")
p = subprocess.Popen("/tmp/counter.py", stdout=f, stderr=f, bufsize=1)

但是,当我运行popen.py时,子进程虽然创建了并且一直在运行,但在输出达到4096字节之前,/tmp/counter.log里什么都不会写入,等到达到这个大小后,才会写入文件。

有没有办法让我这个子进程逐行写入日志文件,而不需要修改counter.py脚本本身呢?

我不想修改counter.py的原因是,子进程可能并不总是运行一个Python脚本。我尝试过运行一个小的可执行文件(用C语言写的),结果也出现了同样的问题。

我试过为文件写一个自我刷新(自我清空)的包装器,并把它用作stdout,就像这里描述的那样,但也没有成功。

我用lsofstrace做了一些调试,以下是我找到的一些信息:

lsof(文件描述符)

手动运行/tmp/counter.py

COMMAND PID   USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
python2 629 daniel    0u   CHR  136,0      0t0      3 /dev/pts/0
python2 629 daniel    1u   CHR  136,0      0t0      3 /dev/pts/0
python2 629 daniel    2u   CHR  136,0      0t0      3 /dev/pts/0

通过/tmp/popen.py运行/tmp/counter.py

COMMAND PID   USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
python2 638 daniel    0u   CHR  136,0      0t0      3 /dev/pts/0
python2 638 daniel    1u   REG  202,0        0    768 /tmp/counter.log
python2 638 daniel    2u   REG  202,0        0    768 /tmp/counter.log

strace(在循环中的系统调用)

手动运行/tmp/counter.py

select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
write(1, "11\n", 3)                     = 3
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
write(1, "12\n", 3)                     = 3
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
write(1, "13\n", 3)                     = 3
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
write(1, "14\n", 3)                     = 3
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
write(1, "15\n", 3)                     = 3

通过/tmp/popen.py运行/tmp/counter.py

select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
...
write(1, "11\n12\n13\n14\n15\n16\n17\n18\n"..., 4096) = 4096

3 个回答

-1

其实,subprocess.Popen不仅可以用来执行Python脚本,还可以用来运行其他类型的可执行文件。下面这段代码可以用来创建用户的定时任务(cron)计划的一个副本:

import subprocess
import shlex

def getTempCrontabFile(argTmpFile='/tmp/tmpFile'):
    # Create a file in r/w mode that will be the target for
    # the crontab utility redirection.
    try:
        tmpFile = open(argTmpFile, 'a+')
    except IOError as customErr:
        print 'Failed to open or create temporary crontab file.'
        print customErr
        return customErr
    # Define the command line to list the cron schedule.
    cmdLine = 'crontab -l'
    # Format the command line into an array of arguments. This is
    # useful for proper formatting of spaces and quoted arguments
    # especially when commands get complicated.
    args = shlex.split(cmdLine)
    # Make the call to Popen using the file we created for stdout.
    result = subprocess.Popen(args, stdout=tmpFile)
    return result
0

一般来说,进程不能一行一行地写入文件,除非这个进程定期刷新数据。不过,你可以让调用的进程看起来像一个终端。遵循CLIB规则的进程会切换到行模式,这样就能满足你的需求。在这个例子中,我设置了一个伪终端,并写入并刷新日志文件。

#!/usr/bin/env python2

import os
import subprocess
import pty

master,slave = pty.openpty()
f = open("/tmp/counter.log", "a+")
p = subprocess.Popen(["python", "counter.py"], stdout=slave, stderr=slave, close_fds=True)
os.close(slave)
reader = os.fdopen(master)
while True:
    data = reader.readline()
    if not data:
        break
    f.write(data)
    f.flush()
    print data.strip()
print 'done'
reader.close()
p.wait()
1

我最后采用的解决办法,虽然没有完全解决问题,但在目前来看是最能接受的折中方案,就是在创建子进程的时候设置一下 PYTHONUNBUFFERED 这个环境变量:

#!/usr/bin/env python2
import subprocess

f = open("/tmp/counter.log", "a+")
p = subprocess.Popen("/tmp/counter.py", stdout=f, stderr=f, env={
    "PYTHONUNBUFFERED": "Yes please"
})

这个方法在额外代码和额外进程方面的开销是最低的,但只适用于子进程是一个Python脚本的情况。

撰写回答