守護(hù)進(jìn)程(Daemon)是運(yùn)行在后臺(tái)的一種特殊進(jìn)程。它獨(dú)立于控制終端并且周期性地執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。守護(hù)進(jìn)程是一種很有用的進(jìn)程。php也可以實(shí)現(xiàn)守護(hù)進(jìn)程的功能。
1、基本概念
進(jìn)程
每個(gè)進(jìn)程都有一個(gè)父進(jìn)程,子進(jìn)程退出,父進(jìn)程能得到子進(jìn)程退出的狀態(tài)。
進(jìn)程組
每個(gè)進(jìn)程都屬于一個(gè)進(jìn)程組,每個(gè)進(jìn)程組都有一個(gè)進(jìn)程組號(hào),該號(hào)等于該進(jìn)程組組長(zhǎng)的PID
2、守護(hù)編程要點(diǎn)
- 在后臺(tái)運(yùn)行。
為避免掛起控制終端將Daemon放入后臺(tái)執(zhí)行。方法是在進(jìn)程中調(diào)用fork使父進(jìn)程終止,讓Daemon在子進(jìn)程中后臺(tái)執(zhí)行。 if($pid=pcntl_fork()) exit(0);//是父進(jìn)程,結(jié)束父進(jìn)程,子進(jìn)程繼續(xù) - 脫離控制終端,登錄會(huì)話和進(jìn)程組
有必要先介紹一下Linux中的進(jìn)程與控制終端,登錄會(huì)話和進(jìn)程組之間的關(guān)系:進(jìn)程屬于一個(gè)進(jìn)程組,進(jìn)程組號(hào)(GID)就是進(jìn)程組長(zhǎng)的進(jìn)程號(hào)(PID)。登錄會(huì)話可以包含多個(gè)進(jìn)程組。這些進(jìn)程組共享一個(gè)控制終端。這個(gè)控制終端通常是創(chuàng)建進(jìn)程的登錄終端。 控制終端,登錄會(huì)話和進(jìn)程組通常是從父進(jìn)程繼承下來(lái)的。我們的目的就是要擺脫它們,使之不受它們的影響。方法是在第1點(diǎn)的基礎(chǔ)上,調(diào)用setsid()使進(jìn)程成為會(huì)話組長(zhǎng): posix_setsid();
說(shuō)明:當(dāng)進(jìn)程是會(huì)話組長(zhǎng)時(shí)setsid()調(diào)用失敗。但第一點(diǎn)已經(jīng)保證進(jìn)程不是會(huì)話組長(zhǎng)。setsid()調(diào)用成功后,進(jìn)程成為新的會(huì)話組長(zhǎng)和新的進(jìn)程組長(zhǎng),并與原來(lái)的登錄會(huì)話和進(jìn)程組脫離。由于會(huì)話過(guò)程對(duì)控制終端的獨(dú)占性,進(jìn)程同時(shí)與控制終端脫離。 - 禁止進(jìn)程重新打開(kāi)控制終端
現(xiàn)在,進(jìn)程已經(jīng)成為無(wú)終端的會(huì)話組長(zhǎng)。但它可以重新申請(qǐng)打開(kāi)一個(gè)控制終端??梢酝ㄟ^(guò)使進(jìn)程不再成為會(huì)話組長(zhǎng)來(lái)禁止進(jìn)程重新打開(kāi)控制終端: if($pid=pcntl_fork()) exit(0);//結(jié)束第一子進(jìn)程,第二子進(jìn)程繼續(xù)(第二子進(jìn)程不再是會(huì)話組長(zhǎng)) - 關(guān)閉打開(kāi)的文件描述符
進(jìn)程從創(chuàng)建它的父進(jìn)程那里繼承了打開(kāi)的文件描述符。如不關(guān)閉,將會(huì)浪費(fèi)系統(tǒng)資源,造成進(jìn)程所在的文件系統(tǒng)無(wú)法卸下以及引起無(wú)法預(yù)料的錯(cuò)誤。按如下方法關(guān)閉它們:
fclose(STDIN),fclose(STDOUT),fclose(STDERR)關(guān)閉標(biāo)準(zhǔn)輸入輸出與錯(cuò)誤顯示。 - 改變當(dāng)前工作目錄
進(jìn)程活動(dòng)時(shí),其工作目錄所在的文件系統(tǒng)不能卸下。一般需要將工作目錄改變到根目錄。對(duì)于需要轉(zhuǎn)儲(chǔ)核心,寫運(yùn)行日志的進(jìn)程將工作目錄改變到特定目錄如chdir("/") - 重設(shè)文件創(chuàng)建掩模
進(jìn)程從創(chuàng)建它的父進(jìn)程那里繼承了文件創(chuàng)建掩模。它可能修改守護(hù)進(jìn)程所創(chuàng)建的文件的存取位。為防止這一點(diǎn),將文件創(chuàng)建掩模清除:umask(0); 給予操作文件的最大權(quán)限。 - 處理SIGCHLD信號(hào)
處理SIGCHLD信號(hào)并不是必須的。但對(duì)于某些進(jìn)程,特別是服務(wù)器進(jìn)程往往在請(qǐng)求到來(lái)時(shí)生成子進(jìn)程處理請(qǐng)求。如果父進(jìn)程不等待子進(jìn)程結(jié)束,子進(jìn)程將成為僵尸進(jìn)程(zombie)從而占用系統(tǒng)資源。如果父進(jìn)程等待子進(jìn)程結(jié)束,將增加父進(jìn)程的負(fù)擔(dān),影 響服務(wù)器進(jìn)程的并發(fā)性能。在Linux下可以簡(jiǎn)單地將SIGCHLD信號(hào)的操作設(shè)為SIG_IGN。 signal(SIGCHLD,SIG_IGN);
這樣,內(nèi)核在子進(jìn)程結(jié)束時(shí)不會(huì)產(chǎn)生僵尸進(jìn)程。這一點(diǎn)與BSD4不同,BSD4下必須顯式等待子進(jìn)程結(jié)束才能釋放僵尸進(jìn)程。關(guān)于信號(hào)的問(wèn)題請(qǐng)參考Linux 信號(hào)說(shuō)明列表
3、實(shí)例
<?php
/**
*@author tengzhaorong@gmail.com
*@date 2013-07-25
* 后臺(tái)腳本控制類
*/
class DaemonCommand{
private $info_dir="/tmp";
private $pid_file="";
private $terminate=false; //是否中斷
private $workers_count=0;
private $gc_enabled=null;
private $workers_max=8; //最多運(yùn)行8個(gè)進(jìn)程
public function __construct($is_sington=false,$user='nobody',$output="/dev/null"){
$this->is_sington=$is_sington; //是否單例運(yùn)行,單例運(yùn)行會(huì)在tmp目錄下建立一個(gè)唯一的PID
$this->user=$user;//設(shè)置運(yùn)行的用戶 默認(rèn)情況下nobody
$this->output=$output; //設(shè)置輸出的地方
$this->checkPcntl();
}
//檢查環(huán)境是否支持pcntl支持
public function checkPcntl(){
if ( ! function_exists('pcntl_signal_dispatch')) {
// PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch
// call sighandler only every 10 ticks
declare(ticks = 10);
}
// Make sure PHP has support for pcntl
if ( ! function_exists('pcntl_signal')) {
$message = 'PHP does not appear to be compiled with the PCNTL extension. This is neccesary for daemonization';
$this->_log($message);
throw new Exception($message);
}
//信號(hào)處理
pcntl_signal(SIGTERM, array(__CLASS__, "signalHandler"),false);
pcntl_signal(SIGINT, array(__CLASS__, "signalHandler"),false);
pcntl_signal(SIGQUIT, array(__CLASS__, "signalHandler"),false);
// Enable PHP 5.3 garbage collection
if (function_exists('gc_enable'))
{
gc_enable();
$this->gc_enabled = gc_enabled();
}
}
// daemon化程序
public function daemonize(){
global $stdin, $stdout, $stderr;
global $argv;
set_time_limit(0);
// 只允許在cli下面運(yùn)行
if (php_sapi_name() != "cli"){
die("only run in command line mode\n");
}
// 只能單例運(yùn)行
if ($this->is_sington==true){
$this->pid_file = $this->info_dir . "/" .__CLASS__ . "_" . substr(basename($argv[0]), 0, -4) . ".pid";
$this->checkPidfile();
}
umask(0); //把文件掩碼清0
if (pcntl_fork() != 0){ //是父進(jìn)程,父進(jìn)程退出
exit();
}
posix_setsid();//設(shè)置新會(huì)話組長(zhǎng),脫離終端
if (pcntl_fork() != 0){ //是第一子進(jìn)程,結(jié)束第一子進(jìn)程
exit();
}
chdir("/"); //改變工作目錄
$this->setUser($this->user) or die("cannot change owner");
//關(guān)閉打開(kāi)的文件描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$stdin = fopen($this->output, 'r');
$stdout = fopen($this->output, 'a');
$stderr = fopen($this->output, 'a');
if ($this->is_sington==true){
$this->createPidfile();
}
}
//--檢測(cè)pid是否已經(jīng)存在
public function checkPidfile(){
if (!file_exists($this->pid_file)){
return true;
}
$pid = file_get_contents($this->pid_file);
$pid = intval($pid);
if ($pid > 0 && posix_kill($pid, 0)){
$this->_log("the daemon process is already started");
}
else {
$this->_log("the daemon proces end abnormally, please check pidfile " . $this->pid_file);
}
exit(1);
}
//----創(chuàng)建pid
public function createPidfile(){
if (!is_dir($this->info_dir)){
mkdir($this->info_dir);
}
$fp = fopen($this->pid_file, 'w') or die("cannot create pid file");
fwrite($fp, posix_getpid());
fclose($fp);
$this->_log("create pid file " . $this->pid_file);
}
//設(shè)置運(yùn)行的用戶
public function setUser($name){
$result = false;
if (empty($name)){
return true;
}
$user = posix_getpwnam($name);
if ($user) {
$uid = $user['uid'];
$gid = $user['gid'];
$result = posix_setuid($uid);
posix_setgid($gid);
}
return $result;
}
//信號(hào)處理函數(shù)
public function signalHandler($signo){
switch($signo){
//用戶自定義信號(hào)
case SIGUSR1: //busy
if ($this->workers_count < $this->workers_max){
$pid = pcntl_fork();
if ($pid > 0){
$this->workers_count ++;
}
}
break;
//子進(jìn)程結(jié)束信號(hào)
case SIGCHLD:
while(($pid=pcntl_waitpid(-1, $status, WNOHANG)) > 0){
$this->workers_count --;
}
break;
//中斷進(jìn)程
case SIGTERM:
case SIGHUP:
case SIGQUIT:
$this->terminate = true;
break;
default:
return false;
}
}
/**
*開(kāi)始開(kāi)啟進(jìn)程
*$count 準(zhǔn)備開(kāi)啟的進(jìn)程數(shù)
*/
public function start($count=1){
$this->_log("daemon process is running now");
pcntl_signal(SIGCHLD, array(__CLASS__, "signalHandler"),false); // if worker die, minus children num
while (true) {
if (function_exists('pcntl_signal_dispatch')){
pcntl_signal_dispatch();
}
if ($this->terminate){
break;
}
$pid=-1;
if($this->workers_count<$count){//控制開(kāi)啟的進(jìn)程數(shù)量
$pid=pcntl_fork();
}
if($pid>0){
$this->workers_count++;
}elseif($pid==0){
// 這個(gè)符號(hào)表示恢復(fù)系統(tǒng)對(duì)信號(hào)的默認(rèn)處理
pcntl_signal(SIGTERM, SIG_DFL);
pcntl_signal(SIGCHLD, SIG_DFL);
if(!empty($this->jobs[$this->workers_count])){
while($this->jobs[$this->workers_count]['runtime']){
if(!empty($this->jobs[$this->workers_count]['argv'])){
call_user_func($this->jobs[$this->workers_count]['function'],$this->jobs[$this->workers_count]['argv']);
}else{
call_user_func($this->jobs[$this->workers_count]['function']);
}
$this->jobs[$this->workers_count]['runtime']--;
sleep(2);
}
exit();
}
return;
}else{
sleep(2);
}
}
$this->mainQuit();
exit(0);
}
//整個(gè)進(jìn)程退出
public function mainQuit(){
if (file_exists($this->pid_file)){
unlink($this->pid_file);
$this->_log("delete pid file " . $this->pid_file);
}
$this->_log("daemon process exit now");
posix_kill(0, SIGKILL);
exit(0);
}
// 添加工作實(shí)例
public function setJobs($jobs=array()){
if(!empty($jobs)){
foreach($jobs as $key=>$value){
if(!isset($value['argv'])||empty($value['argv'])){
$value['argv']="";
}
if(!isset($value['runtime'])||empty($value['runtime'])){
$value['runtime']=1;
}
if(!isset($value['function'])||empty($value['function'])){
$this->log("你必須添加運(yùn)行的函數(shù)!");
exit;
}
$this->jobs[$key+1] = $value;
}
}
}
//日志處理
private function _log($message){
printf("%s\t%d\t%d\t%s\n", date("c"), posix_getpid(), posix_getppid(), $message);
}
}
//調(diào)用方法1
$daemon=new DaemonCommand(true);
$daemon->daemonize();
$daemon->start(2);//開(kāi)啟2個(gè)子進(jìn)程工作
work();
//調(diào)用方法2
$daemon=new DaemonCommand(true);
$daemon->daemonize();
$daemon->setJobs([
['function'=>'work','argv'=>'','runtime'=>3],['function'=>'work','argv'=>'','runtime'=>3]
]);//function 要運(yùn)行的函數(shù),argv運(yùn)行函數(shù)的參數(shù),runtime運(yùn)行的次數(shù)
$daemon->start(2);//開(kāi)啟2個(gè)子進(jìn)程工作 每個(gè)子進(jìn)程處理相應(yīng)的任務(wù)號(hào)
//具體功能的實(shí)現(xiàn)
function work(){
echo "測(cè)試1";
}
?>