Unix環境高級編程——守護進程記錄總結(從基礎到實現)

一、概念及其特徵

    守護進程是系統中生存期較長的一種進程,常常在系統引導裝入時啓動,在系統關閉時終止,沒有控制終端,在後臺運行。守護進程脫離於終端是爲了避免進程在執行過程中的信息在任何終端上顯示並且進程也不會被任何終端所產生的終端信息所打斷。

    在這裏,我們在Linux2.6內核的centos中,ps -ef |awk '{print $1"\t "$2"\t "$3"\t  "$8}'看到:PPID=0的進程有兩個,分別是PID=1的/sbin/init進程和PID=2的[kthreadd]進程。

    其中,[kthreadd]爲內核進程,由它fork出來的子進程都是內核進程,並且內核守護進程的名字出現在方括號中,對於需要在進程上下文執行工作但卻不被用戶層進程(init)上下文調用的每一個內核組件,通常有它自己的內核守護進程。

    而對於init進程,它是一個由內核在引導裝入時啓動的用戶層次的命令,屬於用戶級守護進程,主要負責啓動各運行層次特定系統服務。這些服務通常是在它們自己擁有的守護進程的幫助下實現的。用戶層守護進程缺少控制終端可能是守護進程調用了setsid的結果。大多數用戶層守護進程都是進程組的組長進程以及會話的首進程,而且是這些進程組和會話中的唯一進程。

    守護進程的啓動方式有其特殊之處。它可以在Linux系統啓動時從啓動腳本/etc/rc.d中啓動,可以由作業規劃進程crond啓動,還可以由用戶終端(通常是shell)執行。此外,守護進程必須與其運行前的環境隔離開來。這些環境包括未關閉的文件描述符,控制終端,會話和進程組,工作目錄以及文件創建屏蔽字等。這些環境通常是守護進程從執行它的父進程(特別是shell)中繼承下來的。

 

二、編程規則(細節可參考Unix環境高級編程)

1、調用umask將文件模式創建屏蔽字設置爲一個已知值(通常是0)。如前所述,由繼承得來的文件模式創建屏蔽字可能會被設置爲拒絕權限。我們可以根據我們的具體需求設定特定的權限。

2、調用fork,然後使父進程exit。這樣做,使得當我們以./的shell命令啓動守護進程時,父進程終止會讓shell認爲此命令已經執行完畢,而且,這也使子進程獲得了一個新的進程ID。此外,讓父進程先於子進程exit,會使子進程變爲孤兒進程,這樣子進程成功被init這個用戶級守護進程收養。

3、調用setsid創建一個新會話。這在setsid函數中有介紹,調用setsid,會使這個子進程成爲(a)新會話的首進程,(b)成爲一個新進程組的組長進程,(c)切斷其與控制終端的聯繫,或者就是沒有控制終端。至此,這個子進程作爲新的進程組的組長,完全脫離了其他進程的控制,並且沒有控制終端。

4、將當前工作目錄更改爲根目錄(或某一特定目錄位置)。這是爲了保證守護進程的當前工作目錄在一個掛載的文件系統中,該文件系統不能被卸載。

5、關閉不再需要的文件描述符。根據具體情況來定。

6、某些守護進程可以打開/dev/null使其具有文件描述符0、1、2,這使任何一個試圖讀標準輸入、寫標準輸出或標準錯誤的庫例程都不會產生任何效果。

7、忽略SIGCHLD信號

   這一步並非必須的,只對需要創建子進程的守護進程纔有必要,很多服務器守護進程設計成通過派生子進程來處理客戶端的請求,如果父進程不對SIGCHLD信號進行處理的話,子進程在終止後變成殭屍進程,通過將信號SIGCHLD的處理方式設置爲SIG_IGN可以避免這種情況發生。

8、用日誌系統記錄出錯信息

   因爲守護進程沒有控制終端,當進程出現錯誤時無法寫入到標準輸出上,可以通過調用syslog將出錯信息寫入到指定的文件中。該接口函數包括openlog、syslog、closelog、setlogmask,具體可參考13.4節出錯記錄。

9、守護進程退出處理

   當用戶需要外部停止守護進程運行時,往往會使用 kill命令停止該守護進程。所以,守護進程中需要編碼來實現kill發出的signal信號處理,達到進程的正常退出。可用如下信號函數處理:

signal(SIGTERM, sigterm_handler);

         voidsigterm_handler(int arg)

         {

         //進行相應處理的函數

         }

 


三、一個簡單的守護進程實例

      按照上面的編程規則,結合書中示例,寫了一個簡單的守護進程,可以測試通過。其中爲了方便查看出錯記錄的過程,將syslog函數註釋掉,改在/tmp文件下記錄日誌。此外,想了一下,由於在子進程中已經將守護進程的所有標準IO加入到/dev/null中,因此我們無法通過printf函數來交互了,所以程序中還是有很多瑕疵的,我們只能將所有信息寫入日誌中,後續還需多考慮改進,信號處理函數有問題,還需要加一段信號屏蔽字的處理函數,防止收到的信號進行默認動作處理,也就是這個原因,使我下面這段程序無法觸發往/tmp/daemon.txt文件中記錄退出信息。


<span style="font-size:18px;">#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void daemonize(const char *cmd)
{
        pid_t           pid;
        int             i, fd0, fd1, fd2;
        struct rlimit   r1;

        umask(0);  //第一步:將文件模式創建屏蔽字設置爲0

        /*
         * 第二步:fork子進程,並且使子進程成爲會話首進程,脫離終端控制
         */
        if ((pid = fork()) < 0)
                printf("ERROR:can't fork a son process");
        else if (pid != 0)  //父進程
                exit(0);
        setsid();

        /*
         * 第三步:改變當前工作目前爲根目錄,以防卸載當前文件系統所在的目錄
         */
        if (chdir("/") < 0)
                printf("ERROR:can't change the directory to root(/)");

        /*
         * 先獲取當前最大的文件描述符,並且關閉所有不需要的文件描述符
         */
        if (getrlimit(RLIMIT_NOFILE, &r1) < 0)
                printf("ERROR:can't get the maximum fd");
        if (r1.rlim_max == RLIM_INFINITY)
                r1.rlim_max = 1024;
        for (i = 0; i < r1.rlim_max; i++)
                close(i);

        /*
         * 把fd爲0、1、2三個fd加入到/dev/null中
         */
        fd0 = open("/dev/null", O_RDWR);
        fd1 = dup(0);
        fd2 = dup(0);

        /*
         * 初始化日誌文件,調用syslog函數的openlog
         *

        openlog(cmd, LOG_CONS, LOG_DAEMON);
        if (fd0 != 0 || fd1 != 1 || fd2 != 2){
                syslog(LOG_ERR,"unexpected fd %d %d %d", fd0,fd1,fd2);
                exit(1);
        }
        */

        //以下代碼段爲了演示記錄日誌的效果
        char    *buf="this is a dameon \n";
        int     len, fd;
        len = strlen(buf);
        while(1)
        {
        if((fd=open("/tmp/dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
               {
                   printf("open file err \n");
                   exit(0);
               }
           write(fd,buf,len+1);
            close(fd);
            sleep(60);  //每60秒向日志中記錄一次,daemon還存在
        }

}
void sigterm_handler(int arg)
        {
        //進行相應處理的函數
                char    *buf="I have received a signal,I will exit 3 seconds later \n";
                int     len, fd;
                len = strlen(buf);
                if((fd=open("/tmp/dameon.txt",O_WRONLY|O_APPEND,0600))<0)
                 {
                        printf("open file err \n");
                        exit(0);
                 }
                write(fd,buf,len+1);
                close(fd);
                sleep(3);
                exit(0);
        }

int main()
{
        char    *cmd;
        cmd = "cron";
        daemonize(cmd);
        pause();
        signal(SIGUSR1, sigterm_handler);
}</span>


下面是加了信號屏蔽字處理代碼段的程序,這回可以將SIGUSR1信號抓取,並且寫入日誌了。

#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void siguser1(int signo)
        {
        char    *buf="I have received a signal,I will exit 3 seconds later \n";
                int     len, fd;
                len = strlen(buf);
                if((fd=open("/tmp/dameon_exit.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
                 {
                        printf("open file err \n");
                        exit(0);
                 }
                write(fd,buf,len+1);
                close(fd);
                sleep(3);
                exit(0);
        }

void daemonize(const char *cmd)
{
        pid_t           pid;
        int             i, fd0, fd1, fd2;
        struct rlimit   r1;
        struct sigaction        sa;

        umask(0);  //第一步:將文件模式創建屏蔽字設置爲0

        /*
         * 第二步:fork子進程,並且使子進程成爲會話首進程,脫離終端控制
         */
        if ((pid = fork()) < 0)
                printf("ERROR:can't fork a son process");
        else if (pid != 0)  //父進程
                exit(0);
        setsid();

         /*
          *對SIGUSR1信號進行處理 (注意,這個代碼段必須寫在第三步之前)
          */
        sa.sa_handler = siguser1;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        if (sigaction(SIGUSR1, &sa, NULL) < 0){
                syslog(LOG_ERR,"can't catch SIGUSR1");
                exit(1);
        }


        /*
         * 第三步:改變當前工作目前爲根目錄,以防卸載當前文件系統所在的目錄
         */
        if (chdir("/") < 0)
                printf("ERROR:can't change the directory to root(/)");

        /*
         * 先獲取當前最大的文件描述符,並且關閉所有不需要的文件描述符
         */
        if (getrlimit(RLIMIT_NOFILE, &r1) < 0)
                printf("ERROR:can't get the maximum fd");
        if (r1.rlim_max == RLIM_INFINITY)
                r1.rlim_max = 1024;
        for (i = 0; i < r1.rlim_max; i++)
                close(i);

        /*
         * 把fd爲0、1、2三個fd加入到/dev/null中
         */
        fd0 = open("/dev/null", O_RDWR);
        fd1 = dup(0);
        fd2 = dup(0);

        /*
         * 初始化日誌文件,調用syslog函數的openlog
         *

        openlog(cmd, LOG_CONS, LOG_DAEMON);
        if (fd0 != 0 || fd1 != 1 || fd2 != 2){
                syslog(LOG_ERR,"unexpected fd %d %d %d", fd0,fd1,fd2);
                exit(1);
        }
        */

        //以下代碼段爲了演示記錄日誌的效果
        char    *buf="this is a dameon \n";
        int     len, fd;
        len = strlen(buf);
        while(1)
        {
        if((fd=open("/tmp/dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
               {
                   printf("open file err \n");
                   exit(0);
               }
           write(fd,buf,len+1);
            close(fd);
            sleep(60);  //每60秒向日志中記錄一次,daemon還存在
        }

}

int main()
{
        char    *cmd;
        cmd = "cron";
        daemonize(cmd);
        pause();
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章