深入理解PHP傳參原理(PHP5.2)


首先說下今天想到的一個問題。在編寫php擴展的時候,似乎參數(即傳給zend_parse_parameters的變量)是不需要free的。舉例:

PHP_FUNCTION(test)
{
    char*  str;
    int    str_len;

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

    php_printf(str);

// 無需free(str) }

運行正常:

test("Hello World"); // 打印Hello World

這里不用擔心test函數會發生內存泄露,php會自動幫我們回收這些用於保存參數的變量。

那php究竟是如何做到的呢?要解釋這個問題,還是得看php是怎么傳遞參數的。

EG(argument_stack)簡介

簡單來講,在php中的EG中保存了一個專門用於存放參數的棧,名為argument_stack。每當發生函數調用的時候,php會將傳入的參數壓進EG(argument_stack)。一旦函數調用結束,則EG(argument_stack)被清理,並且等待下一次的函數調用。

關於EG(argument_stack)的struct結構、用途,php5.2和5.3實現有一些區別。本文主要以5.2為例,5.3+的變化后面抽空再說。

上圖是5.2中argument_stack的大概示意圖,看起來簡單明了。其中,棧頂和棧底固定為NULL。函數接收的參數按照從左到右的順序,依次被壓入棧。注意,最后會被額外壓入一個long型的值,表示棧里的參數個數(上圖中為10)。

那被壓入argument_stack的參數究竟是什么呢?其實是一個個zval類型的指針。它們指向的zva有可能是CV變量,有可能是is_ref=1的變量,還有可能是一個常量數字,或者常量字符串。

EG(argument_stack)在php5.2中被具體實現為zend_ptr_stack類型:

typedef struct _zend_ptr_stack {
    int top;                       // 棧中當前元素的個數
    int max;                       // 棧中最多存放元素的個數
    void **elements;               // 棧底
    void **top_element;            // 棧頂
} zend_ptr_stack;

初始化argument_stack

初始化argument_stack的工作是發生在php處理具體的請求之前,更准確說是處於php的php_request_startup->zend_activate->init_executor過程之中。

在init_executor函數里我們發現如下2行:

zend_ptr_stack_init(&EG(argument_stack));
zend_ptr_stack_push(&EG(argument_stack), (void *) NULL);

這2行分別代表着,初始化EG(argument_stack),緊接着壓入一個NULL。由於EG是個全局變量,因此在實際調用zend_ptr_stack_init之前,EG(argument_stack)中的所有數據全部為0。

zend_ptr_stack_init實現很簡單。

ZEND_API void zend_ptr_stack_init(zend_ptr_stack *stack)
{
    stack->top_element = stack->elements = (void **) emalloc(sizeof(void *)*PTR_STACK_BLOCK_SIZE);
    stack->max = PTR_STACK_BLOCK_SIZE;   // 棧的大小被初始化成64
    stack->top = 0;                      // 當前元素個數為0
}

一旦argument_stack被初始化完,則立即會被壓入NULL。這里無須深究,這個NULL其實沒有任何的含義。

NULL入棧之后,整個argument_stack的實際內存分布如下:

參數入棧

在壓入第一個NULL之后,一旦再有參數入棧,則argument_stack會發生如下動作:

stack->top++;
*(stack->top_element++) = 參數;

我們用一段簡單的php代碼來說明問題:

function foo( $str ){
    print_r(123);
}
foo("hello world");

上述代碼在調用foo的時候,傳入了一個字符串常量。因此,實際上被壓入棧的是一個指向存儲“hello world”的zval。用vld來查看編譯之后的opcode:

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。

int ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
        ……
        zval *valptr, *value;

        value = &opline->op1.u.constant;
        ALLOC_ZVAL(valptr);
        INIT_PZVAL_COPY(valptr, value);
        if (!0) {
            zval_copy_ctor(valptr);
        }

// 入棧,valptr指向存放hello world的zval zend_ptr_stack_push(
&EG(argument_stack), valptr); …… }

入棧完成之后的argument_stack為:

參數個數

前文說到,實際上並非把所有參數入棧就完事了。php還會額外壓入一個數字,表示參數的個數,這個工作並非發生在SEND_XXX指令的時候。實際上,在真正執行函數之前,php會將參數個數入棧。

繼續沿用上面的例子,DO_FCALL 指令用於調用foo函數。在調用foo之前,php會自動填入argument_stack最后一塊。

static int zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    // 在argument_stack中壓入2個值
    // 一個是參數個數(即opline->extended_value)
    // 一個是標識棧頂的NULL
    zend_ptr_stack_2_push(&EG(argument_stack), (void *)(zend_uintptr_t)opline->extended_value, NULL);
    ……
    if (EX(function_state).function->type == ZEND_INTERNAL_FUNCTION) {
        ……
    }
    else if (EX(function_state).function->type == ZEND_USER_FUNCTION) {
        ……
        // 調用foo函數
        zend_execute(EG(active_op_array) TSRMLS_CC);
    }
    else { /* ZEND_OVERLOADED_FUNCTION */
        ……
    }
    ……
    // 清理argument_stack
    zend_ptr_stack_clear_multiple(TSRMLS_C);
    ……
    ZEND_VM_NEXT_OPCODE();
}

壓入參數個數和NULL之后,用於foo調用的整個argument_stack已然完成。

獲取參數

繼續跟進上面的例子。讓我們深入到foo函數,看看foo的opcode是什么樣子的。

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 均只能處理一個參數。也就是說如果傳參的過程中有多個參數,那么會產生若干SEND_VAL以及若干RECV。這里引出一個很有趣的話題,傳入參數和獲取參數的順序是怎樣的呢?

答案是,SEND_VAL會將參數從左至右的進行壓棧,而RECV一樣的從左至右獲取參數。

static int ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……// param拿參數的順序是沿着棧頂-->棧底
    if (zend_ptr_stack_get_arg(arg_num, (void **) &param TSRMLS_CC)==FAILURE) {
        ……
    } else {
        zend_free_op free_res;
        zval **var_ptr;

        // 驗證參數
        zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *param TSRMLS_CC);
        var_ptr = get_zval_ptr_ptr(&opline->result, EX(Ts), &free_res, BP_VAR_W);
        
        // 獲取參數
        if (PZVAL_IS_REF(*param)) {
            zend_assign_to_variable_reference(var_ptr, param TSRMLS_CC);
        } else {
            zend_receive(var_ptr, *param TSRMLS_CC);
        }
    }

    ZEND_VM_NEXT_OPCODE();
}

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代碼片段中可以看出,清理工作由zend_ptr_stack_clear_multiple完成的。

static inline void zend_ptr_stack_clear_multiple(TSRMLS_D)
{
    void **p = EG(argument_stack).top_element-2;
    // 取棧頂保存的參數個數
    int delete_count = (int)(zend_uintptr_t) *p; 
    EG(argument_stack).top -= (delete_count+2);
    
    // 從上至下,依次清理
    while (--delete_count>=0) {
        zval *q = *(zval **)(--p);
        *p = NULL;
        zval_ptr_dtor(&q);
    }
    EG(argument_stack).top_element = p;
}

注意這里清理棧中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”的內存地址。假設擴展函數如下:

PHP_FUNCTION(test)
{
    char*  str;
    int    str_len;

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

    str[0] = 'H';
}

則調用

$a = "hello world";
test($a);
echo $a;

會輸出“Hello world”。盡管我們調用test的時候,並非是傳 $a 的引用,但實際效果相當於 test(&$a) 。

簡單來說,內存中只有一份 $a ,不管是CV數組中,還是在argument_stack中。而zend_parse_parameters並沒有拷貝一份數據用於函數執行,事實上它也不能這么做。因此,當函數完畢之后,如果沒有其他地方會用到$a,php清理argument_stack時會幫我們free。如果仍然其他代碼在使用,就更加不能手動free了,否則會破壞 $a 的內存區域。

需要注意的是,並非寫擴展函數中用到的每個變量,php都會自動回收。所以該free的時候,切勿手軟:)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM