红联Linux门户
Linux帮助

说说LINUX程序内存管理那些事

发布时间:2016-10-03 10:49:59来源:linux网站作者:hsiangchen
在LINUX系统中,在内存管理方面,为考虑到简便高效,就像UNIX那样,有时会出现这样的情形,刚刚释放的内存,这时内存已经是无效的了,但是仍然可以访问,这是因为暂时释放的区域实际上根本就没有真正地回收,不过可能过不一会儿就变成无效的了,这是一种延迟回收的策略,如果一直使用一个被释放了的指针,可能会刚开始时,一切都正常,可过会儿就出现地址越界而导致程序出错(在有的系统中,比如Windows和Minix中,如果刚刚释放了一块内存,内核马上修改一下刚刚释放的指针的前几个字节,这样的话会降低系统的性能,这种方式对编程习惯良好的程序员来说是不必要的,在Linux和FreeBSD系统中就没有采用)。
 
如果一个初学者写了程序,把释放的内存又拿来用,可能程序运行得畅通无阻,这是因为在LINUX中,如果用free()函数释放一个地址,系统只是简单标记一下这个地址已经变成可用状态了(available),先前状态则是busy(因为它正在被使用),但是涉及到地址是否可以读写保护等属性都不管了,然后内核转而去做更加正经的事了。如果一个程序分配内存频繁,内核找到刚才被释放的那块被标记了available的内存,然后把它分给调用malloc()的调用者,如果上次还在用那个释放了的内存,这是程序就会出现SengmentFault的错误了,有时会dump出程序的内存映射列表的信息,如果内存分配频率低,错误的程序可能仍然可以运行得畅通无阻。
 
所以对于程序员来说,malloc和free必须要成对出现,因为free释放内存是标记一下就立马走人。若指针不当使用可能就会出现这样的情况,看上去正常的程序其实是有错的,它只是暂时不崩溃,但是把它放到一个内存小一点的计算机上,或者本地计算机内存将待耗尽时,它就崩溃了。不良的指针使用就导致一个不确定的炸弹的产生,虽然不一定马上就炸,但是过会儿,甚至马上就炸。
 
下面一个小例子说明这种问题:
 
假如要实现这样一个函数:
根据所提供的参数size,即内存的大小,来调整内存,每次调用这个函数内存增加1K字节,并且要返回上次内存的尾端位置的指针。
 
代码如下:
void* incmem(void**ptr, long* size)  
{  
void* retp = *ptr+ *size; /* 这是上次指针的尾端*/  
*size += 1024; /*1K字节*/  
*ptr = realloc(*size);  
return retp;  
}  
 
很明显上面的代码是有误的,因为realloc返回的指针不一定和上次相等,retp看似是尾端,但是下面可能已经把*ptr的值改掉了。而在LINUX系统可能上面的代码在运行时并没有任何异常。
 
如把代码改成这样:
void* incmem(void**ptr, long* size)  
{  
long oldsize =*size; /* 原来内存的大小*/  
*size += 1024;  
*ptr = realloc(*size);  
return *ptr +oldsize;  
}  
 
才可保证程序代码的正确性。
 
在LINUX中其实可以自己写个内存管理的辅助函数库,自己用代码实现malloc,realloc,free等这些函数,或者把函数名字改下,改成据如_malloc,_realloc,_free等自定义又便于与标准函数对应的函数,申请内存时,调用自己的函数,自己用这些函数监控内存的使用情况,判断内存是否出错,并且给内存设置保护信息等,当确定自己的程序代码在内存申请与释放没有错误时,再用define把自定义的函数定义成标准的函数。
 
比如:
#ifndef  DEBUG  
#define _malloc(s)malloc(s)  
// ...  
#else  
#include“mymalloc.h”  
#endif  
 
编写这些代码可以很简单,举个简单例子,就是仅仅改一下free()函数,让它每次释放内存后,就置原来的指针为0,但是如果涉及realloc()函数的话,如果free()时,置0的那个指针还有个备份,它仍然可以被误用,那这些方法就都不管用了。也可以把代码编写得很复杂,是个技术活,比如说自己写个类似系统内核内存管理的仿真器,在自己的仿真器中,监控这些内存,分配内存时,挖一块出来,释放内存后,添一块,并把它设置为无效的(需要结合系统的函数来实现,比如用mmap()来申请内存块,用mprotect()来给刚刚释放的内存设置为PROT_NONE,用munmap()释放,复杂一点,可以不急于释放而是用mprotect来标记,这样可以加快内存的申请速度,这就需要自己来管理内存的,用虚拟页方式调试内存错误是切实可行的,但是内存的分配效率是不及普通的malloc类函数的),也可以利用malloc_hook这样的函数,给这些函数安装钩子,也是可行的工具之一。在内存调试方面,有些现成的库,如MEMWATCH,YAMD,ElectricFence等,如果感兴趣的话,可以在网上搜一下,然后拿来用。
 
下面再说个函数的堆栈参数或寄存器参数传递问题:
 
堆栈内存管理遵从先进后出的原则。
 
比如有如下代码:
long size = 1024;  
char* ptr = (char*)malloc(size);  
char* last = (char*)incmem(&ptr, &size);  
 
这里只要涉及修改参数的都要用指针传递,否则的话在函数调用返回后,修改了的值仍然得不到保存,被从栈中弹出而废弃了。
 
如果有如下函数:
void* incmem(void*ptr, long size)  
{  
long  oldsize =size;  
size += 1024;  
ptr =realloc(size);  
return ptr +oldsize;  
}  
 
这是一个存在错误的函数,函数虽正确修改了内存并掉整了大小。但是却把它放到了栈中,函数退出时,ptr和size都被自动废弃了。
 
如有调用代码:
long size = 1024;  
char* ptr = (char*)malloc(size);  
char* last = (char*)incmem(ptr, size);  
 
用汇编代码描述如下:
 
在32位的INTEL80x86平台下:
ptr: dword 0  
size: dword 0
push ptr  
push size  
call incmem  
 
ptr和size都临时放在了CPU的指令使用的栈中,函数修改了栈中的值,而没能更改ptr和size的值。
 
在64位的AMD64平台下(或者INTELx86_64):
ptr: dword 0  
size: qword 0
movq ptr, %rcx  
movq size, %rdx  
call incmem  
 
用寄存器传递也一样,修改了的值是在寄存器里面,不在ptr和size上,所以仍然是错误的。
 
所以要想获得正确的结果,就必须传递要引用的参数的指针,因为传递指针时,堆栈或者寄存器中的值是表示ptr和size的地址,引用此地址的值将导致直接引用ptr和size,也就是callincmem上面的那两行处,函数调用返回后,pop出而废弃的两个地址数值是无关数值(其实这两个地址数并没有修改,修改的值是这两个地址数表示的地址处的数值)。
 
本文永久更新地址:http://www.linuxdiyf.com/linux/24699.html