在Python中我们可以通过
os.system
来以控制台的形式运行程序,但当涉及到需要进行进程间通信时,就需要用到subprocess模块。本文原来是和
multiprocessing
作为一个整体来介绍的,后来进行了拆分,但内容仍然会有所重叠,并且会涉及Python的线程和进程相关机制。
Subprocess文档
指出这是我们唯一在Windows下需要指定
shell = True
来
运行一个程序
的情况,subprocess根据
COMSPEC
环境参数来运行shell。但在下面的讨论中我们看到
shell
的设置也影响了输入输出流重定向的细节。
Subprocess文档
bufsize
等同于
open
函数的
buffering
参数。设为0表示无缓存。设为1表示行缓存。大于1表示使用这个size的缓冲区。负数表示系统默认。
executable
有一些晦涩而不常使用的作用,比较有用的是
shell = True
时会用它来指定用的shell。
preexec_fn
(POSIX)
这个在下面的
强制终止进程
的讨论中会用到,其作用是在子程序执行前在
子程序上下文
中需要执行的语句,实际上是一个Hook。
close_fds
(POSIX)
对于Linux,子进程在执行前会先关闭除标准输入输出外的所有fd。我们使用以下的代码来测试这个特性
1 2 3 4 5 6 7 8 9 10
import os, sys, time, signalimport subprocessif __name__ == '__main__' : fd = os.open("test.txt" , os.O_RDWR | os.O_CREAT) os.write(fd, "line1 \n" ) proc = subprocess.Popen(["python" , "ch.py" ]) if __name__ == '__main__' : os.write(3 , "line2 \n" )
运行发现test.txt中有两行的输出。特别地,这个特性是由Linux保证的,查看subprocess源码,发现它在内部采用了先fork再exec的策略。而
exec也是保持file descriptor
的。
1 2 3 4 5 6 7
self.pid = _posixsubprocess.fork_exec( args, executable_list, close_fds, sorted(fds_to_keep), cwd, env_list, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, errpipe_read, errpipe_write, restore_signals, start_new_session, preexec_fn)
对于Windows事情就没有这么美妙了,
close_fds = True
意味着子进程不会继承任何从父进程过来的句柄,也就是说我们不能重定向标准输入输出了,
一个可行的解决方案
。
cwd
这个设置子线程的环境目录
env
这个设置子线程的环境变量
universal_newlines
这个用来统一所有的换行符模式为
\n
startupinfo
(Windows)
Windows的
CreateProcess
的对应参数
这两个有一些区别
。此外
communicate
会默认调用
stdin.close()
,这相当于向对方发送一个EOF。所以当需要多次向子程序写数据时,并且子程序侦测来自主程序的EOF作为结束提示时,应当使用
stdin.write
。
在写
ATP
时我还遇到程序子程序无法获得
stdin.write()
写入的数据的情况,这是需要设置
shell=True
。
在写Nuft的时候,有一次发现
subprocess.Popen("./bin/node -f./settings.txt -i0", subprocess.PIPE, open("n0.out", "w"), open("n0.err", "w")
这样的是没有输出的,后来我索性在控制台里面执行了
> n1.out
这样的操作,发现在Ctrl+C中断之后也是没有重定向的内容的。所以这个并不是subprocess的问题。根据
SoF
,这实际上是Linux的lazy write问题,我们需要在进程结束的时候手动
fflush
一下。我们可以注册一个SIGTERM和SIGINT等事件的钩子,这样就可以发送SIGTERM来终止进程了。注意我们可能在SIGTERM之后还要在收尾SIGKILL一下,如下所示
1 2 3 4 5 6
if proc.poll() == None : os.killpg(os.getpgid(proc.pid), signal.SIGTERM) time.sleep(0.2 ) os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
Python官网的Bug
,我们在主线程中等待子线程
join
,这个阻塞的过程不能被SIGINT(2)所唤醒,而SIGINT也不能执行其默认的退出处理,稍后尝试了
SIGTERM
等信号也同样不能响应,唯有SIGSTOP和SIGKILL可用。这是因为至少在POSIX系统中
Lock.acquire()
(
PyThread_acquire_lock()
)中无论是信号量还是CV的实现都会忽略信号。这个问题可以通过设置
.join()
的
timeout
参数来解决。注意如果
daemon
为
False
则
timeout
参数无效,这里的
daemon
表示守护线程。守护线程通常是一些不是那么重要的线程,Python会在所有
非守护
线程退出后结束。
我们将在
这篇文章
中专门探讨这个机制。