PHP內核剖析 SAPI之Fpm

以PHP7爲學習基礎,PHP7的源碼爲C編寫的。

參考書籍:《PHP內核剖析》秦鵬/著

GitHub網頁:https://github.com/pangudashu/php7-internal/blob/master/1/fpm.md

目錄

1 概述


1 概述

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

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

(1)Web Server啓動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)

(2)FastCGI進程管理器自身初始化,啓動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的連接。

(3)當客戶端請求到達Web Server時,FastCGI進程管理器選擇並連接到一個CGI解釋器。Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。

(4)FastCGI子進程完成處理後將標準輸出和錯誤信息從同一連接返回Web Server。當FastCGI子進程關閉連接時,請求便告處理完成。FastCGI子進程接着等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。 在CGI模式中,php-cgi在此便退出了。

在上述情況中,你可以想象CGI通常有多慢。每一個Web請求PHP都必須重新解析php.ini、重新載入全部擴展並重初始化全部數據結構。使用FastCGI,所有這些都只在進程啓動時發生一次。一個額外的好處是,持續數據庫連接(Persistent database connection)可以工作。

PHP只是一個腳本解析器,你可以把它理解爲一個普通的函數,輸入是PHP腳本。輸出是執行結果,假如我們想用PHP代替shell,在命令行中執行一個文件,那麼就可以寫一個程序來嵌入PHP解析器,這就是cli模式,這種模式下PHP就是普通的一個命令工具。接着我們又想:能不能讓PHP處理http請求呢?這時就涉及到了網絡處理,PHP需要接收請求、解析協議,然後處理完成返回請求。在網絡應用場景下,PHP並沒有像Golang那樣實現http網絡庫,而是實現了FastCGI協議,然後與web服務器配合實現了http的處理,web服務器來處理http請求,然後將解析的結果再通過FastCGI協議轉發給處理程序,處理程序處理完成後將結果返回給web服務器,web服務器再返回給用戶,如下圖所示。

PHP實現了FastCGI協議的解析,但是並沒有具體實現網絡處理,一般的處理模型:多進程、多線程。

多進程模型通常是主進程只負責管理子進程,而基本的網絡事件由各個子進程處理,nginx、fpm就是這種模式;

多線程模型與多進程類似,只是它是線程粒度,通常會由主線程監聽、接收請求,然後交由子線程處理,memcached就是這種模式,有的也是採用多進程那種模式:主線程只負責管理子線程不處理網絡事件,各個子線程監聽、接收、處理請求,memcached使用udp協議時採用的是這種模式。

2 基本實現

fpm是一種多進程模型,它是由一個master進程和多個worker進程組成。master啓動的時候回創建一個socket,但是不會接受,處理請求,而是fork出worker子進程去接受和處理請求

fpm的實現就是創建一個master進程,在master進程中創建並監聽socket,然後fork出多個worker子進程。

worker子進程各自accept請求,子進程的處理非常簡單,它在啓動後阻塞在accept上,有請求到達後開始讀取請求數據,讀取完成後開始處理然後再返回,在這期間是不會接收其它請求的,也就是說fpm的子進程同時只能響應一個請求,只有把這個請求處理完成後纔會accept下一個請求,這一點與nginx的事件驅動有很大的區別,nginx的子進程通過epoll管理套接字,如果一個請求數據還未發送完成則會處理下一個請求,即一個進程會同時連接多個請求,它是非阻塞的模型,只處理活躍的套接字。

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

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

在php-fpm.conf中通過[pool name]聲明一個worker pool,每個pool各自配置監聽的地址、進程管理方式、worker進程數等。上面這個例子配置監聽端口分別爲9000、9001,pool下的worker進程監聽所屬的端口。

[web1]
listen = 127.0.0.1:9000
...

[web2]
listen = 127.0.0.1:9001
...

            具體實現上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數
    ...
}

2 FPM的初始化

            FPM的main函數位於文件/sapi/fpm/fpm/fpm_main.c中。Fpm在啓動後首先會進行SAPI的註冊操作;接着會進入PHP聲明週期的module startup階段,在這個階段會調用各個擴展定義的MINT函數。然後進行一系列的初始化操作,最後master、worker進程進入不同的處理環節。 

//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結構,各結構的對應關係如下圖。

            (3)fpm_signals_init_main():

            這裏會通過socketpair()創建一個管道,這個管道並不是用於master與worker進程通信的,它只在master進程中使用,具體用途在稍後介紹event事件處理時再作說明。另外設置master的信號處理handler,當master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT這些信號時將調用sig_handler()處理,在此函數中會把收到的信號寫入在fpm_singnals_init_main()中創建管道:

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;
}

static void sig_handler(int signo)
{
    static const char sig_chars[NSIG + 1] = {
        [SIGTERM] = 'T',
        [SIGINT]  = 'I',
        [SIGUSR1] = '1',
        [SIGUSR2] = '2',
        [SIGQUIT] = 'Q',
        [SIGCHLD] = 'C'
    };
    char s;
    ...
    s = sig_chars[signo];
    //將信號通知寫入管道sp[1]端
    write(sp[1], &s, sizeof(s));
    ...
}

            (4)fpm_sockets_init_main()

            創建每個worker pool的socket套接字,將監聽此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進程的後續操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章