一、什麼是守護進程
1、守護進程的引入
在現實生活中, 許多大型的軟件或服務器必須保證7*24小時(一週7天,一天24小時)無障礙的運行,例如淘寶網、百度搜索引擎、支付寶等等,那麼像這樣一種要一直運行的程序怎麼實現呢?究其本質其實就是我們的守護進程。
2、守護進程的定義
守護進程也稱精靈進程(Daemon),是運行在後臺的一種特殊進程。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程。Linux的大多數服務器就是用守護進程實現的。比如,Internet服務器inetd,Web服務器httpd等。同時,守護進程完成許多系統任務。比如,作業規劃進程crond等。
3、守護進程的特點
Linux系統啓動時會啓動很多系統服務進程,這些系統服 務進程沒有控制終端,不能直接和用戶交互。其它進程都是在用戶登錄或運行程序時創建,在運行結束或用戶註銷時終止,但系統服務進程不受用戶登錄註銷的影響,它們一直在運行着。這種進程有一個名稱叫守護進程(Daemon)。
在Linux中,每個系統與用戶進行交流的界面成爲終端,每一個從此終端開始運行的進程都會依附於這個終端,這個終端被稱爲這些進程的控制終端,當控制終端被關閉的時候,相應的進程都會自動關閉。但是守護進程卻能突破這種限制,它脫離於終端並且在後臺運行,並且它脫離終端的目的是爲了避免進程在運行的過程中的信息在任何終端中顯示並且進程也不會被任何終端所產生的終端信息所打斷。它從被執行的時候開始運轉,知道整個系統關閉才退出(當然可以認爲的殺死相應的守護進程)。如果想讓某個進程不因爲用戶或中斷或其他變化而影響,那麼就必須把這個進程變成一個守護進程。
守護進程與終端無任何關聯?
用戶的登錄與註銷與守護進程無關係,不受其影響,守護進程自成進程組,自成會話 ,即pid = gid = sid。
二、守護進程的創建
1、查看系統中的進程
ps axj
參數a表示不僅列當前用戶的進程,也列出所有其他用戶的進程,
參數x表示不僅列有控制終端的進程,也列出所無控制終端的進程,
參數j表示列出與作業控制相關的信息。
凡是TPGID一欄寫着-1的都是沒有控制終端的進程,也就是守護進程。在COMMAND一列用[]括起來的 名字表示內核線程,這些線程在內核裏創建,沒有用戶空間代碼,因此沒有程序文件名和命令行, 通常採用以k開頭的名字,表示Kernel。init進程我們已經很熟悉了,udevd負責維
護/dev目錄下的設備⽂文件,acpid負責電源管理,syslogd負責維護/var/log下的日誌文件,可以看出,守護進程通 常採用以d結尾的名字,表示Daemon。
2、setsid函數
1>創建守護進程最關鍵的一步是調用setsid函數創建一個新的Session,併成爲Session Leader。
#include<unistd.h>
pid_t setsid(void);
返回值:該函數調用成功時返回新創建的Session的id(其實也就是當前進程的id),出錯返回-1。
2>需要注意的是,,調用這個函數之前,當前進程不允許是進程組的Leader,否則該函數返回-1。
解決辦法:先fork再調用setsid,fork創建的子進程和父進程在同一個進 程組中,進程組的Leader必然是該組的第一個進程,所以子進程不可能是該組的第一個進程,在子 進程中調用setsid就不會有問題了。
3>成功調用該函數的結果是:
1. 創建一個新的Session,當前進程成爲Session Leader,當前進程的id就是Session的id。
2. 創建一個新的進程組,當前進程成爲進程組的Leader,當前進程的id就是進程組的id。
3. 如果當前進程原本有一個控制終端,則它失去這個控制終端,成爲一個沒有控制終端的進程。所謂失去控制終端是指,原來的控制終端仍然是打開的,仍然可以讀寫,但只是一個普通的打開文件而不是控制終端了。
3、創建守護進程的步驟
1>調用umask將文件模式創建屏蔽字設置爲0.
umask(0);//umask必須清0,否則創建文件受系統默認權限的影響
文件權限掩碼是屏蔽掉文件權限中的對應位。由於使用fork()函數新創建的子進程繼承了父進程的文件權限掩碼,這就給該子進程使用文件帶了很多的麻煩(比如父進程中的文件沒有執行文件的權限,然而在子進程中希望執行相應的文件這個時候就會出問題)。因此在子進程中要把文件的權限掩碼設置成爲0,即在此時有最大的權限,這樣可以大大增強該守護進程的靈活性。
2>調用fork,父進程退出(exit)。
原因:
1)如果該守護進程是作爲一條簡單的shell命令啓動的,那麼⽗父進程終止使得shell認爲該命令已經執行完畢。
2)保證子進程不是一個進程組的組長進程。
3>調用setsid創建一個新會話。
setsid會導致:
1)調用進程成爲新會話的首進程。
2)調用進程成爲一個進程組的組長進程 。
3)調用進程沒有控制終端。(再次fork一次,保證daemon進程,之後不會打開tty設備)
調用setsid的原因:
由於創建守護進程的第一步是調用fork()函數來創建子進程,再將父進程退出。由於在調用了fork()函數的時候,子進程拷貝了父進程的會話期、進程組、控制終端等資源、雖然父進程退出了,但是會話期、進程組、控制終端等並沒有改變,因此,需要用setsid()韓式來時該子進程完全獨立出來,從而擺脫其他進程的控制。
4>將當前工作目錄更改爲根目錄。
防止當前目錄有一個目錄被刪除,導致守護進程無效。
使用fork()創建的子進程是繼承了父進程的當前工作目錄,由於在進程運行中,當前目錄所在的文件系統是不能卸載的,這對以後使用會造成很多的麻煩。因此通常的做法是讓“/”作爲守護進程的當前目錄,當然也可以指定其他的別的目錄來作爲守護進程的工作目錄。
5>關閉不再需要的文件描述符。
同文件權限碼一樣,用fork()函數新建的子進程會從父進程那裏繼承一些已經打開了的文件。這些文件被打開的文件可能永遠不會被守護進程讀寫,如果不進行關閉的話將會浪費系統的資源,造成進程所在的文件系統無法卸下以及引起預料的錯誤。
如:關閉標準輸入流、標準輸出流、標準錯誤流。
close(0);
close(1);
close(2);
6>其他:忽略SIGCHLD信號。
signal(SIGCHLD,SIG_IGN);
4、自己創建守護進程:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
void mydaemon()
{
umask(0);//將文件模式創建屏蔽字設爲0
pid_t pid = fork();//創建子進程
if (pid == -1)
perror("fork error");
else if (pid == 0)//child
{
setsid();//創建新會話
if (chdir("/") < 0)//更改當前的工作目錄
perror("chdir error");
close(0);//關閉標準輸入流的文件描述符
close(1);//關閉標準輸出流的文件描述符
close(2);//關閉標準錯誤流的文件描述符
signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信號
}
else//father
{
exit(1);//父進程退出
}
}
int main()
{
mydaemon();
while (1);
return 0;
}
ps axj | grep mydaemon查看精靈進程:
這裏寫圖片描述
5、調用系統函數daemon創建守護進程
#include <unistd.h>
int daemon(int nochdir, int noclose);
1>daemon函數主要用於希望脫離控制檯,以守護進程的形式在後臺運行的程序。
2>當nochdir爲0時,daemon將更改當前進程的目錄爲root(“/”)目錄。
3> 當noclose爲0時,daemon將進程的STDIN,STDOUT,STDERR都重定向到/dev/null。
/dev/null:linux下的黑洞,寫入的所有數據會直接丟棄。
用daemon函數創建守護進程:
#include<unistd.h>
#include<stdio.h>
int main()
{
daemon(1, 1);//創建守護進程
while (1);
return 0;
}
三、如何殺死守護進程?
1> 利用ps axj | grep 守護進程名字找到相應的守護進程,然後用kill -9 進程號將對應進程殺死。
2>利用ps -ef命令查找相應的守護進程,再用kill命令將其殺死。
3>也可創建shell腳本對進程的啓動、關閉、重啓進行自動管理。
四、爲什麼有人創建守護進程會fork兩次?
一個daemon函數常見的實現:
int daemon(void)
{
pid_t pid = fork(); //第一次fork
if( pid != 0 )
exit(0);//parent
//first children
if(setsid() == -1)
{
printf("setsid failed\n");
assert(0);
exit(-1);
}
umask(0);
pid = fork(); //第二次fork
if( pid != 0)
exit(0);
//second children
chdir ("/");
for (int i = 0; i < 3; i++)
{
close (i);
}
int stdfd = open ("/dev/null", O_RDWR);
dup2(stdfd, STDOUT_FILENO);
dup2(stdfd, STDERR_FILENO);
return 0;
}
可以看到上面的代碼裏我fork了兩次,雖然說這並不是必須的,但是這的確是對守護進程做出了一些更優化的操作。
首先第一次fork:這裏第一次fork的作用就是讓shell認爲這條命令已經終止,不用掛在終端輸入上;再一個是爲了後面的setsid服務,因爲調用setsid函數的進程不能是進程組組長(會報錯Operation not permitted),如果不fork子進程,那麼此時的父進程是進程組組長,無法調用setsid。所以到這裏子進程便成爲了一個新會話組的組長。
第二次fork:第二次fork是爲了避免後期進程誤操作而再次打開終端。因爲打開一個控制終端的前提條件是該進程必須爲會話組組長,而我們通過第二次fork,確保了第二次fork出來的子進程不會是會話組組長。
下面羅列一下控制終端會產生哪些信號。程序中只要處理好這些信號,同樣能達到上面函數實現的目的。
//後臺進程讀取/寫入終端輸入產生下面兩個信號,或者控制終端不存在情況讀取和寫入會產生
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
//按CTRL-C ,CTRL-\ CTRL-Z會向前臺進程組發送下面這些信號
signal(SIGINT, SIG_IGN );
signal(SIGQUIT, SIG_IGN );
signal(SIGTSTP, SIG_IGN );
//終端斷開,會給會話組長或孤兒進程組所有成員發送下面信號
signal(SIGHUP, SIG_IGN );
還有有些信號也可以由終端shell產生,需要關注
signal(SIGCONT, SIG_IGN );
signal(SIGSTOP, SIG_IGN );
上面這些信號,應該有些程序缺省處理(SIG_ DFL)本身動作就是忽略(SIG_IGN),不是退出進程。不過按照上面寫也不會造成什麼問題。