内核的内存管理

嵌入式学习打卡day2


页表

操作系统和 MMU 使用页表(Page Table)作为‘地址翻译字典’。它的映射单位是固定大小的内存块(通常是 4KB 的 Page)。为了节省物理内存,现代操作系统的页表本身并不是一片巨大的连续内存,而是采用‘多级树状结构’(多级页表),像多级目录一样按需分配、分散存储在物理内存中。


在内核中使用struct page来描述一个物理页,定义在<linux/mm_types.h>,以下是简化版

struct page{
    unsigned long flags; //存放该页的状态标志。具体状态定义在<linux/page-flags.h>
    atomic_t _count;  //记录页被使用的次数
    atomic_t _mapcount; //映射计数。 记录有多少个页表项(PTE)指向这个物理页。
    unsigned long private;//私有数据指针。这个字段的具体含义取决于这个页被用来干什么
    struct address_space *mapping;//如果这个物理页用于缓存某个磁盘上的文件,mapping 会指向该文件对应的地址空间结构
    pgoff_t index;//index 记录了当前页在那个文件中的偏移量
    struct list_head lru;//
    void *virtual;//页的虚拟地址
}

区

为什么内核要划分“区(Zone)”? 根本原因是硬件的物理限制或历史遗留问题。内核本来想对所有的物理页一视同仁,随便抓一个页就能用。但是底层硬件不允许,主要表现为两个痛点:


四大核心区

由于 64 位系统拥有极其庞大的虚拟地址空间(比如 256TB),内核完全有能力把机器上所有的物理内存都永久映射进去。在现代 64 位架构下,ZONE_HIGHMEM 通常是空的(根本不存在)。所有的物理页除了少数留给 DMA 外,其余的全部属于自由自在的 ZONE_NORMAL。


页操作的核心函数


gfp_mask(Get Free Page mask)

在 Linux 内核中,需要申请内存时,你不能只告诉内核“我要多大”的内存,你还必须通过 gfp_mask 告诉内核“你要怎么帮我找这块内存”以及“去哪里找”

  1. 行为修饰符(Action Modifiers):决定“怎么找” 内核的内存管理 配图 1

  1. 区修饰符(Zone Modifiers):决定“去哪找” 内核的内存管理 配图 2

  2. 类型标志(Type Flags):开发者的“快捷套餐” 每次分配内存都要把行为修饰符和区修饰符一个一个通过按位或(|)组合起来,那太容易写错代码了。因此,内核将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。这样,你只需指定一个类型标志就可以了。 内核的内存管理 配图 3


slab层

我们在写底层代码时,经常会遇到需要频繁创建和销毁特定数据结构的情况,内核是如何高效处理这种“小块内存”分配的

  1. 为什么需要 Slab 分配器?

为了解决这一问题,linux 引入了 slab 分配器,来扮演通用数据结构缓存层的角色。


Slab 分配器的设计哲学


slab的架构


思考:为什么需要第二层slab,特定的高速缓存存放对应的对象不行吗?

  1. 作为物理页和细碎对象之间的“批发转换器”
  1. 实现内存的“成建制回收”(对抗内存碎片的核心)

  1. 提供硬件级别的内存对齐与着色(Coloring) Slab 这一层除了划分对象,还负责在物理页内部做精密的“排版”。

内核栈

普通应用程序在用户空间运行时,可以奢侈地使用非常大的栈,而且这个栈还可以根据需要动态增长。但是内核却没有这么奢侈。 内核为每个进程分配的栈是小而且固定的。这样做有两个好处: 大大减少了系统整体的内存消耗。 内核无需承担沉重的栈管理任务(比如动态扩容)

内核栈的大小


为什么要使用单页栈

非常底层的痛点:内存碎片。随着系统长时间运行,物理内存变得越来越碎片化,要想在内存里找到两个未分配的、连续的页会变得极其困难。如果找不到连续的两页,创建新进程就会失败或者严重卡顿。因此,改成单页栈极大缓解了内存分配器的压力。

内存分配函数的选择

  1. 场景一:日常开发的最优解(连续物理内存)

  1. 场景二:与高端内存打交道
  1. 场景三:大内存的救星(只要虚拟连续即可)
  1. 场景四:专属数据结构的兵工厂(Slab)