您好,登錄后才能下訂單哦!
這篇文章主要介紹“PHP中daemon的概念和用法”,在日常操作中,相信很多人在PHP中daemon的概念和用法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”PHP中daemon的概念和用法”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
守護進程(daemon)是一類在后臺運行的特殊進程,用于執(zhí)行特定的系統(tǒng)任務(wù)。本篇文章帶大家了解一下PHP中實現(xiàn)daemon的方法,介紹一下編程中需要注意的地方。
PHP實現(xiàn)守護進程可以通過 pcntl
與 posix
擴展實現(xiàn)。
編程中需要注意的地方有:
通過二次 pcntl_fork()
以及 posix_setsid
讓主進程脫離終端
通過 pcntl_signal()
忽略或者處理 SIGHUP
信號
多進程程序需要通過二次 pcntl_fork()
或者 pcntl_signal()
忽略 SIGCHLD
信號防止子進程變成 Zombie 進程
通過 umask()
設(shè)定文件權(quán)限掩碼,防止繼承文件權(quán)限而來的權(quán)限影響功能
將運行進程的 STDIN/STDOUT/STDERR
重定向到 /dev/null
或者其他流上
如果要做的更好,還需要注意:
如果通過 root 啟動,運行時更換到低權(quán)限用戶身份
及時 chdir()
防止操作錯誤路徑
多進程程序考慮定時重啟,防止內(nèi)存泄露
文章的主角守護進程(daemon),Wikipedia 上的定義是:
在一個多任務(wù)的電腦操作系統(tǒng)中,守護進程(英語:daemon,/?di?m?n/或/?de?m?n/)是一種在后臺執(zhí)行的電腦程序。此類程序會被以進程的形式初始化。守護進程程序的名稱通常以字母“d”結(jié)尾:例如,syslogd就是指管理系統(tǒng)日志的守護進程。
通常,守護進程沒有任何存在的父進程(即PPID=1),且在UNIX系統(tǒng)進程層級中直接位于init之下。守護進程程序通常通過如下方法使自己成為守護進程:對一個子進程運行fork,然后使其父進程立即終止,使得這個子進程能在init下運行。這種方法通常被稱為“脫殼”。
UNIX環(huán)境高級編程(第二版)(以下使用簡稱 APUE 指代) 13章有云:
守護進程也成精靈進程( daemon )是生存周期較長的一種進程。它們常常在系統(tǒng)自舉時啟動,僅在系統(tǒng)關(guān)閉時才終止。因為他們沒有控制終端,所以說他們是在后臺運行的。
這里注意到,daemon有如下特征:
沒有終端
后臺運行
父進程 pid 為1
想要查看運行中的守護進程可以通過 ps -ax
或者 ps -ef
查看,其中 -x
表示會列出沒有控制終端的進程。
fork 系統(tǒng)調(diào)用用于復(fù)制一個與父進程幾乎完全相同的進程,新生成的子進程不同的地方在于與父進程有著不同的 pid 以及有不同的內(nèi)存空間,根據(jù)代碼邏輯實現(xiàn),父子進程可以完成一樣的工作,也可以不同。子進程會從父進程中繼承比如文件描述符一類的資源。
PHP 中的 pcntl
擴展中實現(xiàn)了 pcntl_fork()
函數(shù),用于在 PHP 中 fork 新的進程。
setsid 系統(tǒng)調(diào)用則用于創(chuàng)建一個新的會話并設(shè)定進程組 id。
這里有幾個概念:會話
,進程組
。
在 Linux 中,用戶登錄產(chǎn)生一個會話(Session),一個會話中包含一個或者多個進程組,一個進程組又包含多個進程。每個進程組有一個組長(Session Leader),它的 pid 就是進程組的組 id。進程組長一旦打開一個終端,這一個終端就被稱為控制終端。一旦控制終端發(fā)生異常(斷開、硬件錯誤等),會發(fā)出信號到進程組組長。
后臺運行程序(如 shell 中以&
結(jié)尾執(zhí)行指令)在終端關(guān)閉之后也會被殺死,就是沒有處理好控制終端斷開時發(fā)出的SIGHUP
信號,而SIGHUP
信號對于進程的默認(rèn)行為則是退出進程。
調(diào)用 setsid
系統(tǒng)調(diào)用之后,會讓當(dāng)前的進程新建一個進程組,如果在當(dāng)前進程中不打開終端的話,那么這一個進程組就不會存在控制終端,也就不會出現(xiàn)因為關(guān)閉終端而殺死進程的問題。
PHP 中的 posix
擴展中實現(xiàn)了 posix_setsid()
函數(shù),用于在 PHP 中設(shè)定新的進程組。
父進程比子進程先退出,子進程就會變成孤兒進程。
init 進程會收養(yǎng)孤兒進程,即孤兒進程的 ppid 變?yōu)?1。
首先,setsid
系統(tǒng)調(diào)用不能由進程組組長調(diào)用,會返回-1。
二次 fork 操作的樣例代碼如下:
$pid1 = pcntl_fork(); if ($pid1 > 0) { exit(0); } else if ($pid1 < 0) { exit("Failed to fork 1\n"); } if (-1 == posix_setsid()) { exit("Failed to setsid\n"); } $pid2 = pcntl_fork(); if ($pid2 > 0) { exit(0); } else if ($pid2 < 0) { exit("Failed to fork 2\n"); }
假定我們在終端中執(zhí)行應(yīng)用程序,進程為 a,第一次 fork 會生成子進程 b,如果 fork 成功,父進程 a 退出。b 作為孤兒進程,被 init 進程托管。
此時,進程 b 處于進程組 a 中,進程 b 調(diào)用 posix_setsid
要求生成新的進程組,調(diào)用成功后當(dāng)前進程組變?yōu)?b。
此時進程 b 事實上已經(jīng)脫離任何的控制終端,例程:
<?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); }
執(zhí)行程序之后:
? ~ php56 2fork1.php ? ~ ps ax | grep -v grep | grep -E 'process_|PID' PID TTY STAT TIME COMMAND 28203 ? Ss 0:00 process_b
從 ps 的結(jié)果來看,process_b 的 TTY 已經(jīng)變成了 ?
,即沒有對應(yīng)的控制終端。
代碼走到這里,似乎已經(jīng)完成了功能,關(guān)閉終端之后 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.
這是為了防止實際的工作的進程主動關(guān)聯(lián)或者意外關(guān)聯(lián)控制終端,再次 fork 之后生成的新進程由于不是進程組組長,是不能申請關(guān)聯(lián)控制終端的。
綜上,二次 fork 與 setsid 的作用是生成新的進程組,防止工作進程關(guān)聯(lián)控制終端。
一個進程收到 SIGHUP
信號的默認(rèn)動作是結(jié)束進程。
而 SIGHUP
會在如下情況下發(fā)出:
控制終端斷開,SIGHUP 發(fā)送到進程組組長
進程組組長退出,SIGHUP 會發(fā)送到進程組中的前臺進程
SIGHUP 常被用于通知進程重載配置文件(APUE 中提及,daemon 由于沒有控制終端,被認(rèn)為不可能會收到這一個信號,所以選擇復(fù)用)
由于實際的工作進程不在前臺進程組中,而且進程組的組長已經(jīng)退出并且沒有控制終端,不處理正常情況下當(dāng)然也沒有問題,然而為了防止偶然的收到 SIGHUP
導(dǎo)致進程退出,也為了遵循守護進程程序設(shè)計的慣例,還是應(yīng)當(dāng)處理這一信號。
簡單來說,子進程先于父進程退出,父進程沒有調(diào)用 wait
系統(tǒng)調(diào)用處理,進程變?yōu)?Zombie 進程。
子進程先于父進程退出時,會向父進程發(fā)送 SIGCHLD
信號,如果父進程沒有處理,子進程也會變?yōu)?Zombie 進程。
Zombie 進程會占用可 fork 的進程數(shù),Zombie 進程過多會導(dǎo)致無法 fork 新的進程。
此外,Linux 系統(tǒng)中 ppid 為 init 進程的進程,變?yōu)?Zombie 后會由 init 進程回收管理。
從 Zombie 進程的特點,對于多進程的daemon,可以通過兩個途徑解決這一問題:
父進程處理 SIGCHLD
信號
讓子進程被 init 接管
父進程處理信號無需多說,注冊信號處理回調(diào)函數(shù),調(diào)用回收方法即可。
對于讓子進程被 init 接管,則可以通過2次 fork 的方法,讓第一次 fork 出的子進程 a 再 fork 出實際的工作進程 b,讓 a 先行退出,使得 b 成為孤兒進程,這樣就能被 init 進程托管了。
umask 會從父進程中繼承,影響創(chuàng)建文件的權(quán)限。
PHP 手冊上提到:
umask() 將 PHP 的 umask 設(shè)定為 mask & 0777 并返回原來的 umask。當(dāng) PHP 被作為服務(wù)器模塊使用時,在每個請求結(jié)束后 umask 會被恢復(fù)。
如果父進程的 umask 沒有設(shè)定好,那么在執(zhí)行一些文件操作時,會出現(xiàn)意想不到的效果:
? ~ 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
所以,為了保證每一次都能按照預(yù)期的權(quán)限操作文件,需要置0 umask 值。
這里的0/1/2分別指的是 STDIN/STDOUT/STDERR
,即標(biāo)準(zhǔn)輸入/輸出/錯誤三個流。
首先來看一個樣例:
<?php // not_redirect_std_stream_daemon.php $pid1 = pcntl_fork(); if ($pid1 > 0) { exit(0); } else if ($pid1 < 0) { exit("Failed to fork 1\n"); } if (-1 == posix_setsid()) { exit("Failed to setsid\n"); } $pid2 = pcntl_fork(); if ($pid2 > 0) { exit(0); } else if ($pid2 < 0) { exit("Failed to fork 2\n"); } umask(0); declare(ticks = 1); pcntl_signal(SIGHUP, SIG_IGN); echo getmypid() . "\n"; while(true) { echo time() . "\n"; sleep(10); }
上述代碼幾乎完成了文章最開始部分提及的各個方面,唯一不同的是沒有對標(biāo)準(zhǔn)流做處理。通過 php not_redirect_std_stream_daemon.php
指令也能讓程序在后臺進行。
在 sleep
的間隙,關(guān)閉終端,會發(fā)現(xiàn)進程退出。
通過 strace
觀察系統(tǒng)調(diào)用的情況:
? ~ strace -p 6723 Process 6723 attached - interrupt to quit restart_syscall(<... resuming interrupted call ...>) = 0 write(1, "1503417004\n", 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, "1503417014\n", 11) = -1 EIO (Input/output error) close(2) = 0 close(1) = 0 munmap(0x7f35abf59000, 4096) = 0 close(0) = 0
發(fā)現(xiàn)發(fā)生了 EIO 錯誤,導(dǎo)致進程退出。
原因很簡單,即我們編寫的 daemon 程序使用了當(dāng)時啟動時終端提供的標(biāo)準(zhǔn)流,當(dāng)終端關(guān)閉時,標(biāo)準(zhǔn)流變得不可讀不可寫,一旦嘗試讀寫,會導(dǎo)致進程退出。
在信海龍的博文《一個echo引起的進程崩潰》中也提到過類似的問題。
APUE 13.3中提到過一條編程規(guī)則(第6條):
某些守護進程打開
/dev/null
時期具有文件描述符0、1和2,這樣,任何一個視圖讀標(biāo)準(zhǔn)輸入、寫標(biāo)準(zhǔn)輸出或者標(biāo)準(zhǔn)錯誤的庫例程都不會產(chǎn)生任何效果。因為守護進程并不與終端設(shè)備相關(guān)聯(lián),所以不能在終端設(shè)備上顯示器輸出,也無從從交互式用戶那里接受輸入。及時守護進程是從交互式會話啟動的,但因為守護進程是在后臺運行的,所以登錄會話的終止并不影響守護進程。如果其他用戶在同一終端設(shè)備上登錄,我們也不會在該終端上見到守護進程的輸出,用戶也不可期望他們在終端上的輸入會由守護進程讀取。
簡單來說:
daemon 不應(yīng)使用標(biāo)準(zhǔn)流
0/1/2 要設(shè)定成 /dev/null
例程中使用:
for (i = 0; i < rl.rlim_max; i++) close(i); fd0 = open("/dev/null", O_RDWR); fd1 = dup(0); fd2 = dup(0);
實現(xiàn)了這一個功能。dup()
(參考手冊)系統(tǒng)調(diào)用會復(fù)制輸入?yún)?shù)中的文件描述符,并復(fù)制到最小的未分配文件描述符上。所以上述例程可以理解為:
關(guān)閉所有可以打開的文件描述符,包括標(biāo)準(zhǔn)輸入輸出錯誤; 打開/dev/null并賦值給變量fd0,因為標(biāo)準(zhǔn)輸入已經(jīng)關(guān)閉了,所以/dev/null會綁定到0,即標(biāo)準(zhǔn)輸入; 因為最小未分配文件描述符為1,復(fù)制文件描述符0到文件描述符1,即標(biāo)準(zhǔn)輸出也綁定到/dev/null; 因為最小未分配文件描述符為2,復(fù)制文件描述符0到文件描述符2,即標(biāo)準(zhǔn)錯誤也綁定到/dev/null;復(fù)制代碼
Workerman 中的 Worker.php 中的 resetStd()
方法實現(xiàn)了類似的操作。
/** * 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 中如此實現(xiàn),結(jié)合博文,可能與 PHP 的 GC 機制有關(guān),對于 fd 0 1 2來說,PHP 會維持對這三個資源的引用計數(shù),在直接 fclose 之后,會使得這幾個 fd 對應(yīng)的資源類型的變量引用計數(shù)為0,導(dǎo)致觸發(fā)回收。所需要做的就是將這些變量變?yōu)槿肿兞?,保證引用的存在。
到此,關(guān)于“PHP中daemon的概念和用法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。