B08 — Memory Layout of Kernel Space and User Space: vmalloc vs kmalloc

Key Insight: The division of virtual address space is one of the most important OS design decisions.


x86-64 Virtual Address Space Layout

Modern Linux uses a 48-bit virtual address space. User space and kernel space each have separate regions:

Bash
x86-64 虚拟地址空间布局:

0x0000000000000000 ~ 0x00007FFFFFFFFFFF(128TB):
  用户空间(User Space)
  - 最高 128TB(48-bit 地址的一半)
  - 每个进程有独立的页表(隔离)
  - 最高地址是 0x00007FFF_FFFFFFFF

0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(128TB):
  内核空间(Kernel Space)
  - 最低 128TB(内核直接映射区)
  - 所有进程共享同一个内核页表
  - 内核在此区域访问物理内存

细分(内核空间):
  0xFFFF800000000000 ~ 0xFFFF88007FFFFFFF:
    直接映射区(直接映射到物理内存)
    → 物理地址 + PAGE_OFFSET = 虚拟地址
    → 简单高效,但地址空间有限

  0xFFFF880100000000 ~ 0xFFFFC3FFFFFFFFFF:
    vmalloc 区域(动态映射)
    → 用于 vmalloc、模块加载、ioremap
    → 可以映射任意物理地址,不连续

  0xFFFFC40000000000 ~ 0xFFFFC7FFFFFFFFFFF:
    固定映射区(Fixed Address Mapping)
    → 用于 early boot、ACPI、特殊用途

  0xFFFFC80000000000 ~ 0xFFFFFFFFFFFFFFFF:
    保留区/PCI MMIO/硬件区域
    → 用于 MMIO、ACPI NVS、firmware

The Kernel Memory Allocator Family

The Linux kernel has multiple memory allocators for different scenarios:

Bash
内核内存分配器家族:

1. kmalloc / kfree
   - 分配固定大小的内存(8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 bytes)
   - 基于 SLAB/SLUB 分配器(底层是 buddy system)
   - 优点:O(1) 分配,延迟确定
   - 缺点:有内部碎片(固定大小)

2. vmalloc / vfree
   - 分配虚拟地址连续的内存
   - 物理地址可以不连续
   - 用于大块缓冲区(> 4KB)
   - 优点:地址空间连续
   - 缺点:需要更新页表,可能有 TLB miss

3. alloc_pages / __free_pages
   - 直接从 buddy system 分配页(4KB 或更大)
   - 底层分配,不经过 SLAB
   - 用于大型数据结构和 DMA 缓冲区

4. ioremap / iounmap
   - 把物理地址(外设 MMIO)映射到内核虚拟地址
   - 用于访问设备寄存器
   - 不是从 DRAM 分配,是建立映射

kmalloc: The SLAB/SLUB Allocator

Bash
kmalloc 基于 SLUB 分配器:

kmalloc 是 SLUB 分配器的一个"接口",提供固定大小的分配:
  - 通过 per-CPU 缓存(kmem_cache)加速分配
  - 每个 CPU 有自己的缓存,避免锁竞争
  - 缓存满了才从 buddy system 补充

kmalloc 的大小(Power of 2):
  32 bytes / 64 bytes / 128 bytes / 256 bytes /
  512 bytes / 1024 bytes / 2048 bytes / 4096 bytes

使用示例:
  char *buf = kmalloc(256, GFP_KERNEL);
  if (!buf) return -ENOMEM;
  // ... 使用 ...
  kfree(buf);

GFP_KERNEL 标志:
  - GFP_KERNEL:可以睡眠等待内存(正常分配)
  - GFP_ATOMIC:不能睡眠,中断上下文用
  - GFP_NOIO:不触发磁盘 I/O
  - GFP_NOFS:不触发文件系统操作

Bash
SLUB 分配器的工作原理:

struct kmem_cache {
    const char *name;          // 缓存名称
    size_t object_size;       // 对象大小
    size_t size;               // 对齐后大小
    void (*ctor)(void *);     // 构造函数

    struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 缓存
    struct list_head *node;   // NUMA 节点链表
    unsigned int size;        // 实际分配大小
    unsigned int offset;      // 空闲链表偏移
};

分配路径:
  1. 从 current CPU 的 kmem_cache_cpu 取对象(无锁 O(1))
  2. 如果 CPU 缓存为空,从 page->freelist 取(来自 buddy)
  3. 如果 page 空,从 buddy system 分配新 page
  4. 填充 freelist(预计算好链表)

vmalloc: Virtually Contiguous, Physically Not Necessarily

Bash
vmalloc 的使用场景:

适用:
  - 需要大块内存(> 4KB)
  - 需要虚拟地址连续(但物理地址不要求连续)
  - 例如:DMA 临时缓冲区、大型数据结构

不适用:
  - 需要 DMA 传输(DMA 需要物理地址连续)
  - 需要高性能(vmalloc 比 kmalloc 慢)

vmalloc 的实现:

void *vmalloc(unsigned long size)
{
    // 1. 计算需要的页数
    nr_pages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;

    // 2. 从 vmalloc 区域找一个虚拟地址范围
    //    (通过 vmap_area 红黑树查找)
    addr = __get_vm_area_cached(size, ...);

    // 3. 对每页,从 buddy system 分配物理页
    //    并建立临时映射(pgd → pud → pmd → pte)
    for (i = 0; i < nr_pages; i++) {
        page = alloc_page(GFP_KERNEL);
        // 逐页建立页表映射
        // 不是连续的物理页,但虚拟地址连续
    }

    // 4. 返回虚拟地址
    return (void *)addr;
}

Bash
vmalloc 的性能特点:

1. 分配延迟:
   - 需要分配多个物理页(可能有睡眠)
   - 需要建立多个页表条目(逐页映射)
   - 比 kmalloc 慢(10x~100x 差异)

2. TLB 开销:
   - 物理页不连续 → TLB 条目多 → TLB miss 率高
   - 对于大缓冲区影响明显

3. 内存碎片:
   - vmalloc 区域可能碎片化
   - 长时间运行后,vmalloc 可能失败

优化:vm_map_ram / vmap
  - 把已经分配的物理页批量映射
  - 比逐页 vmalloc 快

ioremap: Mapping Peripheral Registers

Bash
ioremap 不是分配内存,是建立映射:

void __iomem *ioremap(phys_addr_t phys_addr, size_t size)
{
    // 1. 从 vmalloc 区域分配虚拟地址
    // 2. 建立新的页表映射(属性:不可缓存 / device)
    // 3. 返回虚拟地址
}

ioremap 的特殊性:
  - 不是从 DRAM 分配,是映射外设的 MMIO 寄存器
  - 页面属性:Memory Type = Device(不可缓存)
  - 写入可能立刻到外设硬件
  - 读取可能触发外设读取引脚

使用示例:
  volatile u32 *reg = ioremap(0xFED00000, 0x1000);
  *reg = 0x1234;  // 写外设寄存器
  val = *reg;     // 读外设寄存器
  iounmap(reg);

Summary

  • Virtual Address Space Layout: x86-64 user space 128TB (low), kernel space 128TB (high)
  • Kernel Space Subdivision: Direct mapping (physical+offset) / vmalloc area (dynamic mapping) / fixed mapping / MMIO reserved
  • kmalloc: SLUB allocation; fixed size (2^n); O(1) allocation; per-CPU cache
  • vmalloc: Virtually contiguous; physically may be non-contiguous; requires multiple page table mappings; high TLB miss rate
  • ioremap: Maps peripheral MMIO; attribute is device (non-cacheable); not allocated from DRAM

Next (B09): Where does memory data go during system sleep? kswapd, swapping, and Page Frame Reclaim mechanism.


Last modified: 2024年9月12日

Author

Comments

Write a Reply or Comment

Your email address will not be published.