c和php的最主要區(qū)別:是否控制內(nèi)存指針。
內(nèi)存管理
? ? 在php內(nèi)核層,每次都做到及時釋放,這是相當難的事情。php語言內(nèi)核細節(jié)太多,還有版本兼容等等,寫擴展的我們,其實只需要用php提供的工具去做就好了,沒必要讀源碼里的各個實現(xiàn)細節(jié),實在費力不討好。
因此,在php內(nèi)核里申請和釋放內(nèi)存,不使用c語言的malloc這些函數(shù),而是使用php提供的宏。void *emalloc()
? ? ? ?有種情況:當php執(zhí)行時候,遇到了錯誤,就會die掉,此時的實現(xiàn),應該類似longjmp,跳到設置好的地址,估計就是goto。這種情況,會導致:遇到錯誤時候,直接跳出,后面的釋放內(nèi)存的步驟沒有執(zhí)行,就導致內(nèi)存泄露了。 所以,PHP有一個zendMM引擎,扮演這類似OS的作用,如果我們使用zendMM提供的宏申請內(nèi)存,如果出現(xiàn)問題,內(nèi)存會被主動回收。?
有時候,我們需要一些持久內(nèi)存,請求結束也不被回收,這時候,可以使用傳統(tǒng)的malloc等函數(shù)進行內(nèi)存分配。但是有種特殊情況,比如運行之后,根據(jù)程序的邏輯條件,判斷,才知道是否要持久分配。 所以呀,zendMM有對應的宏:(Zend/zend_alloc.h)
#define pemalloc(size, persistent) ?((persistent)?malloc(size): emalloc(size))
第二個參數(shù)是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也是需要持久化參數(shù)的。如果對非永久內(nèi)存使用free,會導致雙倍釋放,在持久內(nèi)存上使用efree,會導致段錯誤。所以,就不要用malloc分配,只用emalloc,和pemalloc。釋放的時候,代碼要記住,內(nèi)存是不是持久非配,然后選擇對應的efree或者pefree(這里有點歧義,2本書上說的不一樣,等弄明白,坐實驗才知道了)
還有一種是safe_emalloc*的宏。這種比可以防止內(nèi)存溢出。
引用計數(shù)
平時多少也了解了些,php底層的變量是共享的,賦值是引用。
$a = "hello world";
$b = $a;
unset($a);
$a賦值給$b,php有做優(yōu)化,不是簡單的copy。 而是讓b也指向"hello world",這時候引用計數(shù)是2,有2個變量引用他。unset時候,只減少引用計數(shù),只有當引用計數(shù)為0時候,才真正的釋放內(nèi)存。
有了引用計數(shù)的優(yōu)化,還需要一些策略來應對特殊情況:
$a = 1;
$b = $a;
$b += 5;
第二行,b和a同時引用1,第三行,a進行了+=操作,如果此時把a和b指向的值1進行和操作,那么會導致b出現(xiàn)錯誤。b和a都會變成6,。所以,這時候有一種情況叫寫時復制
當進行寫操作時候,如果引用計數(shù)<2,說明只有一個變量在引用。 ?否則,說明有多個變量共享,此時,b要進行寫,應該拷貝一份值,獨立出去,這時候,a和b是分別引用的不同的值,此時b就可以進行+=操作了。
C語言實現(xiàn):
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{// 參數(shù)是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的引用計數(shù)減小1 */
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/* 初始化新創(chuàng)建的值的引用計數(shù), 并為新創(chuàng)建的值和varname建立關聯(lián) */
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引擎如果實現(xiàn)呢,其實和上面寫時復制的原理一毛一樣,但是在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本身已經(jīng)是寫時復制機制了,在進行寫時修改,會讓內(nèi)核參數(shù)歧義,因此。這種情況下,內(nèi)核會copy出一份獨立的空間,a和b是寫時修改,b寫時復制
還有一個灰常重要的一點,當一個變量傳遞到函數(shù)里面時候呢,ref_count 一定>=2,一份是自己,還有一個是傳遞給函數(shù)的拷貝。(php里,如果傳參數(shù)到函數(shù)里面,是局部變量,既然是局部變量,說明函數(shù)本身拷貝了一份,因此,這一步的實現(xiàn),是由寫時完成的),后面有接收參數(shù)的方法,那里會用上。也就是說,接收參數(shù)時候,如果需要對這個參數(shù)進行修改,那么要記得在代碼里寫一份寫時復制機制的代碼。內(nèi)核提供了一個修飾符,"/",他會幫我們實現(xiàn)這個寫時復制。