什麼是daemon? PHP中如何實作daemon?
守護程式(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中文網其他相關文章!

熱AI工具

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

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

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

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

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

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

PHP 8.4 帶來了多項新功能、安全性改進和效能改進,同時棄用和刪除了大量功能。 本指南介紹如何在 Ubuntu、Debian 或其衍生版本上安裝 PHP 8.4 或升級到 PHP 8.4

CakePHP 是 PHP 的開源框架。它旨在使應用程式的開發、部署和維護變得更加容易。 CakePHP 基於類似 MVC 的架構,功能強大且易於掌握。模型、視圖和控制器 gu

Visual Studio Code,也稱為 VS Code,是一個免費的原始碼編輯器 - 或整合開發環境 (IDE) - 可用於所有主要作業系統。 VS Code 擁有大量針對多種程式語言的擴展,可以輕鬆編寫

CakePHP 是一個開源MVC 框架。它使應用程式的開發、部署和維護變得更加容易。 CakePHP 有許多函式庫可以減少大多數常見任務的過載。

本教程演示瞭如何使用PHP有效地處理XML文檔。 XML(可擴展的標記語言)是一種用於人類可讀性和機器解析的多功能文本標記語言。它通常用於數據存儲

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元
