標籤:sys.argv tar dup2 chdir 它的 輸出資料流 轉換 mask color
Python編寫守護進程程式思路
1. fork子進程,父進程退出
通常,我們執行服務端程式的時候都會通過終端串連到伺服器,成功串連後會載入shell環境,終端和shell都是進程,shell進程是終端進程的子進程,通過ps命令可以很容易的查看到。在這個shell環境下一開始執行的程式都是shell進程的子進程,自然會受到shell進程的影響。在程式裡fork子進程後,父進程退出,對了shell進程來說,這個父進程就算執行完了,而產生的子進程會被init進程接管,從而也就脫離了終端的控制。
2-4步驟的意義
守護進程必須與其運行前的環境隔離開來。這些環境包括未關閉的檔案描述符、控制終端、會話和進程組、工作目錄以及檔案建立掩碼等。
這些環境通常是守護進程從執行它的父進程(特別是shell)中繼承下來的。
2、修改子進程的工作目錄
子進程在建立的時候會繼承父進程的工作目錄,如果執行的程式是在u盤裡的,就會導致u盤不能卸載。比如Nginx就有它的預設工作目錄 /etc/nginx/conf.d/default.conf
3、建立進程組
使用setsid後,子進程就會成為新會話的首進程(session leader);子進程會成為新進程組的組長進程;子進程沒有控制終端。
4、修改umask
由於umask會屏蔽許可權,所以設定為0,這樣可以避免讀寫檔案時碰到許可權問題。
5、fork孫子進程,子進程退出
經過上面幾個步驟後,子進程會成為新的進程組老大,可以重新申請開啟終端,為了避免這個問題,fork孫子進程出來。
6、重新導向孫子進程的標準輸入資料流、標準輸出資料流、標準錯誤流到/dev/null
因為是守護進程,本身已經脫離了終端,那麼標準輸入資料流、標準輸出資料流、標準錯誤流就沒有什麼意義了。所以都轉向到/dev/null,就是都丟棄的意思。
守護進程的啟動方式有其特殊之處。它可以在系統啟動時從啟動指令碼/etc/rc.d中啟動,可以由inetd守護進程啟動,可以有作業規划進程crond啟動,
還可以由使用者終端(通常是shell)執行。
總之,除開這些特殊性以外,守護進程與普通進程基本上沒有什麼區別。
因此,編寫守護進程實際上是把一個普通進程按照上述的守護進程的特性改造成為守護進程。如果大家對進程的認識比較深入,就對守護進程容易理解和編程了。
Linux系統進程的一些概念
這裡主要是回答針對下面代碼的疑問,為什麼要FORK?為什麼要設定SID等。
這個“1”號進程就是所有進程的父進程,因為這是CentOS7它得啟動機制變化了,如果在CentOS6中那麼1號進程則是INIT進程。但不管怎麼作用是一樣的。
我們平時所理解的守護進程就是你在命令列執行一個程式它自己就在後台運行了,你退出了終端再進去它依然在運行就像Nginx那樣。首先我們要知道幾個概念
進程ID(PID):就是這個進程的進程號
父進程ID(PPID):該進程的父進程ID號
進程組ID(PGID):進程所在進程組ID,每一個進程都屬於一個進程組,一個進程組可以包含多個進程同時包含一個組長進程(如果進程ID和其對應的進程組ID相同則表示該進程是該組的組長)。比如一個程式是多進程的,運行該程式就會啟動多個進程,那麼這些進程都屬於一個進程組,因為你可以針對組來發送訊號,其實也就是管理。
會話ID(SID):當有新的使用者登入Linux時,登入進程會為這個使用者建立一個會話。使用者的登入shell就是會話的首進程。會話的首進程ID會作為整個會話的ID。會話是一個或多個進程組的集合,囊括了登入使用者的所有活動。
ps -axo pid,ppid,pgid,sid,tty,comm
pts/0是綁定到會話的一個終端裝置,這裡之所有有pts/1是因為我開了兩個串連到Linux的終端,都是通過SSH進行登入的。
pts/0的進程ID是29641,它得PPID和PGID都是一樣的,說明它就是進程組29641的組長,為什麼呢?因為我通過SSH登入,登入後啟動並執行第一個就是bash也就是和我進行命令互動的程式,所以你可以看到29641的父進程ID是29639它是一個sshd服務。
這裡為什麼有這麼多1172,上面的1172是守護進程,下面的29639是sshd服務派生出來的一個子進程用於負責一個使用者的串連,進程ID為1172的sshd它的父進程就是1.
交談群組
通常我們執行的命令屬於前端任務,也就是和會話綁定,如果會話消失了任務也就是消失了。我這裡執行一個ping操作,它會一直執行
我們在另外一個終端查看
它的父進程是29641,不就是我們上面的bash麼,而且它的SID也就是會話ID也是29641,因為它屬於哪個會話,如果哪個會話消失了,這個ping操作也可以叫做作業,也就是消失了。我們把那個執行ping命令的終端直接關閉,然後在另外的終端上查看,不一會你就看不到那個ping任務了。所以這就是會話。
其實無論是進程組還是會話都屬於作業控制。會話ID相同的進程只要會話消失,這些進程也就消失了,也就是結束了。
下面我們來說一下進程組
上面這一條命令其實運行了是兩個進程。我們在另外一個終端查看
bash的進程ID是30150,所以由它派生的子進程的父進程ID都是30150,就像下面的tailf和grep.這個不用多數,因為都是在那個會話也就是終端上執行的,所以他們三個的會話ID相同。大家可以看到tailf和grep的進程組ID相同,都是30374說明他們是在一個進程組中,而組長就是tailf的進程其ID為30374。
進程組ID相同我們就可以給進程組發訊號比如去結束這個組裡所有的進程。這還是作業管理的內容。如下面操作:
kill -SIGTERM 30374
另外一個終端的任務自動就結束了
如何判斷你自己當前是哪個終端呢?
關閉終端為什麼有些進程不退出呢?
通過SID的示範我們知道,命令列裡啟動並執行進程會依賴當前會話,所以進程的運行不受會話影響那麼肯定就要脫離之前的會話。另外還需要讓進程脫離當前進程可以理解為當前的bash也就是完全隔斷父子關係,因為畢竟我們是通過bash來啟動並執行程式,bash又依賴終端pts/N這種,如果bash沒了,進程也沒了。看
還是這個命令這回我們放到後台運行,
可以看到它倆的SID和bash的並不相同
但是這時候如果你關閉這個終端,這個任務也就沒了。你可以試一下。
完整代碼
# !/usr/bin/env python# coding: utf-8# python類比linux的守護進程import sys, os, time, atexit, stringfrom signal import SIGTERM__metaclass__ = typeclass Daemon: def __init__(self, pidfile="/tmp/Daemon.pid", stdin=‘/dev/null‘, stdout=‘/dev/null‘, stderr=‘/dev/null‘): # 需要擷取調試資訊,改為stdin=‘/dev/stdin‘, stdout=‘/dev/stdout‘, stderr=‘/dev/stderr‘,以root身份運行。 self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = pidfile self.applicationName = "Application" self._homeDir = "/" # 偵錯模式是否開啟 self._verbose = False # 使用者掩碼,預設為0 self._umask = 0 # 擷取守護進程掩碼 @property def umask(self): return self._umask # 設定守護進程掩碼 @umask.setter def umask(self, umask): self._umask = umask # 擷取當前是否是偵錯模式 @property def VerboseMode(self): return self._verbose # 偵錯模式開關,預設不是偵錯模式 @VerboseMode.setter def VerboseMode(self, verboseMode): self._verbose = verboseMode # 偵錯模式和非偵錯模式設定 def _verbosSwitch(self): # 偵錯模式是輸出日誌到指定檔案,這些檔案在對象初始化時指定 if self._verbose: pass # self.stdin = ‘/dev/stdin‘ # self.stdout = ‘/dev/stdout‘ # self.stderr = ‘/dev/stderr‘ else: self.stdin = ‘/dev/null‘ self.stdout = ‘/dev/null‘ self.stderr = ‘/dev/null‘ def setApplicationName(self, appName): self.applicationName = appName # 擷取和設定進程住目錄 @property def HomeDir(self): return self._homeDir @HomeDir.setter def HomeDir(self, homeDir): self._homeDir = homeDir # 這個方法的主要目的就是脫離主體,為進程創造環境 def _daemonize(self): # 第一步 try: # 第一次fork,產生子進程,脫離父進程,它會返回兩次,PID如果等於0說明是在子進程裡面,如果大於0說明當前是在父進程裡 pid = os.fork() # 如果PID大於0,說明當前在父進程裡,然後sys.exit(0),則是退出父進程,此時子進程還在運行。 if pid > 0: # 退出父進程,此時linux系統的init將會接管子進程 sys.exit(0) except OSError, e: sys.stderr.write(‘fork #1 failed: %d (%s)\n‘ % (e.errno, e.strerror)) sys.exit(1) # 第二、三、四步 os.chdir("/") # 修改進程工作目錄 os.setsid() # 設定新的會話,子進程會成為新會話的首進程,同時也產生一個新的進程組,該進程組ID與會話ID相同 os.umask(self._umask) # 重新設定檔案建立許可權,也就是工作目錄的umask # 第五步 try: # 第二次fork,禁止進程開啟終端,相當於是子進程有派生一個子進程 pid = os.fork() if pid > 0: # 子進程退出,孫子進程運行,此時孫子進程由init進程接管,在CentOS 7中是Systemed。 sys.exit(0) except OSError, e: sys.stderr.write(‘fork #2 failed: %d (%s)\n‘ % (e.errno, e.strerror)) sys.exit(1) # 第六步 # 把之前的刷到硬碟上 sys.stdout.flush() sys.stderr.flush() # 重新導向標準檔案描述符 si = file(self.stdin, ‘r‘) so = file(self.stdout, ‘a+‘) se = file(self.stderr, ‘a+‘, 0) # os.dup2可以原子化的開啟和複製描述符,功能是複製檔案描述符fd到fd2, 如果有需要首先關閉fd2. 在unix,Windows中有效。 # File的 fileno() 方法返回一個整型的檔案描述符(file descriptor FD 整型) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # 註冊退出函數,根據檔案pid判斷是否存在進程 atexit.register(self.delpid) pid = str(os.getpid()) file(self.pidfile, ‘w+‘).write(‘%s\n‘ % pid) # 程式退出後移除PID檔案 def delpid(self): os.remove(self.pidfile) def start(self, *args, **kwargs): # 檢查pid檔案是否存在以探測是否存在進程 try: pid = self._getPid() except IOError: pid = None # 如果PID存在,則說明進程沒有關閉。 if pid: message = ‘pidfile %s already exist. Process already running!\n‘ sys.stderr.write(message % self.pidfile) # 程式退出 sys.exit(1) # 構造進程環境 self._daemonize() # 執行具體任務 self._run(*args, **kwargs) def stop(self): # 從pid檔案中擷取pid try: pid = self._getPid() except IOError: pid = None # 如果程式沒有啟動就直接返回不在執行 if not pid: message = ‘pidfile %s does not exist. Process not running!\n‘ sys.stderr.write(message % self.pidfile) return # 殺進程 try: while 1: # 發送訊號,殺死進程 os.kill(pid, SIGTERM) time.sleep(0.1) message = ‘Process is stopped.\n‘ sys.stderr.write(message) except OSError, err: err = str(err) if err.find(‘No such process‘) > 0: if os.path.exists(self.pidfile): os.remove(self.pidfile) else: print str(err) sys.exit(1) # 擷取PID def _getPid(self): try: # 讀取儲存PID的檔案 pf = file(self.pidfile, ‘r‘) # 轉換成整數 pid = int(pf.read().strip()) # 關閉檔案 pf.close() except IOError: pid = None except SystemExit: pid = None return pid # 重啟的功能就是殺死之前的進程,然後再運行一個 def restart(self, *args, **kwargs): self.stop() self.start(*args, **kwargs) # 擷取精靈運行狀態 def status(self): try: pid = self._getPid() except IOError: pid = None if not pid: message = "No such a process running.\n" sys.stderr.write(message) else: message = "The process is running, PID is %s .\n" sys.stderr.write(message % str(pid)) def _run(self, *args, **kwargs): """ 這裡是孫子進程需要做的事情,你可以繼承這個類,然後重寫這裡的代碼,上面其他的都可以不做修改 """ while True: """ print 等於調用 sys.stdout.write(), sys.stdout.flush()是立即重新整理輸出。正常情況下如果是輸出到控制台那麼會立即輸出 但是重新導向到一個檔案就不會了,因為等於寫檔案,所以需要進行重新整理進行立即輸出。 下面使用print 還是 write都是一樣的。 """ # print ‘%s:hello world\n‘ % (time.ctime(),) sys.stdout.write(‘%s:hello world\n‘ % (time.ctime(),)) sys.stdout.flush() time.sleep(2)if __name__ == ‘__main__‘: daemon = Daemon(‘/tmp/watch_process.pid‘, stdout=‘/tmp/watch_stdout.log‘) if len(sys.argv) == 2: if ‘start‘ == sys.argv[1]: daemon.setApplicationName(sys.argv[0]) daemon.start() elif ‘stop‘ == sys.argv[1]: daemon.stop() elif ‘restart‘ == sys.argv[1]: daemon.restart() elif ‘status‘ == sys.argv[1]: daemon.status() else: print ‘unknown command‘ sys.exit(2) sys.exit(0) else: print ‘usage: %s start|stop|restart|status‘ % sys.argv[0] sys.exit(2)
Python編寫守護進程程式