[翻訳][php拡張機能の開発と組み込み] 第3章 - メモリ管理

黄舟
リリース: 2023-03-05 16:10:01
オリジナル
1113 人が閲覧しました

メモリ管理

PHP と C の最も重要な違いは、メモリ ポインタを制御するかどうかです。

メモリ

PHP では、文字列変数の設定は非常に簡単です。 > では、文字列を自由に変更、コピー、移動できますが、静的な文字列 char *str = "hello world"; で初期化することもできますが、この文字列は変更できません。 、それはコードセグメントに存在するため、保守可能な文字列を作成するには、メモリのブロックを割り当て、 strdup() のような関数を使用してその内容をそこにコピーする必要があります。

{  
   char *str;  
  
   str = strdup("hello world");  
   if (!str) {  
       fprintf(stderr, "Unable to allocate memory!");  
   }  
}
ログイン後にコピー

従来のメモリ管理関数 (malloc() 、無料) ()、strdup()、realloc()、calloc() など) は、PHP のソース コードでは直接使用されません。この章では、

割り当てられたメモリを解放する

のメモリ管理について説明します。すべての以前のすべてのプラットフォームは、要求/解放方式で処理されます。アプリケーションは、その上位層 (通常はオペレーティング システム) に「メモリ使用量が必要です」と伝え、スペースが許せば、オペレーティング システムはそれをプログラムに提供し、記録を保持します。提供されたメモリの量。

アプリケーションがメモリを使用した後、他の場所にメモリを割り当てられるように、そのメモリを OS に返す必要があります。プログラムがメモリを返さない場合、OS はそのメモリを知る方法がありません。は使用されなくなったため、他のプロセスに割り当てることはできません。メモリの一部が解放されず、それを所有するアプリケーションがハンドルを失った場合、誰もそれを直接取得できないため、それを「リーク」と呼びます。典型的なクライアント アプリケーションでは、一定期間後にプロセスが終了し、リークしたメモリが OS によって再利用されるため、小規模でまれなリークは通常許容されます。OS はリークしたメモリをあまり認識していません。プロセスによって割り当てられたメモリは再度使用されないため、Apache などの Web サーバーを含む長時間実行されるサーバー デーモンの場合、プロセスは長期間 (通常は無期限に) 実行されるように設計されています。

大文字と小文字を区別せずに文字列を検索するために、ユーザー空間で stristr() 関数を考慮してください。実際には、干し草の山と針にそれぞれ 1 つずつ作成されます。小文字のコピーが作成され、文字列のオフセットが特定された後、通常の大文字と小文字を区別した検索が実行され、干し草の山と針の文字列の小文字バージョンは見つかりません。これらのコピーを解放しないと、stristr() を使用する各スクリプトが呼び出されるたびにメモリがリークされ、最終的には Web サーバーのプロセスがシステム全体のメモリを占有しますが、どれも使用されなくなります。これはよく書かれた、クリーンで一貫性のあるコードであり、完全に正しいことが保証されています。しかし、PHP インタープリターのような環境では、これはユーザー スクリプトと拡張機能からのアクティベーション リクエストを提供するための解決策の半分にすぎません。関数 ここで、アクティベーション リクエスト全体からジャンプできるようにするには、アクティベーション リクエスト全体からジャンプする方法が必要です。Zend エンジンでそれを処理する方法は、先頭にジャンプ アウト アドレスを設定することです。すべての die()/exit() 呼び出し後、または何らかのキーに遭遇したときのリクエスト。性的エラー (E_ERROR) が発生した場合、longjmp() の実行は、この種のジャンプではありますが、事前に設定されたジャンプ アドレスにリダイレクトされます。処理はプログラム フローを簡素化しますが、問題があります。リソース クリーンアップ コード (free() 呼び出しなど) がジャンプされます。そうしないと、リークが発生します。次の簡略化されたエンジン処理関数呼び出しコードを考慮してください。 ) 行が実行されると、内部プロセッサはエラー レベルがクリティカルであると判断し、longjmp() を呼び出して中断します。 現在のプログラム フローは call_function() を残し、efree(lcase_fname) 行に到達できないようにする必要があります。 efree() 行を php_error_docref() に追加しますが、この call_function() 呼び出しが最初の条件に入ると、分岐 (関数名の検索、通常の実行) はどうなるでしょうか。もう 1 つの点は、fname 自体が割り当てられた文字列であり、それが で使用されるということです。 php_error_docref() 関数は内部的には Trigger_error() と同等であり、php.ini で有効になっている場合は docref.root に追加されるオプションのドキュメント参照です。 3 番目のパラメータは、任意の E_* ファミリ定数にすることができます。 4 番目以降のパラメータは、printf() スタイルに準拠したフォーマット文字列と変数パラメータ リストです。

Zend メモリ管理

によるメモリ リークの解決策。リクエスト バウンス (障害) は Zend メモリ管理 (ZendMM) 層であり、エンジンのこの部分は、呼び出し側アプリケーションにメモリを割り当てるという通常の役割と同等の役割を果たします。つまり、要求されたすべてのメモリ空間を暗黙的に解放します。 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), 如下:

#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)!


ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!