php萬字碼出完美守護進程詳解

前事提要

上期我們詳細學習瞭會話的概念以及用法,會話,進程組,終端的理解對本篇講述的守護進程極其重要,如還不理解相關概念建議翻看我往期關於會話,進程組,終端文章。

基本概念

守護進程(Daemon Process),也就是通常說的 Daemon 進程(精靈進程),是 Linux 中的後臺服務進程。通常獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。並且不跟任何的控制終端關聯,如果想讓某個進程不因為用戶或中斷或其他變化而影響,那麼就必須把這個進程變成一個守護進程。

常見的守護進程包括系統日志進程syslogd、 web服務器httpd、任務規劃守護進程crond,數據庫服務器mysqld等。一般采用以 d 結尾的名字。

查看系統守護進程命令 ps -efj

基本特點

生存周期長[非必須],一般操作系統啟動的時候就啟動,關閉的時候關閉。

守護進程和終端無關聯,也就是他們沒有控制終端,所以當控制終端退出,也不會導致守護進程退出。

守護進程是在後臺運行,不會占著終端,終端可以執行其他命令

守護進程的父進程是1號進程,也就是init進程;

  • 在Linux中 , 大概有三種方式實現腳本後臺化 :

1 . 在命令後添加一個&符號 , 比如 php task.php & . 這個方法的缺點在於 如果terminal終端關閉 , 無論是正常關閉還是非正常關閉 , 這個php進程都會隨著終端關閉而關閉 , 其次是代碼中如果有echo或者print_r之類的輸出文本 , 會被輸出到當前的終端窗口中 .

2 . 使用nohup命令 , 比如 nohup php task.php & . 默認情況下 , 代碼中echo或者print_r之類輸出的文本會被輸出到php代碼同級目錄的nohup.out文件中 . 如果你用exit命令或者關閉按鈕等正常手段關閉終端 , 該進程不會被關閉 , 依然會在後臺持續運行 . 但是如果終端遇到異常退出或者終止 , 該php進程也會隨即退出 . 本質上 , 也並非穩定可靠的daemon方案 .

3 . 使用fork和setsid , 我暫且稱之為 : *nix解決方案

創建守護進程要求

  • 1. 設置文件創建屏蔽字 umask(0)

文件創建屏蔽字是指屏蔽掉文件創建時的對應位(umask() 控制系統文件和目錄默認權限)。由於使用fork系統調用新建的子進程繼承瞭父進程的文件創建掩碼,這就給該子進程使用文件帶來瞭諸多的不便。因此,把文件創建掩碼設置為0,可以大大增強該守護進程的靈活性。

  • 2. 調用fork,父進程退出(exit);

如果該守護進程是作為一條簡單的shell命令啟動的,那麼父進程終止使得shell認為該命令已經執行完畢;保證子進程不是一個進程組的組長進程,為什麼要保證不是進程組組長呢? 因為進程組組長調用setsid創建會話會報錯;

  • 3. 子進程調用setsid 函數來創建會話

先介紹一下Linux中的進程與控制終端,登錄會話和進程組之間的關系:進程屬於一個進程組,進程組號(GID)就是進程組長的進程號(PID)。登錄會話可以包含多個進程組。這些進程組共享一個控制終端。這個控制終端通常是創建進程的登錄終端。

控制終端,登錄會話和進程組通常是從父進程繼承下來的。我們的目的就是要擺脫它們,使之不受它們的影響。方法是在第2點的基礎上,調用setsid()使進程成為會話組長:

setsid()調用成功後,進程成為新的會話組長和新的進程組長,並與原來的登錄會話和進程組脫離。由於會話過程對控制終端的獨占性,進程同時與控制終端脫離。

調用setsid有3個作用:

讓進程擺脫原會話的控制;

讓進程擺脫原進程組的控制;

讓進程擺脫原控制終端的控制

  • 4. 把守護進程工作目錄設置為根目錄 chdir(“/”);

從父進程繼承過來的工作目錄可能在一個掛載的文件系統中。由於守護進程通常在系統再引導之前是一直存在的,所以如果守護進程的當前工作目錄在一個掛載的文件系統中,會導致該文件系統不能被卸載。

  • 5.把一些文件描述符關閉 【標準輸入,標準輸出,標準錯誤】

文件描述符:用來標識一個文件。當你打開一個存在的文件或者創建一個新文件,操作系統都會返回這個文件描述符。後續對這個文件的操作的一些函數,都會用到這個文件描述符作為參數。

Linux中三個特殊的文件描述符,數字分別為0,1,2:

0:標準輸入[鍵盤],對應的符號常量叫 STDIN_FILENO

1:標準輸出[屏幕],對應的符號常量叫 STDOUT_FILENO

2:標準錯誤[屏幕],對應的符號常量叫STDERR_FILENO

進程從創建它的父進程那裡繼承瞭打開的文件描述符。如不關閉,將會浪費系統資源,造成進程所在的文件系統無法卸下以及引起無法預料的錯誤。

  • 6. 當調用setsid函數後,一般會在創建一個子進程,讓會話首進程退出,確保該進程不會再獲得控制終端

(1)調用一次fork的作用:

第一次fork的作用是讓shell認為這條命令已經終止,不用掛在終端輸入上,還有就是為瞭後面的setsid服務,因為調用setsid函數的進程不能是進程組組長,如果不fork出子進程,則此時的父進程是進程組組長,就無法調用setsid。當子進程調用完setsid函數之後,子進程是會話組長也是進程組組長,並且脫離瞭控制終端,此時,不管控制終端如何操作,新的進程都不會收到一些信號使得進程退出。

(2)第二次fork的作用:

雖然當前關閉瞭和終端的聯系,但是後期可能會誤操作打開瞭終端。

隻有會話首進程能打開終端設備, 也就是再fork一次,再把父進程退出,再次fork的子進程作為守護進程繼續運行,保證瞭該守護進程不再是會話的首進程。

第二次不是必須的,是可選的。

  • 7.編寫一個守護進程
<?php
// 1. 設置文件創建屏蔽字
umask(0);
// 2. fork 子進程
$pid = pcntl_fork();

if($pid > 0){
        print("父進程退出\n");
        exit(0);
}
//3. 設置當前子進程為會話首進程,進程組長,斷開與終端的連接,成為後臺進程
if(-1 === posix_setsid()){
        print("sid err \n");
}
// 4. 把守護進程工作目錄設置為根目錄
chdir("/");
//已經成為守護進程~\(≧▽≦)/~啦
while(1){
echo "test".PHP_EOL;
sleep(2);
}

將文件保存為daemon.php,然後php daemon.php執行文件,嗯,執行結果卻有些奇怪,大概類似於下圖:

即便你按Ctrl+C都沒用,終端在不斷輸出test,唯一辦法就是關閉當前終端窗口然後重新開一個,為什麼會這樣,這就涉及到我們上面提到的第5點,需要關閉繼承過來的標準輸出,輸入,錯誤,這樣我們的daemon程序不可以再將終端窗口當作默認的標準輸出瞭。

<?php
// 設置文件創建屏蔽字
umask(0);
// 第一次fork 子進程
$pid = pcntl_fork();
if($pid > 0){
        print("父進程退出\n");
        exit(0);
}
//設置當前子進程為會話首進程,進程組長,斷開與終端的連接,成為後臺進程
if(-1 === posix_setsid()){
        print("sid err \n");
}
//第二次fork 徹底斷開控制終端
$pid = pcntl_fork();
if($pid > 0){
	exit(0);//讓會話首進程退出
}
// 把守護進程工作目錄設置為根目錄
chdir("/");
// 關閉標準輸入,標準輸出,標準錯誤,linux 中使用數字表示文件描述符也就是 0,1,2
fclose(STDIN);//0
fclose(STDOUT);//1
fclose(STDERR);//2
//當關掉以上標準輸出,標準輸入,標準錯誤之後,如果後面要對文件操作(比如打開一個文件,寫入,創建)它返回的文件描述符從0開始,這樣可能造成未知異常
//為瞭避免問題,我們使用輸出從定向到 /dev/null 空設備文件解決這個問題,重新設置0,1,2 文件描述符用來代替標準輸入,標準輸出,標準錯誤,往 /dev/null 寫入數據會被丟棄,這樣就不會向終端輸出數據瞭。
$stdin = fopen("/dev/null",'a');
$stdout = fopen("/dev/null",'a');
$stderr = fopen("/dev/null", 'a');
//已經成為守護進程~\(≧▽≦)/~啦
while(1){
echo "test".PHP_EOL;
sleep(2);
}

空設備

/dev/null : 是一個特殊的設備文件,它丟棄一切寫入其中的數據(像黑洞一些)例如:echo “大雷編程” > /dev/null 輸出重定向文件到黑洞(無任何輸出)。

我們一般把守護進程的標準輸入、標準輸出重定向到空設備(黑洞),從而確保守護進程不從鍵盤接收任何東西,也不把輸出結果打印到屏幕。

到此這篇關於php萬字碼出完美守護進程詳解的文章就介紹到這瞭,更多相關php守護進程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: