Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

php-fpm リクエストサイクル

php-fpm がリクエストを処理しているサイクルをざっくりとまとめました。

php-fpm ワーカープロセスの生成

php-fpm は FastCGI リクエストを処理する SAPI 実装の一つです。いわば、PHP コードを実行する FastCGI サーバです。prefork 型となっており、nginx 等からの FastCGI リクエスト処理はあらかじめ php-fpm コマンド(親プロセス)によって生成されたワーカープロセスが担います。

ワーカープロセスは fpm_children_make() の fork() で生成されるので、ここから処理を開始します。

https://github.com/php/php-src/blob/PHP-8.3.8/sapi/fpm/fpm/fpm_children.c#L410-L492

 while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max)) {

        warned = 0;
        child = fpm_resources_prepare(wp);

                 // (snip)

        pid = fork(); // ワーカープロセス生成

        switch (pid) {

            case 0 : // ワーカープロセス(子プロセス)処理
                fpm_child_resources_use(child);
                fpm_globals.is_child = 1;
                fpm_child_init(wp);
                return 0;

            case -1 : // ERROR
                zlog(ZLOG_DEBUG, "unblocking signals");
                fpm_signals_unblock();
                zlog(ZLOG_SYSERROR, "fork() failed");

                fpm_resources_discard(child);
                return 2;

            default : // php-fpm プロセス(親プロセス)処理
                zlog(ZLOG_DEBUG, "unblocking signals, child born");
                fpm_signals_unblock();
                child->pid = pid;
                fpm_clock_get(&child->started);
                fpm_parent_resources_use(child);

                zlog(is_debug ? ZLOG_DEBUG : ZLOG_NOTICE, "[pool %s] child %d started", wp->config->name, (int) pid);
        }

    }

リクエストループ

ワーカープロセスはおおよそ下記のような流れでリクエストを処理します。

ワーカープロセスは fpm_main.c の main() 内の下記 while() ループの中でリクエスト処理を行います。ワーカープロセスが生きている間はこのループ内で処理を続けます。

     while (EXPECTED(fcgi_accept_request(request) >= 0)) { // 1) リクエスト接続待ち
      
            // 2) リクエスト開始処理

            if (UNEXPECTED(php_fopen_primary_script(&file_handle) == FAILURE)) {
                // PHPファイルがオープンできない場合のエラー処理
            } else {
                fpm_request_executing();

                /* Reset exit status from the previous execution */
                EG(exit_status) = 0;

                php_execute_script(&file_handle); // 3) PHPファイル実行
            }

            // 4) リクエスト終了処理

            /* end of fastcgi loop */
        }

1) リクエスト接続待ち

ワーカープロセスは fcgi_accept_request() 内の accept() にて FastCGI リクエストを待ちます。

req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
[0] from 0x0000aaaad55deb28 in fcgi_accept_request+88 at /php-src/main/fastcgi.c:1406
[1] from 0x0000aaaad55eef88 in main+3604 at /php-src/sapi/fpm/fpm/fpm_main.c:1862

FastCGI リクエストの接続が確立すると処理が再開されます。accept() の戻り値はファイルディスクリプタであり、接続した FastCGI クライアント(nginx等)との通信に利用します。

再開後は FastCGI リクエストの読み込みなどを行って、main() のメインループに制御が戻ります。

listen_socket の生成

listen_socket は親プロセスで生成した値を dup() したものです。listen_socket の生成は、親プロセスにて fpm_sockets_init_main() の下記箇所でネットワーク(TCP/IP)もしくは UNIX ドメインソケットを生成します。

https://github.com/php/php-src/blob/PHP-8.3.8/sapi/fpm/fpm/fpm_sockets.c#L498-L509

     switch (wp->listen_address_domain) {
            case FPM_AF_INET :
                wp->listening_socket = fpm_socket_af_inet_listening_socket(wp);
                break;

            case FPM_AF_UNIX :
                if (0 > fpm_unix_resolve_socket_permissions(wp)) {
                    return -1;
                }
                wp->listening_socket = fpm_socket_af_unix_listening_socket(wp);
                break;
        }
[0] from 0x0000aaaaab3551ec in fpm_sockets_init_main+460 at /php-src/sapi/fpm/fpm/fpm_sockets.c:500
[1] from 0x0000aaaaab33fb44 in fpm_init+308 at /php-src/sapi/fpm/fpm/fpm.c:65
[2] from 0x0000aaaaab34e86c in main+1784 at /php-src/sapi/fpm/fpm/fpm_main.c:1819

FastCGI リクエスト

FastCGI リクエストで送信されるパラメータは、 この時点では fcgi_request* の値である request->env->hash_table に格納されます。ハッシュテーブルの要素にアクセスするとその内容を確認できます。

>>> p request->env->hash_table
$29 = {[0] = 0x0, [1] = 0xaaaaac0a1ea0, [2] = 0x0, [3] = 0x0, [4] = 0xaaaaac0a1ed0, [5] = 0x0, [6] = 0x0, [7] = 0x0, [8] = 0x0, [9] = 0x0, [10] = 0x0, [11] = 0x0,
  [12] = 0xaaaaac0a1f30, [13] = 0x0, [14] = 0x0, [15] = 0x0, [16] = 0x0, [17] = 0x0, [18] = 0x0, [19] = 0x0, [20] = 0x0, [21] = 0x0, [22] = 0x0, [23] = 0xaaaaac0a1f90,
  [24] = 0x0 <repeats 36 times>, [60] = 0xaaaaac0a1f00, [61] = 0x0 <repeats 24 times>, [85] = 0xaaaaac0a1f60, [86] = 0x0 <repeats 12 times>, [98] = 0xaaaaac0a1fc0,
  [99] = 0x0 <repeats 25 times>, [124] = 0xaaaaac0a1ff0, [125] = 0x0, [126] = 0x0, [127] = 0x0}

>>> p &request->env
$44 = (fcgi_hash *) 0xaaaaac0a1a68
>>> p &request->env->hash_table
$45 = (fcgi_hash_bucket *(*)[128]) 0xaaaaac0a1a68

>>> p *request->env->hash_table[98]
$38 = {
  hash_value = 1890,
  var_len = 14,
  var = 0xaaaaac0a380e "REQUEST_METHOD",
  val_len = 3,
  val = 0xaaaaac0a381d "GET",
  next = 0x0,
  list_next = 0xaaaaac0a1f90
}

>>> p *request->env->hash_table[23]
$41 = {
  hash_value = 1815,
  var_len = 15,
  var = 0xaaaaac0a37ef "SCRIPT_FILENAME",
  val_len = 14,
  val = 0xaaaaac0a37ff "/app/index.php",
  next = 0x0,
  list_next = 0xaaaaac0a1f60
}

2) リクエスト開始処理

リスクエスト処理を開始するにあたっての前処理を行います。これには、実行するPHPファイルパスの決定やリクエスト内容をPHPコードが参照できるようにスーパーグローバルに格納するなどがあります。

実行PHPファイルパスの決定

リクエストパラメータに基づいて実行する PHP ファイルパスを決定します。この処理は init_request_info() で実行されます。

https://github.com/php/php-src/blob/PHP-8.3.8/sapi/fpm/fpm/fpm_main.c#L967-L1372

[0] from 0x0000aaaaab34c470 in init_request_info+12 at /php-src/sapi/fpm/fpm/fpm_main.c:969
[1] from 0x0000aaaaab34ea74 in main+2304 at /php-src/sapi/fpm/fpm/fpm_main.c:1867

SCRIPT_FILENAME パラメータの値(値が存在しない場合は PATH_TRANSLATEDの値)をベースに各種チェックを行って PHP ファイルパスを決定します。パスが決定したら、 SG(request_info).path_translated(実体は core_globals.request_info.path_translated)に格納します。

init_request_info() では他にもリクエストパラメータを SG(request_info) に格納する処理が行われるので、ブラウザで http://localhost:8000/?name=hoge にアクセスした場合、下記のような値となっていました。

>>> p sapi_globals.request_info
$13 = {
  request_method = 0xaaaaac0a3721 "GET",
  query_string = 0xaaaaac0a3708 "name=hoge",
  cookie_data = 0xaaaaac0a3b59 "_ga=xxxx; sess=xxxx",
  content_length = 0,
  path_translated = 0xfffff4e02000 "/app/index.php", // 実行 PHP ファイルパス
  request_uri = 0xaaaaac0a374f "/index.php",
  request_body = 0x0,
  content_type = 0xaaaaac0a3732 "",
  headers_only = false,
  no_headers = false,
  headers_read = false,
  post_entry = 0x0,
  content_type_dup = 0x0,
  auth_user = 0x0,
  auth_password = 0x0,
  auth_digest = 0x0,
  argv0 = 0x0,
  current_user = 0x0,
  current_user_length = 0,
  argc = 0,
  argv = 0x0,
  proto_num = 1000
}

スーパーグローバルへの格納

リクエストパラメータが格納された request ポインタはメインループの下記で SG(server_context)(実体は sapi_globals.server_context)に格納されます。

     while (EXPECTED(fcgi_accept_request(request) >= 0)) {
            char *primary_script = NULL;
            request_body_fd = -1;
            SG(server_context) = (void *) request; // ここ
            CGIG(fcgi_logging_request_started) = false;

メインループから実行される init_request_info() で SG(server_context) のリクエスト情報から必要なものは、PG(request_info)(実体は core_globals.request_info)に格納されます。

https://github.com/php/php-src/blob/PHP-8.3.8/sapi/fpm/fpm/fpm_main.c#L967-L1372

[0] from 0x0000aaaaab34c470 in init_request_info+12 at /php-src/sapi/fpm/fpm/fpm_main.c:969
[1] from 0x0000aaaaab34ea74 in main+2304 at /php-src/sapi/fpm/fpm/fpm_main.c:1867

スーパーグローバルへの値のセットは、zend_activate_auto_globals() で行われます。それぞれのスーパーグローバルには値を格納するためのコールバック関数があり、これを利用します。auto_globals_jit 設定が有効な場合、$_ENV$_REQUEST$_SERVERはコンパイル時の利用チェックによって値が格納されます。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_compile.c#L1936-L1949

ZEND_API void zend_activate_auto_globals(void) /* {{{ */
{
    zend_auto_global *auto_global;

    ZEND_HASH_MAP_FOREACH_PTR(CG(auto_globals), auto_global) {
        if (auto_global->jit) {
            auto_global->armed = 1;
        } else if (auto_global->auto_global_callback) {
            auto_global->armed = auto_global->auto_global_callback(auto_global->name);
        } else {
            auto_global->armed = 0;
        }
    } ZEND_HASH_FOREACH_END();
}
[0] from 0x0000aaaaac7621a4 in zend_activate_auto_globals+8 at /php-src/Zend/zend_compile.c:1940
[1] from 0x0000aaaaac6f6848 in php_hash_environment+36 at /php-src/main/php_variables.c:792
[2] from 0x0000aaaaac6e1154 in php_request_startup+600 at /php-src/main/main.c:1818
[3] from 0x0000aaaaac94ea7c in main+2312 at /php-src/sapi/fpm/fpm/fpm_main.c:1873

$_GET の場合は、php_auto_globals_create_get() がコールバック関数です。

php_auto_globals_create_get() では、まず sapi_module.treat_data(PARSE_GET, NULL, NULL) を実行します。この中で先ほど値をセットした PG(request_info).query_string をパースして、PG(http_globals[TRACK_VARS_GET] に値セットします。

https://github.com/php/php-src/blob/PHP-8.3.8/main/php_variables.c#L461-L588

そして、PG(http_globals[TRACK_VARS_GET])(実体は core_globals.http_globals[1])の値を EG(symbol_table)(実体は executor_globals.symbol_table)の _GET キーに格納します。これにより、PHPコード実行時に $_GET 変数の値を参照できます。

https://github.com/php/php-src/blob/PHP-8.3.8/main/php_variables.c#L800-L813

static bool php_auto_globals_create_get(zend_string *name)
{
    if (PG(variables_order) && (strchr(PG(variables_order),'G') || strchr(PG(variables_order),'g'))) {
        sapi_module.treat_data(PARSE_GET, NULL, NULL); // ここで 
    } else {
        zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_GET]);
        array_init(&PG(http_globals)[TRACK_VARS_GET]);
    }

    zend_hash_update(&EG(symbol_table), name, &PG(http_globals)[TRACK_VARS_GET]); // $_GET に値をセット
    Z_ADDREF(PG(http_globals)[TRACK_VARS_GET]);

    return 0; /* don't rearm */
}

この行を実行した後に executor_globals.symbol_table の内容を確認すると、クエリストリングの内容が連想配列として確認できす。

>>> print_ht &executor_globals.symbol_table
Hash(1)[0xaaaaabeb33c8]: {
  [0] "_GET" => [0xfffff4e60200] (refcount=1) array:
}

>>> printzv (zval*)0xfffff4e60200
[0xfffff4e60200] (refcount=1) array:     Hash(1)[0xfffff4e5c000]: {
      [0] "name" => [0xfffff4e5d1c0] (refcount=1) string: hoge
}

$_POST の場合、php_auto_globals_create_post() で POST で送信されたボディの内容が連想配列として格納されます。

https://github.com/php/php-src/blob/PHP-8.3.8/main/php_variables.c#L815-L832

static bool php_auto_globals_create_post(zend_string *name)
{
    if (PG(variables_order) &&
            (strchr(PG(variables_order),'P') || strchr(PG(variables_order),'p')) &&
        !SG(headers_sent) &&
        SG(request_info).request_method &&
        !strcasecmp(SG(request_info).request_method, "POST")) {
        sapi_module.treat_data(PARSE_POST, NULL, NULL);
    } else {
        zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_POST]);
        array_init(&PG(http_globals)[TRACK_VARS_POST]);
    }

    zend_hash_update(&EG(symbol_table), name, &PG(http_globals)[TRACK_VARS_POST]);
    Z_ADDREF(PG(http_globals)[TRACK_VARS_POST]);

    return 0; /* don't rearm */
}

処理後は下記のように値を確認できます。

>>> printzv &core_globals.http_globals[0]

[0xaaaaabeb27e8] (refcount=1) array:     Hash(2)[0xfffff4e5e0c0]: {
      [0] "username" => [0xfffff4e6d1c0] (refcount=1) string: login
      [1] "password" => [0xfffff4e6d1e0] (refcount=1) string: pass
}

>>> print_ht &executor_globals.symbol_table

Hash(2)[0xaaaaabeb33c8]: {
  [0] "_GET" => [0xfffff4e5f200] (refcount=2) array:
  [1] "_POST" => [0xfffff4e5f220] (refcount=1) array:
}
>>> printzv (zval*)0xfffff4e5f220

[0xfffff4e5f220] (refcount=1) array:     Hash(2)[0xfffff4e5e0c0]: {
      [0] "username" => [0xfffff4e6d1c0] (refcount=1) string: login
      [1] "password" => [0xfffff4e6d1e0] (refcount=1) string: pass
}

なお、スーパーグローバルは下記にて CG(auto_globals)(実体は compiler_globals.auto_globals)登録されています。ここでコールバック関数も登録されます。php-fpm の場合は親プロセスで実行されており、ワーカープロセス実行時には登録済みとなっています。

https://github.com/php/php-src/blob/PHP-8.3.8/main/php_variables.c#L974-L983

void php_startup_auto_globals(void)
{
    zend_register_auto_global(zend_string_init_interned("_GET", sizeof("_GET")-1, 1), 0, php_auto_globals_create_get);
    zend_register_auto_global(zend_string_init_interned("_POST", sizeof("_POST")-1, 1), 0, php_auto_globals_create_post);
    zend_register_auto_global(zend_string_init_interned("_COOKIE", sizeof("_COOKIE")-1, 1), 0, php_auto_globals_create_cookie);
    zend_register_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER), PG(auto_globals_jit), php_auto_globals_create_server);
    zend_register_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_ENV), PG(auto_globals_jit), php_auto_globals_create_env);
    zend_register_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_REQUEST), PG(auto_globals_jit), php_auto_globals_create_request);
    zend_register_auto_global(zend_string_init_interned("_FILES", sizeof("_FILES")-1, 1), 0, php_auto_globals_create_files);
}
[0] from 0x0000aaaaab0f7274 in php_startup_auto_globals+8 at /php-src/main/php_variables.c:976
[1] from 0x0000aaaaab0e1a10 in php_module_startup+780 at /php-src/main/main.c:2207
[2] from 0x0000aaaaab34c2e0 in php_cgi_startup+28 at /php-src/sapi/fpm/fpm/fpm_main.c:789
[3] from 0x0000aaaaab34e6e8 in main+1396 at /php-src/sapi/fpm/fpm/fpm_main.c:1763

3) PHPファイル実行

ここまでに決定した PHP ファイルを実行します。PHP ファイルを実行するには php_execute_script() を呼び出します。引数に先ほど決定した PHP ファイルハンドルを与えます。

下記が php_execute_script() です。実際の PHP コード実行は zend_execute_scripts() に処理が委譲されます。zend_execute_scripts() は名前のとおり、複数の PHP ファイルを順次実行することができます。ここでは primary_file(実行PHPファイル)の前後に、prepend_file_p と append_file_p が与えられています。ご存じの方はピンとくると思いますが、prepend_fileappend_file 設定で指定された PHP ファイルはこのようにメインの PHP ファイルの前後に実行されます。

https://github.com/php/php-src/blob/PHP-8.3.8/main/main.c#L2446-L2544

PHPAPI bool php_execute_script(zend_file_handle *primary_file)
{
    // (snip)

    zend_try {
                  // (snip)

        retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS); // PHP コード実行
    } zend_end_try();

    // (ship)
    return retval;
}
[0] from 0x0000aaaaab0e1f4c in php_execute_script+20 at /php-src/main/main.c:2448
[1] from 0x0000aaaaab34ed7c in main+3080 at /php-src/sapi/fpm/fpm/fpm_main.c:1934

zend_execute_scripts() の内容も少し見てみましょう。ここでは可変長引数の files に実行する PHP ファイルハンドルが与えられます。各 PHP ファイルハンドルについてコンパイル、実行を行います。

PHP ファイルのコンパイルは zend_compile_file() で行います。コンパイルで生成されたオペコードは zend_execute() で実行します。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend.c#L1867-L1911

ZEND_API zend_result zend_execute_scripts(int type, zval *retval, int file_count, ...) /* {{{ */
{
            va_list files;
    // (snip)

    va_start(files, file_count);
    for (i = 0; i < file_count; i++) { // 今回の例では、prepend_file_p, primary_file, append_file_p の 3 回ループを回る
        file_handle = va_arg(files, zend_file_handle *);
        if (!file_handle) {
            continue;
        }

        if (ret == FAILURE) {
            continue;
        }

        op_array = zend_compile_file(file_handle, type); // PHP ファイルをコンパイル
        if (file_handle->opened_path) {
            zend_hash_add_empty_element(&EG(included_files), file_handle->opened_path);
        }
        if (op_array) {
            zend_execute(op_array, retval); // オペコード実行
                          // (snip)
        } else if (type==ZEND_REQUIRE) {
            ret = FAILURE;
        }
    }
    va_end(files);

    return ret;
}

なお、zend_compile_file は関数ポインタとなっており、状況に応じて呼ばれる関数は異なります。

extern ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type);

筆者の環境では Phar 拡張が有効なため、phar_compile_file() を経由して、compile_file() が呼ばれています。この compile_file() がコンパイル処理の実体です。Phar 拡張の他に OPcache も zend_compile_file に独自の関数を格納していますが、コンパイル処理自体は compile_file() を呼んでいます。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_language_scanner.l#L638-L658

[0] from 0x0000aaaaab120004 in compile_file+16 at Zend/zend_language_scanner.l:641
[1] from 0x0000aaaaaaea7b48 in phar_compile_file+820 at /php-src/ext/phar/phar.c:3352
[2] from 0x0000aaaaab19df68 in zend_execute_scripts+260 at /php-src/Zend/zend.c:1886
[3] from 0x0000aaaaab0e21b8 in php_execute_script+640 at /php-src/main/main.c:2515
[4] from 0x0000aaaaab34ed7c in main+3080 at /php-src/sapi/fpm/fpm/fpm_main.c:1934

4) リクエスト終了処理

PHP コードの実行が完了すると、リクエスト終了処理を行います。php-fpm 固有の処理もありますが、多くは PHP コード実行の後始末(リソース解放など)がメインです。

リクエスト終了処理が完了すると、リクエスト接続待ちに戻ります。

PHP コード実行リソースの解放

PHP コード実行(コンパイル含む)で確保したリソースの解放は php_request_shutdown() で行います。

https://github.com/php/php-src/blob/PHP-8.3.8/main/main.c#L1832-L1948

[0] from 0x0000aaaaab0e11c0 in php_request_shutdown+16 at /php-src/main/main.c:1836
[1] from 0x0000aaaaab34ef18 in main+3492 at /php-src/sapi/fpm/fpm/fpm_main.c:1970

php_request_shutdown() を実行すると、オブジェクトやグローバル変数の解放の他にユーザ定義の関数やクラス定義も破棄されます。

例えば、関数 foo() というユーザ定義関数があるとします。関数定義は executor_globals.function_table に格納されているので、PHP コード実行終了時はここに foo が存在します。

>>> print_ft executor_globals.function_table
(snip)
  [1025] "getallheaders" => "getallheaders"
  [1026] "fpm_get_status" => "fpm_get_status"
  [1027] "foo" => "foo"
}

php_request_shutdown() を実行すると、foo は消えていることが分かります。また、PHP内部関数エントリについては次回以降のリクエストで再利用されることも見て取れます。(実際、2 回目のリクエスト以降はリクエスト開始処理前でも関数エントリに PHP 内部関数エントリは存在します)

>>> print_ft executor_globals.function_table
(snip)
  [1025] "getallheaders" => "getallheaders"
  [1026] "fpm_get_status" => "fpm_get_status"
}

この関数エントリの削除処理は shutdown_executor() で行われます。function_table への関数エントリ格納は、PHP内部関数 -> ユーザ定義関数の順に行われるので、function_table を逆順に読んで、リクエスト間で共有するインデックス(PHP内部関数の最大インデックス)まで削除を行うような実装になっています。

shutdown_executor() では他にもクラス定義テーブル(EG(class_table))やオブジェクトストア(EG(objects_store))、読み込んだ PHP ファイルパス情報(EG(included_files))など、多くの実行時情報をクリアしています。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_execute_API.c#L403-L494

         ZEND_HASH_MAP_REVERSE_FOREACH_STR_KEY_VAL(EG(function_table), key, zv) {
                zend_function *func = Z_PTR_P(zv);
                if (_idx == EG(persistent_functions_count)) {
                    break;
                }
                destroy_op_array(&func->op_array);
                zend_string_release_ex(key, 0);
            } ZEND_HASH_MAP_FOREACH_END_DEL();
[0] from 0x0000aaaaab180218 in shutdown_executor+12 at /php-src/Zend/zend_execute_API.c:408
[1] from 0x0000aaaaab19c310 in zend_deactivate+96 at /php-src/Zend/zend.c:1292
[2] from 0x0000aaaaab0e13f4 in php_request_shutdown+580 at /php-src/main/main.c:1897
[3] from 0x0000aaaaab34ef18 in main+3492 at /php-src/sapi/fpm/fpm/fpm_main.c:1970

max_requests チェック

php-fpm には pm. max_requests という設定項目があり、これを 1 以上にすることで指定した回数のリクエストを処理すると該当のワーカープロセスが終了します。メモリリークを回避する方法とお馴染みのものです。

この処理はメインループの最後に実装されています。

         requests++;
            if (UNEXPECTED(max_requests && (requests == max_requests))) {
                fcgi_request_set_keep(request, 0);
                fcgi_finish_request(request, 0);
                break;
            }

PHP: 設定 - Manual

FastCGI クライアントとの通信

FastCGI クライアントとの通信は以下の箇所で行われます。

接続開始

接続は 1) で見たように accept() で行います。

リクエスト読み取り

リクエストの読み取りは safe_read() で行います。read() を呼んでおり、第一引数に req->fd を指定していることが分かります。

https://github.com/php/php-src/blob/PHP-8.3.8/main/fastcgi.c#L954-L991

ret = read(req->fd, ((char*)buf)+n, count-n);

下記は、accept() で接続を確立した後に fcgi_read_request() 内で実行しています。

[0] from 0x0000aaaaab33d594 in safe_read+20 at /php-src/main/fastcgi.c:957
[1] from 0x0000aaaaab33dae0 in fcgi_read_request+140 at /php-src/main/fastcgi.c:1061
[2] from 0x0000aaaaab33ed48 in fcgi_accept_request+632 at /php-src/main/fastcgi.c:1483
[3] from 0x0000aaaaab34ef88 in main+3604 at /php-src/sapi/fpm/fpm/fpm_main.c:1862 

下記では、POST リクエスト時のメッセージボディを読み込んでいます。

[0] from 0x0000aaaaab33d594 in safe_read+20 at /php-src/main/fastcgi.c:957
[1] from 0x0000aaaaab33e530 in fcgi_read+72 at /php-src/main/fastcgi.c:1223
[2] from 0x0000aaaaab34b594 in sapi_cgi_read_post+380 at /php-src/sapi/fpm/fpm/fpm_main.c:457
[3] from 0x0000aaaaab0ed830 in sapi_read_post_block+68 at /php-src/main/SAPI.c:242
[4] from 0x0000aaaaab0ed974 in sapi_read_standard_form_data+224 at /php-src/main/SAPI.c:273
[5] from 0x0000aaaaab0ed7c0 in sapi_read_post_data+524 at /php-src/main/SAPI.c:226
[6] from 0x0000aaaaab0ee214 in sapi_activate+432 at /php-src/main/SAPI.c:469
[7] from 0x0000aaaaab0e0f94 in php_request_startup+152 at /php-src/main/main.c:1782
[8] from 0x0000aaaaab34ea7c in main+2312 at /php-src/sapi/fpm/fpm/fpm_main.c:1873

レスポンス書き込み

レスポンス書き込みは safe_write() で行います。write() を呼んでおり、第一引数に req->fd を指定していることが分かります。

https://github.com/php/php-src/blob/PHP-8.3.8/main/fastcgi.c#L917-L952

ret = write(req->fd, ((char*)buf)+n, count-n);

下記ではオペコード ECHO から sapi_module_struct の ub_write(unbuffered write) フィールドに格納した sapi_cgibin_ub_write() から、safe_write() を呼んでいます。つまり、ECHO の出力がクライアントに送信されます。

[0] from 0x0000aaaaab33d4d0 in safe_write+20 at /php-src/main/fastcgi.c:920
[1] from 0x0000aaaaab33ef30 in fcgi_flush+184 at /php-src/main/fastcgi.c:1539
[2] from 0x0000aaaaab33f108 in fcgi_write+396 at /php-src/main/fastcgi.c:1611
[3] from 0x0000aaaaab34ad48 in sapi_cgibin_single_write+76 at /php-src/sapi/fpm/fpm/fpm_main.c:249
[4] from 0x0000aaaaab34add4 in sapi_cgibin_ub_write+48 at /php-src/sapi/fpm/fpm/fpm_main.c:276
[5] from 0x0000aaaaab0fca08 in php_output_op+336 at /php-src/main/output.c:1071
[6] from 0x0000aaaaab0fabec in php_output_write+56 at /php-src/main/output.c:241
[7] from 0x0000aaaaab209f5c in ZEND_ECHO_SPEC_TMPVAR_HANDLER+124 at /php-src/Zend/zend_vm_execute.h:14477
[8] from 0x0000aaaaab27141c in execute_ex+8812 at /php-src/Zend/zend_vm_execute.h:58685
[9] from 0x0000aaaaab274284 in zend_execute+296 at /php-src/Zend/zend_vm_execute.h:61604

下記では php_request_shutdown() からのリクエスト終了処理で、残りのバッファ分と終了ステータスをレスポンスとして出力しています。

[0] from 0x0000aaaaab33d4d0 in safe_write+20 at /php-src/main/fastcgi.c:920
[1] from 0x0000aaaaab33ef30 in fcgi_flush+184 at /php-src/main/fastcgi.c:1539
[2] from 0x0000aaaaab33f40c in fcgi_end+48 at /php-src/main/fastcgi.c:1664
[3] from 0x0000aaaaab33f458 in fcgi_finish_request+48 at /php-src/main/fastcgi.c:1675
[4] from 0x0000aaaaab34c2ac in sapi_cgi_deactivate+68 at /php-src/sapi/fpm/fpm/fpm_main.c:779
[5] from 0x0000aaaaab0ee4c8 in sapi_deactivate_module+468 at /php-src/main/SAPI.c:526
[6] from 0x0000aaaaab0e1470 in php_request_shutdown+704 at /php-src/main/main.c:1909
[7] from 0x0000aaaaab34ef18 in main+3492 at /php-src/sapi/fpm/fpm/fpm_main.c:1970

接続終了

fcgi_close() で切断します。この関数は php_request_shutdown() からの一連のリクエスト終了処理時に呼ばれます。shutdown() で接続を終了し、close() でファイルディスクリプタをクローズしています。

https://github.com/php/php-src/blob/PHP-8.3.8/main/fastcgi.c#L1266-L1315

         shutdown(req->fd, 1);
            /* read any remaining data, it may be omitted */
            while (recv(req->fd, buf, sizeof(buf), 0) > 0) {}
        }
        close(req->fd);
[0] from 0x0000aaaaab33e704 in fcgi_close+20 at /php-src/main/fastcgi.c:1268
[1] from 0x0000aaaaab33f46c in fcgi_finish_request+68 at /php-src/main/fastcgi.c:1676
[2] from 0x0000aaaaab34c2ac in sapi_cgi_deactivate+68 at /php-src/sapi/fpm/fpm/fpm_main.c:779
[3] from 0x0000aaaaab0ee4c8 in sapi_deactivate_module+468 at /php-src/main/SAPI.c:526
[4] from 0x0000aaaaab0e1470 in php_request_shutdown+704 at /php-src/main/main.c:1909
[5] from 0x0000aaaaab34ef18 in main+3492 at /php-src/sapi/fpm/fpm/fpm_main.c:1970

さいごに

PHP コードの実行やそれに伴う前処理、後処理といったコアな機能は、PHP 本体(main/Zend/ ディレクトリ)に実装されており、php-fpm のような SAPI ではそれを利用する形となっています。これは cli のような他の SAPI でも同様なので、仕組みを理解する上で覚えておくと良いでしょう。

このエントリでは、php-fpm ワーカープロセスが FastCGI リクエストの処理サイクルを php-src から見てみました。コードでの確認やデバッガで動きを見る際の足掛かりになるよう実装箇所やスタックトレースを合わせてメモしています。下記のようなデバッグ環境で自分でブレークポイントで停止したり、ステップ実行すると理解が捗るので活用してみてください。

blog.shin1x1.com