一个Python脚本是否可以知道同一脚本的另一个实例是否正在运行并与其通信?

14
我希望防止同一个长时间运行的Python命令行脚本同时运行多个实例,并且我希望新实例能够在自杀之前将数据发送给原始实例。如何以跨平台的方式实现这一点?
具体而言,我想实现以下行为:
1.从命令行启动“foo.py”,它将持续运行很长时间--天或周,直到机器重新启动或父进程将其杀死。
2.每隔几分钟就会再次启动相同的脚本,但使用不同的命令行参数。
3.当启动时,脚本应该查看是否有其他实例正在运行。
4.如果有其他实例正在运行,则第二个实例应将其命令行参数发送给第一个实例,然后第二个实例应退出。
5.如果收到另一个脚本发送的命令行参数,实例#1应该启动一个新线程,并开始执行实例#2要执行的工作。
因此,我正在寻找两件事:Python程序如何知道另一个实例正在运行,然后一个Python命令行程序如何与另一个通信?
使这更加复杂的是,同一个脚本需要在Windows和Linux上运行,因此理想情况下的解决方案仅使用Python标准库而不使用任何特定于操作系统的调用。虽然如果我需要有一个Windows代码路径和一个*nix代码路径(并且在我的代码中有一个大的if语句来选择其中一个),那么如果“同一代码”解决方案不可行,那也没关系。
我意识到我可能可以想出一个基于文件的方法(例如,实例#1监视更改的目录,每个实例在要执行工作时将文件放入该目录),但我有点担心在非正常关闭机器后清理这些文件。我最好能够使用内存中的解决方案。但是我还是很灵活的,如果持久性文件为基础的方法是唯一的方法,我愿意采用该选项。更多细节:我正在尝试这样做是因为我们的服务器使用一种监控工具来运行Python脚本以收集监控数据(例如数据库查询或Web服务调用的结果),然后该监控工具会对其进行索引以备将来使用。有些脚本启动非常昂贵,但在启动后运行起来很便宜(例如建立数据库连接与执行查询操作)。因此,我们选择将它们保持在无限循环中,直到父进程杀死它们。
这样做效果很好,但在较大的服务器上,即使每20分钟收集一次数据,也可能会运行100个相同的脚本实例。这会对RAM、数据库连接限制等造成严重破坏。我们想要从具有1个线程的100个进程切换到一个具有100个线程的进程,每个线程执行以前一个脚本完成的任务。
但是,改变如何通过监控工具调用脚本是不可能的。我们需要保持调用方式不变(使用不同的命令行参数启动进程),但是改变脚本以识别另一个脚本已经在运行,并且让“新”脚本将其工作指令(从命令行参数)发送到“旧”脚本。
顺便说一下,这不是我想逐个脚本执行的操作。相反,我想将此行为打包成一个库,许多脚本作者可以利用 - 我的目标是使脚本作者编写简单的单线程脚本,这些脚本不知道多实例问题,并在底层处理多线程和单实例。

你为什么坚持让工作脚本与命令调用脚本相同?工作脚本可以是一个服务器进程,接收由命令中继客户端发送的命令,并由监控框架调用,其唯一任务是告诉服务器应该做什么。 - Bernd
4个回答

11

使用Alex Martelli的方法设置通信渠道是合适的。我会使用multiprocessing.connection.Listener来创建一个监听器,您可以选择自己的文档。文档位于: http://docs.python.org/library/multiprocessing.html#multiprocessing-listeners-clients

与其使用AF_INET(套接字),您可能选择在Linux上使用AF_UNIX,Windows上使用AF_PIPE。希望这个小“if”不会有太大影响。

编辑:我想一个例子也不会有什么影响。尽管它很基础。

#!/usr/bin/env python

from multiprocessing.connection import Listener, Client
import socket
from array import array
from sys import argv

def myloop(address):
    try:
        listener = Listener(*address)
        conn = listener.accept()
        serve(conn)
    except socket.error, e:
        conn = Client(*address)
        conn.send('this is a client')
        conn.send('close')

def serve(conn):
    while True:
        msg = conn.recv()
        if msg.upper() == 'CLOSE':
            break
        print msg
    conn.close()

if __name__ == '__main__':
    address = ('/tmp/testipc', 'AF_UNIX')
    myloop(address)

这在 OS X 上可以正常工作,所以需要在 Linux 和(替换正确地址后)Windows 上进行测试。从安全角度来看,存在许多注意事项,其中主要问题是 conn.recv 对其数据进行解封,因此使用 recv_bytes 通常更好。


很棒的答案!能够使用命名管道(Windows)或FIFO(Unix),因为我可以将管道/ FIFO命名为唯一的脚本名称,似乎比必须在脚本和端口号之间保持映射更容易。 - Justin Grant

9
一般的方法是在启动时,脚本建立一个通信渠道,并保证该渠道是独占的(其他尝试建立相同通道的操作会以可预测的方式失败),以便进一步的脚本实例可以检测到第一个正在运行的实例并与其通信。
您对跨平台功能的要求强烈表明需要使用套接字作为所需的通信渠道:您可以指定一个“众所周知的端口”来保留您的脚本,比如说12345,并在该端口上打开一个仅监听本地主机(127.0.0.1)的套接字。如果尝试打开该套接字失败,因为所需端口已经“被占用”,那么您可以连接到该端口号,这将让您与现有的脚本进行通信。
如果您不熟悉套接字编程,可以阅读这篇很好的 HOWTO 文档这里。您也可以参考 Python in a Nutshell 中相关章节(当然我对这个有偏见;-)。

嗨,Alex - 感谢您的快速回复!我对使用众所周知的端口方法的主要担忧是可能会发生冲突(我们不拥有服务器,因此其他程序可能会使用这些端口)和端口号管理(因为我们将将单实例技巧应用于由不同脚本作者维护的许多脚本)。是否有解决上述问题的方法,或者我最好使用“命名IPC”机制?我怀疑Windows上的命名管道和*nix上的域套接字可以做到这一点,但我不知道从Python中使用它们有多容易。 - Justin Grant
@Justin,我不确定您如何以跨平台和“本质上互斥”的方式使用命名管道和UNIX域套接字。为了支持您所需的特定功能,您可以通过访问和更新一个.dbm(或sqlite等)存档来记录脚本名称X应该使用的“不太出名的端口”,保留名称到端口的对应关系(如果一个脚本在启动时没有找到它的名称,它会从操作系统获得一个新的端口并记录它),可能还要使用一些文件锁定机制来避免竞态条件。 - Alex Martelli
@Muhammad Alkarouri在下面的回答中(使用multiprocessing包)似乎是一个可行的跨平台解决方案,同时避免了将脚本映射到端口号的复杂性。使用multiprocessing有什么缺点吗? - Justin Grant

1

或许可以尝试使用 socket 进行通信?


0
听起来你最好的选择是坚持使用pid文件,但不仅要包含进程ID,还要包括先前实例正在侦听的端口号。因此,在启动时检查pid文件,如果存在,则查看是否运行了具有该ID的进程-如果是,则将数据发送到它并退出;否则,请使用当前进程的信息覆盖pid文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接