深入瞭解PHP核心(三)概覽-SAPI概述

來源:互聯網
上載者:User
深入理解PHP核心(三)概覽-SAPI概述

本文連結:http://www.orlion.ml/234/

1、在PHP生命週期的各個階段,一些與服務相關的操作都是通過SAPI介面實現。這些內建實現的物理位置在PHP源碼的SAPI目錄。這個目錄存放了PHP對各個伺服器抽象層的代碼,例如命令列程式的實現,Apache的mod_php模組實現以及fastcgi的實現等等

在各個伺服器抽象層之間遵守著相同的約定,這裡我們稱之為SAPI介面。每個SAPI實現都是一個_sapi_module_struct結構體變數。(SAPI介面)。在PHP的源碼中,當需要調用伺服器相關資訊時,全部通過SAPI介面中對應的方法調用實現,而這些方法在各個伺服器抽象層實現時都會有各自的實現。由於很多操作的通用性,有很大一部分介面方法使用的是預設方法。為SPAI的簡單

以cgi模式和apache2伺服器為例,它們的啟動方法如下:

cgi_sapi_module.startup(&cgi_sapi_module) // cgi模式 cgi/cgi_main.c檔案apache_sapi_module.startup(&apache_sapi_module); // apache伺服器  apache2handler/sapi_apache2.c檔案

這裡的cgi_sapi_module是sapi_module_struct結構體的靜態變數。它的startup方法指向php_cgi_startup函數指標。在這個結構體中除了startup函數指標,還有許多其他方法或欄位,這些結構在伺服器的介面實現中都有定義

整個SAPI類似於一個物件導向中的模板方法模式的應用。SAPI.c和SAPI.h檔案所包含的一些函數就是模板方法模式中的抽象模板,各個伺服器對於sapi_module的定義及相關實現則是一個個具體的模板

2、Apache模組

(1)當PHP需要在Apache伺服器下運行時,一般來說,它可以mod_php5模組的形式整合,此時mod_php5模組的作用是接收Aapche傳遞過來的PHP檔案請求,並處理這些請求,然後將處理後的結果返回給Apache。如果我們在Apache啟動前在其設定檔中配置了PHP模組,PHP模組通過註冊apache2的ap_hook_post_config掛鈎,在Apache啟動的時候啟動此模組以接收PHP檔案的請求。

除了這種啟動時的載入方式,Apache的模組可以在啟動並執行時候動態裝載,這意味著對伺服器可以進行功能擴充而不需要重新對原始碼進行編譯,甚至不需要重啟伺服器。我們所需要做的僅僅是給伺服器發送訊號HUP或者AP_SIG_GEACEFUL通知伺服器重新載入模組。但是在動態裝載之前我們需要將模組編譯成為動態連結程式庫。此時的動態載入就是載入動態連結程式庫。Apache中對動態連結程式庫的處理是通過模組mod_so來完成的,因此mod_so模組不能被動態載入,它只能本靜態編譯進Apache的核心。這意味著它和Apache一起啟動的。

Apache是如何載入模組的呢?以mod_php5為例,首先在httpd.conf中添加一行:

LoadModule php5_module modules/mod_php5.so

在設定檔中添加了所示的指令後,Apache在載入模組時會根據模組名尋找模組並載入。Apache的每一個模組都是以module結構體的形式存在,module結構的name屬性在最後是通過宏STANDARD20_MODULE_STUFF以__FILE__體現。通過之前的指令中指定的路徑找到相關的動態連結程式庫檔案後,Apache通過內部的函數擷取動態連結程式庫中的內容,並將模組的內容載入到記憶體中指定變數中。

在真正啟用模組之前,Apache會檢查所有載入的模組是否為真正的Apache模組。最後Apache會調用相關的函數(ap_add_loaded_module)將模組啟用,此處的啟用就是將模組放入相應的鏈表中(ap_top_modules鏈表)

Apache載入的是PHP模組,那麼這個模組時怎麼實現的呢?Apache2的mod_php5模組包括sapi/apache2handler和sapi/apache2filter兩個目錄,在apache2_handle/mod_php5.c檔案中,模組定義的相關代碼如下:

AP_MODULE_DECLARE_DATA module php5_module = {    STANDARD20_MODULE_STUFF,        /* 宏,包括版本,小版本,模組索引,模組名,下一個模組指標等資訊,其中模組名以__FILE__體現*/    create_php_config,      /* create per-directory config structure */    merge_php_config,       /* merge per-directory config structures */    NULL,                   /* create per-server config structure */    NULL,                   /* merge per-server config structures */    php_dir_cmds,           /*模組定義的所有命令*/    php_ap2_register_hook  /*註冊鉤子,此函數通過ap_hoo_開頭的函數在一次處理過程中對於指定的步驟註冊鉤子*/};

它所對應的是Apache的module結構,module的結構定義如下:

typedef struct module_struct module;struct module_struct {    int version;    int minor_version;    int module_index;    const char *name;    void *dynamic_load_handle;    struct module_struct *next;    unsigned long magic;    void (*rewrite_args) (process_rec *process);    void *(*create_dir_config) (apr_pool_t *p, char *dir);    void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf);    void *(*create_server_config) (apr_pool_t *p, server_rec *s);    void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void *new_conf);    const command_rec *cmds;    void (*register_hooks) (apr_pool_t *p);}

上面的模組結構與我們在mod_php5.c中所看到的結構有一點不同,這是由於STANDARD20_MODULE_STUFF的原因,這個宏它包含了前面8個欄位的定義。STANDARD20_MODULE_STUFF宏的定義如下:

/** Use this in all standard modules */#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \                MODULE_MAGIC_NUMBER_MINOR, \                -1, \                __FILE__, \                NULL, \                NULL, \                MODULE_MAGIC_COOKIE, \                                NULL      /* rewrite args spot */

在php5_module定義的結構中,php_dir_cmds是模組定義的所有的指令集合,定義的內容如下:

const command_rec php_dir_cmds[] ={    AP_INIT_TAKE2("php_value", php_apache_value_handler, NULL,        OR_OPTIONS, "PHP Value Modifier"),    AP_INIT_TAKE2("php_flag", php_apache_flag_handler, NULL,        OR_OPTIONS, "PHP Flag Modifier"),    AP_INIT_TAKE2("php_admin_value", php_apache_admin_value_handler,        NULL, ACCESS_CONF|RSRC_CONF, "PHP Value Modifier (Admin)"),    AP_INIT_TAKE2("php_admin_flag", php_apache_admin_flag_handler,        NULL, ACCESS_CONF|RSRC_CONF, "PHP Flag Modifier (Admin)"),    AP_INIT_TAKE1("PHPINIDir", php_apache_phpini_set, NULL,        RSRC_CONF, "Directory containing the php.ini file"),    {NULL}};

這是mod_php5模組定義的指令表。它實際上是一個commond_rec結構的數組。當Apache遇到指令的時候將逐一遍曆各個模組中的指令表,尋找是否有那個模組能夠處理該指令,如果找到,則調用響應的處理函數,如果所有指令表中的模組都不能處理該指令,那麼將報錯,如上所見,mod_php5模組僅提供php_value等5個指令。

php_ap2_register_hook函數的定義如下:

void php_ap2_register_hook(apr_pool_t *p){    ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE);    ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE);    ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE);    ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE);}

以上代碼聲明了pre_config,post_config,handler和child_init4個掛鈎以及對應的處理函數。其中pre_config,post_config,child_init是啟動掛鈎,它們在伺服器啟動時調用。handler掛鈎是請求掛鈎,它在伺服器處理請求時調用。其中在post_config掛鈎中啟動php。它通過php_apache_server_startup函數實現,php_apache_server_startup函數通過調用sapi_startup啟動sapi,並通過調用php_apache2_startup來註冊sapi module struct,最後調用php_module_startup初始化php,其中又會初始化Zend引擎,以及填充zend_module_struct中的treat_data成員(通過php_startup_sapi_content_types)等。

  到這裡,我們知道了Apache載入mod_php5模組的整個過程,可是這個過程與我們的餓SAPI有什麼關係呢?mod_php5也定義了屬於Apache的sapi_module_struct結構:

static sapi_module_struct apache2_sapi_module = {"apache2handler","Apache 2.0 Handler", php_apache2_startup,                /* startup */php_module_shutdown_wrapper,            /* shutdown */ NULL,                       /* activate */NULL,                       /* deactivate */ php_apache_sapi_ub_write,           /* unbuffered write */php_apache_sapi_flush,              /* flush */php_apache_sapi_get_stat,           /* get uid */php_apache_sapi_getenv,             /* getenv */php_error,                  /* error handler */ php_apache_sapi_header_handler,         /* header handler */php_apache_sapi_send_headers,           /* send headers handler */NULL,                       /* send header handler */ php_apache_sapi_read_post,          /* read POST data */php_apache_sapi_read_cookies,           /* read Cookies */ php_apache_sapi_register_variables,php_apache_sapi_log_message,            /* Log message */php_apache_sapi_get_request_time,       /* Request Time */NULL,                       /* Child Terminate */ STANDARD_SAPI_MODULE_PROPERTIES};

這些方法都屬於Apache伺服器,以讀取cookie為例,當我們在Apache伺服器環境下,在PHP中調用讀取Cookie時,最終擷取的資料的位置是在啟用SAPI時,它所調用的方法是read_cookie。

SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);

對於每一個伺服器在載入時,我們都指定了sapi_module,而Apache的sapi_module是apache2_sapi_module。其中對應read_cookie的方法是php_apache_sapi_read_cookie函數。這也是定義SAPI結構的理由:統一介面,面向介面編程,具有更好的擴充性和適應性。

(2)Apache的運行過程

Apache的運行包括啟動階段和運行階段,啟動階段Apache以root完成啟動,整個過程處於單進程單線程的環境中,這個階段包括設定檔解析、模組載入、系統資源初始化(例如記錄檔、共用記憶體段、資料庫連接等)等工作。

在運行階段,Apache主要工作是處理使用者的服務要求,在這個階段Apache以普通使用者運行。主要是安全性考慮,Apache對HTTP的請求可以分為串連、處理和中斷連線三個大的階段。

2、FastCGI

(1)cgi是通用閘道介面(Common Gateway Intedface),它可以讓一個用戶端從網頁瀏覽器向執行在Web伺服器上的程式請求資料。CGI描述了用戶端和這個程式之間傳輸資料的標準。CGI的一個目的是獨立於任何語言,所以CGI可以用任何語言編寫,只要這種語言具有標準輸入、輸出和環境變數。如PHP、perl、tcl等。

FastCGI是Web伺服器和處理常式之間通訊的一種協議,是CGI的一種改進方案,FastCGI像是一個常駐型的CGI,它可以一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI對位人詬病的fork-and-execute模式)。正是因為它只是一個通訊協定,它還支援分布式的運算,即FastCGI程式可以在網站伺服器以外的主機上執行並且接受來自其他網站伺服器的請求

FastCGI的整個流程是這樣的:

  Step1:Web Server啟動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)

  Step2:FastCGI進程管理器自身初始化,啟動多個CGI解譯器進程(可見多個php-cgi)並等待來自web server的串連

  Step3:當用戶端請求到達Web Server時,FastCGI進程管理器選擇並串連到一個CGI解譯器。Web Server將CGI環境變數和標準輸入發送到FastCGI子進程php-cgi

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

(2)php中CGI實現

PHP的CGI實現了Fastcgi協議。是一個TCP或UDP協議的伺服器接受來自Web伺服器的請求,當啟動時建立TCP/UDP協議的伺服器的socket監聽,並接受相關請求並進行處理。隨後就進入了PHP的生命週期:模組初始化,sapi初始化,處理PHP請求,模組關閉,sapi關閉等 就構成了整個CGI的生命週期。

以TCP為例在,在TCP的服務端,一般會執行這樣幾個步驟:

1、調用socket函數建立一個TCP用的流式通訊端;

2、調用bind函數將伺服器的本地地址與前面建立的通訊端綁定;

3、調用listen函數將新建立的通訊端作為監聽,等待用戶端發起的串連,當用戶端有多個串連串連到這個通訊端時,可能需要排隊處理;

4、伺服器處理序調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而建立起一個串連;

5、當與用戶端建立串連後,伺服器調用read_stream函數讀取用戶端的請求;

6、處理完資料後,伺服器調用write函數向用戶端發送應答

TCP上客戶-伺服器事務的時序:

php的CGI實現從cgi_main.c檔案的main函數開始,在main函數中調用了定義在fastcgi.c檔案中的初始化,監聽等函數。對比TCP的流程,我們查看php對TCP協議的實現,雖然php本身也實現了這些流程,但是在main函數中一些過程被封裝成一個函數實現。對應TCP的操作流程,PHP首先會執行建立socket,綁定通訊端,建立監聽:

if (bindpath) {    fcgi_fd = fcgi_listen(bindpath, 128);   //  socket??2sfcgi_init?    ?    ...}

在fastcgi.c檔案中,fcig_listen函數主要用於建立、綁定socket並開始監聽,它走完了前面所列TCP流程的前三個階段,

 if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||        ...        bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||        listen(listen_socket, backlog) < 0) {        ...    }

當服務端初始化完成後,進程調用accept函數進入阻塞狀態,在main函數中我們看到如下代碼:

  while (parent) {        do {            pid = fork();   //  oÒ???J            switch (pid) {            case 0: //  ??J                parent = 0;                 /* don't catch our signals */                sigaction(SIGTERM, &old_term, 0);   //  ?â¯?                sigaction(SIGQUIT, &old_quit, 0);   //  ???£?                sigaction(SIGINT,  &old_int,  0);   //  ??K??                break;                ...                default:                /* Fine */                running++;                break;        } while (parent && (running < children));     ...        while (!fastcgi || fcgi_accept_request(&request) >= 0) {        SG(server_context) = (void *) &request;        init_request_info(TSRMLS_C);        CG(interactive) = 0;                    ...            }

如上的代碼是一個產生子進程,並等待使用者請求。在fcgi_accept_request函數中,程式會調用accept函數阻塞新建立的線程。當使用者的請求到達時,fcgi_accept_request函數會判斷是否處理使用者的請求,其中會過濾某些串連請求,忽略受限制客戶的請求,如果程式受理使用者的請求,他將分析請求的資訊,將相關的變數寫到對應的變數中。其中在讀取請求內容時調用了safe_read方法。如下所示:main()->fcgi_accept_request()->fcgi_read_request()->safe_read()

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count){    size_t n = 0;    do {    ... //  省略 對win32的處理        ret = read(req->fd, ((char*)buf)+n, count-n);   //  非win版本的讀操作D?    ... // 省略    } while (n != count); }

如上對應伺服器端讀取使用者的請求資料。

在請求初始化完成,讀取請求完畢後,就該處理請求的PHP檔案了。假設此次請求為PHP_MODE_STANDARD則會調用php_execute_script執行PHP檔案。在此函數中它先初始化此檔案相關的一些內容,然後再調用zend_execute_scripts函數,對PHP檔案進行詞法分析和文法分析,產生中間代碼,並執行zend_execute函數,從而執行這些中間代碼。

  在處理完使用者的請求後,服務端將返回資訊給用戶端,此時在main函數中調用的是fcgi_finish_request(&request , 1);fcgi_finish_request函數定義在fasftcgi.c檔案中。

  在發送了請求的應答後,伺服器端將會執行關閉操作,僅限於CGI本身的關閉,程式執行的是fcgi_close函數。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.