先講幾個概念
守護進程:
Linux中的后台服務進程。它是一個生存期較長的進程,通常獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。守護進程常常在系統引導裝入時啟動,在系統關閉時終止。
進程組:
是一個或多個進程的集合。進程組有進程組ID來唯一標識。除了進程號(PID)之外,進程組ID也是一個進程的必備屬性。每個進程組都有一個組長進程,其組長進程的進程號等於進程組ID。且該進程組ID不會因組長進程的退出而受到影響。
會話周期:
會話期是一個或多個進程組的集合。通常,一個會話開始於用戶登錄,終止於用戶退出,在此期間該用戶運行的所有進程都屬於這個會話期。
創建一個守護進程大體這樣:
1、fork子進程,父進程退出
為避免掛起,控制終端將 Daemon 放入后台執行,方法是在進程中調用 fork(),然后使父進程終止,我們所有后續工作都在子進程中完成。
<?php
$pid = pcntl_fork();
// 父進程 和 子進程 都會執行下面代碼
if ($pid == -1) {
// 當 pid 為 -1 的時候表示創建子進程失敗,這時返回-1。
return false;
} else if ($pid) {
// 父進程會得到子進程號,所以這里是父進程執行的邏輯
pcntl_wait($status); // 等待子進程中斷,防止子進程成為僵屍進程。
} else {
// 子進程得到的 $pid 為 0,所以這里是 子進程 執行的邏輯。
}
2、在子進程中創建新會話
先介紹一下 Linux 中的 進程 與 控制終端,登錄會話 和 進程組 之間的關系:
進程屬於一個進程組,進程組號(GID)就是進程組長的進程號(PID)。登錄會話可以包含多個進程組。這些進程組共享一個控制終端。這個控制終端通常是創建進程的登錄終端。 控制終端,登錄會話和進程組通常是從父進程繼承下來的。我們的目的就是要擺脫它們,使之不受它們的影響。方法是在第1點的基礎上,調用 posix_setsid(); 使進程成為會話組長。setsid有幾個作用:讓進程擺脫原會話的控制;讓進程擺脫原進程組的控制;
3、改變當前目錄為根目錄
進程活動時,其工作目錄所在的文件系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫運行日志的進程將工作目錄改變到特定目錄如 chdir("/"),如果有特殊的需要,我們也可以把當前工作目錄換成其他的路徑,比如 /tmp。
4、重設文件權限掩碼
進程從父進程那里繼承了文件創建掩模。它可能修改守護進程所創建的文件的存取位。為防止這一點,將文件創建掩模清除:umask(0),如果你的應用程序根本就不涉及創建新文件或是文件訪問權限的設定,這一步不是必須的。
5、關閉文件描述符
同文件權限掩碼一樣,新進程會從父進程那里繼承一些已經打開了的文件。這些被打開的文件可能永遠不被我們的daemon進程讀或寫,但它們一樣消耗系統資源,而且可能導致所在的文件系統無法卸下。文件描述符為 0、1 和 2 的三個文件,輸入、輸出 和 報錯 這三個文件也需要被關閉。在 PHP 中只需要 fclose() 就可以了。
fclose(STDIN); fclose(STDOUT); fclose(STDERR);
6、守護進程退出,處理 SIGCHLD 信號
當用戶需要外部停止守護進程運行時,往往會使用 kill 命令停止該守護進程。所以,守護進程中需要編碼來實現 kill 發出的 signal 信號處理,達到進程的正常退出。處理 SIGCHLD 信號並不是必須的。但對於某些進程,特別是服務器進程往往在請求到來時生成子進程處理請求。如果父進程不等待子進程結束,子進程將成為僵屍進程(zombie)從而占用系統資源。如果父進程等待子進程結束,將增加父進程的負擔,影響服務器進程的並發性能。
<?php
// 使用 ticks 需要 PHP 4.3.0 以上版本
declare(ticks = 1);
// 信號處理函數
function sig_handler($signo) {
switch ($signo) {
case SIGTERM:
// 處理 SIGTERM 信號 - 進程終止
exit;
break;
case SIGHUP:
// 處理 SIGHUP 信號 - 終止控制終端或進程
break;
case SIGUSR1:
// 用戶信號
echo "Caught SIGUSR1...\n";
break;
default:
// 處理所有其他信號
}
}
echo "Installing signal handler...\n";
// 安裝信號處理器
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");
pcntl_signal(SIGUSR1, "sig_handler");
// 或者在PHP 4.3.0以上版本可以使用對象方法
// pcntl_signal(SIGUSR1, array($obj, "do_something");
echo "Generating signal SIGTERM to self...\n";
// 向當前進程發送 SIGUSR1 信號
posix_kill(posix_getpid(), SIGUSR1);
echo "Done\n";
下面實現一個守護進程的示例代碼
<?php
class Deamon {
private $_pidFile;
private $_jobs = array();
private $_infoDir;
public function __construct($dir = '/tmp') {
$this->_setInfoDir($dir);
$this->_pidFile = rtrim($this->_infoDir, '/') . '/' . __CLASS__ . '_pid.log';
$this->_checkPcntl();
}
private function _demonize() {
if (php_sapi_name() != 'cli') {
die('Should run in CLI');
}
$pid = pcntl_fork();
if ($pid < 0) {
die("Can't Fork!");
} else if ($pid > 0) {
exit();
}
if (posix_setsid() === -1) {
die('Could not detach');
}
chdir('/');
umask(0);
$fp = fopen($this->_pidFile, 'w') or die("Can't create pid file");
fwrite($fp, posix_getpid());
fclose($fp);
if (!empty($this->_jobs)) {
foreach ($this->_jobs as $job) {
if (!empty($job['argv'])) {
call_user_func($job['function'], $job['argv']);
} else {
call_user_func($job['function']);
}
}
}
return;
}
private function _setInfoDir($dir = null) {
if (is_dir($dir)) {
$this->_infoDir = $dir;
} else {
$this->_infoDir = __DIR__;
}
}
private function _checkPcntl() {
!function_exists('pcntl_signal') && die('Error:Need PHP Pcntl extension!');
}
private function _getPid() {
if (!file_exists($this->_pidFile)) {
return 0;
}
$pid = intval(file_get_contents($this->_pidFile));
if (posix_kill($pid, SIG_DFL)) {
return $pid;
} else {
unlink($this->_pidFile);
return 0;
}
}
private function _message($message) {
printf("%s %d %d %s" . PHP_EOL, date("Y-m-d H:i:s"), posix_getpid(), posix_getppid(), $message);
}
public function start() {
if ($this->_getPid() > 0) {
$this->_message('Running');
} else {
$this->_demonize();
$this->_message('Start');
}
}
public function stop() {
$pid = $this->_getPid();
if ($pid > 0) {
posix_kill($pid, SIGTERM);
unlink($this->_pidFile);
echo 'Stoped' . PHP_EOL;
} else {
echo "Not Running" . PHP_EOL;
}
}
public function status() {
if ($this->_getPid() > 0) {
$this->_message('Is Running');
} else {
echo 'Not Running' . PHP_EOL;
}
}
public function addJobs($jobs = array()) {
if (!isset($jobs['function']) || empty($jobs['function'])) {
$this->_message('Need function param');
}
if (!isset($jobs['argv']) || empty($jobs['argv'])) {
$jobs['argv'] = "";
}
$this->_jobs[] = $jobs;
}
public function run($argv) {
$param = is_array($argv) && count($argv) == 2 ? $argv[1] : null;
switch ($param) {
case 'start':
$this->start();
break;
case 'stop':
$this->stop();
break;
case 'status':
$this->status();
break;
default:
echo "Argv start|stop|status " . PHP_EOL;
break;
}
}
}
$deamon = new Deamon('');
$deamon->addJobs(array(
'function' => 'test',
'argv' => 'Go'
));
$deamon->run($argv);
function test($param) {
$i = 0;
while (true) {
echo 'Now is ', $param . PHP_EOL;
$i++;
sleep(5);
}
}
延伸閱讀:
