[PEP 3143] Python的标准守护进程模块
守护进程不是服务
守护进程不是服务,或者说,守护进程不完全是服务,服务意味着一个后台进程,和能够通过某种方式与后台进程进行交流与控制的另一个进程;而守护进程仅仅意味着服务的后台进程部分。
标准守护进程库
python-daemon是符合 PEP 3143 标准的守护进程实现模块。
实现逻辑
根据《UNIX环境高级编程》的记录,详细来说,要实现一个守护进程,那么要依次执行以下几步:
- 关闭所有已经打开的文件句柄(注:这并不代表之后守护进程不能再打开文件)。
 - 切换当前工作目录。
 - 重设自身的文检掩码。
 - 进入后台。
 - 脱离进程组。
 - 关闭终端IO。
 - 脱离终端,并且确保不会重新获取终端。
 - 确保正确地处理以下行为:
- 开机启动的情况。
 - 接收到SIGTERM信号的退出。
 - 子进程要发出SIGCLD信号。
 
 
一些守护进程工具,例如 Slack-daemon,不同于 PEP 3143 仅仅实现一个单独的守护进程的目标,它们专注于实现完整的UNIX守护进程功能。参考它们的功能,对于一个守护进程来说,这些额外行为是可取的:
- 设置当前的进程上下文。
 - 检测
initd与inetd,并做出正确的反应。 - 设置SUID与SGID权限,提高安全性。
 - 避免生成内核转储文件,以防止信息泄露。
 - 创建以守护进程命名的PID文件,并保存在合适的位置,以确保守护进程的唯一性(可选)。
 - 重设守护进程所属的用户和组(可选,仅用于root守护进程)。
 - 设置chroot目录(可选,仅可用于root守护进程)。
 - 捕获守护进程的标准输出和标准错误,重定向到syslog或文件(可选)。
 
具体内容
systemd-daemon是围绕一个守护进程上下文类daemon.DaemonContext(),与它的参数实现的:
1  | 对象名 = daemon.DaemonContext(  | 
这些参数都会被转换为实例属性,可以在之后通过实例进行修改,只需要通过对象.属性 = 值即可。
而这个对象有以下这些方法:
打开守护进程上下文,当这个方法返回时,运行中的程序就会变成守护进程,is_open被设为True(换句话说,所有进程分叉过程都交给守护进程上下文解决了,如果你希望了解具体怎么实现的,请看 python-daemon):
1  | 对象.open() -> None  | 
关闭守护进程上下文,**这意味着清除PID文件,以及将is_open设为False**:
1  | 对象.close() -> None  | 
返回守护进程上下文是否处于开启状态:
1  | 对象.is_open -> bool  | 
守护进程接收到指定信号的操作方法:
1  | 对象.terminate([信号])  | 
此外,既然是上下文,它就当然实现了with语句:
执行with语句时自动执行的方法,调用.open()方法并返回实例:
1  | 对象.__enter() -> DaemonContext  | 
退出with语句时自动执行的方法,调用.close()方法,如果关闭成功则返回True:
1  | 对象.__exit__() -> bool  | 
使用
要使用daemon模块,你还至少需要lockfile和signal模块:
1  | import daemon  | 
简单来说,你只需要把守护进程的主逻辑放到上下文中:
1  | with daemon.DaemonContext(pidfile=lockfile.LockFile('/run/xxx.pid')):  | 
更规范的用法是:
1  | # 定义一个在Fork守护进程前的准备函数  | 
效果
你应该能在ps xf的输出中看到一个脱离终端,孤立的守护进程,进程显示的命令就是执行脚本时执行的命令:
1  | PID TTY STAT TIME COMMAND  | 
简化模块
systemd-daemon有一个简化的service模块,非常易于使用。
服务类
service模块定义了一个Service类,用户只需要继承这个类即可:
1  | class MyService(service.Service):  | 
实例化时,需要提供以下参数:
1  | 对象名 = MyService('服务名', pid_dir='PID文件保存目录'[, signals=['信号列表']])  | 
控制服务
实例化服务对象后,就可以轻易的控制服务:
1  | if __name__ == '__main__':  | 
当然,还是建议使用位置参数实现子命令进行操作。
登记日志
service模块封装了syslog日志处理器查找函数find_syslog(),可以直接使用syslog登记日志:
1  | class MyService(service.Service):  | 
等待信号
当.start()方法被调用时,服务会使用一个独立进程运行run()函数;当.stop()方法被调用时,会对run()进程发送SIGTERM信号,此时,run()进程的self.got_sigterm()方法会返回True。此外,也可以使用.kill()方法直接发送SIGKILL信号。
另外,用户可以通过.send_signal()方法给守护进程发送信号,run()函数中,也可以使用self.wait_for_signal()方法等待接收其他信号,不过,这些信号必须在初始化时通过signals参数声明:
1  | class MyService(service.Service):  |