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

 

posted @ 2021-07-02 16:07  www.pu  Views(1778)  Comments(0Edit  收藏  举报