PHP では、文字列変数の設定は非常に簡単です: ですが、C では別の方法で文字列を変更、コピー、移動できます。静的文字列で単純に初期化できます: char *str = "hello world"; ただし、この文字列はコードセグメントに存在するため変更できません。保守可能な文字列を作成するには、メモリを割り当て、次のような関数を使用する必要があります。 strdup() を使用してコンテンツをコピーします。
従来のメモリ管理関数 (malloc()、free()、strdup()、realloc()、calloc() など) は、PHP ソース コードでは直接使用されません。この章では、その理由について説明します。
以前のすべてのプラットフォームでは、メモリ管理は要求/解放方式で処理されていました。アプリケーションは上位層 (通常はオペレーティング システム) に「メモリを使用したい」と伝え、スペースが許せばオペレーティング システムがそれをプログラムに提供します。記録するメモリを提供します。
アプリケーションがメモリを使用した後、他の場所にメモリを割り当てられるように、メモリを OS に返す必要があります。プログラムがメモリを返さない場合、OS はこのメモリが使用されなくなったことを知る方法がありません。他のプロセスに割り当てることはできません。メモリの一部が解放されず、それを所有するアプリケーションがハンドルを失った場合、誰もそれを直接取得できないため、それを「リーク」と呼びます。
典型的なクライアント アプリケーションでは、一定期間後にプロセスが終了し、リークされたメモリが OS によって再利用されるため、まれに発生する小さなリークは通常許容されます。OS はリークされたメモリを認識するのが得意というわけではありません。メモリはありますが、終了したプロセスに割り当てられたメモリは再度使用されないことがわかっています。
Apache のような Web サーバーを含む、長時間実行されるサーバー側デーモンの場合、プロセスは長期間、通常は無期限に実行されるように設計されているため、OS は、メモリ使用量や、いかなる程度のリークにも干渉できません。小さいですが、蓄積するとシステム リソースが枯渇する可能性があります。
ユーザー空間の stristr() 関数を考えてみましょう。大文字と小文字を区別しない方法で文字列を検索するために、この関数は実際に haystack と neede のそれぞれの小文字のコピーを作成し、その後、通常の大文字と小文字を区別した検索を実行して、関連するオフセット量を見つけます。文字列オフセットが特定された後、haystack および needle 文字列の小文字バージョンは使用されなくなり、stristr() を使用する各スクリプトは最終的にメモリを占有します。システム全体のメモリが使用されますが、どれも使用されません。
完璧なソリューションとは、完全に正しいことが保証された、よく書かれたクリーンで一貫性のあるコードです。しかし、PHP インタープリターのような環境では、これは解決策の半分にすぎません。
ユーザー スクリプトのアクティブ化リクエストとそれが配置されている拡張関数からジャンプする機能を提供するには、アクティブ化リクエスト全体からジャンプして Zend で処理する方法が必要です。エンジンは、リクエストの最初にジャンプ アドレスを設定します。 ()/exit() が呼び出された後、または重大なエラー (E_ERROR) が発生した場合、longjmp() が実行され、事前に設定された終了アドレスにリダイレクトされます。
この種の終了処理はプログラム フローを簡素化しますが、問題があります。リソース クリーンアップ コード (free() 呼び出しなど) がスキップされ、リークが発生します。次の簡略化されたエンジン処理関数呼び出しコードを考えてみましょう。
php_error_docref() 行が実行されると、内部プロセッサはエラー レベルが重大であると判断し、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) レイヤーであり、通常オペレーティング システムが果たす役割と同等の役割を果たし、呼び出し元のアプリケーションにメモリを割り当てます。つまり、プロセス空間リクエストの認知的観点から見ると、リクエストが終了すると、プロセスが終了したときに OS が実行するのと同じことを実行できる、つまり、暗黙的に解放されるという点で十分に低レベルです。次の図は、PHP プロセスにおける ZendMM と OS の関係を示しています。
ZendMM は、暗黙的なメモリ クリーニングを提供するだけでなく、スクリプトがシステムで許可されている以上のメモリを要求しようとした場合、または単一プロセス メモリの残りの量を超えた場合に、php.ini のmemory_limit 設定を通じて各リクエストのメモリ使用量を制御します。制限を超えると、ZendMM は自動的に E_ERROR メッセージを生成し、プロセスから飛び出し始めます。さらに、メモリ割り当ての結果が失敗した場合は、longjmp() が即座にチェックするため、ほとんどの場合チェックする必要がありません。エンジンの終端部分にジャンプします。
PHP の内部コードと OS の実メモリ管理層の間のフックの最も複雑な点は、すべての内部メモリ割り当てが一連の関数から選択される必要があることです。たとえば、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) にマップされます。
[CP]
Zend/zend_alloc.h の #define:
#define pemalloc(サイズ、永続)
((永続)?malloc(サイズ): emalloc(サイズ))
ZendMM が提供するアロケーター関数のリストは次のとおりであり、それに対応する従来のアロケーターがリストされています。
伝統的なディスペンサー
phpのアロケーター
void *malloc(size_t count);
void *emalloc(size_t count);
void *pemalloc(size_t count, charpersistent);
void *calloc(size_t count);
void *ecalloc(size_t count);
void *pecalloc(size_t count, charpersistent);
void *realloc(void *ptr, size_t count);
void *eralloc(void *ptr, size_t count);
void *perrealloc(void *ptr, size_t count, charpersistent);
void *strdup(void *ptr);
void *estrdup(void *ptr);
void *pestrdup(void *ptr, charpersistent);
ボイドフリー(void *ptr);
void efree(void *ptr);
void pefree(void *ptr, charpersistent);
pefree には永続化タグを渡す必要があることに気づいたかもしれません。これは、pefree() が呼び出されるときに、ptr が永続的に割り当てられているかどうかがわからないためです。 free. であり、永続的な割り当てで efree() を呼び出すと、メモリ マネージャーが管理情報を参照しようとするため、コードは割り当てられたデータ構造が永続的であるかどうかを記憶する必要があるため、通常はセグメンテーション違反が発生します。
コア アロケータに加えて、ZendMM は特別な関数も追加します:
[CP]
void *estrndup(void *ptr, int len);
len + 1 バイトのメモリを割り当て、ptr から新しく割り当てられたブロックに len バイトをコピーします。
は、おおよそ次のように動作します。
[CP]
void *estrndup(void *ptr, int len)
{
char *dst = emalloc(len + 1);
memcpy(dst, ptr, len);
dst[len] = 0;
夏時間を返す
}
終了 NULL バイトはバッファの最後に静かに配置されます。これにより、文字列の割り当てに estrndup() を使用するすべての関数が、NULL で終了する文字列を期待する関数に結果バッファを渡すことを心配する必要がなくなります。 printf()) estrndup() を使用して非文字列データをコピーするとエラーが発生しますが、この最後のバイトは無駄になりますが、それがもたらす利便性に比べれば、この小さな無駄は大したことではありません。
[CP]
void *safe_emalloc(size_t サイズ、size_t カウント、size_t addtl);
void *safe_pemalloc(size_t サイズ、size_t カウント、size_t addtl、char 永続);
これら 2 つの関数によって割り当てられるメモリ サイズは ((size * count) + addtl) の結果です。「なぜそのような関数を拡張するのですか? emalloc/pemalloc を使用して自分で計算しないのですか?」と疑問に思われるかもしれません。このような状況はまれですが、計算結果がホスト プラットフォームの整数制限を超えると、結果が不良なワード数のセクションに割り当てられる可能性があります。あるいは、さらに悪いことに、safe_emalloc() は、整数のオーバーフローをチェックし、オーバーフローが発生した場合に失敗を明示的に報告することで、この種の落とし穴を回避します。
すべてのメモリ割り当てルーチンに p* コピーがあるわけではありません。たとえば、pestrndup() とsafe_pemalloc() は、PHP 5.1 より前には存在しませんでした。場合によっては、ZendAPI のこれらの欠点に対処する必要があります。
参照数
PHP のような長時間実行のマルチリクエストプロセスではメモリを慎重に割り当てて解放することが非常に重要ですが、これは仕事の半分にすぎません。同時実行性の高いサーバーをより効率的にするには、各リクエストが使用するメモリを最小限にする必要があります。非効率を最小限に抑えるために、次の PHP コード スニペットを検討してください。
[php]
$a = 'Hello World'
;
$b = $a
設定を解除($a)
?>
最初の呼び出しの後、文字列「Hello world」と末尾の NULL を保持する変数が作成され、12 バイトのメモリ ブロックに割り当てられます。次に 2 番目の文を見てください: $b は to に設定され、$ a は has です。同じ値の場合、$a は設定解除 (解放) されます
各変数の割り当てで変数の内容をコピーする必要があると PHP が判断した場合、データのコピー中に追加の 12 バイトの重複文字列がコピーされ、追加のプロセッサ負荷が発生します。3 行目が表示されると、この動作が発生するように見えます。ちょっとばかげていますが、元の変数はアンロードされるため、データのコピーはまったく不要になります。次に、2 つの変数に 10MB のファイルの内容がロードされるとどうなるかを考えてみましょう。ただし、20MB のメモリが必要です。たった 10MB で十分です。エンジンは本当にそのような無駄な作業に多くの時間とメモリを浪費しますか?
php が非常に賢いことはご存知でしょう。
エンジンでは、変数名とその値は 2 つの異なる概念です。その値自体は zend_hash_add() を使用して変数 $a に割り当てます。同じ値を指すには?
[CP]
{
zval *こんにちは
MAKE_STD_ZVAL(ハローヴァル);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),
}
この時点で、$a または $b を確認すると、実際には「Hello World」という文字列が含まれていることがわかります: unset($a); この場合は、unset() です。は、$a が指すデータが別の名前でも参照されていることを認識せず、メモリを解放するだけです。その後の $b へのアクセスは、解放されたメモリ空間を参照するため、エンジンがクラッシュします。もちろん、エンジンがクラッシュするのは望ましくありません。
これは、zval の 3 番目のメンバーである refcount によって解決されます。変数が最初に作成されると、その refcount は 1 に初期化されます。これは、コードが実行されると、作成された変数のみがその変数を指すと考えられるためです。が $b に割り当てられている場合、この値は 2 つの変数によって「参照」されるため、refcount を 2 に増やす必要があります
[CP]
{
zval *こんにちは
MAKE_STD_ZVAL(ハローヴァル);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),
ZVAL_ADDREF(ハローバル)
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),
}
さて、unset() が変数の $a コピーを削除すると、refcount を通して他の誰かがこのデータに興味を持っていることがわかり、refcount を 1 減らすだけで他には何もしません
。
書きながらコピー
参照カウントによってメモリを節約するのは良い考えですが、変数の 1 つだけを変更したい場合はどうすればよいでしょうか。次のコード スニペットを考えてみましょう。
[php]
$a = 1;
$b = $a
$b +=
;
?>
上記のコードのロジックを見てください。処理後も、$a は 1 に等しく、$b は 6 に等しいことが予想されます。メモリを最大限に節約するために、Zend は同じ値のみを必要とすることがわかります。コードの 2 行目が実行された後の $a と $b は、コードの 3 行目に到達するとどうなるでしょうか?
答えは、Zend が refcount を調べ、それが 1 より大きいことを確認し、それを分離することです。Zend エンジンでの分離とは、先ほど説明した処理とは逆の参照ペアを破棄することです。
[CP]
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
/* 変数名には参照が 1 つだけあり、分離は必要ありません */
*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,
/* 新しい zval を返します * */
varcopy を返す
}
これで、エンジンには $b 変数によってのみ参照される zval * が追加され、スクリプトの要求に応じてそれを long に変換し、その値を 5 ずつ増やすことができます。
書きながら修正してください
参照カウントの概念は、ユーザー空間スクリプトで「参照」と呼ばれる、データを維持する新しい方法も作成します。次のユーザー空間コード スニペットを考えてみましょう。
[php]