当前位置 : 主页 > 网络编程 > PHP >

PHP 守护进程类

来源:互联网 收集:自由互联 发布时间:2021-06-30
用 PHP 实现的 Daemon 类。可以在服务器上实现队列或者脱离 crontab 的计划任务。 使用的时候,继承于这个类,并重写 _doTask 方法,通过 main 初始化执行。 ?phpclass Daemon { const DLOG_TO_CONSOL
用 PHP 实现的 Daemon 类。可以在服务器上实现队列或者脱离 crontab 的计划任务。 
使用的时候,继承于这个类,并重写 _doTask 方法,通过 main 初始化执行。
<?php

class Daemon {

    const DLOG_TO_CONSOLE = 1;
    const DLOG_NOTICE = 2;
    const DLOG_WARNING = 4;
    const DLOG_ERROR = 8;
    const DLOG_CRITICAL = 16;

    const DAPC_PATH =  '/tmp/daemon_apc_keys';

    /**
     * User ID
     *
     * @var int
     */
    public $userID = 65534; // nobody

    /**
     * Group ID
     *
     * @var integer
     */
    public $groupID = 65533; // nobody

    /**
     * Terminate daemon when set identity failure ?
     *
     * @var bool
     * @since 1.0.3
     */
    public $requireSetIdentity = false;

    /**
     * Path to PID file
     *
     * @var string
     * @since 1.0.1
     */
    public $pidFileLocation = '/tmp/daemon.pid';

    /**
     * processLocation
     * 进程信息记录目录
     *
     * @var string
     */
    public $processLocation = '';

    /**
     * processHeartLocation
     * 进程心跳包文件
     *
     * @var string
     */
    public $processHeartLocation = '';

    /**
     * Home path
     *
     * @var string
     * @since 1.0
     */
    public $homePath = '/';

    /**
     * Current process ID
     *
     * @var int
     * @since 1.0
     */
    protected $_pid = 0;

    /**
     * Is this process a children
     *
     * @var boolean
     * @since 1.0
     */
    protected $_isChildren = false;

    /**
     * Is daemon running
     *
     * @var boolean
     * @since 1.0
     */
    protected $_isRunning = false;

    /**
     * Constructor
     *
     * @return void
     */
    public function __construct() {

        error_reporting(0);
        set_time_limit(0);
        ob_implicit_flush();

        register_shutdown_function(array(&$this, 'releaseDaemon'));
    }

    /**
     * 启动进程
     *
     * @return bool
     */
    public function main() {

        $this->_logMessage('Starting daemon');

        if (!$this->_daemonize()) {
            $this->_logMessage('Could not start daemon', self::DLOG_ERROR);

            return false;
        }

        $this->_logMessage('Running...');

        $this->_isRunning = true;

        while ($this->_isRunning) {
            $this->_doTask();
        }

        return true;
    }

    /**
     * 停止进程
     *
     * @return void
     */
    public function stop() {

        $this->_logMessage('Stoping daemon');

        $this->_isRunning = false;
    }

    /**
     * Do task
     *
     * @return void
     */
    protected function _doTask() {
        // override this method
    }

    /**
     * _logMessage
     * 记录日志
     *
     * @param string 消息
     * @param integer 级别
     * @return void
     */
    protected function _logMessage($msg, $level = self::DLOG_NOTICE) {
        // override this method
    }

    /**
     * Daemonize
     *
     * Several rules or characteristics that most daemons possess:
     * 1) Check is daemon already running
     * 2) Fork child process
     * 3) Sets identity
     * 4) Make current process a session laeder
     * 5) Write process ID to file
     * 6) Change home path
     * 7) umask(0)
     *
     * @access private
     * @since 1.0
     * @return void
     */
    private function _daemonize() {

        ob_end_flush();

        if ($this->_isDaemonRunning()) {
            // Deamon is already running. Exiting
            return false;
        }

        if (!$this->_fork()) {
            // Coudn't fork. Exiting.
            return false;
        }

        if (!$this->_setIdentity() && $this->requireSetIdentity) {
            // Required identity set failed. Exiting
            return false;
        }

        if (!posix_setsid()) {
            $this->_logMessage('Could not make the current process a session leader', self::DLOG_ERROR);

            return false;
        }

        if (!$fp = fopen($this->pidFileLocation, 'w')) {
            $this->_logMessage('Could not write to PID file', self::DLOG_ERROR);
            return false;
        } else {
            fputs($fp, $this->_pid);
            fclose($fp);
        }

        // 写入监控日志
        $this->writeProcess();

        chdir($this->homePath);
        umask(0);

        declare(ticks = 1);

        pcntl_signal(SIGCHLD, array(&$this, 'sigHandler'));
        pcntl_signal(SIGTERM, array(&$this, 'sigHandler'));
        pcntl_signal(SIGUSR1, array(&$this, 'sigHandler'));
        pcntl_signal(SIGUSR2, array(&$this, 'sigHandler'));

        return true;
    }

    /**
     * Cheks is daemon already running
     *
     * @return bool
     */
    private function _isDaemonRunning() {

        $oldPid = file_get_contents($this->pidFileLocation);

        if ($oldPid !== false && posix_kill(trim($oldPid),0))
        {
            $this->_logMessage('Daemon already running with PID: '.$oldPid, (self::DLOG_TO_CONSOLE | self::DLOG_ERROR));

            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * Forks process
     *
     * @return bool
     */
    private function _fork() {

        $this->_logMessage('Forking...');

        $pid = pcntl_fork();

        if ($pid == -1) {
            // 出错
            $this->_logMessage('Could not fork', self::DLOG_ERROR);

            return false;
        } elseif ($pid) {
            // 父进程
            $this->_logMessage('Killing parent');

            exit();
        } else {
            // fork的子进程
            $this->_isChildren = true;
            $this->_pid = posix_getpid();

            return true;
        }
    }

    /**
     * Sets identity of a daemon and returns result
     *
     * @return bool
     */
    private function _setIdentity() {

        if (!posix_setgid($this->groupID) || !posix_setuid($this->userID))
        {
            $this->_logMessage('Could not set identity', self::DLOG_WARNING);

            return false;
        }
        else
        {
            return true;
        }
    }

    /**
     * Signals handler
     *
     * @access public
     * @since 1.0
     * @return void
     */
    public function sigHandler($sigNo) {

        switch ($sigNo)
        {
            case SIGTERM:   // Shutdown
                $this->_logMessage('Shutdown signal');
                exit();
                break;

            case SIGCHLD:   // Halt
                $this->_logMessage('Halt signal');
                while (pcntl_waitpid(-1, $status, WNOHANG) > 0);
                break;
            case SIGUSR1:   // User-defined
                $this->_logMessage('User-defined signal 1');
                $this->_sigHandlerUser1();
                break;
            case SIGUSR2:   // User-defined
                $this->_logMessage('User-defined signal 2');
                $this->_sigHandlerUser2();
                break;
        }
    }

    /**
     * Signals handler: USR1
     *  主要用于定时清理每个进程里被缓存的域名dns解析记录
     *
     * @return void
     */
    protected function _sigHandlerUser1() {
        apc_clear_cache('user');
    }

    /**
     * Signals handler: USR2
     * 用于写入心跳包文件
     *
     * @return void
     */
    protected function _sigHandlerUser2() {

        $this->_initProcessLocation();

        file_put_contents($this->processHeartLocation, time());

        return true;
    }

    /**
     * Releases daemon pid file
     * This method is called on exit (destructor like)
     *
     * @return void
     */
    public function releaseDaemon() {

        if ($this->_isChildren && is_file($this->pidFileLocation)) {
            $this->_logMessage('Releasing daemon');

            unlink($this->pidFileLocation);
        }
    }

    /**
     * writeProcess
     * 将当前进程信息写入监控日志,另外的脚本会扫描监控日志的数据发送信号,如果没有响应则重启进程
     *
     * @return void
     */
    public function writeProcess() {

        // 初始化 proc
        $this->_initProcessLocation();

        $command = trim(implode(' ', $_SERVER['argv']));

        // 指定进程的目录
        $processDir = $this->processLocation . '/' . $this->_pid;
        $processCmdFile = $processDir . '/cmd';
        $processPwdFile = $processDir . '/pwd';

        // 所有进程所在的目录
        if (!is_dir($this->processLocation)) {
            mkdir($this->processLocation, 0777);
            chmod($processDir, 0777);
        }

        // 查询重复的进程记录
        $pDirObject = dir($this->processLocation);
        while ($pDirObject && (($pid = $pDirObject->read()) !== false)) {
            if ($pid == '.' || $pid == '..' || intval($pid) != $pid) {
                continue;
            }

            $pDir = $this->processLocation . '/' . $pid;
            $pCmdFile = $pDir . '/cmd';
            $pPwdFile = $pDir . '/pwd';
            $pHeartFile = $pDir . '/heart';

            // 根据cmd检查启动相同参数的进程
            if (is_file($pCmdFile) && trim(file_get_contents($pCmdFile)) == $command) {
                unlink($pCmdFile);
                unlink($pPwdFile);
                unlink($pHeartFile);

                // 删目录有缓存
                usleep(1000);

                rmdir($pDir);
            }
        }

        // 新进程目录
        if (!is_dir($processDir)) {
            mkdir($processDir, 0777);
            chmod($processDir, 0777);
        }

        // 写入命令参数
        file_put_contents($processCmdFile, $command);
        file_put_contents($processPwdFile, $_SERVER['PWD']);

        // 写文件有缓存
        usleep(1000);

        return true;
    }

    /**
     * _initProcessLocation
     * 初始化
     *
     * @return void
     */
    protected function _initProcessLocation() {

        $this->processLocation = ROOT_PATH . '/app/data/proc';
        $this->processHeartLocation = $this->processLocation . '/' . $this->_pid . '/heart';
    }
}
网友评论