
版權(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)看看整體的編寫步驟:
- 重新設(shè)置 umask(0)
- 執(zhí)行 fork 并脫離父進(jìn)程
- 重啟 session 會(huì)話
- 改變當(dāng)前工作目錄
- 關(guān)閉文件描述符
- 固定文件描述符 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)程:

知道了守護(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ī)則如下:
-
S[num][name]:?jiǎn)?dòng)守護(hù)進(jìn)程 name,例如:S01printlg -
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)手。
最后,感謝你的閱讀,我們下次再見 :)