linux守護進程、SIGHUP與nohup詳解

前端時間幫忙定位個問題。docker容器故障恢復後,其中的keepalived進程始終沒法啓動,也看不到Keepalived的日誌。前端

strace 查看系統調用以後,發現了緣由所在linux

 1 socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
 2 connect(3, {sa_family=AF_LOCAL, sun_path="/dev/log"}, 110) = -1 ENOENT (No such file or directory)
 3 close(3)                                = 0
 4 open("/var/run/keepalived.pid", O_RDONLY) = 3
 5 fstat(3, {st_mode=S_IFREG|0644, st_size=1, ...}) = 0
 6 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe85ab1b000
 7 read(3, "\n", 4096)                     = 1
 8 read(3, "", 4096)                       = 0
 9 close(3)                                = 0
10 munmap(0x7fe85ab1b000, 4096)            = 0
11 kill(0, SIG_0)                          = 0
12 socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
13 connect(3, {sa_family=AF_LOCAL, sun_path="/dev/log"}, 110) = -1 ENOENT (No such file or directory)
14 close(3)                                = 0
15 exit_group(0)                           = ?
16 +++ exited with 0 +++

這就是一個典型的linux單例守護進程啓動作的事情:檢測進程是否已經存在(判斷記錄文件是否存在以及對應pid進程是否還在執行),並經過syslog套接字文件向syslog服務端發送日誌。docker

很顯然,Keepalived沒法正常啓動是故障宕機時,相應的pid文件沒有清理乾淨,若是僅僅如此,Keepalived應該能夠啓動,通常守護進程啓動都會覆蓋殘留的鎖文件,問題關鍵在read(3, "\n", 4096) : 鎖文件Keepalived.pid是空的!! 而kil 向進程0 發送信號0,執行成功,則Keepalived認爲已經有Keepalived進程正在運行。因此問題出在鎖文件存在且內容爲"\n",故依次清理 keepalived.pid vrrp.pid checkers.pid文件後,Keepalived正常啓動。至於定位爲什麼鎖文件內容爲"\n",那是後話了。shell

經此一事,筆者想寫一寫Linux 守護進程bash

守護進程特色與相關概念

並不是運行時間長的程序便是守護進程,筆者並未找到守護進程最標準的定義,但 守護進程都有下面幾個特色:
一、沒有控制終端,終端名設置爲?號:也就意味着沒有 stdin 0 、stdout 一、stderr 2
二、父進程不是用戶建立的進程,init進程或者systemd(pid=1)以及用戶人爲啓動的用戶層進程通常以pid=1的進程爲父進程,而以kthreadd內核進程建立的守護進程以kthreadd爲父進程
三、守護進程通常是會話首進程、組長進程。
四、工做目錄爲 \ (根),主要是爲了防止佔用磁盤致使沒法卸載磁盤
 
這裏涉及到一些概念,是unix爲了更好管理進程間的關係提出的概念和方法,稍作說明下

控制終端

經過網絡登陸或者終端登陸創建的會話,會分配惟一一個tty終端或者pts僞終端(網絡登陸),實際上它們都是虛擬的,以文件的形式創建在/dev目錄,而並不是實際的物理終端。網絡

在終端中按下的特殊按鍵:中斷鍵(ctrl+c)、退出鍵(ctrl+\)、終端掛起鍵(ctrl + z)會發送給當前終端鏈接的會話中的前臺進程組中的全部進程socket

在網絡登陸程序中,登陸認證守護程序 fork 一個進程處理鏈接,並以ptys_open 函數打開一個僞終端設備(文件)得到文件句柄,並將此句柄複製到子進程中做爲標準輸入、標準輸出、標準錯誤,因此位於此控制終端進程下的全部子進程將能夠持有終端函數

與控制終端相連的會話首進程也叫控制進程spa

進程組

進程組是一個或者多個進程的集合。通常由某個程序fork出一個家族來構成進程組,或者由管道命令創建做業構成進程組。3d

同一個進程組中的全部進程接收來自同一終端的信號。

進程組中的第一個進程做爲進程組的首長,進程組id取首長進程的id。在各個進程中,經過函數getpgrp獲取其所屬進程組id

孤兒進程組

一個進程的父進程終止後,進程變成了孤兒進程,將被pid爲1的進程(init進程或者systemd)收養。

而對孤兒進程組的定義是:進程組中每一個進程的父進程要麼在組中,也麼不在該組所在會話中。

換言之,若是一個進程組中進程的父進程若是是組中成員,或者是init、systemd進程的話,這個進程組就必定是孤兒進程組。這樣的進程組是很常見的,下圖就是一個簡單且典型的孤兒進程組

很顯然,只有一個進程的進程組,而且是孤兒進程的話,進程組將變成孤兒進程組(哪怕它只有一個進程)。

典型的例子是一個父進程fork子進程以後,父進程當即退出,這樣子進程所在的進程組將變爲孤兒進程組。這樣的孤兒進程組中的每一箇中止(Stopped)狀態的每一個進程都將收到掛斷信號(SIGHUP),而後又當即收到繼續信號(SIGCONT)。因此fork子進程以後,退出父進程,若是子進程還須要繼續運行,則須要處理掛斷信號,不然進程對掛斷信號的默認處理將是退出。

此時的孤兒進程組並無變爲後臺進程,一些博客將後臺進程說成是孤兒進程組的一個特色,筆者認爲是不正確的,在他們的示例中,孤兒進程組變爲後臺進程的緣由是:父進程退出後,子進程在運行時向自身發送了SIGTSTP信號,這就像在終端按下終端掛起鍵(ctrl+z)同樣,暫時斷開了進程與控制終端的鏈接,天然變成了後臺進程。

因此這是將進程轉到後臺運行的一個手段,但並不能建立守護進程,後面會將怎麼建立守護進程。

會話

表示一個或多個進程組的集合,在有控制終端的會話中,能夠被分爲一個前臺進程組和多個後臺進程組。

取首進程id爲會話id。

函數getsid用來獲取會話id,而函數setsid用來新建一個會話,只有非首長進程(非進程組的組長)才能調用setsid新建會話。實際上setsid作了三件事

  • 設置當前進程的會話id爲該進程id,此進程成爲會話首進程。
  • 將調用setsid的進程設置爲一個新進程組的首長進程。
  • 斷開已鏈接的控制終端

這三步是建立守護進程的重要步驟。

下圖結合了筆者對這些概念的理解,作出的判斷

 

守護進程的建立

建立守護進程有標準的步驟:
  1. 若是是單例守護進程,結合鎖文件和kill函數檢測是否有進程已經運行
  2. umask取消進程自己的文件掩碼設置,也就是設置Linux文件權限,通常設置爲000,這是爲了防止子進程建立建立一個不能訪問的文件(沒有正確分配權限)。此過程並不是必須,若是守護進程不會建立文件,也能夠不修改
  3. fork出子進程,父進程退出。這樣子進程必定不是組長進程(進程id不等於進程組id)
  4. 子進程調用setsid新建會話(使子進程變爲會話首進程、組長進程,並斷開終端)
  5. 若是是單例守護進程,將pid寫入到記錄鎖文件,通常爲/var/run/xxx.pid
  6. 切換工做目錄到根目錄,這是爲了防止佔用磁盤形成磁盤不能卸載。因此也能夠改到別的目錄,只要保證目錄所在磁盤不會中途卸載
  7. 重定向輸入輸入錯誤文件句柄,將其指向/dev/null。

前面提到,守護進程通常藉助記錄鎖文件來(文件存在而且文件內記錄的pid對應的進程依然活躍)判斷是否已經有進程存在。

多數守護進程並不本身維護日誌文件,而是統一將日誌輸出給遵循syslog協議的日誌進程(如:rsyslogd)處理,統一將日誌輸出至 /var/log/messages,固然這些日誌進程也是能夠配置的。

並且守護進程由於是沒有終端的後臺進程,因此係統不會發送一些跟終端相關的信號給守護進程,程序能夠經過捕捉這些只有可能人爲發送的信號,來處理一些事情,好比處理SIGHUP來動態更新程序配置就是典型例子。下面的代碼演示瞭如何建立一個守護進程。

  1 #include <stdio.h>
  2 #include <syslog.h>
  3 #include <errno.h>
  4 #include <unistd.h>
  5 #include <stdlib.h>
  6 #include <fcntl.h>
  7 #include <signal.h>
  8 #include <sys/types.h>
  9 #include <sys/stat.h>
 10 #include <sys/resource.h>
 11 
 12 #define PID_FILE "/var/run/sampled.pid"
 13 
 14 int sampled_running(){
 15     FILE * pidfile = fopen(PID_FILE,"r");
 16     pid_t pid;
 17     int ret ;
 18 
 19     if (! pidfile) {
 20         return 0;
 21     }
 22 
 23     ret = fscanf(pidfile,"%d",&pid);
 24     if (ret == EOF && ferror(pidfile) != 0){
 25         syslog(LOG_INFO,"Error open pid file %s",PID_FILE);
 26     }
 27 
 28     fclose(pidfile);
 29     
 30     // 檢測進程是否存在
 31     if ( kill(pid , 0 ) ){
 32         syslog(LOG_INFO,"Remove a zombie pid file %s", PID_FILE);
 33         unlink(PID_FILE);
 34         return 0;
 35     }
 36 
 37     return pid;
 38 }
 39 
 40 pid_t sampled(){
 41     pid_t pid;
 42     struct rlimit rl;
 43     int fd,i;
 44 
 45     // 建立子進程,並退出當前父進程
 46     if((pid = fork()) < 0){
 47         syslog(LOG_INFO,"sampled : fork error");
 48         return -1;
 49     }
 50     if ( pid != 0) {
 51         //  父進程直接退出
 52         exit(0);
 53     }
 54 
 55     // 新建會話,成功返回值是會話首進程id,進程組id ,首進程id
 56     pid = setsid();
 57 
 58     if ( pid < -1 ){
 59         syslog(LOG_INFO,"sampled : setsid error");
 60         return -1;
 61     }
 62 
 63     // 將工做目錄切換到根目錄
 64     if ( chdir("/") < 0 ) {
 65         syslog(LOG_INFO,"sampled : chidr error");
 66         return -1;
 67     }
 68 
 69     // 關閉全部打開的句柄,若是肯定父進程未打開過句柄,此步能夠不作
 70     if ( rl.rlim_max == RLIM_INFINITY ){
 71         rl.rlim_max = 1024;
 72     }
 73     for(i = 0 ; i < rl.rlim_max; i ++) {
 74         close(i);
 75     }
 76 
 77     // 重定向輸入輸出錯誤
 78     fd = open("/dev/null",O_RDWR,0);
 79     if(fd != -1){
 80         dup2(fd,STDIN_FILENO);
 81         dup2(fd,STDOUT_FILENO);
 82         dup2(fd,STDERR_FILENO);
 83         if (fd > 2){
 84             close(fd);
 85         }
 86     }
 87     
 88     // 消除文件掩碼
 89     umask(0);
 90     return 0;
 91 }
 92 
 93 int pidfile_write(){
 94     // 這裏不用fopen直接打開文件是不想建立666權限的文件
 95     FILE * pidfile = NULL;
 96     int pidfilefd = creat(PID_FILE,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
 97     if(pidfilefd != -1){
 98         pidfile = fdopen(pidfilefd,"w");
 99     }
100 
101     if (! pidfile){
102         syslog(LOG_INFO,"pidfile write : can't open pidfile:%s",PID_FILE);
103         return 0;
104     }
105     fprintf(pidfile,"%d",getpid());
106     fclose(pidfile);
107     return 1;
108 }
109 
110 int main(){
111     int err,signo;
112     sigset_t mask;
113 
114     if (sampled_running() > 0 ){
115          exit(0);
116     }
117 
118     if ( sampled() != 0 ){
119 
120     }
121     // 寫記錄鎖文件  
122     if (pidfile_write() <= 0) {
123         exit(0);
124     }
125 
126     while(1) {
127         // 捕捉信號
128         err = sigwait(&mask,&signo);
129         if( err != 0  ){
130             syslog(LOG_INFO,"sigwait error : %d",err);
131             exit(1);
132         }
133         switch (signo){
134             default :
135                 syslog(LOG_INFO,"unexpected signal %d \n",signo);
136                 break;
137             case SIGTERM:
138                 syslog(LOG_INFO,"got SIGTERM. exiting");
139                 exit(0);
140         }
141 
142     }
143 
144 }

程序編譯運行結果,能夠看到pid  、進程組id、會話id是同樣的,沒有終端,而且直接由pid爲1的進程接管。此時的進程已經成爲一個守護進程。

 

sighup與nohup

sighup(掛斷)信號在控制終端或者控制進程死亡時向關聯會話中的進程發出,默認進程對SIGHUP信號的處理時終止程序,因此咱們在shell下創建的程序,在登陸退出鏈接斷開以後,會一併退出。

nohup,故名思議就是忽略SIGHUP信號,通常搭配& 一塊兒使用,&表示將此程序提交爲後臺做業或者說後臺進程組。執行下面的命令

nohup bash -c "tail -f /var/log/messages | grep sys" &

nohup與&啓動的程序, 在終端還未關閉時,徹底不像傳統的守護進程,由於其不是會話首進程且持有終端,只是其忽略了SIGHUP信號

從nohup源碼就能夠看到,其實nohup只作了3件事情

  1. dofile函數將輸出重定向到nohup.out文件
  2. signal函數設置SIGHUP信號處理函數爲SIG_IGN宏(指向sigignore函數),以此忽略SIG_HUP信號
  3. execvp函數用新的程序替換當前進程的代碼段、數據段、堆段和棧段。

execvp 函數執行後,新程序(並無fork進程)會繼承一些調用進程屬性,好比:進程id、會話id,控制終端等

登陸鏈接斷開以後

在終端關閉後,nohup起到相似守護進程的效果,可是跟傳統的守護進程仍是有區別的

一、nohup建立的進程工做目錄是你執行命令時所在的目錄
二、0 1 2 標準輸入 標準輸出 標準錯誤  指向nohup.out文件
三、nohup建立的進程組中,除首長進程的父進程id變爲1以外,其他進程依然保留原來的會話id、進程組id、父進程id,都保持不變
相關文章
相關標籤/搜索