python使用fork实现守护进程的方法
作者:zimsan
守护进程(Daemon)也称为精灵进程是一种生存期较长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。
os模块中的fork方法可以创建一个子进程。相当于克隆了父进程
os.fork()
子进程运行时,os.fork方法会返回0;
而父进程运行时,os.fork方法会返回子进程的PID号。
所以可以使用PID来区分两个进程:
#!/usr/bin/env python #coding=utf8 from time import sleep import os try: pid = os.fork() except OSError, e: pass sleep(30)
运行代码,查看进程:
[root@localhost ~]# python test2.py & [1] 2464 [root@localhost ~]# ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 0 2379 2377 0 80 0 - 28879 wait pts/1 00:00:00 bash 0 S 0 2464 2379 0 80 0 - 31318 poll_s pts/1 00:00:00 python 1 S 0 2465 2464 0 80 0 - 31318 poll_s pts/1 00:00:00 python 0 R 0 2466 2379 0 80 0 - 37227 - pts/1 00:00:00 ps
可以看出第二条python进程就是第一条的子进程。
如刚刚所说os.fork()方法区分子进程和父进程
#-*- coding:utf-8 -*- from time import sleep import os print('start+++++++++++++') #创建子进程之前声明的变量 source = 10 try: pid = os.fork() print('pid=',pid) if pid == 0: #子进程 print("this is child process.") source = source - 1 #在子进程中source减1 else: #父进程 print("this is parent process." ) print(source) except (OSError,e): pass print('END---------------')
面代码中,在子进程创建前,声明了一个变量source,然后在子进程中减1,最后打印出source的值,显然父进程打印出来的值应该为10,子进程打印出来的值应该为9。
[root@localhost ~]# python test3.py start+++++++++++++ pid= 2550 this is parent process. 10 END--------------- pid= 0 this is child process. 9 END---------------
简单守护进程例子:
def main(): ''' 程序要执行的逻辑代码 ''' pass # 创建守护进程函数 def createDaemon(): ''' 第一块(创建第一个子进程) ''' # fork 第一个子进程(如果fork成功,父进程自杀,只留下第一个子进程继续向下运行) try: if os.fork() > 0: sys.exit(0) except OSError, error: print '(fork第一个子进程失败)fork #1 failed: %d (%s)' % (error.errno, error.strerror) sys.exit(1) ''' 第一块结束 ''' ###### 第一个进程创建成功后,它的ppid = 1,已是一个守护里程了,但有些功能上还是有被限制。 ###### 所以下面再来创建一个子进程。第二次创建的子进程限制就没那多了,有可能没有,所以最安全。 ###### 下面来创建第二个子进程。 os.chdir('/') # 把第一个子进程的工作目录切换到 / (根目录) os.setsid() # 第一个子进程取得程序的权限 os.umask(0) # 第一个子进程取得工作目录的所有操作(目录的rwx) ''' 第二块(创建第二个子进程) ''' # fork 第二个子进程(如果fork成功,第一个子进程自杀,只留下新创建的第二个子进程) try: pid = os.fork() if pid > 0: print 'Daemon PID %d' % pid sys.exit(0) except OSError, error: print '(fork第二个子进程失败)fork #2 failed: %d (%s)' % (error.errno, error.strerror) sys.exit(1) ''' 第二块结束 ''' ####### 通过上面两个 try 语句块,只留下了第二个子进程在运行了。这时第二个子进程的ppid=1。 ####### 创建的第二个子进程,可以说是一个不受限的守护进程了。 # 重定向标准IO(因为只有第二个子进程在运行了,所以也就是指定整个程序的输入、输出、错误流) # sys.stdout.flush() # 清除程序运行空间的输出流 # sys.stderr.flush() # 清除程序运行空间的错误流 # inputS = file("/dev/null", 'r') # 定义一个 inputS 文件对象 # outputS = file("/dev/null", 'a+') # 定义一个 outputS 文件对象 # errorS = file("/dev/null", 'a+', 0) # 定义一个 errorS 文件对象 # os.dup2(inputS.fileno(), sys.stdin.fileno()) # 把程序的输入流重定向到上面定义的 inputS 文件对象上。 # os.dup2(so.fileno(), sys.stdout.fileno()) # 把程序的 输出流 重定向到上面定义的 outputS 文件对象上。 # os.dup2(se.fileno(), sys.stderr.fileno()) # 把程序的 错误流 重定向到上面定义的 errorS 文件对象上。 main() # main函数为真正程序逻辑代码 if __name__ == "__main__": if platform.system() == "Linux": createDaemon() else: sys.exit()
带控制参数的例子:
编写守护进程的基类,用于继承:
# coding: utf-8 import os import sys import time import atexit import signal class Daemon: def __init__(self, pidfile='/tmp/daemon.pid', stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = pidfile def daemonize(self): if os.path.exists(self.pidfile): raise RuntimeError('Already running.') # First fork (detaches from parent) try: if os.fork() > 0: raise SystemExit(0) except OSError as e: raise RuntimeError('fork #1 faild: {0} ({1})\n'.format(e.errno, e.strerror)) os.chdir('/') os.setsid() os.umask(0o22) # Second fork (relinquish session leadership) try: if os.fork() > 0: raise SystemExit(0) except OSError as e: raise RuntimeError('fork #2 faild: {0} ({1})\n'.format(e.errno, e.strerror)) # Flush I/O buffers sys.stdout.flush() sys.stderr.flush() # Replace file descriptors for stdin, stdout, and stderr with open(self.stdin, 'rb', 0) as f: os.dup2(f.fileno(), sys.stdin.fileno()) with open(self.stdout, 'ab', 0) as f: os.dup2(f.fileno(), sys.stdout.fileno()) with open(self.stderr, 'ab', 0) as f: os.dup2(f.fileno(), sys.stderr.fileno()) # Write the PID file with open(self.pidfile, 'w') as f: print(os.getpid(), file=f) # Arrange to have the PID file removed on exit/signal atexit.register(lambda: os.remove(self.pidfile)) signal.signal(signal.SIGTERM, self.__sigterm_handler) # Signal handler for termination (required) @staticmethod def __sigterm_handler(signo, frame): raise SystemExit(1) def start(self): try: self.daemonize() except RuntimeError as e: print(e, file=sys.stderr) raise SystemExit(1) self.run() def stop(self): try: if os.path.exists(self.pidfile): with open(self.pidfile) as f: os.kill(int(f.read()), signal.SIGTERM) else: print('Not running.', file=sys.stderr) raise SystemExit(1) except OSError as e: if 'No such process' in str(e) and os.path.exists(self.pidfile): os.remove(self.pidfile) def restart(self): self.stop() self.start() def run(self): #继承类重写该方法 pass
编写自己的类:
#导入刚刚编写的基类 from daemon import Daemon #继承 class MyTestDaemon(Daemon): #重写run方法,就是你要后台运行的函数 def run(self): #后台运行的函数,比如shell输出到自己定义的文件 sys.stdout.write('Daemon started with pid {}\n'.format(os.getpid())) while True: sys.stdout.write('Daemon Alive! {}\n'.format(time.ctime())) sys.stdout.flush() time.sleep(5) if __name__ == '__main__': PIDFILE = '/tmp/daemon-example.pid' LOG = '/tmp/daemon-example.log' daemon = MyTestDaemon(pidfile=PIDFILE, stdout=LOG, stderr=LOG) if len(sys.argv) != 2: print('Usage: {} [start|stop]'.format(sys.argv[0]), file=sys.stderr) raise SystemExit(1) if 'start' == sys.argv[1]: daemon.start() elif 'stop' == sys.argv[1]: daemon.stop() elif 'restart' == sys.argv[1]: daemon.restart() else: print('Unknown command {!r}'.format(sys.argv[1]), file=sys.stderr) raise SystemExit(1)
关于两次fork
第二个fork不是必须的,只是为了防止进程打开控制终端。
打开一个控制终端的条件是该进程必须是session leader。第一次fork,setsid之后,子进程成为session leader,进程可以打开终端;第二次fork产生的进程,不再是session leader,进程则无法打开终端。
也就是说,只要程序实现得好,控制程序不主动打开终端,无第二次fork亦可。
代码实现
# coding: utf-8 import os import sys import time import atexit import signal class Daemon: def __init__(self, pidfile='/tmp/daemon.pid', stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = pidfile def daemonize(self): if os.path.exists(self.pidfile): raise RuntimeError('Already running.') # First fork (detaches from parent) try: if os.fork() > 0: raise SystemExit(0) except OSError as e: raise RuntimeError('fork #1 faild: {0} ({1})\n'.format(e.errno, e.strerror)) os.chdir('/') os.setsid() os.umask(0o22) # Second fork (relinquish session leadership) try: if os.fork() > 0: raise SystemExit(0) except OSError as e: raise RuntimeError('fork #2 faild: {0} ({1})\n'.format(e.errno, e.strerror)) # Flush I/O buffers sys.stdout.flush() sys.stderr.flush() # Replace file descriptors for stdin, stdout, and stderr with open(self.stdin, 'rb', 0) as f: os.dup2(f.fileno(), sys.stdin.fileno()) with open(self.stdout, 'ab', 0) as f: os.dup2(f.fileno(), sys.stdout.fileno()) with open(self.stderr, 'ab', 0) as f: os.dup2(f.fileno(), sys.stderr.fileno()) # Write the PID file with open(self.pidfile, 'w') as f: print(os.getpid(), file=f) # Arrange to have the PID file removed on exit/signal atexit.register(lambda: os.remove(self.pidfile)) signal.signal(signal.SIGTERM, self.__sigterm_handler) # Signal handler for termination (required) @staticmethod def __sigterm_handler(signo, frame): raise SystemExit(1) def start(self): try: self.daemonize() except RuntimeError as e: print(e, file=sys.stderr) raise SystemExit(1) self.run() def stop(self): try: if os.path.exists(self.pidfile): with open(self.pidfile) as f: os.kill(int(f.read()), signal.SIGTERM) else: print('Not running.', file=sys.stderr) raise SystemExit(1) except OSError as e: if 'No such process' in str(e) and os.path.exists(self.pidfile): os.remove(self.pidfile) def restart(self): self.stop() self.start() def run(self): pass
使用测试
import os import sys import time from daemon import Daemon class MyTestDaemon(Daemon): def run(self): sys.stdout.write('Daemon started with pid {}\n'.format(os.getpid())) while True: sys.stdout.write('Daemon Alive! {}\n'.format(time.ctime())) sys.stdout.flush() time.sleep(5) if __name__ == '__main__': PIDFILE = '/tmp/daemon-example.pid' LOG = '/tmp/daemon-example.log' daemon = MyTestDaemon(pidfile=PIDFILE, stdout=LOG, stderr=LOG) if len(sys.argv) != 2: print('Usage: {} [start|stop]'.format(sys.argv[0]), file=sys.stderr) raise SystemExit(1) if 'start' == sys.argv[1]: daemon.start() elif 'stop' == sys.argv[1]: daemon.stop() elif 'restart' == sys.argv[1]: daemon.restart() else: print('Unknown command {!r}'.format(sys.argv[1]), file=sys.stderr) raise SystemExit(1)
[daemon] python test.py start 23:45:42 [daemon] cat /tmp/daemon-example.pid 23:45:49 8532 [daemon] ps -ef|grep 8532 | grep -v grep 23:46:07 502 8532 1 0 11:45下午 ?? 0:00.00 python test.py start [daemon] tail -f /tmp/daemon-example.log 23:46:20 Daemon started with pid 8532 Daemon Alive! Fri Dec 2 23:45:49 2016 Daemon Alive! Fri Dec 2 23:45:54 2016 Daemon Alive! Fri Dec 2 23:45:59 2016 Daemon Alive! Fri Dec 2 23:46:04 2016 Daemon Alive! Fri Dec 2 23:46:09 2016 Daemon Alive! Fri Dec 2 23:46:14 2016 Daemon Alive! Fri Dec 2 23:46:19 2016 Daemon Alive! Fri Dec 2 23:46:24 2016 Daemon Alive! Fri Dec 2 23:46:29 2016 Daemon Alive! Fri Dec 2 23:46:34 2016 [daemon] python test.py stop 23:46:36 [daemon] ps -ef|grep 8532 | grep -v grep 23:46:43