【问题标题】:Change Output Redirection of Running Process改变运行进程的输出重定向
【发布时间】:2013-11-22 22:30:23
【问题描述】:

我有一个 parent Python 脚本,它启动 child(启动 grandchildren),一段时间后,我终止了孩子,但孙子们继续抽到标准输出。杀死孩子后,我想抑制/重定向孙辈(及其所有后代)的标准输出和标准错误。

这是父级:

import time
import subprocess
proc = subprocess.Popen('./child.sh')
print("Dad: I have begotten a son!")
time.sleep(1)
proc.kill()
proc.wait()
print("Dad: My son hath died!")
time.sleep(2)
print("Dad: Why does my grandson still speak?")

这是子脚本我无法修改

#!/bin/bash
./grandchild.sh &
echo "Child: I had a son!"
for (( i = 0; i < 10; i++ )); do
    echo "Child: Hi Dad, meet your grandson!"
    sleep 1
done
exit 0

这是一个吵闹的孙子我无法修改

#!/bin/bash
for (( i = 0; i < 10; i++ )); do
    echo "Grandchild: Wahhh"
    sleep 1
done
exit 0

我在杀死孩子之前尝试过这样做:

import os
f = open(os.devnull,"w")
proc.stdout = proc.stderr = f

但它似乎不起作用。输出是:

> ./parent.py
Dad: I have begotten a son!
Child: I had a son!
Child: Hi Dad, meet your grandson!
Grandchild: Wahhh
Dad: My son hath died!
Grandchild: Wahhh
Grandchild: Wahhh
Dad: My grandson still speaketh!
Grandchild: Wahhh
Grandchild: Wahhh
Grandchild: Wahhh
Grandchild: Wahhh
Grandchild: Wahhh
Grandchild: Wahhh
Grandchild: Wahhh

【问题讨论】:

    标签: python subprocess io-redirection


    【解决方案1】:

    当您调用subprocess.Popen 时,您可以告诉它重定向stdout 和/或stderr。如果你不这样做,它会允许操作系统从 Python 进程的实际 STDOUT_FILENOSTDERR_FILENO(它们是固定常量,1 和 2)中复制,从而使它们不被重定向。

    这意味着,如果 Python 的 fd 1 和 2 将进入您的 tty 会话(例如,可能在像 /dev/pts/0 这样的底层设备上),那么孩子——在这种情况下,孙子也是如此——直接交谈到同一个会话(同一个/dev/pts/0)。您在 Python 进程本身中所做的任何事情都无法改变这一点:它们是独立的进程,可以独立、直接地访问会话。

    你可以做的是调用 ./child.sh 并重定向:

    proc = subprocess.Popen('./child.sh', stdout=subprocess.PIPE)
    

    快速旁注编辑:如果您想丢弃子及其孙子的 所有 输出,请打开 os.devnull(如您所做的那样,或使用 os.open() 获取原始整数文件描述符)并将 stdout 和 stderr 连接到底层文件描述符。如果您已将其作为 Python 流打开:

    f = open(os.devnull, "w")
    

    那么底层文件描述符是f.fileno():

    proc = subprocess.Popen('./child.sh', stdout=f.fileno(), stderr=f.fileno())
    

    在这种情况下,您无法从所涉及的任何进程中获得任何输出。


    现在子文件描述符 1 连接到管道实体,而不是直接连接到会话。 (由于上面没有stderr=,所以child中的fd 2还是直接连接到session的。)

    位于操作系统内部的管道实体只是从一端(管道的“写入端”)复制到另一端(“读取端”)。您的 Python 进程可以控制读取端。您必须在读取端调用 OS read 系统调用(通常不是直接调用,但请参见下文),以从中收集输出。

    一般来说,如果您停止从读取端读取,管道会“填满”,并且任何尝试在写入端执行操作系统级别 write 的进程都会被“阻止”,直到有人可以访问读取端(又是你)从中读取。

    如果您丢弃读取端,使管道无处转储其输出,则写入端开始返回 EPIPE 错误并发送 SIGPIPE 信号给任何尝试操作系统级别 write 调用的进程。当您调用操作系统级别的close 系统调用时会发生这种丢弃,假设您没有将描述符移交给其他进程。当您的进程退出时也会发生这种情况(再次在相同的假设下)。

    没有方便的方法可以将读取端连接到像/dev/null 这样的无限数据接收器,至少在大多数类 Unix 系统中(有一些特殊的时髦系统调用可以做到这一点) “管道”)。但是,如果您打算杀死孩子并愿意让其孙辈死于SIGPIPE 信号,您可以简单地关闭描述符(或退出),让筹码落到可能的地方。

    子孙可以通过将SIGPIPE设置为SIG_IGN或屏蔽SIGPIPE来保护自己免于死亡。信号掩码在exec 系统调用中继承,因此在某些情况下,您可以为子级屏蔽SIGPIPE(但有些子级会取消屏蔽信号)。

    如果关闭描述符不合适,您可以创建一个简单地读取和丢弃传入管道数据的新进程。如果您使用fork 系统调用,这很简单。或者,一些类 Unix 系统允许您通过 AF_UNIX 套接字将文件描述符传递给其他不相关的(父/子)进程,因此您可以拥有一个执行此操作的守护进程,可通过 AF_UNIX 套接字访问。 (这对代码来说很重要。)

    如果您希望子进程将其标准错误输出发送到 same 管道,以便您可以读取其标准输出和标准错误,只需将stderr=subprocess.STDOUT 添加到Popen() 调用。如果您希望子进程将其 stderr 输出发送到 单独 管道,请添加 stderr=subprocess.PIPE。但是,如果您执行后者,事情可能会变得有些棘手。

    如上所述,为防止儿童阻塞,您必须调用 OS read 调用。如果只有一根管道,这很容易:

    for line in proc.stdout:
        ...
    

    例如,或者:

    line = proc.stdout.readline()
    

    将一次读取一行管道(Python 中的模缓冲)。您可以阅读任意多行或尽可能少的行。

    但是,如果有 两个 管道,则必须读取“已满”的管道。 Python 的 subprocess 模块定义了 communicate() 函数来为您执行此操作:

    stdout, stderr = proc.communicate()
    

    这里的缺点是communicate() 读取到完成:它需要获得所有 输出,可以到达每个管道的写入端。这意味着它重复调用操作系统级别的read 操作,直到read 指示数据结束。仅当在某个时刻对相应管道的写端具有写访问权限的所有进程都关闭了该管道的该端时,才会发生这种情况。换句话说,它等待子和任何孙子close 连接到管道写入端的描述符。

    一般来说,只使用一个管道要简单得多,尽可能多地读取(但只能尽可能多地),然后简单地关闭管道:

    proc = subprocess.Popen('./child.sh', stdout=subprocess.PIPE)
    line1 = proc.stdout.readline()
    line2 = proc.stdout.readline()
    # that's all we care about
    proc.stdout.close()
    proc.kill()
    status = proc.wait()
    

    这是否足够取决于您的特定问题。

    【讨论】:

    • 谢谢!我能够用你的最终代码块解决问题,但我也非常感谢所有细节。
    • 确实,我不在乎孙子们死得多么优雅,只是他们不会弄乱输出流。这完美地工作。不太方便,因为您必须手动处理流,但我认为这可能需要。
    【解决方案2】:

    如果你不关心孙子;你可以把他们都杀了:

    #!/usr/bin/env python3
    import os
    import signal
    import subprocess
    import sys
    import time
    
    proc = subprocess.Popen('./child.sh', start_new_session=True)
    print("Dad: I have begotten a son!")
    time.sleep(1)
    print("Dad: kill'em all!")
    os.killpg(proc.pid, signal.SIGKILL)
    
    for msg in "dead... silence... crickets... chirping...".split():
        time.sleep(1)
        print(msg, end=' ', flush=True)
    

    您可以使用 preexec_fn=os.setsid 在旧 Python 版本上模拟 start_new_session=True。见Best way to kill all child processes

    你可以在杀戮前收集孩子的输出:

    #!/usr/bin/env python
    import collections
    import os
    import signal
    import threading
    from subprocess import Popen, PIPE, STDOUT
    
    def killall(proc):
        print "Dad: kill'em all!"
        os.killpg(proc.pid, signal.SIGKILL)
        proc.wait()
    
    proc = Popen('./child.sh', stdout=PIPE, stderr=STDOUT, preexec_fn=os.setsid)
    print("Dad: I have begotten a son!")
    
    # kill in a second
    hitman = threading.Timer(1, killall, [proc])
    hitman.start()
    
    # save last 200 lines of output
    q = collections.deque(proc.stdout, maxlen=200)
    hitman.cancel()
    proc.wait()
    
    # print collected output
    print '*'*60
    print ''.join(q).decode('ascii'),
    print '*'*60
    

    Stop reading process output in Python without hang?

    【讨论】:

    • 非常感谢。其实默默地杀死所有的孙子,比简单地让他们噤声还好。
    • “如果你不关心孙子,你可以杀了他们”幸运的是,我们在一个编程论坛......
    【解决方案3】:

    现在,您的子进程可以通过 STDOUT 和 STDERR 与您的终端进行通信。相反,您可以像这样从子流程中劫持这些数据:

    import subprocess
    cmd = ['./child.sh']
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    

    这会将您孩子的所有STDERR 输出重定向到正常的STDOUT 频道,然后通过PIPE 将您孩子的正常STDOUT 输出重定向到您的python 脚本。您现在可以使用line = process.stdout.readline()PIPE 中读取,它会获取单行输出。您可以使用 print(line) 将其打印回 STDOUT。

    一旦你杀死了你的儿子(喘气),停止你的子进程的所有输出。

    有关子流程的更多信息,请参阅我之前的一个与此类似的答案:python subprocess.call output is not interleaved

    【讨论】:

      猜你喜欢
      • 2014-07-05
      • 2010-11-22
      • 2023-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-06
      • 1970-01-01
      相关资源
      最近更新 更多