subprocess创建子进程,subprocess执行py脚本文件,subprocess使用pipenv管理的虚拟环境执行子进程
一、相关参数与方法
》》官方文档《《
Popen类:
subprocess模块中只定义 了一个类: Popen。可以使用Popen来创建进程,并与进程进行复杂的交互。它的构造函数如下: subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0) 参数args可以 是字符串或者序列类型(如:list,元组),用于指定进程的可执行文件及其参数。如果是序列类型,第一个元素通常是可执行文件的路 径。我们也可以显式的使用executeable参数来指定可执行文件的路径。在windows操作系统上,Popen通过调用 CreateProcess()来创建子进程,CreateProcess接收一个字符串参数,如果args是序列类型,系统将会通过 list2cmdline()函数将序列类型转换为字符串。 参数bufsize:指定缓冲。我到现在还不清楚这个参数的具体含义,望各个大牛指点。 参数executable用于指定可执行程序。一般情况下我们通过args参数来设置所要运行的程序。如果将参数shell设为 True,executable将指定程序使用的shell。在windows平台下,默认的shell由COMSPEC环境变量来指定。 参数stdin, stdout, stderr分别表示程序的标准输入、输出、错误句柄。他们可以是PIPE,文件描述符或文件对象,也可以设置为None,表示从父进程继承。 参数preexec_fn只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用。 参 数Close_sfs:在windows平台下,如果close_fds被设置为True,则新创建的子进程将不会继承父进程的输入、输出、错误管 道。我们不能将close_fds设置为True同时重定向子进程的标准输入、输出与错误(stdin, stdout, stderr)。 如果参数shell设为true,程序将通过shell来执行。 参数cwd用于设置子进程的当前目录。 参数env是字典类型,用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。 参数Universal_newlines:不同操作系统下,文本的换行符是不一样的。如:windows下用'\r\n'表示换,而Linux下用 '\n'。如果将此参数设置为True,Python统一把这些换行符当作'\n'来处理。 参数startupinfo与createionflags只在windows下用效,它们将被传递给底层的CreateProcess()函数,用 于设置子进程的一些属性,如:主窗口的外观,进程的优先级等等。 subprocess.PIPE 在创建Popen对象时,subprocess.PIPE可以初始化stdin, stdout或stderr参数。表示与子进程通信的标准流。 subprocess.STDOUT 创建Popen对象时,用于初始化stderr参数,表示将错误通过标准输出流输出。 Popen的方法: Popen.poll() 用于检查子进程是否已经结束。None:正在运行 0:进程正常结束 1:出现错误 Popen.wait() 等待子进程结束。设置并返回returncode属性。 Popen.communicate(input=None) 与子进程进行交互。向 stdin发送数据,可选参数input指定发送到子进程的参数
或从stdout和stderr中读取数据,如使用PIPE时可以用来读取数据,解决子进程管道数据太多的阻塞:
stdout,stderr = p.communicate()
Communicate()返回一个元组:(stdoutdata, stderrdata)
注意:如果希望通过进程的stdin向其发送数据,在创建Popen对象的时候,参数stdin必须被设置为PIPE。
同样,如 果希望从stdout和stderr获取数据,必须将stdout和stderr设置为PIPE。 Popen.send_signal(signal) 向子进程发送信号。 Popen.terminate() 停止(stop)子进程。在windows平台下,该方法将调用Windows API TerminateProcess()来结束子进程。 Popen.kill() 杀死子进程。 Popen.stdin 如果在创建Popen对象是,参数stdin被设置为PIPE,Popen.stdin将返回一个文件对象用于策子进程发送指令。否则返回None。 Popen.stdout 如果在创建Popen对象是,参数stdout被设置为PIPE,Popen.stdout将返回一个文件对象用于策子进程发送指令。否则返回 None。 Popen.stderr 如果在创建Popen对象是,参数stdout被设置为PIPE,Popen.stdout将返回一个文件对象用于策子进程发送指令。否则返回 None。 Popen.pid 获取子进程的进程ID。 Popen.returncode 获取进程的返回值。如果进程还没有结束,返回None。
在 Windows 上,args序列被转换为使用以下规则解析的字符串:
参数由空格分隔,空格或制表符。
被双引号包围的字符串被解释为单个参数,而不管其中包含的空格。带引号的字符串可以嵌入到参数中。
以反斜杠开头的双引号被解释为文字双引号。
反斜杠按字面解释,除非它们紧跟在双引号之前。
如果反斜杠紧跟在双引号之前,则每一对反斜杠都被解释为文字反斜杠。如果反斜杠的数量是奇数,则最后一个反斜杠会转义下一个双引号,如规则 3 中所述
二、执行py脚本文件 subprocess使用pipenv的虚拟环境创建子进程并向子进程传递参数
创建进程的模块有很多,Python手册中推荐使用subprocess来替换一些老的模块和函数,如:os.system、os.spawn*、os.popen*、popen2.*、commands.*
subprocess.Popen可以使用正式环境,也可以使用虚拟环境来创建子进程,并且可以向子进程传递参数
1.1 主进程文件:
# coding:utf-8 import subprocess import time import json # ++++++++++++++++++++++++++++方式一:正常python环境下使用++++++++++++++++++++++++++++++++++++++ # 带参数 # p = subprocess.Popen(['python', './test.py', '参数1', '参数2'],stdin = subprocess.PIPE,stdout = subprocess.PIPE,stderr = subprocess.PIPE) # 不带参数 # p = subprocess.Popen('python ./test.py',stdin = subprocess.PIPE,stdout = subprocess.PIPE,stderr = subprocess.PIPE) # ++++++++++++++++++++++++++++++方式二:pipenv管理的的虚拟环境下运行时,就必须要指明pipenv,不指明的话导入虚拟环境中的模块就会报错停止运行子进程+++++++++++++ # 不带参数 # p = subprocess.Popen('pipenv run python ./test.py',stdin = subprocess.PIPE,stdout = subprocess.PIPE,stderr = subprocess.PIPE) # 带参数 (列表中只能是字符串,其他类型比如字典数据需要用json处理成字符串) p = subprocess.Popen(['pipenv','run','python','./111.py','参数1',json.dumps({'1':1,'2':2}),],stdin = subprocess.PIPE,stdout = subprocess.PIPE,stderr = subprocess.PIPE) print([x.decode("utf8").strip() for x in p.stderr.readlines()]) # 打印被执行文件的报错 # 被执行文件中所有print打印的内容,这个会等待子进程即被执行文件执行完毕才会输出 stdout = p.stdout.readlines() result = [x.decode("utf8").strip() for x in stdout] print(result) # 如果不加p.stdout的输出的话,主进程直接就结束了,子进程自然也不会运行,我们可以这样来测试一下 # while True: # time.sleep(4) # print('让子进程再运行4秒')
1.2 子进程test.py文件:
import '可以放一个虚拟环境中安装的模块来测试' import sys print(9999) if __name__ == "__main__": a = sys.argv[1] b = sys.argv[2] print('argv',sys.argv) # ['./111.py', '参数1', '参数2'] print(b,type(b)) # {"1": 1, "2": 2} <class \'str\'>' print(json.loads(b),type(json.loads(b))) # {'1': 1, '2': 2} <class 'dict'>"
三、subprocess创建子进程 执行exe可执行程序
# Python手册中推荐使用subprocess来替换一些老的模块和函数,如:os.system、os.spawn*、os.popen*、popen2.*、commands.* import subprocess import psutil import time def get_pid(pname): pid_lis = [] for proc in psutil.process_iter(): #print(“pid-%d,name:%s” % (proc.pid,proc.name())) if proc.name() == pname: pid_lis.append(proc.pid) # return proc.pid return pid_lis print(get_pid('WeChat.exe')) # subprocess.run(['taskkill', '/f', '/t', '/im', 'WeChat.exe']) process = subprocess.Popen('C:\\Program Files (x86)\\Tencent\\WeChat\\WeChat.exe') time.sleep(6) print(get_pid('WeChat.exe')) print('subprocess执行命令启动程序',process.pid)
四、subprocess.Popen 创建的子进程卡死 PIPE管道阻塞问题
目的:想做一个双进程,主进程持续运行收到服务端启动任务,会启动一个pipenv管理的自动化脚本main.py的子进程,主进程受到停止任务,会杀死子进程
尝试1:使用第一种方式,结果自动化脚本先运行一段时间,然后假死
尝试2:使用第二种方式,主进程一直等待子进程直到其完毕,那么子进程就正常运行,但是这显然不是我的目的,我需要主进程子进程同时正常工作
尝试3:分析后怀疑是PIPE管道阻塞造成的,就去掉了PIPE,使用默认父进程的标准输出,这样主进程和子进程就可以都正常工作了
import subprocess import time import signal def set_signal_handler(): _flag_quit = True def main_loop(): # signal.SIGINT为表示要捕捉的信号,由键盘引起的终端(Ctrl c)引起终止程序,第2个参数是个函数指针,表示捕捉到该信号要执行的函数 signal.signal(signal.SIGINT, set_signal_handler) first_flag = 1 _flag_quit = False wx_pid = None popen = None while True: try: # ctrl+c 结束程序运行,除此之外程序会一直运行 if _flag_quit: if wx_pid: print('终止子进程**************************************', wx_pid) os.system(f"taskkill /t /f /pid {wx_pid}") print('我要停止了') return 'end' # 启动子进程 if first_flag == 1: first_flag += 1 # # 第一种 使用subprocess.PIPE,但是不阻塞: # # 1.子进程会执行一段时间然后就假死了,猜测子进程假死原因是subprocess.PIPE中堆积了大量子进程的输出,缓存满了管道阻塞 # # 2.主进程终端命令行不会输出子进程的输出和日志 # popen = subprocess.Popen('pipenv run python ./main.py', stdin=subprocess.PIPE, stdout=subprocess.PIPE, # stderr=subprocess.PIPE) # # 第二种 使用subprocess.PIPE,阻塞: # # 1.在子进程完成前一直等待,子进程正常运行但是主进程会卡在这里什么也不做,直到子进程完成 # # 2.主进程终端命令行不会输出子进程的输出和日志,直到子进程完成会一股脑输出到命令行 # popen = subprocess.Popen('pipenv run python ./main.py', stdin=subprocess.PIPE, stdout=subprocess.PIPE, # stderr=subprocess.PIPE) # print([x.decode("utf8").strip() for x in p.stderr.readlines()]) # 打印被执行文件的报错 # # 被执行文件中所有print打印的内容,这个是同步的会阻塞,等待子进程即被执行文件执行完毕才会输出 # stdout = popen.stdout.readlines() # result = [x.decode("utf8").strip() for x in stdout] # print(stdout,result) # 第三种 不使用subprocess.PIPE: # 1.启动完子进程,主进程正常工作,循环执行自己要做的事,子进程也会正常执行 # 没有使用消息管道PIPE,默认继承父进程标准输出,子进程的输出和日志会和主进程的一起显示在命令行 popen = subprocess.Popen('pipenv run python ./main.py') wx_pid = popen.pid # 这中间可以做一系列主进程要做的动作 pass pass pass # 有子进程的情况下,检测是否存活 if popen: if popen.poll() == 0 or popen.poll() == 1: # popen.poll(),,None-进程未结束在运行、0-进程正常结束、1-错误 # 清空 popen = None time.sleep(5) except BaseException as e: print(e) # 停止子进程 os.system(f"taskkill /t /f /pid {wx_pid}") if popen: wx_pid = None popen = None
五、subprocess.Popen 创建的子进程死掉,popen.poll()却检测不到的bug
1.subprocess的bug描述:
用上面 第四节 中的代码创建的子进程 wx_pid=popen.pid
子进程中我还开了子线程,然后在主进程中用popen.poll()监控子进程的运行状态
有一天子进程忽然没有日志输出了,我以为是子进程和子线程产生了死锁卡住了
可是我用进程号去系统里面查看,该子进程已经没了,但是popen.poll()却检测不到,这应该是subprocess的一个bug
所以不能过分依赖subprocess,应该自己检测子进程号的存活状态来补充popen.poll()的不足:
2.python根据进程号检测进程是否存活
用到 psutil 模块,需要额外安装
import psutil pid = 1136 # int类型 print(psutil.pid_exists(pid)) # 根据有无进程存在返回 True/False