從 0 開始學(xué)習(xí) Linux 系列之「18.守護(hù)進(jìn)程」

daemon

版權(quán)聲明:本文為 cdeveloper 原創(chuàng)文章,可以隨意轉(zhuǎn)載,但必須在明確位置注明出處!

什么是守護(hù)進(jìn)程?

守護(hù)進(jìn)程可以簡(jiǎn)單的理解為后臺(tái)的服務(wù)進(jìn)程,很多上層的服務(wù)器都是以守護(hù)進(jìn)程為基礎(chǔ)開發(fā)的。例如 Linux 上運(yùn)行的 Apache 服務(wù)器,Android 系統(tǒng)的 Service 服務(wù),它們的底層都由 Linux 的守護(hù)進(jìn)程提供服務(wù)。

這篇文章介紹的是在 Linux 編寫守護(hù)進(jìn)程的方法。

編寫守護(hù)進(jìn)程的 6 個(gè)步驟

先來(lái)看看整體的編寫步驟:

  1. 重新設(shè)置 umask(0)
  2. 執(zhí)行 fork 并脫離父進(jìn)程
  3. 重啟 session 會(huì)話
  4. 改變當(dāng)前工作目錄
  5. 關(guān)閉文件描述符
  6. 固定文件描述符 0, 1, 2 到 /dev/null

下面我們來(lái)寫一個(gè)守護(hù)進(jìn)程的初始化程序 daemon_init.c 來(lái)學(xué)習(xí)這 6 個(gè)步驟。

1. 重新設(shè)置 umask

進(jìn)程從創(chuàng)建他的父進(jìn)程那里繼承文件創(chuàng)建的掩碼,它可能修改守護(hù)進(jìn)程所創(chuàng)建的文件的權(quán)限,這里清除它:

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    umask(0);
}

2. 執(zhí)行 fork 并脫離父進(jìn)程

既然是服務(wù)進(jìn)程,意味著是可以獨(dú)立運(yùn)行的,不會(huì)因?yàn)楦高M(jìn)程退出而銷毀,在 Linux 系統(tǒng)中有一個(gè)進(jìn)程號(hào)為 1 的 init 進(jìn)程,這個(gè)進(jìn)程一直存在與系統(tǒng)中,當(dāng)我們的子進(jìn)程從父進(jìn)程脫離后,子進(jìn)程變?yōu)楣聝哼M(jìn)程,隨之系統(tǒng)的 init 進(jìn)程會(huì)接管對(duì)這個(gè)進(jìn)程的控制。我們看看代碼:

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    // 2. 脫離父進(jìn)程
    pid_t pid = fork()  ; 

    if(pid < 0)
        exit(1);    // 進(jìn)程創(chuàng)建失敗
    else if(pid > 0)
        exit(0);    // 退出父進(jìn)程

    return 0;
}

3. 重啟 session 會(huì)話

使用 setsid 重新開啟一個(gè) session 會(huì)話,防止脫離父進(jìn)程的子進(jìn)程重新受控于字符終端進(jìn)程,盡量加上這一步防備工作讓 fork 的子進(jìn)程直接受控于 init 進(jìn)程,要知道編寫守護(hù)進(jìn)程的核心是讓子進(jìn)程直接受控于 init 進(jìn)程

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    // 2. 脫離父進(jìn)程
    // 3. 重啟 session 會(huì)話
    setsid();
}

4. 改變工作目錄

進(jìn)程活動(dòng)時(shí),其工作目錄所在的文件系統(tǒng)不能卸載,一般需要將守護(hù)進(jìn)程工作目錄改變到根目錄 /

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    // 2. 脫離父進(jìn)程
    // 3. 重啟 session 會(huì)話
    // 4. 改變工作目錄
    chdir("/");
}

5. 關(guān)閉文件描述符

進(jìn)程從創(chuàng)建它的父進(jìn)程哪里繼承了打開的文件描述符,若不關(guān)閉將會(huì)造成資源浪費(fèi),造成進(jìn)程所在的文件系統(tǒng)無(wú)法卸下以及引起無(wú)法預(yù)料的錯(cuò)誤:

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    // 2. 脫離父進(jìn)程
    // 3. 重啟 session 會(huì)話
    // 4. 改變工作目錄
    // 5. 得到并關(guān)閉文件描述符
    struct rlimit rl;
    getrlimit(RLIMIT_NOFILE, &rl);
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for(int i = 0; i < rl.rlim_max; i++)    
        close(i); 
}

6. 固定文件描述符 0, 1, 2 到 /dev/null

守護(hù)進(jìn)程在后臺(tái)運(yùn)行,不會(huì)與用戶發(fā)生直接的交互,我們不希望在終端上看到守護(hù)進(jìn)程的輸出,用戶也不期望他們?cè)诮K端上的輸入被守護(hù)進(jìn)程讀取,因此我們將文件描述符 0, 1, 2 定位到 /dev/null

void daemon_init(void) {
    // 1. 重新設(shè)置 umask
    // 2. 脫離父進(jìn)程
    // 3. 重啟 session 會(huì)話
    // 4. 改變工作目錄
    // 5. 得到并關(guān)閉文件描述符
    // 6. 固定文件描述符 0, 1, 2 到 /dev/null
    int fd0 = open("/dev/null", O_RDWR);
    int fd1 = dup(0);
    int fd2 = dup(0);
}

這樣,我們 daemon_init 就寫好了,可以用這個(gè)函數(shù)來(lái)初始化一個(gè)守護(hù)進(jìn)程了,我們來(lái)測(cè)試測(cè)試。

測(cè)試 daemon_init 初始化函數(shù)

我們編寫一個(gè) printlg.c 循環(huán)向文件中輸出信息:

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

void daemon_init(void);

int main(void) {
    // 初始化守護(hù)進(jìn)程
    daemon_init();
    
    // 守護(hù)進(jìn)程邏輯
    char *msg = "I'm printlg process...\n" ;
    int msg_len = strlen(msg);

    int fd = open("/tmp/test_printlg.log", O_RDWR | O_CREAT | O_APPEND, 0666); 
    if(fd < 0) { 
        printf("open /tmp/test_printlg.log fail.\n");
        exit(1);
    }

    while(1) {
        // 每隔 3s 輸出 msg 到 /tmp/test_printlg.log 文件中
        write(fd, msg, msg_len); 
        sleep(3); 
    }

    close(fd);

    return 0;
}

完整的代碼參考:printlg.c,下面我們來(lái)測(cè)試這個(gè)守護(hù)進(jìn)程能夠工作。

測(cè)試 printlg.c 守護(hù)進(jìn)程

編譯

gcc printlg.c -o printlg

運(yùn)行

./printlg

結(jié)果最終寫入到 /tmp/test_printlg.log 文件中,我們每隔 3 s 查看這個(gè)文件的內(nèi)容,發(fā)現(xiàn)內(nèi)容在不斷增多的:

cat /tmp/test_printlg.log

# 結(jié)果
I'm printlg process...
I'm printlg process...
I'm printlg process...

到此為止,一個(gè)守護(hù)進(jìn)程就寫好了,但是很多的守護(hù)進(jìn)程都可以開機(jī)自啟動(dòng),我們的可不可以呢?

讓守護(hù)進(jìn)程開機(jī)自啟動(dòng)

很多的守護(hù)進(jìn)程都設(shè)置了開機(jī)自啟動(dòng),我們也來(lái)讓 printlg 能夠開機(jī)自啟動(dòng),先來(lái)了解自啟動(dòng)的原理。

每個(gè)系統(tǒng)啟動(dòng)級(jí)別的守護(hù)進(jìn)程分別在 /etc/rcN.d 下,比如我的圖形界面的啟動(dòng)級(jí)別是 5,那么在這個(gè)啟動(dòng)級(jí)別下自動(dòng)運(yùn)行和禁止啟動(dòng)守護(hù)進(jìn)程都在 /etc/rc5.d 下。這里我的 ubuntu 系統(tǒng)的守護(hù)進(jìn)程目錄是 /etc/rcN.d,如果你是其他的 Linux 可能會(huì)不太一樣,你可以使用 whereis rc[N].d 來(lái)看看具體的目錄位置,不要死記硬背。

這是我的 ubuntu 的 /etc/rc5.d 下的守護(hù)進(jìn)程:

mydaemon

知道了守護(hù)進(jìn)程的位置,現(xiàn)在就可以把 printlg 放在 /etc/rc5.d/ 下,并且還要改名稱,因?yàn)橄到y(tǒng)需要根據(jù)指定的名稱來(lái)使用 for 循環(huán)來(lái)啟動(dòng)或者關(guān)閉每個(gè)程序,命名規(guī)則如下:

  1. S[num][name]:?jiǎn)?dòng)守護(hù)進(jìn)程 name,例如:S01printlg
  2. K[num][name]:禁止啟動(dòng)守護(hù)進(jìn)程 name,例如:K01printlg

我們這里肯定要啟動(dòng)了,所以將 printlg 命名為 S01printlg

mv printlg /etc/rc5.d/S01printlg

之后我們重啟機(jī)器,再次查看 /tmp/test_printlg 文件,就可以看到服務(wù)已經(jīng)啟動(dòng)了,這樣守護(hù)進(jìn)程就成功自啟動(dòng)啦。

結(jié)語(yǔ)

這次我們學(xué)習(xí)了如何在 Linux 編寫一個(gè)守護(hù)進(jìn)程,守護(hù)進(jìn)程其實(shí)就是一個(gè)后臺(tái)的服務(wù)程序,學(xué)習(xí)如何在 Linux 創(chuàng)建服務(wù)程序還是非常有必要的,因?yàn)槲覀儫o(wú)時(shí)無(wú)刻都在使用很多系統(tǒng)提供的服務(wù),了解原理以后再使用 Linux 的服務(wù)會(huì)更加得心應(yīng)手。

最后,感謝你的閱讀,我們下次再見 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容