Python
的subprocess
模块,用来创建和管理子进程(不是线程),并能够与创建的子进程的stdin
,stdout
,stderr
连接通信,获取子进程执行结束后的返回码,在执行超时或执行错误时得到异常。
从Python3.5
版本开始,subprocess
模块内部又进行了一次整合 ,最后就剩下官方推荐的两个接口函数,分别是:
subprocess.run()
subprocess.Popen()
考虑到这个模块对外接口的函数和对象名称都比较特别,本文就这样来引入:
>>> from subprocess import *
>>> dir()
['CalledProcessError', 'CompletedProcess', 'DEVNULL', 'PIPE', 'Popen', 'STDOUT', 'SubprocessError', 'TimeoutExpired', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'call', 'check_call', 'check_output', 'getoutput', 'getstatusoutput', 'run']
call
,check_call
,check_output
,getoutput
,getstatusoutput
这些函数,都被run
函数代替了,它们在存在只是为了保持向下兼容。
从Python3.5
开始,出现了run
函数,用来代替之前版本的一些函数接口。run
函数的作用是:执行args
参数所表示的命令,等待命令执行完毕,返回一个CompletedProcess
对象。
注意:run
函数是同步函数,要等待
run()
函数的接口参数:
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None,
capture_output=False, shell=False, cwd=None, timeout=None,
check=False, encoding=None, errors=None, text=None, env=None,
universal_newlines=None)
args参数,就是要通过创建进程而执行的命令及参数,run
函数通过args
来创建一个进程并执行。
shell参数,表示是否通过shell
来执行命令(Linux
下默认为/bin/sh
),默认是False
,这时args
只能是一个不带参数的命令字符串,或者是命令和参数组成的一个list
,如果shell=True
,args
就可以是一个我们常见的命令字符串。
>>> run('ls')
>>> run(['ls','-lh'])
>>> run('ls -lh', shell=True)
注意run
函数返回的CompletedProcess
对象,里面包含了args
,以及命令执行的返回码。下面的代码示例,说明了访问CompletedProcess
对象的方式:
>>> proc = run('ls')
Desktop Downloads Music Public test
Documents examples.desktop Pictures Templates Videos
>>> proc.args
'ls'
>>> proc.returncode
0
input
参数,命令的具体输入内容,默认None
,表示没有输入。input
与stdin
不能同时使用。先看一个有input
参数的例子:
>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf')
adfs
fsfsf
>>> proc
CompletedProcess(args='grep fs', returncode=0)
# input默认是一个bytes流。
>>> f = open('tt.t','r')
>>> proc = run('cat -n', shell=True, stdin=f)
1 12345
2 abcde
3 xyz..
>>> f.close()
None
,如上面的代码示例,输出就直接打印出来了;>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf',stdout=PIPE)
>>> proc
CompletedProcess(args='grep fs', returncode=0, stdout=b'adfs\nfsfsf\n')
>>> proc.stdout
b'adfs\nfsfsf\n'
stdout=PIPE
,表示将stdout
重定向到管道,用了这个参数,grep fs
命令的结果,就不会直接打印出来,而是存入了proc.stdout
这个管道内。
error
输出途径;>>> proc = run('ls fs',shell=True,stdout=PIPE,stderr=PIPE)
>>> proc.stdout
b''
>>> proc.stderr
b"ls: cannot access 'fs': No such file or directory\n"
看一个stdout
与input
配合起来使用的例子,有点像我们在Linux shell
输入的有管道的命令行:
>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf',stdout=PIPE)
>>> run('cat -n',shell=True, input=proc.stdout)
1 adfs
2 fsfsf
CompletedProcess(args='cat -n', returncode=0)
把stderr
重定向到stdout
:
>>> proc = run('ls kk', shell=True, stdout=PIPE, stderr=STDOUT)
>>> proc.stdout
b"ls: cannot access 'kk': No such file or directory\n"
stdout
和stderr
。capture_output=True
的效果与设置stdout=PIPE, stderr=PIPE一样。设置了capture_output=True,就不能再设置stdout和stderr:>>> proc = run('ls kk', shell=True, capture_output=True)
>>> proc
CompletedProcess(args='ls kk', returncode=2, stdout=b'', stderr=b"ls: cannot access 'kk': No such file or directory\n")
>>> proc.stdout
b''
>>> proc.stderr
b"ls: cannot access 'kk': No such file or directory\n"
>>> proc = run('ls -lh', shell=True, cwd='/usr/local')
total 36K
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 bin
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 etc
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 games
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 include
drwxr-xr-x 3 root root 4.0K Jun 28 21:54 lib
lrwxrwxrwx 1 root root 9 Jun 28 21:32 man -> share/man
drwxr-xr-x 6 root root 4.0K Jun 28 23:34 python-3.7.3
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 sbin
drwxr-xr-x 6 root root 4.0K Feb 9 16:15 share
drwxr-xr-x 2 root root 4.0K Feb 9 16:12 src
这两个参数的作用是一样的,universal_newlines
这个参数的存在也是为了向下兼容(Python3.7
开始有text
参数,3.5
和3.6
都是universal_newlines
参数),因此我们使用text
就好了。text
参数的作用是,将stdin
,stdout
,stderr
修改为string
模式。上面的示例代码,都是bytes
流:
>>> run('grep fs', shell=True, input=b'asdfs\nfdfs', capture_output=True)
CompletedProcess(args='grep fs', returncode=0, stdout=b'asdfs\nfdfs\n', stderr=b'')
>>> run('grep fs', shell=True, input='asdfs\nfdfs', capture_output=True, text=True)
CompletedProcess(args='grep fs', returncode=0, stdout='asdfs\nfdfs\n', stderr='')
subprocess.TimeoutExpired
异常会抛出。timeout
参数的单位是秒。>>> try:
... run('python3', shell=True, input=b'import time;time.sleep(30)', timeout=1)
... except TimeoutExpired:
... print('timeout happened...')
...
timeout happened...
以上代码,就是sleep 30秒,run函数设置timeout为1秒,触发subprocess.TimeoutExpired后,打印一点信息出来。
check=True
,在子进程的返回不为0
的时候,抛出subprocess.CalledProcessError
异常。这时,run
函数返回的CompletedProcess
对象的returncode
不可用。>>> try:
... proc = run('ls kk', shell=True, check=True, stderr=PIPE)
... except CalledProcessError:
... print(proc.returncode)
...
0
上面这段代码,走到了except
里面,因为kk
目录不存在,但是打印出来的returncode
却是0
,run
函数没有成功返回,而是抛出异常,因此返回值不可用。
run
函数的底层,就是Popen
函数。run
函数是同步的,要等待子进程实行结束,或者超时。Popen
创建子进程后,采用异步的方式,不会等待,要通过poll
函数来判断子进程是否执行完毕。
Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None,
universal_newlines=None, startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False, pass_fds=(), *,
encoding=None, errors=None, text=None)
参数args
,stdin
,stdout
,stderr
,shell
,cwd
,universal_newlines
,text
与run
函数的含义和用法都是一样的。
Popen
函数的基本用法:>>> proc = Popen('ls -hl', shell=True, stdout=PIPE, stderr=STDOUT)
>>> out, _ = proc.communicate()
>>> print(out.decode())
total 37M
-rw-r--r-- 1 xinlin xinlin 535 Jun 29 06:03 apache_log_reader.py
-rw-r--r-- 1 xinlin xinlin 3.2M Jun 30 02:55 py.maixj.sql
-rw-r--r-- 1 xinlin xinlin 3.2M Jun 29 19:20 py.online.sql
drwxr-xr-x 19 xinlin xinlin 4.0K Jun 28 23:24 Python-3.7.3
-rw-r--r-- 1 xinlin xinlin 22M Mar 25 13:59 Python-3.7.3.tgz
-rw-r--r-- 1 xinlin xinlin 27 Jul 5 01:05 sleep.py
-rw-r--r-- 1 xinlin xinlin 18 Jul 5 00:10 tt.t
-rw-r--r-- 1 xinlin xinlin 800 Jun 29 03:26 walktree.py
-rw-r--r-- 1 xinlin xinlin 8.2M Jun 29 05:47 www.access_log_2019_06_28
>>> proc.returncode
0
>>> proc.pid
2985
Popen
函数以异步的方式创建一个子进程,返回一个Popen
对象。我们通过communicate
函数来获取stdout
和stderr
。communicate
函数返回一个tuple
,以上示例是将stderr=STDOUT
,因此使用 _ 来表示为空的stderr
。
因为subprocess
模块是用调用shell
命令的方式创建进程,我们可以直接用这一行shell命令启动后台进程:
import subprocess
subprocess.Popen('python3.8 start.py', shell=True)
Popen
对象的communicate
函数有两个参数,input
和timeout
,分别用来设置给子进程的输入和超时时间。有timeout
参数,表示communicate
函数会等待子进程执行结束,或者超时。
>>> proc = Popen('grep fs', shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
>>> out, err = proc.communicate(b'adfs\nfsmnjkl')
>>> out
b'adfs\nfsmnjkl\n'
>>> err
b''
1. Popen
以异步的方式创建子进程,创建时可以设定stdin
,stdout
和stderr
全部指向PIPE
,此时子进程的输入输出全部都在管道中,就像我们再shell
命令行直接使用管道(|)
一样!
2. 在使用管道(|)
连接多个程序的时候,前一个程序的输出成为了后一个程序的输入,此时如果假设后一个程序时通过subprocess
的Popen
创建的,那么此时此子进程的stdin
,就是前一个程序的输出,而它的stdout
和stderr
,通过communicate
函数,可以直接获得。
通过子进程的communicate函数,我们可以像使用shell的管道一样,直接连接多个程序的输入和输出;但是,这种输入和输出,也跟shell管道一样,是一次性的;即如果某个程序有运行时会连续多次获取输入,communicate就无能为力(此时就要使用pexpect)。
再来一个有timeout
的例子:
>>> proc = Popen('python3', shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
>>> try:
... out,err=proc.communicate(b'import time;time.sleep(30)', 1)
... except TimeoutExpired:
... print('time out...')
...
time out...
Popen
对象有一个 wait
成员函数,也可以设置一个timeout
来等待子进程的结束:
>>> try:
... proc=Popen('python3 -c "import time;time.sleep(30)"',shell=True,stdout=PIPE)
... returncode = proc.wait(15)
... except TimeoutExpired:
... print('after waiting 15 seconds, timeout finally...')
...
after waiting 15 seconds, timeout finally...
注意对returncode
的赋值,如果timeout
发生,returncode
就是not defined
。当然也可以通过proc.returncode
来获取。如果异常,proc.returncode
的值是None
。
如果想确定子进程执行的时长,可以采用poll
函数:
>>> def test_Popen():
... import time
... proc=Popen('python3 -c "import time;time.sleep(10)"',shell=True,stdout=PIPE)
... i = 0
... while True:
... returncode = proc.poll()
... if returncode is None:
... time.sleep(2)
... i += 2
... print('sleep',i,'seconds')
... continue
... else:
... print('sub process is terminated with returncode',returncode)
... break
...
>>> test_Popen()
sleep 2 seconds
sleep 4 seconds
sleep 6 seconds
sleep 8 seconds
sleep 10 seconds
sleep 12 seconds
sub process is terminated with returncode 0
Popen
对象还有下列几个成员函数:
Popen.send_signal(signal)
Popen.terminate()
Popen.kill()
大体思路:既然是实时获取subprocess
创建的子进程的输出,我们就不能使用run
,而要使用Popen
创建异步子进程,然后通过poll
函数来查看异步子进程的执行状态,如果执行没有结束,我们就直接去读取子进程的stdout
,然后在主进程中处理。
subproc.py
文件中:import time
while True:
print('subprocess print...', flush=True)
time.sleep(1)
注意:print函数一定要使用flush=True,否则子进程的输出都在缓存中,主进程也无法读取出来!
proc.py
文件中:from subprocess import *
proc = Popen('python subproc.py', shell=True, stdout=PIPE, stderr=PIPE)
while True:
rcode = proc.poll()
if rcode is None:
print('from subprocess: ', end='')
line = proc.stdout.readline().strip()
print(line.decode())
E:\py>python proc.py
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: Traceback (most recent call last):
File "proc.py", line 10, in
line = proc.stdout.readline().strip()
KeyboardInterrupt
用这个技术,可以在python
命令行程序上套一层GUI
的壳!在GUI
程序中,用subprocess
以子进程的方式启动命令行程序,实时获取子进程的输出并在GUI
中显示出来。
不过有个问题,原命令行程序的print
如果没有加flush=True
参数怎么办?可以使用python
命令行的-u
参数!
如果subproc.py的代码是这样的,即print没有flush=True:
import time
while True:
print('subprocess print...')
time.sleep(1)
proc.py的代码修改如下,使用 -u 参数:
from subprocess import *
proc = Popen('python -u subproc.py', shell=True, stdout=PIPE, stderr=PIPE)
while True:
rcode = proc.poll()
if rcode is None:
print('from subprocess: ', end='')
line = proc.stdout.readline().strip()
print(line.decode())
python标准库中有一个多进程模块,multiprocesing,它可以支持在代码创建多个进程协同运行的计算模型,此模块很多接口名和参数,都与多线程一致。
从代码上看多进程,依然是给进程指定一个函数作为入口,python底层自动启动一个独立进程从此入口开始执行。如果是GUI程序,多进程可以更好的实现多个tkinter的root窗口。
CPython
解释器中的GIL
(Global Intercepto Lock,全局解释器锁。GIL
并不是Python
的特性,它是在实现Python
解析器(CPython
)时所引入的一个概念),限制了多线程无法充分利用多CPU
资源,因此有了多进程的用武之地。如果IO
密集型的任务,多线程就可以了。如果是计算密集型的任务,有多个CPU
时,就要考虑多进程,以充分利用资源。
CPU
永远不可能实现并行,即一个CPU
不能同时运行多个程序,但是可以在随机分配的时间片内交替执行(并发),就好像一个人不能同时看两本书,但是却能够先看第一本书半分钟,再看第二本书半分钟,这样来回切换。 Guido van Rossum(吉多·范罗苏姆)创建Python
时就只考虑到单核CPU
,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁,于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。由于大量的程序开发者接收了这套机制,现在代码量越来越多,已经不容易通过C代码去解决这个问题。
每个线程在执行时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU,也就是说多线程并不是真正意义上的并行执行。
那么,我们改如何解决GIL锁的问题呢?
问题1: 什么时候会释放Gil锁?
问题2: 互斥锁和Gil锁的关系
Gil锁 : 保证同一时刻只有一个线程能使用到CPU。
互斥锁 : 多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱。
GIL与多线程:有了GIL的存在,Python有这两个特点:
也就是说Python中的多线程是假的多线程,Python解释器虽然可以开启多个线程,但同一时间只有一个线程能在解释器中执行,而做到这一点正是由于GIL锁的存在,它的存在使得CPU的资源同一时间只会给一个线程使用,而由于开启线程的开销小,所以多线程才能有一片用武之地,不然就真的是鸡肋了。
from multiprocessing import Process
import time
import random
def fun1(i):
time.sleep(random.randint(0,2))
print('multiprocess test %d' %i)
if __name__ == '__main__':
process_list = []
for i in range(5): #开启5个子进程执行fun1函数
p = Process(target=fun1,args=(i,)) #实例化进程对象
p.start()
process_list.append(p)
for p in process_list:
p.join()
print('Done!')
通过multiprocessing
模块的Process
创建进程对象,然后start
。join
用来阻塞等待进程执行结束。接口基本与多线程一致。
多进程的入口也就是一个普普通通的函数!但是它是以独立进程的方式运行。
以上代码执行效果:
D:\py>python mptest.py
multiprocess test 1
multiprocess test 4
multiprocess test 0
multiprocess test 2
multiprocess test 3
Done!
D:\py>python mptest.py
multiprocess test 0
multiprocess test 2
multiprocess test 3
multiprocess test 1
multiprocess test 4
Done!
from multiprocessing import Process
import time
import random
class MyProcess(Process): #继承Process类
def __init__(self,name):
super(MyProcess,self).__init__()
self.name = name
def run(self):
time.sleep(random.randint(0,2))
print('测试多进程 %s' % self.name)
if __name__ == '__main__':
process_list = []
for i in range(5): #开启5个子进程执行fun1函数
p = MyProcess(str(i)*8) #实例化进程对象
p.start()
process_list.append(p)
for p in process_list:
p.join()
print('Done!')
以上代码执行效果:
D:\py>python mptest.py
测试多进程 11111111
测试多进程 33333333
测试多进程 00000000
测试多进程 22222222
测试多进程 44444444
Done!
D:\py>python mptest.py
测试多进程 22222222
测试多进程 11111111
测试多进程 44444444
测试多进程 00000000
测试多进程 33333333
Done!
multiprocess.Queue
,是提供个python
多进程见通信使用的。python
有一个好的设计,即多线程和多进程的接口基本相同,现在这两个Queue
的使用接口也基本相同:
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, 'hello'])
def g(q):
q.put([48, None, 'hello gggg'])
def h(q):
print(q.get())
print(q.get())
if __name__ == '__main__':
q = Queue()
p1 = Process(target=f, args=(q,))
p2 = Process(target=g, args=(q,))
p3 = Process(target=h, args=(q,))
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
主进程开了3个子进程,两个做put,一个做get。运行结果:
[42, None, 'hello']
[48, None, 'hello gggg']
相对于multiprocess.Queue
,pipe
更快,但是不适合多个进程间的通信:
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello'])
for i in range(2):
print(conn.recv())
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print("parent_conn_recv: ", parent_conn.recv())
parent_conn.send('i am your father!!')
parent_conn.send('boy...')
p.join()
多进程通信的Pipe
管道构建的时候返回两个对象,这两个对象之间可以通过send
和recv
方法传递数据。
以上代码执行效果:
parent_conn_recv: [42, None, 'hello']
i am your father!!
boy...
如上示例,test.py
的执行,就是启动一个新的python
进程,然后自己就退出了。这个新的Python
进程,就成了后台进程,logout
后也依然存在。
os.system
底层调用的C
标准接口system
,它只能返回子进程的返回码,而无法与子进程进行复杂交互。我们就简单理解它可以直接在python
代码中执行shell
命令吧,子进程的输出会直接出现的父进程的stdout
内。
>>> a = os.system('ls -ahl')
total 56K
drwxr-xr-x 5 pi pi 4.0K Jul 3 14:22 .
drwxr-xr-x 5 pi pi 4.0K Jun 24 12:00 ..
-rw-r--r-- 1 pi pi 2.9K Jun 18 20:49 common.py
-rw-r--r-- 1 pi pi 164 Jun 18 20:49 config.ini
drwxr-xr-x 8 pi pi 4.0K Jul 3 10:01 .git
drwxr-xr-x 2 pi pi 4.0K Jul 3 10:00 __pycache__
-rw-r--r-- 1 pi pi 48 Jun 7 22:17 README.md
-rw-r--r-- 1 pi pi 603 Jun 18 20:49 start.py
-rw-r--r-- 1 pi pi 16K Jul 3 10:00 tcp_server.py
drwxr-xr-x 2 pi pi 4.0K Jul 2 10:48 test
-rw-r--r-- 1 pi pi 2.1K Jun 18 20:49 udp_hb.py
>>> a
0
关于子进程的创建需要明确两点: (1)
父进程的环境变量(environment variables)会默认传递到子进程中(工作目录PWD就是环境变量之一) (2)
使用os.system函数,子进程无法影响父进程中的环境变量。
有时我们在python
中调试代码的时候,还是挺方便的。比如上面的示例,查看一下某个路径的文件列表,或者在不离开当前python
环境的情况下,启动一个后台进程。