workerman 簡要分析

wokerman 啓動分析

@(學習)[workerman, php]php

前期想說的

也是最近纔看的代碼,遇到不懂得地方就去google,因此這篇文章裏面穿插了不少參考資料,能夠直接點擊閱覽。html

須要瞭解一些知識pcntlposixlibevent,而後咱們從服務的啓動開始來看。linux

啓動

runAll顧名思義,運行全部的,註釋中也寫了,Run all worker instances,運行全部的實例,也就是說腳本中能夠同時new多個worker服務,這也是後面一個重要的$workers包含了每個$worker都是一個服務實例。而後會根據每個實例初始化count個子進程。web

/**
 * Run all worker instances.
 *
 * @return void
 */
public static function runAll()
{
    // 判斷是否命令行模式
    self::checkSapiEnv();
    // 建立目錄、設置權限、綁定時鐘信號腳本
    self::init();
    // 解析cli的命令,完成start、reload、restart、kill、stop等
    self::parseCommand();
    // 是否開啓守護進程
    self::daemonize();
    // 對socket進行了一系列的配置
    self::initWorkers();
    // 註冊信號處理器
    self::installSignal();
    // 保存主進程的pid
    self::saveMasterPid();
    // 每一個$worker服務fork出count個子進程,而後給每一個子進程綁定loop循環監聽事件tcp
    self::forkWorkers();
    self::displayUI();
    self::resetStd();
    // 主進程來監聽子進程狀態
    self::monitorWorkers();
}

生成實例的id

根據對象生成hashid,而且同一個對象生成的id是同樣的。redis

$this->workerId = spl_object_hash($this);

而且將當前對象$this存儲到靜態數組中self::$_workers編程

建立資源環境

$this->_context = stream_context_create($context_option);

校驗是否命令行模式

只有命令行模式能夠執行api

if(php_sapi_name() != 'cli') {}

獲取初始化運行的腳本文件的絕對路徑

debug_backtrace返回的數組的最後一個元素就是初始化的文件,第一個元素就是當前執行該命令的文件。數組

$backtrace = debug_backtrace();
$self::startFile = $backtrace[count($backtrace) -1 ]['file']

生成pid的存儲文件

debug_backtrace獲取到的絕對路徑經過_進行分割做爲pid的文件名。服務器

建立日誌文件

若是路徑沒定義直接指定../workerman.logsocket

touch(self::$logFile);
chmod(self::$logFile,0622); // 可讀寫、寫、寫

初始化當前主進程狀態

self:$_status = self::STATUS_STARTING;

腳本使用self::$_status來存儲當前運行的狀態(current status),一共有四種狀態

// 開始
const STATUS_STARTING = 1;
// 運行
const STATUS_RUNNING = 2;
// 關閉
const STATUS_SHUTDOWN=4;
// 從新加載
const STATUS_RELOADING=8;

臨時文件

slef::$_statisticsFile = sys_get_temp_dir().'/workerman.status';

設置進程標題

// 設置當前進程的標題
cli_set_process_title($title);
// 若是不存在上面的方法,那麼使用proc_title擴展
if(extension_loaded('proctitle') && function_exists('setproctitle')) {
    setproctitle($title);
}

填充idMap

每一個$worker_id都是當前腳本中要初始化的實例,每一個服務要開啓$worker->count個子進程,先用0來填充數組。後面每開啓一個子進程,會將子進程的pid存儲到idMap中,用來後面主進程監控子進程,若是子進程意外終止,主進程能夠從新佛。

self::$_idMap[$worker_id] = array_fill(0, $worker->count, 0);

進程信號處理器

pcntl_signal安裝一個信號處理器,用來監聽信號SIGALRM。若是不安裝SIGALRM信號,當進程接收到SIGALRM信號時會默認終止進程。

pcntl_signal(SIGALRM, ['\Workerman\Lib\Timer','signalHandle'],false);

解析cli參數

容許進程腳本接受參數,用來完成相應的指令。

xxx.php start -d 會開啓守護進程
xxx.php start 會進入debug模式

判斷進程是否已經存在

從pid文件中讀取存在的進程號,若是進程號存在,而且進程也存活。若是傳入的命令是start而且pid文件中的進程id和當前腳本執行的不同,那麼說明腳本重複執行了,報錯。
posix_kill($master_pid,0)用來判斷進程是否存在,posix_kill本意是向該進程發送信號。

// Get master process PID.
$master_pid      = @file_get_contents(self::$pidFile);
$master_is_alive = $master_pid && @posix_kill($master_pid, 0);
// Master is still alive?
if ($master_is_alive) {
    // 若是已經有進程存在,而且當前執行的進程和已存在的進程不同,報錯。
    if ($command === 'start' && posix_getpid() != $master_pid) {
        self::log("Workerman[$start_file] already running");
        exit;
    }
}

命令處理

使用pcntl_signal註冊了信號,而後使用posix_kill發送信號,使用pcntl_signal_dispatch來處理信號。

kill

先發送kill -SIGINT ,千分之一毫秒以後發送kill -SIGKILL

status

  1. 刪掉臨時文件

  2. 向進程發送SIGUSR2信號

  3. 從臨時文件中讀取內容

  4. 結束腳本

restart、stop

向進程發送終止信號

$master_pid && posix_kill($master_pid, SIGINT);

while(1)循環判斷是否進程已經被終止,若是超過期間5s,那麼寫入日誌終止失敗,而且結束當前腳本。(沒有結束進程,只是結束了當前的命令腳本)

reload

發送特殊信號SIGUSR1,可是這個信號不會被當即執行,而是須要等待pcntl_signal_dispatch來進行信號分發。

posix_kill($master_pid,SIGUSR1);

例如用戶如今輸入了php worker.php reload,那麼會使全部的進程從新進行加載配置。

  1. 當前腳本解析參數

  2. 判斷進程是否存活,若是進程pid存在,可是進程沒有存活,那麼報錯,說not run

  3. 進程存活,註冊信號SIGUSR1

  4. 退出當前腳本

  5. 運行中的父進程開始觸發全部的信號,因爲以前已經安裝了信號處理方法,因此會觸發self::reload()方法。

啓動守護進程

fork兩次並非爲了不殭屍進程,而是爲了不svr4系統從新打開終端

守護進程詳解

守護進程爲何要fork兩次?

方法通用,能夠稍做修改,用到別的地方。用這個方法作過一個隊列監聽,redis的list啓動一個rpop。
umask(0)將默認權限的掩碼修改成0,即將要建立的全部的文佳你的權限都是777
$pid = pcntl_fork()啓動子進程,判斷$pid是否存在,只有在父進程中pcntl_fork()纔會返回id,咱們要將父進程kill掉。
posix_setsid()將當前子進程設置爲會話組leader
再次建立子進程,爲了防止在SVR4的系統下從新打開控制終端。

protected static function daemonize()
{
    if (!self::$daemonize) {
        return;
    }
    // 將默認權限掩碼修改成0,意味着即將要建立的文件的權限都是777
    umask(0);
    // 子進程
    $pid = pcntl_fork();
    // 子進程建立失敗
    if (-1 === $pid) {
        throw new Exception('fork fail');
    } elseif ($pid > 0) {  //說明當前進程是父進程,只有在父進程中fork纔會返回pid
        // 關閉父進程,讓子進程成爲孤兒進程被init進程收養
        exit(0);
    }
    // 將子進程做爲進程組的leader,開啓一個新的會話,脫離以前的會話和進程組。即便用戶logout也不會終止
    if (-1 === posix_setsid()) {
        throw new Exception("setsid fail");
    }
    // Fork again avoid SVR4 system regain the control of terminal.
    // 避免svr4系統從新獲取控制終端
    $pid = pcntl_fork();
    if (-1 === $pid) {
        throw new Exception("fork fail");
    } elseif (0 !== $pid) {
        // 若是不是孫子進程,直接幹掉。讓孫子進程成爲孤兒進程被init進程(1號進程收養)
        exit(0);
    }
}

初始化全部的woker實例

獲取進程的當前用戶信息

$user_info = posix_getpwuid(posix_getuid());

開啓監聽

UNIX域套接字相關知識
建立socket服務,啓動一個本地的socket服務。若是是unix域socket的話,$local_socket須要是本地的文件。我的理解就是經過某個端口來監聽某個協議。

$this->_mainSocket = stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context);

將包含socket的流導入到socket的擴展資源中

Imports a stream that encapsulates a socket into a socket extension resource.

關於爲何要使用socket_import_stream?

php提供兩種socket:php提供了兩種類型的socket,stream_socket 和 sockets,兩者api不兼容。stream_socket是php內置的,能夠直接使用,而且api和stream 的api通用(能夠調用fread fwrite...)。sockets須要php安裝sockets擴展才能使用。

設置socket

設置心跳檢測,減小傳輸延遲,設置爲非阻塞模式。

SO_KEEPALIVE
TCP/IP Socket心跳機制so_keepalive的三個參數詳解
TCP Keepalive HOWTO

設置心跳

SO_KEEPALIVE 保持鏈接檢測對方主機是否崩潰,避免(服務器)永遠阻塞於TCP鏈接的輸入

@socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);

設置最小化傳輸延遲
提升linux上socket性能

// 最小化傳輸延遲,而不是追求最小化報文數量
@socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);

設置爲非阻塞模式

在非阻塞模式下,調用 fgets() 老是會當即返回;而在阻塞模式下,將會一直等到從資源流裏面獲取到數據才能返回。

stream_set_blocking($this->_mainSocket,0);

註冊信號處理器

// stop
pcntl_signal(SIGINT, array('\Workerman\Worker', 'signalHandler'), false);
// reload 從新加載的時候發送R1信號
pcntl_signal(SIGUSR1, array('\Workerman\Worker', 'signalHandler'), false);
// status 查看狀態的時候發送R2信號
pcntl_signal(SIGUSR2, array('\Workerman\Worker', 'signalHandler'), false);
// ignore
pcntl_signal(SIGPIPE, SIG_IGN, false);

保存當前進程的pid

若是已經開啓了守護進程,那麼獲取的是當前孫子進程(守護進程)的pid,將其寫入到pid文件中。

將每一個woker實例(服務)建立count個子進程

遍歷整個$workers,將進程按照裏面的每個實例的參數fork count個數的子進程。而且將子進程的號碼,記錄到父進程中

// 建立子進程
$pid = pcntl_fork();
// Get available worker id.
$id = self::getId($worker->workerId, 0);
// For master process.
// 若是當前進程是父進程
if ($pid > 0) {
    // 將子進程的號碼,記錄到父進程中(重要)
    self::$_pidMap[$worker->workerId][$pid] = $pid;
    // 父進程將本身的pid寫入到idMap的第一位(很是重要),在此以後,同一個實例下每一個進程的id都不同。
    self::$_idMap[$worker->workerId][$id]   = $pid;
} // For child processes.

讓每一個子進程下都開始啓動對應worker的服務

$worker->run();

設置當前狀態爲運行中

self::$_status = self::STATUS_RUNNING;

設置globalEvent

self::getEventLoopName從三個事件擴展中選擇一個libevent,event,ev

事件監聽

向socket添加事件監聽回調函數

self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptUdpConnection'));

displayUI

終端下打印UI界面

再次重置標準化輸入輸出

self::resetStd();

監控全部子進程

PHP多進程編程初步
PHP經過PCNTL擴展實現進程控制

pcntl_signal()函數僅僅是註冊信號和它的處理方法,真正接收到信號並調用其處理方法的是pcntl_signal_dispatch()函數,而posix_kill會發送信號。

只有父進程纔會監控全部的子進程,由於子進程運行的是run方法

  1. 觸發全部的信號

  2. 等待子進程退出

  3. 將退出的子進程從實例維護的pidMap中移除

  4. 將退出的子進程從對應的idMap中進行重置爲0,取消佔取的位置

  5. 若是腳本仍然在執行,可是子進程退出了,那麼重啓子進程

  6. 獲取全部的子進程號,若是子進程都退出了,那麼結束父進程

關於pcntl_wait

pcntl_signal(SIGUSR1,'signalHandle1'); 用來註冊信號;posix_kill($pid,SIGUSR1)向指定進程發送信號(返回結果是即時的,可是並不會觸發信號所綁定的行爲);pcntl_signal_dispatch()用來觸發收到的信號的回調函數。

pcntl_wait($status,WUNTRACED)開啓阻塞模式來監控子進程是否退出,以後子進程退出以後,纔會執行後面的操做。

<?php

$pid = pcntl_fork();
if($pid == -1) {
    echo "建立子進程失敗\n";
    exit();
}

function signalHandle1() {
    echo "信號1回調\n";
}
function signalHandle2() {
    echo "信號2回調\n";
}
// 註冊信號
pcntl_signal(SIGUSR1, 'signalHandle1');
pcntl_signal(SIGUSR2, 'signalHandle2');

if($pid > 0) {
        $status = 0;
        // 等待子進程終止
        pcntl_wait($status,WUNTRACED);
        echo "wait已執行 {$pid}?\n";
        // 執行信號的回調函數
        pcntl_signal_dispatch();
}else {
    // 發送信號2
    posix_kill($pid,SIGUSR2);
    // 處理信號2的回調函數
    pcntl_signal_dispatch();
    // 父進程處理信號2
    sleep(3);
    // 子進程發送信號,看是否pcntl_wait會執行(結果,wait沒有執行)
    posix_kill($pid,SIGUSR1);
    // 繼續等待,觀察是否pcntl_wait等子進程結束後執行
    sleep(3);
    echo "子進程終止\n"; // 這個時候wait往下執行了,看來wait等待子進程讓父進程進行了掛起操做
}

輸出結果

信號2回調
子進程終止
wait已執行 18425?
信號2回調
信號1回調
相關文章
相關標籤/搜索