歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於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有下面的3個作用:

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

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

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

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

說明:當進程是會話組長時setsid()調用失敗。但第一點已經保證進程不是會話組長。setsid()調用成功後,進程成為新的會話組長和新的進程組長,並與原來的登錄會話和進程組脫離。由於會話過程對控制終端的獨占性,進程同時與控制終端脫離。 3. 禁止進程重新打開控制終端
現在,進程已經成為無終端的會話組長。但它可以重新申請打開一個控制終端。可以通過使進程不再成為會話組長來禁止進程重新打開控制終端:
if(pid=fork())

exit(0);//結束第一子進程,第二子進程繼續(第二子進程不再是會話組長)

注意:很多讀者就會問,為什麼要創建兩次進程呢? 這是因為第二步結束後,進程創建了一個新的會話組,並成為會話組長,而會話組長可能獲得控制終端,如果獲得了控制終端那麼或這個進程就不是守護進程了。所以添加了這幾句代碼,讓進程失去會話組長的身份,從而沒有獲得控制終端的權限。

4. 關閉打開的文件描述符
同文件權限碼一樣,用fork函數新建的子進程會從父進程那裡繼承一些已經打開了的文件。這些被打開的文件可能永遠不會被守護進程讀寫,但它們一樣消耗系統資源,而且可能導致所在的文件系統無法卸下。 在上面的第二步之後,守護進程已經與所屬的控制終端失去了聯系。因此從終端輸入的字符不可能達到守護進程,守護進程中用常規方法(如printf)輸出的字符也不可能在終端上顯示出來。所以,文件描述符為0、1和2 的3個文件(常說的輸入、輸出和報錯)已經失去了存在的價值,也應被關閉。通常按如下方式關閉文件描述符: for(i=0;i5. 改變當前工作目錄
這一步也是必要的步驟。使用fork創建的子進程繼承了父進程的當前工作目錄。由於在進程運行中,當前目錄所在的文件系統(如“/mnt/usb”)是不能卸載的,這對以後的使用會造成諸多的麻煩(比如系統由於某種原因要進入單用戶模式)。因此,通常的做法是讓"/"作為守護進程的當前工作目錄,這樣就可以避免上述的問題,當然,如有特殊需要,也可以把當前工作目錄換成其他的路徑,如/tmp。改變工作目錄的常見函數式chdir。
6. 重設文件創建掩模
進程從創建它的父進程那裡繼承了文件創建掩模。它可能修改守護進程所創建的文件的存取位。為防止這一點,將文件創建掩模清除:umask(0);
7. 處理SIGCHLD信號
處理SIGCHLD信號並不是必須的。但對於某些進程,特別是服務器進程往往在請求到來時生成子進程處理請求。如果父進程不等待子進程結束,子進程將成為僵屍進程(zombie)從而占用系統資源。如果父進程等待子進程結束,將增加父進程的負擔,影響服務器進程的並發性能。在Linux下可以簡單地將SIGCHLD信號的操作設為SIG_IGN。
signal(SIGCHLD,SIG_IGN);

這樣,內核在子進程結束時不會產生僵屍進程。這一點與BSD4不同,BSD4下必須顯式等待子進程結束才能釋放僵屍進程。

 

因為linux裡的進程都屬於一顆樹,樹的根結點是linux系統初始化結束階段時啟動的init進程,這個進程的pid是1,所有的其他進程都是它的子孫。除了init,任何進程一定有他的父進程,而父進程會負責分配(fork)、回收(wait4)它申請的進程資源。這個樹狀關系也比較健壯,當某個進程還在運行時,它的父進程卻退出了,這個進程卻沒有成為孤兒進程,因為linux有一個機制,init進程會接管它,成為它的父進程。這也是守護進程的由來了,因為守護進程的其中一個要求就是希望init成為守護進程的父進程。

如果某個進程自身終止了,在調用exit清理完相關的內容文件等資源後,它就會進入ZOMBIE狀態,它的父進程會調用wait4來回收這個task_struct,但是,如果父進程一直沒有調用wait4去釋放子進程的task_struct,問題就來了,這個task_struct誰來回收呢?永遠沒有人,除非父進程終止後,被init進程接管這個ZOMBIE進程,然後調用wait4來回收進程描述符。如果父進程一直在運行著,這個ZOMBIE會永遠的占用系統資源,用KILL發任何信號量也不能釋放它。這是很可怕的,因為服務器上可能會出現無數ZOMBIE進程導致機器掛掉。

8.守護進程退出處理
當用戶需要外部停止守護進程運行時,往往會使用 kill命令停止該守護進程。所以,守護進程中需要編碼來實現kill發出的signal信號處理,達到進程的正常退出。
===============================
signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg)
{
_running = 0;
}
===============================
這樣,一個簡單的守護進程就建立起來了。

 

#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); 
        } 
    } 
} 
查看進程:ps -ef
從輸出可以發現test守護進程的各種特性滿足上面的要求。

Copyright © Linux教程網 All Rights Reserved