KalonDaemon - 守护进程PHP版


守护进程也称精灵进程(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 
{
    
}
?>
备注:首先php要安装pcntl这个扩展,安装方式如下:
首先到php的源码目录:
root@wangjian-EX460-EX461:~/下载/lamp/php-5.5.0alpha1/ext/pcntl# pwd
/root/下载/lamp/php-5.5.0alpha1/ext/pcntl
root@wangjian-EX460-EX461:~/下载/lamp/php-5.5.0alpha1/ext/pcntl# /usr/local/php/bin/phpize
root@wangjian-EX460-EX461:~/下载/lamp/php-5.5.0alpha1/ext/pcntl# ./configure --with-php-config=/usr/local/php/bin/php-config 
root@wangjian-EX460-EX461:~/下载/lamp/php-5.5.0alpha1/ext/pcntl# make &&make install
最后生成了一个扩展
root@wangjian-EX460-EX461:~/下载/lamp/php-5.5.0alpha1/ext/pcntl# ls /usr/local/php/lib/php/extensions/debug-zts-20121113/
curl.so  pcntl.so

摘自:http://blog.csdn.net/phpkernel/article/details/6458991

优质内容筛选与推荐>>
1、ip数据网络基础
2、言论
3、Linux top命令简介
4、Codeforces243C-Colorado Potato Beetle(离散化+bfs)
5、zend studio 快捷键收集


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号