ホームページ > バックエンド開発 > PHPチュートリアル > PHPパラメータ受け渡し原理の詳細な分析

PHPパラメータ受け渡し原理の詳細な分析

WBOY
リリース: 2016-07-25 09:13:20
オリジナル
1315 人が閲覧しました

PHP 拡張機能を作成する場合、パラメーター (つまり、zend_parse_parameters に渡される変数) を自由にする必要はないようです。 例:

  1. PHP_FUNCTION(test)

  2. {
  3. char* str;
  4. int str_len;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, " s" ,&str , &str_len) == FAILURE) {

  6. RETURN_FALSE;
  7. }
  8. php_printf(str);

  9. // free(str) は必要ありません
  10. }
正常に動作するコード
をコピーします: test("Hello World"); // Hello World を出力します。 ここでは、テスト関数でのメモリ リークを心配する必要はありません。PHP は、パラメータの保存に使用されるこれらの変数を自動的にリサイクルします。

では、php はどのように行うのでしょうか?この問題を説明するには、PHP がパラメータを渡す方法を調べる必要があります。

EG (argument_stack) の紹介 簡単に言うと、パラメーターの保存に特に使用されるスタックは、argument_stack という名前で PHP の EG に保存されます。関数呼び出しが発生するたびに、php は受信パラメータを EG (argument_stack) にプッシュします。関数呼び出しが終了すると、EG (argument_stack) はクリアされ、次の関数呼び出しを待ちます。

EG (argument_stack) の構造体と目的に関して、php5.2 と 5.3 の実装ではいくつかの違いがあります。この記事では主に 5.2 を例に挙げ、5.3 以降の変更点については後ほど説明します。

PHPパラメータ受け渡し原理の詳細な分析

上の図は 5.2 の argument_stack の大まかな図で、シンプルで明瞭に見えます。このうちスタックの上下はNULL固定となります。関数によって受け取られたパラメータは、左から右の順序でスタックにプッシュされます。最後に追加の長い値がプッシュされ、スタック内のパラメータの数 (上の図では 10) が示されることに注意してください。

argument_stack にプッシュされるパラメータは何ですか?実際、これらは zval 型のポインタです。 それらが指す zva は、CV 変数、is_ref=1 の変数、または定数または定数文字列である可能性があります。

EG (argument_stack) は、php5.2 では zend_ptr_stack タイプとして具体的に実装されています。

typedef struct _zend_ptr_stack {
    int top // スタック内の現在の要素の数
  1. int max // スタックに格納されている要素の最大数
  2. void **elements; stack
  3. void * *top_element; // スタックの先頭
  4. } zend_ptr_stack;
  5. argument_stack を初期化する argument_stack の初期化作業は、PHP が特定のリクエストを処理する前に行われます。より正確には、PHP インタープリターの起動プロセス中に行われます。
init_executor 関数内に次の 2 行が見つかりました。

zend_ptr_stack_init(&EG(argument_stack));

zend_ptr_stack_push(&EG(argument_stack), (void *) NULL);
    コードをコピー
  1. これらの 2 行は、初期化 EG(ar Gument_stack)、その後NULL をプッシュします。 EG はグローバル変数であるため、zend_ptr_stack_init が実際に呼び出される前は、EG (argument_stack) 内のすべてのデータはすべて 0 になります。
zend_ptr_stack_init の実装は非常に簡単です。 + gt; max = PTR_STACK_BLOCK_SIZE; // スタック サイズは 64 に初期化されます stack->top = 0; // 現在の要素数は 0 です}

argument_stack が初期化されたら、 NULL がプッシュされます。ここで詳しく説明する必要はありませんが、この NULL には実際には意味がありません。

NULL がスタックにプッシュされた後、argument_stack 全体の実際のメモリ配分は次のようになります。
  1. パラメータをスタックにプッシュする 最初の NULL をプッシュした後、別のパラメーターがスタックにプッシュされると、argument_stack で次のアクションが発生します。 スタック->トップ++; *(スタック->top_element++) = パラメーター;
  2. 単純な PHP コードを使用して問題を説明します。
function foo( $str ){
print_r(123);}foo("hello world");

コードをコピーPHPパラメータ受け渡し原理の詳細な分析

上記のコードは、foo を呼び出すときに文字列定数を渡します。したがって、実際にスタックにプッシュされるのは、ストレージ「hello world」を指す zval です。 vld を使用して、コンパイルされたオペコードを表示します。

  1. line # * op fetch ext return オペランド
  2. ------------------------------------------------- - -------------------------------------------------
  3. 3 0 > NOP
  4. 6 1 SEND_VAL OP1[ IS_CONST (458754) 'hello world' ]
  5. 2 DO_FCALL 1 OP1[ IS_CONST (458752) 'foo' ]
  6. 15 3 > RETURN OP1[ IS_CONST (0) 1 ]
Copy コード

SEND_VAL 命令が実際に行うことは、「hello world」を argument_stack にプッシュすることです。

  1. int ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. zval *valptr, *value;
  4. value = &opline- &g t;op1.u.定数 ;

  5. ALLOC_ZVAL(valptr);
  6. INIT_PZVAL_COPY(valptr, value);
  7. if (!0) {
  8. zval_copy_ctor(valptr);
  9. }
  10. // スタックにプッシュし、valptr は格納先をポイントします。 hello world zval

  11. zend_ptr_stack_push(&EG(argument_stack), valptr);
  12. ……
  13. }
コードをコピーします
プッシュが完了した後の argument_stack は次のとおりです:

PHPパラメータ受け渡し原理の詳細な分析3

パラメータの数 前述したように、実際にはすべてのパラメータをスタックにプッシュするだけではありません。 PHP は、パラメーターの数を表す追加の数値もプッシュします。この作業は、SEND_XXX コマンド中には発生しません。実際、関数を実際に実行する前に、PHP はパラメーターの数をスタックにプッシュします。

引き続き上記の例を使用すると、DO_FCALL 命令を使用して foo 関数を呼び出します。 foo を呼び出す前に、php は argument_stack の最後のブロックを自動的に埋めます。

  1. static int zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
  2. {
  3. ……
  4. // 2 つの値を argument_stack にプッシュします
  5. // 1 つはパラメータの数です (つまり、opline->extended_value)
  6. // 1 つはスタックの最上位にある識別子 NULL です
  7. zend_ptr_stack_2_push(&EG(argument_stack), (void *)(zend_uintptr_t)opline->extended_value, NULL);
  8. if (EX(function_state).function->type = = ZEND_INTERNAL_FUNCTION) {
  9. ……
  10. }
  11. else if (EX(function_state).function->type == ZEND_USER_FUNCTION) {
  12. ……
  13. // foo 関数を呼び出す
  14. zend_execute(EG(active_op_array) TSRMLS_CC);
  15. }
  16. else { /* ZEND_OVERLOADED_FUNCTION +/ used for foo 呼び出しの argument_stack 全体が完了しました。
  17. パラメータを取得する 上の例を続けます。 foo 関数を詳しく見て、foo のオペコードがどのようなものかを確認してください。
  18. line # * op fetch ext return オペランド
  19. ------------------------------------------------- - -------------------------------------------------
  20. 3 0 > RECV OP1[ IS_CONST (0) 1 ]
4 1 SEND_VAL OP1[ IS_CONST (5) 123 ]
2 DO_FCALL 1 OP1[ IS_CONST (459027) 'print_r' ] 5 3 > RETURN OP1[ IS_CONST (0) ) null ]

PHPパラメータ受け渡し原理の詳細な分析コードをコピー

最初の命令は RECV で、文字通りスタック内のパラメータを取得するために使用されます。実はSEND_VALとRECVはなんとなく対応している気がします。 SEND_VAL は各関数呼び出しの前に、RECV が関数内で実行されます。完全に対応しているとは言えませんが、実はRECV命令は必ずしも必要ではありません。 RECV は、ユーザー定義関数が呼び出された場合にのみ生成されます。私たちが作成する拡張関数と PHP に付属する組み込み関数には RECV がありません。

各 SEND_VAL と RECV は 1 つのパラメーターのみを処理できることに注意してください。つまり、パラメータの受け渡し処理で複数のパラメータがある場合、複数の SEND_VAL と複数の RECV が生成されます。これは非常に興味深いトピックにつながります。パラメーターの受け渡しとパラメーターの取得の順序は何ですか?

答えは、SEND_VAL がスタック上のパラメータを左から右にプッシュするのに対し、RECV はパラメータを左から右に取得するということです。

  1. static int ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. ……//param はスタックの先頭から順にパラメータを取得します
  4. if (zend_ptr_stack_get_arg(ar) g_num , (void * *) ¶m TSRMLS_CC)==FAILURE) {
  5. ……
  6. } else {
  7. zend_free_op free_res;
  8. zval **var_ptr;
  9. // パラメータを確認します

  10. zend_verify_arg_type((zend_function; *) EG( active_op_array), arg_num, *param TSRMLS_CC);
  11. var_ptr = get_zval_ptr_ptr(&opline->result, EX(Ts), &free_res, BP_VAR_W);
  12. // パラメータを取得します
  13. if (PZVAL_IS_REF(*param)) {
  14. zend_assign_to_variable_reference( var_ptr, param TSRMLS_CC);
  15. } else {
  16. zend_receive(var_ptr, *param TSRMLS_CC);
  17. }
  18. }
  19. ZEND_VM_NEXT_OPCODE();

  20. }& lt;/p>
コピー コード
zend_assign_to_variable_reference と zend_receive は「パラメータの取得」を完了します。 「パラメータを取得する」ということは、実際には何をするのかを理解するのが簡単ではありません。

最終的には、「パラメーターの取得」は、現在の関数の実行中にこのパラメーターを「シンボル テーブル」に追加することです。これは、具体的には EG (current_execute_data)->symbol_table に対応します。この例では、RECV が完了した後、関数本体のsymbol_table にシンボル 'str' があり、その値は "hello world" です。

しかし、RECV はパラメーターを読み取るだけで、スタック上でポップ操作を実行しないため、argument_stack はまったく変更されていません。

PHPパラメータ受け渡し原理の詳細な分析5

argument_stack をクリーンアップ foo 内の print_r も関数呼び出しであるため、スタック プッシュ --> スタック クリア操作も発生します。したがって、print_r が実行される前の argument_stack は次のようになります。

PHPパラメータ受け渡し原理の詳細な分析6

print_r が実行された後、argument_stack は foo が RECV を終了したばかりの状態に戻ります。

print_r を呼び出す具体的なプロセスは、この記事の焦点では​​ありません。私たちが関心があるのは、PHP が foo を呼び出した後に argument_stack をどのようにクリーンアップするかということです。

上記の do_fcall コード スニペットからわかるように、クリーニング作業は zend_ptr_stack_clear_multiple によって完了します。

  1. static inline void zend_ptr_stack_clear_multiple(TSRMLS_D)
  2. {
  3. void **p = EG(argument_stack).top_element-2;
  4. // スタックの先頭に保存されているパラメータの数を取得します
  5. int delete_count = ( int)(zend_uintptr_t ) *p;
  6. EG(argument_stack).top -= (delete_count+2);
  7. // (--delete_count>=0) の間、上から下にクリーンアップします
  8. zval *q = * (zval * *)(--p);
  9. *p = NULL;
  10. zval_ptr_dtor(&q);
  11. }
  12. EG(argument_stack).top_element = p;
  13. }
コードをコピー
ここではスタック内の zval ポインタ、zval_ptr_dtor が使用されます。 zval_ptr_dtor は、refcount を 1 だけデクリメントします。refcount が 0 に減少すると、変数を格納しているメモリ領域が実際にリサイクルされます。

この記事の例では、foo が呼び出された後、「hello world」の zval ステータスが保存されます。

    value "hello world"
  1. refcount 1
  2. type 6
  3. is_ref 0
コードをコピー

refcount は 1 のみなので、zval_ptr_dtor は実際にメモリから「hello world」を破棄します。

スタック削除後のargument_stackのメモリステータスは次のとおりです:

PHPパラメータ受け渡し原理の詳細な分析7

上の図の argument_stack は、初期化されたばかりのときと同じであることがわかります。この時点で、argument_stack は次の関数呼び出しに対応できるようになります。

記事冒頭の質問に戻ります... なぜfree(str)が必要ないのでしょうか? argument_stack を理解すると、この問題を簡単に理解できるようになります。

str は、実際に zval に「hello world」が格納されているメモリアドレスを指しているためです。拡張関数が次のとおりであると仮定します。

  1. PHP_FUNCTION(test)

  2. {
  3. char* str;
  4. int str_len;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, " s" ,&str , &str_len) == FAILURE) {

  6. RETURN_FALSE;
  7. }
  8. str[0] = 'H';

  9. }
コードをコピーしてください
電話

  1. $a = "hello world";
  2. test($a);
  3. echo $a;
コードをコピー
すると「Hello world」と出力されます。 test を呼び出すとき、$a の参照は渡しませんが、実際の効果は test(&$a) と同等です。

簡単に言うと、CV 配列であろうとargument_stack であろうと、メモリ内には $a のコピーが 1 つだけ存在します。 zend_parse_parameters は関数実行用のデータのコピーをコピーしません。実際、そうすることはできません。したがって、関数が完了したときに $a が他の場所で使用されていない場合、PHP は argument_stack をクリーンアップするときに $a を解放するのに役立ちます。他のコードで使用されている場合、手動で解放することはできません。そうしないと、$a のメモリ領域が破壊されてしまいます。

拡張関数の作成に使用されるすべての変数が PHP によって自動的にリサイクルされるわけではないことに注意してください。だから、自由になるときは、優しくしないでください:)


関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート