解释PEP 3143中正确的守护进程行为

9 投票
1 回答
1700 浏览
提问于 2025-04-18 05:39

我在我的树莓派上有一些用Python写的任务,这些任务需要频繁地“睡觉”:做一些事情,可能需要一两秒,或者三秒,然后就要等上几分钟甚至几个小时。为了在这段“睡觉”的时间里把控制权交还给操作系统(Linux),我需要把这些任务变成守护进程。实现这一点的一种方法是使用Python的标准守护进程库。

不过,守护进程并不是那么容易理解。根据PEP 3143中的说明,一个表现良好的守护进程应该做到以下几点:

  • 关闭所有打开的文件描述符。
  • 改变当前的工作目录。
  • 重置文件访问创建掩码。
  • 在后台运行。
  • 与进程组脱离关系。
  • 忽略终端的输入输出信号。
  • 与控制终端脱离关系。
  • 不要重新获取控制终端。
  • 正确处理以下情况:
    • 由System V初始化进程启动。
    • 通过SIGTERM信号终止守护进程。
    • 子进程生成SIGCLD信号。

对于像我这样的Linux/Unix新手来说,这些内容听起来有点复杂。我想知道我为什么要这样做。那么,这些要求背后的原因是什么呢?

1 个回答

13

PEP 3142 这个文档的要求来源于已故的 W. Richard Stevens 的书《Unix 网络编程》('UNP')。下面的解释是从这本书中引用或总结的。网上不太容易找到这些内容,而且下载可能是违法的。所以我从图书馆借来了这本书。提到的页面是第二版的第一卷(1998年)。而 PEP 参考的是第一版(1990年)。

关闭所有打开的文件描述符。

“我们关闭从执行守护进程的进程(即 shell)继承的任何打开的描述符。[..] 一些守护进程会打开 /dev/null 进行读写,并将该描述符复制到标准输入、标准输出和标准错误。”

(这个 'Howdy World' Python 守护进程 展示了这一点。)

“这保证了常用的描述符是打开的,从这些描述符读取时返回 0(文件结束),内核会丢弃写入这三个描述符的任何内容。打开这些描述符的原因是,守护进程调用的任何库函数都假设可以从标准输入读取或写入标准输出或标准错误,这样就不会失败。或者,有些守护进程会打开一个日志文件,在运行时写入,并将其描述符复制到标准输出和标准错误。”(UNP,第337页)

改变当前工作目录

“打印守护进程可能会切换到打印机的缓存目录,在那里完成所有工作。[...] 守护进程可以在文件系统的任何地方启动,如果它保持在那里,该文件系统就无法卸载。”(UNP,第337页)

为什么你想要卸载一个文件系统呢?有两个原因:
1. 你想把可能会填满用户数据的目录和专门用于操作系统的目录分开(并能够挂载和卸载)。
2. 如果你从 USB 闪存启动一个守护进程,你希望能够卸载那个闪存,而不干扰守护进程。

重置文件访问创建掩码。

“这样,如果守护进程创建自己的文件,继承的文件模式创建掩码中的权限位就不会影响新文件的权限位。”(UNP,第337页)

在后台运行。
根据定义,

“守护进程是一个在后台运行并且独立于所有终端控制的进程。”(UNP,第331页)

与进程组脱离关联。
要理解这一点,你需要知道什么是进程组,这意味着你需要了解 fork 的作用。

fork 的作用

fork 是在 Unix 中创建新进程的唯一方法。(在 Linux 中,还有 clone)。理解 fork 的关键是它在调用时会返回两次(一次):一次是在调用进程(即父进程)中,返回新创建进程(即子进程)的进程 ID,另一次是在子进程中。“在 fork 返回时,父进程已知的所有描述符都与子进程共享。”(UNP,第102页)

当一个进程想要执行另一个程序时,它通过调用 fork 创建一个新进程,这个新进程是它自己的副本。然后其中一个进程(通常是子进程)调用新程序。(UNP,第102页)

为什么要与进程组脱离关联

关键是会话领导者可能会获得控制终端。守护进程绝不能这样,它必须保持在后台。这是通过调用 fork 两次实现的:父进程 fork 创建一个子进程,子进程再 fork 创建一个孙进程。父进程和子进程都结束了,但孙进程仍然存在。因为它是孙进程,所以它不是会话领导者,因此无法获得控制终端。(总结自 UNP,第335页)

双重 fork 的详细讨论可以在 这里 和下面的评论中找到。

忽略终端输入输出信号。

“从终端键生成的信号不得影响从该终端早先启动的任何守护进程。”(UNP,第331页)

与控制终端脱离关联,并且不重新获得控制终端。
现在,原因显而易见:

“如果守护进程是从终端启动的,我们希望能够在稍后使用该终端进行其他任务。例如,如果我们从终端启动守护进程,注销该终端,然后其他人登录该终端,我们不希望在下一个用户的终端会话中出现任何守护进程的错误消息。”(UNP,第331页)

正确处理以下情况:

  • 由 System V init 进程启动

    • 显然,守护进程应该在启动时可以启动。
  • 守护进程被 SIGTERM 信号终止

    • SIGTERM 意思是终止信号。在关机时,init 进程通常会向所有进程发送 SIGTERM,通常等待 5 到 20 秒,以给它们时间清理和终止。(UNP,第135页)此外,子进程可以向其父进程发送 SIGTERM,当其父进程应该停止当前操作时。(UNP,第408页)
  • 子进程生成 SIGCLD 信号

    • Stevens 讨论的是 SIGCHLD,而不是 SIGCLD。它们之间的区别对于理解守护进程的行为并不重要。如果子进程终止,它会向其父进程发送 SIGCHLD。如果父进程没有捕获它,子进程就会变成僵尸进程。(UNP,第118页)哦,真有趣。

最后,当我开始在 UNP 中寻找我问题的答案时,我很快意识到我真的应该多读一些这本书。它有 900 多页(!),出版于 1998 年(!),但我相信 UNP 中的概念和解释经得起时间的考验,光辉依旧。Stevens 不仅非常了解他所谈论的内容,他还理解其中的难点,并使其更易于理解。这真的很少见。

撰写回答