【PHP】一次請求過程的解析

Uchiha_Ponny發表於2019-07-09

一、概要

PHP-FPM啟動後,master程式會陷入event_loop(0)中來管理維持worker程式,而fork出的worker程式會回到主函式開始迴圈接收、處理請求。一次請求可以總結為 請求接收、請求處理、請求結束 三個階段,下面就詳細來講一下。

執行環境: Mac 10.14.2 + PHP 7.3.7

二、請求接收階段

  1. 對listen_socket加鎖: 因為accept()會有驚群問題,在呼叫accept()之前會對listen_socket加鎖。驚群問題在Linux2.6版本中得到解決,核心在收到一個客戶端連線時只會喚醒等待佇列上的第一個程式。
  2. 獲取client_socket: worker程式會呼叫 accept(listen_socket, (struct sockaddr *)&sa, &len) 從全連線佇列中接受一個連線,如果佇列中暫時沒有則會一直阻塞著,這裡的listen_socket是在fcgi_listen()中建立監聽的。
  3. 判斷client_socket是否被允許: 滿足如下請求之一即可
    1. client_socket為unix_socket,表明客戶端為本機
    2. 客戶端地址在allowed_clients列表裡,allowed_clients是通過listen.allowed_clients引數配置
  4. 等待client_socket上的可讀事件發生: 在do-while迴圈中呼叫poll()來監聽client_socket上的可讀事件,這裡的while條件是while (ret < 0 && errno == EINTR); EINTR錯誤是當阻塞中的poll()被捕獲到的訊號中斷所產生的錯誤,所以可以重新執行poll系統呼叫。
  5. 讀取client_socket中的資料: 這裡是對FastCGI協議的一個實現,Nginx會按照FastCGI協議的訊息格式傳送資料,worker程式再按照協議多次read()資料並解析,訊息傳遞大致如下。關於PHP如何實現FastCGI協議可以看下這篇文章

FastCGI訊息傳遞

三、請求處理階段

初始化

在上一階段讀取到請求資料後,worker程式接著會初始化輸出相關的堆疊、初始化編譯階段用到的compiler_globals(CG巨集)、執行階段用到的executor_globals(EG巨集)、執行每個擴充套件的PHP_RINIT_FUNCTION函式 等等。

ZendVM

講到請求處理階段就不得不提ZendVM,大家都知道PHP是解釋型語言,ZendVM就是PHP的直譯器,負責PHP的解析、執行。計算機理解不了PHP程式碼,但是ZendVM可以,對PHP而言,ZendVM就像是真正的“計算機“,這臺“計算機“可以識別的指令就是自己事先定義好的opcode。在執行時,PHP會被編譯為一系列opcode指令,ZendVM會逐個呼叫opcode對應的機器指令,最終完成PHP程式碼的執行。

ZendVM執行過程

  1. 詞法語法分析,生成AST: 這一步的目的是生成抽象語法樹AST,AST是PHP7引入的概念,PHP7之前是在語法分析後就直接生成opcode了。在這過程中語法分析器yacc不斷呼叫詞法分析器re2c將PHP程式碼切割為token,然後yacc根據token組合匹配語法規則,最終生成AST。
  2. 解析AST,生成zend_op_array:這一步的目的是生成zend_op_arrayzend_op_array是編譯後所有opline指令的集合,也包括編譯期間生成的關鍵資料。對於ZendVM而言,zend_op_array就是可執行資料。
  3. ZendVM執行zend_op_arrayzend_op_array作為ZendVM編譯器的輸出,也是ZendVM執行器的輸入。執行時,ZendVM執行器會呼叫opcode相應的handler完成指令的處理,其中handler是每條opcode對應的C語言編寫的處理邏輯。

四、請求結束階段

  1. 執行使用者通過register_shutdown_function()註冊的關閉函式
  2. 釋放資源,清理符號表,銷燬超全域性變數,重置max_execution_time 等等
  3. 沖刷掉所有緩衝區
  4. 執行每個擴充套件的PHP_RSHUTDOWN_FUNCTION函式
  5. …...

經過以上的清理操作,worker程式就準備好接收處理下一個請求了。

相關文章