守护进程的创建方法

原理

Linux下编程常常需要程序以守护进程的方式启动,根据UNIX环境编程知识可知通常的做法(代码层面)就是fork以及会话、权限、I/O设置等。写多了感到挺无聊的,于是采用一个类的方式把这个过程封装一次,程序代码只要重写run方法即可,就像多线程编程中继承Thread类重写run方法。大致流程如下:

  1. fork 退出父进程 (在此前可以处理好缓冲区数据以免带到子进程中)
  2. 更改子进程目录、修改文件掩码、更新会话ID设置为首领
  3. fork 退出父进程
  4. I/O重定向设置
  5. 管理当前进程的pid(确保其唯一性)
  6. 登记进程终止时的处理函数(通常是删除pid文件、释放可能引起安全问题的资源)
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import os
import atexit
import signal
import sys

class Daemon:

def __init__(self, *args, pidfile='./center.pid', stdin='/dev/null',
stdout='/dev/null',
stderr='/dev/null', **kwargs):
self.pidfile = pidfile
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.args = args
self.kwargs = kwargs

def set_pidfile(self):
# 保存pid到文件
if os.path.exists(self.pidfile):
with open(self.pidfile, 'r') as fp:
pid = fp.read()
raise RuntimeError("Process[{}] already running".format(pid))
pid = os.getpid()
with open(self.pid, 'w') as fp:
print(pid, file=fp)

atexit.register(lambda: os.remote(self.pidfile))

def fork(self):
try:
if os.fork() > 0:
raise SystemExit(0)
except OSError as err:
raise RuntimeError("fork #1 failed.")

os.chdir("/")
os.umask()
# 父进程退出后,子进程称为孤儿。调用os.setsid()创建一个全新的会话。
# 把子进程设置为leadership,确保没有与之关联的终端。
#
os.setsid()

try:
if os.fork() > 0: # 第二次fork使守护进程放弃获取新终端的能力
raise SystemExit(0)
except Exception as err:
raise RuntimeError("fork #2 failed.")

def set_stdio(self):
# 重定向IO
sys.stdout.flush()
sys.stderr.flush()

with open(self.stdin, 'rb', 0) as fp:
os.dup2(fp.fileno(), sys.stdin.fileno())
with open(self.stdout, 'ab', 0) as fp:
os.dup2(fp.fileno(), sys.stdout.fileno())
with open(self.stderr, 'ab', 0) as fp:
os.dup2(fp.fileno(), sys.stderr.fileno())

def _sigterm_handler(self, signo, frame):
# 设置退出信号
raise SystemExit(0)

def set_signal(self):
signal.signal(signal.SIGTERM, self._sigterm_handler)

def start(self):
self.set_pidfile()
self.fork()
self.set_stdio()
self.set_signal()
self.run()

def run(self):
# 继承的类重写该方法,运行需要的程序逻辑
pass

该类的I/O重定向默认写到黑洞上。

从上面代码注意到,fork两次。为什么呢?通常情况下fork一次就可以使子进程在后台运行了。还有一些细节问题代码已经有注释了。

从语法上,我们可以把Daemon类改为装饰器,被装饰的函数以daemon进程的方式执行。只需要在Daemon类中添加__call__方法稍加处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class Daemon:

....


def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
self.start()
func(*args, **kwargs)
return
return wrapper

@Daemon()
def func(*args, **kwargs)
# do func

func(1, 2, a=3, b=4) # execute in daemon process

fork两次的原因

  1. 第一次fork,脱离父进程,设置新的会话并成为首领。脱离创建子进程的终端的能力。
  2. 第二次fork,是进程失去获取新终端的能力。

应用

1
2
3
4
5
6
7
8
9

class ForkTest(Daemon):

def run(self):
# 需要实现的功能

fork = ForkTest() # 可以传入参数
fork.start()

在HTTP服务器编程上的应用

有时候我们并不是破坏原有代码的逻辑,想以混合的方式嵌入创建守护进程的功能。那么我们可以编写一个Mixin类的方法。下面的列子结合HTTP服务器编程。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

from http.server import HTTPServer, SimpleHTTPRequestHandler

class DaemonMixin:

def __init__(self, *args, pidfile='./center.pid', stdin='/dev/null',
stdout='/dev/null',
stderr='/dev/null', **kwargs):
self.pidfile = pidfile
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
super().__init__(*args, **kwargs)

def serve_forever(self, poll_interval=0.5):
self.start()
super().serve_forever(poll_interval)

def set_pidfile(self):
# 保存pid到文件
if os.path.exists(self.pidfile):
with open(self.pidfile, 'r') as fp:
pid = fp.read()
raise RuntimeError("Process[{}] already running".format(pid))
pid = os.getpid()
with open(self.pid, 'w') as fp:
print(pid, file=fp)

atexit.register(lambda: os.remote(self.pidfile))

def fork(self):
try:
if os.fork() > 0:
raise SystemExit(0)
except OSError as err:
raise RuntimeError("fork #1 failed.")

os.chdir("/")
os.umask()
# 父进程退出后,子进程称为孤儿。调用os.setsid()创建一个全新的会话。
# 把子进程设置为leadership,确保没有与之关联的终端。
#
os.setsid()

try:
if os.fork() > 0: # 第二次fork使守护进程放弃获取新终端的能力
raise SystemExit(0)
except Exception as err:
raise RuntimeError("fork #2 failed.")

def set_stdio(self):
# 重定向IO
sys.stdout.flush()
sys.stderr.flush()

with open(self.stdin, 'rb', 0) as fp:
os.dup2(fp.fileno(), sys.stdin.fileno())
with open(self.stdout, 'ab', 0) as fp:
os.dup2(fp.fileno(), sys.stdout.fileno())
with open(self.stderr, 'ab', 0) as fp:
os.dup2(fp.fileno(), sys.stderr.fileno())

def _sigterm_handler(self, signo, frame):
# 设置退出信号
raise SystemExit(0)

def set_signal(self):
signal.signal(signal.SIGTERM, self._sigterm_handler)

def start(self):
self.set_pidfile()
self.fork()
self.set_stdio()
self.set_signal()

class DaemonTCPServer(DaemonMixin, HTTPServer):
# 继承HTTPServer, 并没有修改HTTPServer的任何代码
pass

dserver = DaemonTCPServer(("0.0.0.0", 8080), SimpleHTTPRequestHandler)
dserver.serve_forever()

DaemonMixin以面向切面的方式实现,在为HTTPServer类添加Daemon功能时并没有修改该类的代码。__init__在不同的情况下需要特殊的处理。DaemonTCPServer继承父类的顺序是DaemonMixin、HTTPServer,根据MRO(方法解析顺序),父类会优先调用DaemonMixin的__init__进行初始化。

1
2
3
4
5
6
7
8
9
10
11

def __init__(self, *args, pidfile='./center.pid',
stdin='/dev/null',
stdout='/dev/null',
stderr='/dev/null', **kwargs):
self.pidfile = pidfile
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
super().__init__(*args, **kwargs)

然后根据super().__init__(*args, **kwargs)初始化HTTPServer的参数。

Python提供了装饰器语法糖,通过一定的技巧,让进程以装饰器的方式创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def process_daemonize(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
class Daemon(ProcessDaemon):
def __init__(self, *args, **kwargs):
super().__init__()
self.args = args
self.kwargs = kwargs

def run(self):
func(*self.args, **self.kwargs)

d = Daemon(*args, **kwargs)
d.start()
return wrapper

转载请包括本文地址:https://allenwind.github.io/blog/1923
更多文章请参考:https://allenwind.github.io/blog/archives/