守护进程的创建方法
原理
Linux下编程常常需要程序以守护进程的方式启动,根据UNIX环境编程知识可知通常的做法(代码层面)就是fork以及会话、权限、I/O设置等。写多了感到挺无聊的,于是采用一个类的方式把这个过程封装一次,程序代码只要重写run方法即可,就像多线程编程中继承Thread类重写run方法。大致流程如下:
- fork 退出父进程 (在此前可以处理好缓冲区数据以免带到子进程中)
- 更改子进程目录、修改文件掩码、更新会话ID设置为首领
- fork 退出父进程
- I/O重定向设置
- 管理当前进程的pid(确保其唯一性)
- 登记进程终止时的处理函数(通常是删除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): 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()
try: if os.fork() > 0: raise SystemExit(0) except Exception as err: raise RuntimeError("fork #2 failed.")
def set_stdio(self): 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)
func(1, 2, a=3, b=4)
|
fork两次的原因
- 第一次fork,脱离父进程,设置新的会话并成为首领。脱离创建子进程的终端的能力。
- 第二次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): 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()
try: if os.fork() > 0: raise SystemExit(0) except Exception as err: raise RuntimeError("fork #2 failed.")
def set_stdio(self): 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): 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/