從進程組、會話、終端的概念深入理解守護進程
一、寫在前面
「守護進程」是 Linux 的一種長期運行的后台服務進程,也有人稱它為「精靈進程」。我們常見的 httpd、named、sshd 等服務都是以守護進程 Daemon 方式運行的,通常服務名稱以字母d結尾,也就是 Daemon 第一個字母。與普通進程相比它大概有如下特點:
- 無需控制終端(不需要與用戶交互)
- 在后台運行
- 生命周期比較長,一般是隨系統啟動和關閉
二、守護進程必要性
為什么要設置為守護進程,普通進程不可以嗎?
當我們在命令行提示符后輸入類似./helloworld
程序時,在程序運行時終端被占用,此時無法執行其它操作。即使使用./helloworld &
方式后台運行,當連接終端的網絡出現問題,那么也會導致運行程序中斷。這些因素對於長期運行的服務來說很不友好,而「守護進程」可以很好的解決這個問題。
三、對進程組、會話、終端的理解
「守護進程」理解起來並不復雜,代碼編寫上有基本固定的套路。如果想要深入理解「守護進程」基本原理,那么必須要首先理解 Linux 的進程、進程組、會話、終端等概念。
1、進程
- 進程是 Linux 進行資源分配的最小單位
- 前台進程,例如這樣:
$ ./hello
- 后台進程,例如這樣:
$ ./hello &
釋放對控制終端的占用
2、進程組
每個進程都會屬於一個進程組,進程組中可以包含一個或多個進程。進程組中有一個進程組長,組長的進程 ID 是進程組 ID(PGID)
$ ps -o pid,pgid,ppid,comm | cat
PID PGID PPID COMMAND
10179 10179 10177 bash
10263 10263 10179 ps
10264 10263 10179 cat
下邊通過簡單的示例來理解進程組
- bash:進程和進程組ID都是 10179,父進程其實是 sshd(10177)
- ps:進程和進程組ID都是 10263,父進程是 bash(10179),因為是在 Shell 上執行的命令
- cat:進程組 ID 與 ps 的進程組 ID 相同,父進程同樣是 bash(10179)
容易理解 Bash 就是Shell進程,Shell 父進程是 sshd;ps 與 cat 通過管道符號一起運行,屬於一個進程組,其父進程都是 Bash;一個進程組也被稱為「作業」。
3、會話(session)
多個進程組構成一個「會話」,建立會話的進程是會話的領導進程,該進程 ID 為會話的 SID。會話中的每個進程組稱為一個「作業」。會話可以有一個進程組稱為會話的「前台作業」,其它進程組為「后台作業」
一個會話可以有一個控制終端,當控制終端有輸入和輸出時都會傳遞給前台進程組,比如Ctrl + Z
。會話的意義在於能將多個作業通過一個終端控制,一個前台操作,其它后台運行。
4、前后台作業相關操作
讓作業由進入后台運行:
$ ping localhost >/dev/null &
[1] 10269 # 終端顯示
# [1]:作業ID 10269:進程組ID
給后台作業發信號 SIGTERM
$ kill -SIGTERM -10269 # 發信號給進程組
$ kill -SIGTERM %1 # 發信號給作業1
讓后台進程切換到前台:
$ fg %1
# ping 進程重新切到前台
四、編寫守護進程
編寫守護進程看似復雜,但實際上也是遵循一個特定的流程。
1、創建子進程,父進程退出
進程 fork 后,父進程退出。這么做的原因有 2 點:
- 如果守護進程是通過 Shell 啟動,父進程退出,Shell 就會認為任務執行完畢,這時子進程由 init 收養
- 子進程繼承父進程的進程組 ID,保證了子進程不是進程組組長,因為后邊調用
setsid()
要求必須不是進程組長
2、子進程創建新會話
調用setsid()
創建一個新的會話,並成為新會話組長。這個步驟主要是要與繼承父進程的會話、進程組、終端脫離關系。
3、禁止子進程重新打開終端
此刻子進程是會話組長,為了防止子進程重新打開終端,再次 fork 后退出父進程,也就是此子進程。這時子進程 2 不再是會話組長,無法再打開終端。其實這一步驟不是必須的,不過加上這一步驟會顯得更加嚴謹。
4、設置當前目錄為根目錄
如果守護進程的當前工作目錄是/usr/home
目錄,那么管理員在卸載/usr
分區時會報錯的。為了避免這個問題,可以調用chdir()
函數將工作目錄設置為根目錄/
。
5、設置文件權限掩碼
文件權限掩碼是指屏蔽掉文件權限中的對應位。由於使用 fork()
函數新建的子進程繼承了父進程的文件權限掩碼,這就給該子進程使用文件帶來了諸多的麻煩。因此,把文件權限掩碼設置為 0,可以大大增強該守護進程的靈活性。通常使用方法是umask(0)
。
6、關閉文件描述符
子進程會繼承已經打開的文件,它們占用系統資源,且可能導致所在文件系統無法卸載。此時守護進程與終端脫離,常說的輸入、輸出、錯誤描述符也應該關閉。
五、守護進程的出錯處理
由於守護進程脫離了終端,不能將錯誤信息輸出到控制終端,即使 gdb 也無法正常調試。常用的方法是使用 syslog 服務,將錯誤信息輸入到/var/log/messages
中。
syslog 是 Linux 中的系統日志管理服務,通過守護進程 syslogd 來維護。該守護進程在啟動時會讀一個配置文件/etc/syslog.conf
。該文件決定了不同種類的消息會發送向何處。
六、守護進程編碼示例
pid_t pid, sid;
int i;
openlog("daemon_syslog", LOG_PID, LOG_DAEMON);
pid = fork(); // 第1步
if (pid < 0) exit(-1);
else if (pid > 0) exit(0); // 父進程第一次退出
if ((sid = setsid()) < 0) // 第2步
{
syslog(LOG_ERR, "%s\n", "setsid");
exit(-1);
}
// 第3步 第二次父進程退出
if ((pid = fork()) > 0) exit(0);
if ((sid = chdir("/")) < 0) // 第4步
{
syslog(LOG_ERR, "%s\n", "chdir");
exit(-1);
}
umask(0); // 第5步
// 第6步:關閉繼承的文件描述符
for(i = 0; i < getdtablesize(); i++)
{
close(i);
}
while(1)
{
do_something();
}
closelog();
exit(0);
到這里基本上把守護進程的內容全部說清楚了,內容不少,概念比較晦澀,如果希望理解的比較透徹的話,可能需要多看幾遍了。