将子进程的stdout/stderr重定向到文件
我有一个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
,就像这里描述的那样,但也没有成功。
我用lsof
和strace
做了一些调试,以下是我找到的一些信息:
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 个回答
其实,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
一般来说,进程不能一行一行地写入文件,除非这个进程定期刷新数据。不过,你可以让调用的进程看起来像一个终端。遵循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()
我最后采用的解决办法,虽然没有完全解决问题,但在目前来看是最能接受的折中方案,就是在创建子进程的时候设置一下 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脚本的情况。