Linux守護進程(Daemon)介紹與C++實現


1. 守護進程簡介

  守護進程(deamon)是生存期長的一種進程。它們常常在系統引導裝入時啟動(如果需要守護進程隨系統自啟動,需要在/etc/init.d目錄下放置響應的啟動腳本,或者利用systemctl來控制,還有一些其他方法如supervisor等,讀者可自行網上搜索相關用法),僅在系統關閉時才終止。因為它們沒有控制終端,所以說它們是在后台運行的
  從daemon的啟動和管理方式區分,可以將daemon分為兩大類:可獨立啟動的daemon(stand alone)和由一個超級daemon(super daemon)來統一管理的daemon。

  • stand alone:可單獨自行啟動的daemon。這種daemon啟動后會一直占用內存和系統資源,最大的優點是響應速度快,多用於能夠隨時接受遠程請求的服務,如WWW的daemon(httpd)、FTP的daemon(vsftpd)等。
  • super daemon:由一個特殊的daemon來統一管理。這種服務通過一個統一的daemon在需要時負責喚醒,當沒有遠程請求時,這些服務都是未啟動的,等到有遠程請求過來時,super daemon才喚醒相應的服務。當遠程請求結束后,被喚醒的服務會關閉並釋放系統資源。早期的super daemon是inetd,后來被xinetd替代了。super daemon本身是一個stand alone的服務,因為它需要管理后續的其他服務,所以它自己本身當然需要常駐內存中。

2. 守護進程創建步驟

  1. 執行一個fork(),之后父進程退出,子進程繼續執行。(結果就是daemon成為了init進程的子進程。)之所以要做這一步是因為下面兩個原因:
    • 假設daemon是從命令行啟動的,父進程的終止會被shell發現,shell在發現之后會顯示出另一個shell提示符並讓子進程繼續在后台運行。
    • 子進程被確保不會稱為一個進程組組長進程,因為它從其父進程那里繼承了進程組ID並且擁有了自己的唯一的進程ID,而這個進程ID與繼承而來的進程組ID是不同的,這樣才能夠成功地執行下面一個步驟。
  2. 子進程調用setsid()開啟一個新回話並釋放它與控制終端之間的所有關聯關系。結果就是使子進程: (a)成為新會話的首進程,(b)成為一個新進程組的組長進程,(c)沒有控制終端。
  3. 如果daemon從來沒有打開過終端設備,那么就無需擔心daemon會重新請求一個控制終端了。如果daemon后面可能會打開一個終端設備,那么必須要采取措施來確保這個設備不會成為控制終端。這可以通過下面兩種方式實現:
    • 在所有可能應用到一個終端設備上的open()調用中指定O_NOCTTY標記。
    • 或者更簡單地說,在setsid()調用之后執行第二個fork(),然后再次讓父進程退出並讓孫子進程繼續執行。這樣就確保了子進程不會稱為會話組長,因此根據System V中獲取終端的規則,進程永遠不會重新請求一個控制終端。(多一個fork()調用不會帶來任何壞處。)
  4. 清除進程的umask以確保當daemon創建文件和目錄時擁有所需的權限
  5. 修改進程的當前工作目錄,通常會改為根目錄(/)。這樣做是有必要的,因為daemon通常會一直運行直至系統關閉為止。如果daemon的當前工作目錄為不包含/的文件系統,那么就無法卸載該文件系統。或者daemon可以將工作目錄改為完成任務時所在的目錄或在配置文件中定義一個目錄,只要包含這個目錄的文件系統永遠不會被卸載即可。
  6. 關閉daemon從其父進程繼承而來的所有打開着的文件描述符。(daemon可能需要保持繼承而來的文件描述的打開狀態,因此這一步是可選的或者可變更的。)之所以這樣做的原因有很多。由於daemon失去了控制終端並且是在后台運行的,因此讓daemon保持文件描述符0(標准輸入)、1(標准輸出)和2(標准錯誤)的打開狀態毫無意義,因為它們指向的就是控制終端。此外,無法卸載長時間運行的daemon打開的文件所在的文件系統。因此,通常的做法是關閉所有無用的打開着的文件描述符,因為文件描述符是一種有限的資源。
  7. 在關閉了文件描述符0、1和2之后,daemon通常會打開/dev/null並使用dup2()(或類似的函數)使所有這些描述符指向這個設備。之所以要這樣做是因為下面兩個原因:
    • 它確保了當daemon調用了在這些描述符上執行I/O的庫函數時不會出乎意料地失敗。
    • 它防止了daemon后面使用描述符1或2打開一個文件的情況,因為庫函數會將這些描述符當做標准輸出和標准錯誤來寫入數據(進而破壞了原有的數據)。

3. C++實現

  直接將下面的示例代碼負責粘貼到文件中,例如這里文件名為example_daemon.cpp,直接g++編譯。

# 生成名為 example_daemon 的可執行文件
$ g++ -o example_daemon example_daemon.cpp
$ ./example_daemon 

# 可以看到 example_daemon 進程在后台運行,並且其父進程ID為1
$ ps -ef | grep example_daemon
lvnux   17889     1  0 14:37 ?        00:00:00 ./example_daemon

  示例代碼:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>


bool start_daemon()
{
    int fd;

    switch (fork()) {
        case -1:
            printf("fork() failed\n");
            return false;

        case 0:
            break;

        default:
            exit(0);
    }
    
    /*
    pid_t setsid(void);
    進程調用setsid()可建立一個新對話期間。
    如果調用此函數的進程不是一個進程組的組長,則此函數創建一個新對話期,結果為:
        1、此進程變成該新對話期的對話期首進程(session leader,對話期首進程是創建該對話期的進程)。
           此進程是該新對話期中的唯一進程。
        2、此進程成為一個新進程組的組長進程。新進程組ID就是調用進程的進程ID。
        3、此進程沒有控制終端。如果在調用setsid之前次進程有一個控制終端,那么這種聯系也被解除。
    如果調用進程已經是一個進程組的組長,則此函數返回錯誤。為了保證不處於這種情況,通常先調用fork(),
    然后使其父進程終止,而子進程繼續執行。因為子進程繼承了父進程的進程組ID,而子進程的進程ID則是新
    分配的,兩者不可能相等,所以這就保證了子進程不是一個進程組的組長。
    */
    if (setsid() == -1) {
        printf("setsid() failed\n");
        return false;
    }
    
    switch (fork()) {
        case -1:
            printf("fork() failed\n");
            return false;

        case 0:
            break;

        default:
            exit(0);
    }

    umask(0);
    chdir("/");
    
    long maxfd;
    if ((maxfd = sysconf(_SC_OPEN_MAX)) != -1)
    {
        for (fd = 0; fd < maxfd; fd++)
        {
            close(fd);
        }
    }

    fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        printf("open(\"/dev/null\") failed\n");
        return false;
    }
    
    /*
    // Standard file descriptors.
    #define STDIN_FILENO    0   // Standard input.
    #define STDOUT_FILENO   1   // Standard output.
    #define STDERR_FILENO   2   // Standard error output.
    */
    
    /*
    int dup2(int oldfd, int newfd);
    dup2()用來復制參數oldfd所指的文件描述符,並將它拷貝至參數newfd后一塊返回。
    如果newfd已經打開,則先將其關閉。
    如果oldfd為非法描述符,dup2()返回錯誤,並且newfd不會被關閉。
    如果oldfd為合法描述符,並且newfd與oldfd相等,則dup2()不做任何事,直接返回newfd。
    */
    if (dup2(fd, STDIN_FILENO) == -1) {
        printf("dup2(STDIN) failed\n");
        return false;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        printf("dup2(STDOUT) failed\n");
        return false;
    }

    if (dup2(fd, STDERR_FILENO) == -1) {
        printf("dup2(STDERR) failed\n");
        return false;
    }

    if (fd > STDERR_FILENO) {
        if (close(fd) == -1) {
            printf("close() failed\n");
            return false;
        }
    }

    return true;
}


int main(int argc, char** argv)
{
    start_daemon();
    
    while (true)
    {
        sleep(100);
    }
    
    return 0;
}

4. 參考文獻

  1. 《Linux_UNIX系統編程手冊》
  2. 《UNIX環境高級編程》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM