Python 启动一个完全独立于启动进程的进程

Posted

技术标签:

【中文标题】Python 启动一个完全独立于启动进程的进程【英文标题】:Python launch a process completely independent of the launching process 【发布时间】:2014-07-15 22:38:33 【问题描述】:

我正在使用 python 2.5.2 或 2.7,这是一个启动各种任务的 HTTP 服务器 (BaseHTTPServer)。其中一个进程是一个长时间运行的进程。我想做的是启动这个过程,然后关闭我的 HTTP 服务器并重新启动。

问题是我的服务器关闭(关闭所有线程并且python.exe进程退出Windows显示的活动任务列表,启动的进程仍在运行,但netstat -ab显示系统进程有我的HTTP 服务器在 LISTENING 状态下侦听的端口,并与曾经是我的 HTTP 服务器的进程 ID 相关联。该端口保持打开状态,直到启动的进程完成,这使得我无法重新启动我的 HTTP 服务器。

无论我是杀死 python 进程,还是 CTRL-C 窗口,都会表现出相同的行为。我已经阅读了大量文档,每个人都建议使用 subprocess.Popen,但即使使用它似乎也会将主进程的某些部分与启动的进程相关联。

我按如下方式启动该实用程序:

try:

    # NOTE: subprocess.Popen is hanging up the 8091 port until the utility finishes.
    #       This needs to be addressed, otherwise, I'll never be able to restart the
    #       client when the utility has been launched.

    listParams = [ 'C:/MyPath/My.exe', '-f', os.path.join ( sXMLDir, sXmlFile ) ]

    proc = subprocess.Popen ( listParams, cwd='C:/MyPath', creationflags=0x00000008 )
    iSts = 200
    sStatus = 'Utility was successfully launched.'

except:
    iSts = CMClasses.HTTPSTS_STARTSLEDGE_SYSTEM
    sStatus = 'An exception occurred launching utility: ' + str ( sys.exc_type ) + ":" + str ( sys.exc_value  ) + '.'

我的 HTTP 服务器实现如下,它允许我的主程序处理 CTRL-C:

class LaunchHTTPServer ( Thread ):

    def __init__ ( self, sPort, CMRequestHandler ):
        Thread.__init__ ( self )
        self.notifyWindow     =  None
        self.Port             = sPort
        self.CMRequestHandler = CMRequestHandler
        self.bExecute         = True

    def run ( self ):
        server = stoppableHttpServer(('',self.Port), self.CMRequestHandler )
        server.serve_forever()
        server.socket.close()

    def getExecute ( self ):
        return ( self.bExecute )

    def endThread ( self ):
        pass


class stoppableHttpServer ( BaseHTTPServer.HTTPServer ):

    def serve_forever ( self ):
        self.stop = False
        while not self.stop:
            self.handle_request()


def main ( argv ):

    ...

    try:
        ....
        tLaunchHTTPServer = LaunchHTTPServer ( iCMClientPort, CMRequestHandler )
        tLaunchHTTPServer.start()
        ...

    except KeyboardInterrupt:
        logging.info ( 'main: Request to stop received' )

    # End the communication threads

    logging.info ( 'Requesting CMRequestHandler to close.' )
    conn = httplib.HTTPConnection ( "localhost:%d" % iCMClientPort )
    conn.request ( "QUIT", "/" )
    conn.getresponse()
    conn.close()

这是启动实用程序之前 netstat -ab 的结果(我的 python 进程是 3728,我的端口是 8091):

活动连接

原始本地地址外部地址状态PID

TCP vtxshm-po-0101:8091 vtxshm-po-0101:0 监听 3728 [python.exe]

TCP vtxshm-po-0101:8091 vtxshm-po-0101:23193 TIME_WAIT 0 [框架服务.exe]

以下是 netstat -ab 在启动实用程序后以及按 Control-C 并停止 python 后的结果。 (请注意,操作系统认为此端口仍处于侦听状态,分配给 PID 3728,但该进程不再存在于任务管理器中,现在归系统所有,并且与 snmp.exe 有某种关联(我们不甚至使用))。这些连接被理解为来自另一台服务器的启动实用程序的请求。

活动连接

原始本地地址外部地址状态PID

TCP vtxshm-po-0101:8091 vtxshm-po-0101:0 监听 3728 [系统]

TCP vtxshm-po-0101:8091 CH2ChaosMonkeyServer:2133 TIME_WAIT 0 TCP vtxshm-po-0101:8091 CH2ChaosMonkeyServer:2134 TIME_WAIT 0 TCP vtxshm-po-0101:8091 vtxshm-po-0101:23223 TIME_WAIT 0 [snmp.exe]

有没有人成功地从 python 启动了一个进程并完全独立于启动进程运行?如果是这样,你能分享一下这个秘密吗?

【问题讨论】:

看看setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 【参考方案1】:

我将回答一个稍微不同的问题,因为这是我能找到的最接近的类似问题。

我在使用 subprocess.Popen() 侦听端口并在子进程中执行脚本的程序时遇到问题。如果我正在执行的脚本在后台(通过 &)运行长时间运行的作业,并且我终止了主程序,那么长时间运行的作业将接管主程序的侦听端口。

主程序:

import BaseHTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
import subprocess

class RequestHandler(BaseHTTPRequestHandler):
   def do_GET(self):
        proc = subprocess.Popen('start_long_proc.sh', shell=True)
        stdout, stderr = proc.communicate(input)

        self.send_response(200)
        self.end_headers()
        self.wfile.write('request served')

httpd = BaseHTTPServer.HTTPServer(('', 8000), RequestHandler)
httpd.serve_forever()

当 httpd 服务器收到请求时,它会运行“start_long_proc.sh”。该脚本如下所示:

sleep 600&

start_long_proc.sh 立即返回,请求服务完成。现在问题来了:

如果我在sleep 仍在运行时终止网络服务器,sleep 将接管侦听端口:

$ netstat -pant | grep 0.0.0.0:8000
tcp    0   0 0.0.0.0:8000    0.0.0.0:*   LISTEN      24809/sleep     

解决方案这里是在调用脚本时关闭除0、1、2(stdin、stdout、stderr)以外的所有文件描述符。 Popen() 为此提供了一个标志 'close_fds':

proc = subprocess.Popen('start_long_proc.sh', shell=True, close_fds=True)

现在睡眠不再与监听端口绑定:

$ netstat -pant | grep 0.0.0.0:8000 $

发生这种情况的原因是子进程从其父进程继承了打开的文件描述符。让我失望的是,其中包括监听端口。我之前看过'close_fds'选项,但认为它也关闭了STDIN、STDOUT和STDERR。但事实并非如此,因此您仍然可以像往常一样与子进程通信。

【讨论】:

【参考方案2】:

所以你定义:

def run ( self ):
    server = stoppableHttpServer(('',self.Port), self.CMRequestHandler )
    server.serve_forever()
    server.socket.close()

这里没有在您的实例中保留对变量server 的引用。这是一个问题,因为这意味着无法更改下面类中的self.stop 标志:

class stoppableHttpServer ( BaseHTTPServer.HTTPServer ): 
    def serve_forever ( self ):
        self.stop = False
        while not self.stop:
            self.handle_request()

当您执行server_forever 方法时,它会阻塞其线程。由于没有保留对其父实例的引用,因此您无法设置self.stop = True。事实上,在代码中并没有尝试这样做,因此套接字很可能会挂起。如果handle_request 阻塞(如果您没有设置超时),这也是一个问题。

您应该注意serve_forever 的默认实现可以用server.shutdown 停止,因此检查self.stop 标志的状态是多余的。

我建议将您的 LaunchHTTPServer 类更新为:

# as the only part of the code that needs to run as a thread is the serve_forever method
# there is no need to have this class as a thread
class LaunchHTTPServer (object):
    def __init__ ( self, sPort, CMRequestHandler ):
        self.notifyWindow     = None
        self.Port             = sPort
        self.CMRequestHandler = CMRequestHandler
        self.bExecute         = True
        self.server           = None
        self.server_thread    = None 

    def start ( self ):
        # Here you can use the default HTTPServer implementation, as the server is already stoppable
        self.server = BaseHTTPServer.HTTPServer(('',self.Port), self.CMRequestHandler )
        self.server_thread = Thread(target=self.server.serve_forever)
        self.server_thread.start()

    def stop( self ):
        try:
            self.server.shutdown()
            self.server.socket.close()
            self.server_thread.join()
            self.server,self.server_thread = None,None
         except Exception as error:
            pass # catch and raise which ever errors you desire here 


    def getExecute ( self ):
        return ( self.bExecute )

    def endThread ( self ):
        pass

通过上述设置,您现在可以从键盘捕获 Ctrl-C 中断,并确保调用实例的 stop 方法以干净地关闭套接字并退出。

【讨论】:

尽管您提供了对 HTTP 服务器的更清晰的拆解,但仍存在相同的行为。在调用 stop() 时,也不例外。我对 subprocess.Popen() 的理解不正确,存在错误,或者这是不可能的。感谢您的意见。 对不起,它没有更多用处。也许您可以考虑设置 Python 守护程序,而不是启动进程并退出。 抱歉这么久才回复。修改了启动守护线程的代码,它所做的只是执行相同的 subprocess.Popen() 并且效果很好。 +1 用于跟进此问题(一个好的新用户问题令人耳目一新)。

以上是关于Python 启动一个完全独立于启动进程的进程的主要内容,如果未能解决你的问题,请参考以下文章

用python启动一个独立的进程

python multiprocessing 独立启动和关闭进程

在 Qt 中启动单独的进程

python之多线程

如何在 Python 中生成一个新的独立进程

python 进程