如何編寫python的daemon程式
以前把守護程序與後臺任務搞混了,後面看了文章才知道這兩者的區別,寫此文表達自己對守護程序的理解.
1:什麼是守護程序?
所謂守護程序是一種是 Linux 的一種長期執行的後臺服務程序,httpd、named、sshd 等服務都是以守護程序 Daemon 方式執行的,通常服務名稱以字母d結尾,也就是 Daemon 第一個字母.
- 無需控制終端(不需要與使用者互動)
- 在後臺執行
- 生命週期比較長,一般是隨系統啟動和關閉
2:守護程序必要性
通常我們執行任務時是在前臺執行,佔領了當前終端,此時無法進行操作,就算我們添加了 &符號,將程式放到後臺,但也就因為終端斷網等問題,導致程式中斷。
所要知道的是:在目前的linux上,有了systemd這個服務,這個服務管理工具可以方便我們寫在後臺執行的程式,甚至可以代替這種守護程序。通過把寫服務的配置檔案,讓systemd監控我們的程式,可以隨系統啟動而執行,可以設定啟動條件,及其的方便。
3:程序組
$ ps -o pid,pgid,ppid,comm | cat PID PGID PPID COMMAND 10179 10179 10177 bash 10263 10263 10179 ps 10264 10263 10179 cat
- bash:程序和程序組ID都是 10179,父程序其實是 sshd(10177)
- ps:程序和程序組ID都是 10263,父程序是 bash(10179),因為是在 Shell 上執行的命令
- cat:程序組 ID 與 ps 的程序組 ID 相同,父程序同樣是 bash(10179)
4:會話組
多個程序構成一個程序組,而會話組是由多個程序組構建而。而程序組又被稱為job,會話有前臺作業,也會有後臺作業;一個會話可以有一個控制終端,當控制終端有輸入和輸出時都會傳遞給前臺程序組,比如Ctrl + Z。會話的意義在於能將多個作業通過一個終端控制,一個前臺操作,其它後臺執行。
那麼如何編寫守護程序呢?
其實編寫守護程序很簡單,只需要遵循一下幾點即可
1:建立子程序,父程序退出
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 49 49 49 pts/2 70 Ss 0 0:00 /bin/bash 49 70 70 49 pts/2 70 R+ 0 0:00 \_ ps axjf 0 17 17 17 pts/1 68 Ss 0 0:00 /bin/bash 17 68 68 17 pts/1 68 S+ 0 0:00 \_ python hello.py 68 69 68 17 pts/1 68 S+ 0 0:00 \_ python hello.py 0 1 1 1 pts/0 1 Ss+ 0 0:00 /bin/bash
程序 fork 後,父程序退出。這麼做的原因有 2 點:
如果守護程序是通過 Shell 啟動,父程序退出,Shell 就會認為任務執行完畢,這時子程序由 init 收養
子程序繼承父程序的程序組 ID,保證了子程序不是程序組組長,因為後邊呼叫setsid()要求必須不是程序組長
PGID就是程序所屬的Group的Leader的PID,如果PGID=PID,那麼該程序是Group Leader
2、子程序建立新會話
呼叫setsid()建立一個新的會話,併成為新會話組長。這個步驟主要是要與繼承父程序的會話、程序組、終端脫離關係。
那麼問題來了,為什麼程序組組長無法呼叫setsid()呢?
對於程序組長來說,程序組 ID 已經和 PID 相同了,如果它被允許呼叫setsid()的話,它的程序組 ID 會保持不變,會出現:
1:程序組長屬於新的會話;
2:老的程序組成員屬於舊的會話。
這樣情況變成了一個程序組的成員屬於不同的會話,Linux想要禁止這種情況的發生。
3、禁止子程序重新開啟終端
此刻子程序是會話組長,為了防止子程序重新開啟終端,再次 fork 後退出父程序,也就是此子程序。這時子程序 2 不再是會話組長,無法再開啟終端。其實這一步驟不是必須的,不過加上這一步驟會顯得更加嚴謹。
4、設定當前目錄為根目錄
如果守護程序的當前工作目錄是/usr/home目錄,那麼管理員在解除安裝/usr分割槽時會報錯的。為了避免這個問題,可以呼叫chdir()函式將工作目錄設定為根目錄/。
5、設定檔案許可權掩碼
檔案許可權掩碼是指遮蔽掉檔案許可權中的對應位。由於使用 fork()函式新建的子程序繼承了父程序的檔案許可權掩碼,這就給該子程序使用檔案帶來了諸多的麻煩。因此,把檔案許可權掩碼設定為 0,可以大大增強該守護程序的靈活性。通常使用方法是umask(0)。
6、關閉檔案描述符
子程序會繼承已經開啟的檔案,它們佔用系統資源,且可能導致所在檔案系統無法解除安裝。此時守護程序與終端脫離,常說的輸入、輸出、錯誤描述符也應該關閉,畢竟這個時候也不會使用終端了。
守護程序的出錯處理
由於守護程序脫離了終端,不能將錯誤資訊輸出到控制終端,即使 gdb 也無法正常除錯。常用的方法是使用 syslog 服務,將錯誤資訊輸入到/var/log/messages中。
syslog 是 Linux 中的系統日誌管理服務,通過守護程序 syslogd 來維護。該守護程序在啟動時會讀一個配置檔案/etc/syslog.conf。該檔案決定了不同種類的訊息會發送向何處。
程式碼展示
import os import sys def daemonize(pid_file=None): pid = os.fork() if pid: sys.exit(0) os.setsid() _pid = os.fork() if _pid: sys.exit(0) os.umask(0) os.chdir('/') sys.stdout.flush() sys.stderr.flush() with open('/dev/null') as read_null,open('/dev/null','w') as write_null: os.dup2(read_null.fileno(),sys.stdin.fileno()) os.dup2(write_null.fileno(),sys.stdout.fileno()) os.dup2(write_null.fileno(),sys.stderr.fileno()) if pid_file: with open(pid_file,'w+') as f: f.write(str(os.getpid())) if __name__ == "__main__": daemonize('test.txt')
關於os.dup2這個函式
os.dup2() 方法用於將一個檔案描述符 fd 複製到另一個 fd2。
Unix,Windows 上可用。
>>> import os >>> f = open("hello.txt","a") >>> os.dup2(f.fileno(),1) >>> f.close() >>> print("hello world") >>> print("changed") cat hello.txt 1 hello world changed
附加話題
為什麼伺服器端常常fork兩次呢?
因為這是為了避免產生殭屍程序。
當我們只fork()一次後,存在父程序和子程序。這時有兩種方法來避免產生殭屍程序:
- 父程序呼叫waitpid()等函式來接收子程序退出狀態。
- 父程序先結束,子程序則自動託管到Init程序(pid = 1)。
目前先考慮子程序先於父程序結束的情況:
- 若父程序未處理子程序退出狀態,在父程序退出前,子程序一直處於殭屍程序狀態。
- 若父程序呼叫waitpid()(這裡使用阻塞呼叫確保子程序先於父程序結束)來等待子程序結束,將會使父程序在呼叫waitpid()後進入睡眠狀態,只有子程序結束父程序的waitpid()才會返回。 如果存在子程序結束,但父程序還未執行到waitpid()的情況,那麼這段時期子程序也將處於殭屍程序狀態。
由此,可以看出父程序與子程序有父子關係,除非保證父程序先於子程序結束或者保證父程序在子程序結束前執行waitpid(),子程序均有機會成為殭屍程序。那麼如何使父程序更方便地建立不會成為殭屍程序的子程序呢?這就要用兩次fork()了。
父程序一次fork()後產生一個子程序隨後立即執行waitpid(子程序pid,NULL,0)來等待子程序結束,然後子程序fork()後產生孫子程序隨後立即exit(0)。這樣子程序順利終止(父程序僅僅給子程序收屍,並不需要子程序的返回值),然後父程序繼續執行。這時的孫子程序由於失去了它的父程序(即是父程序的子程序),將被轉交給Init程序託管。於是父程序與孫子程序無繼承關係了,它們的父程序均為Init,Init程序在其子程序結束時會自動收屍,這樣也就不會產生殭屍程序了。
以上就是如何編寫python的daemon程式的詳細內容,更多關於python的daemon程式的資料請關注我們其它相關文章!