php-srcを読み解く:アーキテクチャ、レイヤー構造、そしてリクエストライフサイクル
前提知識
- ›Cの基礎知識(構造体、ポインタ、関数ポインタ)
- ›インタープリタ型言語の動作に関する一般的な理解
- ›プロセスライフサイクルの概念への理解
php-srcを読み解く:アーキテクチャ、レイヤー構造、そしてリクエストライフサイクル
サーバーサイド言語が判明しているWebサイトのうち、約77%がPHPで動いています。それにもかかわらず、ほとんどのPHP開発者は自分のコードを動かすエンジンの中身を覗いたことがありません。php-srcリポジトリは200万行を超えるCコードで構成されており、熟練したシステムプログラマーでさえ圧倒されてしまいます。この記事では、そのコードベースを読み進めるための「地図」を提供します。コードベースを4つのアーキテクチャレイヤーに分解し、PHPがApache、Nginx、CLIのいずれの環境でも同じ動作を実現するための仕組みを解説し、main()の最初の呼び出しから最終的なシャットダウンまで、PHPリクエストのライフサイクルを完全に追いかけます。
このシリーズを読み終えるころには、php-src内のどのファイルを開いても、それが全体のどこに位置するのかがわかるようになります。
トップレベルのディレクトリ構成
アーキテクチャの詳細へ入る前に、リポジトリのトップレベルレイアウトを把握しておきましょう。各ディレクトリには明確な役割があります。
| ディレクトリ | 役割 |
|---|---|
Zend/ |
Zend Engine — レキサ、パーサ、コンパイラ、VM、メモリアロケータ、GC、型システム |
main/ |
PHPランタイムの接着剤 — ライフサイクルの制御、INIシステム、ストリーム、SAPIブリッジ |
sapi/ |
Server APIのエントリポイント — CLI、FPM、CGI、Apacheモジュール、Embed、phpdbg |
ext/ |
72以上のバンドル拡張機能 — standard、json、opcache、pdo、curlなど |
TSRM/ |
Thread Safe Resource Manager — スレッドローカルストレージの抽象化 |
build/ |
ビルドシステムスクリプト(autoconf、libtoolヘルパー) |
win32/ |
Windows向けのビルド設定と互換レイヤー |
tests/ |
エンジンと拡張機能向けの.phptテストファイル |
Zend/Optimizer/ |
SSAベースのオプティマイザ(Zend/内に存在するが、独立したサブシステム) |
graph TD
subgraph "php-src repository"
SAPI["sapi/<br/>CLI, FPM, CGI, Apache, Embed"]
MAIN["main/<br/>Runtime, INI, Streams, SAPI bridge"]
EXT["ext/<br/>72+ bundled extensions"]
ZEND["Zend/<br/>Engine: lexer, parser, compiler, VM, GC"]
OPT["Zend/Optimizer/<br/>SSA optimizer"]
TSRM_DIR["TSRM/<br/>Thread safety"]
BUILD["build/, win32/<br/>Build system"]
TESTS["tests/<br/>.phpt test suite"]
end
SAPI --> MAIN
MAIN --> ZEND
EXT --> ZEND
OPT --> ZEND
ZEND --> TSRM_DIR
ヒント: 特定の機能を探すときは、PHPから呼び出せる関数なら
ext/、言語のセマンティクスならZend/、ランタイムの挙動ならmain/、ホスト環境との統合ならsapi/から調べ始めるのが効率的です。
4つのアーキテクチャレイヤー
php-srcは4つのレイヤーが積み重なった構造になっています。各レイヤーはその下位のレイヤーにのみ依存し、それぞれが明確な責務を持ちます。
flowchart TB
subgraph L4["Layer 4: SAPIs"]
CLI["CLI"]
FPM["FPM"]
CGI["CGI"]
APACHE["Apache"]
EMBED["Embed"]
end
subgraph L3["Layer 3: PHP Runtime (main/)"]
LIFECYCLE["Lifecycle Orchestration"]
INI["INI System"]
STREAMS["Streams I/O"]
SAPI_BRIDGE["SAPI Bridge"]
end
subgraph L2["Layer 2: Zend Engine"]
COMPILER["Compiler"]
VM["Virtual Machine"]
MM["Memory Manager"]
GC["Garbage Collector"]
TYPES["Type System"]
end
subgraph L1["Layer 1: TSRM"]
TLS["Thread-Local Storage"]
end
L4 --> L3
L3 --> L2
L2 --> L1
Layer 1 — TSRM は最下層に位置し、グローバルな状態へのスレッドセーフなアクセスを提供します。一般的な非ZTS(非スレッドセーフ)ビルドでは、TSRMはコンパイル時に除外されます。PG()、EG()、CG()、SG()といったグローバルアクセサマクロは、直接構造体フィールドへのアクセスに展開されます。ZTSビルド(イベント駆動型SAPIやWindows IISで使用)では、スレッドローカルストレージを経由したルックアップが行われます。
Layer 2 — Zend Engine は言語のコア部分です。レキサ、パーサ、AST、コンパイラ、仮想マシン、メモリアロケータ、ガベージコレクタ、そして基本型システム(zval、HashTable、zend_string、zend_object)が含まれます。エンジン自体はHTTP、ファイルI/O、設定ファイルについては何も知りません。PHPのオペコードをコンパイルして実行することだけが仕事です。
Layer 3 — PHPランタイム(main/) はエンジンと外部世界をつなぐ橋渡し役です。起動・シャットダウンのライフサイクルを制御し、INIファイルを解析し、ストリームI/Oの抽象化を管理し、エンジンをホスト環境から切り離すSAPIブリッジを提供します。
Layer 4 — SAPI はエントリポイントです。各SAPIは、特定のホスト環境において入力の読み取り、出力の書き込み、ヘッダーの送信、エラーのロギングをどう行うかをランタイムに伝えるコントラクト(関数ポインタのvtable)を実装します。
グローバル状態アクセサマクロは特に重要です。各レイヤーには専用のマクロでアクセスする独自のグローバル構造体があります。
| マクロ | 構造体 | レイヤー | 内容 |
|---|---|---|---|
PG() |
php_core_globals |
Runtime | INI設定、エラー処理、ファイルアップロード状態 |
EG() |
zend_executor_globals |
Engine | 現在のexecute_data、シンボルテーブル、例外状態 |
CG() |
zend_compiler_globals |
Engine | アクティブなop_array、AST、コンパイル状態 |
SG() |
sapi_globals_struct |
Runtime | リクエスト情報、ヘッダー、現在のSAPIモジュール |
これらのマクロはphp-src全体に登場します。一目で認識できるようになると、コードの読解がぐっと楽になります。
SAPIコントラクト
SAPI(Server API)コントラクトは、PHPの設計の中でも特に優れた部分のひとつです。これはsapi_module_structという単一の構造体で、PHPとホスト環境のやり取りをすべて抽象化する約30個の関数ポインタを持ちます。
定義はmain/SAPI.hで確認できます。主なコールバックは以下のとおりです。
classDiagram
class sapi_module_struct {
+char *name
+char *pretty_name
+startup(sapi_module_struct*) int
+shutdown(sapi_module_struct*) int
+activate() int
+deactivate() int
+ub_write(char*, size_t) size_t
+flush(void*) void
+header_handler(sapi_header_struct*, ...) int
+send_headers(sapi_headers_struct*) int
+send_header(sapi_header_struct*, void*) void
+read_post(char*, size_t) size_t
+read_cookies() char*
+register_server_variables(zval*) void
+log_message(char*, int) void
+get_fd(int*) int
+ini_defaults(HashTable*) void
}
CLI SAPIを例に具体的に見てみましょう。sapi/cli/php_cli.cのモジュール定義では、CLI固有の実装が次のように割り当てられています。
ub_write→fwrite()でstdoutに書き出すread_post→ 何も返さない(CLIにはPOSTボディがない)read_cookies→NULLを返す(CLIにはCookieがない)register_server_variables→argv、argc、SCRIPT_FILENAMEで$_SERVERを埋めるlog_message→stderrに書き出す
このコントラクトがあるからこそ、Zend Engineはwrite()やfwrite()を直接呼び出しません。常にsapi_module.ub_write()を経由するため、PHPがApacheモジュールとして動いていても、FastCGIワーカーとして動いていても、組み込みのスクリプトエンジンとして動いていても、適切な処理が実行されます。
SAPIエントリポイントの比較
各SAPIは独自のmain()関数を持ちますが、最終的にはすべて同じライフサイクル関数の呼び出しに収束します。主要なSAPIの違いは次のとおりです。
| SAPI | エントリファイル | プロセスモデル | リクエストループ |
|---|---|---|---|
| CLI | sapi/cli/php_cli.c |
シングルプロセス、シングルリクエスト | スクリプトを実行して終了 |
| FPM | sapi/fpm/fpm/fpm_main.c |
マスター + ワーカープール | 各ワーカーでaccept()ループ |
| CGI | sapi/cgi/cgi_main.c |
Webサーバーがリクエストごとに起動 | シングルリクエスト処理後に終了 |
| Apache | sapi/apache2handler/sapi_apache2.c |
.soモジュールとしてロード |
Apacheのリクエストハンドラから呼ばれる |
| Embed | sapi/embed/php_embed.c |
ホストアプリケーションに組み込み | ホストがライフサイクルを制御 |
CLI SAPIは最もシンプルです。main()でコマンドライン引数を解析し、php_module_startup()を呼び出し、シングルリクエストを実行して、シャットダウンします。FPMは最も複雑で、ワーカープロセスをforkし、設定可能なサイズのプールを管理し、各ワーカーがaccept() → php_request_startup() → 実行 → php_request_shutdown()のループを繰り返します。
違いはありますが、どのSAPIも最終的にはmain/main.cにある同じ4つのライフサイクル関数を呼び出す点で一致しています。これが全SAPIの収束点です。
リクエストライフサイクル
ライフサイクルはPHPの実行モデルの根幹です。CLI、FPM、Apacheのいずれのプロセスも、同じ4フェーズのパターンに従います。
sequenceDiagram
participant SAPI as SAPI main()
participant Runtime as main/main.c
participant Zend as Zend Engine
participant Ext as Extensions
Note over SAPI,Ext: Phase 1: Module Startup (once per process)
SAPI->>Runtime: php_module_startup()
Runtime->>Zend: zend_startup()
Zend->>Zend: Init memory manager, scanner, compiler, VM
Runtime->>Runtime: Parse php.ini
Runtime->>Ext: Call each extension's MINIT()
Note over SAPI,Ext: Phase 2: Request Startup (once per request)
SAPI->>Runtime: php_request_startup()
Runtime->>Zend: zend_activate()
Zend->>Zend: Reset memory arena, init symbol tables
Runtime->>Ext: Call each extension's RINIT()
Note over SAPI,Ext: Phase 3: Execution
SAPI->>Zend: zend_execute_scripts()
Zend->>Zend: Compile source → opcodes
Zend->>Zend: Execute opcodes in VM
Note over SAPI,Ext: Phase 4: Request Shutdown
SAPI->>Runtime: php_request_shutdown()
Runtime->>Ext: Call each extension's RSHUTDOWN()
Runtime->>Zend: zend_deactivate()
Zend->>Zend: Free request memory, destroy symbol tables
Note over SAPI,Ext: Phase 5: Module Shutdown (once per process)
SAPI->>Runtime: php_module_shutdown()
Runtime->>Ext: Call each extension's MSHUTDOWN()
Runtime->>Zend: zend_shutdown()
フェーズ1:Module Startup はプロセスの起動時(またはApacheモジュールのロード時)に一度だけ実行されます。中心となる関数はmain/main.cのphp_module_startup()です。ここでzend_startup()を呼び出し、メモリマネージャ、スキャナ、コンパイラ、エグゼキュータ、組み込み関数を初期化します。続いてphp.iniを解析し、コアのINI設定を登録し、拡張機能のリストを走査して各拡張のMINIT(Module Init)フックを呼び出します。クラス、定数、内部関数の登録はここで行われます。
フェーズ2:Request Startup は各リクエストの前に実行されます。main/main.cのphp_request_startup()がzend_activate()を呼び出し、リクエストごとのメモリアリーナをリセットし、シンボルテーブルを再初期化し、エグゼキュータの状態をクリアします。その後、各拡張のRINIT(Request Init)フックが呼ばれます。たとえばsession拡張はここでセッションストアを開き、opcacheはここでオプティマイザの準備を行います。
フェーズ3:Execution で実際にPHPコードが動きます。SAPIがzend_execute_scripts()を呼び出すと、ソースファイルがop_arrayにコンパイルされ(またはOPcacheからキャッシュ済みのものが取得され)、VMに渡されて実行されます。
フェーズ4:Request Shutdown は起動の逆順で処理されます。php_request_shutdown()が各拡張のRSHUTDOWNを呼び出し、続いてzend_deactivate()がリクエストごとのデータをすべて破棄します。FPMとApacheでは、プロセスはフェーズ2に戻り、次のリクエストを待ちます。
フェーズ5:Module Shutdown はプロセスの終了時に実行されます。拡張機能のMSHUTDOWNが呼ばれ、最後にzend_shutdown()がエンジンを解体します。
ヒント: Module Startup(一度だけ)とRequest Startup(リクエストごと)が明確に分離されているのは、PHPの「シェアードナッシング」アーキテクチャが機能する理由でもあります。各リクエストはクリーンな状態から始まるため、前のリクエストの状態が漏れることはありません。また、OPcacheがキャッシュしていない限り、コードの変更を反映するためにPHPを「再起動」する必要がないのもこの設計のおかげです。
設定:INIシステム
PHPの設定システムはライフサイクルと密接に結びついています。INIファイルはModule Startupの際に解析され、変更モード(change-mode)システムによって、各設定をどのライフサイクルフェーズで変更できるかが制御されます。
flowchart TD
A["Process Start"] --> B["Scan for php.ini"]
B --> C["Parse php.ini directives"]
C --> D["Apply PHP_INI_SYSTEM settings"]
D --> E["Extensions register INI entries in MINIT"]
E --> F["Per-request: scan .user.ini"]
F --> G["Apply PHP_INI_PERDIR settings"]
G --> H["Runtime: ini_set() calls"]
H --> I["Apply PHP_INI_USER / PHP_INI_ALL settings"]
すべてのINIディレクティブには、いつ変更できるかを決める変更モードがあります。
| モード | 定数 | 設定できる場所 |
|---|---|---|
PHP_INI_SYSTEM |
4 | php.iniのみ — プロセスの再起動が必要 |
PHP_INI_PERDIR |
6 | php.ini、.user.ini、またはhttpd.conf |
PHP_INI_USER |
7 | 上記のすべて + 実行時のini_set() |
PHP_INI_ALL |
7 | USERと同じ — どこでも設定可能 |
INIエントリはZend/zend_ini.hで定義され、各拡張機能がMINITの中でSTD_PHP_INI_ENTRYなどのマクロを使って登録します。実際の解析はphp_module_startup()内のphp_init_config()がINIファイルを検索・解析する形で行われます。
.user.ini機能(user_ini.filenameで制御)により、非CLI SAPIではディレクトリ単位の設定上書きが可能です。これはRequest Startupの際に設定可能なキャッシュTTL(user_ini.cache_ttl)付きでスキャンされるため、リクエストごとにファイルシステムへのアクセスが発生するオーバーヘッドはありません。
次回の内容
これで全体の地図が手に入りました。4つのレイヤー、SAPIコントラクト、すべてのPHPリクエストを支配するライフサイクルを把握できました。次回はZend Engineのコアデータ構造に踏み込みます。すべてのPHP値を表現する16バイトのzval、PHPの配列を支えるデュアルモードのHashTable、「すべてをアロケートして一気に解放する」PHPのモデルを驚くほど高速にするメモリアロケータを詳しく見ていきます。これらの構造体を理解することは、エンジンコードのどの部分を読む上でも欠かせない基礎です。