进程的虚拟内存分布
内存分配的原理
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成
- brk,直接将数据段(.data)的最高地址指针_edata往高地址推;
- mmap,在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
- 这两种方式分配的都是虚拟内存,没有分配物理内存。
- 如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。
- 只有在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
两种分配方式调用的时机
- malloc小于128k的内存,使用brk分配内存
- malloc大于128k的内存,使用mmap分配内存
brk与mmap分配内存的区别
- brk在堆上的连续空间分配内存,mmap在文件映射区域分配内存
- brk分配的内存使用free释放后并不会立马归还给os,而是缓存到内存池中
- mmap分配的内存使用free释放后会立马归还给os
为什么不全部使用mmap分配内存
- 向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间。所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。
- mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。
频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。
等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
为什么不全部使用brk分配内存
如上图所示,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
内存映射
Linux 通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping),虚拟内存区域可以映射到两种类型的对象中的一种:
-
Linux 文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理内存,直到 CPU 第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。
-
匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。CPU 第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在内存中的。注意在磁盘和内存之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页(demand-zero page)。
无论在哪种情况中,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫做交换空间(swap space)或者交换区域(swap area)。需要意识到的很重要的一点是,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
mmap与munmap
mmap 函数要求内核创建一个新的虚拟内存区域,最好是从地址 start 开始的一个区域,并将文件描述符 fd 指定的对象的一个连续的片(chunk)映射到这个新的区域。
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
// 返回:若成功时则为指向映射区域的指针,若出错则为 MAP_FAILED(-1)。
连续的对象片大小为 length 字节,从距文件开始处偏移量为 offset 字节的地方开始。start 地址仅仅是一个暗示,通常被定义为 NULL。为了我们的目的,我们总是假设起始地址为 NULL。如下图所示
参数 prot 包含描述新映射的虚拟内存区域的访问权限位(即在相应区域结构中的 vm_prot 位)。
-
PROT_EXEC:这个区域内的页面由可以被 CPU 执行的指令组成。
-
PROT_READ:这个区域内的页面可读。
-
PROT_WRITE:这个区域内的页面可写。
-
PROT_NONE:这个区域内的页面不能被访问。
参数 flags 由描述被映射对象类型的位组成。如果设置了 MAP_ANON 标记位,那么被映射的对象就是一个匿名对象,而相应的虚拟页面是请求二进制零的。MAP_PRI-VATE 表示被映射的对象是一个私有的、写时复制的对象,而 MAP_SHARED 表示是一个共享对象。例如
bufp = Mmap(NULL, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);
让内核创建一个新的包含 size 字节的只读、私有、请求二进制零的虚拟内存区域。如果调用成功,那么 bufp 包含新区域的地址。
munmap 函数删除虚拟内存的区域
#include <unistd.h>
#include <sys/mman.h>
int munmap(void *start, size_t length);
// 返回:若成功则为 0,若出错则为 -1。
munmap 函数删除从虚拟地址 start 开始的,由接下来 length 字节组成的区域。接下来对已删除区域的引用会导致段错误。
参考