PHP内核
2019-04-25 15:58:46
9
0
0
写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改。 COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C++的STL等。 在PHP内核中,COW也是主要的内存优化手段。 在前面关于变量和内存的讨论中,引用计数对变量的销毁与回收中起着至关重要的标识作用。 引用计数存在的意义,就是为了使得COW可以正常运作,从而实现对内存的优化使用。
写时复制的作用
经过上面的描述,大家可能会COW有了个主观的印象,下面让我们看一个小例子, 非常容易看到COW在内存使用优化方面的明显作用:
03 | var_dump(memory_get_usage()); |
05 | $tipi = array_fill (0, 100000, 'php-internal' ); |
06 | var_dump(memory_get_usage()); |
09 | var_dump(memory_get_usage()); |
11 | foreach ( $tipi_copy as $i ){ |
14 | var_dump(memory_get_usage()); |
上面的代码比较典型的突出了COW的作用,在一个数组变量$tipi被赋值给$tipi_copy时, 内存的使用并没有立刻增加一半,甚至在循环遍历数 $tipi_copy时, 实际上遍历的,仍是$tipi指向的同一块内存。
也就是说,即使我们不使用引用,一个变量被赋值后,只要我们不改变变量的值 ,也与使用引用一样。 进一步讲,就算变量的值立刻被改变,新值的内存分配也会洽如其分。 据此我们很容易就可以想到一些COW可以非常有效的控制内存使用的场景, 如函数参数的传递,大数组的复制等等。
在这个例子中,如果$tipi_copy的值发生了变化,$tipi的值是不应该发生变化的, 那么,此时PHP内核又会如何去做呢?我们引入下面的示例:
PHP内核
2019-04-25 15:58:02
15
0
0
在维基百科中有这样一段描述: 凡是位于速度相差较大的两种硬件之间的,用于协调两者数据传输速度差异的结构,均可称之为Cache。 从最初始的处理器与内存间的Cache开始,都是为了让数据访问的速度适应CPU的处理速度, 其基于的原理是内存中“程序执行与数据访问的局域性行为”。 同样PHP内存管理中的缓存也是基于“程序执行与数据访问的局域性行为”的原理。 引入缓存,就是为了减少小块内存块的查询次数,为最近访问的数据提供更快的访问方式。
PHP将缓存添加到内存管理机制中做了如下一些操作:
- 标识缓存和缓存的大小限制,即何时使用缓存,在某些情况下可以以最少的修改禁用掉缓存
- 缓存的存储结构,即缓存的存放位置、结构和存放的逻辑
- 初始化缓存
- 获取缓存中内容
- 写入缓存
- 释放缓存或者清空缓存列表
首先我们看标识缓存和缓存的大小限制,在PHP内核中,是否使用缓存的标识是宏ZEND_MM_CACHE(Zend/zend_alloc.c 400行), 缓存的大小限制与size_t结构大小有关,假设size_t占4位,则默认情况下,PHP内核给PHP内存管理的限制是128K(32 * 4 * 1024)。 如下所示代码:
1 | #define ZEND_MM_NUM_BUCKETS (sizeof(size_t) << 3) |
4 | #define ZEND_MM_CACHE_SIZE (ZEND_MM_NUM_BUCKETS * 4 * 1024) |
如果在某些应用下需要禁用缓存,则将ZEND_MM_CACHE宏设置为0,重新编译PHP即可。 为了实现这个一处修改所有地方都生效的功能,则在每个需要调用缓存的地方在编译时都会判断ZEND_MM_CACHE是否定义为1。
如果我们启用了缓存,则在堆层结构中增加了两个字段:
5 | zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS]; |
如上所示,cached表示已缓存元素使用内存的总大小,zend_mm_free_block结构的数组装载被缓存的块。 在初始化内存管理时
PHP内核
2019-04-25 15:57:32
7
0
0
垃圾回收机制是一种动态存储分配方案。它会自动释放程序不再需要的已分配的内存块。 自动回收内存的过程叫垃圾收集。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑。 在现在的流行各种语言当中,垃圾回收机制是新一代语言所共有的特征,如Python、PHP、Eiffel、C#、Ruby等都使用了垃圾回收机制。 虽然垃圾回收是现在比较流行的做法,但是它的年纪已经不小了。早在20世纪60年代MIT开发的Lisp系统中就已经有了它的身影, 但是由于当时技术条件不成熟,从而使得垃圾回收机制成了一个看起来很美的技术,直到20世纪90年代Java的出现,垃圾回收机制才被广泛应用。
PHP也在语言层实现了内存的动态管理,这在前面的章节中已经有了详细的说明, 内存的动态管理将开发人员从繁琐的内存管理中解救出来。与此配套,PHP也提供了语言层的垃圾回收机制, 让程序员不必过分关心程序内存分配。
在PHP5.3版本之前,PHP只有简单的基于引用计数的垃圾回收,当一个变量的引用计数变为0时, PHP将在内存中销毁这个变量,只是这里的垃圾并不能称之为垃圾。 并且PHP在一个生命周期结束后就会释放此进程/线程所点的内容,这种方式决定了PHP在前期不需要过多考虑内存的泄露问题。 但是随着PHP的发展,PHP开发者的增加以及其所承载的业务范围的扩大,在PHP5.3中引入了更加完善的垃圾回收机制。 新的垃圾回收机制解决了无法处理循环的引用内存泄漏问题。PHP5.3中的垃圾回收机制使用了文章引用计数系统中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems) 中的同步算法。关于这个算法的介绍我们就不再赘述,在PHP的官方文档有图文并茂的介绍:回收周期(Collecting Cycles)。
如前面所说,在PHP中,主要的内存管理手段是引用计数,引入垃圾收集机制的目的是为了打破引用计数中的循环引用,从而防止因为这个而产生的内存泄露。 垃圾收集机制基于PHP的动态内存管理而存在。PHP5.3为引入垃圾收集机制,在变量存储的基本结构上有一些变动,如下所示:
PHP内核
2019-04-25 15:55:37
17
0
0
对于PHP这种需要同时处理多个请求的程序来说,申请和释放内存的时候应该慎之又慎,一不小心便会酿成大错。另一方面,除了要安全的申请和释放内存外,还应该做到内存的最小化使用,因为它可能要处理每秒钟数以千计的请求,为了提高系统整体的性能,每一次操作都应该只使用最少的内存,对于不必要的相同数据的复制则应该能免则免。我们来看下面这段PHP代码:
PHP内核
2019-04-25 15:55:00
11
0
0
PHP底层对内存的管理, 围绕着小块内存列表(free_buckets)、 大块内存列表(large_free_buckets)和 剩余内存列表(rest_buckets)三个列表来分层进行的。 ZendMM向系统进行的内存申请,并不是有需要时向系统即时申请, 而是由ZendMM的最底层(heap层)先向系统申请一大块的内存,通过对上面三种列表的填充, 建立一个类似于内存池的管理机制。 在程序
PHP内核
2019-04-25 15:54:24
10
0
0
内存管理一般会包括以下内容:
- 是否有足够的内存供我们的程序使用;
- 如何从足够可用的内存中获取部分内存;
- 对于使用后的内存,是否可以将其销毁并将其重新分配给其它程序使用。
与此对应,PHP的内容管理也包含这样的内容,只是这些内容在ZEND内核中是以宏的形式作为接口提供给外部使用。 后面两个操作分别对应emalloc宏,efree宏,而第一个操作可以根据emalloc宏返回结果检测。
PHP内核
2019-04-25 15:52:55
7
0
0
在PHP里,我们可以定义字符串变量,比如 <?php $str="nowamagic"; ?>,$str这个字符串变量可以被自由的修改与复制等。这一切在C语言里看起来都是不可能的事情,我们用#char *p = "hello";#来定义一个字符串,但它是常量,是不能被修改的,如果你用p[1]='c';来修改这个字符串会引发段错误(Gcc,c99),为了修改C语言里的字符串常量,我们往往需要定义
PHP内核
2019-04-25 15:52:06
10
0
0
内存是计算机非常关键的部件之一,是暂时存储程序以及数据的空间,CPU只有有限的寄存器可以用于存储计算数据,而大部分的数据都是存储在内存中的,程序运行都是在内存中进行的。和CPU计算能力一样, 内存也是决定计算效率的一个关键部分。
计算中的资源中主要包含:CPU计算能力,内存资源以及I/O。现代计算机为了充分利用资源, 而出现了多任务操作系统,通过进程调度来共享CPU计算资源,通过虚拟存储来分享内存存储能力。 本章的内存管理中不会介绍操作系统级别的虚拟存储技术,而是关注在应用层面: 如何高效的利用有限的内存资源。
目前除了使用C/C++等这类的低层编程语言以外,很多编程语言都将内存管理移到了语言之后, 例如Java, 各种脚本语言:PHP/Python/Ruby等等,程序手动维护内存的成本非常大, 而这些脚本语言或新型语言都专注于特定领域,这样能将程序员从内存管理中解放出来专注于业务的实现。 虽然程序员不需要手动维护内存,而在程序运行过程中内存的使用还是要进行管理的, 内存管理的工作也就编程语言实现程序员的工作了。
内存管理的主要工作是尽可能高效的利用内存。
内存的使用操作包括申请内存,销毁内存,修改内存的大小等。 如果申请了内存在使用完后没有及时释放则可能会造成内存泄露,如果这种情况出现在常驻程序中, 久而久之,程序会把机器的内存耗光。所以对于类似于PHP这样没有低层内存管理的语言来说, 内存管理是其至关重要的一个模块,它在很大程序上决定了程序的执行效率,
在PHP层面来看,定义的变量、类、函数等等实体在运行过程中都会涉及到内存的申请和释放, 例如变量可能会在超出作用域后会进行销毁,在计算过程中会产生的临时数据等都会有内存操作, 像类对象,函数定义等数据则会在请求结束之后才会被释放。在这过程中合适申请内存合适释放内存就比较关键了。 PHP从开始就有一套属于自己的内存管理机制,在5.3之前使用的是经典的引用计数技术, 但引用技术存在一定的技术缺陷,在PHP5.3之后,引入了新的垃圾回收机制,至此,PHP的内存管理机制更加完善。
从某个意义上讲,资源总是有限的,计算机资源也是如此,衡量一个计算机处理能里的指标有很多, 同时也根据不同的应用需要会有不同的指标,比如3D游戏对显卡就有些要求,而Web服务器对吞吐量及响应时间有要求, 通常CPU、内存及硬盘的读取和计算速度具有决定性的作用
PHP内核
2019-04-25 15:51:38
12
0
0
PHP是弱类型的动态语言,在前面的章节中我们已经介绍了PHP的变量都存放在一个名为ZVAL的容器中, ZVAL包含了变量的类型和各种类型变量的值。 PHP中的变量不需要显式的数据类型定义,可以给变量赋值任意类型的数据, PHP变量之间的数据类型转换有两种:隐式和显式转换。
隐式类型转换
隐式类型转换也被称为自动类型转换,是指不需要程序员书写代码,由编程语言自动完成的类型转换。 在PHP中,我们经常遇到的隐式转换有:
1.直接的变量赋值操作
在PHP中,直接对变量的赋值操作是隐式类型转换最简单的方式,也是我们最常见的一种方式,或许我们已经习以为常,从而没有感觉到变量的变化。 在直接赋值的操作中,变量的数据类型由赋予的值决定,即左值的数据类型由右值的数据类型决定。 比如,当把一个字符串类型的数据赋值给变量时,不管该变量以前是什么类型的变量,此时该变量就是一个字符串类型的变量。 看一段代码:
1 | $string = "To love someone sincerely means to love all the people, to love the world and life, too." |
上面的代码,当执行完第三行代码,$string变量的类型就是一个整形了。 通过VLD扩展可以查到第三次赋值操作的中间代码及操作数的类型,再找到赋值的最后实现为zend_assign_to_variable函数。 这在前面的小节中已经详细介绍过了。我们这个例子是很简单的一种赋值,在源码中是直接将$string的ZVAL容器的指针指向$integer变量指向的指针, 并将$integer的引用计数加1。这个操作在本质上改变了$string变量的内容,而原有的变量内容则被垃圾收集机制回收。关于赋值的具体细节,请返回上一节查看。
2.运算式结果对变量的赋值操作
我们常说的隐式类型转换是将一个表达式的结果赋值给一个变量,在运算的过程中发生了隐式的类型转换。 这种类型转换不仅仅在PHP语言,在其它众多的语言中也有见到,这是我们常规意义上的隐式类型转换。 这种类型转换又分为两种情况:
- 表达式的操作数为同一数据类型 这种情况的作用以上面的直接变量的类型转换是同一种情况,只是此时右值变成了表达式的运算结果。
- 表达式的操作数不为同的数据类型 这种情况
PHP内核
2019-04-25 15:51:05
5
0
0
global语句的作用是定义全局变量,例如如果想在函数内访问全局作用域内的变量则可以通过global声明来定义。 下面从语法解释开始分析。
1. 词法解析
查看 Zend/zend_language_scanner.l文件,搜索 global关键字。我们可以找到如下代码:
1 | <ST_IN_SCRIPTING> "global" { |
2. 语法解析
在词法解析完后,获得了token,此时通过这个token,我们去Zend/zend_language_parser.y文件中查找。找到相关代码如下:
1 | | T_GLOBAL global_var_list ';' |
4 | global_var_list ',' global_var { zend_do_fetch_global_variable(&$3, NULL, ZEND_FETCH_GLOBAL_LOCK TSRMLS_CC); } |
5 | | global_var { zend_do_fetch_global_variable(&$1, NULL, ZEND_FETCH_GLOBAL_LOCK TSRMLS_CC); } |
上面代码中的$3是指global_var(如果不清楚yacc的语法,可以查阅yacc入门类的文章。)
从上面的代码可以知道,对于全局变量的声明调用的是zend_do_fetch_global_variable函数,查找此函数的实现在Zend/zend_compile.c文件。
01 | void zend_do_fetch_global_variable(znode *varname, const znode *static_assignment, int fetch_type TSRMLS_DC) |
04 | opline->opcode = ZEND_FETCH_W; |
05 | opline->result.op_type = |