1. 为什么需要vmalloc机制

第一次接触Linux内核内存管理时,很多人会有这样的疑问:内核已经有了kmalloc这样高效的内存分配器,为什么还需要vmalloc?这个问题要从x86架构的历史说起。在32位系统上,内核只能直接访问1GB的地址空间(3GB-4GB),其中前896MB被设计为直接映射区。这意味着物理内存的前896MB被线性映射到这个区域,访问时只需要简单的地址偏移计算(虚拟地址=物理地址+3GB)。

但这里有个致命限制:如果物理内存超过896MB,多出来的部分就无法通过直接映射区访问了。想象一下你买了4GB内存的电脑,内核却只能用前896MB,这显然不合理。vmalloc机制就是为了解决这个痛点而生的,它像一座桥梁,让内核能够访问那些"遥不可及"的高端内存。

实际项目中我遇到过这样的场景:开发视频处理驱动时需要申请200MB的缓冲区,用kmalloc总是失败,换成vmalloc就完美解决。这是因为:

  1. kmalloc要求物理连续,系统运行一段时间后很难找到这么大的连续空间
  2. vmalloc只需要虚拟地址连续,物理页可以分散,通过页表巧妙"组装"成连续的虚拟空间

2. vmalloc的工作原理

2.1 地址空间布局

现代Linux内核采用更灵活的地址空间划分,但基本原理相同。以x86_64为例:

+---------------------+ 0xffffffffffffffff
|                     |
|    Kernel Space     |
|                     |
+---------------------+ 0xffff800000000000
|                     |
|     vmalloc区       |
|                     |
+---------------------+ VMALLOC_START
|                     |
|    直接映射区        |
|                     |
+---------------------+ __PAGE_OFFSET
|                     |
|    User Space       |
|                     |
+---------------------+ 0x0000000000000000

vmalloc区的起始地址VMALLOC_START在32位系统通常是0xc0000000(3GB)加上vmalloc_offset,64位系统则有更大空间。

2.2 核心数据结构

内核用两个关键结构管理vmalloc区域:

struct vm_struct {
    struct vm_struct *next;
    void *addr;
    unsigned long size;
    unsigned long flags;
    struct page **pages;
    unsigned int nr_pages;
    void *caller;
};

struct vmap_area {
    unsigned long va_start;
    unsigned long va_end;
    struct rb_node rb_node;
    struct list_head list;
};

vm_struct描述一个vmalloc区域,而vmap_area用于红黑树管理。这种设计让我想起数据库的索引——红黑树提供O(log n)的查找效率,链表则便于遍历。

3. vmalloc的实现细节

3.1 分配流程

当调用vmalloc(100MB)时,内核会:

  1. 计算对齐后的size(100MB → 100MB + 保护页)
  2. 在红黑树中查找合适大小的空闲区域
  3. 如果没有足够空间,尝试回收僵尸vmap_area
  4. 分配vm_struct和vmap_area结构
  5. 通过伙伴系统分配物理页
  6. 建立页表映射

关键函数调用链:

vmalloc -> __vmalloc_node_flags -> __vmalloc_node -> __vmalloc_node_range
           -> __get_vm_area_node -> alloc_vmap_area
           -> __vmalloc_area_node -> map_vm_area

3.2 页表映射的玄机

最精妙的是页表处理。由于内核空间是所有进程共享的,vmalloc选择修改init进程的页表:

static int vmalloc_area_pages(unsigned long address, unsigned long size)
{
    pgd_t *dir = pgd_offset_k(address); // 获取init_mm的页目录
    // ...
    do {
        pmd_t *pmd = pmd_alloc(&init_mm, dir, address);
        alloc_area_pmd(pmd, address, end - address, ...);
    } while(...);
}

其他进程访问vmalloc区域时,会触发缺页异常,内核将init_mm的页表项复制到当前进程。这种"写时复制"的机制既保证了效率,又确保了一致性。

4. 性能考量与最佳实践

4.1 为什么vmalloc比kmalloc慢

在我的性能测试中,vmalloc比kmalloc慢3-5倍,原因在于:

  1. 需要建立页表映射
  2. TLB失效导致的缓存抖动
  3. 物理页不连续增加缓存未命中率

一个实际案例:网络驱动中用vmalloc分配接收缓冲区,吞吐量下降了40%。改用kmalloc后性能恢复正常。

4.2 什么时候该用vmalloc

根据经验,这些场景适合使用vmalloc:

  1. 分配大块内存(>128KB)
  2. 模块加载时分配代码/数据区
  3. 需要虚拟地址连续但物理不连续的缓冲区
  4. 驱动需要兼容多种架构(如DMA映射)

5. 高级话题:vmalloc的变种

5.1 vzalloc

相当于vmalloc+memset,适合需要清零内存的场景:

void *vzalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, -1, GFP_KERNEL | __GFP_ZERO);
}

5.2 vmap

将离散的物理页映射到连续虚拟空间:

void *vmap(struct page **pages, unsigned int count, 
           unsigned long flags, pgprot_t prot)
{
    struct vm_struct *area;
    area = get_vm_area(size, flags);
    if (map_vm_area(area, prot, pages))
        return NULL;
    return area->addr;
}

在DMA scatter-gather操作中特别有用。

6. 调试与问题排查

遇到vmalloc失败时,可以:

  1. 检查dmesg输出
  2. 查看/proc/vmallocinfo
  3. 使用vmalloc_debug内核选项
  4. 检查系统内存碎片情况

一个实用技巧:在模块代码中加入:

pr_info("vmalloc area: %px, size: %lu\n", ptr, size);

这样可以在日志中跟踪内存分配情况。

7. 现代内核的改进

随着64位系统普及,vmalloc的角色有所变化:

  1. 直接映射区可以覆盖更大物理内存
  2. 引入vmalloc_fault加速缺页处理
  3. 改进红黑树算法减少锁竞争
  4. 支持NUMA架构的智能分配

但vmalloc机制的核心思想依然适用,特别是在嵌入式系统和大内存场景下。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐