【问题标题】:Redirecting stdout/stderr of a child process to a file将子进程的 stdout/stderr 重定向到文件
【发布时间】:2014-07-10 21:28:00
【问题描述】:

我有一个 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,如here 所述,但这也不起作用。

我已经对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

【问题讨论】:

  • 您似乎没有使用您的 Popen 实例的 communicate 方法。还有,在counter.py,直接写sys.stdout然后flush怎么样?
  • @LukasGraf 这确实有效。无论如何我可以纯粹在代码中实现这一点吗?我不想在Popen 的调用行中手动指定它或通过编辑counter.py
  • @Midnighter 我不需要使用communicate 因为我不想与子进程通信,在我的问题中我明确提到我希望能够做到这一点更改counter.py
  • @DanielGibbs 您不能在另一个进程中更改文件的缓冲区设置。在不更改 counter.py 的情况下,您可以使用 Python 解释器和 -u(无缓冲)选项启动该脚本。编辑:在启动 counter.py 之前交替设置 PYTHONUNBUFFERED 环境变量。
  • Popen() constructor 的文档说将 bufsize 设置为 0 表示无缓冲,我认为这就是您想要的。

标签: python stdout stderr child-process


【解决方案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 脚本时才有效。

【讨论】:

    【解决方案2】:

    一般来说,除非进程定期刷新,否则无法让进程逐行写入文件。但是你可以让调用进程看起来像一个终端。遵循 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 - 是的。如果这是一个问题,您可能会喜欢运行许多子进程的某种保姆程序。但在简单的情况下,您将为每个孩子配备一名保姆。而且它可能会变得更复杂 - 阅读 daemon 模块,了解让程序在后台运行的最佳方式。
    【解决方案3】:

    实际上,subprocess.Popen 可以用于 Python 脚本以外的可执行文件。这是一个创建用户 cron 计划副本的 sn-p:

    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
    

    【讨论】:

    • 是的,我知道。这没有回答我的问题。
    猜你喜欢
    • 2012-07-14
    • 2014-07-22
    • 1970-01-01
    • 2015-08-18
    • 1970-01-01
    • 2019-04-12
    • 2011-12-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多