PHP7源码学习20190425-PHP生命周期浅析

47次阅读

共计 4539 个字符,预计需要花费 12 分钟才能阅读完成。

Grape

视频传送门:【每日学习记录】使用录像设备记录每天的学习


今天我们来看下 PHP 的生命周期,我们都知道 PHP 生命周期有五个步骤,那么在源码层级是怎么去实现 PHP 生命周期呢?首先,我们抛出本文的几个问题:

  1. php 的生命周期是什么?每个阶段做了什么?
  2. 为什么会有 FPM?
  3. cli 执行代码和请求经过 fpm 执行有什么区别?

       思考 ing。。。。

好的,接下来我们解释上边三个问题。

1. 什么是 php 的生命周期?每个阶段做了什么?

这个问题相信大家都能够回答,php 的生命周期有五个步骤:

- php_module_startup:模块初始化
- php_request_startup:请求初始化
- php_execute_script:执行脚本
- php_request_shutdown:请求关闭
- php_module_shutdown:模块关闭

在执行完这个个步骤之后,就走过了 PHP 的一生,感觉设计者完全借鉴了人的一生去设计的生命周期,出生,成长奋斗,结婚生子,完成理想以及老去,妙啊。
那么,对于这五个步骤有什么意义呢?我们来逐个了解一下。我们拿 cli 来举例子(入口在 sapi/cli/php.ini),我们假设 sapi 的初始化等步骤已经完成,因为本文重点是 PHP 生命周期,着着重讲解五个步骤。

php_module_startup

看名字就这道这个函数的作用,模块的初始化,即调用每个拓展源码中的的 PHP_MINIT_FUNCTION 中的方法初始化模块, 进行一些模块所需变量的申请, 内存分配等。
这一步骤主要完成的工作有以下几点:

- 初始化 zend_utility_functions 结构. 这个结构是设置 zend 的函数指针, 比如错误处理函数, 输出函数, 流操作函数等.
- 设置环境变量.
- 加载 php.ini 配置.
- 加载 php 内置扩展.
- 写日志.
- 注册 php 内部函数集.
- 调用 php_ini_register_extensions, 加载所有外部扩展
- 开启所有扩展
- 一些清理操作.

我们看一下加载 php.ini 配置,代码如下:


     /* this will read in php.ini, set up the configuration parameters,
           load zend extensions and register php function extensions
           to be loaded later */
        if (php_init_config() == FAILURE) {return FAILURE;}//   php_init_config 函数会在这里检查所有 php.ini 配置, 并且找到所有加载的模块, 添加到 php_extension_lists 结构中.
    
        /* Register PHP core ini entries */
        REGISTER_INI_ENTRIES();// 展开后为 zend_register_ini_entries(ini_entries, module_number),ini_entries 是 PHP_INI_BEGIN/END()两个宏生成的配置映射规则数组,通常会把这个操作放到 PHP_MINIT_FUNCTION()中。// 注意:此时 php.ini 已经解析到 configuration_hash 哈希表中,zend_register_ini_entries()将根据配置 name 查找这个哈希表,// 如果找到了表明用户在 php.ini 中配置了该项,// 然后将调用此规则指定的 on_modify 函数进行赋值,此处更详细的介绍请看[https://www.kancloud.cn/nickbai/php7/363320]
    对于其它的一些操作是怎么实现的,大家可以自行查看源码。

php_request_startup

请求初始化阶段,即接受到客户端的请求后调用每个拓展的 PHP_RINIT_FUNCTION 中的方法, 初始化 PHP 脚本的执行环境。
在此函数的实现种主要有以下几个函数:

  • zend_interned_strings_activate():初始化内部字符串哈希表
  • php_output_activate():启动 php 的输出
  • zend_activate():激活 Zend 引擎
  • sapi_activate():激活 SAPI,进行编译器,重置 gc,执行器以及词法扫描器。
  • zend_signal_activate(),处理一些信号
  • zend_activate_modules():回调各扩展定义的 request_startup 钩子函数。

php_execute_script

执行脚本阶段,入口是 php_execute_script()。此过程和 2 一样,均在 do_cli 函数内完成。首先获取真正执行的文件信息等,把要执行的文件放在 included_files 列表里边。然后会调用 zend_execute_scripts()去真正执行。真正执行的时候就涉及到了编译,执行,op_array 之类的概念。编译过程又涉及到词法分析,语法分析和抽象语法树 (AST) 等概念。执行的话会涉及到 opcode 的概念。这些概念在之前的文章中已经讲解过具体实现,感兴趣的读者可以自行前往。传送门:笔记汇总。

php_request_shutdown

请求关闭阶段。在这个阶段总共有 16 个步骤,在源码里有着明确的注释,无谓就是做一些“清理”操作,我们看下源码怎么做的。

EG(current_execute_data) = NULL;/*EG(current_execute_data) 指向 nirvana,因此无法在 zend_executor 回调函数中安全地访问.*/
php_deactivate_ticks()// 清空 tick 函数
1.php_call_shutdown_functions()// 调用注册了 register_shutdown_function()的所有可能的 shutdown 函数
2.zend_call_destructors()// 调用所有可能的__destruct() 函数
3.php_output_discard_all()/php_output_end_all():// 刷新所有输出缓冲区
4.zend_unset_timeout()// 重置 max_execution_time(响应发送后不再执行 php 代码)
5.zend_deactivate_modules()// 调用所有扩展 RSHUTDOWN 函数
6.php_output_deactivate()// 关闭输出层(发送设置好的 HTTP 头文件,清除输出处理程序等)
7.php_free_shutdown_functions()// 释放 shutdown 函数
8.zval_ptr_dtor()// 销毁 super-globals
9.php_free_request_globals()// 释放 request-bound globals
10.zend_deactivate()// 关闭扫描仪 / 执行器 / 编译器并还原 ini 条目
11.zend_post_deactivate_modules// 调用 rshutdown 后的所有扩展
12.sapi_deactivate//SAPI 相关的 shutdown (free stuff)
13.virtual_cwd_deactivate// 释放 virtual CWD 内存
14.php_shutdown_stream_hashes// 破坏流哈希表
15.zend_interned_strings_deactivate()/shutdown_memory_manager():Free Willy (here be crashes)
16.zend_unset_timeout():重置 max_execution_time

php_module_shutdown

模块关闭阶段:与模块初始化阶段相反,这个阶段将清理资源、各 php 模块关闭等操作。具体的代码函数调用不再赘述。

2. 为什么会有 FPM?

我们在看过 cli 下生命周期的五个阶段之后会发现一个问题,这种形式好像有个问题,就是它每来一次请求就会有这五个阶段,这样会造成多大的资源浪费啊。那么为了解决这个问题,FPM 应运而生,FPM(FastCGI Process Manager)是 PHP FastCGI 运行模式的一个进程管理器。
概括来说,fpm 的实现就是创建一个 master 进程,在 master 进程中创建并监听 socket,然后 fork 出多个子进程,这些子进程各自 accept 请求,子进程的处理非常简单,它在启动后阻塞在 accept 上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说 fpm 的子进程同时只能响应一个请求,只有把这个请求处理完成后才会 accept 下一个请求,这一点与 nginx 的事件驱动有很大的区别 nginx 的子进程通过 epoll 管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。
知道它的工作机制我们就可以想象一下他会如何去改善 cli 模式下每个请求都完成一次初始化的问题,我们猜测一下,他会在 master 进程进行一次初始化之后在请求阶段循环,直至结束,这样就达到了不用多次初始化的目的。好的我们看下它是怎么实现的?
首先进行 fpm_init, 此步主要是对 fpm 进行初始化,加载 fpm 配置文件,分配用于和 worker 进行通信的共享内存,创建 worker_pool 的套接字,启动 master 的事件管理器(fpm 实现了一个事件管理器用于管理 IO、定时事件,其中 IO 事件通过 kqueue、epoll、poll、select 等管理,定时事件就是定时器,一定时间后触发某个事件)等等操作。
接下来就是 fpm_run 的过程,master 将 fork 出 worker 进程,worker 进程返回 main()中继续向下执行,后面的流程就是 worker 进程不断 accept 请求,然后执行 PHP 脚本并返回。fpm_run 整体流程如下:

1. 等待请求:worker 进程阻塞在 fcgi_accept_request() 等待请求;2. 解析请求:fastcgi 请求到达后被 worker 接收,然后开始接收并解析请求数据,直到 request 数据完全到达;3. 请求初始化:执行 php_request_startup(),此阶段会调用每个扩展的:PHP_RINIT_FUNCTION();4. 编译、执行:由 php_execute_script() 完成 PHP 脚本的编译、执行;5. 关闭请求:请求完成后执行 php_request_shutdown(),此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION(),然后进入步骤 (1) 等待下一个请求;

在这个阶段,master 进程将进入 fpm_event_loop()来依赖注册的几个事件进行不同的操作。
到此,对于 fpm 的简单叙述就到此为止了。可以理解 fpm 的诞生就是一剂灵丹妙药,拉长了 PHP 的生命战线

3. cli 执行代码和请求经过 fpm 执行有什么区别?

其实我觉得这个问题在看过上边两个问题之后答案就已经出来了~,那么这块就让聪明的你来解决啦。

正文完
 0