那么我们每次调用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 |
到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。