Read OSS

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_writefwrite()stdoutに書き出す
  • read_post → 何も返さない(CLIにはPOSTボディがない)
  • read_cookiesNULLを返す(CLIにはCookieがない)
  • register_server_variablesargvargcSCRIPT_FILENAME$_SERVERを埋める
  • log_messagestderrに書き出す

このコントラクトがあるからこそ、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.cphp_module_startup()です。ここでzend_startup()を呼び出し、メモリマネージャ、スキャナ、コンパイラ、エグゼキュータ、組み込み関数を初期化します。続いてphp.iniを解析し、コアのINI設定を登録し、拡張機能のリストを走査して各拡張のMINIT(Module Init)フックを呼び出します。クラス、定数、内部関数の登録はここで行われます。

フェーズ2:Request Startup は各リクエストの前に実行されます。main/main.cphp_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のモデルを驚くほど高速にするメモリアロケータを詳しく見ていきます。これらの構造体を理解することは、エンジンコードのどの部分を読む上でも欠かせない基礎です。