查看: 1946|回复: 2
打印 上一主题 下一主题

代码测试之内存泄露

[复制链接]
跳转到指定楼层
沙发
发表于 2014-7-22 18:53:37 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 伊海 于 2014-7-28 10:55 编辑

  代码测试之内存泄露

   在我们个人编程的过程当中,内存泄露虽然不会像内存溢出那样造成各种莫名奇妙的问题,但是它的危害也是不可忽视的。一方面,内存的泄露导致我们的软件在运行过程中占用了越来越多的内存,占有资源而又得不到及时清理,这会导致我们程序的效率越来越低;另一方面,它会影响我们用户的体验,失去市场的竞争能力。

    常见的内存泄露是这样的:


复制代码

    如上图所示,我们在函数process的处理过程中,每一次都需要对内存进行申请,但是在函数结束的时候却没有进行释放。如果这样的一段代码出现在业务侧,那么后果是难以想象的。举个例子来说,如果我们服务器每秒钟需要接受100个用户的并发访问,每个用户过来的数据,我们都需要本地申请内存重新保存一份。处理结束之后,如果内存没有得到很好地释放,就会导致我们服务器可用的物理内存越来越少。一旦达到某一个临界点之后,操作系统不得不通过内外存的调度来满足我们申请新内存的需求,这在另一方面来讲又会降低服务器服务的质量。

    内存泄露的危害是不言而喻的,但是查找内存泄露却是一件苦难而且复杂的工作。我们都知道,解决bug是一件非常简单的事情,但是寻找bug的出处却是一件非常吃力的事情。因此,我们有必要在自己编写代码的时候,就把查找内存泄露的工作放在很重要的位置上面。那么有没有什么办法来解决这一问题呢?

    我想要做到解决内存泄露,必须做到下面两个方面:

    (1)必须记录内存在哪个函数申请的,具体文件的行数是多少

    (2)内存应该什么时候被释放

   要完成第1个条件其实并不困难。我们可以用节点的方法记录我们申请的内存:

    a)设置节点的数据结构

  1. typedef struct _MEMORY_NODE  
  2. {  
  3.     char functionName[64];  
  4.     int line;  
  5.     void* pAddress;  
  6.     struct _MEMORY_NODE* next;
  7. }MEMORY_NODE;
复制代码

    其中 functionName记录函数名称,line记录行数, pAddress记录分配的地址, next记录下一个内存节点。

   

回复

使用道具 举报

板凳
 楼主| 发表于 2014-7-22 18:54:05 | 只看该作者

代码测试之内存泄露

本帖最后由 伊海 于 2014-7-22 19:09 编辑

b)修改内存的分配函数
    对业务侧的malloc进行函数修改,添加下面一句宏语句
    #define malloc(param)  MemoryMalloc(__FUNCTION__, __LINE__, param)

    在桩函数侧书写下面的代码
[cpp] view plaincopy
void* MemoryMalloc(const char* name, int line, int size)  
{  
    void* pData = (void*)malloc(size);  
    MEMORY_NODE* pMemNode = NULL;  
    if(NULL == pData) return NULL;  
    memset((char*)pData, 0, size);  
  
    pMemNode = (MEMORY_NODE*)malloc(sizeof(MEMORY_NODE));  
    if(NULL == pMemNode){  
        free(pData);  
        return NULL;  
    }  
    memset((char*)pMemNode, 0, sizeof(MEMORY_NODE));  
    memmove(pMemNode->functionName, name, strlen(name));  
    pMemNode->line = line;  
    pMemNode->pAddress = pData;  
    pMemNode->next = NULL;  
    add_memory_node(pMemNode);  
  
    return pData;  
}  

    内存的分配过程中还涉及到了节点的添加,所以我们还需要添加下面的代码
[cpp] view plaincopy
static MEMORY_NODE* gMemNode = NULL;  
  
void add_memory_node(MEMORY_NODE* pMemNode)  
{  
    MEMORY_NODE* pNode = gMemNode;  
    if(NULL == pMemNode) return;  
    if(NULL == gMemNode){  
        gMemNode = pMemNode;  
        return;  
    }  
  
    while(NULL != pNode->next){  
        pNode = pNode->next;  
    }  
    pNode->next = pMemNode;  
    return;  
}  
    文中gMemNode表示所有内存节点的根节点,我们每增加一次malloc过程就会对内存节点进行记录。在记录过程中,我们还会记录调用malloc的函数名称和具体文件行数,这主要是为了方便我们在后面进行故障定位的时候更好地查找。

   完成了第一个条件之后,我们就要对第二个条件进行完成。
   a)内存什么时候释放,这取决于我们在函数中是怎么实现的,但是我们在编写测试用例的时候却是应该知道内存释放没有,比如说如果测试用例全部结束了,我们有理由相信assert(gMemNode == NULL)这应该是恒等于真的。
    b)内存释放的时候,我们应该做些什么?和节点的添加一样,我们在内存释放的时候需要free指定的内存,free节点,free节点的内存,下面就是在释放的时候我们需要进行的操作

    对业务侧的free函数进行修改,添加下面一句宏代码,
    #define free(param)      MemoryFree(param)

    在桩函数侧输入下面的代码:
[cpp] view plaincopy
void MemoryFree(void* pAddress)  
{  
    if(NULL == pAddress) return;  
    delete_memory_node(pAddress);  
    free(pAddress);  
}  

    在删除内存的时候,需要删除节点,删除节点的内存
[cpp] view plaincopy
void delete_memory_node(void* pAddress)  
{  
    MEMORY_NODE* pHead = gMemNode;  
    MEMORY_NODE* pMemNode = gMemNode;  
    while(NULL != pMemNode){  
        if(pAddress == pMemNode->pAddress)  
            break;  
        pMemNode = pMemNode->next;  
    }  
    if(NULL == pMemNode) {  
        assert(1 == 0);  
        return;  
    }  
  
    while(pMemNode != pHead->next){  
        pHead = pHead->next;  
    }  
  
    if(pMemNode == gMemNode){  
        gMemNode = gMemNode->next;  
    }else{  
        pHead->next = pMemNode->next;  
    }  
    free(pMemNode);  
    return;  
}  

    有了上面一小段代码的帮助,我们在编写测试用例的时候,就可以在函数执行后,通过判断内存节点是否为空的方法判断内存是否已经释放。如果内存没有释放,我们还能通过节点的信息帮助我们是哪里发生了错误,但是这个方法还有两个缺点:
    (1)没有考虑缓存的情况,好多内存分配了之后并不会在函数中马上释放,而是放在缓存池中等待下一次调用,这就需要我们准确把握和判断了。
    (2)代码中节点删除和添加的时候没有考虑多进程的情形,应该考虑用一个互斥锁或者是信号量加以保护。

(全文完)
回复 支持 反对

使用道具 举报

地板
 楼主| 发表于 2014-7-23 18:10:47 | 只看该作者
本帖最后由 伊海 于 2014-7-23 18:18 编辑

b)修改内存的分配函数
    对业务侧的malloc进行函数修改,添加下面一句宏语句
  1. #define malloc(param) MemoryMalloc(__FUNCTION__, __LINE__, param)
复制代码
   在桩函数侧书写下面的代码
  1. [cpp] view plaincopy
  2. void* MemoryMalloc(const char* name, int line, int size)  
  3. {  
  4.     void* pData = (void*)malloc(size);  
  5.     MEMORY_NODE* pMemNode = NULL;  
  6.     if(NULL == pData) return NULL;  
  7.     memset((char*)pData, 0, size);  
  8.   
  9.     pMemNode = (MEMORY_NODE*)malloc(sizeof(MEMORY_NODE));  
  10.     if(NULL == pMemNode){  
  11.         free(pData);  
  12.         return NULL;  
  13.     }  
  14.     memset((char*)pMemNode, 0, sizeof(MEMORY_NODE));  
  15.     memmove(pMemNode->functionName, name, strlen(name));  
  16.     pMemNode->line = line;  
  17.     pMemNode->pAddress = pData;  
  18.     pMemNode->next = NULL;  
  19.     add_memory_node(pMemNode);  
  20.   
  21.     return pData;  
  22. }
复制代码
   内存的分配过程中还涉及到了节点的添加,所以我们还需要添加下面的代码
  1. [cpp] view plaincopy
  2. static MEMORY_NODE* gMemNode = NULL;  
  3.   
  4. void add_memory_node(MEMORY_NODE* pMemNode)  
  5. {  
  6.     MEMORY_NODE* pNode = gMemNode;  
  7.     if(NULL == pMemNode) return;  
  8.     if(NULL == gMemNode){  
  9.         gMemNode = pMemNode;  
  10.         return;  
  11.     }  
  12.   
  13.     while(NULL != pNode->next){  
  14.         pNode = pNode->next;  
  15.     }  
  16.     pNode->next = pMemNode;  
  17.     return;  
  18. }
复制代码
   文中gMemNode表示所有内存节点的根节点,我们每增加一次malloc过程就会对内存节点进行记录。在记录过程中,我们还会记录调用malloc的函数名称和具体文件行数,这主要是为了方便我们在后面进行故障定位的时候更好地查找。

   完成了第一个条件之后,我们就要对第二个条件进行完成。
   a)内存什么时候释放,这取决于我们在函数中是怎么实现的,但是我们在编写测试用例的时候却是应该知道内存释放没有,比如说如果测试用例全部结束了,我们有理由相信assert(gMemNode == NULL)这应该是恒等于真的。
    b)内存释放的时候,我们应该做些什么?和节点的添加一样,我们在内存释放的时候需要free指定的内存,free节点,free节点的内存,下面就是在释放的时候我们需要进行的操作

    对业务侧的free函数进行修改,添加下面一句宏代码,
  1. #define free(param)      MemoryFree(param)
复制代码
   在桩函数侧输入下面的代码:
  1. [cpp] view plaincopy
  2. void MemoryFree(void* pAddress)  
  3. {  
  4.     if(NULL == pAddress) return;  
  5.     delete_memory_node(pAddress);  
  6.     free(pAddress);  
  7. }
复制代码
   在删除内存的时候,需要删除节点,删除节点的内存
  1. [cpp] view plaincopy
  2. void delete_memory_node(void* pAddress)  
  3. {  
  4.     MEMORY_NODE* pHead = gMemNode;  
  5.     MEMORY_NODE* pMemNode = gMemNode;  
  6.     while(NULL != pMemNode){  
  7.         if(pAddress == pMemNode->pAddress)  
  8.             break;  
  9.         pMemNode = pMemNode->next;  
  10.     }  
  11.     if(NULL == pMemNode) {  
  12.         assert(1 == 0);  
  13.         return;  
  14.     }  
  15.   
  16.     while(pMemNode != pHead->next){  
  17.         pHead = pHead->next;  
  18.     }  
  19.   
  20.     if(pMemNode == gMemNode){  
  21.         gMemNode = gMemNode->next;  
  22.     }else{  
  23.         pHead->next = pMemNode->next;  
  24.     }  
  25.     free(pMemNode);  
  26.     return;  
  27. }
复制代码
   有了上面一小段代码的帮助,我们在编写测试用例的时候,就可以在函数执行后,通过判断内存节点是否为空的方法判断内存是否已经释放。如果内存没有释放,我们还能通过节点的信息帮助我们是哪里发生了错误,但是这个方法还有两个缺点:
    (1)没有考虑缓存的情况,好多内存分配了之后并不会在函数中马上释放,而是放在缓存池中等待下一次调用,这就需要我们准确把握和判断了。
    (2)代码中节点删除和添加的时候没有考虑多进程的情形,应该考虑用一个互斥锁或者是信号量加以保护。
(全文完)

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入因仑

本版积分规则

快速回复 返回顶部 返回列表