在研读《php7底层设计与源码实现》这本书时,在作者介绍zend_string_extend函数的时候又卡壳了,原文是这样的:
zend_string_extend函数可以对字符串进行扩容。之所以会介绍这个函数,是因为它除了实现了扩容之外还能很好地体现写时分离的思想。
static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, int persistent)
{
zend_string *ret;
ZEND_ASSERT(len >= ZSTR_LEN(s));
if (!ZSTR_IS_INTERNED(s)) {
if (EXPECTED(GC_REFCOUNT(s) == 1)) {
ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
ZSTR_LEN(ret) = len;
zend_string_forget_hash_val(ret);
return ret;
} else {
GC_DELREF(s);
}
}
ret = zend_string_alloc(len, persistent);
memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), ZSTR_LEN(s) + 1);
return ret;
}
以上就是zend_string_extend函数的全貌。
说到扩容,词义很好理解,就是根据新的长度申请新的内存空间。
第1步:当需扩容的字符串是普通字符串且refcount等于1时,直接调用perealloc函数分配内存,扩容一步到位。
注意:perealloc函数,当参数persistent=1时调用系统函数realloc申请内存;当persistent! =1时调用PHP的内存池的erealloc函数申请内存。两者实现的功能相似,以realloc函数的作用为例,它会先判断当前的指针是否有足够的连续空间。如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放的,不需要使用free函数),同时返回新分配的内存区域的首地址。
第2步:当需扩容的字符串引用计数大于1或类型为内部字符串时,则调用zend_string_alloc函数申请一块新内存,并把原值拷贝进去。对于普通字符串还需要对老字符串进行refcout--操作。经过上面两个步骤就完成了字符串的扩容,而第2步中的扩容实际就是分离过程,当refcount>1,通过申请新内存及拷贝值等操作,生成两份不关联的字符串数据。
上面为了阅读方便和自己加深记忆和理解,就把原文引用到上放了。
这里我当时不理解的是,为什么使用这个扩容函数就能做到写时分离呢?

- 第一步:调用zend_string_extend函数进行扩容/写时分离,先判断该字符串类型是否是内部字符串类型,如果是普通字符串类型,然后再接着判断该普通字符串当前引用计数是否为1,如果为1,则直接调用perealloc函数进行扩容即可。
简单分析:
首先需要说明的是,内部字符串主要指的是PHP已知字符串、PHP代码中的字面量、标识符等字符串,这些字符串是不会被垃圾回收的,所以也就不用考虑refcount了,直接跳出if就好;
其次,就是为什么当refcount=1时直接扩容,而大于1时却需要refcount-1?个人理解是,如果refocunt=1,则说明没有其他zval引用该字符串,所以这个时候调用zend_string_extend函数,本质上其实就是单纯的扩容;而当refcount>1又说明,该字符串被多个zval引用着,这个时候调用zend_string_extend就不单单是扩容那么简单了,本质其实就是写时分离,引用该字符串的某个zval要独立了。 - 第二步:如果是需要扩容的字符串是内部字符串,或者,在第一步中已经知道这个字符串是普通字符串,并且refcount>1,然后已经进行refcount-1操作后,接下来就是进行写时复制,先申请新内存地址,然后将原字符串的val拷贝到新内存中。至此,就完成了完整的写时分离。
简单分析:
这里我的理解是只有refcount>1的普通字符串才涉及写时复制的概念。
如果值没改变仅是复制了,会出现写时分离吗?
个人开始的时候认为应该不会出现写时分离,因为个人觉得值没改变那么就以用同一字符串岂不是更搞笑么。事实却是出现了写时分离,实践如下:
<?php
$tmp = 'cow';
$a = 'string' . $tmp;
$b = $a;
$b = 'stringcow';
可以看出,$b和之前$a的值是一模一样的,然后通过gdb进行调试,结果如下:

$b = $a;进行赋值操作后的结果,zend_string的gc引用计数是2,变量a和变量b都指向该zend_string,值为“stringcow”;


$b = 'stringcow';之后的变量a的情况,第二章则是变量b的情况,通过str的地址值可以得出,此时已经指向两个zend_string结构体了,而变量a指向的zend_string的refcount已经变成1,变量b指向的zend_string的refcount也为1,但是type_info的值已经跟变量a的不一样了。
所以,综上,只要后来发生了赋值操作,都会触发写时复制。只有zval为string、array、resource时,才会有写时分离,对象、传址引用等不支持。