程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> [php擴展和嵌入式] -內存管理

[php擴展和嵌入式] -內存管理

編輯:關於PHP編程

內存管理 php和c最重要的區別就是是否控制內存指針. 內存 在php中, 設置一個字符串變量很簡單: <?php $str = 'hello world'; ?>, 字符串可以自由的修改, 拷貝, 移動. 在C中, 則是另外一種方式, 雖然你可以簡單的用靜態字符串初始化: char *str = "hello world"; 但是這個字符串不能被修改, 因為它存在於代碼段. 要創建一個可維護的字符串, 你需要分配一塊內存, 並使用一個strdup()這樣的函數將內容拷貝到其中. [cpp]  {      char *str;         str = strdup("hello world");      if (!str) {          fprintf(stderr, "Unable to allocate memory!");      }   }   傳統的內存管理函數(malloc(), free(), strdup(), realloc(), calloc()等)不會被php的源代碼直接使用, 本章將解釋這麼做的原因. 釋放分配的內存 內存管理在以前的所有平台上都以請求/釋放的方式處理. 應用告訴它的上層(通常是操作系統)"我想要一些內存使用", 如果空間允許, 操作系統提供給程序, 並對提供出去的內存進行一個記錄. 應用使用完內存後, 應該將內存還給OS以使其可以被分配給其他地方. 如果程序沒有還回內存, OS就沒有辦法知道這段內存已經不再使用, 這樣就無法分配給其他進程. 如果一塊內存沒有被釋放, 並且擁有它的應用丟失了對它的句柄, 我們就稱為"洩露", 因為已經沒有人可以直接得到它了. 在典型的客戶端應用中, 小的不頻繁的洩露通常是可以容忍的, 因為進程會在一段時間後終止, 這樣洩露的內存就會被OS回收. 並不是說OS很牛知道洩露的內存, 而是它知道為已經終止的進程分配的內存都不會再使用. 對於長時間運行的服務端守護進程, 包括apache這樣的webserver, 進程被設計為運行很長周期, 通常是無限期的. 因此OS就無法干涉內存使用, 任何程度的洩露無論多小都可能累加到足夠導致系統資源耗盡. 考慮用戶空間的stristr()函數; 為了不區分大小寫查找字符串, 它實際上為haystack和needle各創建了一份小寫的拷貝, 接著執行普通的區分大小寫的搜索去查找相關的偏移量. 在字符串的偏移量被定位後, haystack和needle字符串的小寫版本都不會再使用了. 如果沒有釋放這些拷貝, 那麼每個使用stristr()的腳本每次被調用的時候都會洩露一些內存. 最終, webserver進程會占用整個系統的內存, 但是卻都沒有使用. 完美的解決方案是編寫良好的, 干淨的, 一致的代碼, 保證它們絕對正確. 不過在php解釋器這樣的環境中, 這只是解決方案的一半. 錯誤處理 為了提供從用戶腳本的激活請求和所在的擴展函數中跳出的能力, 需要存在一種方法跳出整個激活請求. Zend引擎中的處理方式是在請求開始的地方設置一個跳出地址, 在所有的die()/exit()調用後, 或者碰到一些關鍵性錯誤(E_ERROR)時, 執行longjmp()轉向到預先設置的跳出地址. 雖然這種跳出處理簡化了程序流程, 但它存在一個問題: 資源清理代碼(比如free()調用)會被跳過, 會因此帶來洩露. 考慮下面簡化的引擎處理函數調用的代碼: [cpp]  void call_function(const char *fname, int fname_len TSRMLS_DC)   {       zend_function *fe;       char *lcase_fname;       /* php函數是大小寫不敏感的, 為了簡化在函數表中對它們的定位, 所有的函數名都隱式的翻譯為小寫 */       lcase_fname = estrndup(fname, fname_len);       zend_str_tolower(lcase_fname, fname_len);          if (zend_hash_find(EG(function_table),               lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {           zend_execute(fe->op_array TSRMLS_CC);       } else {           php_error_docref(NULL TSRMLS_CC, E_ERROR,                            "Call to undefined function: %s()", fname);       }       efree(lcase_fname);   }   當php_error_docref()一行執行到時, 內部的處理器看到錯誤級別是關鍵性的, 就調用longjmp()中斷當前程序流, 離開call_function(), 這樣就不能到達efree(lcase_fname)一行. 那你就可能會想, 把efree()行移動到php_error_docref()上面, 但是如果這個call_function()調用進入第一個條件分支呢(查找到了函數名, 正常執行)? 還有一點, fname自己是一個分配的字符串, 並且它在錯誤消息中被使用, 在使用完之前你不能釋放它. php_error_docref()函數是一個內部等價於trigger_error(). 第一個參數是一個可選的文檔引用, 如果在php.ini中啟用它將被追加到docref.root後面. 第三個參數可以是任意的E_*族常量標記錯誤的嚴重程度. 第四個和後面的參數是符合printf()樣式的格式串和可變參列表. Zend內存管理 由於請求跳出(故障)產生的內存洩露的解決方案是Zend內存管理(ZendMM)層. 引擎的這一部分扮演了相當於操作系統通常扮演的角色, 分配內存給調用應用. 不同的是, 站在進程空間請求的認知角度, 它足夠底層, 當請求die的時候, 它可以執行和OS在進程die時所做的相同的事情. 也就是說它會隱式的釋放所有請求擁有的內存空間. 下圖展示了在php進程中ZendMM和OS的關系: 除了提供隱式的內存清理, ZendMM還通過php.ini的設置memory_limit控制了每個請求的內存使用. 如果腳本嘗試請求超過系統允許的, 或超過單進程內存限制剩余量的內存, ZendMM會自動的引發一個E_ERROR消息, 並開始跳出進程. 一個額外的好處是多數時候內存分配的結果不需要檢查, 因為如果失敗會立即longjmp()跳出到引擎的終止部分. 在php內部代碼和OS真實的內存管理層之間hook的完成, 最復雜的是要求所有內部的內存分配要從一組函數中選擇. 例如, 分配一個16字節的內存塊不是使用malloc(16), php代碼應該使用emalloc(16). 除了執行真正的內存分配任務, ZendMM還要標記內存塊所綁定請求的相關信息, 以便在請求被故障跳出時, ZendMM可以隱式的釋放它(分配的內存). 很多時候內存需要分配, 並使用超過單請求生命周期的時間. 這種分配我們稱為持久化分配, 因為它們在請求結束後持久的存在, 可以使用傳統的內存分配器執行分配, 因為它們不可以被ZendMM打上每個請求的信息. 有時, 只有在運行時才能知道特定的分配需要持久化還是不需要, 因此ZendMM暴露了一些幫助宏, 由它們來替代其他的內存分配函數, 但是在末尾增加了附加的參數來標記是否持久化. 如果你真的想要持久化的分配, 這個參數應該被設置為1, 這種情況下內存分配的請求將會傳遞給傳統的malloc()族分配器. 如果運行時邏輯確定這個塊不需要持久化 則這個參數被設置為0, 調用將會被轉向到單請求內存分配器函數. 例如, pemalloc(buffer_len, 1)映射到malloc(buffer_len), 而pemalloc(buffer_len, 0)映射到emalloc(buffer_len), 如下: [cpp]   #define in Zend/zend_alloc.h:      #define pemalloc(size, persistent) \               ((persistent)?malloc(size): emalloc(size))   ZendMM提供的分配器函數列表如下, 並列出了它們對應的傳統分配器.   傳統分配器 php中的分配器 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_t count); 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要求傳遞持久化標記. 這是因為在pefree()調用時, 它並不知道ptr是否是持久分配的. 在廢持久分配的指針上調用free()可能導致雙重的free, 而在持久化的分配上調用efree()通常會導致段錯誤, 因為內存管理器會嘗試查看管理信息, 而它不存在. 你的代碼需要記住它分配的數據結構是不是持久化的. 除了核心的分配器外, ZendMM還增加了特殊的函數: [cpp]   void *estrndup(void *ptr, int len);   它分配len + 1字節的內存, 並從ptr拷貝len個字節到新分配的塊中. estrndup()的行為大致如下: [cpp]   void *estrndup(void *ptr, int len)   {       char *dst = emalloc(len + 1);       memcpy(dst, ptr, len);       dst[len] = 0;       return dst;   }   終止NULL字節被悄悄的放到了緩沖區末尾, 這樣做確保了所有使用estrndup()進行字符串賦值的函數不用擔心將結果緩沖區傳遞給期望NULL終止字符串的函數(比如printf())時產生錯誤. 在使用estrndup()拷貝非字符串數據時, 這個最後一個字節將被浪費, 但是相比帶來的方便, 這點小浪費就不算什麼了. [cpp]   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)的結果. 你可能會問, "為什麼要擴充這樣一個函數? 為什麼不是使用emalloc/pemalloc, 然後自己計算呢?" 理由來源於它的名字"安全". 盡管這種情況很少有可能發生, 但仍然是有可能的, 當計算的結果溢出所在主機平台的整型限制時, 結果會很糟糕. 可能導致分配負的字節數, 更糟的是分配一個正值的內存大小, 但卻小於所請求的大小. safe_emalloc()通過檢查整型溢出避免了這種類型的陷阱, 如果發生溢出, 它會顯式的報告失敗. 並不是所有的內存分配例程都有p*副本. 例如, pestrndup()和safe_pemalloc()在php 5.1之前就不存在. 有時你需要在ZendAPI的這些不足上工作. 引用計數 在php這樣長時間運行的多請求進程中謹慎的分配和釋放內存非常重要, 但這只是一半工作. 為了讓高並發的服務器更加高效, 每個請求需要使用盡可能少的內存, 最小化不需要的數據拷貝. 考慮下面的php代碼片段: [php]   <?php       $a = 'Hello World';       $b = $a;       unset($a);   ?>   在第一次調用後, 一個變量被創建, 它被賦予12字節的內存塊, 保存了字符串"Hello world"以及結尾的NULL. 現在來看第二句: $b被設置為和$a相同的值, 接著$a被unset(釋放) 如果php認為每個變量賦值都需要拷貝變量的內容, 那麼在數據拷貝期間就需要額外的12字節拷貝重復的字符串, 以及額外的處理器負載. 在第三行出現的時候, 這種行為看起來就有些可笑了, 原來的變量被卸載使得數據的復制完全不需要. 現在我們更進一步想想當兩個變量中被裝載的是一個10MB文件的內容時, 會發生什麼? 它需要20MB的內存, 然而只要10MB就足夠了. 引擎真的會做這種無用功浪費這麼多的時間和內存嗎? 你知道php是很聰明的. 還記得嗎? 在引擎中變量名和它的值是兩個不同的概念. 它的值是自身是一個沒有名字的zval *. 使用zend_hash_add()將它賦值給變量$a. 那麼兩個變量名指向相同的值可以嗎? [cpp]   {       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);   }   此時, 在你檢查$a或$b的時候, 你可以看到, 它們實際都包含了字符串"Hello World". 不幸的是, 接著來了第三行: unset($a);. 這種情況下, unset()並不知道$a指向的數據還被另外一個名字引用, 它只是釋放掉內存. 任何後續對$b的訪問都將查看已經被釋放的內存空間, 這將導致引擎崩潰. 當然, 你並不希望引擎崩潰. 這通過zval的第三個成員: refcount解決. 當一個變量第一次被創建時, 它的refcount被初始化為1, 因為我們認為只有創建時的那個變量指向它. 當你的代碼執行到將helloval賦值給$b時, 它需要將refcount增加到2, 因為這個值現在被兩個變量"引用" [cpp]  {       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);   }   現在, 當unset()刪除變量的$a拷貝時, 它通過refcount看到還有別人對這個數據感興趣, 因此它只是將refcount減1, 其他什麼事情都不做. 寫時復制 通過引用計數節省內存是一個很好的主意, 但是當你只想修改其中一個變量時該怎麼辦呢? 考慮下面的代碼片段: [php]   <?php       $a = 1;       $b = $a;       $b += 5;   ?>   看上面代碼的邏輯, 處理完後期望$a仍然等於1, 而$b等於6. 現在你知道, Zend為了最大化節省內存, 在第二行代碼執行後$a和$b只想同一個zval, 那麼到達第三行代碼時會發生什麼呢? $b也會被修改嗎? 答案是Zend查看refcount, 看到它大於1, 就對它進行了隔離. Zend引擎中的隔離是破壞一個引用對, 它和你剛才看到的處理是對立的: [cpp]  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 < 2) {          /* 變量名只有一個引用, 不需要隔離 */          return *varval;      }      /* 其他情況, 對zval *做一次淺拷貝 */      MAKE_STD_ZVAL(varcopy);      varcopy = *varval;      /* 對zval *進行一次深拷貝 */      zval_copy_ctor(varcopy);         /* 破壞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變量引用的zval *, 就可以將它轉換為long, 並將它的值按照腳本請求增加5. 寫時修改 引用計數的概念還創建了一種新的數據維護方式, 用戶空間腳本將這種方式稱為"引用". 考慮下面的用戶空間代碼片段: [php]   <?php       $a = 1;       $b = &$a;       $b += 5;   ?>   憑借你在php方面的經驗, 直覺上你可能認識到$a的值現在應該是6, 即便它被初始化為1並沒有被(直接)修改過. 發生這種情況是因為在引擎將$b的值增加5的時候, 它注意到$b是$a的一個引用, 它就說"對於我來說不隔離它的值就修改是沒有問題的, 因為我原本就想要所有的引用變量都看到變更" 但是引擎怎麼知道呢? 很簡單, 它查看zval結構的最後一個元素: is_ref. 它只是一個簡單的開關, 定義了zval是值還是用戶空間中的引用. 在前面的代碼片段中, 第一行執行後, 為$a創建的zval, refcount是1, is_ref是0, 因為它僅僅屬於一個變量($a), 並沒有其他變量的引用指向它. 第二行執行時, 這個zval的refcount增加到2, 但是此時, 因為腳本中增加了一個取地址符(&)標記它是引用傳值, 因此將is_ref設置為1. 最後, 在第三行中, 引擎獲得$b關聯的zval, 檢查是否需要隔離. 此時這個zval不會被隔離, 因為在前面我們沒有包含的一段代碼(如下). 在get_var_and_separate()中檢查refcount的地方, 還有另外一個條件: [cpp  if ((*varval)->is_ref || (*varval)->refcount < 2) {       /* varname只有在真的是引用方式, 或者只被一個變量引用時才會不發生隔離 */       return *varval;   }   此時, 即便refcount為2, 隔離處理也會被短路, 因為這個值是引用傳值的. 引擎可以自由的修改它而不用擔心引用它的其他變量被意外修改. 隔離的問題 對於這些拷貝和引用, 有一些組合是is_ref和refcount無法很好的處理的. 考慮下面的代碼: [php]   <?php       $a = 1;       $b = $a;       $c = &$a;   ?>   這裡你有一個值需要被3個不同的變量關聯, 兩個是寫時修改的引用方式, 另外一個是隔離的寫時復制上下文. 僅僅使用is_ref和refcount怎樣來描述這種關系呢? 答案是: 沒有. 這種情況下, 值必須被復制到兩個分離的zval *, 雖然兩者包含相同的數據. 如下圖: 類似的, 下面的代碼塊將導致相同的沖突, 並強制值隔離到一個拷貝中(如下圖) [php]   <?php       $a = 1;       $b = &$a;       $c = $a;   ?>   注意, 這裡兩種情況下, $b都和原來的zval對象關聯, 因為在隔離發生的時候, 引擎不知道操作中涉及的第三個變量的名字. 小結 php是一種托管語言. 從用戶空間一側考慮, 小心的控制資源和內存就意味著更容易的原型涉及和更少的崩潰. 在你深入研究揭開引擎的面紗後, 就不能再有博彩心裡, 而是對運行環境完整性的開發和維護負責.  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved