1. 应用程序内存布局
在 Linux 系统中,应用程序的内存被分为若干个逻辑段,如图:
其中,各个分段的意义是:
- 代码段:程序编译后的可执行代码(指令)存放区域,在编译时确定了,同一个程序在不同机器上、在同一个机器上的不同次运行,同一个方法的入口地址都是确定的
- 数据段:存放程序已初始化的静态常量和全局变量
- BSS 段:存放未初始化的静态常量和全局变量
- 堆:动态分配的内存区域,从低地址向高地址增长,大小不固定
- 栈:存放局部变量、函数调用上下文等,栈的大小在程序启动时就固定了,一般是 8MB,系统提供了参数可以修改
在上述分段中,文件映射段和堆的内存是由程序动态分配的,通常使用 C 标准库的malloc
或mmap
方法来执行
2. malloc 是如何分配内存的
2.1 malloc 概述
实际上,malloc
是 C 标准库函数,而不是系统调用,mmap
和 brk
是系统调用,malloc 申请内存时有两种方式:
- 方式一:通过系统调用
brk
从堆分配内存,具体方法是将堆空间的最高地址指针往高地址扩展,扩充堆区的大小 - 方式二:通过系统调用
mmap
从文件映射区分配内存,具体方法是在文件映射区中找一块足够的空间,进行分配
需要注意的是,此两种方式分配的都是 虚拟内存,并没有分配物理内存,那么什么时候进行物理内存的分配呢?在第一次访问虚拟空间时,查找页表失败,产生缺页中断,会进行物理分配的内存,并建立虚拟内存地址和物理内存地址的映射关系(生成页表项)
C 标准库中提供
malloc / free
函数分配和释放内存,这两个函数底层由brk / mmap /unmap
等系统调用实现。
分别什么情况用 brk
和 mmap
呢?
malloc
源码中定义了一个阈值 M_MMAP_THRESHOLD
,默认为 128K
- 当分配的值小于该阈值时,调用
brk
- 否则,调用
mmap
2.2 一个例子
2.2.1 首先看看 brk
内存分配
- 程序启动后,虚拟内存空间初始布局如图1 所示
- 程序执行
A = malloc(30K)
后,执行系统调用brk
,将堆顶指针王高地址增加 30K,得到图2所示的内存布局。注意,此时只是完成了虚拟内存的分配,对应的物理内存还没分配,页表项也没创建,等到程序第一次读取 A 这块内存时,发生缺页中断,内核才会分配物理内存并建立对应页表项 - 程序执行
B = malloc(40K)
后,同样的堆顶指针往高地址增加,如图3 所示
2.2.2 当 mmap
分配较大内存
程序执行
C = malloc(200K)
,待分配的值超过了阈值,使用mmap
分配,在堆和栈中间找一块空闲内存,并初始化为0,如图4所示。这样做的一个重要原因是,使用brk
移动堆顶地址的方式分配内存,只有高地址的内存被释放后,才能释放低地址的内存,对于内存释放的顺序有依赖,如图4中,必须先释放 B ,才能释放 A,对于大块内存分配如果使用此种方式,将造成很多大块内存无法按需释放,而mmap
分配的内存无依赖,可以单独释放程序执行
D=malloc(100k)
后,内存空间如图5所示程序调用
free(C)
释放内存,将 C 对应的 虚拟内存和物理内存一起释放,得到图6所示
2.2.3 内存的释放
- 调用
free(B)
后的内存布局如图7,B 对应的虚拟内存和物理内存都没有释放,因为只有一个栈顶指针,由于 D 内存的存在,无法回推。当然,B 部分的内存是可重用的,此时如果来一个 40K 的分配请求,很可能就把 B 给分配返回了。 - 程序执行
free(D)
后,内存布局如图 8 所示,B和D构成了一块 140K 的空闲内存 - 系统默认当高地址空间的空闲内存超过 128K(由
M_TRIM_THRESHOLD
选项调节)时,会自动执行内存紧缩操作 (trim
),在上一步free(D)
完成后,进行内存紧缩,内存布局变成图9所示,栈顶地址降低,释放对应的虚拟内存和物理内存
3. 总结
malloc 细节包括以下几点:
- 当分配请求超过阈值时,使用
mmap
进行分配 - 分配请求小于阈值时,使用
brk
分配 - 分配时并没有立即分配物理地址,只是分配了虚拟地址,第一次访问时才建立页表项,分配物理地址
- 释放
mmap
分配的地址时,可以立即释放 - 释放
brk
分配的内存时,不会立即释放,但可以重用,执行释放时会检查堆顶指针附近的最大空闲块,如果超过阈值,则会执行内存紧缩策略,真正释放物理地址