Linux守護程式的程式設計實現(轉)

長風破浪發表於2015-03-10

http://blog.csdn.net/zg_hover/article/details/2553321

http://blog.csdn.net/kongdefei5000/article/details/8808147

 

守護程式(Daemon)是執行在後臺的一種特殊程式。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。守護程式是一種很有用的程式。Linux的大多數伺服器就是用守護程式實現的。比如,Internet伺服器inetd,Web伺服器httpd等。同時,守護程式完成許多系統任務。比如,作業規劃程式crond,列印程式lpd等。
守護程式的程式設計本身並不複雜,複雜的是各種版本的Unix的實現機制不盡相同,造成不同Unix環境下守護程式的程式設計規則並不一致。這需要讀者注意,照搬某些書上的規則(特別是BSD4.3和低版本的System V)到Linux會出現錯誤的。下面將全面介紹Linux下守護程式的程式設計要點並給出詳細例項。
一. 守護程式及其特性
守護程式最重要的特性是後臺執行。在這一點上DOS下的常駐記憶體程式TSR與之相似。其次,守護程式必須與其執行前的環境隔離開來。這些環境包括未關閉的檔案描述符,控制終端,會話和程式組,工作目錄以及檔案建立掩模等。這些環境通常是守護程式從執行它的父程式(特別是shell)中繼承下來的。最後,守護程式的啟動方式有其特殊之處。它可以在Linux系統啟動時從啟動指令碼/etc/rc.d中啟動,可以由作業規劃程式crond啟動,還可以由使用者終端(通常是shell)執行。
總之,除開這些特殊性以外,守護程式與普通程式基本上沒有什麼區別。因此,編寫守護程式實際上是把一個普通程式按照上述的守護程式的特性改造成為守護程式。如果讀者對程式有比較深入的認識就更容易理解和程式設計了。
二. 守護程式的程式設計要點
前面講過,不同Unix環境下守護程式的程式設計規則並不一致。所幸的是守護程式的程式設計原則其實都一樣,區別在於具體的實現細節不同。這個原則就是要滿足守護程式的特性。同時,Linux是基於Syetem V的SVR4並遵循Posix標準,實現起來與BSD4相比更方便。程式設計要點如下;
1. 在後臺執行。
為避免掛起控制終端將Daemon放入後臺執行。方法是在程式中呼叫fork使父程式終止,讓Daemon在子程式中後臺執行。
if(pid=fork())
exit(0);//是父程式,結束父程式,子程式繼續
2. 脫離控制終端,登入會話和程式組
有必要先介紹一下Linux中的程式與控制終端,登入會話和程式組之間的關係:程式屬於一個程式組,程式組號(GID)就是程式組長的程式號(PID)。登入會話可以包含多個程式組。這些程式組共享一個控制終端。這個控制終端通常是建立程式的登入終端。
控制終端,登入會話和程式組通常是從父程式繼承下來的。我們的目的就是要擺脫它們,使之不受它們的影響。方法是在第1點的基礎上,呼叫setsid()使程式成為會話組長:
setsid();
說明:當程式是會話組長時setsid()呼叫失敗。但第一點已經保證程式不是會話組長。setsid()呼叫成功後,程式成為新的會話組長和新的程式組長,並與原來的登入會話和程式組脫離。由於會話過程對控制終端的獨佔性,程式同時與控制終端脫離。
3. 禁止程式重新開啟控制終端
現在,程式已經成為無終端的會話組長。但它可以重新申請開啟一個控制終端。可以通過使程式不再成為會話組長來禁止程式重新開啟控制終端:

if(pid=fork())
exit(0);//結束第一子程式,第二子程式繼續(第二子程式不再是會話組長)
4. 關閉開啟的檔案描述符
程式從建立它的父程式那裡繼承了開啟的檔案描述符。如不關閉,將會浪費系統資源,造成程式所在的檔案系統無法卸下以及引起無法預料的錯誤。按如下方法關閉它們:
for(i=0;i 關閉開啟的檔案描述符close(i);>
5. 改變當前工作目錄
程式活動時,其工作目錄所在的檔案系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫執行日誌的程式將工作目錄改變到特定目錄如/tmpchdir("/")
6. 重設檔案建立掩模
程式從建立它的父程式那裡繼承了檔案建立掩模。它可能修改守護程式所建立的檔案的存取位。為防止這一點,將檔案建立掩模清除:umask(0);
7. 處理SIGCHLD訊號
處理SIGCHLD訊號並不是必須的。但對於某些程式,特別是伺服器程式往往在請求到來時生成子程式處理請求。如果父程式不等待子程式結束,子程式將成為殭屍程式(zombie)從而佔用系統資源。如果父程式等待子程式結束,將增加父程式的負擔,影響伺服器程式的併發效能。在Linux下可以簡單地將SIGCHLD訊號的操作設為SIG_IGN。
signal(SIGCHLD,SIG_IGN);
這樣,核心在子程式結束時不會產生殭屍程式。這一點與BSD4不同,BSD4下必須顯式等待子程式結束才能釋放殭屍程式。
三. 守護程式例項
守護程式例項包括兩部分:主程式test.c和初始化程式init.c。主程式每隔一分鐘向/tmp目錄中的日誌test.log報告執行狀態。初始化程式中的init_daemon函式負責生成守護程式。讀者可以利用init_daemon函式生成自己的守護程式。
1. init.c清單

#include < unistd.h > 
#include < signal.h > 
#include < sys/param.h > 
#include < sys/types.h > 
#include < sys/stat.h > 
void init_daemon(void) 
{ 
int pid; 
int i; 
if(pid=fork()) 
exit(0);//是父程式,結束父程式 
else if(pid< 0) 
exit(1);//fork失敗,退出 
//是第一子程式,後臺繼續執行 
setsid();//第一子程式成為新的會話組長和程式組長 
//並與控制終端分離 
if(pid=fork()) 
exit(0);//是第一子程式,結束第一子程式 
else if(pid< 0) 
exit(1);//fork失敗,退出 
//是第二子程式,繼續 
//第二子程式不再是會話組長 

for(i=0;i< NOFILE;++i)//關閉開啟的檔案描述符 
close(i); 
chdir("/tmp");//改變工作目錄到/tmp 
umask(0);//重設檔案建立掩模 
return; 
} 

2. test.c清單

#include < stdio.h > 
#include < time.h > 

void init_daemon(void);//守護程式初始化函式 

main() 
{ 
FILE *fp; 
time_t t; 
init_daemon();//初始化為Daemon 

while(1)//每隔一分鐘向test.log報告執行狀態 
{ 
sleep(60);//睡眠一分鐘 
if((fp=fopen("test.log","a")) >=0) 
{ 
t=time(0); 
fprintf(fp,"Im here at %s/n",asctime(localtime(&t)) ); 
fclose(fp); 
} 
} 
} 

以上程式在RedHat Linux6.0下編譯通過。步驟如下:
編譯:gcc -g -o test init.c test.c
執行:./test
檢視程式:ps -ef
從輸出可以發現test守護程式的各種特性滿足上面的要求。

說明:在系統呼叫庫中有一個庫函式可以直接使一個程式變成守護程式,
       #include <unistd.h>
       int daemon(int nochdir, int noclose);

如果nochdir的值為0,則將切換工作目錄為根目錄;如果noclose為0,則將標準輸入,輸出和標準錯誤都重定向到/dev /null

例子:

  

/*
*功能:建立一個守護程式,監視系統所有執行的程式
*/
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>

 #include <unistd.h>

int main()
{
    FILE *fp;
    FILE *fstream;
    signal(SIGCHLD, SIG_IGN); // 忽略子程式結束訊號,防止出現殭屍程式          
      daemon(0,0);//初始化守護程式,就是建立一個守護程式
    while(1)
    {    
        /*PID 程式ID ,user:程式開闢使用者,comm:程式名,lstart:程式開始時間,etime:程式持續時間*/
        fstream=popen("ps -eo pid,user,comm,lstart,etime>test.txt","r");  
        //如果執行命令失敗,則寫入錯誤報告
        if(fstream==NULL)
        {
            //在開啟或者建立error.log成功的情況下,寫入錯誤(使用errno時失敗)
            if((fp = fopen("error.log", "a+")) != NULL)
            {
                fprintf(fp, "%s\n", "執行命令失敗");
                fclose(fp);
            }
            else
               exit(1);  //寫入錯誤失敗,則終止程式推出並關閉所有程式
        }
        else
          pclose(fstream); //關閉popen開啟的I/O流
        sleep(120);   //設定成5分鐘獲取一次系統程式情況
    }
    return 0;
}

命令:gcc -o testdaemon testdaemon.c

編譯程式

命令:./testdaemon

執行程式

命令:ps -ef| egrep 'testdaemon'

通過ps檢視程式情況,可以看到程式的父程式id為1,即init程式

命令:lsof -c testdaemon

用lsof檢視test程式所開啟的檔案,可以看到檔案描述符0,1,2都被重定向到/dev/null

 

相關文章