在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('\n%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s\n%(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:\n") #sys.exit(1) raise RuntimeError('fork #1 faild: {0} ({1})\n'.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:\n") #sys.exit(1) raise RuntimeError('fork #2 faild: {0} ({1})\n'.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('\n') 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)
