首頁  >  文章  >  後端開發  >  PHP傳參原理深入解析

PHP傳參原理深入解析

WBOY
WBOY原創
2016-07-25 09:13:201285瀏覽

在寫php擴充時,似乎參數(即傳給zend_parse_parameters的變數)是不需要free的。 舉例:

  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. // 不需要free(str)
  9. }
複製程式碼
運作正常: 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+的變化後面抽空再說。

PHP傳參原理深入解析

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

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

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

  1. typedef struct _zend_ptr_stack {
  2. int top; // 堆疊中目前元素的數量
  3. int max;的數量
  4. void **elements; // 棧底
  5. void **top_element; // 棧頂
  6. } zend_ptr_stack;
複製程式碼

複製程式碼

初始化argument_stack 初始化argument_stack的工作是發生在php處理特定的請求之前,更準確地說是處於php解釋器的啟動過程之中。
在init_executor函數裡我們發現如下2行:
zend_ptr_stack_init(&EG(argument_stack));zend_ptr_stack_push(&EG(argument)> 🎜>複製程式碼

這2行分別代表著,初始化EG(argument_stack),緊接著壓入一個NULL。由於EG是個全域變量,因此在實際呼叫zend_ptr_stack_init之前,EG(argument_stack)中的所有資料全部為0。

zend_ptr_stack_init實作很簡單。

  1. ZEND_API void zend_ptr_stack_init(zend_ptr_stack *stack)
  2. {
  3. stack->topp. sizeof(void *)*PTR_STACK_BLOCK_SIZE);
  4. stack->max = PTR_STACK_BLOCK_SIZE; // 堆疊的大小初始化成64
  5. stack->top = 0; // 目前元素個數為0
  6. }
  7. }
複製程式碼

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

PHP傳參原理深入解析NULL入棧之後,整個argument_stack的實際記憶體分佈如下:

參數入棧 在壓入第一個NULL之後,一旦再有參數入棧,則argument_stack會發生以下動作: stack->top++; *(stack->top_element++) = 參數;

用一段簡單的php程式碼來說明問題:
  1. function foo( $str ){
  2. print_r(123);
  3. }
  4. foo("hello world");
複製程式碼

上述程式碼在呼叫foo時,傳入了一個字串常數。因此,實際上被壓入堆疊的是一個指向儲存「hello world」的zval。用vld來查看編譯之後的opcode:

  1. line # * op fetch ext return operands
  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 ]
複製程式碼

SEND_VALhello指令」壓入argument_stack。

  1. int ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. value = &opline->op1.u.constant;

  4. ALLOC_ZVAL(valptr);
  5. INIT_PZVAL_COPY(valptr, value);
  6. if (!0) { }
  7. // 入棧,valptr指向存放hello world的zval

  8. zend_ptr_stack_push(&EG(argument_stack), valptr);
  9. ……
  10. ……
  11. >}
複製程式碼
入棧完成之後的argument_stack為:

PHP傳參原理深入解析3參數個數 前文說到,其實並非把所有參數入棧就完事了。 php也會額外壓入一個數字,表示參數的個數,這個工作並非發生在SEND_XXX指令時。實際上,在真正執行函數之前,php會將參數個數入堆疊。

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

    static int zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
  1. {
  2. …extended_value)
  3. // 一個是標識棧頂的NULL
  4. zend_ptr_stack_2_push(&EG(argument_stack), (void *)(zend_uintptr_t));
  5. ……
  6. if (EX(function_state).function->type == ZEND_INTERNAL_FUNCTION) {
  7. ……
  8. }
  9. else if (EX(function_state).==function->type ==function-> == ZEND_USER_FUNCTION) {
  10. ……
  11. // 呼叫foo函數
  12. zend_execute(EG(active_op_array) TSRMLS_CC);
  13. }
  14. else { /* ZENDLS_CC);
  15. }
  16. else { /* ZEND_OVERP> }
  17. ……
  18. // 清理argument_stack
  19. zend_ptr_stack_clear_multiple(TSRMLS_C);
  20. ……
  21. ZEND_VM_NEXT_OPCODE();
  22. >
  23. 壓入參數個數和NULL之後,用於foo呼叫的整個argument_stack已然完成。
  24. 取得參數 繼續跟進上面的例子。 深入foo函數,看看foo的opcode是什麼樣子的。
line # * op fetch ext return operands

--------------------- -------------------------------------------------- ----------PHP傳參原理深入解析 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 ]
  1. 複製程式碼
  2. 第一條指令是RECV,從字面上理解便是用來取得堆疊中參數的。實際上,SEND_VAL和RECV有點對應的感覺。每次函數呼叫之前SEND_VAL,在函數內部進行RECV。為什麼不說是完全對應,實際上RECV指令並非一定需要。只有當使用者定義的函數被呼叫是,才會產生RECV。我們寫的擴充函數,php自帶的內建函數,都不會有RECV。

    需要額外指出的是,每次SEND_VAL和RECV 均只能處理一個參數。也就是說如果傳參的過程中有多個參數,那麼就會產生若干SEND_VAL以及若干RECV。這裡引出一個很有趣的議題,傳入參數和取得參數的順序是怎麼樣的呢?

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

    1. static int ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

    2. {
    3. {棧底
    4. if (zend_ptr_stack_get_arg(arg_num, (void **) ¶m TSRMLS_CC)==FAILURE) {
    5. ……
    6. } else {
    7. zend_free_op free_resval🎜> } *var_ptr;
    8. // 驗證參數

    9. zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *param TSRMLS_CC);
    10. var_ptr = get_zparam TSRMLS_CC);
    11. var_ptr = get_zparam TSRMLS_CC);
    12. var_ptr = get_zparam TSRMLS_CC);
    13. var_ptr = get_zparam. EX(Ts), &free_res, BP_VAR_W);
    14. // 取得參數
    15. if (PZVAL_IS_REF(*param)) {
    16. zend_assign_to_variable_reference(varm)) {
    17. zend_assign_to_variable_reference(varm)) {
    18. zend_assign_to_variable_reference(var_pRMs) }; 🎜> zend_receive(var_ptr, *param TSRMLS_CC);
    19. }
    20. }
    21. 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操作。

    PHP傳參原理深入解析5清理argument_stack foo內部的print_r也是一個函數調用,因此也會產生壓棧-->清棧的操作。因此print_r執行之前的argument_stack為:

    PHP傳參原理深入解析6print_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)
    1. {
    2. voidoidp = EG(argument_p. / 取棧頂保存的參數個數
    3. int delete_count = (int)(zend_uintptr_t) *p;
    4. EG(argument_stack).top -= (delete_count+2);
    5. // 從上至下,依序清理
    6. while (--delete_count>=0) {
    7. zval *q = *(zval **)(--p);
    8. *p = NULL;
    9. zval_ptr_dtor (&q);
    10. }
    11. EG(argument_stack).top_element = p;
    12. }
    13. 複製程式碼
    在本文範例中,foo調用完畢之後,儲存「hello world」的zval狀態為:

    value "hello world"
    refcount 1
      type 6
    1. is_ref 0
    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. }}
    複製程式碼
    則調用

    1. $a = "hello world";
    2. test($a);
    3. echo $a;
    複製程式碼

    會輸出「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