【PHP7源码学习】2019-04-25 PHP生命周期浅析

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执行有什么区别?

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

    原文作者:NoSay
    原文地址: https://segmentfault.com/a/1190000020030115
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞