1. 程式人生 > >建立守護程序為什麼要fork兩次

建立守護程序為什麼要fork兩次

一、守護程序的基本概念:
守護程序也叫精靈程序,它是大多數伺服器的載體。守護程序是一種執行在後臺的一種特殊的程序,它獨立於控制終端並且週期性的執行某種任務或是等待處理某些發生的時間。如果想讓某個程序不因為使用者或中斷或其他變化而影響,那麼就必須把這個程序變成一個守護程序。

舉個例子:在現實生活中, 許多大型的軟體或伺服器必須保證7*24小時(一週7天,一天24小時)無障礙的執行,例如淘寶網、百度搜索引擎、支付寶等等,那麼像這樣一種要一直執行的程式怎麼實現呢?究其本質其實就是我們的守護程序。

二、守護程序的特點:
1).自成程序組,自成會話,與控制終端脫關聯;
2).守護程序的父程序是1號程序;
3).守護程序的命令一般以字元d結尾;
4).守護程序的生命週期是7*24小時不掉線;
5).一般的網路伺服器都以守護程序的形式在後臺執行,比如常見的http,ftp等等伺服器都是以守護程序的形式在後臺執行;


三、建立守護程序

1、檢視系統中的程序

ps axj
  • 1

引數a表示不僅列當前使用者的程序,也列出所有其他使用者的程序, 
引數x表示不僅列有控制終端的程序,也列出所無控制終端的程序, 
引數j表示列出與作業控制相關的資訊。

2、setsid函式 
(1)建立守護程序最關鍵的一步是呼叫setsid函式建立一個新的Session,併成為Session Leader。

#include<unistd.h>
pid_t setsid(void);
  • 1
  • 2

返回值:該函式呼叫成功時返回新建立的Session的id(其實也就是當前程序的id),出錯返回-1。

(2)需要注意的是,,呼叫這個函式之前,當前程序不允許是程序組的Leader,否則該函式返回-1。 
解決辦法:先fork再呼叫setsid,fork建立的子程序和父程序在同一個進 程組中,程序組的Leader必然是該組的第一個程序,所以子程序不可能是該組的第一個程序,在子 程序中呼叫setsid就不會有問題了。

(3)成功呼叫該函式的結果是: 
1. 建立一個新的Session,當前程序成為Session Leader,當前程序的id就是Session的id。 
2. 建立一個新的程序組,當前程序成為程序組的Leader,當前程序的id就是程序組的id。 
3. 如果當前程序原本有一個控制終端,則它失去這個控制終端,成為一個沒有控制終端的程序。所謂失去控制終端是指,原來的控制終端仍然是開啟的,仍然可以讀寫,但只是一個普通的開啟檔案而不是控制終端了。

3、建立守護程序的步驟 

方式一:

(1)呼叫umask將檔案模式建立遮蔽字設定為0.

umask(0);//umask必須清0,否則建立檔案受系統預設許可權的影響
  • 1

檔案許可權掩碼是遮蔽掉檔案許可權中的對應位。由於使用fork()函式新建立的子程序繼承了父程序的檔案許可權掩碼,這就給該子程序使用檔案帶了很多的麻煩(比如父程序中的檔案沒有執行檔案的許可權,然而在子程序中希望執行相應的檔案這個時候就會出問題)。因此在子程序中要把檔案的許可權掩碼設定成為0,即在此時有最大的許可權,這樣可以大大增強該守護程序的靈活性。

(2)呼叫fork,父程序退出(exit)。 
原因: 
1)如果該守護程序是作為一條簡單的shell命令啟動的,那麼⽗父程序終止使得shell認為該命令已經執行完畢。
2)保證子程序不是一個程序組的組長程序。

(3)呼叫setsid建立一個新會話。 
setsid會導致: 
1)呼叫程序成為新會話的首程序。 
2)呼叫程序成為一個程序組的組長程序 。 
3)呼叫程序沒有控制終端。(再次fork一次,保證daemon程序,之後不會開啟tty裝置)

呼叫setsid的原因: 
由於建立守護程序的第一步是呼叫fork()函式來建立子程序,再將父程序退出。由於在呼叫了fork()函式的時候,子程序拷貝了父程序的會話期、程序組、控制終端等資源、雖然父程序退出了,但是會話期、程序組、控制終端等並沒有改變,因此,需要用setsid()韓式來時該子程序完全獨立出來,從而擺脫其他程序的控制。

(4)將當前工作目錄更改為根目錄。 
防止當前目錄有一個目錄被刪除,導致守護程序無效。 
使用fork()建立的子程序是繼承了父程序的當前工作目錄,由於在程序執行中,當前目錄所在的檔案系統是不能解除安裝的,這對以後使用會造成很多的麻煩。因此通常的做法是讓“/”作為守護程序的當前目錄,當然也可以指定其他的別的目錄來作為守護程序的工作目錄。

(5)關閉不再需要的檔案描述符。 
同文件許可權碼一樣,用fork()函式新建的子程序會從父程序那裡繼承一些已經打開了的檔案。這些檔案被開啟的檔案可能永遠不會被守護程序讀寫,如果不進行關閉的話將會浪費系統的資源,造成程序所在的檔案系統無法卸下以及引起預料的錯誤。

如:關閉標準輸入流、標準輸出流、標準錯誤流。

close(0)close(1)close(2)
  • 1
  • 2
  • 3

(6)其他:忽略SIGCHLD訊號

signal(SIGCHLD,SIG_IGN);

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int creat_daemon()
{  
    //(1)呼叫umask將檔案模式建立遮蔽字設定為0.
    umask(0);    
   //(2)呼叫fork,父程序退出(exit)。  
    pid_t id = fork();
    if(id > 0)
    {
        exit(1);
    }
    else if(id == 0)
    {
        setsid();         //(3)建立一個新的會話
        if(chdir("/")<0)  //(4)將新建會話的工作目錄改成根目錄
        {
           perror("chdir");
           return;
        }
       //(5)關閉從父程序繼承來的檔案
        close(0);
        //close(1); 假如這裡沒有關閉會出現什麼情況,看下面結果
        close(2);
       //(6)忽略SIGCHLD訊號。
        signal(SIGCHLD, SIG_IGN);
 
    }
}

int main()
{
    creat_daemon();
    while(1);
    return 0;
}


由上圖可知通過執行自己編寫的mydaemon在Linux後臺建立了一個守護程序,它的父程序是1號程序,它自己是自成程序組,自成會話的。

在Linux的根目錄下有一個目錄proc,/proc目錄中包含許多以數字命名的子目錄,這些數字表示系統當前正在執行程序的程序號,裡面包含對應程序相關的多個資訊檔案。例如 cd /proc/18225,進入到剛才建立的守護程序的id裡面,並檢視它程序id所對應的檔案描述符fd的情況。


可以發現當1號檔案描述符不關閉的時候,此時的1號檔案描述符指向的是自己的特殊的裝置檔案/dev/pts/1。所以在建立守護程序的時候就是有一個步驟要關閉不必要的檔案描述符。  

方式二:建立守護程序也可以呼叫daemon函式:int daemon(int nochdir, int noclose); 

一般呼叫daemon有兩種方式:
1).daemon(0,0); //預設更改工作目錄且更改檔案描述符0,1,2為空,使其指向/dev/null。這個/dev/null就類似黑洞,不管哪個檔案描述符對其進行寫操作,它都會直接將資料丟棄;
2).daemon(1,0); //不更改工作目錄也不更改檔案描述符;

守護程序要fork兩次的原因:

第一次fork:這裡第一次fork的作用就是讓shell認為這條命令已經終止,不用掛在終端輸入上;再一個是為了後面的setsid服務,因為呼叫setsid函式的程序不能是程序組組長,如果不fork子程序,那麼此時的父程序是程序組組長,無法呼叫setsid。所以到這裡子程序便成為了一個新會話組的組長。

第二次fork:第2次fork不是必須的。也看到很多開源服務沒有fork第二次。fork第二次主要目的是。防止程序再次開啟一個控制終端。因為開啟一個控制終端的前臺條件是該程序必須是會話組長。再fork一次,子程序ID != sid(sid是程序父程序的sid)。所以也無法開啟新的控制終端。

daemon目的就是防止終端產生的一些訊號讓程序退出。上面函式並沒有直接呼叫signal函式去處理它。而是間接通過fork和setsid函式使用更少程式碼優雅處理。而被有些人誤以為是防止父程序沒有正常退出而導致的僵死程序的原因需要這樣處理。