[php-src]Php擴展的內存泄漏處理思路


內容均以php5.6.14為例.

 

一. 封裝函數時產生 memory leaks.

 

[weichen@localhost www]$ php 2.php 
[122,3333]
[Tue Jul 10 15:34:42 2016]  Script:  '/home/www/2.php'
/home/weichen/Downloads/pdoner/pdoner.c(83) :  Freeing 0x7F86B52F79F8 (32 bytes), script=/home/www/2.php
[Tue Jul 10 15:34:42 2016]  Script:  '/home/www/2.php'
/home/weichen/Downloads/php-5.6.14/ext/standard/string.c(1161) :  Freeing 0x7F86B52F7B60 (79 bytes), script=/home/www/2.php
=== Total 2 memory leaks detected ===

php編譯開啟 --enable-debug,如果擴展中存在內存泄漏,會有相應提示。內存泄漏問題相當困擾。

為什么會有內存泄露?是你的函數一直在申請內存做某件事,而功能完成后沒有釋放內存。

 

網上的 hello world 程序很多,基本是不講內存處理的,即便稍作修改,也無法用於真實項目。

所以釋放內存也是底層程序的關鍵點。現在分析一下上面的提示信息,總共檢測有 2 處內存泄漏。

 

第(1)處:

[Tue Jul 10 15:34:42 2016]  Script:  '/home/www/2.php'
/home/weichen/Downloads/pdoner/pdoner.c(83) :  Freeing 0x7F86B52F79F8 (32 bytes), script=/home/www/2.php

 

提示我們 pdoner.c(83) 有問題,回到程序中是 MAKE_STD_ZVAL(glue); 給 glue 初始化沒問題,問題是用完了沒有釋放,很容易想到的是要釋放掉 glue。

zval_ptr_dtor(&glue);

 

編譯安裝依舊有問題,出現段錯誤一般是指針使用有誤:

[weichen@localhost www]$ php 2.php 
[Tue Jul 10 14:58:25 2016]  Script:  '/home/www/2.php'
---------------------------------------
/home/weichen/Downloads/php-5.6.14/Zend/zend_execute.h(79) : Block 0x7fb3f93459c8 status:
/home/weichen/Downloads/php-5.6.14/Zend/zend_variables.c(37) : Actual location (location was relayed)
Invalid pointer: ((thread_id=0x007A7C7A) != (expected=0x06567840))
Segmentation fault (core dumped)

zval_ptr_dtor 使用有什么講究?這時候最好先查閱一下內核中的用法。

 

zend_API.h 中有這樣一處用法:

#define ZVAL_ZVAL(z, zv, copy, dtor) do {       \
        zval *__z = (z);                        \
        zval *__zv = (zv);                      \
        ZVAL_COPY_VALUE(__z, __zv);             \
        if (copy) {                             \
            zval_copy_ctor(__z);                \
        }                                       \
        if (dtor) {                             \
            if (!copy) {                        \
                ZVAL_NULL(__zv);                \
            }                                   \
            zval_ptr_dtor(&__zv);               \
        }                                       \
    } while (0)

注意,在調用 zval_ptr_dtor 銷毀 __zv 之前,調用了 ZVAL_NULL(__zv) 把指針置為null。

 

所以我們照這種方式,把 glue 設為null。

ZVAL_NULL(glue);
zval_ptr_dtor(&glue);

編譯運行,可以看到只剩一處提示了。

 

第(2)處:

[Tue Jul 10 15:34:42 2016]  Script:  '/home/www/2.php'
/home/weichen/Downloads/php-5.6.14/ext/standard/string.c(1161) :  Freeing 0x7F86B52F7B60 (79 bytes), script=/home/www/2.php

當時這個還沒有解決掉,先繼續往下看,回頭再一起看這個的解決辦法。

 

二. 封裝類時產生 memory leaks. 

 

PDONER_ERRS_* 均為定義的常量,下面是無內存泄漏的版本:

/* {{{ proto public Errs::__construct(void) */
PHP_METHOD(errs, __construct)
{
    zval *msg;
    MAKE_STD_ZVAL(msg);
    array_init(msg);

    add_index_string(msg, PDONER_ERRS_SUCC, "成功", 1);
    add_index_string(msg, PDONER_ERRS_FAIL, "失敗", 1);
    add_index_string(msg, PDONER_ERRS_EXCEP, "異常", 1);
    add_index_string(msg, PDONER_ERRS_UNKNOW, "未知", 1);

    zend_update_property(errs_ce, getThis(), ZEND_STRL(PDONER_ERRS_PROPERTY_NAME_MSG), msg TSRMLS_CC);

    zval_ptr_dtor(&msg);

/*
    add_index_string(msg, PDONER_ERRS_SUCC, "成功", 0);
    add_index_string(msg, PDONER_ERRS_FAIL, "失敗", 0);
    add_index_string(msg, PDONER_ERRS_EXCEP, "異常", 0);
    add_index_string(msg, PDONER_ERRS_UNKNOW, "未知", 0);

    add_property_zval_ex(getThis(), PDONER_ERRS_PROPERTY_NAME_MSG, sizeof(PDONER_ERRS_PROPERTY_NAME_MSG), msg TSRMLS_CC);
*/
}
/* }}} */

擴展類的屬性無法直接初始化成數組和對象,所以只能以修改屬性的方式操作,在構造函數內 或者 MINIT 階段。

注意,由於屬性賦值在 construct 階段,如果沒有實例化類,擴展內部 zend_read_property 時還是沒有值的。

 

zend_update_property 第二個參數是當前對象,所以沒有辦法用在 MINIT 階段,上面用法參考了 Yaf-2.3.5:

PHP_METHOD(yaf_config_ini, __construct) {
    zval *filename, *section = NULL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|z", &filename, &section) == FAILURE) {
        zval *prop;
        MAKE_STD_ZVAL(prop);
        array_init(prop);
        zend_update_property(yaf_config_ini_ce, getThis(), ZEND_STRL(YAF_CONFIG_PROPERT_NAME), prop TSRMLS_CC);
        zval_ptr_dtor(&prop);
        return;
    }

    (void)yaf_config_ini_instance(getThis(), filename, section TSRMLS_CC);
}

 

非理想情況下:

 

1). 我們的構造函數中,如果傳了非 duplicate 的參數:add_index_string(msg, PDONER_ERRS_SUCC, "成功", 0);

調用 Errs::$msg 可以成功輸出內容,但是有段錯誤:

 

[Thu Jul 10 17:04:38 2016] Script: '/home/www/2.php'
---------------------------------------
/home/weichen/Downloads/php-5.6.14/Zend/zend_execute.h(79) : Block 0x7fbf5ed4bb4e status:
/home/weichen/Downloads/php-5.6.14/Zend/zend_variables.c(37) : Actual location (location was relayed)
Invalid pointer: ((thread_id=0x646F6C70) != (expected=0x6BF6E840))
Segmentation fault (core dumped)

 

Reference:when to duplicate string using add_index_string?

http://grokbase.com/t/php/php-dev/01285y74sp/when-to-duplicate-string-using-add-index-string

有其它地方用,則復制一份出去;沒有其它地方用,duplicate 填 0 .

 

2). 啟用注釋段代碼的情況,可以成功輸出,卻有 11 處內存泄漏:

[Thu Jul 10 17:06:56 2016] Script: '/home/www/2.php'
/home/weichen/Downloads/php-5.6.14/Zend/zend_API.c(1369) : Freeing 0x7F1900D44820 (72 bytes), script=/home/www/2.php
/home/weichen/Downloads/php-5.6.14/Zend/zend_hash.c(419) : Actual location (location was relayed)
Last leak repeated 3 times
[Thu Jul 10 17:06:56 2016] Script: '/home/www/2.php'
/home/weichen/Downloads/php-5.6.14/Zend/zend_API.c(1366) : Freeing 0x7F1900D448C8 (32 bytes), script=/home/www/2.php
Last leak repeated 3 times
[Thu Jul 10 17:06:56 2016] Script: '/home/www/2.php'
/home/weichen/Downloads/pdoner/pdoner.c(120) : Freeing 0x7F1900D45C58 (32 bytes), script=/home/www/2.php
[Thu Jul 10 17:06:56 2016] Script: '/home/www/2.php'
/home/weichen/Downloads/php-5.6.14/Zend/zend_hash.c(392) : Freeing 0x7F1900D45D58 (64 bytes), script=/home/www/2.php
/home/weichen/Downloads/php-5.6.14/Zend/zend_alloc.c(2583) : Actual location (location was relayed)
[Thu Jul 10 17:06:56 2016] Script: '/home/www/2.php'
/home/weichen/Downloads/pdoner/pdoner.c(121) : Freeing 0x7F1900D467C0 (72 bytes), script=/home/www/2.php
/home/weichen/Downloads/php-5.6.14/Zend/zend_API.c(1011) : Actual location (location was relayed)
=== Total 11 memory leaks detected ===

 

可見是沒有銷毀 zval *msg 的緣故。你加上  zval_ptr_dtor(&msg); 又會報段錯誤。

注意上面的用法沒有這樣用:add_property_zval_ex(getThis(), ZEND_STRL(PDONER_ERRS_PROPERTY_NAME_MSG), msg TSRMLS_CC);

 

三. 字符串返回值的難題.

 

回到第(2)處的字符串問題上,剛開始是這么做的:

    char *src1 = "[";
    char *ori = Z_STRVAL_P(return_value);
    char *src2 = "]";

    char *dest = (char *)emalloc(1024);
    
    strcat(dest, src1);
    strcat(dest, ori);
    strcat(dest, src2);

    RETURN_STRING(dest, 0);

顯然 dest 是長期占用內存的,但你如何在返回值之后,還能再把它銷毀呢,恐怕無法做到。

這里就要引入一個概念,當你的函數沒有返回值時,函數默認返回的變量是 zval *return_value,也就是你用它就不會有問題。

另外,我們用內核中提供的字符串連接函數 concat_function(zval *result, zval *op1, zval *op2) 代替 strcat 更有效的處理。

concat_function 在 ./Zend/zend_operators.c:1422,有時候不清楚用法最好是看它的實現。

 

使用 concat_function 之后,有一些要注意的問題,看代碼:

    // 第一種方法,引入一個變量
    zval result;

    concat_function(&result, &op1, return_value TSRMLS_CC);
    concat_function(&result, &result, &op2 TSRMLS_CC);

    // 1. zval_ptr_dtor(&return_value) was wrong.
    // 2. forget zval_dtor(return_value) will cause memory leaks.

    zval_dtor(return_value);

    // copy result to return_value;
    // if "zval result" is not zero-terminated, use ZVAL_ZVAL() instead, like the way 2. (PHP Warning:  String is not zero-terminated.)
    ZVAL_COPY_VALUE(return_value, &result);
       
    // 第二種方法,更簡潔
    concat_function(&op1, &op1, return_value TSRMLS_CC);
    concat_function(&op1, &op1, &op2 TSRMLS_CC);
    zval_dtor(return_value);
    ZVAL_ZVAL(return_value, &op1, 0, 1); 

    zval_dtor(&op1);
    zval_dtor(&op2);

上面是 pdoner 擴展函數 pd_implode_json 的實現:https://github.com/farwish/pdoner

 

php 未開啟 debug 模式情況下使用 valgrind 工具:http://tina.reeze.cn/book/?p=chapt06/06-07-memory-leaks

 

Link: http://www.cnblogs.com/farwish/p/5663993.html


免責聲明!

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



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