守護進程在Linux/Unix系統中有著廣泛的應用。有時,開發人員也想把自己的程序變成守護進程。在創建一個守護進程的時候,要接觸到子進程、進程組、會晤期、信號機制、文件、目錄和控制終端等多個概念。因此守護進程還是比較復雜的,在這裡詳細地討論Linux/Unix的守護進程的編寫,總結出八條經驗,並給出應用范例。
編程要點
1.屏蔽一些有關控制終端操作的信號。防止在守護進程沒有正常運轉起來時,控制終端受到干擾退出或掛起。示例如下:
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
所有的信號都有自己的名字。這些名字都以“SIG”開頭,只是後面有所不同。開發人員可以通過這些名字了解到系統中發生了什麼事。當信號出現時,開發人員可以要求系統進行以下三種操作:
◆ 忽略信號。大多數信號都是采取這種方式進行處理的,這裡就采用了這種用法。但值得注意的是對SIGKILL和SIGSTOP信號不能做忽略處理。
◆ 捕捉信號。最常見的情況就是,如果捕捉到SIGCHID信號,則表示子進程已經終止。然後可在此信號的捕捉函數中調用waitpid()函數取得該子進程的進程ID和它的終止狀態。另外,如果進程創建了臨時文件,那麼就要為進程終止信號SIGTERM編寫一個信號捕捉函數來清除這些臨時文件。
◆ 執行系統的默認動作。對絕大多數信號而言,系統的默認動作都是終止該進程。
對這些有關終端的信號,一般采用忽略處理,從而保障了終端免受干擾。
這類信號分別是,SIGTTOU(表示後台進程寫控制終端)、SIGTTIN(表示後台進程讀控制終端)、SIGTSTP(表示終端掛起)和SIGHUP(進程組長退出時向所有會議成員發出的)。
2.將程序進入後台執行。由於守護進程最終脫離控制終端,到後台去運行。方法是在進程中調用fork使父進程終止,讓Daemon在子進程中後台執行。這就是常說的“脫殼”。子進程繼續函數fork()的定義如下:
#include
#include
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
void syslog(int priority,char *format,...);
其中參數priority指明了進程要寫入信息的等級和用途。第二個參數是一個格式串,指定了記錄輸出的格式。在這個串的最後需要指定一個%m,對應errno錯誤碼。
應用范例
下面給出Linux下編程的守護進程的應用范例,在UNIX中,不同版本實現的細節可能不一致,但其實現的原則是與Linux一致的。
#include
#include
#include
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下編譯通過。這個程序比較簡單,但基本體現了守護進程的編程要點。讀者針對實際應用中不同的需要,還可以做相應的調整。