本文共 16467 字,大约阅读时间需要 54 分钟。
守护进程也称精灵进程(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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- class KalonDaemon
- {
-
-
-
-
-
- private $_pidFilePath = "/var/run";
-
-
-
-
-
-
- private $_pidFileName = "daemon.pid";
-
-
-
-
-
-
- private $_verbose = false;
-
-
-
-
-
-
- private $_singleton = true;
-
-
-
-
-
-
- private $_closeStdHandle = true;
-
-
-
-
-
-
- private $_pid = 0;
-
-
-
-
-
-
- private $_execFile = "";
-
-
-
-
-
-
-
- private $_signalHandlerFuns = array();
-
-
-
-
-
-
-
- public function __construct($configs = array())
- {
-
- if (is_array($configs))
- $this->setConfigs($configs);
- }
-
-
-
-
- public function _checkRequirement()
- {
-
- if (!extension_loaded('pcntl'))
- throw new KalonDaemonException("daemon needs support of pcntl extension, please enable it.");
-
-
- if ('cli' != php_sapi_name())
- throw new KalonDaemonException("daemon only works in cli sapi.");
- }
-
-
-
-
-
-
-
-
-
-
-
- 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;
- }
- }
- }
-
-
-
-
-
-
-
- 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;
- }
-
-
-
-
-
-
- public function getPidFilePath()
- {
- return $this->_pidFilePath;
- }
-
-
-
-
-
-
-
- public function setPidFileName($name)
- {
- if (empty($name))
- return false;
-
- $this->_pidFileName = trim($name);
- return true;
- }
-
-
-
-
-
-
- public function getPidFileName()
- {
- return $this->_pidFileName;
- }
-
-
-
-
-
-
-
-
- public function setVerbose($open = true)
- {
- $this->_verbose = (boolean) $open;
- return true;
- }
-
-
-
-
-
-
- public function getVerbose()
- {
- return $this->_verbose;
- }
-
-
-
-
-
-
-
-
-
- public function setSingleton($singleton = true)
- {
- $this->_singleton = (boolean) $singleton;
- return true;
- }
-
-
-
-
-
-
- public function getSingleton()
- {
- return $this->_singleton;
- }
-
-
-
-
-
-
-
- public function setCloseStdHandle($close = true)
- {
- $this->_closeStdHandle = (boolean) $close;
- return true;
- }
-
-
-
-
-
-
- public function getCloseStdHandle()
- {
- return $this->_closeStdHandle;
- }
-
-
-
-
-
-
-
-
-
- public function start()
- {
-
- $this->_checkRequirement();
-
-
- $this->_daemonize();
-
-
- if(!pcntl_signal(SIGTERM, array($this,"signalHandler")))
- throw new KalonDaemonException("Cannot setup signal handler for signo {$signo}");
-
-
-
-
-
-
- if ($this->_closeStdHandle) {
-
-
-
- }
-
- return true;
- }
-
-
-
-
-
-
-
-
-
- public function stop($force = false)
- {
- if ($force)
- $signo = SIGKILL;
- else
- $signo = SIGTERM;
-
-
- 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;
- }
-
-
-
-
- public function restart()
- {
- $this->stop();
-
- sleep(1);
-
- $this->start();
- }
-
-
-
-
-
- public function getDaemonPid()
- {
- return $this->_getPidFromFile();
- }
-
-
-
-
-
-
- public function signalHandler($signo)
- {
- $signFuns = $this->_signalHandlerFuns[$signo];
- if (is_array($signFuns)) {
- foreach ($signFuns as $fun) {
- call_user_func($fun);
- }
- }
-
-
- switch ($signo) {
- case SIGTERM:
- exit;
- break;
- default:
-
- }
-
- }
-
- 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.");
- }
-
-
- return true;
- }
-
-
-
-
-
- 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;
- }
-
-
-
-
-
-
-
-
-
-
-
- private function _daemonize()
- {
-
- if ($this->_singleton) {
- $isRunning = $this->_checkRunning();
- if ($isRunning)
- throw new KalonDaemonException("Daemon already running");
- }
-
-
- $pid = pcntl_fork();
-
- if ($pid == -1) {
-
- throw new KalonDaemonException("Error happened while fork process");
- } elseif ($pid) {
-
- exit();
- } else {
-
- $this->_pid = posix_getpid();
- }
-
- $this->_out("Daemon started with pid {$this->_pid}...");
-
-
- if (!posix_setsid())
- throw new KalonDaemonException("Cannot detach from terminal");
-
-
- if ($this->_singleton)
- $this->_logPid();
-
- return $this->_pid;
- }
-
-
-
-
-
-
- private function _getPidFromFile()
- {
-
- if ($this->_pid)
- return (int)$this->_pid;
-
- $pidFile = $this->_pidFilePath . "/" . $this->_pidFileName;
-
- 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;
- }
-
-
-
-
-
-
-
- private function _checkRunning()
- {
- $pid = $this->_getPidFromFile();
-
-
- if(false === $pid)
- return false;
-
-
- 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);
-
-
- 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);
-
-
-
- if ($currentRealPath != $exeRealPath)
- return false;
- else
- return true;
- }
-
-
-
-
- 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);
- }
-
-
-
-
-
-
-
- private function _unlinkPidFile()
- {
- $pidFile = $this->_pidFilePath . '/' . $this->_pidFileName;
- return @unlink($pidFile);
- }
-
-
-
-
-
-
-
-
- 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);
- }
-
-
-
-
-
-
-
- private function _getFreebsdProcExe($pid)
- {
- $strProcExeFile = "/proc/" . $pid . "/file";
- if (false === ($strLink = @readlink($strProcExeFile))) {
-
- return false;
- }
-
- return $strLink;
- }
-
-
-
-
-
-
-
- private function _getLinuxProcExe($pid)
- {
- $strProcExeFile = "/proc/" . $pid . "/exe";
- if (false === ($strLink = @readlink($strProcExeFile))) {
-
- return false;
- }
-
- return $strLink;
- }
-
-
-
-
-
-
-
- private function _getFreebsdProcArgs($pid)
- {
- return $this->_getLinuxProcArgs($pid);
- }
-
-
-
-
-
-
-
- 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]);
- }
-
-
-
-
-
-
-
- private function _getLinuxProcCwd($pid)
- {
- $strProcExeFile = "/proc/" . $pid . "/cwd";
- if (false === ($strLink = @readlink($strProcExeFile))) {
- throw new KalonDaemonException("Cannot read link file {$strProcExeFile}");
- }
-
- return $strLink;
- }
-
-
-
-
-
-
-
-
- private function _out($str)
- {
- if ($this->_verbose) {
- fwrite(STDOUT, $str . "/n");
- }
- return true;
- }
-
- }
-
-
-
-
- class KalonDaemonException extends Exception
- {
-
- }
- ?>