Linux 内核 vmalloc 虚拟内存分配器深度工程实战:从 VMA 分配到页表填充的完整链路

引言

内核中并非所有内存分配都需要物理连续。当模块代码需要加载、I/O 缓冲区需要大块非连续映射、或者 per-CPU 动态区域需要创建时,vmalloc 系列函数便成为不可或缺的工具。

与 kmalloc(基于 SLUB 分配器,保证物理连续)不同,vmalloc 提供的是物理不一定连续但虚拟连续的内存区域。这种设计带来了灵活性,也引入了 TLB 压力大、页表映射开销高、以及地址转换延迟增加等代价。

本文将从工程实战角度深入剖析 vmalloc 的完整工作机制:从 vfree_area 红黑树与伙伴系统的协同分配,到多级页表填充与 TLB shootdown 协议;从 __vmalloc_node_range 的慢速分配路径,到 vmalloc_huge/vmalloc_nohuge 的优化策略;再到 VM_IOREMAP、VM_MAP、VM_USERMAP 四种 vmap 场景的区分与内部标记、set_memory_* 权限改写的底层机制、实际测量 TLB miss 开销的方法与调优建议。

一、vmalloc 的地址空间组织

1.1 vmalloc 区在虚拟地址空间中的位置

在 x86_64 架构中,vmalloc 区位于内核虚拟地址空间的特定范围:

用户空间         0x0000 0000 0000 - 0x0000 7FFF FFFF FFFF (128TB)--- PAGE_OFFSET ------------------------------------------------直接映射区(direct mapping)  0xFFFF 8000 0000 - start_kernel (所有物理内存一一映射)--- VMALLOC_START (典型: 0xFFFF C900 0000) --------------------vmalloc 区                   ~128TB - 16TB (具体取决于 CONFIG)--- VMALLOC_END (典型: 0xFFFF E8FF FFFF FFFF) ---模块映射区 (modules)          ~640MB--- VMEMMAP_START -----------------------------------struct page 映射区 (vmemmap)--- PKMAP_BASE / FIXADDR_START ---------------------固定映射区--- VSYSCALL / vDSO area ---------------------------
vmalloc 区的起始地址 `VMALLOC_START` 随内核配置和处理器架构变化,在 x86_64 上通常为 `0xFFFFC90000000000`。整个 vmalloc 区的总大小由 `CONFIG_KERNEL` 和 LKMM 决定,典型值在 128TB 到 32TB 之间。
// arch/x86/include/asm/pgtable_64.h#define VMALLOC_START    (unsigned long)0xFFFFC90000000000UL#define VMALLOC_END      (unsigned long)0xFFFFE8FFFFFFFFFFUL#define VMALLOC_SIZE     (VMALLOC_END - VMALLOC_START   1) // ~128TB 实际受限于 vmap 空间

1.2 vm_struct: vmalloc 区域的内核管理元数据

每个 vmalloc 分配的内核区域都由 vm_struct 结构体描述:

struct vm_struct {    struct vm_struct    *next;        // 全局 vmap_area_list 链表    void                *addr;        // 虚拟起始地址    unsigned long       size;         // 区域总大小 (含 guard page)    unsigned long       flags;        // VM_ALLOC/VM_IOREMAP/VM_MAP 等    struct page         **pages;      // 指向物理页数组    unsigned int        nr_pages;     // 页数    phys_addr_t         phys_addr;    // 物理连续时记录首地址    const void          *caller;      // 分配者 (__builtin_return_address)};

关键字段 pages[] 数组保存了指向每个物理页面的 struct page * 指针。当分配成功后,内核通过四级页表(x86_64 上 PGD→P4D→PUD→PMD→PTE)将这些分散的物理页面映射到连续的虚拟地址区间。

1.3 vmap_area: 虚拟区间红黑树节点

为了高效管理 vmalloc 区内的空闲虚拟地址区间,内核使用红黑树组织已分配的 vmap_area:

struct vmap_area {    unsigned long va_start;    // 虚拟起始地址    unsigned long va_end;      // 虚拟结束地址    struct rb_node rb_node;    // 红黑树节点    // 按起始地址排序,用于空闲区间查找    // 空闲链表:list_head purge_list;};

查找空闲区间时使用 rb_find_next_obj_fit_next_slot() 变体算法,在红黑树中找到第一个不小于请求大小的空闲区间,时间复杂度 O(log n)。当分配请求无法满足时,触发 purge_vmap_area_lazy() 以释放延迟解映射的区域。

二、__vmalloc_node_range: 分配核心路径解析

2.1 快速分配路径(VM_ALLOC)

vmalloc 实际调用 vmalloc_node → vmalloc_node_flags → __vmalloc_node_range 的完整链路:

static void *__vmalloc_node_range(unsigned long size, unsigned long align,        unsigned long start, unsigned long end, gfp_t gfp_mask,        pgprot_t prot, unsigned long vm_flags, int node,        const void *caller){    // 步骤 1: 对齐大小,增加 guard page    size = PAGE_ALIGN(size);    size  = PAGE_SIZE; // 添加一个不可访问的 guard page    // 步骤 2: 查找空闲虚拟地址区间    va = alloc_vmap_area(size, align, start, end, node);    if (!va)        return NULL;    // 步骤 2: 向伙伴系统申请物理页    area = kzalloc_node(sizeof(*area), gfp_mask, node);    area-                        
                    
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部