站長資訊網
        最全最豐富的資訊網站

        什么是daemon?PHP中如何實現daemon?

        守護進程(daemon)是一類在后臺運行的特殊進程,用于執行特定的系統任務。本篇文章帶大家了解一下PHP中實現daemon的方法,介紹一下編程中需要注意的地方。

        什么是daemon?PHP中如何實現daemon?

        PHP實現守護進程可以通過 pcntlposix 擴展實現。

        編程中需要注意的地方有:

        • 通過二次 pcntl_fork() 以及 posix_setsid 讓主進程脫離終端
        • 通過 pcntl_signal() 忽略或者處理 SIGHUP 信號
        • 多進程程序需要通過二次 pcntl_fork() 或者 pcntl_signal() 忽略 SIGCHLD 信號防止子進程變成 Zombie 進程
        • 通過 umask() 設定文件權限掩碼,防止繼承文件權限而來的權限影響功能
        • 將運行進程的 STDIN/STDOUT/STDERR 重定向到 /dev/null 或者其他流上

        如果要做的更好,還需要注意:

        • 如果通過 root 啟動,運行時更換到低權限用戶身份
        • 及時 chdir() 防止操作錯誤路徑
        • 多進程程序考慮定時重啟,防止內存泄露

        什么是daemon

        文章的主角守護進程(daemon),Wikipedia 上的定義是:

        在一個多任務的電腦操作系統中,守護進程(英語:daemon,/?di?m?n/或/?de?m?n/)是一種在后臺執行的電腦程序。此類程序會被以進程的形式初始化。守護進程程序的名稱通常以字母“d”結尾:例如,syslogd就是指管理系統日志的守護進程。
        通常,守護進程沒有任何存在的父進程(即PPID=1),且在UNIX系統進程層級中直接位于init之下。守護進程程序通常通過如下方法使自己成為守護進程:對一個子進程運行fork,然后使其父進程立即終止,使得這個子進程能在init下運行。這種方法通常被稱為“脫殼”。

        UNIX環境高級編程(第二版)(以下使用簡稱 APUE 指代) 13章有云:

        守護進程也成精靈進程( daemon )是生存周期較長的一種進程。它們常常在系統自舉時啟動,僅在系統關閉時才終止。因為他們沒有控制終端,所以說他們是在后臺運行的。

        這里注意到,daemon有如下特征:

        • 沒有終端
        • 后臺運行
        • 父進程 pid 為1

        想要查看運行中的守護進程可以通過 ps -ax 或者 ps -ef 查看,其中 -x 表示會列出沒有控制終端的進程。

        實現關注點

        二次 fork 與 setsid

        fork 系統調用

        fork 系統調用用于復制一個與父進程幾乎完全相同的進程,新生成的子進程不同的地方在于與父進程有著不同的 pid 以及有不同的內存空間,根據代碼邏輯實現,父子進程可以完成一樣的工作,也可以不同。子進程會從父進程中繼承比如文件描述符一類的資源。

        PHP 中的 pcntl 擴展中實現了 pcntl_fork() 函數,用于在 PHP 中 fork 新的進程。

        setsid 系統調用

        setsid 系統調用則用于創建一個新的會話并設定進程組 id。

        這里有幾個概念:會話進程組

        在 Linux 中,用戶登錄產生一個會話(Session),一個會話中包含一個或者多個進程組,一個進程組又包含多個進程。每個進程組有一個組長(Session Leader),它的 pid 就是進程組的組 id。進程組長一旦打開一個終端,這一個終端就被稱為控制終端。一旦控制終端發生異常(斷開、硬件錯誤等),會發出信號到進程組組長。

        后臺運行程序(如 shell 中以&結尾執行指令)在終端關閉之后也會被殺死,就是沒有處理好控制終端斷開時發出的SIGHUP信號,而SIGHUP信號對于進程的默認行為則是退出進程。

        調用 setsid 系統調用之后,會讓當前的進程新建一個進程組,如果在當前進程中不打開終端的話,那么這一個進程組就不會存在控制終端,也就不會出現因為關閉終端而殺死進程的問題。

        PHP 中的 posix 擴展中實現了 posix_setsid() 函數,用于在 PHP 中設定新的進程組。

        孤兒進程

        父進程比子進程先退出,子進程就會變成孤兒進程。

        init 進程會收養孤兒進程,即孤兒進程的 ppid 變為 1。

        二次 fork 的作用

        首先,setsid 系統調用不能由進程組組長調用,會返回-1。

        二次 fork 操作的樣例代碼如下:

        $pid1 = pcntl_fork();  if ($pid1 > 0) {     exit(0); } else if ($pid1 < 0) {     exit("Failed to fork 1n"); }  if (-1 == posix_setsid()) {     exit("Failed to setsidn"); }  $pid2 = pcntl_fork();  if ($pid2 > 0) {     exit(0); } else if ($pid2 < 0) {     exit("Failed to fork 2n"); }

        假定我們在終端中執行應用程序,進程為 a,第一次 fork 會生成子進程 b,如果 fork 成功,父進程 a 退出。b 作為孤兒進程,被 init 進程托管。

        此時,進程 b 處于進程組 a 中,進程 b 調用 posix_setsid 要求生成新的進程組,調用成功后當前進程組變為 b。

        此時進程 b 事實上已經脫離任何的控制終端,例程:

        <?php  cli_set_process_title('process_a');  $pidA = pcntl_fork();  if ($pidA > 0) {     exit(0); } else if ($pidA < 0) {     exit(1); }  cli_set_process_title('process_b');  if (-1 === posix_setsid()) {     exit(2); }  while(true) {     sleep(1); }

        執行程序之后:

        ?  ~ php56 2fork1.php ?  ~ ps ax | grep -v grep | grep -E 'process_|PID'   PID TTY      STAT   TIME COMMAND 28203 ?        Ss     0:00 process_b

        從 ps 的結果來看,process_b 的 TTY 已經變成了 ,即沒有對應的控制終端。

        代碼走到這里,似乎已經完成了功能,關閉終端之后 process_b 也沒有被殺死,但是為什么還要進行第二次 fork 操作呢?

        StackOverflow 上的一個回答寫的很好:

        The second fork(2) is there to ensure that the new process is not a session leader, so it won’t be able to (accidentally) allocate a controlling terminal, since daemons are not supposed to ever have a controlling terminal.

        這是為了防止實際的工作的進程主動關聯或者意外關聯控制終端,再次 fork 之后生成的新進程由于不是進程組組長,是不能申請關聯控制終端的。

        綜上,二次 fork 與 setsid 的作用是生成新的進程組,防止工作進程關聯控制終端。

        SIGHUP 信號處理

        一個進程收到 SIGHUP 信號的默認動作是結束進程。

        SIGHUP 會在如下情況下發出:

        • 控制終端斷開,SIGHUP 發送到進程組組長
        • 進程組組長退出,SIGHUP 會發送到進程組中的前臺進程
        • SIGHUP 常被用于通知進程重載配置文件(APUE 中提及,daemon 由于沒有控制終端,被認為不可能會收到這一個信號,所以選擇復用)

        由于實際的工作進程不在前臺進程組中,而且進程組的組長已經退出并且沒有控制終端,不處理正常情況下當然也沒有問題,然而為了防止偶然的收到 SIGHUP 導致進程退出,也為了遵循守護進程程序設計的慣例,還是應當處理這一信號。

        Zombie 進程處理

        何為 Zombie 進程

        簡單來說,子進程先于父進程退出,父進程沒有調用 wait 系統調用處理,進程變為 Zombie 進程。

        子進程先于父進程退出時,會向父進程發送 SIGCHLD 信號,如果父進程沒有處理,子進程也會變為 Zombie 進程。

        Zombie 進程會占用可 fork 的進程數,Zombie 進程過多會導致無法 fork 新的進程。

        此外,Linux 系統中 ppid 為 init 進程的進程,變為 Zombie 后會由 init 進程回收管理。

        Zombie 進程的處理

        從 Zombie 進程的特點,對于多進程的daemon,可以通過兩個途徑解決這一問題:

        • 父進程處理 SIGCHLD 信號
        • 讓子進程被 init 接管

        父進程處理信號無需多說,注冊信號處理回調函數,調用回收方法即可。

        對于讓子進程被 init 接管,則可以通過2次 fork 的方法,讓第一次 fork 出的子進程 a 再 fork 出實際的工作進程 b,讓 a 先行退出,使得 b 成為孤兒進程,這樣就能被 init 進程托管了。

        umask

        umask 會從父進程中繼承,影響創建文件的權限。

        PHP 手冊上提到:

        umask() 將 PHP 的 umask 設定為 mask & 0777 并返回原來的 umask。當 PHP 被作為服務器模塊使用時,在每個請求結束后 umask 會被恢復。

        如果父進程的 umask 沒有設定好,那么在執行一些文件操作時,會出現意想不到的效果:

        ?  ~ cat test_umask.php <?php         chdir('/tmp');         umask(0066);         mkdir('test_umask', 0777); ?  ~ php test_umask.php ?  ~ ll /tmp | grep umask drwx--x--x 2 root root 4.0K 8月  22 17:35 test_umask

        所以,為了保證每一次都能按照預期的權限操作文件,需要置0 umask 值。

        重定向0/1/2

        這里的0/1/2分別指的是 STDIN/STDOUT/STDERR,即標準輸入/輸出/錯誤三個流。

        樣例

        首先來看一個樣例:

        <?php  // not_redirect_std_stream_daemon.php  $pid1 = pcntl_fork();  if ($pid1 > 0) {     exit(0); } else if ($pid1 < 0) {     exit("Failed to fork 1n"); }  if (-1 == posix_setsid()) {     exit("Failed to setsidn"); }  $pid2 = pcntl_fork();  if ($pid2 > 0) {     exit(0); } else if ($pid2 < 0) {     exit("Failed to fork 2n"); }  umask(0); declare(ticks = 1); pcntl_signal(SIGHUP, SIG_IGN);  echo getmypid() . "n";  while(true) {     echo time() . "n";     sleep(10); }

        上述代碼幾乎完成了文章最開始部分提及的各個方面,唯一不同的是沒有對標準流做處理。通過 php not_redirect_std_stream_daemon.php 指令也能讓程序在后臺進行。

        sleep 的間隙,關閉終端,會發現進程退出。

        通過 strace 觀察系統調用的情況:

        ?  ~ strace -p 6723 Process 6723 attached - interrupt to quit restart_syscall(<... resuming interrupted call ...>) = 0 write(1, "1503417004n", 11)            = 11 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 nanosleep({10, 0}, 0x7fff71a30ec0)      = 0 write(1, "1503417014n", 11)            = -1 EIO (Input/output error) close(2)                                = 0 close(1)                                = 0 munmap(0x7f35abf59000, 4096)            = 0 close(0)                                = 0

        發現發生了 EIO 錯誤,導致進程退出。

        原因很簡單,即我們編寫的 daemon 程序使用了當時啟動時終端提供的標準流,當終端關閉時,標準流變得不可讀不可寫,一旦嘗試讀寫,會導致進程退出。

        在信海龍的博文《一個echo引起的進程崩潰》中也提到過類似的問題。

        解決方案

        APUE 樣例

        APUE 13.3中提到過一條編程規則(第6條):

        某些守護進程打開 /dev/null 時期具有文件描述符0、1和2,這樣,任何一個視圖讀標準輸入、寫標準輸出或者標準錯誤的庫例程都不會產生任何效果。因為守護進程并不與終端設備相關聯,所以不能在終端設備上顯示器輸出,也無從從交互式用戶那里接受輸入。及時守護進程是從交互式會話啟動的,但因為守護進程是在后臺運行的,所以登錄會話的終止并不影響守護進程。如果其他用戶在同一終端設備上登錄,我們也不會在該終端上見到守護進程的輸出,用戶也不可期望他們在終端上的輸入會由守護進程讀取。

        簡單來說:

        • daemon 不應使用標準流
        • 0/1/2 要設定成 /dev/null

        例程中使用:

        for (i = 0; i < rl.rlim_max; i++) 	close(i);  fd0 = open("/dev/null", O_RDWR); fd1 = dup(0); fd2 = dup(0);

        實現了這一個功能。dup() (參考手冊)系統調用會復制輸入參數中的文件描述符,并復制到最小的未分配文件描述符上。所以上述例程可以理解為:

        關閉所有可以打開的文件描述符,包括標準輸入輸出錯誤; 打開/dev/null并賦值給變量fd0,因為標準輸入已經關閉了,所以/dev/null會綁定到0,即標準輸入; 因為最小未分配文件描述符為1,復制文件描述符0到文件描述符1,即標準輸出也綁定到/dev/null; 因為最小未分配文件描述符為2,復制文件描述符0到文件描述符2,即標準錯誤也綁定到/dev/null;復制代碼

        開源項目實現:Workerman

        Workerman 中的 Worker.php 中的 resetStd() 方法實現了類似的操作。

        /** * Redirect standard input and output. * * @throws Exception */ public static function resetStd() {    if (!self::$daemonize) {        return;    }    global $STDOUT, $STDERR;    $handle = fopen(self::$stdoutFile, "a");    if ($handle) {        unset($handle);        @fclose(STDOUT);        @fclose(STDERR);        $STDOUT = fopen(self::$stdoutFile, "a");        $STDERR = fopen(self::$stdoutFile, "a");    } else {        throw new Exception('can not open stdoutFile ' . self::$stdoutFile);    } }

        Workerman 中如此實現,結合博文,可能與 PHP 的 GC 機制有關,對于 fd 0 1 2來說,PHP 會維持對這三個資源的引用計數,在直接 fclose 之后,會使得這幾個 fd 對應的資源類型的變量引用計數為0,導致觸發回收。所需要做的就是將這些變量變為全局變量,保證引用的存在。

        推薦學習:《PHP視頻教程》

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 久久精品无码专区免费青青| 国产精品久久久久影视不卡| 久久99国产乱子伦精品免费| 国产精品尹人在线观看| 91精品国产91久久综合| 曰韩精品无码一区二区三区| 国产一区二区精品久久凹凸| 日韩精品一区二区三区四区| 国产精品无码无需播放器| 亚洲欧美精品午睡沙发| 国产乱子伦精品免费视频| 国内精品一级毛片免费看| 国语精品一区二区三区| 亚洲国产精品一区二区久久hs| 久久e热在这里只有国产中文精品99| 青草青草久热精品视频在线网站| 久久99国产综合精品女同| 中文字幕精品无码久久久久久3D日动漫 | 久久久无码人妻精品无码| 欧美精品高清在线xxxx| 久久97久久97精品免视看| 国产欧美日韩精品专区| 国产精品热久久无码av| 国产精品美女免费视频观看| 国产精品秘入口福利姬网站| 97精品人妻一区二区三区香蕉| 久久青青草原精品影院| 青青草精品视频| 国产成人精品免费大全| 国产成人精品亚洲精品| 国产AV国片精品一区二区| 97精品在线播放| 国产在线精品国自产拍影院| 国产精品哟女在线观看| 久久精品国产亚洲精品| 人妻少妇看A偷人无码精品视频| 乱人伦人妻精品一区二区| 久久久精品视频免费观看| 久久久久久久久久久免费精品| 欧美日韩成人精品久久久免费看 | 漂亮人妻被黑人久久精品|