<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sapi on Tarragon</title><link>https://tarrragon.github.io/blog/tags/sapi/</link><description>Recent content in Sapi on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 05 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/sapi/index.xml" rel="self" type="application/rss+xml"/><item><title>PHP 的請求生命週期與 SAPI：直譯器的位置、程序壽命與狀態壽命</title><link>https://tarrragon.github.io/blog/programming/php/01-server-runtime/request-lifecycle-and-sapi/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/php/01-server-runtime/request-lifecycle-and-sapi/</guid><description>&lt;p>這篇說明 PHP 直譯器與 web 伺服器之間的那一層：一個 HTTP 請求進來之後，PHP 在什麼時候被啟動、哪些狀態在請求結束時被清掉、哪些資源留在程序裡。範圍不含 PHP 語法與框架；文中的行為取自 PHP 8.5，用官方 Docker 映像檔實測。&lt;/p>
&lt;h2 id="一個請求在-php-裡經過的階段">一個請求在 PHP 裡經過的階段&lt;/h2>
&lt;p>PHP 的執行分成兩層生命週期：&lt;strong>程序層&lt;/strong>與&lt;strong>請求層&lt;/strong>。程序層的工作每個程序只做一次，請求層的工作每個請求都做一次。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>階段&lt;/th>
 &lt;th>PHP 在這個階段做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>程序層&lt;/td>
 &lt;td>模組啟動&lt;/td>
 &lt;td>讀 &lt;code>php.ini&lt;/code>、載入並初始化擴充（&lt;code>pdo_pgsql&lt;/code>、&lt;code>redis&lt;/code> 這類），每個擴充註冊自己的函式與類別&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>請求層&lt;/td>
 &lt;td>請求啟動&lt;/td>
 &lt;td>依這個請求建立 &lt;code>$_SERVER&lt;/code>、&lt;code>$_GET&lt;/code>、&lt;code>$_POST&lt;/code>、&lt;code>$_COOKIE&lt;/code> 等超全域變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>請求層&lt;/td>
 &lt;td>執行腳本&lt;/td>
 &lt;td>把 &lt;code>.php&lt;/code> 檔編譯成 opcode（Zend 引擎執行的指令），再執行它&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>請求層&lt;/td>
 &lt;td>請求結束&lt;/td>
 &lt;td>釋放這個請求建立的所有變數、物件與資源，送出剩下的輸出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程序層&lt;/td>
 &lt;td>模組結束&lt;/td>
 &lt;td>程序要結束時，各擴充釋放自己的資源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>請求結束那一步的範圍很大：全域變數、函式內的 &lt;code>static&lt;/code> 變數、類別的靜態屬性、開過的檔案與一般的資料庫連線，全部在這一步釋放。下一個請求就算由同一個程序處理，也看不到上一個請求留下的任何 PHP 變數。這個設計叫 &lt;strong>shared-nothing&lt;/strong>：請求之間不共享應用程式的狀態，每個請求從空白開始（見 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/shared-nothing-request/" data-link-title="Shared-nothing 請求模型" data-link-desc="要判斷 PHP 的全域變數、static 變數或類別靜態屬性會不會留到下一個請求，或評估換成常駐 worker（Octane、FrankenPHP worker）時哪些程式碼會出問題時">Shared-nothing 請求模型&lt;/a>）。&lt;/p>
&lt;p>這件事可以直接量。下面這支腳本每次執行都把函式內的 &lt;code>static&lt;/code> 變數與一個全域變數加一，並印出處理它的程序編號：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="o">&amp;lt;?&lt;/span>&lt;span class="nx">php&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">function&lt;/span> &lt;span class="nf">counter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">static&lt;/span> &lt;span class="nv">$n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="o">++&lt;/span>&lt;span class="nv">$n&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">$GLOBALS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;g&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$GLOBALS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;g&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">??&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">echo&lt;/span> &lt;span class="s2">&amp;#34;pid=&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">getmypid&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s2">&amp;#34; static=&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">counter&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s2">&amp;#34; global=&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$GLOBALS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;g&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34; sapi=&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">PHP_SAPI&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 Nginx 加 PHP-FPM 的環境連續請求四次，輸出是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pid=7 static=1 global=1 sapi=fpm-fcgi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pid=6 static=1 global=1 sapi=fpm-fcgi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">pid=7 static=1 global=1 sapi=fpm-fcgi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">pid=6 static=1 global=1 sapi=fpm-fcgi&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個程序（PID 6 與 7）輪流處理請求，同一個程序處理第二次時，兩個計數器仍然是 1。程序活了下來，PHP 變數沒有。&lt;/p>
&lt;h3 id="請求結束後留在程序裡的資源">請求結束後留在程序裡的資源&lt;/h3>
&lt;p>請求結束只清掉 PHP 程式碼建立的變數，程序本身持有的資源可以跨請求存在，而且只有程序活得夠久時才有意義：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OPcache&lt;/strong>：把編譯好的 opcode 存在共享記憶體裡，下一個請求不必重新編譯同一個檔案。它的效果完全取決於程序活多久，在 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/opcache/" data-link-title="OPcache 與程序壽命：編譯結果的快取在各種執行模型下的效果" data-link-desc="說明 OPcache 快取的是什麼、它存在哪裡，以及為什麼它的效果取決於執行 PHP 的程序活多久：CGI 下命中次數永遠為零、PHP-FPM 的 worker 共用同一份快取；框架請求開與關 OPcache 的延遲差、memory_consumption 與 max_accelerated_files 的估算，以及部署新程式碼時 validate_timestamps 與 revalidate_freq 決定舊版還會執行多久">OPcache 與程序壽命&lt;/a> 展開。&lt;/li>
&lt;li>&lt;strong>持久連線&lt;/strong>：PDO 開了 &lt;code>PDO::ATTR_PERSISTENT&lt;/code> 的資料庫連線，請求結束時不關閉，留給同一個程序的下一個請求重用。&lt;/li>
&lt;li>&lt;strong>APCu 這類記憶體快取擴充&lt;/strong>：資料放在程序（或程序群共用）的記憶體裡，請求結束不清除。&lt;/li>
&lt;/ul>
&lt;p>所以 shared-nothing 的精確範圍是「PHP 程式碼建立的狀態」。程序層的資源由程序壽命決定，而程序壽命由下一節的 SAPI 決定。&lt;/p>
&lt;h2 id="sapiphp-直譯器與外部環境之間的介面">SAPI：PHP 直譯器與外部環境之間的介面&lt;/h2>
&lt;p>&lt;strong>SAPI&lt;/strong>（Server API）是 PHP 核心與外部環境之間的一層。PHP 的直譯器本身不知道請求從哪裡來：它可能被命令列啟動、被 web 伺服器 fork 出來、被嵌在 Apache 裡、或在一個常駐的伺服器裡被呼叫。SAPI 負責兩件事：把外部環境帶來的請求翻譯成 PHP 認得的形式（填好 &lt;code>$_SERVER&lt;/code>、&lt;code>$_GET&lt;/code>，把請求內容交給 &lt;code>php://input&lt;/code>），以及把 PHP 的輸出與回應標頭交回外部環境。同一份 PHP 核心配上不同的 SAPI，就是不同的執行方式（見 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/sapi/" data-link-title="SAPI（Server API）" data-link-desc="在 PHP_SAPI 或 phpinfo 看到 cli、fpm-fcgi、apache2handler、frankenphp 這類值，要知道它代表 PHP 以哪種方式被啟動、請求怎麼交進來時">SAPI&lt;/a>）。&lt;/p>
&lt;p>程式裡可以用 &lt;code>PHP_SAPI&lt;/code> 常數（或 &lt;code>php_sapi_name()&lt;/code>）讀到目前的 SAPI 名稱。上一節的輸出裡 &lt;code>sapi=fpm-fcgi&lt;/code> 就是這個值。實測過的幾種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;code>PHP_SAPI&lt;/code> 的值&lt;/th>
 &lt;th>啟動方式&lt;/th>
 &lt;th>誰對外講 HTTP&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>cli&lt;/code>&lt;/td>
 &lt;td>在命令列執行 &lt;code>php script.php&lt;/code>&lt;/td>
 &lt;td>沒有 HTTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cgi-fcgi&lt;/code>&lt;/td>
 &lt;td>&lt;code>php-cgi&lt;/code> 這支程式，被 web 伺服器以 CGI 啟動，或以 FastCGI 模式常駐&lt;/td>
 &lt;td>web 伺服器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>apache2handler&lt;/code>&lt;/td>
 &lt;td>mod_php：PHP 以 Apache 模組的形式載入 Apache 的程序裡&lt;/td>
 &lt;td>Apache&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>fpm-fcgi&lt;/code>&lt;/td>
 &lt;td>PHP-FPM：一個常駐的主程序管理一群 worker，用 FastCGI 接請求&lt;/td>
 &lt;td>前面的 web 伺服器（Nginx、Apache）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>frankenphp&lt;/code>&lt;/td>
 &lt;td>FrankenPHP：PHP 嵌在一個用 Go 寫的 web 伺服器裡&lt;/td>
 &lt;td>FrankenPHP 自己&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>cgi-fcgi&lt;/code> 這個名字反映了一件事：&lt;code>php-cgi&lt;/code> 同一支程式能以兩種協定運作。CGI 與 FastCGI 是兩個不同的協定，FastCGI 沿用 CGI 傳遞請求資訊的那套參數名稱，改掉的是程序壽命。被 web 伺服器以 CGI 執行時，&lt;code>php-cgi&lt;/code> 處理完一個請求就結束；以 FastCGI 模式啟動時，它常駐下來接收多個請求。兩者的差別在 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/cgi/" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/fastcgi/" data-link-title="FastCGI：讓 web 伺服器與常駐的 PHP 程序溝通的協定" data-link-desc="說明 FastCGI 協定怎麼在 CGI 的基礎上去掉每個請求重啟程序這一點：常駐的應用程式程序、web 伺服器與它之間的連線、一個請求在協定裡的樣子，以及 SCRIPT_FILENAME 由 web 伺服器決定會帶來的安全邊界（cgi.fix_pathinfo 與 try_files）">FastCGI&lt;/a> 兩篇分別說明。&lt;/p></description><content:encoded><![CDATA[<p>這篇說明 PHP 直譯器與 web 伺服器之間的那一層：一個 HTTP 請求進來之後，PHP 在什麼時候被啟動、哪些狀態在請求結束時被清掉、哪些資源留在程序裡。範圍不含 PHP 語法與框架；文中的行為取自 PHP 8.5，用官方 Docker 映像檔實測。</p>
<h2 id="一個請求在-php-裡經過的階段">一個請求在 PHP 裡經過的階段</h2>
<p>PHP 的執行分成兩層生命週期：<strong>程序層</strong>與<strong>請求層</strong>。程序層的工作每個程序只做一次，請求層的工作每個請求都做一次。</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>階段</th>
          <th>PHP 在這個階段做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>程序層</td>
          <td>模組啟動</td>
          <td>讀 <code>php.ini</code>、載入並初始化擴充（<code>pdo_pgsql</code>、<code>redis</code> 這類），每個擴充註冊自己的函式與類別</td>
      </tr>
      <tr>
          <td>請求層</td>
          <td>請求啟動</td>
          <td>依這個請求建立 <code>$_SERVER</code>、<code>$_GET</code>、<code>$_POST</code>、<code>$_COOKIE</code> 等超全域變數</td>
      </tr>
      <tr>
          <td>請求層</td>
          <td>執行腳本</td>
          <td>把 <code>.php</code> 檔編譯成 opcode（Zend 引擎執行的指令），再執行它</td>
      </tr>
      <tr>
          <td>請求層</td>
          <td>請求結束</td>
          <td>釋放這個請求建立的所有變數、物件與資源，送出剩下的輸出</td>
      </tr>
      <tr>
          <td>程序層</td>
          <td>模組結束</td>
          <td>程序要結束時，各擴充釋放自己的資源</td>
      </tr>
  </tbody>
</table>
<p>請求結束那一步的範圍很大：全域變數、函式內的 <code>static</code> 變數、類別的靜態屬性、開過的檔案與一般的資料庫連線，全部在這一步釋放。下一個請求就算由同一個程序處理，也看不到上一個請求留下的任何 PHP 變數。這個設計叫 <strong>shared-nothing</strong>：請求之間不共享應用程式的狀態，每個請求從空白開始（見 <a href="/blog/programming/php/knowledge-cards/shared-nothing-request/" data-link-title="Shared-nothing 請求模型" data-link-desc="要判斷 PHP 的全域變數、static 變數或類別靜態屬性會不會留到下一個請求，或評估換成常駐 worker（Octane、FrankenPHP worker）時哪些程式碼會出問題時">Shared-nothing 請求模型</a>）。</p>
<p>這件事可以直接量。下面這支腳本每次執行都把函式內的 <code>static</code> 變數與一個全域變數加一，並印出處理它的程序編號：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">function</span> <span class="nf">counter</span><span class="p">()</span> <span class="p">{</span> <span class="k">static</span> <span class="nv">$n</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">return</span> <span class="o">++</span><span class="nv">$n</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">&#39;g&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">&#39;g&#39;</span><span class="p">]</span> <span class="o">??</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">echo</span> <span class="s2">&#34;pid=&#34;</span><span class="p">,</span> <span class="nx">getmypid</span><span class="p">(),</span> <span class="s2">&#34; static=&#34;</span><span class="p">,</span> <span class="nx">counter</span><span class="p">(),</span> <span class="s2">&#34; global=&#34;</span><span class="p">,</span> <span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">&#39;g&#39;</span><span class="p">],</span> <span class="s2">&#34; sapi=&#34;</span><span class="p">,</span> <span class="nx">PHP_SAPI</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span></span></span></code></pre></div><p>在 Nginx 加 PHP-FPM 的環境連續請求四次，輸出是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">pid=7 static=1 global=1 sapi=fpm-fcgi
</span></span><span class="line"><span class="ln">2</span><span class="cl">pid=6 static=1 global=1 sapi=fpm-fcgi
</span></span><span class="line"><span class="ln">3</span><span class="cl">pid=7 static=1 global=1 sapi=fpm-fcgi
</span></span><span class="line"><span class="ln">4</span><span class="cl">pid=6 static=1 global=1 sapi=fpm-fcgi</span></span></code></pre></div><p>兩個程序（PID 6 與 7）輪流處理請求，同一個程序處理第二次時，兩個計數器仍然是 1。程序活了下來，PHP 變數沒有。</p>
<h3 id="請求結束後留在程序裡的資源">請求結束後留在程序裡的資源</h3>
<p>請求結束只清掉 PHP 程式碼建立的變數，程序本身持有的資源可以跨請求存在，而且只有程序活得夠久時才有意義：</p>
<ul>
<li><strong>OPcache</strong>：把編譯好的 opcode 存在共享記憶體裡，下一個請求不必重新編譯同一個檔案。它的效果完全取決於程序活多久，在 <a href="/blog/programming/php/01-server-runtime/opcache/" data-link-title="OPcache 與程序壽命：編譯結果的快取在各種執行模型下的效果" data-link-desc="說明 OPcache 快取的是什麼、它存在哪裡，以及為什麼它的效果取決於執行 PHP 的程序活多久：CGI 下命中次數永遠為零、PHP-FPM 的 worker 共用同一份快取；框架請求開與關 OPcache 的延遲差、memory_consumption 與 max_accelerated_files 的估算，以及部署新程式碼時 validate_timestamps 與 revalidate_freq 決定舊版還會執行多久">OPcache 與程序壽命</a> 展開。</li>
<li><strong>持久連線</strong>：PDO 開了 <code>PDO::ATTR_PERSISTENT</code> 的資料庫連線，請求結束時不關閉，留給同一個程序的下一個請求重用。</li>
<li><strong>APCu 這類記憶體快取擴充</strong>：資料放在程序（或程序群共用）的記憶體裡，請求結束不清除。</li>
</ul>
<p>所以 shared-nothing 的精確範圍是「PHP 程式碼建立的狀態」。程序層的資源由程序壽命決定，而程序壽命由下一節的 SAPI 決定。</p>
<h2 id="sapiphp-直譯器與外部環境之間的介面">SAPI：PHP 直譯器與外部環境之間的介面</h2>
<p><strong>SAPI</strong>（Server API）是 PHP 核心與外部環境之間的一層。PHP 的直譯器本身不知道請求從哪裡來：它可能被命令列啟動、被 web 伺服器 fork 出來、被嵌在 Apache 裡、或在一個常駐的伺服器裡被呼叫。SAPI 負責兩件事：把外部環境帶來的請求翻譯成 PHP 認得的形式（填好 <code>$_SERVER</code>、<code>$_GET</code>，把請求內容交給 <code>php://input</code>），以及把 PHP 的輸出與回應標頭交回外部環境。同一份 PHP 核心配上不同的 SAPI，就是不同的執行方式（見 <a href="/blog/programming/php/knowledge-cards/sapi/" data-link-title="SAPI（Server API）" data-link-desc="在 PHP_SAPI 或 phpinfo 看到 cli、fpm-fcgi、apache2handler、frankenphp 這類值，要知道它代表 PHP 以哪種方式被啟動、請求怎麼交進來時">SAPI</a>）。</p>
<p>程式裡可以用 <code>PHP_SAPI</code> 常數（或 <code>php_sapi_name()</code>）讀到目前的 SAPI 名稱。上一節的輸出裡 <code>sapi=fpm-fcgi</code> 就是這個值。實測過的幾種：</p>
<table>
  <thead>
      <tr>
          <th><code>PHP_SAPI</code> 的值</th>
          <th>啟動方式</th>
          <th>誰對外講 HTTP</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>cli</code></td>
          <td>在命令列執行 <code>php script.php</code></td>
          <td>沒有 HTTP</td>
      </tr>
      <tr>
          <td><code>cgi-fcgi</code></td>
          <td><code>php-cgi</code> 這支程式，被 web 伺服器以 CGI 啟動，或以 FastCGI 模式常駐</td>
          <td>web 伺服器</td>
      </tr>
      <tr>
          <td><code>apache2handler</code></td>
          <td>mod_php：PHP 以 Apache 模組的形式載入 Apache 的程序裡</td>
          <td>Apache</td>
      </tr>
      <tr>
          <td><code>fpm-fcgi</code></td>
          <td>PHP-FPM：一個常駐的主程序管理一群 worker，用 FastCGI 接請求</td>
          <td>前面的 web 伺服器（Nginx、Apache）</td>
      </tr>
      <tr>
          <td><code>frankenphp</code></td>
          <td>FrankenPHP：PHP 嵌在一個用 Go 寫的 web 伺服器裡</td>
          <td>FrankenPHP 自己</td>
      </tr>
  </tbody>
</table>
<p><code>cgi-fcgi</code> 這個名字反映了一件事：<code>php-cgi</code> 同一支程式能以兩種協定運作。CGI 與 FastCGI 是兩個不同的協定，FastCGI 沿用 CGI 傳遞請求資訊的那套參數名稱，改掉的是程序壽命。被 web 伺服器以 CGI 執行時，<code>php-cgi</code> 處理完一個請求就結束；以 FastCGI 模式啟動時，它常駐下來接收多個請求。兩者的差別在 <a href="/blog/programming/php/01-server-runtime/cgi/" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI</a> 與 <a href="/blog/programming/php/01-server-runtime/fastcgi/" data-link-title="FastCGI：讓 web 伺服器與常駐的 PHP 程序溝通的協定" data-link-desc="說明 FastCGI 協定怎麼在 CGI 的基礎上去掉每個請求重啟程序這一點：常駐的應用程式程序、web 伺服器與它之間的連線、一個請求在協定裡的樣子，以及 SCRIPT_FILENAME 由 web 伺服器決定會帶來的安全邊界（cgi.fix_pathinfo 與 try_files）">FastCGI</a> 兩篇分別說明。</p>
<h2 id="比較執行模型的三個問題">比較執行模型的三個問題</h2>
<p>SAPI 的種類很多，而它們之間的差異可以歸到三個問題上。每一種執行模型對這三個問題各有一組答案，後面的行為差異幾乎都從這組答案推得出來。</p>
<p><strong>直譯器住在哪裡。</strong> PHP 直譯器可能住在 web 伺服器自己的程序裡（mod_php），也可能住在另一個獨立的程序裡（CGI、PHP-FPM），或住在某個程序的執行緒裡（FrankenPHP）。住在 web 伺服器外面時，web 伺服器與 PHP 之間需要一種溝通方式，CGI 與 FastCGI 就是這種方式。這也決定了誰對外講 HTTP：直譯器住在 web 伺服器外面時，PHP 這一側只收得到 web 伺服器翻譯過的請求。</p>
<p><strong>執行它的程序活多久。</strong> 程序可能只活一個請求（CGI），也可能活過成千上萬個請求（mod_php、PHP-FPM、FrankenPHP）。程序活得越久，程序層的工作（讀設定、載入擴充）攤在越多請求上，OPcache 這類程序層的快取也才有編譯結果可以重用。</p>
<p><strong>應用程式的狀態活多久。</strong> 在 shared-nothing 的模型裡，狀態只活一個請求，框架每個請求都要重新啟動一次：載入設定、註冊服務、建立路由表。常駐 worker 模式（FrankenPHP 的 worker 模式、Laravel Octane）讓應用程式啟動一次後留在記憶體裡，狀態活過很多個請求，省下每次重新啟動框架的成本，代價是上一個請求留下的變數與物件會被下一個請求看到：放在靜態屬性裡的「目前登入的使用者」會被下一個使用者的請求讀到，每個請求往靜態陣列加資料的程式會讓記憶體隨請求數一路增長。</p>
<table>
  <thead>
      <tr>
          <th>執行模型</th>
          <th>直譯器住在</th>
          <th>程序壽命</th>
          <th>應用程式狀態壽命</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CGI</td>
          <td>每個請求新啟動的程序</td>
          <td>一個請求</td>
          <td>一個請求</td>
      </tr>
      <tr>
          <td>mod_php</td>
          <td>Apache 的子程序</td>
          <td>很多請求</td>
          <td>一個請求</td>
      </tr>
      <tr>
          <td>FastCGI（PHP-FPM）</td>
          <td>獨立的常駐 worker 程序</td>
          <td>很多請求</td>
          <td>一個請求</td>
      </tr>
      <tr>
          <td>FrankenPHP 傳統模式</td>
          <td>常駐程序裡的執行緒</td>
          <td>很多請求</td>
          <td>一個請求</td>
      </tr>
      <tr>
          <td>常駐 worker（FrankenPHP worker、Octane）</td>
          <td>常駐的 worker</td>
          <td>很多請求</td>
          <td>很多請求</td>
      </tr>
  </tbody>
</table>
<p>這張表裡最容易混在一起的是程序壽命與應用程式狀態壽命這兩欄，而它們是兩件獨立的事：FrankenPHP 的傳統模式只有一個程序、而且一直不結束，同一支計數腳本在它上面跑三次，三次的 PID 都是 1、計數器三次都是 1；換成 worker 模式，PID 仍然是 1，計數器變成 1、2、3、4。程序一樣常駐，差在 PHP 有沒有在請求之間清空狀態。</p>
<h2 id="三個問題各自展開的位置">三個問題各自展開的位置</h2>
<p>這個模型的三個問題，每換掉一個答案就走到另一種執行模型：</p>
<ul>
<li><strong>把直譯器放在每個請求新啟動的程序裡</strong>，就是 <a href="/blog/programming/php/01-server-runtime/cgi/" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI</a>：最簡單的隔離，每個請求付一次啟動 PHP 的成本。</li>
<li><strong>把直譯器放進 web 伺服器的程序裡</strong>，就是 <a href="/blog/programming/php/01-server-runtime/mod-php/" data-link-title="mod_php：PHP 直譯器嵌進 Apache 程序的執行模型" data-link-desc="說明 mod_php 怎麼把 PHP 直譯器載入 Apache 的每個子程序：為什麼它要搭配 prefork MPM、每個子程序都帶著 PHP 對記憶體與並發的影響、所有站台共用 Apache 的執行帳號、php.ini 與 .htaccess 的設定方式，以及從 mod_php 搬到 PHP-FPM 時 .htaccess 的 php_value 與 .user.ini 的差異">mod_php</a>：省掉啟動成本，代價是 web 伺服器的每個程序都帶著整個 PHP，包括只負責送圖片的那些。</li>
<li><strong>把直譯器放在 web 伺服器外面、讓程序常駐</strong>，需要一個讓兩邊溝通的協定，就是 <a href="/blog/programming/php/01-server-runtime/fastcgi/" data-link-title="FastCGI：讓 web 伺服器與常駐的 PHP 程序溝通的協定" data-link-desc="說明 FastCGI 協定怎麼在 CGI 的基礎上去掉每個請求重啟程序這一點：常駐的應用程式程序、web 伺服器與它之間的連線、一個請求在協定裡的樣子，以及 SCRIPT_FILENAME 由 web 伺服器決定會帶來的安全邊界（cgi.fix_pathinfo 與 try_files）">FastCGI</a>；管理那群常駐程序的實作是 <a href="/blog/programming/php/01-server-runtime/php-fpm/" data-link-title="PHP-FPM：常駐 PHP worker 的程序管理與主要的調校旋鈕" data-link-desc="說明 PHP-FPM 怎麼管理一群常駐的 PHP worker：pool 怎麼做到每個站台不同執行帳號、pm 的三種開程序策略、max_children 如何決定並發上限與請求排隊、max_requests 定期換掉 worker 控制記憶體、request_terminate_timeout 與 slowlog 怎麼處理跑太久的請求、status 頁怎麼讀，以及每一項對壓測數字的影響">PHP-FPM</a>。</li>
<li><strong>讓程序活得更久</strong>對效能的意義集中在 <a href="/blog/programming/php/01-server-runtime/opcache/" data-link-title="OPcache 與程序壽命：編譯結果的快取在各種執行模型下的效果" data-link-desc="說明 OPcache 快取的是什麼、它存在哪裡，以及為什麼它的效果取決於執行 PHP 的程序活多久：CGI 下命中次數永遠為零、PHP-FPM 的 worker 共用同一份快取；框架請求開與關 OPcache 的延遲差、memory_consumption 與 max_accelerated_files 的估算，以及部署新程式碼時 validate_timestamps 與 revalidate_freq 決定舊版還會執行多久">OPcache</a>：程序壽命從一個請求變成很多個請求時，被重用的是什麼。</li>
<li><strong>讓應用程式的狀態也活過很多請求</strong>，就是 <a href="/blog/programming/php/01-server-runtime/long-running-workers/" data-link-title="常駐 worker 模型：FrankenPHP worker 模式、Laravel Octane 與跨請求殘留的狀態" data-link-desc="說明讓 PHP 應用程式只啟動一次、在記憶體裡連續處理請求的常駐 worker 模型：它省下的是每個請求重新啟動框架的成本，失去的是 shared-nothing 的隔離；以 FrankenPHP 的 worker 模式與 Laravel Octane 實測靜態屬性跨請求累積、記憶體隨請求數線性增長與 max-requests 重啟，以及寫給這個模型的程式要避開的寫法">常駐 worker 模型</a>：框架只啟動一次，同時失去 shared-nothing 的隔離。</li>
</ul>
]]></content:encoded></item></channel></rss>