php的擴展和嵌入–php內存管理

php對內存的管理機制相當的詳盡,它在這一點上更類似與java的垃圾回收機制。而對於c語言或者c++大部分時候都隻能由程式員自己把申請的空間釋放掉。在php中,由於要應對成千上萬的連接,同時這些連接往往還需要保持很長的時間。這並不同於c中程式結束瞭相應的內存塊就會被回收。

所以僅僅依靠程式員在寫程式的時候註意內存回收是不夠的,php肯定要有一些自己內部的、與連接相關的內存管理機制來保證不發生任何的內存泄露。

在本文中,首先對php的內存機制進行一個介紹:

那些在c語言中的空間函數,比如malloc() free() strdup() realloc() calloc(),php中會有不同的形式。

返還申請的內存:對於程式員來說,每一塊申請的內存都應該返還,如果不還就會導致內存泄漏。在那些不要求一直運行的程式中,稍許的內存泄漏在整個進程被殺掉之後就結束瞭。但是類似於apache這種一直運行的web
server,小的內存泄漏最終會導致程式的崩潰。

錯誤處理的例子

在進行錯誤處理的時候,采用的機制一般是是Zend Engine會設定一個跳出地址,一旦發生exit或die或任何嚴重錯誤E_ERROR的時候,就會利用一個longjmp()跳到這個地址上面去。但是這種做法幾乎都會導致內存泄漏。因為free的操作都會被跳掉。(這個問題在c++裡面也同樣存在,就是在設計類的時候,絕不要把錯誤處理或告警函數寫在構造或者析構函數內,同樣的原因,由於對象已經處在瞭銷毀或創建的階段,所以任何錯誤函數處理都可能打斷這一過程,從而可能導致內存泄漏。)
下面的代碼中就給出瞭這樣的一個例子:

void call_function(const char *fname, int fname_len TSRMLS_DC)
{
    zend_function *fe;
    char *lcase_fname;
    /* PHP function names are case-insensitive to simplify locating them in the function tables all function names are implicitly
     * translated to lowercase
     */
    lcase_fname = estrndup(fname, fname_len);//創造一個函數名的副本
    zend_str_tolower(lcase_fname, fname_len);//都轉換成小寫,這樣的尋找的時候很方便,這應該也是php函數表中進行函數標識的方式。
    if (zend_hash_find(EG(function_table),
            lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {?SUCCESS。這個是要在函數表裡面尋找待調用的函數。
        zend_execute(fe->op_array TSRMLS_CC);
    } else {
              php_error_docref(NULL TSRMLS_CC, E_ERROR,
                         "Call to undefined function: %s()", fname); //等同於Trigger_error() 
    }
    efree(lcase_fname);
}

在這個例子中,提供瞭一個php在調用函數時候的功能。當php調用函數時,需要到函數表也就是function_table中去尋找相應的函數,而在尋找之前要先轉換到小寫字母,這樣在尋找的時候可以提高查找的效率。
而通過zend_hash_find函數如果找到瞭要調用的函數,就使用zend_execute進行調用。而如果沒找到的haunted就要跳出報錯,顯示沒找到。但是問題來瞭,註意之前為瞭尋找函數創建瞭一個小寫版本的函數名字符串。這個字符串一直到用到zend_hash_find函數,一旦沒找到進入瞭報錯之後,那麼這個字符串所對應的內存空間必然就找不回來瞭,這就造成瞭內存的泄露。

因此,php提供瞭Zend內存管理,Zend memory management也稱為ZendMM。

  • php中的內存管理與操作系統的機制類似,但是對象是針對每一個請求所涉及的內存的。
  • 除此之外ZendMM還會控制ini文件裡面規定的memZ喎?/kf/ware/vc/” target=”_blank” class=”keylink”>vcnlfbGltaXSjrNKyvs3Kx8u10ru1qcO/uPbH68fzy/nSqsfztcTE2rTms6y5/cHL1eK49m1lbW9yeSBsaW1pdKOsxMfDtNKyu+HJ6sfryqew3KGjPGxpPtTazbzW0LXE1+7PwsPmv7S1vcHLy/zT67LZ1/fPtc2zz+DBqs+1tcTSu7LjoaPV67bUstnX98+1zbPW0LXEserXvLXExNq05snqx+u6zcrNt8W1xLe9t6ijrHBocNbQtrzT0LbU06a1xLqvyv2ho9Xi0Km6r8r9sqKyu8rH0ru49rzytaW1xMzmu7ujrMv8w8fW0LD8uqzT0MzYtqi1xNDFz6KjrNTa1eLQqdDFz6K1xLDv1vrPwr7NxNy5u7DRw7+49sfrx/PL+cnqx+u1xMTatOa/6b340NCx6sq2oaPV4tH5vs3E3Lm7yrXP1rbUw7+49sfrx/O1xMTatObH+NPyvfjQ0LfWsfC1xLncwO2hozxsaT7NrMqx1NrNvNbQv7S1vcHL0ru5ssG91tbE2rTmx+vH87XEt73KvaO6cGVyc2lzdGVudLrNcGVyLXJlcXVlc3SjrLbU09pwZXJzaXN0ZW50wLTLtbLusru24Lj6z7XNs7XEx+vH877N0rvR+cHLo6zSsr7NysfLtcrHtsDBotTaw7/Su7j2x+vH89auzeK1xKOssru74dTax+vH873hyvjWrrrzsbu72MrVoaO1q8rH09DKsbryyse38XBlcnNpc3RlbnS/ycTc0qpydW50aW1lssXE3NaqtcCjrMv50tTU2tXi1tbH6b/2z8KjrNDo0qrSu7j2ZmxhZ8C01rjKvtXi0ru146GjttTT2srHt/HKx3BlcnNpc3RlbnSjrL340NDE2rTmx+vH87XEt73KvcrHsrvSu9H5tcSho8/Cw+a4+LP2ttTTprnYz7WjugoKPHVsPgo8bGk+cGVtYWxsb2MoYnVmZmVyX2xlbiwxKSA9PSBtYWxsb2MoYnVmZmVyX2xlbik8bGk+cGVybWFsbG9jKGJ1ZmZlcl9sZW4sMCkgPT0gZW1hbGxvYyhidWZmZXJfbGVuKdXi1tbBqs+1ysfTw7rqtqjS5bXEt73Kvb72tqi1xDxsaT4jZGVmaW5lIHBlbWFsbG9jKHNpemUscGVyc2lzdGVudCkgXDxsaT4KCgo8bGk+Cjx1bD4KKChwZXJzaXN0ZW50KT9tYWxsb2Moc2l6ZSk6ZW1hbGxvYyhzaXplKSkKCmZsYWc9MbHtyr7Kx3BlcnNpc3RlbnS1xKOszqowse3KvrK7ysejrL7NuPrSu7DjtcS4vcr009rH68fztcRlbWFsbG9j0rvR+cHLoaMKPGJyPgoKCjxicj4KCs/CzbzW0L/J0tS/tLW9z7XNs7XExNq05snqx+u6r8r90+twaHDW0LXExNq05snqx+u6r8r9tcS21LHI16q7u828o7oKCjxpbWcgc3JjPQ==”/uploadfile/Collfiles/20131213/20131213091641239.jpg” alt=”\”>

    如果你對malloc、calloc和realloc這些函數還不太熟悉,請移步:
    https://www.cppblog.com/sandywin/archive/2011/09/14/155746.html

    除此之外,還有兩個安全模式的內存函數

    void *safe_emalloc(size_t size, size_t count, size_t addtl);
    void *safe_pemalloc(size_t size, size_t count, size_t addtl, char persistent);
    他們申請的空間是size*count + addtl,存在的原因是為瞭避免int型的溢出。

    接下來說一個更有趣的,php中的引用計數

    很多語言中都有引用,很多時候也都會使用引用。通過引用可以節省空間,因為有時候並沒有必要為每個變量都制造一個副本。
    所謂引用計數,就是指同一塊內存空間被多少個變量引用瞭,從而避免可能的內存錯誤操作。
    先看下面的一段代碼:

    	* {
    	*     zval *helloval;
    	*     MAKE_STD_ZVAL(helloval);
    	*     ZVAL_STRING(helloval, "Hello World", 1);
    	*     zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),
    	*                                            &helloval, sizeof(zval*), NULL);
    	*     zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),
    	*                                            &helloval, sizeof(zval*), NULL);
    	* }
    

    這段代碼首先聲明瞭一個zval變量,再用MAKE_STD_ZVAL進行瞭初始化,接下來用ZVAL_STRING附瞭初值。然後對這個變量,給出瞭兩個變量名。第一個是a,第二個是b,毫無疑問,第二個肯定是一個引用。但是這段代碼這麼寫肯定有問題,問題就在於你在用zend_hash_add之後並沒有更新相應的引用計數。zend並不知道你多加瞭這麼一個引用,這就導致釋放內存的時候可能導致兩次釋放。所以經過修改之後的正確代碼如下:

    	* {
    	*     zval *helloval;
    	*     MAKE_STD_ZVAL(helloval);
    	*     ZVAL_STRING(helloval, "Hello World", 1);
    	*     zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),
    	*                                            &helloval, sizeof(zval*), NULL);
    	*     ZVAL_ADDREF(helloval);//加上這個之後,就不會有重新釋放同一塊內存空間這樣的錯誤瞭
    	*     zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),
    	*                                            &helloval, sizeof(zval*), NULL);
    	* }
    

    進行瞭ZVAL_ADDREF之後,下一次unset變量的時候,會先查看ref_count引用計數,如果=1就釋放,如果>1就隻是-1,並不進行內存釋放。

    Copy on Write
    再來看下面的這一段php代碼:

    
    

    很顯然在第二行的時候b聲明瞭一個a的引用,那麼在執行完瞭第三行的代碼之後,b增加瞭,a增不增加呢?很多時候可能並不想增加。所以這個時候當Zend檢測到refCount>1之後,就會執行一個變量分離的操作,把原來的一塊內存變成兩塊內存:

    zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
    {
        zval **varval, *varcopy;
        if (zend_hash_find(EG(active_symbol_table),
                           varname, varname_len + 1, (void**)&varval) == FAILURE) {
           /*符號表裡沒找到 */
           return NULL;
       }
       if ((*varval)->refcount refcount = 1;
           varcopy->is_ref = 0;
           zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,
                                            &varcopy, sizeof(zval*), NULL);
       /* Return the new zval* */
       return varcopy;
    }

    首先看到瞭兩個判斷語句,第一個判斷語句先在符號表裡面看看有沒有找到相應的變量,如果沒找到也就沒必要分離瞭。第二個判斷語句是看輸入的變量的引用次數是不是小於2,如果是的話那就說明輸入變量*varval是唯一的,也沒必要分離。
    否則的話肯定有引用,這個時候就要制作一個副本varcopy。這個副本會承襲varname對應的值,但是不同之處在於幫它重新申請瞭內存空間,重新初始化瞭refcount和is_ref參數。
    以a、b為例,在$b+=5,執行之後,b作為varname去尋找是否有引用,發現還有一個引用a,這個時候就把b的值拷出來,然後重新申請一片空間,在重新註冊為b。這樣的話就是兩塊獨立的內存塊瞭。

    Change on Write
    再看一個代碼片段:

    
    

    如果你覺得想要a跟著b一起改變,那沒有問題,隻要顯式的用&符號進行引用聲明就可以瞭。這樣的話is_ref標志位就會被置1. 這時候也就沒必要進行內存塊的分離瞭。所以在上面的代碼中要把第二個if語句的判斷更改一下:

    if ((*varval)->is_ref || (*varval)->refcount < 2) {
        /* varname is the only actual reference,
         * or it's a full reference to other variables
         * either way: no separating to be done
         */
        return *varval;
    }

    再看最後一種情況,這種情況最糾結:

    
    

    既不是copy on write也不是change on wirte,那沒辦法瞭,隻好分離一下。這裡隻好b獨立出來瞭:

    對php內存管理的一些機制就說到這裡,感覺php確實是一門相當神奇的語言。哈哈。

You May Also Like