c和php的最主要区别:是否控制内存指针。
内存管理
在php内核层,每次都做到及时释放,这是相当难的事情。php语言内核细节太多,还有版本兼容等等,写扩展的我们,其实只需要用php提供的工具去做就好了,没必要读源码里的各个实现细节,实在费力不讨好。
因此,在php内核里申请和释放内存,不使用c语言的malloc这些函数,而是使用php提供的宏。void *emalloc()
有种情况:当php执行时候,遇到了错误,就会die掉,此时的实现,应该类似longjmp,跳到设置好的地址,估计就是goto。这种情况,会导致:遇到错误时候,直接跳出,后面的释放内存的步骤没有执行,就导致内存泄露了。 所以,PHP有一个zendMM引擎,扮演这类似OS的作用,如果我们使用zendMM提供的宏申请内存,如果出现问题,内存会被主动回收。
有时候,我们需要一些持久内存,请求结束也不被回收,这时候,可以使用传统的malloc等函数进行内存分配。但是有种特殊情况,比如运行之后,根据程序的逻辑条件,判断,才知道是否要持久分配。 所以呀,zendMM有对应的宏:(Zend/zend_alloc.h)
#define pemalloc(size, persistent) ((persistent)?malloc(size): emalloc(size))
第二个参数是0和1,表示是否持久分配。
void *malloc(size_t count); void *emalloc(size_t count);
void *pemalloc(size_t count, char persistent);
void *calloc(size_t count); void *ecalloc(size_t count); void *pecalloc(size_t count, char persistent);
void *realloc(void *ptr, size_tcount); void *erealloc(void *ptr, size_t count); void *perealloc(void *ptr, size_t count, char persistent);
void *strdup(void *ptr); void *estrdup(void *ptr); void *pestrdup(void *ptr, char persistent);
void free(void *ptr); void efree(void *ptr); void pefree(void *ptr, char persistent);
pefree也是需要持久化参数的。如果对非永久内存使用free,会导致双倍释放,在持久内存上使用efree,会导致段错误。所以,就不要用malloc分配,只用emalloc,和pemalloc。释放的时候,代码要记住,内存是不是持久非配,然后选择对应的efree或者pefree(这里有点歧义,2本书上说的不一样,等弄明白,坐实验才知道了)
还有一种是safe_emalloc*的宏。这种比可以防止内存溢出。
引用计数
平时多少也了解了些,php底层的变量是共享的,赋值是引用。
$a = "hello world";
$b = $a;
unset($a);
$a赋值给$b,php有做优化,不是简单的copy。 而是让b也指向"hello world",这时候引用计数是2,有2个变量引用他。unset时候,只减少引用计数,只有当引用计数为0时候,才真正的释放内存。
有了引用计数的优化,还需要一些策略来应对特殊情况:
$a = 1;
$b = $a;
$b += 5;
第二行,b和a同时引用1,第三行,a进行了+=操作,如果此时把a和b指向的值1进行和操作,那么会导致b出现错误。b和a都会变成6,。所以,这时候有一种情况叫写时复制
当进行写操作时候,如果引用计数<2,说明只有一个变量在引用。 否则,说明有多个变量共享,此时,b要进行写,应该拷贝一份值,独立出去,这时候,a和b是分别引用的不同的值,此时b就可以进行+=操作了。
C语言实现:
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{// 参数是b
zval **varval, *varcopy;//注意一个是1级指针,一个是2级
if (zend_hash_find(EG(active_symbol_table),
varname, varname_len + 1, (void**)&varval) == FAILURE) {//查找到的变量地址由varval存着
/* 变量不存在 */
return NULL;
}
if ((*varval)->refcount < 2) { //后面还会介绍一种情况
/* 变量名只有一个引用, 不需要隔离 */
return *varval;
}
/* 其他情况, 对zval *做一次浅拷贝 */
MAKE_STD_ZVAL(varcopy);//浅拷贝就是分配一段空间
varcopy = *varval;//
/* 对zval *进行一次深拷贝 */
zval_copy_ctor(varcopy);//深拷贝就是把结构给复制了,复制varcpy指向的地址
/* 破坏varname和varval之间的关系, 这一步会将varval的引用计数减小1 */
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/* 初始化新创建的值的引用计数, 并为新创建的值和varname建立关联 */
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,
&varcopy, sizeof(zval*), NULL);
/* 返回新的zval * */
return varcopy;
}
这时候b = varcopy,与a是独立的空间,此时,在进行b+=5,就可以互不干扰了
$a = 1;$b = &$a;
$b += 5;
这时候呢,在php语法里,b和a都是相同地址的引用,应该是a和b指向相同的地址,并且值变成了6。那么zend引擎如果实现呢,其实和上面写时复制的原理一毛一样,但是在if那多一个判断,如果是引用,那么就不复制隔离。zval结构除了refcount,还有一个is_ref(是否有引用),
if((*varval)->is_ref || (*varval)->ref_count <2){
return *varval;
}
这两种情况,分别是写时复制机制和写时修改机制,任何一个变量,只能使用一种机制。考虑一种情况:
$a = 1;
$b = $a;
$c = &$a;
当$b = $a时候,是写时复制机制
$c = &$a,这时候是写时修改,由于a本身已经是写时复制机制了,在进行写时修改,会让内核参数歧义,因此。这种情况下,内核会copy出一份独立的空间,a和b是写时修改,b写时复制
还有一个灰常重要的一点,当一个变量传递到函数里面时候呢,ref_count 一定>=2,一份是自己,还有一个是传递给函数的拷贝。(php里,如果传参数到函数里面,是局部变量,既然是局部变量,说明函数本身拷贝了一份,因此,这一步的实现,是由写时完成的),后面有接收参数的方法,那里会用上。也就是说,接收参数时候,如果需要对这个参数进行修改,那么要记得在代码里写一份写时复制机制的代码。内核提供了一个修饰符,"/",他会帮我们实现这个写时复制。