ホームページ >php教程 >php手册 >PHP パラメータ受け渡しの原理を深く理解する

PHP パラメータ受け渡しの原理を深く理解する

WBOY
WBOYオリジナル
2016-06-16 09:16:23966ブラウズ

まず、今日頭に浮かんだ疑問についてお話します。 PHP 拡張機能を作成する場合、パラメーター (つまり、zend_parse_parameters に渡される変数) を自由にする必要はないようです。例:

<span PHP_FUNCTION(test)
{
    </span><span char</span>*<span   str;
    </span><span int</span><span     str_len;

    </span><span if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span "</span><span s</span><span "</span>, &str, &str_len) ==<span  FAILURE) {
        RETURN_FALSE;
    }

    php_printf(str);<br />    <br />    </span><span //</span><span  无需free(str)   </span>
}

正常に動作しています:

test("Hello World"); <span //</span><span  打印Hello World</span>

テスト関数でのメモリ リークを心配する必要はありません。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 以降の変更点については後ほど説明します。

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

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

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

typedef <span struct</span><span  _zend_ptr_stack {
    </span><span int</span><span  top;                       // 栈中当前元素的个数
    </span><span int</span><span  max;                       // 栈中最多存放元素的个数
    </span><span void</span> **<span elements;               // 栈底
    </span><span void</span> **<span top_element;            // 栈顶
} zend_ptr_stack;</span>

argument_stack を初期化します

argument_stack の初期化作業は、PHP が特定のリクエストを処理する前に、より正確には PHP インタープリターの起動プロセス中に発生します。

init_executor 関数には次の 2 行があります:

zend_ptr_stack_init(&<span EG(argument_stack));
zend_ptr_stack_push(</span>&EG(argument_stack), (<span void</span> *) NULL);

これらの 2 行は、それぞれ EG (argument_stack) の初期化と、NULL のプッシュを表しています。 EG はグローバル変数であるため、zend_ptr_stack_init が実際に呼び出される前は、EG (argument_stack) 内のすべてのデータはすべて 0 になります。

zend_ptr_stack_init の実装は非常に簡単です。

ZEND_API <span void</span> zend_ptr_stack_init(zend_ptr_stack *<span stack)
{
    stack</span>->top_element = stack->elements = (<span void</span> **) emalloc(<span sizeof</span>(<span void</span> *)*<span PTR_STACK_BLOCK_SIZE);
    stack</span>->max =<span  PTR_STACK_BLOCK_SIZE;   // 栈的大小被初始化成64
    stack</span>->top = <span 0</span><span ;                      // 当前元素个数为0
}</span>

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

NULL がスタックにプッシュされた後、argument_stack 全体の実際のメモリ配分は次のようになります:

パラメータをスタックにプッシュします

最初の NULL をプッシュした後、別のパラメーターがスタックにプッシュされると、argument_stack で次のアクションが発生します。

stack->top++<span ;
</span>*(stack->top_element++) =<span  参数;</span>

簡単な PHP コードを使用して問題を説明します。

<span function</span> foo( <span $str</span><span  ){
    </span><span print_r</span>(<span 123</span><span );
}
foo(</span>"hello world");

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

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   NOP
   6     1      SEND_VAL                                                  OP1[  IS_CONST (458754) 'hello world' ]
         2      DO_FCALL                                      1           OP1[  IS_CONST (458752) 'foo' ]
  15     3    > RETURN                                                    OP1[  IS_CONST (0) 1 ]

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

<span int</span><span  ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
        &hellip;&hellip;</span><span 
        zval </span>*<span valptr, </span>*<span value;

        value </span>= &opline-><span op1.u.constant;
        ALLOC_ZVAL(valptr);
        INIT_PZVAL_COPY(valptr, value);
        </span><span if</span> (!<span 0</span><span ) {
            zval_copy_ctor(valptr);
        }<br /><br />        <span // 入栈,<span valptr</span>指向存放hello world的zval</span>
        zend_ptr_stack_push(</span>&<span EG(argument_stack), valptr);  
<span         &hellip;&hellip;</span>
}</span>

プッシュ完了後の argument_stack は次のとおりです:

パラメータの数

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

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

<span static</span> <span int</span><span  zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
    &hellip;&hellip;
    </span><span //</span><span  在argument_stack中压入2个值
    </span><span //</span><span  一个是参数个数(即opline->extended_value)
    </span><span //</span><span  一个是标识栈顶的NULL</span>
    zend_ptr_stack_2_push(&EG(argument_stack), (<span void</span> *)(zend_uintptr_t)opline-><span extended_value, NULL);
    &hellip;&hellip;
    </span><span if</span> (EX(function_state).function->type ==<span  ZEND_INTERNAL_FUNCTION) {
        &hellip;&hellip;
    }
    </span><span else</span> <span if</span> (EX(function_state).function->type ==<span  ZEND_USER_FUNCTION) {
        &hellip;&hellip;
        </span><span //</span><span  调用foo函数</span>
<span         zend_execute(EG(active_op_array) TSRMLS_CC);
    }
    </span><span else</span> { <span /*</span><span  ZEND_OVERLOADED_FUNCTION </span><span */</span><span 
        &hellip;&hellip;
    }
    &hellip;&hellip;
    </span><span //</span><span  清理<span argument_stack</span></span>
<span     zend_ptr_stack_clear_multiple(TSRMLS_C);
    &hellip;&hellip;
    ZEND_VM_NEXT_OPCODE();
}</span>

パラメータの数と NULL をプッシュした後、foo 呼び出しの argument_stack 全体が完了します。

パラメータを取得

引き続き上記の例に従います。 foo 関数を詳しく見て、foo のオペコードがどのようなものかを見てみましょう。

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   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 ]

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

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

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

<span static</span> <span int</span><span  ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    &hellip;&hellip;</span><span //</span><span  param拿参数的顺序是沿着栈顶-->栈底</span>
    <span if</span> (zend_ptr_stack_get_arg(arg_num, (<span void</span> **) &param TSRMLS_CC)==<span FAILURE) {
        &hellip;&hellip;
    } </span><span else</span><span  {
        zend_free_op free_res;
        zval </span>**<span var_ptr;

        </span><span //</span><span  验证参数</span>
        zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *<span param TSRMLS_CC);
        var_ptr </span>= get_zval_ptr_ptr(&opline->result, EX(Ts), &<span free_res, BP_VAR_W);
        
        </span><span //</span><span  获取参数</span>
        <span if</span> (PZVAL_IS_REF(*<span param)) {
            zend_assign_to_variable_reference(var_ptr, param TSRMLS_CC);
        } </span><span else</span><span  {
            zend_receive(var_ptr, </span>*<span param TSRMLS_CC);
        }
    }

    ZEND_VM_NEXT_OPCODE();
}</span>

zend_assign_to_variable_reference と zend_receive はどちらも「パラメータの取得」を完了します。 「パラメータを取得する」ということは、実際には何をするのかを理解するのが簡単ではありません。

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

但argument_stack并没有发生一丝变化,因为RECV仅仅是读取参数,而不会对栈产生类似pop操作。

清理argument_stack

foo内部的print_r也是一个函数调用,因此也会产生压栈-->清栈的操作。因此print_r执行之前的argument_stack为:

print_r执行之后argument_stack又回到了foo刚RECV完的状态。

具体调用print_r的过程并非本文阐述的重点。我们关心的是当调用foo结束之后,php是如何清理argument_stack的。

上面展示的do_fcall代码片段中可以看出,清理工作由

<span static</span> inline <span void</span><span  zend_ptr_stack_clear_multiple(TSRMLS_D)
{
    </span><span void</span> **p = EG(argument_stack).top_element-<span 2</span><span ;
    </span><span //</span><span  取栈顶保存的参数个数</span>
    <span int</span> delete_count = (<span int</span>)(zend_uintptr_t) *<span p; 
    EG(argument_stack).top </span>-= (delete_count+<span 2</span><span );
    
    </span><span //</span><span  从上至下,依次清理</span>
    <span while</span> (--delete_count>=<span 0</span><span ) {
        zval </span>*q = *(zval **)(--<span p);
        </span>*p =<span  NULL;
        zval_ptr_dtor(</span>&<span q);
    }
    EG(argument_stack).top_element </span>=<span  p;
}</span>

注意这里清理栈中zval指针,使用的是zval_ptr_dtor。zval_ptr_dtor会将refcount减1,一旦refcount减为0,则保存该变量的内存区域会被真正的回收掉。

在本文示例中,foo调用完毕之后,保存“hello world”的zval状态为:

value        "hello world"
refcount     1
type         6
is_ref       0

由于refcount只剩1,因此,zval_ptr_dtor会将“hello world”真正从内存中销毁。

消栈完毕之后的argument_stack内存状态为:

可以看出上图中的argument_stack与刚被初始化之后是一样的。此时argument_stack真正做好了迎接下一次函数调用的准备。

回到文章刚开始的问题...

为何无需free(str)呢?弄明白了argument_stack之后就很好理解这个问题了。

因为str指向的是zval中实际存放“hello world”的内存地址。假设扩展函数如下:

<span PHP_FUNCTION(test)
{
    </span><span char</span>*<span   str;
    </span><span int</span><span     str_len;

    </span><span if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span "</span><span s</span><span "</span>, &str, &str_len) ==<span  FAILURE) {
        RETURN_FALSE;
    }

    str[</span><span 0</span>] = <span '</span><span H</span><span '</span><span ;
}</span>

则调用

$a = <span "</span><span hello world</span><span "</span><span ;
test($a);
echo $a;</span>

会输出“Hello world”。尽管我们调用test的时候,并非是传$a的引用,但实际效果相当于test(&$a)。

简单来说,内存中只有一份$a,不管是CV数组中,还是在argument_stack中。而zend_parse_parameters并没有拷贝一份数据用于函数执行,事实上它也不能这么做。因此,当函数完毕之后,如果没有其他地方会用到$a,php清理argument_stack时会帮我们free。如果仍然其他代码在使用,就更加不能手动free了,否则会破坏$a的内存区域。

需要注意的是,并非写扩展函数中用到的每个变量,php都会自动回收。所以该free的时候,切勿手软:)

 

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