Linux下使用daemon函數編寫後臺程序

以前我們在看《unix環境高級編程》的時候,有專門的整章詳細介紹如何編寫一個後臺daemon程序(精靈程序),主要涉及到創建會話組,切換工 作目錄,設置文件屏蔽字,關閉不必要的描述符等多個操作。這些操作對於每一個後臺程序來說都是類似的。

在Linux中專門提供了一個函數來完成這個daemon化的過程,這個函數的原型如下

int daemon (int __nochdir, int __noclose);

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

經過這個函數調用後的程序將運行在後臺,成爲一個daemon程序,而linux下大多的服務都是以此方式運行的。

我們來看一個簡單的例子。例如編寫例子程序test.c

#include <unistd.h>
#include <stdio.h>
 
int do_sth()
{
    //Add what u want
    return 0;
}
int main()
{
    daemon(0,0);
    while ( 1 )
    {
        do_sth();
        sleep(1);
    }
}

編譯並運行

[leconte@localhost daemon]$ gcc -o test test.c
[leconte@localhost daemon]$ ./test

程序進入了後臺,通過ps查看進程情況,可以看到進程的父進程id爲1,即init進程

 

用lsof查看test進程所打開的文件,可以看到文件描述符0,1,2都被重定向到/dev/null

並且能夠看到,進程的當前工作目錄(cwd)爲根目錄/,daemon函數已經幫我們完成了daemon化的過程,接下來我們只需要關注於程序功能 的實現了。

 

摘要:針對Linux環境下的守護進程daemon,分析了一般性守護進程的編寫方法,並提出若干見解,通過總結歸納進而爲設計和開發守護進程提供了有意的參考,給出了基於Linux守護進程實現的主要思想。

關鍵詞: 守護進程;信號量;控制終端

  
  1 引言
  Linux在啓動時需要啓動很多系統服務,它們向本地和網絡用戶提供了Linux的系統功能接口,直接面嚮應用程序和用戶。提供這些服務的程序是由運行在後臺的守護進程(Daemons)來執行的。
  編寫守護進程實際上是把一個普通進程按照守護進程的特性進行改造。比如,網絡通信服務中的守護進程需要能同時接受多個請求,它不斷地在偵聽端等待遠程的連接請求,收到請求後,創建一個子進程,讓其負責與遠端的通信,而自己則繼續返回偵聽。子進程和父進程間的通信採用消息機制,因此守護進程的開發涉及到子進程、進程組、會晤期、信號量、文件權限、目錄和控制終端等多個概念。本文主要分析守護進程的概念、實現原理以及編寫守護進程的方法。 
  
  2 Linux進程結構
  2.1 進程的創建
  Linux使用fork()函數來創建一個子進程,當fork()調用失敗時系統返回-1。一旦子進程被創建,父子進程一起從fork()處繼續執行,相互競爭系統的資源。如果希望子進程繼續執行,而父進程阻塞直到子進程完成任務,這時候可以調用wait()或者waitpid()系統調用。
  pid_t wait(int *stat_loc);
  pid_t waitpid(pid_t pid,int *stat_loc,int options);
  wait()系統調用會使父進程阻塞直到一個子進程結束或者是父進程接受到了一個信號。如果沒有父進程或者沒有子進程或者它的子進程已經結束了,wai()會立即返回。成功時(因一個子進程結束)wait()將返回子進程的ID,否則返回-1。
  2.2 守護進程的創建
  守護進程最重要的特性是後臺運行,因此守護進程必須與其運行前的環境隔離開來。這些環境包括未關閉的文件描述符、控制終端、會話和進程組、工作目錄以及文件創建掩碼等。這些環境是守護進程從執行它的父進程(特別是Shell)中繼承下來的。以下程序使用一個INIT_DAEMON宏來實現守護進程的初始化工作。
  #define INIT_DAEMON

  {

             if (fork()>0)

             exit(0);// 是父進程,結束父進程

        else if(fork()<0)exit(1);// fork失敗,退出
        setsid(); // 第一子進程成爲新的進程組長,並與控制終端分離
         if(fork()>0) exit(0);//是第一子進程,結束第一子進程
         else if(fork<0) exit(1);// fork失敗,退出。
  } //是第二子進程,繼續,第二子進程不再是會話組長
  第一次調用fork函數,爲避免掛起,控制終端將守護進程放入後臺執行,然後調用setsid()函數脫離控制終端和進程組,使該進程成爲會話組長,並與原來的登錄會話和進程組脫離。此時進程已經成爲無終端的會話組長,但它可以重新申請打開一個控制終端。爲了避免這種情況,可以通過使進程不再成爲會話組長來禁止進程重新打開控制終端,這就需要第二次調用fork()函數。父進程(會話組長)退出,子進程繼續執行,並不再擁有打開控制終端的能力。在正在執行的進程中調用INIT_DAEMON後,進程將成爲守護進程,脫離控制終端進入後臺執行。
  2.3 信號量機制
  爲了防止在守護進程沒有正常運轉起來時,控制終端受到干擾退出或掛起,需要將以下有關控制終端操作的信號屏蔽。
  signal(SIGTTOU, SIG_IGN);// 後臺進程寫控制終端
  signal(SIGTTIN, SIG_IGN);// 後臺進程讀控制終端
  signal(SIGTSTP, SIG_IGN);// 終端掛起
  signal(SIGHUP, SIG_IGN);// 進程組長退出時向所有會議成員發出
  當信號出現時,開發人員可以要求系統進行以下三種操作:
  (1)忽略信號。大多數信號都是採取這種方式進行處理,但是對SIGKILL和SIGSTOP信號不能做忽略處理。
  (2)捕捉信號。最常見的情況是,如果捕捉到SIGCHID信號,則表示子進程已經終止。可在此信號的捕捉函數中調用waitpid()函數取得該子進程的進程ID和它的終止狀態。
(3)執行系統的默認動作。對絕大多數信號而言,系統的默認動作都是終止該進程。
  
  3 守護進程開發準則
  3.1 控制終端
  首先使用ps命令打印系統中各個進程的狀態。
  所有守護進程都以超級用戶(用戶ID爲0)的優先權運行。沒有一個守護進程具有控制終端,終端名稱設置爲問號(?)、終端前臺進程組ID設置爲-1。缺少控制終端是守護進程調用了setsid()的結果。
  3.2 進程組
  進程組是一個或多個進程的集合。進程組ID類似於進程ID,可存放在 pid_t 數據類型中。每個進程組有一個組長進程,組長進程的標識是其進程組ID等於其進程ID。進程組組長可以創建一個進程組,創建該組中的進程,然後終止。只要在某個進程組中有一個進程存在,則該進程就存在,這與其組長進程是否終止無關。
  3.3 會話期
  會話期(Session)是一個或多個進程組的集合。在一個會話期中有3個進程組,通常是有Shell的管道線將幾個進程編成一組。一個會話期可以有一個單獨的控制終端,即在其上登錄的終端設備(終端登錄)或僞終端設備(網絡登錄),但這個控制終端並不是必需的。如果一個會話期有一個控制終端,則它有一個前臺進程組,其他進程組爲後臺進程組。
  3.4 脫離控制終端,登錄會話和進程組
  登錄會話可以包含多個進程組,這些進程組共享一個控制終端,這個控制終端通常是創建進程的控制終端。登錄會話和進程組通常是從父進程繼承下來的。需要說明的是,當進程是會話組長時,setsid()調用將失敗,2.2已經保證進程不是會話組長。setsid()調用成功後,進程成爲新的會話組長和新的進程組長,並與原來的登錄會話和進程組脫離,由於會話過程對控制終端的獨佔性,進程同時與控制終端脫離。具體是操作就是: 
  (1)成爲新對話期的首進程;
  (2)成爲一個新進程組的首進程;
  (3)沒有控制終端。
  守護進程要擺脫從父進程繼承下來的控制終端它們影響,可以使用setsid()函數設置新會話的領頭進程,並與原來的登錄會話和進程組脫離。這只是其中的一種方法,還可以使用如下處理方法:
  if ((fd = open("/dev/tty",O_RDWR)) >= 0)
  {ioctl(fd,TIOCNOTTY,NULL);
  // 其中/dev/tty是一個流設備,也是終端映射
  close(fd); } // 調用close()函數將終端關閉
  3.5 關閉文件描述符
  進程從創建它的父進程那裏繼承了打開的文件描述符,如果守護進程留下一個處於打開狀態的普通文件,將阻止該文件被任何其他進程從文件系統中刪除。一般來說,必要的是關閉0、1、2三個文件描述符,即標準輸入(STDINT)、標準輸出(STDOU)、標準錯誤(STDERR)。關閉不必要的連接甚至更爲重要,因爲在該終端上的用戶退出系統後,將執行vhangup()系統調用,守護進程訪問該終端的權利於是被撤消。這表示守護進程雖有它以爲處於打開狀態的文件描述符,事實上已不再能通過這些文件描述符訪問該終端。關閉三者的代碼如下:
  for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
  // fdtablesize是一個進程一次可以打開進程的最大數
  close(fd); // 關閉打開的文件描述符,包括標準輸入、標準輸出和標準錯誤輸出
  如果程序想保留0、1、2三個文件描述符,那麼循環應繞過這三者。實現代碼如下:
  for (fd =3, fdtablesize = getdtablesize();fd < fdtablesize; fd++) 
  close(fd);
  
  4 結束語
  守護進程廣泛應用於Linux/Unix環境下的系統管理、網絡通信以及嵌入式應用等領域。本文分析了Linux守護進程的結構與實現原理,所欠缺的是構建程序的通用化,原因是存在不同環境之間切換並執行不同的任務,同時還必須考慮其他系統之間的所有差異,今後的工作主要集中在標準化以及簡化異構環境中的管理任務的方法上。

beyes2010-05-16 02:09

Linux 守護進程的編程方法

守護進程(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,"I'm here at %s/n",asctime(localtime(&t)) );
fclose(fp);
}
}
}

 

 

發佈了1 篇原創文章 · 獲贊 8 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章