ホームページ  >  記事  >  バックエンド開発  >  PHP でのメモリ管理の分析、PHP による動的メモリの割り当てと解放_PHP チュートリアル

PHP でのメモリ管理の分析、PHP による動的メモリの割り当てと解放_PHP チュートリアル

WBOY
WBOYオリジナル
2016-07-21 15:02:15981ブラウズ

概要 メモリ管理は、サーバー デーモンなどの長時間実行されるプログラムに非常に重要な影響を与えるため、PHP がメモリをどのように割り当て、解放するかを理解することは、そのようなプログラムを作成する上で非常に重要です。この記事では、PHP のメモリ管理の問題に焦点を当てます。

1. メモリ
PHP では、文字列変数の入力は非常に簡単で、「」というステートメントだけで、文字列を自由に変更、コピー、移動できます。 。 C 言語では、「char *str = "hello world ";"; のような単純な静的文字列を記述することはできますが、文字列はプログラム空間に存在するため変更できません。操作文字列を作成するには、メモリ ブロックを割り当て、関数 (strdup() など) を使用してその内容をコピーする必要があります。

コードをコピーします コードは次のとおりです:

{
char *str;
str = strdup("hello world");
if (!str) {
fprintf(stderr, "割り当てられませんメモリ!" );
}
}

後で分析するさまざまな理由により、従来のメモリ管理関数 (malloc()、free()、strdup()、realloc()、calloc() など) .) はほとんどどちらも PHP ソース コードから直接使用することはできません。

2. メモリを解放する
ほぼすべてのプラットフォームで、メモリ管理はリクエストとリリースのモデルを通じて実装されます。まず、アプリケーションはその下の層 (通常は「オペレーティング システム」) に「メモリ空間を使用したい」と要求します。空き領域がある場合、オペレーティング システムはその空き領域をプログラムに提供し、他のプログラムに割り当てられないようにマークします。

アプリケーションがこのメモリの使用を終了したら、他のプログラムに割り当てられるように、メモリを OS に返す必要があります。プログラムがこのメモリを返さない場合、OS はこのメモリがもう使用されておらず、別のプロセスに割り当てることができるかどうかを知る方法がありません。メモリ ブロックが解放されず、所有者アプリケーションがメモリ ブロックを失った場合、そのメモリは他のプログラムで利用できなくなるため、アプリケーションは「脆弱」であると言われます。

一般的なクライアント アプリケーションでは、小規模でまれなメモリ リークが OS によって「許容」される場合があります。これは、後でプロセスが終了するときに、リークしたメモリが暗黙的に OS に返されるためです。 OS はどのプログラムにメモリを割り当てたかを認識しており、プログラムが終了するとメモリが不要になることが確実であるため、これは問題ありません。

Apache などの Web サーバーや拡張 php モジュールなど、長時間実行されるサーバー デーモンの場合、プロセスは長時間実行されるように設計されていることがよくあります。 OS はメモリ使用量をクリーンアップできないため、プログラム リークは、たとえどんなに小さくても、繰り返し操作を引き起こし、最終的にはすべてのシステム リソースを使い果たしてしまいます。

ここで、ユーザー空間の stristr() 関数について考えてみましょう。大文字と小文字を区別しない検索を使用して文字列を見つけるために、実際には 2 つの文字列それぞれの小さなコピーが作成され、より伝統的な Type case- が実行されます。相対オフセットを見つけるための敏感な検索。ただし、文字列のオフセットを特定した後は、これらの小文字バージョンの文字列は使用されなくなります。これらのコピーを解放しないと、stristr() を使用するすべてのスクリプトが呼び出されるたびにメモリ リークが発生します。最終的に、Web サーバー プロセスはすべてのシステム メモリを獲得しますが、それを使用できなくなります。

理想的な解決策は、適切でクリーンで一貫性のあるコードを書くことであると言っても過言ではありません。これは確かに真実ですが、PHP インタープリターのような環境では、この見方は半分しか真実ではありません。

3. エラー処理
ユーザー空間スクリプトとそれに依存する拡張機能のアクティブなリクエストを「ジャンプアウト」するには、アクティブなリクエストを完全に「ジャンプアウト」するメソッドを使用する必要があります。これは Zend Engine 内に実装されています。リクエストの先頭に「ジャンプアウト」アドレスを設定し、次に、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_to lower(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, "未定義関数の呼び出し: %s()", fname);
}
efree(lcase_fname); ;
}

php_error_docref() 行が実行されると、内部エラー ハンドラーはエラー レベルが重大であることを理解し、それに応じて longjmp() を呼び出して現在のプログラム フローを中断して call_function() 関数を終了するか、efree をまったく実行しないこともあります(lcase_fname) この行。 efree() 行を zend_error() 行の上に移動することもできますが、call_function() ルーチンを呼び出す行はどうでしょうか。 fname 自体はおそらく割り当てられた文字列であり、エラー メッセージ処理で使用されるまで解放することはできません。

この php_error_docref() 関数は、trigger_error() 関数の内部同等の実装であることに注意してください。 最初のパラメータは、docref に追加されるオプションのドキュメント参照です。 3 番目のパラメーターには、エラーの重大度を示すために使用されるよく知られた E_* ファミリの定数のいずれかを指定できます。 4 番目 (最後の) 引数は、printf() スタイルの書式設定と変数引数リストのスタイルに従います。

4. Zend メモリ マネージャー
上記の「バウンス」リクエスト中のメモリ リークを解決する解決策の 1 つは、Zend メモリ管理 (ZendMM) レイヤーを使用することです。エンジンのこの部分は、呼び出し側プログラムにメモリを割り当てる、オペレーティング システムのメモリ管理動作に非常に似ています。違いは、プロセス領域が非常に少なく、「リクエストを認識する」ため、リクエストが終了すると、プロセスが終了するときに OS が行うのと同じ動作を実行できることです。つまり、リクエストによって占有されていたすべてのメモリが暗黙的に解放されます。図 1 は、ZendMM と OS および PHP プロセスとの関係を示しています。

深入探讨PHP中的内存管理问题
図 1. Zend メモリ マネージャーはシステム コールを置き換えて、各リクエストに対するメモリ割り当てを実装します。

ZendMM は暗黙的なメモリクリア機能を提供することに加えて、php.ini のmemory_limit の設定に従って各メモリリクエストの使用を制御することもできます。スクリプトがシステムで利用可能なメモリを超えるメモリ、または一度に要求する最大量を超えるメモリを要求しようとすると、ZendMM は自動的に E_ERROR メッセージを発行し、適切な「終了」プロセスを開始します。このアプローチのさらなる利点は、失敗するとすぐにエンジンの終了部分にジャンプするため、ほとんどのメモリ割り当て呼び出しの戻り値をチェックする必要がないことです。

PHP の内部コードと OS の実際のメモリ管理層を「フック」する原理は複雑ではありません。内部的に割り当てられたメモリはすべて、特定のオプション関数のセットを使用して実装する必要があります。たとえば、PHP コードでは、malloc(16) を使用して 16 バイトのメモリ ブロックを割り当てる代わりに、emalloc(16) を使用します。実際のメモリ割り当てタスクの実行に加えて、ZendMM はメモリ ブロックに対応するバインディング リクエスト タイプをマークします。これにより、リクエストが「バウンス」したときに、ZendMM はそれを暗黙的に解放できます。

多くの場合、単一のリクエストの期間よりも長い期間にわたってメモリを割り当てる必要があります。このタイプの割り当て (リクエストの完了後に持続するため「永続的割り当て」と呼ばれます) は、ZendMM が各リクエストに使用する余分なオーバーヘッドを追加しないため、従来のメモリ アロケータを使用して実装できます。ただし、特定の割り当てに永続的な割り当てが必要かどうかが実行時まで決定されない場合があるため、ZendMM は他のメモリ割り当て関数と同様に動作するヘルパー マクロのセットをエクスポートしますが、最後の追加パラメータを使用して永続的な割り当てであるかどうかを示します。

本当に永続的な割り当てを実装したい場合は、このパラメータを 1 に設定する必要があります。この場合、リクエストは従来の malloc() アロケータ ファミリを通じて渡されます。ただし、ランタイム ロジックがこのブロックに永続的な割り当てが必要ないと判断した場合は、このパラメーターを 0 に設定することができ、呼び出しはリクエストごとにメモリ アロケーター関数に調整されます。

たとえば、次のステートメントを使用して、pemalloc(buffer_len, 1) は malloc(buffer_len) にマップされ、pemalloc(buffer_len, 0) は emalloc(buffer_len) にマップされます: Zend/zend_alloc.h の
#define:
#define pemalloc(size,persistent) ((persistent)?malloc(size): emalloc(size))

ZendMM で提供されるこれらのアロケーター関数はすべて、以下の表からより伝統的な対応する実装を見つけることができます。
表 1 は、ZendMM でサポートされている各アロケーター関数と、その e/pe に対応する実装を示しています。
表 1. 従来のアロケーターと PHP 固有のアロケーター。

分配器函数 e/pe对应实现
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() を呼び出すと、解放される領域が 2 倍になる可能性がありますが、永続割り当てで 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, charpersistent);
これらの関数によって割り当てられるメモリ空間の最終的なサイズは ((サイズ*カウント)+addtl)。 「なぜ追加の機能を提供するのですか? なぜ emalloc/pemalloc を使用しないのですか? 理由は簡単です。安全のためです。」可能性は非常に小さい場合もありますが、この「非常に小さい可能性」がホスト プラットフォームのメモリ オーバーフローの原因となります。これにより、負のバイト数のスペースが割り当てられる可能性があり、さらに悪いことに、呼び出し側プログラムが必要とするバイト数よりも少ないバイト数が割り当てられる可能性があります。 Safe_emalloc() は、整数オーバーフローをチェックし、そのようなオーバーフローが発生したときに明示的に先頭で終了することで、このタイプのトラップを回避できます。
すべてのメモリ割り当てルーチンに、対応する p* と同等の実装があるわけではないことに注意してください。たとえば、pestrndup() は存在せず、safe_pemalloc() も PHP 5.1 より前には存在しませんでした。

5. 参照カウント
慎重なメモリの割り当てと解放は、PHP (複数リクエストのプロセス) の長期的なパフォーマンスに非常に大きな影響を与えますが、これは問題の半分にすぎません。 1 秒あたり数千のヒットを処理するサーバーを効率的に実行するには、各リクエストで使用するメモリをできる限り少なくし、不必要なデータ コピー操作を最小限に抑える必要があります。次の PHP コード スニペットを考えてみましょう:

コードをコピーします コードは次のとおりです:

<?php
$a = 'Hello World';
$b = $a;
unset($a) ;
?>

最初の呼び出しの後、変数が 1 つだけ作成され、終端の NULL 文字を含む文字列「Hello World」を格納するために 12 バイトのメモリ ブロックがそれに割り当てられます。次に、次の 2 行を見てみましょう。$b が変数 $a と同じ値に設定され、その後、変数 $a が解放されます。

PHP が変数の割り当てごとに変数の内容をコピーする必要がある場合、上記の例でコピーされる文字列については、さらに 12 バイトをコピーする必要があり、データのコピー中に追加のプロセッサの読み込みが実行されます。コードの 3 行目が表示されると、元の変数が解放され、データ全体のコピーが完全に不要になるため、この動作は最初は少しばかげているように思えます。実際、もう少し考えて、10MB ファイルの内容が 2 つの変数にロードされたときに何が起こるかを想像してみましょう。これには 20MB のスペースが必要ですが、この時点では 10 で十分です。エンジンはそのような無駄な作業に多くの時間とメモリを浪費するでしょうか?
PHP の設計者はすでにこれを理解していることを知っておく必要があります。

エンジンでは、変数名とその値は実際には 2 つの異なる概念であることに注意してください。値自体は名前のない 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」が含まれていることがわかります。残念ながら、次に、コードの 3 行目「unset($a);」の実行を続けます。現時点では、unset() は $a 変数が指すデータが別の変数でも使用されていることを知らないため、ただ盲目的にメモリを解放します。それ以降の変数 $b へのアクセスは、メモリ領域が解放されたものとして解釈されるため、エンジンがクラッシュします。

この問題は、zval の 4 番目のメンバー refcount (いくつかの形式があります) を利用して解決できます。変数が最初に作成されて値が割り当てられると、その refcount は 1 に初期化されます。これは、変数が最初に作成されたときに対応する変数によってのみ使用されると想定されるためです。コード スニペットが helloval を $b に割り当て始めると、refcount の値を 2 に増やす必要があるため、値は 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 のカウント値をデクリメントして無視する必要があります。それ。

6. Copy on Write

refcounting によってメモリを節約することは確かに良い考えですが、変数の 1 つの値のみを変更したい場合はどうなるでしょうか?これを行うには、次のコード スニペットを検討してください。

コードをコピー

コードは次のとおりです。 <?php
$a = 1;
$b = $a;
$b += 5;
?>


上記の論理フローを通して、$a の値は依然として 1 に等しく、$b の値は最終的に 6 になることがわかります。そしてこの時点で、Zend が $a と $b の両方に同じ zval を参照させることでメモリを節約しようとしていることもわかります (コードの 2 行目を参照)。それでは、実行が 3 行目に達し、$b 変数の値を変更する必要がある場合はどうなるでしょうか?
その答えは、Zend は refcount の値を調べ、その値が 1 より大きい場合には必ず分離する必要があるということです。 Zend Engine では、デタッチは参照ペアを破棄するプロセスであり、先ほど説明したプロセスとは逆です:



コードをコピーします
コードは次のとおりです: 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) {
/* 変数はまったく存在しない - 失敗 原因 exit */
return NULL;
}
if ((*varval)->refcount /* varname は唯一の実際の参照であり、
* を切り離す必要はありません
* /
return *varval;
}
/* それ以外の場合は、zval**の値の別のコピーを作成します**/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* zval**/
zval_copy_ctor内の割り当てられた構造体をコピーします(varcopy);
/*古いバージョンのvarnameを削除します
*これにより、プロセス内のvarvalのrefcountの値が減ります
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/*参照を初期化します新しく作成した値のカウント、そしてそれを
* varname 変数に追加します
*/
varcopy->refcount = 1;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &varcopy , sizeof(zval*), NULL);
/*return new zval* */
return varcopy;
}


さて、エンジンには変数 $b のみが所有する zval* があるため (エンジンはこれを知っています)、この値は、スクリプトの要求に応じて、long 値に変換され、5 ずつ増分されます。

7. Change-on-write
参照カウントの概念の導入は、新しいデータ操作の可能性にもつながります。その形式は、ユーザー空間スクリプト マネージャーからの「参照」に関連しているようです。次のユーザー空間のコード スニペットを考えてみましょう。


コードをコピーします。

コードは次のとおりです。 <?php$a = 1;
$b = &$a;
$b += 5;
?>


上記の PHP コードでは、$a の値が 1 から始まり (直接) 変更されていないにもかかわらず、現在 6 になっていることがわかります。これは、エンジンが $b の値を 5 ずつインクリメントし始めると、$b が $a への参照であることに気づき、「すべての参照変数を使用したいため、その値をデタッチせずに変更できる」と考えるために発生します。 」。

しかし、エンジンはどうやってそれを知るのでしょうか?これは単純で、zval 構造体の最後の 4 番目の要素 (is_ref) を確認するだけです。これは、値が実際にユーザー空間スタイル参照セットの一部であるかどうかを定義する単純なオン/オフ ビットです。前のコード スニペットでは、最初の行が実行されると、$a に対して作成された値は refcount 1 と is_ref 値 0 を取得します。これは、この値が 1 つの変数 ($a) のみによって所有され、他の変数によって所有されていないためです。書き込み参照の変更を行うそれに。 2 行目では、この値の refcount 要素が 2 に増加しますが、今回は is_ref 要素が 1 に設定されています (スクリプトには完全な参照を示す「&」記号が含まれているため)。

最後に、3行目で、エンジンは変数$bに関連付けられた値を再度取り出し、分離が必要かどうかをチェックします。以前はチェックが含まれていなかったため、今回は値が分離されません。以下は、get_var_and_ Separate() 関数の refcount チェックに関連するコードの一部です:
コードをコピー コードは次のとおりです:

if ((*varval)->is_ref || (*varval) ->refcount /* varname は唯一の実際の参照です、
* または他の変数への完全な参照です
* いずれにしても: デタッチは行われません
*/
return *varval;
}

今回は、refcountが2ですが、この値は完全参照のため切り離されません。エンジンは他の変数値の変更を気にすることなく、自由に変更できます。

8. 分離の問題
上記で説明したコピーおよび参照技術はすでに存在しますが、is_ref および refcount 操作では解決できない問題がまだいくつかあります。次の PHP コードのブロックを考えてみましょう:

コードをコピー コードは次のとおりです:

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

ここでは、3 つの異なる変数に関連付ける必要がある値があります。 2 つの変数は「change-on-write」を使用して完全に参照され、3 番目の変数は取り外し可能な「copy-on-write」コンテキスト内にあります。この関係を説明するために is_ref と refcount のみを使用した場合、どのような値が機能するでしょうか?
答えは次のとおりです。どれも機能しません。この場合、値は 2 つの別々の zval* にコピーする必要がありますが、どちらにもまったく同じデータが含まれています (図 2 を参照)。

深入探讨PHP中的内存管理问题
図 2. 参照時の強制分離


同様に、次のコード ブロックも同じ競合を引き起こし、値のコピーを強制します (図 3 を参照)。

深入探讨PHP中的内存管理问题
図3. コピー中の強制分離

コードをコピーします コードは次のとおりです:

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

どちらの場合も、切り離しが発生したときにエンジンには操作に関係する 3 番目の変数の名前を知る方法がないため、$b は元の zval オブジェクトに関連付けられます。

9. 概要
PHP はホスティング言語です。平均的なユーザーの観点から見ると、リソースとメモリをこのように注意深く制御することは、プロトタイピングが容易になり、競合が少なくなることを意味します。しかし、「内部」の奥深くに進むと、すべての約束は消え去ったように見え、最終的にはランタイム環境全体の一貫性を維持するために真に責任のある開発者に依存する必要があります。

www.bkjia.comtru​​ehttp://www.bkjia.com/PHPjc/327944.html技術記事まとめ メモリ管理は、サーバー デーモンなどの長時間実行されるプログラムに非常に重要な影響を与えるため、PHP がメモリをどのように割り当て、解放するかを理解することは、そのようなプログラムを作成する上で非常に重要です。
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。