在linux环境上,使用守护进程保护python程序在后台运行,不受会话控制终端影响。
0x01 守护进程和后台运行的区别:
1、让程序在后台运行,只用在程序启动的时候在结尾加&,这样在控制终端进行输入不会影响程序的运行。
如python main.py& 程序启动后,可以在该控制继续进行输入,不影响main.py的运行。但是如果关闭会话连接,main.py进程就关闭了。
2、后台运行的程序有控制终端,守护进程没有。即如果main.py运行的会话连接断开,不影响main.py进程的运行。
父进程退出后,子进程被pid为1的进程(init进程)接管,时候子进程不受终端退出的影响。
0x02 守护进程编程原理:
1、创建子进程,父进程退出
使用os.fork创建进程,并将父进程退出。
目的:让子进程脱离父进程控制,被init进程接管,即变成pid为1的进程的子进程。
2、在子进程中创建新的会话
使用setid创建新会话,并担任该会话组的组长。
目的:
- 让进程摆脱原会话的控制
- 让进程摆脱原进程组的控制
- 让进程摆脱原控制终端的控制
由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。
3、重设文件权限掩码
使用umask(0)重新设置文件权限,是为了去掉父进程遗留的文件权限设置。
4、禁止进程重新打开控制终端
在基于SystemV的系统中,有建议再一次调用fork 并使父进程退出。而新产生的进程将会成为真正的守护进程。这一步骤将保证守护进程不是一个sessionleader,进而阻止它获取一个控制终端。
第二次fork不是必须的,打开一个控制终端的条件是该进程必须是session leader。第一次fork,setsid之后,子进程成为session leader,进程可以打开终端;第二次fork产生的进程,不再是session leader,进程则无法打开终端。
5、改变当前工作目录
防止占用别的路径的working dir的fd,导致一些block不能unmount。但注意这里修改了以后,守护进程下面的子进程log打印就都在被修改的路径下了。
0x03 DEMO
#!/usr/bin/env python #coding: utf-8 #pythonlinux的守护进程 import sys import os import time import string import ctypes import datetime from logger import * logyyx = Logger('tsl.log', logging.ERROR, logging.DEBUG) class Daemon: def __init__(self, findCmd, runCmd, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): self.findCmd = findCmd self.runCmd = runCmd self.stdin = stdin self.stdout = stdout self.stderr = stderr #self.logger = logging.getLogger() ''' def LoggerInit(self): logfile = '/home/***/log/tsl.log' hdlr=logging.FileHandler(logfile) formatter = logging.Formatter(' %(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s') hdlr.setFormatter(formatter) self.logger.addHandler(hdlr) self.logger.setLevel(logging.NOTSET) return ''' def daemonize(self): try: #第一次fork,生成子进程,脱离父进程 if os.fork() > 0: raise SystemExit(0) #退出主进程 except OSError as e: logyyx.error("fork #1 failed: ") #sys.exit(1) raise RuntimeError('fork #1 faild: {0} ({1}) '.format(e.errno, e.strerror)) os.setsid() #设置新的会话连接 os.umask(0) #重新设置文件创建权限 try: #第二次fork,禁止进程打开终端 if os.fork() > 0: raise SystemExit(0) except OSError as e: logyyx.error("fork #2 failed: ") #sys.exit(1) raise RuntimeError('fork #2 faild: {0} ({1}) '.format(e.errno, e.strerror)) os.chdir("/") # 修改工作目录 # 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()) return def start(self): #检查pid文件是否存在以探测是否存在进程 esb = os.popen(self.findCmd).read().strip() if not (esb == '0'): print"the deamon is already running!!!" return else: #启动监控 self.daemonize() self.run() def run(self): now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") while True: try: esb = os.popen(self.findCmd).read().strip() if (esb == '0'): logyyx.info("deamon on %s" %now) os.system(self.runCmd) except: pass time.sleep(10) def KillPid(self,name): ps_str = 'ps aux |grep '+name+' | grep -v grep' x= os.popen(ps_str).read() if x: proc = x.split(' ') for line in proc: print line try: proc_id = line.split()[1] os.system('kill -9 %s' % proc_id) except: pass else: return def checkpid(self, name): findCmd='ps -fe |grep '+name+' | grep -v grep | wc -l' esb = os.popen(findCmd).read().strip() if not (esb == '0'): #杀进程 try: self.KillPid(name) except: print"kill %s failed!!!" % name logyyx.error("the deamon %s kill failed" % name) return return def stop(self): self.checkpid('main.py') self.checkpid('deamon.py') return def restart(self): self.stop() self.start() if __name__ == "__main__": findCmd = 'ps -fe |grep main.py | grep -v grep | wc -l' runCmd = 'python /home/***/main.py' LOG = './tsl.log' daemon = Daemon(findCmd, runCmd, stdout=LOG, stderr=LOG) #daemon.start() if len(sys.argv) != 2: print('Usage: {} [start|stop]'.format(sys.argv[0])) 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 {0}'.format(sys.argv[1])) raise SystemExit(1)