守护进程也称精灵进程(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
- {
- }
- ?>