PHP建立守护进程(Daemon)详解

基本概念

守护进程

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端而且周期性地执行某种任务或等待处理某些发生的事件。php

进程组

是一个或多个进程的集合,进程组有进程组ID来惟一标识。除了进程号(PID)以外,进程组ID也是一个进程的必备属性。每一个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。编程

会话

会话是一个或多个进程组的集合。一般一个会话开始于用户登陆,终止于用户退出,在此期间该用户运行的全部进程都属于这个会话期。api

守护进程编程要点

1. 成为后台进程

fork子进程且父进程退出,控制终端将子进程放入后台执行,方法是在进程中调用fork(),而后父进程终止,全部后续工做在子进程中进行。bash

用fork建立子进程,父进程退出,子进程成为孤儿进程被init接管,子进程变为后台进程。函数

2. 在子进程中建立新会话

先介绍一下Linux中的进程控制终端登录会话进程组之间的关系。进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话能够包含多个进程组,这些进程组共享一个控制终端,这个控制终端一般是建立进程的登录终端。控制终端、登录会话和进程组一般是从父进程继承下来的,咱们的目的就是要让子进程脱离它们的控制。方法是在子进程中调用posix_setsid()使之成为会话组长。setsid的做用就是让进程摆脱原会话和原进程组的控制。学习

Linux内核经过维护会话和进程组来管理多用户进程。每一个进程是一个进程组的成员,而每一个进程组又是某个会话的成员。通常而言,当用户在某个终端上登陆时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。相似的,每一个会话也对应有一个领头进程。同一会话中的进程经过该会话的领头进程和一个终端相连,该终端做为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户经过控制终端,能够向该控制终端所控制的会话中的进程发送键盘信号。同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端得到输入,而其余进程均是后台进程,可能分属于不一样的后台进程组。this

3. 改变当前目录为根目录

进程活动时,其工做目录所在的文件系统不能卸载,通常须要将工做目录改变到根目录。对于须要写运行日志的进程将工做目录改变到特定目录如chdir('/'),若有须要,也能够把当前工做目录换成其余路径。编码

4. 重设文件权限掩码

进程从父进程那里继承了文件建立掩模,它可能修改守护进程所建立的文件的存取位。为防止这一点,经过umask(0)能够将文件掩模清除,若是应用程序根本就不涉及建立新文件或是文件访问权限的限定,这一步不是必须的。spa

5. 关闭文件描述符

同文件权限掩码同样,新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不被咱们的Daemon进程读或写,但它们同样消耗系统资源,并且可能致使所在的文件系统没法卸载。文件描述符为0、一、2的三个文件(分别表明标准输入、标准输出、标准错误),也须要被关闭,在PHP中只须要fclose()就能够了。日志

fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
复制代码

6. 守护进程退出时发送信号并处理

当用户须要外部中止守护进程运行时,每每会使用kill命令中止该守护进程。因此守护进程中须要编码来实现kill发出的signal信号处理,达到进程的正常退出。

//每执行n条低级语句就检查一次该进程是否有未处理过的信号(n经过ticks指定)
declare(ticks=1);

//信号处理函数
function sigHandler($signo) {
    switch ($signo) {
        case SIGTERM:
            //处理SIGTERM信号-进程终止
            break;
        case SIGHUP:
            //处理SIGHUP信号-终止控制终端或进程
            break;
        case SIGUSR1:
            //用户信号
            echo 'Caught SIGUSR1...'.PHP_EOL;
            break;
        default:
            //处理全部其余信号
    }
}

//安装信号处理器
pcntl_signal(SIGTERM, 'sigHandler');
pcntl_signal(SIGHUP, 'sigHandler');
pcntl_signal(SIGUSR1, 'sigHandler');

//向当前进程发送SIGUSR1信号
posix_kill(posix_getpid(), SIGUSR1);
复制代码

守护进程示例

abstract class Daemon {
    private $name;
    private $prefix;
    private $pidFile;

    private $stdin;
    private $stdout;
    private $stderr;
    
    public function __construct($config=[]) {
        $this->checkEnvironment();
        $this->initData($config);
    }
        
    /** * 检测环境是否知足要求 */
    private function checkEnvironment() {
        if (php_sapi_name() != 'cli') {
            exit('The program should run in CLI.'.PHP_EOL);
        }
        if (!extension_loaded('pcntl')) {
            exit('Need PHP pcntl extension.'.PHP_EOL);
        }
        if (!extension_loaded('posix')) {
            exit('Need PHP posix extension.'.PHP_EOL);
        }
    }

    /** * 初始化数据函数 */
    private function initData($config) {
        $this->name = isset($config['name']) ? strtolower($config['name']) : strtolower(__CLASS__);
        $this->prefix = isset($config['prefix']) ? $config['prefix'] : '/tmp';
        if (!file_exists($this->prefix) || !is_dir($this->prefix)) mkdir($this->prefix, 0755, true);
        $runDir = $this->prefix.'/run';
        $logDir = $this->prefix.'/log';
        if (!file_exists($runDir)) mkdir($runDir, 0755);
        if (!file_exists($logDir)) mkdir($logDir, 0755);
        $this->pidFile = $runDir.'/'.$this->name.'.pid';
        $this->stdin = '/dev/null';
        $this->stdout = $logDir.'/'.$this->name.'.log';
        $this->stderr = $logDir.'/'.$this->name.'.error';
    }

    /** * 检测pid文件是否存在 */
    private function checkPidFile() {
        if (file_exists($this->pidFile)) {
            $pid = intval(file_get_contents($this->pidFile));
            //向进程发送一个默认信号用来查看进程是否还存活
            if ($pid > 0 && posix_kill($pid, 0)) {
                return true;
            } else {
                unlink($this->pidFile);
                return false;
            }
        }
        return false;
    }

    /** * 建立pid文件 */
    private function createPidFile() {
        if (($fp=fopen($this->pidFile, 'w')) === false) {
            exit('Pid file create failed.'.PHP_EOL);
        }
        fwrite($fp, posix_getpid());
        fclose($fp);
    }

    /** * daemon化程序 */
    private function daemonize() {   
        global $stdin, $stdout, $stderr;
        //建立子进程,父进程退出,在子进程中运行
        $pid = pcntl_fork();
        if ($pid < 0) {
            exit('Fork failed.'.PHP_EOL);
        } else if ($pid > 0) {
            //父进程退出
            exit(0);
        }
        //设置当前进程为会话组长
        if (posix_setsid() < 0) {
            exit('Session leader set failed.'.PHP_EOL);
        }
        //改变当前目录为根目录
        chdir('/');
        //重设文件掩码
        umask(0);
        //关闭打开的文件描述符
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        /** * 若是关闭了标准输入/输出/错误描述符 * 那么打开的前三个文件描述符将成为新的标准输入/输出/错误的文件描述符 * 使用的$stdin,$stdout,$stderr就是普通的变量 * 必须指定为全局变量,不然文件描述符将在函数执行完毕后被释放 */
        $stdin = fopen($this->stdin, 'r');
        $stdout = fopen($this->stdout, 'a+');
        $stderr = fopen($this->stderr, 'a+');
        //生成pid文件
        $this->createPidFile();
        //执行任务
        $this->work();
    }

    abstract protected function work();

    /** * 启动服务 */
    private function start() {
        if ($this->checkPidFile()) {
            echo date('Y-m-d H:i:s').' The '.$this->name.' is running.'.PHP_EOL;  
        } else {
            echo date('Y-m-d H:i:s').' Successfully started '.$this->name.'.'.PHP_EOL;
            $this->daemonize();
        }
    }

    /** * 中止服务 */
    private function stop() {
        if (file_exists($this->pidFile)) {
            $pid = intval(file_get_contents($this->pidFile));
            if ($pid > 0 && posix_kill($pid, SIGTERM)) {
                unlink($this->pidFile);
                echo date('Y-m-d H:i:s').' Successfully stopped '.$this->name.'.'.PHP_EOL; 
            } else {
                echo date('Y-m-d H:i:s').' Failed stopped '.$this->name.'.'.PHP_EOL; 
            }
        } else {
            echo date('Y-m-d H:i:s').' The '.$this->name.' is stopped.'.PHP_EOL; 
        }
    }

    /** * 检测服务状态 */
    private function status() {
        if ($this->checkPidFile()) {
            echo date('Y-m-d H:i:s').' The '.$this->name.' is running.'.PHP_EOL; 
        } else {
            echo date('Y-m-d H:i:s').' The '.$this->name.' is stopped.'.PHP_EOL; 
        }
    }

    public function run($argv) {
        $action = null;
        if (is_array($argv) && count($argv) == 2) {
            $action = $argv[1];
        }
        switch ($action) {
            case 'start':
                $this->start();
                break;
            case 'stop':
                $this->stop();
                break;
            case 'status':
                $this->status();
                break;
            default:
                echo 'Usage: start|stop|status'.PHP_EOL;
        }
    }
}
复制代码

上面定义了一个Daemon抽象类,里边包含一个work()抽象方法,咱们只须要继承这个类并实现work()方法,而后调用run()方法启动程序。

class TimerJob extends Daemon {
    protected function work() {
        //安装信号处理器
        pcntl_signal(SIGALRM, function() {
            echo 'Hello World!'.PHP_EOL;
        });
        $tick = 3;
        while (true) {
            pcntl_alarm($tick);
            //调用等待信号的处理器
            pcntl_signal_dispatch();
            sleep($tick);
        }
    }
}

$job = new TimerJob([
    'name' => 'timer',
    'prefix' => '/data/server/workspace/daemon',
]);
$job->run($argv);
复制代码

启动服务查看进程,以下所示:

$ php daemon.php start
2019-06-10 17:57:32 Successfully started timer.
$ ps -ef | head -1;ps -ef | grep daemon.php | grep -v grep
UID   PID  PPID   C STIME   TTY           TIME CMD
501 46608     1   0  5:57下午 ??         0:00.00 php daemon.php start
复制代码

还能够根据使用方法传入以下选项:

Usage: start|stop|status
复制代码

经过对守护进程基础知识的学习,在工做中面对此类业务场景时,咱们就可使用PHP来建立守护进程了。

相关文章
相关标签/搜索