一個 Daemon 守護進程除了存在記憶體中默默執行工作之外,也需要能夠接收訊號來處理事情,並且能夠寫入一些 Log 訊息讓使用者查詢。
前幾天寫了 用 PHP Daemon 創建 HTTP Server,但範例中的 Daemon 其實不夠完整,我們這次額外來實作讓 Daemon 可以接收 Unix 的訊號(Signal),來處理事情吧。
什麼是訊號(Signal)
我們取「老陳獨白」網誌中的「Linux 信號signal處理機制」來解釋:
因此我們可以知道,訊號的功能是為了傳遞發生的事件給背景執行中的工作。它與事件(Event)驅動的程式不太一樣,我們並不傳遞任何參數或值給背景程序,單純的只是通知並觸發一個事件,而且通常是系統及別的事件,例如程序終止、重開或錯誤發生等等,而程序可以選擇回應處理或忽略。
訊號可以從 Linux 系統的核心送往某一個程序,也可以從一個程序送往某一個程序。例如要終止某一個程式,可以從一個命令 kill
送出 SIGTERM
訊號給該程序,來終止該程序的執行。
為什麼要接收訊號
我們寫的 Daemon 通常是掛著來自動處理事情,有時候事情處理到一半尚未完成,系統卻發出了終止程序的指令(例如SIGTERM
),或許我們會想要把當下的任務處理完再結束,或是執行其他工作如回收記憶體、備份當下執行狀態或紀錄日誌等等。此時就可以註冊自己的訊號處理機制,先執行完之後,再由程序自己主動終止。
最常遇到的狀況例如 Queue 佇列排程系統,當下在處理的事務應該要先完成或是重新改為未執行的狀態,否則重開 Daemon 後,說不定就少一條紀錄了。
如何接收訊號
Linux 系統中所有的背景程序,都有一些預設的訊號處理機制,例如按下 Ctrl + C 會發出 SIGINT
訊號,而輸入 kill {pid}
會發出 SIGTERM
訊號,並自動結束程序。
詳細訊號說明可以看這幾篇文章,對訊號的介紹非常明瞭,也提供了對照表:
而在 PHP 中,我們是採用 pcntl_signal()
函式來註冊自己的訊號處理機制,使用方式很簡單,就像平常常用的 callback 方法:
// 直接指派一個函式處理 SIGTERM 訊號
pcntl_signal(SIGTERM, 'signal');
// 指派物件中的方法
pcntl_signal(SIGTERM, array($this, 'signal'));
// 指派一個靜態類別的方法
pcntl_signal(SIGTERM, array('SomeClass', 'signal'));
接著我們當我們執行 kill {pid}
指令時,Linux 就會發出 SIGTERM
訊號給我們的程式。
要注意的是,當我們註冊自己的函式以後,就是蓋掉預設行為了,如果我們沒有自行在程式碼中 exit 程序的話,這個 Daemon 會刪也刪不掉,一直存在背景中,這時就要靠 kill -sigkill {pid}
或 kill -9 {pid}
才能強制刪除。
如何讓程序刪除自己
我們都知道刪除程序要靠 ps
指令查出 pid 以後,再用 kill {pid}
來刪除程序,就算程式要刪除自己也是要知道 pid。但在我們寫的程式裏面,如何得知自己的 pid 呢?
此時我們可以額外寫一個 .pid
檔案在 /tmp 中,例如 /tmp/daemon/daemon.pid ,等到要刪除時,再從暫存檔中提取 pid 來用。
範例程式
我現在寫一個範例用的 Daemon 程式叫做 helloworld.php
,原始碼如下:
<?php
/**
* A PHP daemon demo file.
*
* @author Asika
* @email asika@asikart.com
* @date 2013-10-12
*
* @copyright Copyright (C) 2013 - Asika.
* @license GNU General Public License version 2 or later; see LICENSE
*/
/**
* Daemon Application.
*/
class DaemonSignalListener
{
/**
* Store process id for close self.
*
* @var int
*/
protected $processId;
/**
* File path to save pid.
*
* @var string
*/
protected $pidFile = '/tmp/daemon/daemon.pid';
/**
* Daemon log file.
*
* @var string
*/
protected $logFile = '/var/www/daemon.log';
/**
* Create a daemon process.
*
* @return DaemonHttpApplication Return self to support chaining.
*/
public function execute()
{
// Create first child.
if(pcntl_fork())
{
// I'm the parent
// Protect against Zombie children
pcntl_wait($status);
exit;
}
// Make first child as session leader.
posix_setsid();
// Create second child.
$pid = pcntl_fork();
if($pid)
{
// If pid not 0, means this process is parent, close it.
$this->processId = $pid;
$this->storeProcessId();
exit;
}
$this->addLog('Daemonized');
fwrite(STDOUT, "Daemon Start\n-----------------------------------------\n");
$this->registerSignalHandler();
// Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor
// incoming signals after each tick and call the relevant signal handler automatically.
declare (ticks = 1);
while(true)
{
$this->doExecute();
}
return $this;
}
/**
* Method to run the application routines.
* Most likely you will want to fetch a queue to do something.
*
* @return void
*/
protected function doExecute()
{
// Do some stuff you want.
}
/**
* Method to attach signal handler to the known signals.
*
* @return void
*/
protected function registerSignalHandler()
{
$this->addLog('registerHendler');
pcntl_signal(SIGINT, array($this, 'shutdown'));
pcntl_signal(SIGTERM, array($this, 'shutdown'));
pcntl_signal(SIGUSR1, array($this, 'customSignal'));
}
/**
* Store the pid to file.
*
* @return DaemonSignalListener Return self to support chaining.
*/
protected function storeProcessId()
{
$file = $this->pidFile;
// Make sure that the folder where we are writing the process id file exists.
$folder = dirname($file);
if(!is_dir($folder))
{
mkdir($folder);
}
file_put_contents($file, $this->processId);
return $this;
}
/**
* Shut down our daemon.
*
* @param integer $signal The received POSIX signal.
*/
public function shutdown($signal)
{
$this->addLog('Shutdown by signal: ' . $signal);
$pid = file_get_contents($this->pidFile);
// Remove the process id file.
@ unlink($this->pidFile);
passthru('kill -9 ' . $pid);
exit;
}
/**
* Hendle the SIGUSR1 signal.
*
* @param integer $signal The received POSIX signal.
*/
public function customSignal($signal)
{
$this->addLog('Execute custom signal: ' . $signal);
}
/**
* Add a log to log file.
*
* @param string $text Log string.
*/
protected function addLog($text)
{
$file = $this->logFile;
$time = new Datetime();
$text = sprintf("%s - %s\n", $text, $time->format('Y-m-d H:i:s'));
$fp = fopen($file, 'a+');
fwrite($fp,$text);
fclose($fp);
}
}
// Start Daemon
// --------------------------------------------------
$daemon = new DaemonSignalListener();
$daemon->execute();
說明
Daemon 啟用
這是一個 Daemon 物件,執行 execute()
後就會把自己常駐在背景中執行。Daemon 本身的創建方法不再贅述,參考上一篇: 用 PHP Daemon 創建 HTTP Server,execute 方法內大多數一模一樣,但是額外加了一些功能來讓 Daemon 有能力註冊訊號。
下面這一段程式碼利用 $this->storeProcessId()
來把 pid 儲存在檔案內。要在父程序內執行,因為父程序才會取得子程序的 pid。子程序回傳的會是 0。
// Create second child.
$pid = pcntl_fork();
if($pid)
{
// If pid not 0, means this process is parent, close it.
$this->processId = $pid;
$this->storeProcessId();
exit;
}
下面這一段程式碼用日誌紀錄了 Daemon 的啟用,並呼叫 $this->registerSignalHandler()
註冊訊號,然後非常重要的一點是一定要有 declare (ticks = 1)
這一行,才能真正監聽訊號,否則你怎麼樣都無法把訊號送進來的。
最後我們跑個無窮迴圈吧,doExecute()
是空方法,看你想做什麼事都好。
$this->addLog('Daemonized');
fwrite(STDOUT, "Daemon Start\n-----------------------------------------\n");
$this->registerSignalHandler();
// Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor
// incoming signals after each tick and call the relevant signal handler automatically.
declare (ticks = 1);
while(true)
{
$this->doExecute();
}
訊號註冊
我們其實很簡單的寫成三行來分別註冊三個不同的方法,範例程式也就三個就夠用了,SIGINT
與 SIGTERM
都是程序終止,交給 $this->shutdown()
方法處理。SIGUSR1
則是使用者自訂訊號,給 $this->customSignal()
方法處理。
/**
* Method to attach signal handler to the known signals.
*
* @return void
*/
protected function registerSignalHandler()
{
$this->addLog('registerHendler');
pcntl_signal(SIGINT, array($this, 'shutdown'));
pcntl_signal(SIGTERM, array($this, 'shutdown'));
pcntl_signal(SIGUSR1, array($this, 'customSignal'));
}
Shutdown 流程
我們一開始在 storeProcessId()
中就先儲存好 pid 到暫存檔了,接下來就把他讀出來,並根據 pid 刪除程序。
/**
* Shut down our daemon.
*
* @param integer $signal The received POSIX signal.
*/
public function shutdown($signal)
{
$this->addLog('Shutdown by signal: ' . $signal);
$pid = file_get_contents($this->pidFile);
// Remove the process id file.
@ unlink($this->pidFile);
passthru('kill -9 ' . $pid);
exit;
}
同樣用 $this->addLog()
做個 log 紀錄,才知道發生了什麼事。最後記得要 exit 阿。
日誌檔
我稍微偷懶,直接將日誌寫在網頁根目錄 /var/www/daemon.log
中,方便查詢。
/**
* Add a log to log file.
*
* @param string $text Log string.
*/
protected function addLog($text)
{
$file = $this->logFile;
$time = new Datetime();
$text = sprintf("%s - %s\n", $text, $time->format('Y-m-d H:i:s'));
$fp = fopen($file, 'a+');
fwrite($fp,$text);
fclose($fp);
}
測試我們的 Daemon
執行
一切就緒後,我們在命令列鍵入:
$ php helloworld.php
輸出結果:
$ php helloworld.php
Daemon Start
-----------------------------------------
$
檢查程序存在背景中
輸入:
$ ps -ef | grep hello
輸出結果:
$ ps -ef | grep hello
apache 8845 1 99 16:04 ? 00:01:21 php helloworld.php
apache 8847 8410 0 16:06 pts/0 00:00:00 grep --color=auto hello
$
打開日誌檔
輸入:
$ vim /var/www/daemon.log
內容:
Daemonized - 2013-10-13 16:04:54
registerHendler - 2013-10-13 16:04:54
可以看到 Daemon 的啟動與 Signal handler 的註冊都被記錄下來了。
終止程序
我們現在要藉由 kill
來終止程序,如果成功覆蓋預設的 SIGTERM
,應該會在日誌檔中有紀錄,否則便直接刪除了。
輸入:
$ kill 8845
再次打開 daemon.log
,成功多出第三條紀錄:
Daemonized - 2013-10-13 16:04:54
registerHendler - 2013-10-13 16:04:54
Shutdown by signal: 15 - 2013-10-13 16:12:51
使用其他訊號
剛剛用的 SIGTERM
只有在 kill 時才發送,其實除此之外,我們還可以用 kill 來發送其他訊號,例如 kill -sigusr1 xxxx
就是送出 SIGUSR1
訊號,就這麼簡單。
範例程式中的 registerSignalHandler()
有寫一個 pcntl_signal(SIGUSR1, array($this, 'customSignal'));
來註冊自定義訊號的處理,而方法內也非常簡單,只是在日誌中寫一條新紀錄而已:
/**
* Hendle the SIGUSR1 signal.
*
* @param integer $signal The received POSIX signal.
*/
public function customSignal($signal)
{
$this->addLog('Execute custom signal: ' . $signal);
}
所以我們依照剛剛的過程重新開啟 Daemon ,並輸入 kill -sigusr1 {pid}
試試看:
接著再度打開 daemon.log
確實出現一條 Execute custom signal: 10 - 2013-10-13 16:17:27
的紀錄,表示訊號監聽成功。
結語
其實除非我們要寫的是公開發行的 Daemon 處理函式庫或框架,不然把 Signal 全搞熟似乎對 PHP 開發者來說沒有特別實用。而對於原先就寫 C 語言或 Linux 程式的人來說,這似乎也只是小兒科。總之因為很少看到中文文章中把 PHP 的訊號處理講的比較完整的,所以就趁機寫一篇留底吧。