分享PHP守护进程类

5年以前  |  阅读数:1049 次  |  编程语言:PHP 

用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';
      }
    }
 相关文章:
PHP分页显示制作详细讲解
SSH 登录失败:Host key verification failed
获取IMSI
将二进制数据转为16进制以便显示
获取IMEI
文件下载
贪吃蛇
双位运算符
PHP自定义函数获取搜索引擎来源关键字的方法
Java生成UUID
发送邮件
年的日历图
提取后缀名
在Zeus Web Server中安装PHP语言支持
让你成为最历害的git提交人
Yii2汉字转拼音类的实例代码
再谈PHP中单双引号的区别详解
指定应用ID以获取对应的应用名称
Python 2与Python 3版本和编码的对比
php封装的page分页类完整实例