Python 实现守护进程:原理与代码详解

南乔
南乔
发布时间:2026-10-14 | 5人浏览过
原创

本文详细解析在 Python 中创建守护进程(Daemon)的方法与核心原理。通过两次 Fork 机制、会话组管理、文件描述符重定向及权限掩码设置,实现服务脱离终端控制并常驻后台。文章提供完整 Python 代码示例,并深入剖析守护进程与后台进程的区别,帮助开发者构建稳定的 Linux 后台服务。

守护进程概念与背景

假设你正在开发一个 Python 服务程序,并通过命令行启动它。此时,该 Python 服务作为终端程序的子进程运行。一旦你关闭终端窗口,这个命令行程序也会随之终止。为了让你的 Python 服务能够独立于终端运行,即使关闭终端也能常驻系统,你需要将其转换为守护进程。守护进程(Daemon)是指在系统后台运行的程序,它独立于控制终端,负责执行周期性任务或响应触发事件。这类进程通常以字母“d”结尾命名,例如常见的 httpd、syslogd、systemd 和 dockerd 等。

Python 守护进程代码实现

代码实现

Python 能够以简洁的方式实现守护进程。以下提供具体的代码实现及其详细注释:

核心代码解析


# coding=utf8
import os
import sys
import atexit
 
 
def daemonize(pid_file=None):
  """
  创建守护进程
  :param pid_file: 保存进程id的文件
  :return:
  """
  # 从父进程fork一个子进程出来
  pid = os.fork()
  # 子进程的pid一定为0,父进程大于0
  if pid:
    # 退出父进程,sys.exit()方法比os._exit()方法会多执行一些刷新缓冲工作
    sys.exit(0)
 
  # 子进程默认继承父进程的工作目录,最好是变更到根目录,否则回影响文件系统的卸载
  os.chdir('/')
  # 子进程默认继承父进程的umask(文件权限掩码),重设为0(完全控制),以免影响程序读写文件
  os.umask(0)
  # 让子进程成为新的会话组长和进程组长
  os.setsid()
 
  # 注意了,这里是第2次fork,也就是子进程的子进程,我们把它叫为孙子进程
  _pid = os.fork()
  if _pid:
    # 退出子进程
    sys.exit(0)
 
  # 此时,孙子进程已经是守护进程了,接下来重定向标准输入、输出、错误的描述符(是重定向而不是关闭, 这样可以避免程序在 print 的时候出错)
 
  # 刷新缓冲区先,小心使得万年船
  sys.stdout.flush()
  sys.stderr.flush()
 
  # dup2函数原子化地关闭和复制文件描述符,重定向到/dev/nul,即丢弃所有输入输出
  with open('/dev/null') as read_null, open('/dev/null', 'w') as write_null:
    os.dup2(read_null.fileno(), sys.stdin.fileno())
    os.dup2(write_null.fileno(), sys.stdout.fileno())
    os.dup2(write_null.fileno(), sys.stderr.fileno())
 
  # 写入pid文件
  if pid_file:
    with open(pid_file, 'w+') as f:
      f.write(str(os.getpid()))
    # 注册退出函数,进程异常退出时移除pid文件
    atexit.register(os.remove, pid_file)

守护进程创建步骤

编写守护进程的核心步骤概括如下:

  1. 1. 调用 fork 创建子进程,并立即退出父进程。
  2. 2. 子进程修改工作目录(chdir)、重置文件权限掩码(umask),并调用 setsid 创建新的进程组和会话组。
  3. 3. 子进程再次 fork 创建孙子进程,并退出子进程。
  4. 4. 孙子进程刷新缓冲区,并将标准输入、标准输出和标准错误重定向至 /dev/null(即丢弃所有输入输出)。
  5. 5. (可选)将进程 ID(PID)写入指定文件。

核心原理详解

理解守护进程的关键要点

两次 Fork 的必要性

为什么需要 Fork 两次?

第一次 Fork 的目的是使进程脱离终端的控制。父进程退出是因为终端关闭或键盘操作会向其发送信号;而通过 Fork 产生的子进程,在父进程退出后成为孤儿进程,随后被操作系统的 init 进程接管,从而摆脱终端控制。实际上,第二次 Fork 并非绝对必要(许多开源项目代码中并未进行两次 Fork)。但出于谨慎考虑,防止进程重新打开控制终端,通常会进行第二次 Fork。因为子进程此时已成为会话组长(会话中的首个进程),拥有打开控制终端的能力;而通过第二次 Fork 产生的孙子进程,由于不再是会话组长,便无法再打开控制终端。

文件描述符与重定向

文件描述符

在 Linux 系统中,“一切皆文件”。文件描述符是内核为已打开文件创建的索引,通常为非负整数。进程通过文件描述符执行 I/O 操作。默认情况下,0 代表标准输入,1 代表标准输出,2 代表标准错误。

权限掩码 umask 机制

umask 权限掩码

在 Linux 中,任何文件都具备读(read)、写(write)和执行(execute)三种权限。其中,读权限用数字 4 表示,写权限为 2,执行权限为 1。使用命令 ls -l 可查看文件权限,r/w/x 分别表示具有读/写/执行权限。此外,文件还涉及用户(User)、用户组(Group)和其他用户(Others)三种身份权限,通常用三个数字表示,例如 754:

  • 7 代表 User 权限,即文件拥有者的权限。
  • 5 代表 Group 权限,即拥有者所在用户组成员的权限。
  • 4 代表 Others 权限,即其他用户的权限。

umask 用于控制默认权限,防止新建文件或文件夹拥有过高权限。系统默认 umask 通常为 022(可通过命令 umask 查看),这意味着默认创建文件的权限为 644,文件夹权限为 755。可以看出规律:文件权限与 umask 相加结果为 666,文件夹权限与 umask 相加结果为 777。

进程组与会话组管理

进程组

每个进程都属于一个进程组(PG, Process Group),一个进程组可包含多个进程。进程组有一个进程组长(Leader),其进程 ID(PID, Process ID)即作为整个进程组的 ID(PGID, Process Group ID)。

会话组

登录终端时,系统会创建一个会话,多个进程组可包含在一个会话中。创建会话的进程即为会话组长。已经是会话组长的进程,不能再调用 setsid() 方法创建新会话。因此,在上述代码中,子进程可以调用 setsid(),而父进程不能,因为父进程本身就是会话组长。此外,sh(Bourne Shell)不支持会话机制,因为会话机制需要 Shell 支持工作控制(Job Control)。

守护进程与后台进程的区别

守护进程与后台进程

通过 & 符号可以将命令放入后台执行,但这与守护进程不同:

  1. 守护进程与终端无关,是被 init 进程收养的孤儿进程;而后台进程的父进程仍是终端,仍可在终端打印信息。
  2. 守护进程在关闭终端时依然运行;而后台进程会随用户退出而停止,除非使用 nohup 命令。
  3. 守护进程改变了会话、进程组、工作目录和文件描述符;而后台进程直接继承父进程(Shell)的这些属性。

简而言之:守护进程如同默默奋斗、完全独立的有为青年;而后台进程则像继承父辈资产的富二代,仍与父进程(终端)紧密相连。

以上即为 Python 创建守护进程的完整示例与解析。如需了解更多关于 Python 守护进程的资料,请持续关注本站其他相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表