1. 程式人生 > >Linux 守護程序建立原理及簡易方法

Linux 守護程序建立原理及簡易方法

1:什麼是Linux下的守護程序

  Linux daemon是運行於後臺常駐記憶體的一種特殊程序,週期性的執行或者等待trigger執行某個任務,與使用者互動斷開,獨立於控制終端。一個守護程序的父程序是init程序,它是一個孤兒程序,沒有控制終端,所以任何輸出,無論是向標準輸出裝置stdout還是標準出錯裝置stderr的輸出都被丟到了/dev/null中。守護程序一般用作伺服器程序,如httpd,syslogd等。

2:程序,程序組,會話,控制終端之間的關係

因為守護程序的建立需要改變這些環境引數,所以瞭解它們之間的關係很重要:

上圖就描述了它們之間的聯絡:

  2.1 程序組

:它是由一個或多個程序組成,程序組號(GID)就是這些程序中的程序組長的PID。

  2.2 會話:其實叫做會話期(session),它包括了期間所有的程序組,一般一個會話期開始於使用者login,一般login的是shell終端,所以shell終端又是此次會話期的首程序,會話一般結束於logout。對於非程序組長,它可以呼叫setsid()建立一個新的會話。

  2.3 控制終端(tty):一般就是指shell終端,它在會話期中可有也可以沒有。

3:建立一個daemon的幾個步驟

  3.1 例項(建立一個daemon,每隔10秒向/mydaemon.log檔案寫入當前時間一共三次)

void mydaemon(void)
{    
    pid_t pid;
    int fd, i, nfiles;
    struct rlimit rl;

    pid = fork();
    if(pid < 0)
        ERROR_EXIT("First fork failed!");

    if(pid > 0)
        exit(EXIT_SUCCESS);// father exit

    if(setsid() == -1)
        ERROR_EXIT("setsid failed!");

    pid = fork();
    if(pid < 0)
        ERROR_EXIT("Second fork failed!");

    if(pid > 0)// father exit
        exit(EXIT_SUCCESS);
    #ifdef RLIMIT_NOFILE
    /* 關閉從父程序繼承來的檔案描述符 */
    if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
        ERROR_EXIT("getrlimit failed!");
    nfiles = rl.rlim_cur = rl.rlim_max;
    setrlimit(RLIMIT_NOFILE, &rl);
    for(i=3; i<nfiles; i++)
        close(i);
   #endif
    /* 重定向標準的3個檔案描述符 */
    if(fd = open("/dev/null", O_RDWR) < 0)
        ERROR_EXIT("open /dev/null failed!");
    for(i=0; i<3; i++)
        dup2(fd, i);
   if(fd > 2) close(fd);
    /* 改變工作目錄和檔案掩碼常量 */
    chdir("/");
    umask(0);
}

3.2 解讀建立daemon的過程

  A(7~12行):成為後臺程序

    用fork建立子程序,父程序退出,子程序成為孤兒程序被init接管,子程序變為後臺程序。

  B(14~15行):脫離父程序的控制終端,登陸會話和程序組

    呼叫setsid()讓子程序成為新會話的組長,脫離父程序的會話期。setsid()在呼叫者是某程序組組長時會失敗,但是A已經保證了子程序不會是組長,B之後子程序變成了新會話組的組長。

  C(17~22行):禁止程序重新開啟控制終端

    因為會話組的組長有許可權重新開啟控制終端,所以這裡第二次fork將子程序結束,留著孫程序,孫程序不是會話組的組長所以沒有權利再開啟控制終端,這樣整個程式就與控制終端隔離了。

  D(23~31行):關閉檔案描述符

    程序從建立它的父程序那裡繼承了開啟的檔案描述符。如不關閉,將會浪費系統資源,造成程序所在的檔案系統無法卸下以及引起無法預料的錯誤。

  E(32~36行):重定向0,1,2標準檔案描述符

    將三個標準檔案描述符定向到/dev/null中

  F(38~40行):改變工作目錄和檔案掩碼

    程序活動時,其工作目錄所在的檔案系統不能卸下(比如工作目錄在一個NFS中,執行一個daemon會導致umount無法成功)。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫執行日誌的程序將工作目錄改變到特定目錄如chdir("/tmp"),程序從建立它的父程序那裡繼承了檔案建立掩模。它可能修改守護程序所建立的檔案的存取位。為防止這一點,將檔案建立掩模清除:umask(0);

  注:D,E,F三步是對當前工作環境的修改,可以先做,因為這些修改都會被子程序繼承下來

4:例項執行

#define ERROR_EXIT(m)\
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while(0)

int main(int argc, char **argv)
{
    time_t t;
    int fd, i;
    mydaemon();
    fd = open("./mydaemon.log", O_RDWR|O_CREAT, 0644);
    if(fd < 0)
        ERROR_EXIT("open /mydaemon.log failed!");
    for(i=0; i<3; i++)
    {
        t = time(0);
        char *buf = asctime(localtime(&t));
        write(fd, buf, strlen(buf));
        sleep(10);
    }
    close(fd);
    return 0;
}

上圖是main函式,執行結果如下圖:

有圖可知,在open /mydaemon.log檔案沒有許可權,而切換到root許可權後執行成功,檔案的內容也是每10秒間隔寫入一次時間。因為我建立的mydaemon程式的工作目錄已經切換到了根目錄,所以普通使用者沒有在根目錄下建立檔案的許可權。如果這裡將檔案建立在當前目錄的話就不用切換到root許可權。

5:函式daemon()

  其實在linux下已經有函式daemon函式用於建立一個後臺程式了,所以上面的工作已經被加入了函式庫,直接使用輪子。

  5.1 daemon函式原型及描述

#include <unistd.h>
int daemon(int nochdir, int noclose);

DESCRIPTION
  The daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system daemons.
  If nochdir is zero, daemon() changes the process's current working directory to the root directory ("/"); otherwise,the current working directory is left unchanged.
  If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are mad to these file descriptors.

通過man手冊的描述可知,函式daemon接收兩個引數:

  nochdir:如果是0,將當前工作目錄切換到根目錄"/",否則工作目錄不改變。

  noclose:如果是0,將0,1,2重定向到/dev/null,否則不變。

  5.2 mydaemon和daemon

  其實可以將mydeamon函式稍加修改符合daemon函式

void mydaemon(int nochdir, int noclose)
{    
    pid_t pid;
    int fd, i, nfiles;
    struct rlimit rl;

    pid = fork();
    if(pid < 0)
        ERROR_EXIT("First fork failed!");

    if(pid > 0)
        exit(EXIT_SUCCESS);// father exit

    if(setsid() == -1)
        ERROR_EXIT("setsid failed!");

    pid = fork();
    if(pid < 0)
        ERROR_EXIT("Second fork failed!");

    if(pid > 0)// father exit
        exit(EXIT_SUCCESS);
   #ifdef RLIMIT_NOFILE
    /* 關閉從父程序繼承來的檔案描述符 */
    if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
        ERROR_EXIT("getrlimit failed!");
    nfiles = rl.rlim_cur = rl.rlim_max;
    setrlimit(RLIMIT_NOFILE, &rl);
    for(i=3; i<nfiles; i++)
        close(i);
    #endif
    /* 重定向標準的3個檔案描述符 */
    if(!noclose)
    {
        if(fd = open("/dev/null", O_RDWR) < 0)
            ERROR_EXIT("open /dev/null failed!");
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);    
        if(fd > 2)
            close(fd);
    }

    /* 改變工作目錄和檔案掩碼常量 */
    if(!nochdir)
        chdir("/");
    umask(0);
}

 

問題:這樣普通使用者呼叫mydaemon(0,0)函式時還是會在console提示open mydaemon.log failed!: Permission denied,但是在37~39行已經將它們重定向到了/dev/null了,很疑惑??當你close(0,1,2)之後普通使用者才不會提示錯誤資訊。