B08 —— 内核空间和用户空间的内存布局:vmalloc vs kmalloc
金句:虚拟地址空间的划分,是操作系统最重要的设计决策之一。
x86-64 的虚拟地址空间布局
现代 Linux 使用 48-bit 虚拟地址空间,用户空间和内核空间各自有独立的区域:
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内核内存分配器家族
Linux 内核有多个内存分配器,分别针对不同场景:
内核内存分配器家族:
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:SLAB/SLUB 分配器
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:不触发文件系统操作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:虚拟连续,物理不一定连续
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;
}vmalloc 的性能特点:
1. 分配延迟:
- 需要分配多个物理页(可能有睡眠)
- 需要建立多个页表条目(逐页映射)
- 比 kmalloc 慢(10x~100x 差异)
2. TLB 开销:
- 物理页不连续 → TLB 条目多 → TLB miss 率高
- 对于大缓冲区影响明显
3. 内存碎片:
- vmalloc 区域可能碎片化
- 长时间运行后,vmalloc 可能失败
优化:vm_map_ram / vmap
- 把已经分配的物理页批量映射
- 比逐页 vmalloc 快ioremap:映射外设寄存器
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);总结
- 虚拟地址空间布局:x86-64 用户空间 128TB(低),内核空间 128TB(高)
- 内核空间细分:直接映射区(物理+offset)/ vmalloc 区域(动态映射)/ 固定映射区 / MMIO 保留区
- kmalloc:SLUB 分配,固定大小(2^n),O(1) 分配,per-CPU 缓存
- vmalloc:虚拟地址连续,物理可不连续,需要建立多个页表映射,TLB miss 高
- ioremap:映射外设 MMIO,属性为 device(不可缓存),不是从 DRAM 分配
下篇预告(B09):系统休眠时内存数据去哪了?kswapd、swapping、以及 Page Frame reclaim 机制。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论