標籤:linux核心
大體步驟如下
1. 調用fork()函數建立子進程後,讓父進程立即exit(),這樣產生的子進程變成孤兒進程,由init進程接管。
2. 調用setsid()函數,使得新建立的進程脫離控制終端,同時建立新的進程組,並成為該進程組的首進程。在linux系統中,所有的進程都屬於各自的進程組,進程組是一個或多個進程的集合,一個進程組中至少有一個進程成員,否則就消亡了。每個進程組都有一個進程組ID,是由領頭進程的進程號決定的,會話則是一個或多個進程組的集合,每個會話都有一個領頭進程,會話和進程組是linux核心用於管理多使用者情況下使用者進程的方法,每個進程都屬於一個進程組,而進程組又屬於某個會話,當使用者從終端登入系統時,系統會建立一個新的會話,在該終端上啟動的進程都會被系統劃歸到會話的進程組中。會話中的進程通過該會話中的領頭進程與一個終端相連,該終端是會話的控制終端,一個會話只能有一個控制終端,如果會話存在一個控制終端時,則它必然擁有一個前台進程組,屬於該組的進程可以從從控制終端獲得輸入。由於守護進程沒有控制終端,而使用fork()函數建立的子進程會繼承父進程的控制終端,會話和進程組,因此,必須用setsid()建立新的會話,以脫離父進程的影響。Setsid函數將建立新的會話,並使得調用setsid函數的進程成為新會話的領頭進程。調用setsid函數的進程是新建立會話中的唯一的進程組,進程組ID為調用進程的進程號。Setsid函數產生這一結果還有個條件,即調用進程不為一個進程的領頭進程。由於在第一步調用fork的父進程退出,使得子進程不可能是進程組的領頭進程。該會話的領頭進程沒有控制終端與其相連,至此,滿足了守護進程沒有控制終端的要求。
3. 更改當前工作目錄
使用fork()函數建立的子進程會繼承父進程的當前工作目錄,當進程工作沒有結束時,其工作目錄是不能被卸載的.為了防止此問題,守護進程一般要chdir()了數將其工作目錄更改到別的目錄下(一般為/根目錄,因為根目錄是永遠不會被卸載的,除非關機).
4. 關閉檔案描述符,並重新導向標準輸入,輸出和錯誤
子進程會繼承父進程某些開啟了的檔案描述符,如果不使用這些檔案描述符,則需要關閉它們.守護進程是運行在系統背景,不應該在終端有任何的輸出資訊,可以使用dup()函數將其重新導向到/dev/null空裝置上.
5. 設定守護進程的檔案許可權建立掩碼
守護進程會建立一些臨時檔案,出於性的考慮,往往不希望這些檔案被別的使用者查看,這時可以用umask()函數修改檔案許可權,建立掩碼的取值.
上面說的是建立守護進程的大體步驟,解釋其中的兩點:
1、setsid的作用
一、讓進程擺脫原會話的控制
二. 讓進程擺脫原進程組的控制三. 讓進程擺脫原控制終端的控制
setsid()就是將進程和它當前的對話過程和進程組分離開,並且把它設定成一個新的對話過程的領頭進程。也就是說讓調用setsid()函數的這個進程脫離原有的進程組,自立門戶。同時需要注意一點就是,調用這個函數的進程不能是一個進程組的進程組長,不然進程組長自立門戶一戶,這個進程組的其他進程也就會消亡(這個和平時自己建立父子進程,然後父子進程各自做各自的事情還不太一樣)。這也就解釋了上面紅色字型的那句話。在子進程中調用setsid,不管這個時候父進程退出與否,子進程都不會是它當前所在進程組的進程組長。所以調用這個函數的是子進程,下面這個例子就是在父進程中調用setsid的副作用,會使父進程所在的進程組的其他子進程銷毀
pid_t pid = fork();
if (pid == 0) {
...
int result = execl(path, "adb", "fork-server", "server", NULL);
} else {
// run a program in a new session
setsid();//之前parent和child運行在同一個session裡,而且parent是session頭,
//所以作為session頭的parent如果exit結束執行的話,那麼會話session組中的所有進程將都被殺死;
//所以執行setsid()之後,parent將重新獲得一個新的會話session組id,child將仍持有原有的會話session組,
//這時parent退出之後,將不會影響到child了。
}
會話session是一個或多個進程組的集合。進程調用setsid函數建立一個新會話。如果調用此函數的進程不是一個進程組的組長,則此函數就會建立一個新會話,該進程變成會話的首進程,然後該進程成為一個新進程組的組長進程,該進程沒有控制終端。因為會話首進程是具有唯一進程ID的單個進程,所以可以將會話首進程的進程ID視為會話Id。
2、其實建立daemon進程是調用fork()兩次的。
一、如果開啟daemon進程的父進程本身可能不單單是為了建立daemon,假如父進程在某個地方一直阻塞,這時守護進程一直在運行,父進程卻沒有正常退出。如果守護進程因為正常或非正常原因退出了,就會變成ZOMBIE進程。
如果fork兩次呢?父進程先fork出一個兒子進程,兒子進程再fork出孫子進程做為守護進程,然後兒子進程立刻退出,守護進程被init進程接管,這樣無論父進程做什麼事,無論怎麼被阻塞,都與守護進程無關了。所以,fork兩次的守護進程很安全,避免了殭屍進程出現的可能性。
二、第一個子進程調用setsid()函數,調用以後第一個子進程才成為了新的進程組的進程組長(注意,調用setsid()函數的第一個子進程在調用這個函數之前不是進程組的進程組長)
第一子進程成為新的交談群組長和進程組長,
進程組長有權利申請開啟一個控制終端,第二次fork的意義就在此,關閉掉第一子進程,使第二子進程成為守護進程,並且因為沒有進程組長, 所以守護進程不會被關閉。
下面是個程式碼範例:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <time.h> #include <syslog.h> #include <signal.h> #include <sys/param.h> #include <fcntl.h> int init_daemon(void) { int pid; int i; int fd; /*忽略終端I/O訊號,STOP訊號*/ signal (SIGTTOU, SIG_IGN); signal (SIGTTIN, SIG_IGN); signal (SIGTSTP, SIG_IGN); signal (SIGHUP, SIG_IGN); printf(“ppid = %d\n”,getppid()); pid = fork(); fd = open(“/dev/tty”,O_RDONLY); printf(“!fd = %d\n”,fd); close(fd); if (pid > 0) { printf(“Parent process pid = %d\n”,getpid()); exit(0); //結束父進程,使得子進程成為後台進程 } else if (pid < 0) return -1; printf(“First Child process pid = %d\n”,getpid()); //當前進程為第一子進程 //建立一個新的進程組,在這個新的進程組中,子進程成為這個進程組的首進程,以使該進程脫離所有終端 printf(“pgid = %d\n”,getpgid(getpid())); //printf(“pid = %d\n”,getpid()); setsid(); printf(“pgid = %d\n”,getpgid(getpid())); //printf(“pid = %d\n”,getpid()); fd = open(“/dev/tty”,O_RDONLY); printf(“fd = %d\n”,fd); close(fd); //再次建立一個子進程,退出父進程(第一子進程),保證該進程不是進程組長,同時讓該進程無法再開啟一個新的終端 pid = fork(); fd = open(“/dev/tty”,O_RDONLY); printf(“#fd = %d\n”,fd); close(fd); if (pid > 0) { printf(“First Child process pid = %d\n”,getpid()); exit(0); } else if (pid < 0) return -1; //關閉所有從父進程繼承的不再需要的檔案描述符 for (i=0; i < NOFILE; close(i++)); //改變工作目錄,使得進程不與任何檔案系統聯絡 chdir(“/”); //將檔案檢測字設定為0 umask(0); //忽略SIGCHLD訊號 signal(SIGCHLD,SIG_IGN); return 0; } int main(int argc, char *argv[]) { time_t now; init_daemon(); syslog(LOG_USER|LOG_INFO,”測試守護進程!\n”); while(1) { sleep(8); time(&now); syslog(LOG_USER|LOG_INFO,”系統時間:\t%s\t\t\n”,ctime(&now)); } }
如何編寫一個守護進程daemon