内存管理
操作系统为进程提供虚拟内存,每个应用程序进程都认为自己独占全部、连续的内存。在 64 位系统上:
- 虚拟内存大小为256TB
- 地址范围:0x0000_0000_00000000 到 0x0000_FFFF_FFFFFFFF,64 为数可以使用 16 个 16 进制数来表示,而由于 linux 中的虚拟内存范围其实最大不超过 0x0000_7fff_ffffffff,因此地址范围可以使用 12 位 16 禁止数来简述:0x0000_00000000 0x7fff_ffffffff
分页方式
linux 系统的虚拟内存管理采用的是分页方式。不使用分段方式的理由是:分段方式容易产生外部内存碎片,造成内存浪费。
分页机制将内存条的内存地址空间分成一个个区间进行管理:页。在分配和管理内存的时候不是以一个一个字节为单位管理,而是以一个页为最小的操作和管理单位进行的,一个内存页通常包含 4K 个内存位置,一个位置存放一个 Byte。
对于一个进程来说,它的虚拟内存地址空间中的内存也同样使用分页的方式进行。进程的内存需要进行动态分配使用,按需申请,每次申请一个或者多个页表,每个虚拟页表都可以对应一个真实的物理地址中的页表。操作系统以进程为单位,为程序创建一个内存地址映射表,也叫页表,页表记录着从虚拟地址空间中的一个页的首地址,到对应的真实物理页的内存条上的首地址,的映射关系。
多级页表
简单地使用一个页表记录虚拟空间到物理空间的内存页映射关系,会面临一个问题是,页表占用了太多的空间,因为页表也需要占用内存。对于 32 位的系统页表需要占用 4MB,勉强接受;而 64 位的系统页表需要占用 33554432 GB 的空间,这个空间显然是无法接受的。可以使用多级页表解决这个问题。
2^32 / 4k * 4 = 4 MB
2^64 / 4k * 8 = 33554432 GB
多级页表是一个类似于字典树的结构,多级多分支,对于一个 64 位的虚拟内存地址空间中的地址,会先对其进行前缀划分,从而确定其在多级页表上的查找路径。前 16 位不可用,中间的 32 位作为中间地址,后 16 位作为页内偏移;进程中保存着页表基地址,即 0 级页表的地址,在进行进程切换的时候,将该地址写入页表基地址寄存器即可完成进程的页表切换。
在需要读取某个地址上的数据的时候,先根据该地址的中间 36 位按顺序读取 4 级页表找到物理页的首地址,然后根据页内偏移找到该字节的地址,然后读取数据。
多级页表中,0 级页表必定存在,然后 1 级页表不一定都存在,因为 0 级页表中的地址不一定都是有效值,同理 2,3 级页表不一定存在,只有当需要的时候触发缺页异常,由系统临时分配,由此可以节省内存的空间;
TLB 缓存
多级页表是为了节省空间的,但它牺牲了内存读取的时间,因为增加了多级页表的机制,导致访问单次内存的时间大大增加。为了避免重复走多级页表,在 CPU 内部增加 TLB 单元,(Translation Lookaside Buffer 转址旁路缓存),用于缓存走完 4 级页表之后,哪些虚拟页已经成功与物理页映射过了。它是一个有限大小的哈希表缓存,过期算法由硬件级实现。
内存大页
为了减少多级页表造成的访问延迟,可以使用内存大页来解决。linux 默认的页表大小是 4K,使用四级缓存,它允许调整页的大小为 2M 和 1G,相对应的页表采用 3 级页表和 2 级页表。这样可以减少访问路径的深度,同时一次加载一大块内存,可以显著减少访问的次数,但是这样会造成内部碎片的产生。同时,大页可以增加 TLB 的命中率和减少过期计算。
malloc 流程
在一个进程中,虚拟地址空间被划分成一个个 4K 大小的虚拟页,分别可以与真实的内存条上的物理内存页一一对应。
当程序调用 malloc 申请内存时,Linux 的内存分配流程如下:
- 内存分配请求,程序调用 malloc 函数申请内存,如果申请的内存小于 128KB,使用 brk 系统调用;如果申请的内存大于 128KB,使用 mmap 系统调用
- 虚拟内存分配,内核在进程的虚拟地址空间中分配一段连续的虚拟内存,此时只分配虚拟地址空间,不分配实际的物理内存;在页表中创建对应的页表项,但标记为"不存在"
- 首次访问触发缺页异常,当程序首次访问这段内存时,由于页表项标记为"不存在",CPU 触发缺页异常(Page Fault),操作系统接管处理缺页异常
- 物理内存分配,操作系统检查访问的虚拟地址是否合法,如果合法,分配物理内存页,更新页表,建立虚拟地址到物理地址的映射;将页表项标记为"存在";
- TLB 更新,新的页表项被添加到 TLB 中,后续访问可以直接通过 TLB 快速获取物理地址,如果 TLB 已满,根据硬件算法淘汰旧项;
- 内存访问,程序可以正常访问内存,如果访问的地址超出分配范围,触发段错误(Segmentation Fault)
- 内存释放,当调用 free 释放内存时,如果使用 brk 分配的内存,可能通过收缩堆来释放,如果使用 mmap 分配的内存,直接解除映射,对应的物理内存页被回收,页表项被标记为"不存在",TLB 中的对应项被标记为无效
- 内存优化,系统会维护一个空闲页框链表,使用伙伴系统算法管理物理内存页,通过页面置换算法(如 LRU)管理内存压力,使用内存压缩技术减少内存碎片
这个过程体现了 Linux 内存管理的几个重要特性:
- 延迟分配:只有在实际使用时才分配物理内存
- 按需分页:通过缺页异常机制实现
- 写时复制:多个进程共享同一物理页,直到需要修改
- 内存回收:通过页面置换和内存压缩保持系统稳定
进阶主题
- 内核页缓存(Page Cache)与缓冲区(Buffer Cache)
- NUMA 架构
- 内存回收机制(kswapd、OOM Killer)
- Cgroup 限制内存使用
- Transparent HugePages(THP)