[PEP 3143] Python的标准守护进程模块

守护进程不是服务

守护进程不是服务,或者说,守护进程不完全是服务,服务意味着一个后台进程,和能够通过某种方式与后台进程进行交流与控制的另一个进程;而守护进程仅仅意味着服务的后台进程部分

标准守护进程库

python-daemon是符合 PEP 3143 标准的守护进程实现模块。

实现逻辑

根据《UNIX环境高级编程》的记录,详细来说,要实现一个守护进程,那么要依次执行以下几步:

  • 关闭所有已经打开的文件句柄(注:这并不代表之后守护进程不能再打开文件)。
  • 切换当前工作目录。
  • 重设自身的文检掩码。
  • 进入后台。
  • 脱离进程组
  • 关闭终端IO。
  • 脱离终端,并且确保不会重新获取终端
  • 确保正确地处理以下行为:
    • 开机启动的情况。
    • 接收到SIGTERM信号的退出。
    • 子进程要发出SIGCLD信号。

一些守护进程工具,例如 Slack-daemon,不同于 PEP 3143 仅仅实现一个单独的守护进程的目标,它们专注于实现完整的UNIX守护进程功能。参考它们的功能,对于一个守护进程来说,这些额外行为是可取的:

  • 设置当前的进程上下文。
  • 检测initdinetd,并做出正确的反应。
  • 设置SUID与SGID权限,提高安全性。
  • 避免生成内核转储文件,以防止信息泄露。
  • 创建以守护进程命名的PID文件,并保存在合适的位置,以确保守护进程的唯一性(可选)。
  • 重设守护进程所属的用户和组(可选,仅用于root守护进程)。
  • 设置chroot目录(可选,仅可用于root守护进程)。
  • 捕获守护进程的标准输出和标准错误,重定向到syslog或文件(可选)。

具体内容

systemd-daemon是围绕一个守护进程上下文类daemon.DaemonContext(),与它的参数实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对象名 = daemon.DaemonContext(
files_preserve=[保留的文件句柄列表],
chroot_directory=CHROOT目录,
working_directory=工作目录, # 默认为/
umask=掩码, # 默认为0
pidfile=PID文件锁对象, # 几乎总要设置,PID文件会在守护进程结束时自动清理
detach_process=True, # 是否守护进程化,默认是True,除非你希望通过手动控制
uid=执行用户UID, # 默认是当前用户
gid=执行用户GID, # 默认是当前用户
prevent_core=True, # 禁止生成转储文件,防止信息泄露,默认是True
signal_map={信号: 函数}, # 信号处理函数映射,默认为{signal.SIGTERM: 'terminate'},设为None表示无视信号,设为字符串表示DaemonContext的一个实例属性,设为其他值表示信号处理函数
stdin=None, # 标准输入来源,必须是流对象,至少要有w+权限
stdout=None, # 标准输出目标,必须是流对象,至少要有w+权限
stderr=None, # 标准错误目标,必须是流对象,至少要有w+权限
)

这些参数都会被转换为实例属性,可以在之后通过实例进行修改,只需要通过对象.属性 = 值即可

而这个对象有以下这些方法:

打开守护进程上下文,当这个方法返回时,运行中的程序就会变成守护进程,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模块,你还至少需要lockfilesignal模块:

1
2
3
4
5
import daemon
# 用于给PID文件上锁
import lockfile
# 用于提供信号
import signal

简单来说,你只需要把守护进程的主逻辑放到上下文中:

1
2
3
4
5
6
7
with daemon.DaemonContext(pidfile=lockfile.LockFile('/run/xxx.pid')):
# 接下来的内容就是会在守护进程中执行的程序
# 在进入循环之前,你也可以进行一些处理,比如打开一些文件之类的
before_loop()
# 循环不一定要写在外面,写在函数里面也可以
while True:
# 实现一些功能...

更规范的用法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 定义一个在Fork守护进程前的准备函数
def before_daemon():
......

# 定义一个在守护进程进入循环之前的准备函数
def before_loop():
......

# 定义主函数
def main_program():
# 循环不一定要写在外面,写在函数里面也可以
while True:
......

# 定义一个退出前的清理函数
def cleanup():
......

# 定义一个重载函数
def reload():
......

# 先实例化一个上下文对象,注意给PID文件加锁
daemon_context = daemon.DaemonContext(pidfile=lockfile.FileLock('/run/xxx.pid'),
signal_map = {signal.SIGTERM: cleanup(), signal.SIGHUP: reload()} # 在捕获SIGTERM信号时进行清理,在捕获SIGHUP信号时进行重载
)

# 在这里执行进入守护进程前的处理函数
before_daemon()

# 在这里实现守护进程中运行的程序
with daemon_context:
# 在这里执行进入循环之前的处理函数,提示:和before_daemon()的主要区别在于进程环境不同
before_loop()
# 进入主程序循环
main_program()

效果

你应该能在ps xf的输出中看到一个脱离终端,孤立的守护进程,进程显示的命令就是执行脚本时执行的命令:

1
2
PID    TTY    STAT    TIME    COMMAND
2023 ? S 0:00 python daemon.py

简化模块

systemd-daemon有一个简化的service模块,非常易于使用。

服务类

service模块定义了一个Service类,用户只需要继承这个类即可:

1
2
3
4
5
6
7
8
9
10
class MyService(service.Service):
def __init__(self, *args, **kwargs):
# 构造函数里必须先调用父类的构造函数
super(MyService, self).__init__(*args, **kwargs)

# 守护进程主逻辑,必须实现
def run(self):
# 主循环
while not self.got_sigterm():
......

实例化时,需要提供以下参数:

1
对象名 = MyService('服务名', pid_dir='PID文件保存目录'[, signals=['信号列表']])

控制服务

实例化服务对象后,就可以轻易的控制服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == '__main__':
my_service = MyService('我的服务', pid_dir='/run')

# 启动
my_service.start() -> None
# 发送SIGTERM信号停止
my_service.stop([block=False]) -> None
# 发送SIGKILL信号杀死
my_service.kill([block=False]) -> None
# 检测是否运行
my_service.is_running() -> bool
# 获取PID
my_service.get_pid() -> int

当然,还是建议使用位置参数实现子命令进行操作。

登记日志

service模块封装了syslog日志处理器查找函数find_syslog(),可以直接使用syslog登记日志:

1
2
3
4
5
6
7
8
9
10
class MyService(service.Service):
def __init__(self, *args, **kwargs):
super(MyService, self).__init__(*args, **kwargs)
# 这个日志器对象是免费的
self.logger.addHandler(logging.handlers.SysLogHandler(address=find_syslog()), facility=logging.handlers.SysLogHandler.LOG_DAEMON)
......

def run(self):
while not self.got_sigterm():
self.logger.info('日志')

等待信号

.start()方法被调用时,服务会使用一个独立进程运行run()函数;当.stop()方法被调用时,会对run()进程发送SIGTERM信号,此时,run()进程的self.got_sigterm()方法会返回True。此外,也可以使用.kill()方法直接发送SIGKILL信号。

另外,用户可以通过.send_signal()方法给守护进程发送信号,run()函数中,也可以使用self.wait_for_signal()方法等待接收其他信号,不过,这些信号必须在初始化时通过signals参数声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyService(service.Service):
def __init__(self, *args, **kwargs):
super(MyService, self).__init__(*args, **kwargs)

def run(self):
while not self.got_sigterm():
......
# 阻塞直到收到SIGTERM
self.wait_for_sigterm()

# 检测是否收到指定信号
while not self.got_signal(信号):
......
# 阻塞直到收到指定信号
self.wait_for_signal(信号)

my_service = MyService('服务名', pid_dir='/run', signals=[signal.SIGHUP])
# 发送信号
my_service.send_signal(信号名)
# 清除信号行为
my_service.clear()