Python实现守护进程的完整指南与代码解析
本文详细解析在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)
概括守护进程编写的核心步骤如下:
- 执行fork调用创建子进程,随后立即退出父进程。
- 子进程需变更工作目录(chdir)、重置文件权限掩码(umask),并调用setsid创建新的会话组和进程组。
- 子进程再次执行fork调用创建孙子进程,随后退出子进程。
- 孙子进程负责刷新缓冲区,并将标准输入、输出及错误文件描述符重定向至/dev/null(即丢弃所有I/O操作)。
- 可选步骤:将进程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)。
守护进程与后台进程
使用&符号可将命令放入后台执行,但这与守护进程有本质区别:
- 守护进程与终端无关,是被init进程收养的孤儿进程;而后台进程的父进程仍是终端,仍可向终端打印信息。
- 守护进程在关闭终端时依然运行;而后台进程会随用户退出而停止,除非使用nohup命令。
- 守护进程会改变会话、进程组、工作目录和文件描述符;而后台进程直接继承父进程(Shell)的这些属性。
简而言之:守护进程是默默奋斗、完全独立的“有为青年”,而后台进程则是继承父进程资产的“富二代”。
总结
以上就是本文的全部内容,希望这些内容能对您的学习或工作有所帮助。如有任何疑问,欢迎留言交流。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













