[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): |