目錄
#什麼是daemon
實作關注點
首頁 後端開發 php教程 什麼是daemon? PHP中如何實作daemon?

什麼是daemon? PHP中如何實作daemon?

Jun 23, 2021 pm 08:30 PM
php

守護程式(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 -axps -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('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 指令也能让程序在后台进行。

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('can not open stdoutFile ' . self::$stdoutFile);
   }
}
登入後複製

Workerman 中如此实现,结合博文,可能与 PHP 的 GC 机制有关,对于 fd 0 1 2来说,PHP 会维持对这三个资源的引用计数,在直接 fclose 之后,会使得这几个 fd 对应的资源类型的变量引用计数为0,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。

推荐学习:《PHP视频教程

以上是什麼是daemon? PHP中如何實作daemon?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

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

適用於 Ubuntu 和 Debian 的 PHP 8.4 安裝和升級指南 適用於 Ubuntu 和 Debian 的 PHP 8.4 安裝和升級指南 Dec 24, 2024 pm 04:42 PM

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

討論 CakePHP 討論 CakePHP Sep 10, 2024 pm 05:28 PM

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

CakePHP 檔案上傳 CakePHP 檔案上傳 Sep 10, 2024 pm 05:27 PM

為了進行文件上傳,我們將使用表單助理。這是文件上傳的範例。

如何設定 Visual Studio Code (VS Code) 進行 PHP 開發 如何設定 Visual Studio Code (VS Code) 進行 PHP 開發 Dec 20, 2024 am 11:31 AM

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

CakePHP 快速指南 CakePHP 快速指南 Sep 10, 2024 pm 05:27 PM

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

您如何在PHP中解析和處理HTML/XML? 您如何在PHP中解析和處理HTML/XML? Feb 07, 2025 am 11:57 AM

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

在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

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

php程序在字符串中計數元音 php程序在字符串中計數元音 Feb 07, 2025 pm 12:12 PM

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

See all articles