文章目錄
- 1、守護進程的概念
- 2、如何查看守護進程
- 3、編寫守護進程的步驟
- 3.1 創建子進程,父進程退出
- 3.2 在子進程中創建新會話
- 3.3 改變當前工作目錄
- 3.4 重設文件權限掩碼
- 3.5 關閉不需要的文件描述符
- 3.6 某些特殊的守護進程打開/dev/null
- 4、守護進程代碼示例
1、守護進程的概念
守護進程(Daemon Process),也就是通常說的 Daemon 進程(精靈進程),是 Linux 中的后臺服務進程。它是一個生存期較長的進程,通常獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。其他進程都是在用戶登錄或運行程序時創建,在運行結束或者用戶注銷時終止,但系統服務進程不受用戶登錄注銷的影響,它們一直運行,這就是守護進程。
守護進程是個特殊的孤兒進程
,這種進程脫離終端,為什么要脫離終端呢?之所以脫離于終端是為了避免進程被任何終端所產生的信息所打斷,其在執行過程中的信息也不在任何終端上顯示。由于在 Linux 中,每一個系統與用戶進行交流的界面稱為終端,每一個從此終端開始運行的進程都會依附于這個終端,這個終端就稱為這些進程的控制終端,當控制終端被關閉時,相應的進程都會自動關閉。
Linux 的大多數服務器就是用守護進程實現的。比如,Internet 服務器 inetd,Web 服務器 httpd 等。
2、如何查看守護進程
在終端上使用命令 ps axj
a:表示顯示所有進程,包括其他用戶的進程。
x:不僅可以顯示有控制終端的進程,也可以顯示沒有控制終端的進程。
j:表示列出與作業控制相關的信息。
上述字段含義如下:
PPID:父進程ID。
PID:當前進程ID。
PGID:當前進程的進程組ID。
SID:會話ID。
TTY:該進程在哪個終端下運作,其中“?”表示與終端機無關,例如守護進程
;tty1-tty6是本機上的登錄者進程;pts/0等表示網絡連接進主機的進程。
TPGID:終端進程組ID。
STAT:進程狀態,其中“S”表示睡眠狀態,“R”表示運行狀態,“Z”表示僵尸狀態,“T”表示停止狀態,“W”表示等待狀態。
UID:用戶ID。
TIME:該進程使用的CPU時間。
COMMAND:正在運行的進程的命令名。
從上圖中可以看出守護進程都有以下特點:
守護進程基本上都是以超級用戶啟動( UID 為 0 )
沒有控制終端( TTY 為 ?)
終端進程組 ID 為 -1 ( TPGID 表示終端進程組 ID)
注意:COMMAND字段帶有[ ]的叫內核守護進程,不帶[ ]的叫普通守護進程,也叫做用戶守護進程。
一般情況下,守護進程可以通過以下方式啟動:
- 在系統啟動時由啟動腳本啟動,這些啟動腳本通常放在 /etc/rc.d 目錄下
- 利用 inetd 超級服務器啟動,如 telnet 等
- 由 cron 定時啟動以及在終端用 nohup 啟動的進程也是守護進程
這里面存放的基本都是守護進程的腳本
在Linux中,守護進程有兩種方式,一種是svsy方式,一種是xinetd方式(超級守護進程)。 每個守護進程都會有一個腳本,可以理解成工作配置文件,守護進程的腳本需要放在指定位置,獨立啟動守護進程:放在/etc/rc.d 目錄下,當然也包括xinet的shell腳本;超級守護進程:按照xinet中腳本的指示,它所管理的守護進程位于/etc/xinetd.config目錄下。
sysv:
獨立啟動,一開機運行就會進入內存,一直處于listen狀態,即使該守護進程不運行也會一直占用系統資源,但是其最大的優點就是,它一直啟動,當有請求時會立即響應,響應速度快,比如http服務,這樣的進程都保存在/etc/rc.d/init.d目錄下
xinet d:
超級守護進程,管理眾多的進程,比如telnet服務。xinetd自己是一個sysv,它就像老板一樣,自己常駐于內存,管理其它的進程,其它進程就相當于它的員工,在其它進程沒有用時會睡眠,并不占用系統資源,當有工作時候老板xinetd會通知它的員工,喚醒某個進程來執行作業。這種方式適合于那些不是經常被人使用,不需要常駐內存的程序,但是此方式響應時間長,但是節省系統資源,方便管理。超級守護進程的配置文件是/etc/xinetd.conf,超級守護進程的子進程們存放在/etc/xinetd.d/目錄下
3、編寫守護進程的步驟
3.1 創建子進程,父進程退出
由于守護進程是脫離控制終端的,因此完成第一步后子進程變成后臺進程。之后的所有工作都在子進程中完成。而用戶通過 shell 可以執行其他的命令,從而在形式上做到了與控制終端的脫離。
雖然父進程退出了,但是子進程也不是進程組的組長進程,因為父進程退出,子進程成為孤兒進程,接著子進程會被init進程給領養,成為init 進程的子進程
父進程先退出,子進程就會成為孤兒進程
子進程退出,父進程沒有進行wait,子進程會成為僵尸進程
3.2 在子進程中創建新會話
這個步驟是創建守護進程中最重要的一步,在這里使用的函數是 setsid() 。
這里先要明確兩個概念:進程組和會話期。
進程組
進程組是一個或多個進程的集合。進程組由進程組 ID 來唯一標識。除了進程號( PID )之外,進程組 ID 也是一個進程的必備屬性。
每個進程組都有一個組長進程,其組長進程的進程號等于進程組 ID ,且進程組 ID 不會因組長進程的退出而受到影響。
會話期
會話期是一個或多個進程組的集合。通常一個會話開始于用戶登錄,終止于用戶退出;或者說開始于終端打開,結束于終端關閉。會話期的第一個進程稱為會話組長。在此期間該用戶運行的所有進程都屬于這個會話期。
進程組和會話期之間的關系如圖:
setsid()函數說明
使用指令 man 2 setsid 查看詳細信息
#include <sys/types.h>
#include <unistd.h>pid_t setsid(void);
功能:
??如果調用進程不是進程組長,則 setsid() 將創建一個新會話。調用進程將成為新會話的會話組組長(即,其會話 ID 與其進程 ID 相同)。同時調用進程也將成為會話中新進程組的進程組組長(即,其進程組 ID 與其進程 ID 相同)。調用進程將是新進程組和新會話中的唯一進程。
參數:無
返回:
??成功:返回調用進程的(新)會話ID
??失敗:返回(pid_t)-1,并設置 errno
上面已經提到,setsid() 函數用于創建一個新的會話,并擔任該會話的組長,所以調用 setsid() 有下面 3 個作用
1、讓進程擺脫原會話的控制
2、讓進程擺脫原進程組的控制
3、讓進程擺脫原控制終端的控制
由于在調用 fork() 函數時,子進程 全盤復制 了父進程的會話期進程組和控制終端等。所以雖然父進程退出了,但原先的 會話期、進程組、控制終端等并沒有改變,因此,子進程并不是真正意義上的獨立,而 setsid()
函數能夠使進程完全獨立出來,從而脫離所有其他進程的控制。
3.3 改變當前工作目錄
使用 fork() 函數創建的子進程是完全繼承了父進程的當前工作目錄,所以從父進程繼承過來的當前工作目錄可能是一個掛載的文件系統中。因為守護進程有一般情況是在系統在引導之前是一直從在的,所以在進程工作的過程中當前目錄所在的文件系統(比如“/mnt/usb” 等)是不能卸載的。
因此,一般的做法是將根目錄作為守護進程的當前工作目錄,這樣就可以避免上述問題。當然,如有特殊需要,也可以把當前工作目錄換成其他的路徑,如“/tmp”。
改變工作目錄的函數是 chdir() 函數,其函數原型如下所示:
#include <unistd.h>int chdir(const char *path);
功能:
??改變調用者的工作目錄
參數:
??path:新的工作目錄的路徑
返回:
??成功:返回0
??失敗:返回-1,同時設置errno
3.4 重設文件權限掩碼
文件權限掩碼(通常用八進制表示)的作用是屏蔽文件權限中的對應位。例如,如果文件權限掩碼是0050,它表示屏蔽了文件所屬用戶組的可讀與可執行權限。由于使用 fork() 函數新建的子進程繼承了父進程的文件權限掩碼,這就給該子進程使用文件帶來了一定的影響。如果守護進程需要創建文件,那么他可能需要設置特定的權限。因此,把文件權限掩碼設置為一個已知的值(通常設置為0),可以增強該守護進程的靈活性。
umask的數值共有四位,例如上面的輸出0050,四位數表示四組權限值,分別是文件特殊權限,文件所有者權限,文件所屬用戶組權限,其他用戶權限。
這里我們先忽略掉文件特殊權限位。
可讀權限r表示4,可寫權限w表示2,可執行權限x表示1
umask值指的是需要從原始默認權限減掉的權限!我們已經知道r、w、x的數值分別是4、2、1。 所以如果要去掉可讀和可執行權限,umask值中相應的位就是5
如果要去掉讀權限,那就是4,去掉讀與寫權限,就是6,去掉執行與寫權限,就是3,去掉寫的權限,就是5!
新建文件和目錄的默認權限值就是在原始默認權限的基礎上去掉umask值,umask值與原始默認權限共同決定了新建文件和目錄的默認權限值。
在使用open()建立新文件時, 該參數mode 并非真正建立文件的權限, 而是 (mode&~umask)的權限值。
設置文件權限掩碼的函數是 umask()
。在這里,通常的使用方法為 umask(0)
。其函數原型如下所示:
#include <sys/types.h>
#include <sys/stat.h>mode_t umask(mode_t mask);
功能:
??umask() 將調用進程的文件模式創建掩碼(umask)設置為 mask & 0777(即僅使用掩碼的文件權限位)。
參數:
??mask:要設置的權限值,用八進制表示
返回:
??此系統調用始終成功,并返回掩碼的上一個值。
3.5 關閉不需要的文件描述符
同樣地,用 fork() 函數新建的子進程會從父進程那里繼承一些已經打開了的文件。這些被打開的文件可能永遠不會被守護進程訪問,但它們一樣占用系統資源,而且還可能導致所在的文件系統無法被卸載。
特別是守護進程和終端無關,所以指向終端設備的標準輸入、標準輸出和標準錯誤流等已經不再使用,應當被關閉。
可以使用函數 getdtablesize()
來獲取當前進程文件描述符表的大小,并通過使用 close() 來依次關閉。
函數原型如下:
#include <unistd.h>
int getdtablesize(void);
getdtablesize()函數返回進程可以打開的最大文件數,比文件描述符的最大可能值多一個。
#include <unistd.h>
int close(int fd);
close() 用于關閉文件描述符,關閉成功則返回 0,失敗則返回 -1 并設置 errno
所以關閉文件描述符的代碼可以如下寫法:
int num = getdtablesize(); // 獲取當前進程文件描述符表大小for (int i = 0; i < num; i++)
{close (i);
}
3.6 某些特殊的守護進程打開/dev/null
某些特殊的守護進程打開/dev/null,使其具有文件描述符0、1、2,這樣任何一個試圖讀標準輸入、標準輸出、標準出錯時都不會有任何效果,這樣符合了守護進程不與終端設備相關聯的屬性。
/dev/null 是Linux下的黑洞文件,向里面寫入的所有數據都將被忽略
4、守護進程代碼示例
#include <stdio.h> //for perror...
#include <string.h> //for strlen...
#include <stdlib.h> //for EXIT_FAILURE EXIT_SUCCESS...
#include <fcntl.h> //for O_RDWR | O_CREAT | O_APPEND...
#include <unistd.h> //for fork chidr setsid getdtablesize close...
#include <sys/types.h> //for umask...
#include <signal.h> //for signal...volatile sig_atomic_t runing = 1;void sigint_handler(int sig)
{int fd = open("/tmp/dameon.log2", O_RDWR | O_CREAT | O_APPEND, 0644);char *p = "守護進程運行結束!\n";write(fd, p, strlen(p));close(fd);runing = 0;
}int main()
{// 創建子進程,父進程退出pid_t id = fork();if (id == -1){perror("fork");exit(EXIT_FAILURE);}if (id > 0) // 父進程{printf("父進程id:%d\n", getpid());exit(EXIT_SUCCESS);}//打印子進程號printf("子進程id:%d\n", getpid());// 在子進程中創建新會話pid_t temp_pid = setsid();// 改變當前的工作路徑chdir("/");// 改變進程本身的umaskumask(0);int num = getdtablesize(); /* 獲取當前進程文件描述符表大小 */int i = 0;for (i = 0; i < num; i++){close(i);}// 屏蔽一些控制終端操作的信號signal(SIGTTOU, SIG_IGN);signal(SIGTTIN, SIG_IGN);signal(SIGTSTP, SIG_IGN);signal(SIGHUP, SIG_IGN);// 對SIGINT進行捕獲signal(SIGINT, sigint_handler);while (runing){int fd = open("/tmp/dameon.log", O_RDWR | O_CREAT | O_APPEND, 0644);if (fd == -1){perror("open");exit(EXIT_FAILURE);}char *p = "這個一個守護進程!\n";write(fd, p, strlen(p));close(fd);sleep(3);}return 0;
}
編譯然后運行
查看對應的日志文件
向這個進程發送2號信號,進程則會捕獲到2號信號,觸發自定義函數,再次查看進程,發現進程已經結束