ホームページ >php教程 >php手册 >[シェア]PHP におけるメモリ管理の問題についての詳細な議論

[シェア]PHP におけるメモリ管理の問題についての詳細な議論

WBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWB
WBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBオリジナル
2016-06-21 08:52:561136ブラウズ

1.記憶

PHP では、文字列変数に値を入力するのは非常に簡単で、必要なのは「」という 1 つのステートメントだけであり、文字列は自由に変更、コピー、移動できます。 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 つの文字列それぞれの小さなコピーが作成され、その後、より伝統的な大文字と小文字を区別する検索が実行されます。相対オフセットを見つけます。ただし、文字列のオフセットを特定した後は、これらの小文字バージョンの文字列は使用されなくなります。これらのコピーを解放しないと、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() を実行しません。 all lcase_fname) この行。 efree() 行を zend_error() 行の上に移動することもできますが、call_function() ルーチンを呼び出す行はどうでしょうか。 fname 自体は割り当てられた文字列である可能性が高く、エラー メッセージ処理で使用されるまで解放することはできません。

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

4. Zend メモリマネージャー

上記の「バウンス」リクエスト中のメモリ リークに対する 1 つの解決策は、Zend Memory Management (ZendMM) レイヤーを使用することです。エンジンのこの部分は、呼び出し側プログラムにメモリを割り当てる、オペレーティング システムのメモリ管理動作に非常に似ています。違いは、プロセス領域が非常に少なく、「リクエストを認識する」ため、リクエストが終了すると、プロセスが終了するときに OS が行うのと同じ動作を実行できることです。つまり、リクエストによって占有されていたすべてのメモリが暗黙的に解放されます。図 1 は、ZendMM と OS および 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(サイズ, 永続) ((永続)?malloc(サイズ): emalloc(サイズ))


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;
dst を返す;
}

ここで、NULL バイトがバッファの最後に暗黙的に配置されることにより、 estrndup() を使用して文字列コピー操作を実装する関数は、結果バッファを期待する printf() などの関数に渡すことを心配する必要がなくなります。 NULL はターミネータとして機能します。 estrndup() を使用して非文字列データをコピーする場合、最後のバイトは基本的に無駄になりますが、明らかに利点が欠点を上回ります。


void *safe_emalloc(size_t サイズ, size_t カウント, size_t addtl);

void *safe_pemalloc(size_t size, size_t count, size_t addtl, charpersistent);


これらの関数によって割り当てられるメモリ空間の最終的なサイズは ((size*count)+addtl) です。 「なぜ追加の機能を提供するのですか? なぜ emalloc/pemalloc を使用しないのですか? 理由は簡単です。安全のためです。」可能性が非常に小さい場合もありますが、ホスト プラットフォームのメモリがオーバーフローする原因となるのは、この「非常に小さい可能性」です。これにより、負のバイト数のスペースが割り当てられる可能性があり、さらに悪いことに、呼び出し側プログラムが必要とするバイト数よりも少ないバイト数が割り当てられる可能性があります。 Safe_emalloc() は、整数オーバーフローをチェックし、そのようなオーバーフローが発生した場合に明示的に先頭で終了することで、このタイプのトラップを回避できます。

すべてのメモリ割り当てルーチンに、対応する p* と同等の実装があるわけではないことに注意してください。たとえば、pestrndup() は存在せず、safe_pemalloc() も PHP 5.1 より前には存在しませんでした。

5. 参照数

メモリの割り当てと割り当て解除を慎重に行うことは、PHP (複数リクエストのプロセス) の長期的なパフォーマンスに大きな影響を与えますが、これは問題の半分にすぎません。 1 秒あたり数千のヒットを処理するサーバーを効率的に実行するには、各リクエストで使用するメモリをできる限り少なくし、不必要なデータ コピー操作を最小限に抑える必要があります。次の 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(ハローヴァル);
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(ハローヴァル);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), &helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(ハローヴァル);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), &helloval, sizeof(zval*), NULL);
}

ここで、unset() が $a の対応するコピーを削除すると、refcount パラメーターから、そのデータに関心のあるユーザーが他にいることを確認できるため、refcount をデクリメントして、そのままにしておく必要があります。

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

$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) {
/* 変数はまったく存在しません - 失敗すると終了します*/
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);
/*新しく作成した値の参照カウントを初期化し、
にアタッチします * 変数名変数
*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &varcopy, sizeof(zval*), NULL);
/*新しい zval を返します* */
varcopy を返します;
}


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

7. 変更時書き込み

参照カウントの概念の導入は、ユーザー空間スクリプト マネージャーに関連すると思われる「参照」の形でのデータ操作の新たな可能性にもつながります。次のユーザー空間のコード スニペットを考えてみましょう:

$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 < 2) {
/* varname は唯一の実際の参照です、
* または、別の変数への完全な参照
*いずれの方法: 分離は実行されません


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
前の記事:2進数形式次の記事:2進数形式