Python实现守护进程的完整指南与代码解析

小石头
小石头
发布时间:2026-10-13 | 5人浏览过
原创

本文详细解析在Python中实现守护进程(Daemon)的方法。通过两次fork机制、setsid会话创建及文件描述符重定向等技术,使程序脱离终端控制并常驻后台。文章提供了完整的Python代码实现,并深入解释了fork原理、umask权限掩码、进程组与会话组的区别,以及守护进程与后台进程的核心差异,帮助开发者掌握Linux环境下的服务常驻技巧。

场景设置:

假设你正在开发一个Python服务程序,并通过命令行启动。此时,该Python服务作为终端程序的子进程运行。一旦你关闭终端窗口或断开连接,依附于该终端的命令行程序也会随之终止。

为了让Python服务摆脱对终端的依赖,实现长期在系统后台运行,我们需要将其配置为守护进程。

守护进程(Daemon)是指在系统后台运行、独立于控制终端的程序,通常用于执行周期性任务或响应触发事件。在Linux系统中,这类进程名称常以字母“d”结尾,例如httpd、syslogd、systemd以及dockerd等。

代码实现

使用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. 执行fork调用创建子进程,随后立即退出父进程。
  2. 子进程需变更工作目录(chdir)、重置文件权限掩码(umask),并调用setsid创建新的会话组和进程组。
  3. 子进程再次执行fork调用创建孙子进程,随后退出子进程。
  4. 孙子进程负责刷新缓冲区,并将标准输入、输出及错误文件描述符重定向至/dev/null(即丢弃所有I/O操作)。
  5. 可选步骤:将进程ID(PID)写入指定文件。

理解几个要点

为什么要进行两次fork操作?

第一次fork的目的是使进程脱离终端的控制。父进程退出后,子进程成为孤儿进程,被操作系统init进程接管,从而不再受终端信号(如键盘敲击或关闭终端)的影响。

第二次fork并非绝对必要(许多开源项目代码中并未执行两次fork),但出于谨慎考虑,它可以防止子进程再次打开控制终端。因为此时的子进程已成为会话组长,拥有打开控制终端的能力;通过再次fork生成的孙子进程则无法再打开控制终端,从而确保守护进程的纯粹性。

文件描述符

在Linux系统中,“一切皆文件”。文件描述符是内核为已打开文件创建的索引,通常为非负整数。进程通过文件描述符执行I/O操作。

默认情况下,文件描述符0代表标准输入,1代表标准输出,2代表标准错误。

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)即作为整个进程组的ID(PGID)。

会话组

登录终端时会创建一个会话,多个进程组可包含在一个会话中。创建会话的进程即为会话组长。已是会话组长的进程不能再调用setsid()创建新会话。因此,上述代码中子进程可调用setsid(),而父进程不能,因为它本身已是会话组长。

此外,sh(Bourne Shell)不支持会话机制,因为会话机制需要Shell支持工作控制(Job Control)。

守护进程与后台进程

使用&符号可将命令放入后台执行,但这与守护进程有本质区别:

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

简而言之:守护进程是默默奋斗、完全独立的“有为青年”,而后台进程则是继承父进程资产的“富二代”。

总结

以上就是本文的全部内容,希望这些内容能对您的学习或工作有所帮助。如有任何疑问,欢迎留言交流。

相关标签:

本站声明

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

Python 相关产品和应用

全部下载

专题列表