守護程式(daemon)是一類在背景運行的特殊進程,用於執行特定的系統任務。本篇文章帶大家了解PHP中實作daemon的方法,介紹一下程式設計中需要注意的地方。
PHP實作守護程式可以透過 pcntl
與 posix
擴充功能實作。
程式設計中需要注意的地方有:
- 透過二次化
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 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"); }
假定我們在終端機中執行應用程序,進程為a,第一次fork 會產生子進程b,如果fork 成功,父親進程a 退出。 b 作為孤兒進程,被 init 進程所託管。
此時,進程 b 處於進程組 a 中,進程 b 呼叫 posix_setsid
要求產生新的進程組,呼叫成功後目前進程組變成 b。
此時進程b 事實上已經脫離任何的控制終端,程式:
<?php cli_set_process_title(&#39;process_a&#39;); $pidA = pcntl_fork(); if ($pidA > 0) { exit(0); } else if ($pidA < 0) { exit(1); } cli_set_process_title(&#39;process_b&#39;); if (-1 === posix_setsid()) { exit(2); } while(true) { sleep(1); }
執行程式之後:
➜ ~ php56 2fork1.php ➜ ~ ps ax | grep -v grep | grep -E &#39;process_|PID&#39; 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(&#39;/tmp&#39;); umask(0066); mkdir(&#39;test_umask&#39;, 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
指令也能让程序在后台进行。在
sleep
的间隙,关闭终端,会发现进程退出。通过
strace
观察系统调用的情况:➜ ~ 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发现发生了 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(&#39;can not open stdoutFile &#39; . self::$stdoutFile); } }
Workerman 中如此实现,结合博文,可能与 PHP 的 GC 机制有关,对于 fd 0 1 2来说,PHP 会维持对这三个资源的引用计数,在直接 fclose 之后,会使得这几个 fd 对应的资源类型的变量引用计数为0,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。
推荐学习:《PHP视频教程》
以上是什麼是daemon? PHP中如何實作daemon?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

PHP用於構建動態網站,其核心功能包括:1.生成動態內容,通過與數據庫對接實時生成網頁;2.處理用戶交互和表單提交,驗證輸入並響應操作;3.管理會話和用戶認證,提供個性化體驗;4.優化性能和遵循最佳實踐,提升網站效率和安全性。

PHP在數據庫操作和服務器端邏輯處理中使用MySQLi和PDO擴展進行數據庫交互,並通過會話管理等功能處理服務器端邏輯。 1)使用MySQLi或PDO連接數據庫,執行SQL查詢。 2)通過會話管理等功能處理HTTP請求和用戶狀態。 3)使用事務確保數據庫操作的原子性。 4)防止SQL注入,使用異常處理和關閉連接來調試。 5)通過索引和緩存優化性能,編寫可讀性高的代碼並進行錯誤處理。

在PHP中使用預處理語句和PDO可以有效防範SQL注入攻擊。 1)使用PDO連接數據庫並設置錯誤模式。 2)通過prepare方法創建預處理語句,使用佔位符和execute方法傳遞數據。 3)處理查詢結果並確保代碼的安全性和性能。

PHP和Python各有優劣,選擇取決於項目需求和個人偏好。 1.PHP適合快速開發和維護大型Web應用。 2.Python在數據科學和機器學習領域佔據主導地位。

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP可以輕鬆創建互動網頁內容。 1)通過嵌入HTML動態生成內容,根據用戶輸入或數據庫數據實時展示。 2)處理表單提交並生成動態輸出,確保使用htmlspecialchars防XSS。 3)結合MySQL創建用戶註冊系統,使用password_hash和預處理語句增強安全性。掌握這些技巧將提升Web開發效率。

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3漢化版
中文版,非常好用

Atom編輯器mac版下載
最受歡迎的的開源編輯器

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器

禪工作室 13.0.1
強大的PHP整合開發環境

DVWA
Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中