那么我们每次调用malloc来分配一块内存,都进行相应的系统调用呢?
答案是否定的,这里我要引入一个新的概念,glibc的内存管理器。
我们知道malloc和free等函数都是包含在glibc库里面的库函数,我们试想一下,每做一次内存操作,都要调用系统调用的话,那么程序将多么的低效。
实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。
内存管理器面临的困难
我们在写程序的时候,每次申请的内存块大小不规律,而且存在频繁的申请和释放,这样不可避免的就会产生内存碎块。而内存碎块,直接会导致大块内存申请无法满足,从而更多的占用系统资源;如果进行碎块整理的话,又会增加cpu的负荷,很多都是互相矛盾的指标,这里我就不细说了。
我们在写程序时,涉及内存时,有两个概念heap和stack。传统的说法stack的内存地址是向下增长的,heap的内存地址是向上增长的。
函数malloc和free,主要是针对heap进行操作,由程序员自主控制内存的访问。
在这里heap的内存地址向上增长,这句话不完全正确。
glibc对于heap内存申请大于128k的内存申请,glibc采用mmap的方式向内核申请内存,这不能保证内存地址向上增长;小于128k的则采用brk,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。
这里我先讲大块内存的申请,也即对应于mmap系统调用。
对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放,这个过程中间不会产生内存碎块等问题。
针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次进行内存申请时,glibc会将堆顶向上增长来扩展内存空间,也就是我们所说的堆地址向上增长。在对这些小块内存进行操作时,便会产生内存碎块的问题。实际上brk和sbrk系统调用,就是调整heap顶地址指针。
那么heap堆的内存是什么时候释放呢?
当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。
下面我要讲一个内存空洞的问题:
一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?
很遗憾,不能。
这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?
这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。
提一个问题:
1
2
|
char *p=malloc(
2
);
free(p)
|
为什么申请内存的时候,需要两个参数,一个是内存大小,一个是返回的指针;而释放内存的时候,却只要内存的指针呢?
这主要是和glibc的内存管理机制有关。glibc中,为每一块内存维护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的大小,然后是分配给进程的内存。
1
2
|
chunk ------size
p------------ content
|
在进程释放内存时,只要 指针-4 便可以找到该块内存的大小,从而释放掉。
注:glibc在做内存申请时,最少分配16个字节,以便能够维护chunk结构。
glibc提供的调试工具:
为了方便调试,glibc 为用户提供了 malloc 等等函数的钩子(hook),如 __malloc_hook
对应的是一个函数指针,
1
|
void
*
function
(size_t size,
const
void
*caller)
|
其中 caller 是调用 malloc 返回值的接受者(一个指针的地址)。另外有 __malloc_initialize_hook函数指针,仅仅会调用一次(第一次分配动态内存时)。(malloc.h)
一些使用 malloc 的统计量(SVID 扩展)可以用 struct mallinfo 储存,可调用获得。
1
|
struct mallinfo mallinfo (
void
)
|
如何检测 memory leakage?glibc 提供了一个函数
void mtrace (void)及其反作用void muntrace (void)
这时会依赖于一个环境变量 MALLOC_TRACE 所指的文件,把一些信息记录在该文件中
用于侦测 memory leakage,其本质是安装了前面提到的 hook。一般将这些函数用
#ifdef DEBUGGING 包裹以便在非调试态下减少开销。产生的文件据说不建议自己去读,
而使用 mtrace 程序(perl 脚本来进行分析)。下面用一个简单的例子说明这个过程,这是
源程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include
#include
#include
intmain(
int
argc, char *argv[] )
{
int
*p, *q ;
#ifdef DEBUGGING
mtrace( ) ;
#endif
p = malloc( sizeof(
int
) ) ;
q = malloc( sizeof(
int
) ) ;
printf
(
"p = %p\nq = %p\n"
, p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return
0 ;
}
|
很简单的程序,其中 q 没有被释放。我们设置了环境变量后并且 touch 出该文件
执行结果如下:
p = 0x98c0378q =
0x98c0388
|
该文件内容如下
1
2
3
4
|
= Start
@./test30:[
0x8048446
] +
0x98c0378
0x4
@./test30:[
0x8048455
] +
0x98c0388
0x4
@./test30:[
0x804848f
] -
0x98c0378
|
到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。