将子进程的标准输出/错误重定向到文件

4

我有一个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(在while循环期间的系统调用)

手动运行/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

1
@DanielGibbs,您无法更改另一个进程中文件的缓冲设置。不更改 "counter.py" 的情况下,您可以使用 Python 解释器和“-u”(非缓冲)选项启动该脚本。编辑:或者在启动“counter.py”之前设置“PYTHONUNBUFFERED”环境变量作为替代方案。 - BlackJack
Popen() 构造函数 的文档称将 bufsize 设置为 0 意味着无缓冲,我认为这正是你想要的。 - martineau
我已经尝试过了。就像我尝试将其设置为1以进行行缓冲一样,但都没有起作用。 - DanielGibbs
@BlackJack 但是如果另一个进程是二进制可执行文件呢?我遇到了同样的问题:正常运行时,它按预期输出到标准输出,但当我尝试重定向到文件时,它会缓冲。 - DanielGibbs
据我所知,你无法对此做任何事情。 - BlackJack
显示剩余3条评论
3个回答

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脚本的情况。

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()

有趣的,我会看一下那个终端的东西,但是你的例子并不完全符合我的要求,因为我希望在生成子进程后父进程能够终止。 - DanielGibbs
这个例子可以转变成一个中间执行器,父级调用这个脚本,然后再调用子脚本,父级退出,但这个脚本仍然存在。 - tdelaney
那么,对于每个子进程,我最终都会有两个? - DanielGibbs
@DanielGibbs - 是的。如果这是个问题,你可以使用某种保姆程序来运行多个子进程。但在简单情况下,每个孩子都需要一个保姆。而且它可能会变得更加复杂 - 阅读有关守护程序模块的信息,以了解让程序在后台运行的最佳方法。 - tdelaney

-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

是的,我知道。但这并没有回答我的问题。 - DanielGibbs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接