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來建立守護進程了。

相關文章
相關標籤/搜索