編寫Linux/Unix守護進程

守護進程在Linux/Unix系統中有着廣泛的應用。有時,開發人員也想把自己的程序變成守護進程。在創建一個守護進程的時候,要接觸到子進程、進程組、會晤期、信號機制、文件、目錄和控制終端等多個概念。因此守護進程還是比較複雜的,在這裏詳細地討論Linux/Unix的守護進程的編寫,總結出八條經驗,並給出應用範例。


編程要點

    1.屏蔽一些有關控制終端操作的信號。防止在守護進程沒有正常運轉起來時,控制終端受到干擾退出或掛起。示例如下:

 signal(SIGTSTP,SIG_IGN); //表示終端掛起
signal(SIGINT,  SIG_IGN);// 終端中斷  
signal(SIGHUP,  SIG_IGN);// 進程組長退出時向所有會議成員發出的  
signal(SIGQUIT, SIG_IGN);// 終端退出  
signal(SIGPIPE, SIG_IGN);// 向無讀進程的管道寫數據 
signal(SIGTTOU, SIG_IGN);// 後臺程序嘗試寫操作  
signal(SIGTTIN, SIG_IGN);// 後臺程序嘗試讀操作  
signal(SIGTERM, SIG_IGN);// 終止


    所有的信號都有自己的名字。這些名字都以“SIG”開頭,只是後面有所不同。開發人員可以通過這些名字瞭解到系統中發生了什麼事。當信號出現時,開發人員可以要求系統進行以下三種操作:
    ◆ 忽略信號。大多數信號都是採取這種方式進行處理的,這裏就採用了這種用法。但值得注意的是對SIGKILL和SIGSTOP信號不能做忽略處理。
    ◆ 捕捉信號。最常見的情況就是,如果捕捉到SIGCHID信號,則表示子進程已經終止。然後可在此信號的捕捉函數中調用waitpid()函數取得該子進程的進程ID和它的終止狀態。另外,如果進程創建了臨時文件,那麼就要爲進程終止信號SIGTERM編寫一個信號捕捉函數來清除這些臨時文件。
    ◆ 執行系統的默認動作。對絕大多數信號而言,系統的默認動作都是終止該進程。

    對這些有關終端的信號,一般採用忽略處理,從而保障了終端免受干擾。

    2.將程序進入後臺執行。由於守護進程最終脫離控制終端,到後臺去運行。方法是在進程中調用fork使父進程終止,讓Daemon在子進程中後臺執行。這就是常說的“脫殼”。子進程繼續函數fork()的定義如下:

#include <sys/types.h>
#include <unistd.h>
 pid_t fork(void);


    該函數是Linux/Unix編程中非常重要的函數。它被調用一次,但返回兩次。這兩次返回的區別是子進程的返回值爲“0”,而父進程的返回值爲子進程的ID。如果出錯則返回“-1”。

    3.脫離控制終端、登錄會話和進程組。開發人員如果要擺脫它們,不受它們的影響,一般使用 setsid() 設置新會話的領頭進程,並與原來的登錄會話和進程組脫離。這只是其中的一種方法,也有如下處理的辦法:

if  ((fd = open("/dev/tty",O_RDWR)) >= 0) { 
ioctl(fd,TIOCNOTTY,NULL); 
close(fd); 
}


    其中/dev/tty是一個流設備,也是終端映射,調用close()函數將終端關閉。

    4.禁止進程重新打開控制終端。進程已經成爲無終端的會話組長,但它可以重新申請打開一個控制終端。開發人員可以通過不再讓進程成爲會話組長的方式來禁止進程重新打開控制終端,需要再次調用fork函數。
    上面的程序代碼表示結束第一子進程,第二子進程繼續(第二子進程不再是會話組長)。

    5. 關閉打開的文件描述符,並重定向標準輸入、標準輸出和標準錯誤輸出的文件描述符。進程從創建它的父進程那裏繼承了打開的文件描述符。如果不關閉,將會浪費系統資源,引起無法預料的錯誤。關閉三者的代碼如下:

for (fd = 0, fdtablesize = getdtablesize(); 
 fd < fdtablesize; fd++) 
  close(fd);


    但標準輸入、標準輸出和標準錯誤輸出的重定向是可選的。也許有的程序想保留標準輸入(0)、標準輸出(1)和標準錯誤輸出(2),那麼循環應繞過這三者。代碼如下:

for (fd =3, fdtablesize = getdtablesize();
fd < fdtablesize; fd++) 
  close(fd);


    有的程序有些特殊的需求,還需要將這三者重新定向。示例如下:

error=open("/tmp/error",O_WRONLY|O_CREAT,
0600);
  dup2(error,2);
 close(error);
 in=open("/tmp/in",O_RDONLY|O_CREAT,0600);
 if(dup2(in,0)==-1)  perror("in");
 close(in);
out=open("/tmp/out",O_WRONLY|O_CREAT,0600);
 if(dup2(out,1)==-1) perror("out");
 close(out);


    6.改變工作目錄到根目錄或特定目錄進程活動時,其工作目錄所在的文件系統不能卸下。

    一般需要將工作目錄改變到根目錄或特定目錄,注意用戶對此目錄需要有讀寫權。防止超級用戶卸載設備時系統報告設備忙。

    7.處理SIGCHLD信號。SIGCHLD信號是子進程結束時,向內核發送的信號。

如果父進程不等待子進程結束,子進程將成爲殭屍進程(zombie)從而佔用系統資源。因此需要對SIGCHLD信號做出處理,回收殭屍進程的資源,避免造成不必要的資源浪費。可以用如下語句:
    signal(SIGCHLD,(void *)reap_status);

    捕捉信號SIGCHLD,用下面的函數進行處理:

void reap_status() 
 { int pid; 
   union wait status; 
   while ((pid = wait3(&status,WNOHANG,NULL)) > 0) 
  …… }


    8.在Linux/Unix下有個syslogd的守護進程,向用戶提供了syslog()系統調用。任何程序都可以通過syslog記錄事件。

    由於syslog非常好用和易配置,所以很多程序都使用syslog來發送它們的記錄信息。一般守護進程也使用syslog向系統輸出信息。syslog有三個函數,一般只需要用syslog(...)函數,openlog()/closelog()可有可無。syslog()在shslog.h定義如下:

#include <syslog.h>
void syslog(int priority,char *format,...);


    其中參數priority指明瞭進程要寫入信息的等級和用途。第二個參數是一個格式串,指定了記錄輸出的格式。在這個串的最後需要指定一個%m,對應errno錯誤碼。

    應用範例

    下面給出Linux下編程的守護進程的應用範例,在UNIX中,不同版本實現的細節可能不一致,但其實現的原則是與Linux一致的。

#include <stdio.h> 
#include <signal.h> 
#include <sys/file.h> 
main(int argc,char **argv)
{
  time_t now;
  int childpid,fd,fdtablesize;
  int error,in,out;
  /* 忽略終端 I/O信號,STOP信號 */
 signal(SIGTTOU,SIG_IGN);
 signal(SIGTTIN,SIG_IGN);
  signal(SIGTSTP,SIG_IGN); 
  signal(SIGHUP ,SIG_IGN);
  /* 父進程退出,程序進入後臺運行 */
  if(fork()!=0) exit(1);
   if(setsid()<0)exit(1);/* 創建一個新的會議組 */ 
  /* 子進程退出,孫進程沒有控制終端了 */  
  if(fork()!=0) exit(1);
  if(chdir("/tmp")==-1)exit(1);
/* 關閉打開的文件描述符,包括標準輸入、標準輸出和標準錯誤輸出 */ 
 for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) 
   close(fd);
   umask(0);/*重設文件創建掩模 */ 
   signal(SIGCHLD,SIG_IGN);/* 忽略SIGCHLD信號 */ 
/*打開log系統*/
  syslog(LOG_USER|LOG_INFO,"守護進程測試!\n");  
   while(1)  
   {  
    time(&now);
   syslog(LOG_USER|LOG_INFO,"當前時間:\t%s\t\t\n",ctime(&now));
    sleep(6);
     }  
 }


    此程序在Turbo Linux 4.0下編譯通過。這個程序比較簡單,但基本體現了守護進程的編程要點。讀者針對實際應用中不同的需要,還可以做相應的調整。


創建守護進程的一般步驟

 

(1) 創建子進程,退出父進程

爲了脫離控制終端需要退出父進程,之後的工作都由子進程完成。在Linux中父進程先於子進程退出會造成子進程成爲孤兒進程,而每當系統發現一個孤兒進程時,就會自動由1號進程(init)收養它,這樣,原先的子進程就會變成init進程的子進程。

ps –ef | grep ProcName          通過PID/PPID查看進程的父子關係

 

(2) 在子進程中創建新的會話

使用系統函數setsid來完成。

man 2 setsid    查看關於setsid函數的說明

setsid – creates a session and sets theprocess group ID

#include <unistd.h>

pid_t setsid(void);

setsid() creates a new session if thecalling process is not a process group leader. The calling process is theleader of the new session, the process group leader of the new process group,and has no controlling tty. The process group ID and session ID of the callingprocess are set to the PID of the calling process. The calling process will bethe only process in this new process group and in this new session.

進程組:是一個或多個進程的集合。進程組有進程組ID來唯一標識。除了進程號PID之外,進程組ID也是一個進程的必備屬性。每個進程組都有一個組長進程,其組長進程的進程號等於進程組ID,且該進程組ID不會因組長進程的退出而受到影響。

setsid函數作用:用於創建一個新的會話,並擔任該會話組的組長。調用setsid有3個作用

(a) 讓進程擺脫原會話的控制;

(b) 讓進程擺脫原進程組的控制;

(c) 讓進程擺脫原控制終端的控制;

使用setsid函數的目的:由於創建守護進程的第一步調用了fork函數來創建子進程再將父進程退出。由於在調用fork函數時,子進程拷貝了父進程的會話期、進程組、控制終端等,雖然父進程退出了,但會話期、進程組、控制終端等並沒有改變,因此,這還不是真正意義上的獨立開了。使用setsid函數後,能夠使進程完全獨立出來,從而擺脫其他進程的控制。

 

(3) 改變當前目錄爲根目錄

使用fork創建的子進程繼承了父進程的當前的工作目錄。由於在進程運行中,當前目錄所在的文件系統是不能卸載的,這對以後的使用會造成諸多的麻煩。因此,通常的做法是讓根目錄”/”作爲守護進程的當前工作目錄。這樣就可以避免上述的問題。如有特殊的需求,也可以把當前工作目錄換成其他的路徑。改變工作目錄的方法是使用chdir函數。

 

(4) 重設文件權限掩碼

文件權限掩碼:是指屏蔽掉文件權限中的對應位。例如,有個文件權限掩碼是050,它就屏蔽了文件組擁有者的可讀與可執行權限(對應二進制爲,rwx, 101)。由於fork函數創建的子進程繼承了父進程的文件權限掩碼,這就給子進程使用文件帶來了諸多的麻煩。因此,把文件權限掩碼設置爲0(即,不屏蔽任何權限),可以增強該守護進程的靈活性。設置文件權限掩碼的函數是umask。通常的使用方法爲umask(0)。

 

(5) 關閉文件描述符

用fork創建的子進程也會從父進程那裏繼承一些已經打開了的文件。這些被打開的文件可能永遠不會被守護進程讀寫,但它們一樣消耗系統資源,而且可能導致所在的文件系統無法卸載。在使用setsid調用之後,守護進程已經與所屬的控制終端失去了聯繫,因此從終端輸入的字符不可能達到守護進程,守護進程中用常規方法(如printf)輸出的字符也不可能在終端上顯示出來。所以,文件描述符爲0、1、2(即,標準輸入、標準輸出、標準錯誤輸出)的三個文件已經失去了存在的價值,也應該關閉。

 

(6) 守護進程退出處理

當用戶需要外部停止守護進程時,通常使用kill命令停止該守護進程。所以,守護進程中需要編碼來實現kill發出的signal信號處理,達到進程正常退出。


發佈了13 篇原創文章 · 獲贊 58 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章