PHP守护进程类 -- KalonDaemon
本文地址:http://tongxinmao.com/Article/Detail/id/14
守护进程也称精灵进程(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] view plaincopy
<?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] view plaincopy
<?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
{
}
?>
上一篇:PHP CLI模式下的多进程应用
下一篇:七牛谈分布式存储的元数据设计