搜尋
首頁後端開發php教程[翻譯][php擴充開發與嵌入式]第3章-記憶體管理

記憶體管理

php和c最重要的區別就是是否控制記憶體指標.

記憶體

在php中, 設定一個字串變數很簡單: , 字元字串可以自由的修改, 拷貝, 移動. 在C中, 則是另外一種方式, 雖然你可以簡單的用靜態字串初始化: char *str = "hello world"; 但是這個字串不能被修改, 因為它存在於程式碼段. 要建立一個可維護的字串, 你需要分配一塊記憶體, 並使用一個strdup()這樣的函數將內容拷貝到其中.

{  
   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()呼叫)會被跳過,會因此帶來洩漏. 考慮下面簡化的引擎處理函數調用的代碼:

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的關係:

[翻譯][php擴充開發與嵌入式]第3章-記憶體管理

除了提供隐式的内存清理, 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), 如下:

#define in Zend/zend_alloc.h:  
  
#define pemalloc(size, persistent) \  
            ((persistent)?malloc(size): emalloc(size))
传统分配器 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还增加了特殊的函数:

void *estrndup(void *ptr, int len);

它分配len + 1字节的内存, 并从ptr拷贝len个字节到新分配的块中. estrndup()的行为大致如下:

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()拷贝非字符串数据时, 这个最后一个字节将被浪费, 但是相比带来的方便, 这点小浪费就不算什么了.

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  
    $a = &#39;Hello World&#39;;  
    $b = $a;  
    unset($a);  
?>

在第一次调用后, 一个变量被创建, 它被赋予12字节的内存块, 保存了字符串"Hello world"以及结尾的NULL. 现在来看第二句: $b被设置为和$a相同的值, 接着$a被unset(释放)

如果php认为每个变量赋值都需要拷贝变量的内容, 那么在数据拷贝期间就需要额外的12字节拷贝重复的字符串, 以及额外的处理器负载. 在第三行出现的时候, 这种行为看起来就有些可笑了, 原来的变量被卸载使得数据的复制完全不需要. 现在我们更进一步想想当两个变量中被装载的是一个10MB文件的内容时, 会发生什么? 它需要20MB的内存, 然而只要10MB就足够了. 引擎真的会做这种无用功浪费这么多的时间和内存吗?

你知道php是很聪明的.

还记得吗? 在引擎中变量名和它的值是两个不同的概念. 它的值是自身是一个没有名字的zval *. 使用zend_hash_add()将它赋值给变量$a. 那么两个变量名指向相同的值可以吗?

{  
    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, 因为这个值现在被两个变量"引用"

{  
    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  
    $a = 1;  
    $b = $a;  
    $b += 5;  
?>

看上面代码的逻辑, 处理完后期望$a仍然等于1, 而$b等于6. 现在你知道, Zend为了最大化节省内存, 在第二行代码执行后$a和$b只想同一个zval, 那么到达第三行代码时会发生什么呢? $b也会被修改吗?

答案是Zend查看refcount, 看到它大于1, 就对它进行了隔离. Zend引擎中的隔离是破坏一个引用对, 它和你刚才看到的处理是对立的:

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  
    $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的地方, 还有另外一个条件:

if ((*varval)->is_ref || (*varval)->refcount < 2) {  
    /* varname只有在真的是引用方式, 或者只被一个变量引用时才会不发生隔离 */  
    return *varval;  
}

此时, 即便refcount为2, 隔离处理也会被短路, 因为这个值是引用传值的. 引擎可以自由的修改它而不用担心引用它的其他变量被意外修改.

隔离的问题

对于这些拷贝和引用, 有一些组合是is_ref和refcount无法很好的处理的. 考虑下面的代码:

<?php  
    $a = 1;  
    $b = $a;  
    $c = &$a;  
?>

这里你有一个值需要被3个不同的变量关联, 两个是写时修改的引用方式, 另外一个是隔离的写时复制上下文. 仅仅使用is_ref和refcount怎样来描述这种关系呢?

答案是: 没有. 这种情况下, 值必须被复制到两个分离的zval *, 虽然两者包含相同的数据. 如下图:

[翻譯][php擴充開發與嵌入式]第3章-記憶體管理

类似的, 下面的代码块将导致相同的冲突, 并强制值隔离到一个拷贝中(如下图)

[翻譯][php擴充開發與嵌入式]第3章-記憶體管理

<?php  
    $a = 1;  
    $b = &$a;  
    $c = $a;  
?>

注意, 这里两种情况下, $b都和原来的zval对象关联, 因为在隔离发生的时候, 引擎不知道操作中涉及的第三个变量的名字.

小结

php是一种托管语言. 从用户空间一侧考虑, 小心的控制资源和内存就意味着更容易的原型涉及和更少的崩溃. 在你深入研究揭开引擎的面纱后, 就不能再有博彩心里, 而是对运行环境完整性的开发和维护负责.

以上就是 [翻译][php扩展开发和嵌入式]第3章-内存管理的内容,更多相关内容请关注PHP中文网(www.php.cn)!


陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
PHP行動:現實世界中的示例和應用程序PHP行動:現實世界中的示例和應用程序Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP:輕鬆創建交互式Web內容PHP:輕鬆創建交互式Web內容Apr 14, 2025 am 12:15 AM

PHP可以輕鬆創建互動網頁內容。 1)通過嵌入HTML動態生成內容,根據用戶輸入或數據庫數據實時展示。 2)處理表單提交並生成動態輸出,確保使用htmlspecialchars防XSS。 3)結合MySQL創建用戶註冊系統,使用password_hash和預處理語句增強安全性。掌握這些技巧將提升Web開發效率。

PHP和Python:比較兩種流行的編程語言PHP和Python:比較兩種流行的編程語言Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP的持久相關性:它還活著嗎?PHP的持久相關性:它還活著嗎?Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

PHP的當前狀態:查看網絡開發趨勢PHP的當前狀態:查看網絡開發趨勢Apr 13, 2025 am 12:20 AM

PHP在現代Web開發中仍然重要,尤其在內容管理和電子商務平台。 1)PHP擁有豐富的生態系統和強大框架支持,如Laravel和Symfony。 2)性能優化可通過OPcache和Nginx實現。 3)PHP8.0引入JIT編譯器,提升性能。 4)雲原生應用通過Docker和Kubernetes部署,提高靈活性和可擴展性。

PHP與其他語言:比較PHP與其他語言:比較Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP與Python:核心功能PHP與Python:核心功能Apr 13, 2025 am 12:16 AM

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

PHP:網絡開發的關鍵語言PHP:網絡開發的關鍵語言Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境