php7的一些变化

1. php7的变化:
  1)抽象语法树
     之前的php版本、php代码在语法解析阶段直接生成了zendvm指令、也就是在zend_language_parser.y中直接生成opline指令、使得编译器和执行器耦合在一起
     php7是先生成抽象语法树、然后将抽象语法树编译成ZendVM指令,使得php的编译和执行隔离开
  2)Native TLS
     在5的版本中、有一些变量是需要在不同函数间共享的、而在多线程的情况下、不能通过简单的全局变量来实现,为此php提供了一个线程安全资源管理器、将全局资源进行线程隔离,在使用全局资源时、先要获取本线程的资源池,这个过程是比较耗时的,php5通过线程传递的方式将本线程的资源池传递给其它函数、避免重复查找,这样几乎所有的函数都得加上接收资源池的参数、即TSRM_DC宏所加的参数、然后调用其它函数时把这个参数传递下去、容易泄漏,且不太优雅
    php7使用Native TLS(线程局部存储)来保存线程的资源池,就是通过__thread来保存一个全局变量、这样,这个变量就是线程共享的了、不同线程的修改不会相互影响
  3)指定函数参数、返回值类型
     php7新增特性
  4)zval结构体的变化
     struct _zval_struct{}
     typedef union _zvalue_value{}
     php7将计数器 refcount__gc 放到了val中、
  5)异常处理
     一些php5中直接抛出fatal error的错误、php7改为了异常抛出、继承throwable可以得到
  6)hashtable的变化
     hashtable即哈希表、也称为散列表、是php数组的内部实现结构,也是php内核中使用很频繁的一个结构、函数符号表、类符号表、常量符号表都是通过hashtable实现的
     php7的hashtable从72byte减小到了32byte、数组元素bucket结构也从72byte减小到了32byte
  7)执行器
     execute_data、opline采用寄存器变量存储、执行器的调度函数为:execute_ex、这个函数负责执行php代码编译生成的zendVM指令、在执行期间会频繁的调用execute_data、opline两个变量,5的版本、这两个变量是通过参数传递给各指令handler的,7中使用寄存器存储、避免了参数出栈入栈的操作,同时寄存器相对于内存的访问更快,这个优化使php的性能提升了5%
  8)新的参数解析方式:
     php7在保留原参数解析方式zend_parse_parameters()的同时提供了另外一种高效的参数解析方式

几个源码目录:
SAPI:应用接口层
main:php的主要代码、主要为输出输入、web通信,以及php框架的初始化操作等
      eg fastcgi协议的解析、扩展的加载、php配置的解析等由它完成
zend:php解析器的主要实现,即zendvm,是php语言的核心实现、代码的解释执行主要由zend完成
ext:是php的扩展目录
TSRM:为线程安全相关的实现

php的生命周期:
模块初始化、请求初始化、执行脚本、请求关闭、模块关闭5个阶段,根据sapi不同的实现、各个阶段的执行情况会有差异、比如命令行模式下、每个请求都会完整的经历这些阶段、fastcgi模式下则只在启动时执行一次模块初始化操作,在sapi关闭时才执行模块关闭操作,每个请求其实只经历了请求初始化、执行脚本和请求关闭的3个阶段

模块初始化、只在sapi启动时执行一次,对fpm而言、模块初始化只在fpm的master进程启动时执行一次,master创建一个socket但不接收、处理请求,由fork出来的worker进程来进行处理,master的工作主要是fork或者kill worker进程,比如、当请求较多,worker进程处理不过来时、master会fork新的worker进行处理,空闲时、master会kill部分worker避免系统占用,浪费时间

worker主要是处理请求、每个worker进程会竞争的接收请求,接收成功后解析fastcgi、然后执行相应的脚本,处理完成后关闭请求、等待新的连接,这就是一个worker的生命周期,可以看出一个worker只处理一个请求,与nginx的事件模型不同、nginx的子进程通过epoll管理套接字,如果一个请求还未发完、则先处理下一个请求,所以一个进程可以同时连接多个请求、是非阻塞型的模型,只处理活跃的套接字,fpm的这种管理方式也简化了php的资源管理、使得fpm模式下不需要考虑并发导致的资源冲突

master和worker不直接通信、通过共享内存来获取worker进程的信息、比如worker进程的当前状态、已处理的请求数、master通过信号发生的方式来kill woker进程
fpm可以同时监听多个端口、每个端口对应一个worker pool、每个poll下对应多个worker进程,类似nginx中server的概念


worker进程的处理:
worker进程返回man函数后将继续向下执行,此后就是worker不断accept请求,有请求到达后将读取并解析fastcgi协议的数据、完成解析后执行php脚本、执行完成关闭请求、等待新的请求到达、周期如下:
1)等待请求:会阻塞在fcgi_accept_request中等待请求
2)解析请求:fastcgi请求到达后被worker接收,然后开始接收并解析请求数据、直到request数据完全到达 - 阻塞
3)请求初始化:执行php_request_startup, 这个阶段会调用每个扩展的php_rinit_function
4)执行php脚本:由php_execute_script完成php脚本的编译、执行操作
5)请求完成后执行php_request_shutdown,这个阶段会调用每个扩展的php_rshutdown_function,然后进入步骤1

master的进程管理:
1)静态模式(static):比较简单,在启动时根据pm.max_children配置fork出响应的worker进程即可
2)动态模式(dynamic):比较常用,启动时根据pm.start_server时来初始化一定数量的worker,运行期间若发现worker空闲数低于pm.min_spare_servers(表示请求较多、worker处理不完了)则会fork worker进程,但总的worker数不能超过pm.max_children,若发现空闲进程多于pm.max_spare_children,则会杀掉一些worker,根据这4个配置项动态调控worker值
3)按需模式:像传统的cgi,在启动时不分配worker、请求到达则通知master fork出worker进程,总的worker数不会超过pm.max_children,处理完成后worker不会立即退出,当空闲时间超出pm.worker_idle_timeout再退出

1. 信号定时器
2. 进程检查定时器
检测每个worker pool时,会首先根据此pool下所有worker的状态计算处于空闲、忙碌状态的worker数,这个就是根据每个worker进程fpm_scoreboard_proc_s->request_stage状态来判断的,接着根据fpm的模式进行处理、如果是按需模式,会判断空闲时间最久的worker是否达到了pm.process_idle_timeout的值,到了就kill掉,每次只会kill或者fork出一个worker进程,多个worker同时达到process_idle_timeout的值、则需要等待下个周期、fork一样、不同的是,若发现好几个周期都在fork进程、说明worker远远不足、就把fork数翻倍、上限是32
3. 执行超时检测器:
fpm.conf有一个request_terminate_timeout的配置项,如果worker处理一个请求的总时长超过这个值,master会向此worker发送kill-term信号,杀掉这个进程、默认0,表示关闭这个机制
fpm中记录的slow_log也是通过这个定时器完成的


4. php的内存管理
  在C中直接使用malloc、free进行内存的分配和释放,但频繁的分配和释放内存会产生内存碎片、降低系统性能,php的变量的分配和释放会非常的频繁、若直接通过malloc的方式爱进行分配、会造成严重的性能问题,作为语言级的应用、这种损耗不太能接受、所以php实现了自己的内存池ZendMM,来替代glibc的malloc和free、以解决内存频繁分配、释放的问题

  它定义了chunk、page、slot三种粒度的内存操作,每个chunk的大小为2MB、每个page 4kB,一个chunk切割成512个page、每个page切割成若干个slot,申请内存时、按照申请的大小、执行不同的分配策略
  huge:大于2MB,直接调用系统分配,分配若干个chunk
  large:申请内存大于3092B(3/4page)、小于2044kb(511page)分配若干个page
  small:申请内存小于3092B,内存池提前定义好了30种不同大小的内存(81632...、3072),它们分布在不同的page上、申请内存时直接在相应的内存上查找相应的slot,内存池通过zend_mm_heap结构存储内存池的主要信息,比如大内存链表、chunk链表、slot各大小内存链表等
  大内存分配的实际上是若干个chunk,然后通过一个zend_mm_huge_list结构进行管理,大内存之间构成单向链表
  chunk是内存池向系统申请、释放内存的最小粒度,chunk之间构成双向链表,第一个chunk的地址保存于zend_mm_heap->main_chunk,第一个page的内存用于保存chunk自己的结构体成员,比如前后chunk的指针、当前chunk各page的使用情况等
  相同大小的slot之间构成单链表

  内存池的初始化在php_module_startup阶段完成、初始化过程主要是分配heap结构,这个过程在start_memory_manager过程完成,如果是多线程环境,则会为每一个线程分配一个内存池,线程之间互不影响,初始化时会根据环境变量use_zend_alloc_huge_pages来设定是否开启内存大页,非线程安全环境下、分配的heap会保存到alloc_globals中也就是AG宏,需要注意的是,zend_mm_heap这个结构不是单独分配的,嵌入在chunk结构体中,,因为chunk结构体占用了一个page、但实际上它用不了那么大的内存,为了尽可能利用空间、就把它放在了这里

  内存分配:
  1. huge超过2MB的内存分配,分配时,会将它对齐到n个chunk,还会分配一个zned_mm_huge_list结构,管理所有的huge内存,内存对齐的过程是内存池在申请后自己调整的,而不是简单的由操作系统来完成,会先按照实际大小申请一次,如果刚好可以达到内存对齐、无需调整、直接返回使用,如果不是ZEND_MM_CHUNK_SIZE(2MB)的整数倍,则zendMM会释放掉这块儿内存,然后按照实际内存大小+ZEND_MM_CHUNK_SIZE再申请一次,多申请的那块是用来调整的

  2. large分配:申请的内存大小在3072B(3/4page)和2044k(511个page)之间时,内存池会在chunk上查找对应数量的page返回,large的申请粒度是page,chunk上有free_map和map两个成员用于记录page的分配信息
  free_map就是512bit,用来记录该chunk上page的分配情况,已使用则置位1
  map用来记录page的分配类型及分配的page页数,每个page对应一个数组成员,最高2位记录page的分配类型、01是large、10是small,分配时从第一个chunk开始遍历、依次查找chunk是否有满足要求的page,若当前chunk没有合适的、则查找下一个chunk,若直到最后都没有合适的、则重新分配一个chunk,申请时,不知谁查找到足够页数的page、而是尽可能的填满chunk的空隙,尽可能的与已分配的page连接在一起,避免中间出现page空隙(以减少后续分配时的查找次数)
  1)从第一个chunk分组(0~63)开始检查,当前分组有可用page、先检测当前page的bit位、找到第一个和最后一个空闲page的位置,如果不够则把这些page都标记为1(已分配)、进行查找其它分组,如果page恰好、则直接使用、中断检索,如果page比需要的大、表示可用,但不是最优的,会接着查找其它chunk直到最后比较出来一个最优的(可最大程度利用page的)
  2)找到合适的page后、就设置相应的page信息、即free_map和map信息然后返回page地址

  3. small分配:
  会先检查相应规格的内存是否已分配、若未分配或者分配已使用完、则申请相应页数的page、page的分配过程与large分配一致,申请到page之后,按照固定大小切割成slot、slot之间用单链表连接,链表头部保存至AG(mm_heap)->free_slot

内存池释放的粒度是chunk,通过efree来完成
1)huge内存的释放、large、smal类型的因为chunk的第一个page被占用、所以不可能是相对chunk的偏移量为0,由此区分chunk类型和large、small类型,释放时,之间将占用的chunk释放、同时从AG链表删除
2)large内存的释放:若计算得到offset不为0,表示该地址是large或者small内存、然后根据offset算出是第几个page、得到page之后可以从chunk->map中得到page的分配类型、就可以释放指定类型的内存了,large并不会直接释放、而是将page的分配信息置为未分配、若释放后、发现该chunk下都是未分配的,则释放chunk、释放时优选选择把chunk移到AG,缓存数达到一定值后就不再继续缓存新加入的chunk、将内存归还系统、避免占用过多的内存,在分配chunk时若发现chached_chunks中有缓存的chunk直接取出使用、不向系统发出申请
3)small类型的释放、直接将释放的slot插回该规则slot的可用链表的头部即可、比较简单



TSRM的基本实现:
TSRM的核心思想就是为不同的线程分配独立的内存空间、若一个资源可能会被多线程使用、那么就需要预先向TSRM注册资源、TSRM为这个资源分配唯一的资源id,并把这种资源的大小、初始化函数等保存到一个叫tsrm_resource_type的结构中,各线程只能通过tsrm分配的唯一资源id来访问这个资源、当线程拿着资源id来访问时、若是第一次请求、则根据资源注册时指定的资源大小分配一块内存、然后调用初始化函数进行初始化、并把这个资源保留下来供这个线程后续使用。
TSRM为每个线程分配一个tsrm_tls_entry结构、用于保存所有的公共资源、所有的tsrm_tls_entry结构保存在tsrm_tls_table数组中、这个是全局变量、访问需要加锁,tsrm_tls_entry在tsrm初始化时按照预设定的线程数分配、每个线程的tsrm_tls_entry结构在这个数组中的位置是根据线程id和预设的线程数tsrm_tls_table_size取模得到的,也就是可能存在多个线程保存在tsrm_tls_table的同一位置,所以tsrm_tls_entry是链表结构,线程在查找自己的tsrm_table_entry时,需要遍历链表比较thread_id确定是否是当前线程的

公共资源使用前必须向TSRM注册、注册时要提供:
1)资源的大小
2)资源的初始化函数
3)资源的清理函数

从上边可以看出、资源的获取主要分为3个过程:
1)获取线程id  2)查找tsrm_tls_entry这个过程需要加锁、遍历,比较耗时  3)根据资源id从tsrm_tls_entry->storage中获取资源、
由于2)需要加锁、同一时间只能有一个线程进行、php的资源访问又特别频繁,所以TSRM通过线程私有数据TSD优化了这个问题:即各线程可以根据同名的key取到不同的变量地址
又通过线程局部存储优化了tsrm_tls_entry->storage的查找次数


 zend虚拟机
    zend虚拟机是php的解释器、是php语言实现的核心,解释型语言与实际计算机之间多了一层解释器、来屏蔽不同平台之间机器语言的差异,因此解释型语言可以方便的运行在不同平台对应的解释器上、由解释器来处理不同平台之间的差异、实现跨平台的运行,代价是:效率低多了解释这一步。另外,同样的计算、需要执行更多的指令来执行
    zendVM由2部分组成:编译器、执行器,其中编译器负责将php代码解释为zendvm可识别的指令(opline)、同时生产对应的符号表(函数、类等)、执行器负责执行opcode的机器指令

    opline是zendVM定义的执行指令、每条指令的编码为opcode,等价于机器指令,opline的组成:对何数据,如何处理,前者称为操作数、也就是指令的操作对象,后者称为opcode,即指令的处理动作,目前php一共定义了173条opcode

操作数类型:
1)IS_CONST:常量、eg $a = 123; $b = 'hello'其中123和hello都是固定不变的、在编译阶段会被分配到单独的内存区,执行时若发现是IS_CONST类型就会到字面量存储的位置获取数据
2)IS_CV:compile variable,也就是php脚本中通过$声明的变量、比如 上例中的$a$b 就是CV变量
3)IS_VAR: php变量,这个php的变量没有显式的哎php脚本中定义、不是直接在代码里通过$var_name 定义的、最常见的是php函数的返回值、eg $a = time();注意、它的返回值并不是$a、实际上是指两个指令、函数调用&赋值
4)IS_TMP_VAR:临时变量、eg $a = 'hello' . time(); 字符串拼接指令的执行结果就是临时变量、主要用于一些操作的中间结果
5)IS_UNUSED:表示操作数没有使用



 php的编译和执行:
 php的编译就是将php的脚本代码根据定义的语法规则解析为opcode指令、在这个过程中先后经历词法分析、语法分析生成抽象语法树,然后将抽象语法树编译为opcode指令,最终输出opcode

 词法分析会逐行读入源代码、然后按照构词规则、将php代码切割为定义好的可识别的token,eg $a = 123,其中$a 会被识别为T_VARIABLE
 语法分析是在词法分析的基础上、将单词序列组合成各类语法短语、如语句声明、表达式、函数定义等



寄存器变量是存放在CPU的寄存器中、使用时无需访问内存、直接从寄存器读写、访问速度上 寄存器变量>内存>硬盘,
php7对opline和execute_data变量的存储方式优化、采用全局寄存器变量来保存他们的地址,直接从寄存器读取,比从栈上读取更快、同时省掉了传参的入栈流程,但是只有局部自动变量和形参才能够被定义为寄存器变量、全局变量和局部静态变量都不能被定义为寄存器变量、且一个计算机中的寄存器变量也是有限的、一般为2-3个、寄存器变量过多时,会被转化为自动变量、且受到寄存器长度的限制、寄存器变量只能是char、int或者指针型,而不能是其它的数据类型,不能是复杂的数据类型、寄存器变量无变量地址,所以不能使用&求其地址

php7在execute_ex()执行各opcode的过程中、不再将execute_data作为参数传递给handler,而是通过寄存器报错execute_data及opline的地址、handler使用时直接从全局变量读取、执行完再把下一条更新到全局变量
info register r14可以查看r14寄存器的内容


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值