目录
用户空间内存分配
结构体
内核就是使用 vm_area_struct 对象来记录一个内存分区(如 代码段、数据段 和 堆空间 等),下面介绍一下 vm_area_struct 对象各个字段的作用:
• vm_mm:指定了当前内存分区所属的内存管理对象。
• vm_start:内存分区的开始地址。
• vm_end:内存分区的结束地址。
• vm_next:通过这个指针把进程中所有的内存分区连接成一个链表。
• vm_rb:另外,为了快速查找内存分区,内核还把进程的所有内存分区保存到一棵红黑树中。vm_rb 就是红黑树的节点,用于把内存分区保存到红黑树中。
mm_struct 对象各个字段的作用:
• mmap:指向由进程所有内存分区连接成的链表。
• mm_rb:内核为了加快查找内存分区的速度,使用了红黑树保存所有内存分区,这个就是红黑树的根节点。
• start_brk:堆空间的开始内存地址。
• brk:堆空间的顶部内存地址。
start_brk 和 brk 字段用来记录堆空间的范围, 如 图 所示。一般来说,start_brk 是不会变的,而 brk 会随着分配内存和释放内存而变化。
malloc
malloc 申请内存的时候,会有两种方式向操作系统申请堆内存:
方式一:通过 brk() 系统调用从堆分配内存
方式二:通过 mmap() 系统调用在文件映射区域分配内存;
malloc() 源码里默认定义了一个阈值:
- 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
- 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
对于 「malloc 申请的内存,free 释放内存会归还给操作系统吗?」这个问题,我们可以做个总结了:
- malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
- malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放
brk()
通过移动 brk 指针就可以达到申请(向上移动)和释放(向下移动)堆空间的内存
brk()系统调用,主要有以下几个步骤:
• 1、判断堆空间的大小是否超出限制,如果超出限制,就不作任何处理,直接返回旧的 brk 值。
• 2、如果新的 brk 值跟旧的 brk 值一致,那么也不用作任何处理。
• 3、如果新的 brk 值发生变化,那么就调用 do_brk 函数进行下一步处理。
• 4、设置进程的 brk 指针(堆空间顶部)为新的 brk 的值。
我们看到第 3 步调用了 do_brk 函数来处理,do_brk 函数的实现有点小复杂,所以这里介绍一下大概处理流程:
• 通过堆空间的起始地址 start_brk 从进程内存分区红黑树中找到其对应的内存分区对象(也就是 vm_area_struct)。
• 把堆空间的内存分区对象的 vm_end 字段设置为新的 brk 值。
此时,malloc 函数只是移动 brk 指针,但并没有申请物理内存。虚拟内存地址必须映射到物理内存地址才能被使用。如果对没有进行映射的虚拟内存地址进行读写操作,那么将会发生 缺页异常。
异常中执行如下:
• 获取触发 缺页异常 的虚拟内存地址(读写哪个虚拟内存地址导致的)。
• 查看此虚拟内存地址是否被申请(是否在 brk 指针内),如果不在 brk 指针内,将会导致 Segmention Fault 错误(也就是常见的coredump),进程将会异常退出。
• 如果虚拟内存地址在 brk 指针内,那么将此虚拟内存地址映射到物理内存地址上,完成 缺页异常 修复过程,并且返回到触发异常的地方进行运行。映射的主要工作就是完成对进程 页表 的填充
映射中,分为两个不同的处理逻辑:
• 如果是读操作导致的,那么将会使用 零页 进行映射(零页 是 Linux 内核中一个比较特殊的内存页,所有读操作引起的 缺页异常 都会指向此页,从而可以减少物理内存的消耗),并且设置其为只读(因为 零页 是不能进行写操作)。如果下次对此页进行写操作,将会触发写操作的 缺页异常,从而进入下面步骤。
• 如果是写操作导致的,就申请一块新的物理内存页,然后根据物理内存页的地址生成映射关系,再对页表项进行填充(映射)。
从上面的过程可以看出,不对申请的虚拟内存地址进行读写操作是不会触发申请新的物理内存。所以,这就解释了为什么申请 1GB 的内存,但实际上只使用了 404 KB 的物理内存。