Linux系統啟動時會啟動很多系統服務進程,例如第 1.3 節 “網絡登錄過程”講的inetd,這些系統服務進程沒有控制終端,不能直接和用戶交互。其它進程都是在用戶登錄或運行程序時創建,在運行結束或用戶註銷時終止,但系統服務進程不受用戶登錄註銷的影響,它們一直在運行着。這種進程有一個名稱叫守護進程(Daemon)。
下面我們用ps axj命令查看系統中的進程。參數a表示不僅列當前用戶的進程,也列出所有其他用戶的進程,參數x表示不僅列有控制終端的進程,也列出所有無控制終端的進程,參數j表示列出與作業控制相關的信息。
$ ps axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:01 /sbin/init
0 2 0 0 ? -1 S< 0 0:00 [kthreadd]
2 3 0 0 ? -1 S< 0 0:00 [migration/0]
2 4 0 0 ? -1 S< 0 0:00 [ksoftirqd/0]
...
1 2373 2373 2373 ? -1 S<s 0 0:00 /sbin/udevd --daemon
...
1 4680 4680 4680 ? -1 Ss 0 0:00 /usr/sbin/acpid -c /etc
...
1 4808 4808 4808 ? -1 Ss 102 0:00 /sbin/syslogd -u syslog
...凡是TPGID一欄寫着-1的都是沒有控制終端的進程,也就是守護進程。在COMMAND一列用[]括起來的名字表示內核綫程,這些綫程在內核裡創建,沒有用戶空間代碼,因此沒有程序檔案名和命令行,通常採用以k開頭的名字,表示Kernel。init進程我們已經很熟悉了,udevd負責維護/dev目錄下的設備檔案,acpid負責電源管理,syslogd負責維護/var/log下的日誌檔案,可以看出,守護進程通常採用以d結尾的名字,表示Daemon。
創建守護進程最關鍵的一步是調用setsid函數創建一個新的Session,並成為Session Leader。
#include <unistd.h> pid_t setsid(void);
該函數調用成功時返回新創建的Session的id(其實也就是當前進程的id),出錯返回-1。注意,調用這個函數之前,當前進程不允許是進程組的Leader,否則該函數返回-1。要保證當前進程不是進程組的Leader也很容易,只要先fork再調用setsid就行了。fork創建的子進程和父進程在同一個進程組中,進程組的Leader必然是該組的第一個進程,所以子進程不可能是該組的第一個進程,在子進程中調用setsid就不會有問題了。
成功調用該函數的結果是:
創建一個新的Session,當前進程成為Session Leader,當前進程的id就是Session的id。
創建一個新的進程組,當前進程成為進程組的Leader,當前進程的id就是進程組的id。
如果當前進程原本有一個控制終端,則它失去這個控制終端,成為一個沒有控制終端的進程。所謂失去控制終端是指,原來的控制終端仍然是打開的,仍然可以讀寫,但只是一個普通的打開檔案而不是控制終端了。
例 34.2. 創建守護進程
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
void daemonize(void)
{
pid_t pid;
/*
* Become a session leader to lose controlling TTY.
*/
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
} else if (pid != 0) /* parent */
exit(0);
setsid();
/*
* Change the current working directory to the root.
*/
if (chdir("/") < 0) {
perror("chdir");
exit(1);
}
/*
* Attach file descriptors 0, 1, and 2 to /dev/null.
*/
close(0);
open("/dev/null", O_RDWR);
dup2(0, 1);
dup2(0, 2);
}
int main(void)
{
daemonize();
while(1);
}為了確保調用setsid的進程不是進程組的Leader,首先fork出一個子進程,父進程退出,然後子進程調用setsid創建新的Session,成為守護進程。按照守護進程的慣例,通常將當前工作目錄切換到根目錄,將檔案描述符0、1、2重定向到/dev/null。Linux也提供了一個庫函數daemon(3)實現我們的daemonize函數的功能,它帶兩個參數指示要不要切換工作目錄到根目錄,以及要不要把檔案描述符0、1、2重定向到/dev/null。
$ ./a.out
$ ps
PID TTY TIME CMD
11494 pts/0 00:00:00 bash
13271 pts/0 00:00:00 ps
$ ps xj | grep a.out
1 13270 13270 13270 ? -1 Rs 1000 0:05 ./a.out
11494 13273 13272 11494 pts/0 13272 S+ 1000 0:00 grep a.out
(關閉終端窗口重新打開,或者註銷重新登錄)
$ ps xj | grep a.out
1 13270 13270 13270 ? -1 Rs 1000 0:21 ./a.out
13282 13338 13337 13282 pts/1 13337 S+ 1000 0:00 grep a.out
$ kill 13270運行這個程序,它變成一個守護進程,不再和當前終端關聯。用ps命令看不到,必須運行帶x參數的ps命令才能看到。另外還可以看到,用戶關閉終端窗口或註銷也不會影響守護進程的運行。