一.PHP基本架構

原文github地址

1.PHP概述

1.1 PHP的歷史發展

1995年由Lerdorf創建PHP,高級腳本語言,尤其適合Web開發,快速、靈活和實用是PHP最重要的特點。

在1997年,任職于 Technion IIT 公司的兩個以色列程序設計師:Zeev Suraski 和 Andi Gutmans,重寫了PHP的解析器。

1998年6月正式發布 PHP 3。Zeev Suraski 和 Andi Gutmans 在 PHP 3 發布后開始改寫 PHP 的核心,這個在1999年發布的解析器稱為 Zend Engine。

在2000年5月22日,以Zend Engine 1.0為基礎的PHP 4正式發布。

2004年7月13日則發布了PHP 5,PHP 5則使用了第二代的Zend Engine。

2015年6月11日,PHP官網發布消息,正式公開發布PHP7第一版的alpha版本。

1.2 特性

PHP 獨特的語法混合了 C、Java、Perl 以及 PHP 自創新的語法,豐富的語法支持、同時支持面向對象、面向過程,相比C、Java等語言具有語法簡潔、使用靈活、開發效率高。

1.3 PHP的相關組成
1.3.1 SAPI

SAPI是PHP的接入層,它接收用戶的請求,然后調用PHP內核提供的一些接口完成PHP腳本的執行,所以嚴格意義上講SAPI并不算PHP內核的一部分。PHP本身可以理解為是一個庫函數,提供語言的編譯與執行服務,它有標準的輸入、輸出。

PHP中常用的SAPI有CLI、php-fpm(CGI),CLI是命令行下執行PHP腳本的實現:bin/php script.php,它是單進程的,處理模型比較簡單,而php-fpm相對比較復雜,它實現了網絡處理模塊,用于與web服務器交互。

1.3.2 Zend引擎

Zend是PHP語言實現的最為重要的部分,是PHP最基礎、最核心的部分,它的源碼在/Zend目錄下,PHP代碼從編譯到執行都是由Zend完成的,后面章節絕大部分的源碼分析都是針對Zend的。Zend整體由兩個部分組成:

  • 編譯器: 負責將PHP代碼編譯為抽象語法樹,然后進一步編譯為可執行的opcodes,這個過程相當于GCC的工作,編譯器是一個語言實現的基礎
  • 執行器: 負責執行編譯器輸出的opcodes,也就是執行PHP腳本中編寫的代碼邏輯

2. 執行流程

PHP的生命周期
image.png
  • php_module_startup() 模塊初始化階段
  • php_request_startup() 請求初始化階段
  • php_execute_script() 執行PHP腳本階段
  • php_request_shutdown() 請求結束階段
  • php_module_shutdown() 模塊關閉階段

3. FPM

3.1 概述

FPM(FastCGI Process Manager)是PHP FastCGI運行模式的一個進程管理器,核心功能是進程管理,那么它用來管理什么進程呢?這個問題就需要從FastCGI說起了。

FastCGI是Web服務器(如:Nginx、Apache)和處理程序之間的一種通信協議,它是與Http類似的一種應用層通信協議,注意:它只是一種協議!

web服務器來處理http請求,然后將解析的結果再通過FastCGI協議轉發給處理程序,處理程序處理完成后將結果返回給web服務器,web服務器再返回給用戶,如下圖所示。

image.png

PHP實現了FastCGI協議的解析,但是并沒有具體實現網絡處理,一般的處理模型:多進程、多線程。多進程模型通常是主進程只負責管理子進程,而基本的網絡事件由各個子進程處理,nginx、fpm就是這種模式。

3.2 基本實現

fpm的實現就是創建一個master進程,在master進程中創建并監聽socket,然后fork出多個子進程,這些子進程各自accept請求。

fpm的子進程子進程在啟動后阻塞在accept上,同時只能響應一個請求,只有把這個請求處理完成后才會accept下一個請求。(nginx則是一個進程會同時連接多個請求,它是非阻塞的模型,只處理活躍的套接字)。

fpm的master進程與worker進程之間不會直接進行通信,master通過共享內存獲取worker進程的信息,比如worker進程當前狀態、已處理請求數等,當master進程要殺掉一個worker進程時則通過發送信號的方式通知worker進程。

fpm可以同時監聽多個端口,每個端口對應一個worker pool,而每個pool下對應多個worker進程,類似nginx中server概念。

image.png

具體實現上worker pool通過fpm_worker_pool_s這個結構表示,多個worker pool組成一個單鏈表:

struct fpm_worker_pool_s {
    struct fpm_worker_pool_s *next; //指向下一個worker pool
    struct fpm_worker_pool_config_s *config; //conf配置:pm、max_children、start_servers...
    int listening_socket; //監聽的套接字
    ...

    //以下這個值用于master定時檢查、記錄worker數
    struct fpm_child_s *children; //當前pool的worker鏈表
    int running_children; //當前pool的worker運行總數
    int idle_spawn_rate;
    int warn_max_children;

    struct fpm_scoreboard_s *scoreboard; //記錄worker的運行信息,比如空閑、忙碌worker數
    ...
}
3.3 FPM的初始化

fpm的啟動流程,從main()函數開始:

//sapi/fpm/fpm/fpm_main.c
int main(int argc, char *argv[])
{
    ...
    //注冊SAPI:將全局變量sapi_module設置為cgi_sapi_module
    sapi_startup(&cgi_sapi_module);
    ...
    //執行php_module_starup()
    if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
        return FPM_EXIT_SOFTWARE;
    }
    ...
    //初始化
    if(0 > fpm_init(...)){
        ...
    }
    ...
    fpm_is_running = 1;

    fcgi_fd = fpm_run(&max_requests);//后面都是worker進程的操作,master進程不會走到下面
    parent = 0;
    ...
}

fpm_init()主要有以下幾個關鍵操作:

  • 1.fpm_conf_init_main():
    解析php-fpm.conf配置文件,分配worker pool內存結構并保存到全局變量中:fpm_worker_all_pools,各worker pool配置解析到fpm_worker_pool_s->config中。
  • 2.fpm_scoreboard_init_main():
    分配用于記錄worker進程運行信息的共享內存,按照worker pool的最大worker進程數分配,每個worker pool分配一個fpm_scoreboard_s結構,pool下對應的每個worker進程分配一個fpm_scoreboard_proc_s結構,各結構的對應關系如下圖。
image.png
  • 3.fpm_signals_init_main(): master的信號處理
static int sp[2];

int fpm_signals_init_main()
{
    struct sigaction act;

    //創建一個全雙工管道
    if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
        return -1;
    }
    //注冊信號處理handler
    act.sa_handler = sig_handler;
    sigfillset(&act.sa_mask);
    if (0 > sigaction(SIGTERM,  &act, 0) ||
        0 > sigaction(SIGINT,   &act, 0) ||
        0 > sigaction(SIGUSR1,  &act, 0) ||
        0 > sigaction(SIGUSR2,  &act, 0) ||
        0 > sigaction(SIGCHLD,  &act, 0) ||
        0 > sigaction(SIGQUIT,  &act, 0)) {
        return -1;
    }
    return 0;
}
  • 4.fpm_sockets_init_main(): 創建每個worker pool的socket套接字。
  • 5.fpm_event_init_main():
    啟動master的事件管理,fpm實現了一個事件管理器用于管理IO、定時事件,其中IO事件通過kqueue、epoll、poll、select等管理,定時事件就是定時器,一定時間后觸發某個事件。
    在fpm_init()初始化完成后接下來就是最關鍵的fpm_run()操作了,此環節將fork子進程,啟動進程管理器,另外master進程將不會再返回,只有各worker進程會返回,也就是說fpm_run()之后的操作均是worker進程的。
int fpm_run(int *max_requests)
{
    struct fpm_worker_pool_s *wp;
    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
        //調用fpm_children_make() fork子進程
        is_parent = fpm_children_create_initial(wp);
        
        if (!is_parent) {
            goto run_child;
        }
    }
    //master進程將進入event循環,不再往下走
    fpm_event_loop(0);

run_child: //只有worker進程會到這里

    *max_requests = fpm_globals.max_requests;
    return fpm_globals.listening_socket; //返回監聽的套接字
}

在fork后worker進程返回了監聽的套接字繼續main()后面的處理,而master將永遠阻塞在fpm_event_loop(),接下來分別介紹master、worker進程的后續操作。

3.4 請求處理

fpm_run()執行后將fork出worker進程,worker進程返回main()中繼續向下執行,后面的流程就是worker進程不斷accept請求,然后執行PHP腳本并返回。整體流程如下:

  • 1.等待請求: worker進程阻塞在fcgi_accept_request()等待請求;
  • 2.解析請求: fastcgi請求到達后被worker接收,然后開始接收并解析請求數據,直到request數據完全到達;
  • 3.請求初始化: 執行php_request_startup(),此階段會調用每個擴展的:PHP_RINIT_FUNCTION();
  • 4.編譯、執行: 由php_execute_script()完成PHP腳本的編譯、執行;
  • 5.關閉請求: 請求完成后執行php_request_shutdown(),此階段會調用每個擴展的:PHP_RSHUTDOWN_FUNCTION(),然后進入步驟1等待下一個請求。

worker進程一次請求的處理被劃分為5個階段:

  • FPM_REQUEST_ACCEPTING: 等待請求階段
  • FPM_REQUEST_READING_HEADERS: 讀取fastcgi請求header階段
  • FPM_REQUEST_INFO: 獲取請求信息階段,此階段是將請求的method、query stirng、request uri等信息保存到各worker進程的fpm_scoreboard_proc_s結構中,此操作需要加鎖,因為master進程也會操作此結構
  • FPM_REQUEST_EXECUTING: 執行請求階段
  • FPM_REQUEST_END: 沒有使用
  • FPM_REQUEST_FINISHED: 請求處理完成

worker處理到各個階段時將會把當前階段更新到fpm_scoreboard_proc_s->request_stage,master進程正是通過這個標識判斷worker進程是否空閑的。

3.5 進程管理

這一節我們來看下master是如何管理worker進程的,首先介紹下三種不同的進程管理方式:

  • static: 這種方式比較簡單,在啟動時master按照pm.max_children配置fork出相應數量的worker進程,即worker進程數是固定不變的
  • dynamic: 動態進程管理,首先在fpm啟動時按照pm.start_servers初始化一定數量的worker,運行期間如果master發現空閑worker數低于pm.min_spare_servers配置數(表示請求比較多,worker處理不過來了)則會fork worker進程,但總的worker數不能超過pm.max_children,如果master發現空閑worker數超過了pm.max_spare_servers(表示閑著的worker太多了)則會殺掉一些worker,避免占用過多資源,master通過這4個值來控制worker數
  • ondemand: 這種方式一般很少用,在啟動時不分配worker進程,等到有請求了后再通知master進程fork worker進程,總的worker數不超過pm.max_children,處理完成后worker進程不會立即退出,當空閑時間超過pm.process_idle_timeout后再退出
    前面介紹到在fpm_run()master進程將進入fpm_event_loop()
void fpm_event_loop(int err)
{
    //創建一個io read的監聽事件,這里監聽的就是在fpm_init()階段中通過socketpair()創建管道sp[0]
    //當sp[0]可讀時將回調fpm_got_signal()
    fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
    fpm_event_add(&signal_fd_event, 0);

    //如果在php-fpm.conf配置了request_terminate_timeout則啟動心跳檢查
    if (fpm_globals.heartbeat > 0) {
        fpm_pctl_heartbeat(NULL, 0, NULL);
    }
    //定時觸發進程管理
    fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
    
    //進入事件循環,master進程將阻塞在此
    while (1) {
        ...
        //等待IO事件
        ret = module->wait(fpm_event_queue_fd, timeout);
        ...
        //檢查定時器事件
        ...
    }
}

這就是master整體的處理,其進程管理主要依賴注冊的幾個事件,接下來我們詳細分析下這幾個事件的功能。

(1)sp[1]管道可讀事件:

fpm_init()階段master曾創建了一個全雙工的管道:sp,然后在這里創建了一個sp[0]可讀的事件,當sp[0]可讀時將交由fpm_got_signal()處理,向sp[1]寫數據時sp[0]才會可讀,那么什么時機會向sp[1]寫數據呢?前面已經提到了:當master收到注冊的那幾種信號時會寫入sp[1]端,這個時候將觸發sp[0]可讀事件。

image.png

這個事件是master用于處理信號的,我們根據master注冊的信號逐個看下不同用途:

  • SIGINT/SIGTERM/SIGQUIT: 退出fpm,在master收到退出信號后將向所有的worker進程發送退出信號,然后master退出
  • SIGUSR1: 重新加載日志文件,生產環境中通常會對日志進行切割,切割后會生成一個新的日志文件,如果fpm不重新加載將無法繼續寫入日志,這個時候就需要向master發送一個USR1的信號
  • SIGUSR2: 重啟fpm,首先master也是會向所有的worker進程發送退出信號,然后master會調用execvp()重新啟動fpm,最后舊的master退出
  • SIGCHLD: 這個信號是子進程退出時操作系統發送給父進程的,子進程退出時,內核將子進程置為僵尸狀態,這個進程稱為僵尸進程,它只保留最小的一些內核數據結構,以便父進程查詢子進程的退出狀態,只有當父進程調用wait或者waitpid函數查詢子進程退出狀態后子進程才告終止,fpm中當worker進程因為異常原因(比如coredump了)退出而非master主動殺掉時master將受到此信號,這個時候父進程將調用waitpid()查下子進程的退出,然后檢查下是不是需要重新fork新的worker

具體處理邏輯在fpm_got_signal()函數中,這里不再羅列。

(2)fpm_pctl_perform_idle_server_maintenance_heartbeat():

這是進程管理實現的主要事件,master啟動了一個定時器,每隔1s觸發一次,主要用于dynamic、ondemand模式下的worker管理,master會定時檢查各worker pool的worker進程數,通過此定時器實現worker數量的控制,處理邏輯如下:

static void fpm_pctl_perform_idle_server_maintenance(struct timeval *now)
{
    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
        struct fpm_child_s *last_idle_child = NULL; //空閑時間最久的worker
        int idle = 0; //空閑worker數
        int active = 0; //忙碌worker數
        
        for (child = wp->children; child; child = child->next) {
            //根據worker進程的fpm_scoreboard_proc_s->request_stage判斷
            if (fpm_request_is_idle(child)) {
                //找空閑時間最久的worker
                ...
                idle++;
            }else{
                active++;
            }
        }
        ...
        //ondemand模式
        if (wp->config->pm == PM_STYLE_ONDEMAND) {
            if (!last_idle_child) continue;

            fpm_request_last_activity(last_idle_child, &last);
            fpm_clock_get(&now);
            if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) {
                //如果空閑時間最長的worker空閑時間超過了process_idle_timeout則殺掉該worker
                last_idle_child->idle_kill = 1;
                fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
            } 
            continue;
        }
        //dynamic
        if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
        if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
            //空閑worker太多了,殺掉
            last_idle_child->idle_kill = 1;
            fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
            wp->idle_spawn_rate = 1;
            continue;
        }
        if (idle < wp->config->pm_min_spare_servers) {
            //空閑worker太少了,如果總worker數未達到max數則fork
            ...
        }
    }
}

(3)fpm_pctl_heartbeat():

這個事件是用于限制worker處理單個請求最大耗時的,php-fpm.conf中有一個request_terminate_timeout的配置項,如果worker處理一個請求的總時長超過了這個值那么master將會向此worker進程發送kill -TERM信號殺掉worker進程,此配置單位為秒,默認值為0表示關閉此機制,另外fpm打印的slow log也是在這里完成的。

static void fpm_pctl_check_request_timeout(struct timeval *now)
{   
    struct fpm_worker_pool_s *wp;

    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
        int terminate_timeout = wp->config->request_terminate_timeout;
        int slowlog_timeout = wp->config->request_slowlog_timeout;
        struct fpm_child_s *child;

        if (terminate_timeout || slowlog_timeout) { 
            for (child = wp->children; child; child = child->next) {
                //檢查當前當前worker處理的請求是否超時
                fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);
            }
        }
    }
}

除了上面這幾個事件外還有一個沒有提到,那就是ondemand模式下master監聽的新請求到達的事件,因為ondemand模式下fpm啟動時是不會預創建worker的,有請求時才會生成子進程,所以請求到達時需要通知master進程,這個事件是在fpm_children_create_initial()時注冊的,事件處理函數為fpm_pctl_on_socket_accept(),具體邏輯這里不再展開,比較容易理解。

到目前為止我們已經把fpm的核心實現介紹完了,事實上fpm的實現還是比較簡單的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 第一章 Nginx簡介 Nginx是什么 沒有聽過Nginx?那么一定聽過它的“同行”Apache吧!Ngi...
    JokerW閱讀 32,736評論 24 1,002
  • Nginx的工作原理 1.Nginx的模塊與工作原理 Nginx由內核和模塊組成,其中,內核的設計非常微小和簡潔,...
    架構飛毛腿閱讀 6,056評論 1 27
  • php7基礎邏輯 worker進程 進程管理方式 static:啟動master的時候按照pm.max_child...
    影帆閱讀 623評論 0 1
  • 是誰踩碎了一地金色 任由將枯的葉跳舞 是誰碾壓了一簇潔白 任由將開的花哭泣 你在腳踏車上臆想 不經意間讓悲傷流露 ...
    惘然一書閱讀 256評論 0 1
  • 四月第一篇日記,簡單打卡。 暴風雨之前的平靜,我總是很平靜。
    發呆的李相赫閱讀 109評論 0 0