声明:这篇文章是我09年的时候发表在自己之前的BLOG上的,属于原创内容,现在将文章转移到这里。
守护进程也称精灵进程(daemon),是生存期较长的一种进程。它们常常用在系统自举时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。UNIX类操作系统有很多的守护进程,它们执行日常事务活动。
目前有大量的web站点基与PHP开发,业务逻辑都是由PHP来实现,很多时候我们也需要一个PHP的daemon来做一些日常事务,例如我们想每隔一个小时统计一下数据库中的某项数据,每天定期的执行一些备份或则监控任务。这些任务在apache模块的web环境下实现比较困难而且容易引发很多问题。
这里我介绍一款我自己写的PHP5版的daemon类 - KalonDaemon. ^_^ 现在和大家一起分享。
概要:
KalonDaemon是一款PHP5的daemon类,我们在PHP代码中可以直接包含并且使用,KalonDaemon工作在cli sapi下( command line interface),它能把一个普通的PHP进程变成一个守护进程。
使用方式:
在PHP脚本中包含了KalonDaemon设置好参数然后调用start()方法。然后我们在命令行下用PHP cli执行脚本,比如cli sapi路径为 /usr/local/bin/php, 我们编写的程序路径 /home/test/mydaemon.php,那么我们用以下方式运行程序: /usr/local/bin/php /home/test/mydaemon.php 根据需要可以在后面添加别的参数。
工作流程:
KalonDaemon遵循大部分unix类系统下的守护进程编程规则,主要工作流程如下:
1. 调用pcntl_fork,然后使父进程退出(exit).这样做实现如下几点:第一,如果该守护进程是作为一条shell命令启动,那么父进程终止使得 shell认为这条命令已经执行完毕;第二,子进程继承父进程的进程组ID,但是具有一个新的进程ID,这就保证了子进程不是一个进程组的组长,这对于下面要做的posix_setsid调用是必要的前提条件。
2.调用posix_setsid以创建一个新的会话,这样新进程就成为了新会话的首进程,同时是新进程组的组长进程,而且没有控制终端。
3.设置进程信号回调函数,方便我们用其它进程对守护进程进行控制。
以下是mydaemon.php的源码:
<?php require_once './KalonDaemon.php'; declare(ticks = 1); $toDo = $_SERVER['argv'][1]; $daemonConf = array('pidFileName' => 'mydaemon.pid', 'verbose' => true); function myHandler1() { sleep(5); echo "This handler1 works./n"; } function myHandler2() { echo "This handler2 works./n"; } try { $daemon = new KalonDaemon($daemonConf); if ($toDo == 'start') { $daemon->addSignalHandler(SIGUSR1, 'myHandler1'); $daemon->addSignalHandler(SIGUSR2, 'myHandler2'); $daemon->start(); for (;;) { echo "running./n"; sleep(1000); } } elseif ($toDo == 'stop') { $daemon->stop(); } else { die("unknown action."); } } catch (KalonDaemonException $e) { echo $e->getMessage(); echo "/n"; } ?>
在命令行下执行:
/path/to/phpcli/php mydaemon.php start
输出如下信息:
Daemon started with pid 8976...
running.
说明守护进程已经开始运行,进程号为8976,当然一般情况进程号每次都会不一样。
由于mydaemon.php中有一个死循环,每次循环会睡眠1000秒,所以进程永远不会终止。
mydaemon.php中为守护进程注册了两个信号句柄,信号SIGUSR1对应函数myHandler1(), 信号SIGUSR2对应myHandler2(),我们可以通过kill命令给进程发送这两个信号来唤醒进程。
kill -SIGUSR2 8976
输出信息如下:
This handler2 works.
running.
说明睡眠中的进程被唤醒,并且执行了myHandler2()函数,然后再次进入了循环。
当我们需要终止守护进程的时候,可以用以下命令:
/path/to/phpcli/php mydaemon.php stop
输出信息如下:
Daemon stopped with pid 8976...
这样守护进程就终止了。
这样的特性可以在某些应用场景非常有用,比如服务器在接受到一些上传的数据之后,需要唤醒守护进程来处理这些数据。守护进程可以长期出去睡眠状态等待,当数据到来之后,发送信号唤醒守护进程,守护进程马上开始处理这些数据。这样要比定期的轮询效率高很多,而且不会有延迟现象。
KalonDaemon.php
<?php /** * Kalon Daemon -> A Unix Daemon for PHP5 * This is a free daemon tool, you can use it anyway you like. * * NOTICE: * 1:This tool must run in cli sapi, any other sapis will cause a * KalonDaemonException thrown.so you need to use this tool in a * command line interface,command such as: /path/to/php mydaemon.php * * 2:Daemon needs pcntl and posix extension support. Make sure your cli * sapi has loaded these two extension.The posix is compiled in php by * default, while pcntl must be compiled or dynamic load by yourself. * Missing anyone of these extension will cause a KalonDaemonException * thrown. * * USAGE: * *put the code below in mydaemon.php * require_once '/path/to/KalonDaemon.php'; declare(ticks = 1); $toDo = $_SERVER['argv'][1]; $daemonConf = array('pidFileName' => 'mydaemon.pid', 'verbose' => true); function myHandler1() { sleep(5); echo "This handler1 works./n"; } function myHandler2() { echo "This handler2 works./n"; } try { $daemon = new KalonDaemon($daemonConf); if ($toDo == 'start') { $daemon->addSignalHandler(SIGUSR1, 'myHandler1'); $daemon->addSignalHandler(SIGUSR2, 'myHandler2'); $daemon->start(); for (;;) { echo "running./n"; sleep(1000); } } elseif ($toDo == 'stop') { $daemon->stop(); } else { die("unknown action."); } } catch (KalonDaemonException $e) { echo $e->getMessage(); echo "/n"; } * * then open a command shell: * start daemon: * /path/to/phpcli/php /path/to/mydaemon.php start * * stop daemon: * /path/to/phpcli/php /path/to/mydaemon.php stop * * * * @author 玉面修罗 - Kalon * @version 1.0 * @site: http://blog.csdn.net/phpkernel * E-mail/MSN: xiuluo-999@163.com */ class KalonDaemon { /** * path of pid file * * @var string */ private $_pidFilePath = "/var/run"; /** * name of pid file * * @var string */ private $_pidFileName = "daemon.pid"; /** * out put run information * * @var boolean */ private $_verbose = false; /** * default singleton model * * @var boolean */ private $_singleton = true; /** * close file handle STDIN STDOUT STDERR * NOTICE: we do not close STDIN STDOUT STDERR indeed for some reason. * @var boolean */ private $_closeStdHandle = true; /** * pid of daemon * * @var int */ private $_pid = 0; /** * exec file * * @var string */ private $_execFile = ""; /** * function handlers for signal number * * @var array */ private $_signalHandlerFuns = array(); /** * set config * * @param array $configs */ public function __construct($configs = array()) { //load config if (is_array($configs)) $this->setConfigs($configs); } /** * pctntl is needed,and only works in cli sapi */ public function _checkRequirement() { //check if pctnl loaded if (!extension_loaded('pcntl')) throw new KalonDaemonException("daemon needs support of pcntl extension, please enable it."); //check sapi name,only for cli if ('cli' != php_sapi_name()) throw new KalonDaemonException("daemon only works in cli sapi."); } /** * set configs * pidFilePath: path of pid file * pidFileName: name of pid file * verbose : output process information * singleton : singleton model,only one instance of daemon at one time * closeStdHandle : close STDIN STDOUT STDERR when daemon run success * * @param array $configs */ public function setConfigs($configs) { foreach ((array) $configs as $item => $config) { switch ($item) { case "pidFilePath": $this->setPidFilePath($config); break; case "pidFileName": $this->setPidFileName($config); break; case "verbose": $this->setVerbose($config); break; case "singleton": $this->setSingleton($config); break; case "closeStdHandle"; $this->setCloseStdHandle($config); break; default: throw new KalonDaemonException("Unknown config item {$item}"); break; } } } /** * set Pid File Path * * @param string $path * @return boolean */ public function setPidFilePath($path) { if (empty($path)) return false; if(!is_dir($path)) if (!mkdir($path, 0777)) throw new KalonDaemonException("setPidFilePath: cannnot make dir {$path}."); $this->_pidFilePath = rtrim($path, "/"); return true; } /** * get Pid File Path * * @return string */ public function getPidFilePath() { return $this->_pidFilePath; } /** * set Pid File Name * * @param string $name * @return boolean */ public function setPidFileName($name) { if (empty($name)) return false; $this->_pidFileName = trim($name); return true; } /** * get Pid File Name * * @return string */ public function getPidFileName() { return $this->_pidFileName; } /** * set Open Output * if sets to true,daemon will output start and stop information ,etc * * @param boolean $open * @return boolean */ public function setVerbose($open = true) { $this->_verbose = (boolean) $open; return true; } /** * get Open Output * * @return boolean */ public function getVerbose() { return $this->_verbose; } /** * set Singleton * if sets to true, daemon will keep singleton,which means that there is only one * instance of daemon at one time. * * @param boolean $singleton * @return boolean */ public function setSingleton($singleton = true) { $this->_singleton = (boolean) $singleton; return true; } /** * get Singleton * * @return boolean */ public function getSingleton() { return $this->_singleton; } /** * set Close Std Handle * * @param boolean $close * @return boolean */ public function setCloseStdHandle($close = true) { $this->_closeStdHandle = (boolean) $close; return true; } /** * get Close Std Handle * * @return boolean */ public function getCloseStdHandle() { return $this->_closeStdHandle; } /** * start daemon * 1.daemonize * 2.setup signal handlers * 3.close STDIN STDOUT STDERR * * @return boolean */ public function start() { //this line used to put in the __construct,for some reason I move it here. $this->_checkRequirement(); //do daemon $this->_daemonize(); //default handler for stop if(!pcntl_signal(SIGTERM, array($this,"signalHandler"))) throw new KalonDaemonException("Cannot setup signal handler for signo {$signo}"); //close file handle STDIN STDOUT STDERR //notic!!!This makes no use in PHP4 and some early version of PHP5 //if we close these handle without dup to /dev/null,php process will die //when operating on them. if ($this->_closeStdHandle) { //fclose(STDIN); //fclose(STDOUT); //fclose(STDERR); } return true; } /** * stop daemon * 1.get daemon pid from pid file * 2.send signal to daemon * * @param boolean $force kill -9 or kill * @return boolean */ public function stop($force = false) { if ($force) $signo = SIGKILL; //kill -9 else $signo = SIGTERM; //kill //only use in singleton model if (!$this->_singleton) throw new KalonDaemonException("'stop' only use in singleton model."); if (false === ($pid = $this->_getPidFromFile())) throw new KalonDaemonException("daemon is not running,cannot stop."); if (!posix_kill($pid, $signo)) { throw new KalonDaemonException("Cannot send signal $signo to daemon."); } $this->_unlinkPidFile(); $this->_out("Daemon stopped with pid {$pid}..."); return true; } /** * restart daemon */ public function restart() { $this->stop(); //sleep to wait sleep(1); $this->start(); } /** * get daemon pid * @return int */ public function getDaemonPid() { return $this->_getPidFromFile(); } /** * signalHander for dameon * * @param int $signo */ public function signalHandler($signo) { $signFuns = $this->_signalHandlerFuns[$signo]; if (is_array($signFuns)) { foreach ($signFuns as $fun) { call_user_func($fun); } } //default action switch ($signo) { case SIGTERM: exit; break; default: // handle all other signals } } public function addSignalHandler($signo, $fun) { if (is_string($fun)) { if (!function_exists($fun)) { throw new KalonDaemonException("handler function {$fun} not exists"); } }elseif (is_array($fun)) { if (!@method_exists($fun[0], $fun[1])) { throw new KalonDaemonException("handler method not exists"); } } else { throw new KalonDaemonException("error handler."); } if(!pcntl_signal($signo, array($this,"signalHandler"))) throw new KalonDaemonException("Cannot setup signal handler for signo {$signo}"); $this->_signalHandlerFuns[$signo][] = $fun; return $this; } public function sendSignal($signo) { if (false === ($pid = $this->_getPidFromFile())) throw new KalonDaemonException("daemon is not running,cannot send signal."); if (!posix_kill($pid, $signo)) { throw new KalonDaemonException("Cannot send signal $signo to daemon."); } //$this->_out("Send signal $signo to pid $pid..."); return true; } /** * daemon is active? * @return boolean */ public function isActive() { try { $pid = $this->_getPidFromFile(); } catch (KalonDaemonException $e) { return false; } if (false === $pid) return false; if (false === ($active = @pcntl_getpriority($pid))) return false; else return true; } /** * daemonize * 1.check running , if singaleton model * 2.forck process * 3.detach from controlling terminal * 4.log pid * * @return boolean */ private function _daemonize() { //single model, first check if running if ($this->_singleton) { $isRunning = $this->_checkRunning(); if ($isRunning) throw new KalonDaemonException("Daemon already running"); } //fork current process $pid = pcntl_fork(); if ($pid == -1) { //fork error throw new KalonDaemonException("Error happened while fork process"); } elseif ($pid) { //parent exit exit(); } else { //child, get pid $this->_pid = posix_getpid(); } $this->_out("Daemon started with pid {$this->_pid}..."); //detach from controlling terminal if (!posix_setsid()) throw new KalonDaemonException("Cannot detach from terminal"); //log pid in singleton model if ($this->_singleton) $this->_logPid(); return $this->_pid; } /** * get Pid From File * * @return int */ private function _getPidFromFile() { //if is set if ($this->_pid) return (int)$this->_pid; $pidFile = $this->_pidFilePath . "/" . $this->_pidFileName; //no pid file,it's the first time of running if (!file_exists($pidFile)) return false; if (!$handle = fopen($pidFile, "r")) throw new KalonDaemonException("Cannot open pid file {$pidFile} for read"); if (($pid = fread($handle, 1024)) === false) throw new KalonDaemonException("Cannot read from pid file {$pidFile}"); fclose($handle); return $this->_pid = (int) $pid; } /** * _checkRunning * in singleton mode ,we check if daemon running * * @return boolean */ private function _checkRunning() { $pid = $this->_getPidFromFile(); //no pid file,not running if(false === $pid) return false; //get exe file path from pid switch(strtolower(PHP_OS)) { case "freebsd": $strExe = $this->_getFreebsdProcExe($pid); if($strExe === false) return false; $strArgs = $this->_getFreebsdProcArgs($pid); break; case "linux": $strExe = $this->_getLinuxProcExe($pid); if($strExe === false) return false; $strArgs = $this->_getLinuxProcArgs($pid); break; default: return false; } $exeRealPath = $this->_getDaemonRealPath($strArgs, $pid); //get exe file path from command if ($strExe != PHP_BINDIR . "/php") return false; $selfFile = ""; $sapi = php_sapi_name(); switch($sapi) { case "cgi": case "cgi-fcgi": $selfFile = $_SERVER['argv'][0]; break; default: $selfFile = $_SERVER['PHP_SELF']; break; } $currentRealPath = realpath($selfFile); //compare two path if ($currentRealPath != $exeRealPath) return false; else return true; } /** * log Pid */ private function _logPid() { $pidFile = $this->_pidFilePath . "/" . $this->_pidFileName; if (!$handle = fopen($pidFile, "w")) { throw new KalonDaemonException("Cannot open pid file {$pidFile} for write"); } if (fwrite($handle, $this->_pid) == false) { throw new KalonDaemonException("Cannot write to pid file {$pidFile}"); } fclose($handle); } /** * unlink pid file * in singleton mode, unlink pid file while daemon stop * * @return boolean */ private function _unlinkPidFile() { $pidFile = $this->_pidFilePath . '/' . $this->_pidFileName; return @unlink($pidFile); } /** * get Daemon RealPath * * @param string $daemonFile * @param int $daemonPid * @return string */ private function _getDaemonRealPath($daemonFile, $daemonPid) { $daemonFile = trim($daemonFile); if(substr($daemonFile,0,1) !== "/") { $cwd = $this->_getLinuxProcCwd($daemonPid); $cwd = rtrim($cwd, "/"); $cwd = $cwd . "/" . $daemonFile; $cwd = realpath($cwd); return $cwd; } return realpath($daemonFile); } /** * get Freebsd ProcExe * * @param int $pid * @return string */ private function _getFreebsdProcExe($pid) { $strProcExeFile = "/proc/" . $pid . "/file"; if (false === ($strLink = @readlink($strProcExeFile))) { //throw new KalonDaemonException("Cannot read link file {$strProcExeFile}"); return false; } return $strLink; } /** * get Linux Proc Exe * * @param int $pid * @return string */ private function _getLinuxProcExe($pid) { $strProcExeFile = "/proc/" . $pid . "/exe"; if (false === ($strLink = @readlink($strProcExeFile))) { //throw new KalonDaemonException("Cannot read link file {$strProcExeFile}"); return false; } return $strLink; } /** * get Freebsd Proc Args * * @param int $pid * @return string */ private function _getFreebsdProcArgs($pid) { return $this->_getLinuxProcArgs($pid); } /** * get Linux Proc Args * * @param int $pid * @return string */ private function _getLinuxProcArgs($pid) { $strProcCmdlineFile = "/proc/" . $pid . "/cmdline"; if (!$fp = @fopen($strProcCmdlineFile, "r")) { throw new KalonDaemonException("Cannot open file {$strProcCmdlineFile} for read"); } if (!$strContents = fread($fp, 4096)) { throw new KalonDaemonException("Cannot read or empty file {$strProcCmdlineFile}"); } fclose($fp); $strContents = preg_replace("/[^/w/.///-]/", " " , trim($strContents)); $strContents = preg_replace("//s+/", " ", $strContents); $arrTemp = explode(" ", $strContents); if(count($arrTemp) < 2) { throw new KalonDaemonException("Invalid content in {$strProcCmdlineFile}"); } return trim($arrTemp[1]); } /** * get Linux Proc Cwd * * @param int $pid * @return string */ private function _getLinuxProcCwd($pid) { $strProcExeFile = "/proc/" . $pid . "/cwd"; if (false === ($strLink = @readlink($strProcExeFile))) { throw new KalonDaemonException("Cannot read link file {$strProcExeFile}"); } return $strLink; } /** * out put process info * if open _openOutput * * @param string $str * @return boolean */ private function _out($str) { if ($this->_verbose) { fwrite(STDOUT, $str . "/n"); } return true; } } /** * Exception for KalonDaemon */ class KalonDaemonException extends Exception { } ?>