写点什么

PHP7 内核实现原理 - 启动过程

作者:菜皮日记
  • 2023-09-09
    北京
  • 本文字数:3737 字

    阅读完需:约 12 分钟

FPM 启动和初始化 worker 的过程

代码在源码 /sapi/fpm/fpm/fpm_main.c 中

  • fpm_conf_init_main() 函数解析 php-fpm.conf 配置文件,分配 worker pool 的内存空间。每个 worker pool 用结构体 fpm_worker_pool_s 表示,每个 pool 中的有一个 fpm_scoreboard_s 结构体,用来管理具体一个 worker

  • fpm_scoreboard_init_main() 函数分配每个 worker 的内存,在 pool 的 fpm_scoreboard_s 结构体中,每个 worker 使用 fpm_scoreboard_proc_s 结构体表示

  • 之后 master 给每个 worker pool 创建 socket、注册监听的信号等。

即解析 php-fpm.conf → 初始化 worker pool→ 初始化 worker

关系类似:fpm_worker_pool_s(fpm_scoreboard_s(fpm_scoreboard_proc_s))

FPM worker/PHP 处理请求的过程

  • 等待请求:worker 阻塞在 fcgi_accept_request 函数等待请求到来。

  • 解析请求:FastCGI 接到请求并解析请求数据。

  • 请求初始化:执行 php_request_startup 此阶段会调用每个扩展的:PHP_RINIT_FUNCTION 函数 即 RINIT。

  • 编译、执行:由 php_execute_script 完成 PHP 脚本的编译、执行

  • 关闭请求:请求完成后执行 php_request_shutdown ,此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION 即 RSHUTDOWN,重新进入下一轮等待。

// master执行本函数 创建并初始化workerint fpm_run(int *max_requests){    struct fpm_worker_pool_s *wp;    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {        //调用fpm_children_make() fork子进程        is_parent = fpm_children_create_initial(wp);                if (!is_parent) {            goto run_child;        }    }    //master进程将进入event循环,不再往下走    fpm_event_loop(0);
run_child: //只有worker进程会到这里
    *max_requests = fpm_globals.max_requests;    return fpm_globals.listening_socket; //返回监听的套接字}
int main(int argc, char *argv[]){    ...    fcgi_fd = fpm_run(&max_requests);    parent = 0;
    //初始化fastcgi请求    request = fpm_init_request(fcgi_fd);        //worker进程将阻塞在这,等待请求    while (EXPECTED(fcgi_accept_request(request) >= 0)) {        SG(server_context) = (void *) request;        init_request_info();                //请求开始 RINIT        if (UNEXPECTED(php_request_startup() == FAILURE)) {            ...        }        ...
        fpm_request_executing();        //编译、执行PHP脚本        php_execute_script(&file_handle);        ...        //请求结束 RSHUTDOWN        php_request_shutdown((void *) 0);        ...    }    ...    //worker进程退出 MSHUTDOWN    php_module_shutdown();    ...}
复制代码

PHP 执行的几个阶段或生命周期

从请求放宽到整个 PHP 的执行阶段,在上面请求的处理前后,增加模块的初始化和关闭阶段:

  • 模块初始化 php_module_startup MINIT 初始化各个模块初始化部分全局变量和常量解析 php.ini 初始化 Zend 引擎和核心组件

  • 请求初始化 php_request_startup RINIT 重置垃圾回收初始化编译器、执行器、扫描器等

  • 执行 PHP 脚本 php_execute_script EXEC 词法分析,得到 tokens 语法分析 得到 抽象语法树 AST 编译成 opcodes 执行 opcodes

  • 请求结束 php_request_shutdown RSHUTDOWN 销毁 request 相关的全局变量关闭编译器、执行器还原 ini 配置

  • 模块关闭 php_module_shutdown MSHUTDOWN 销毁全局变量关闭所有扩展、垃圾回收、内存管理等

Token、AST 和 opcodes 之间的关系

PHP 代码 => Token => 抽象语法树 => Opcodes => 执行

  • 源代码通过词法分析得到 TokenToken 是 PHP 代码被切割成的有意义的标识。PHP7 一共有 137 种 Token,在 zend_language_parser.h 文件中做了定义。

  • 基于语法分析器将 Token 转换成抽象语法树(AST)Token 就是一个个的词块,但是单独的词块不能表达完整的语义,还需要借助一定的规则进行组织串联。所以就需要语法分析器根据语法匹配 Token,将 Token 进行串联。语法分析器串联完 Token 后的产物就是抽象语法树(AST,Abstract Syntax Tree)。AST 是 PHP7 版本的新特性,之前版本的 PHP 代码的执行过程中是没有生成 AST 这一步的。它的作用主要是实现了 PHP 编译器和解释器的解耦,提升了可维护性。

  • 将语法树转换成 Opcode 需要将语法树转换成 Opcode,才能被引擎直接执行。

  • 执行 Opcodesopcodes 是 opcode 的集合形式,是 PHP 执行过程中的中间代码。PHP 工程优化措施中有一个比较常见的 “开启 opcache”,指的技术这里将 opcodes 进行缓存。通过省去从源码到 opcode 的阶段,引擎直接执行缓存好的 opacode,以提升性能。

Token 例子

如下代码中,经过第一部词法分析后得到一些 token

php -r 'print_r(Token_get_all("<?php echo \"hello\"; "));'
复制代码

输出:

Array(    [0] => Array        (            [0] => 379            [1] => <?php            [2] => 1        )
    [1] => Array        (            [0] => 328            [1] => echo            [2] => 1        )
    [2] => Array        (            [0] => 382            [1] =>            [2] => 1        )
    [3] => Array        (            [0] => 323            [1] => "hello"            [2] => 1        )
    [4] => ;    [5] => Array        (            [0] => 382            [1] =>            [2] => 1        )
)
复制代码

Token_get_all 函数可以打印解析的 token。数组的第一个值为 Token 对应的枚举值。第二个值为 Token 对应的原始字符串内容。第三个值为代码对应的行号。可以看出,词法解析器将 “<? php echo "hello world"; ” 这段文本内容切分成了 4 部分。

AST 抽象语法树

之后生成 AST 抽象语法树,可理解为对语法的一种抽象,再用 AST 生成 opcodes,是 opcode 的集合,交给 zend 执行。如这样一段代码:

<?php$a = 1;$b = $a + 2;echo $b;
复制代码

生成的 AST 大概这个意思:

如果使用 php-parser 解析生成 AST,结果这样:

array(    0: Stmt_Expression(        expr: Expr_Assign(            var: Expr_Variable(                name: a            )            expr: Scalar_LNumber(                value: 1            )        )    )    1: Stmt_Expression(        expr: Expr_Assign(            var: Expr_Variable(                name: b            )            expr: Expr_BinaryOp_Plus(                left: Expr_Variable(                    name: a                )                right: Scalar_LNumber(                    value: 2                )            )        )    )    2: Stmt_Echo(        exprs: array(            0: Expr_Variable(                name: b            )        )    ))
复制代码

opcode

PHP 是构建在 Zend 虚拟机(Zend VM)之上的,PHP 的 opcode 就是 Zend 虚拟机中的指令。

在 php_execute_script 执行脚本阶段,会先经过词法分析,得到 tokens,之后进行语法分析,之后得到 opcode,最后交给执行器执行 opcode。

使用 vld 扩展可查看生成的 opcode

php -dvld.active=1 b.php
复制代码

附加-程序执行的词法分析和语法分析介绍

词法分析是把 PHP 代码分割成一个个的“单元”(TOKEN),语法分析则将这些“单元”转化为 Zend Engine 可执行的操作。然后 PHP 内部的 Zend Engine 对这些操作进行顺次的执行。

词法分析和语法分析一般按使用 Lex 和 Yacc 来实现。

  • Lex(Lexical Analyzer)主要用于做词法分析

  • Yacc(Yet Another Compiler-Compiler)主要用来做语法分析

关于 lex yacc 的解释:https://www.cnblogs.com/hdk1993/p/4922801.html

系列文章《Python 解释器源码剖析》https://www.cnblogs.com/traditional/p/11511685.html

用户头像

菜皮日记

关注

全干程序员 2018-08-08 加入

还未添加个人简介

评论

发布
暂无评论
PHP7内核实现原理-启动过程_php_菜皮日记_InfoQ写作社区