第七十章:vmalloc
元婴后期涉及内核源码:
一
林小源从 Slab 工坊出来,沿着一条狭窄的通道继续往前走。通道的墙壁上嵌满了发光的符号——虚拟地址,一段接一段,像某种古老的铭文。
通道尽头是一片开阔的空间。和别处不同,这里的地面上没有整齐的方块,也没有刻满线条的棋盘。这里的地面是碎片化的——一块块独立的浮岛,悬浮在虚空中,彼此之间有缝隙。但奇妙的是,从高处看下去,这些浮岛排列成一条完美的直线,像是连续的一整块。
"这是什么地方?"
"vmalloc 的领地。"一个声音从半空中传来。
林小源抬头。一个穿着深蓝色长袍的修行者悬浮在半空中,双手各托着一块碎片——两块完全不相邻的浮岛。他的眼睛像两面镜子,映射出无数条光线,每一条都连接着不同的碎片。
"我是 vmalloc。"他说,"虚拟连续,物理不连续。"
他把两块碎片放在空中。它们相距很远——一块在东,一块在西——但他用一条光线把它们连起来。从光线的角度看,两块碎片像是紧挨在一起的。
"这就是我的魔法。"vmalloc 说,"物理页面散落在各处,但通过页表映射,我把它们拼成一段连续的虚拟地址。对使用者来说,它们是连续的——他们看不到缝隙。"
林小源走到一块浮岛旁边,低头看了看。浮岛下面的虚空中,能看到其他浮岛的位置——它们确实不相邻,一块在 0x10000000,另一块在 0x20003000,第三块在 0x30001000。但在虚拟地址空间里,它们被映射成了连续的四个页面。
"为什么不用 kmalloc?"林小源问。
vmalloc 笑了。他的笑容里带着一种"你问到点子上了"的意味。
"kmalloc 分配的是物理连续的内存。"他说,"如果你需要 4KB、8KB、甚至 128KB——kmalloc 没问题,伙伴系统能找到连续的物理页面。但如果你需要 16MB 呢?32MB 呢?"
他伸出手,指向远处。远处的伙伴系统领地里,那些发光的柱子已经很矮了——10 阶的柱子上几乎没有大方块了。
"伙伴系统找不到那么大的连续物理页面。"vmalloc 说,"物理内存已经被分配得七零八落了。但虚拟地址空间——"他指了指头顶,"虚拟地址空间很大。只要能找到零散的物理页面,我就能把它们拼成一段连续的虚拟内存。"
二
林小源在 vmalloc 的领地里走了一圈,渐渐注意到了这里的布局。
这片空间被划分成三个区域,每个区域的地面颜色不同。最近的一块是金色的,地面上刻着"直接映射区"四个字。中间的一块是银色的,刻着"vmalloc 区"。最远处的一块是铜色的,刻着"固定映射区"。
"那些是什么?"
"内核虚拟地址空间的三个区域。"vmalloc 说。他飘到金色区域旁边,用手指点了点地面。
"直接映射区——这是 kmalloc 使用的地方。物理内存直接映射到虚拟地址,偏移是固定的。物理地址 0x10000000 对应虚拟地址 0xFFFF800010000000——加上一个固定的偏移就行。简单,快速,但需要物理连续。"
他又飘到银色区域。
"vmalloc 区——这是我的地盘。物理页面不连续,但通过页表映射成连续的虚拟地址。分配的时候需要逐页修改页表,所以比 kmalloc 慢。但能处理大块内存。"
"铜色的呢?"
"固定映射区。"vmalloc 说,"特殊的用途,特殊的映射关系。你暂时不需要了解。"
林小源看着这三个区域。金色的区域最大,几乎占了一半的空间。银色的区域其次。铜色的区域最小。
"内核的地址空间也是有分区的。"他喃喃自语。
"当然。"vmalloc 说,"每一寸地址空间都有它的用途。就像一座城市——商业区、工业区、住宅区,各司其职。你不能把工厂建在住宅区里,也不能用 vmalloc 的方式去分配 kmalloc 该分配的内存。"
三
林小源正要离开,vmalloc 叫住了他。
"你知道我的代价吗?"
"代价?"
vmalloc 从空中飘下来,落在林小源面前。他的表情变得严肃。
"kmalloc 分配内存,只需要一次——伙伴系统找到合适的块,返回地址,完事。"他说,"但我不一样。我需要——"
他伸出手指,一条一条地数:
"第一,在虚拟地址空间中找到一段连续的空闲区域。第二,逐页分配物理页面——每一页可能来自不同的地方。第三,逐页修改页表,把虚拟页面映射到物理页面。第四,刷新 TLB。"
他收回手指。
"每一次分配,都需要修改页表。页表修改是昂贵的——需要获取锁,需要同步多核,需要刷新 TLB。比 kmalloc 慢得多。"
林小源点了点头。他能感觉到——每次 vmalloc 映射一块浮岛的时候,周围的空间都会轻微震动,像是有什么沉重的机制在运转。
"还有一个更严重的问题。"vmalloc 说,"我的内存不能用于 DMA。"
"为什么?"
"因为 DMA 需要物理连续的内存。"vmalloc 说,"DMA 控制器不认虚拟地址——它只认物理地址。如果你给它一段 vmalloc 的内存,它看到的物理页面是散落的,根本没法连续读写。"
他看着林小源,眼神很认真。
"所以你要记住:vmalloc 是用来处理大块内存的——网络缓冲区、内核模块加载、大型数据结构。但如果你需要 DMA,或者需要极高的性能——用 kmalloc。"
林小源看着脚下那些碎片化的浮岛。它们看起来不完整,不连续,但通过页表的魔法,它们变成了一段完美的连续内存。这就是虚拟地址空间的力量——用映射掩盖物理的碎片。
"每种分配器都有自己的战场。"他轻声说。
"对。"vmalloc 说,"没有万能的分配器。只有合适的分配器。"
/*
* kmalloc vs vmalloc:
*
* kmalloc:
* - 物理连续
* - 虚拟连续
* - 适合小块内存(通常 < 128KB)
* - 使用 Slab 分配器
* - 快速
*
* vmalloc:
* - 物理不连续
* - 虚拟连续
* - 适合大块内存(可以分配几 MB)
* - 使用页表映射
* - 较慢(需要修改页表)
*
* vmalloc 的工作原理:
* 1. 在内核虚拟地址空间中找到一段连续的区域
* 2. 逐页分配物理页面(不连续)
* 3. 修改页表,把虚拟页面映射到物理页面
* 4. 返回虚拟地址
*/
struct vm_struct {
void *addr; /* 虚拟地址 */
unsigned long size; /* 大小 */
int nr_pages; /* 物理页数 */
unsigned long pages[4]; /* 物理页地址(不连续) */
};
printf("=== vmalloc — 虚拟连续的内存 ===\n\n");
/* 模拟 vmalloc 分配 16KB (4 页) */
struct vm_struct vm = {
.addr = (void *)0xFFFF800010000000,
.size = 16384,
.nr_pages = 4,
};
/* 物理页面不连续 */
vm.pages[0] = 0x10000000; /* 物理地址 1 */
vm.pages[1] = 0x20003000; /* 物理地址 2 */
vm.pages[2] = 0x30001000; /* 物理地址 3 */
vm.pages[3] = 0x0FFF8000; /* 物理地址 4 */
printf("vmalloc 分配 16KB:\n");
printf(" 虚拟地址: %p\n", vm.addr);
printf(" 大小: %lu 字节\n", vm.size);
printf(" 物理页数: %d\n\n", vm.nr_pages);
printf("物理页面(不连续):\n");
for (int i = 0; i < vm.nr_pages; i++) {
unsigned long va = (unsigned long)vm.addr + i * 4096;
printf(" VA 0x%lx → PA 0x%lx\n", va, vm.pages[i]);
}
printf("\n--- kmalloc vs vmalloc ---\n");
printf("kmalloc:\n");
printf(" 物理连续: 是\n");
printf(" 虚拟连续: 是\n");
printf(" 大小限制: 通常 < 128KB\n");
printf(" 速度: 快\n");
printf(" 用途: 设备驱动、小数据结构\n\n");
printf("vmalloc:\n");
printf(" 物理连续: 否\n");
printf(" 虚拟连续: 是\n");
printf(" 大大限制: 可以分配几 MB\n");
printf(" 速度: 较慢\n");
printf(" 用途: 大缓冲区、模块加载\n\n");
printf("--- vmalloc 的使用场景 ---\n");
printf("1. 分配大缓冲区(如网络缓冲区)\n");
printf("2. 加载内核模块\n");
printf("3. 分配大的数据结构\n");
printf("4. 不需要物理连续的场景\n");#include <stdio.h>
/*
* kmalloc vs vmalloc:
*
* kmalloc:
* - 物理连续
* - 虚拟连续
* - 适合小块内存(通常 < 128KB)
* - 使用 Slab 分配器
* - 快速
*
* vmalloc:
* - 物理不连续
* - 虚拟连续
* - 适合大块内存(可以分配几 MB)
* - 使用页表映射
* - 较慢(需要修改页表)
*
* vmalloc 的工作原理:
* 1. 在内核虚拟地址空间中找到一段连续的区域
* 2. 逐页分配物理页面(不连续)
* 3. 修改页表,把虚拟页面映射到物理页面
* 4. 返回虚拟地址
*/
struct vm_struct {
void *addr; /* 虚拟地址 */
unsigned long size; /* 大小 */
int nr_pages; /* 物理页数 */
unsigned long pages[4]; /* 物理页地址(不连续) */
};
int main() {
printf("=== vmalloc — 虚拟连续的内存 ===\n\n");
/* 模拟 vmalloc 分配 16KB (4 页) */
struct vm_struct vm = {
.addr = (void *)0xFFFF800010000000,
.size = 16384,
.nr_pages = 4,
};
/* 物理页面不连续 */
vm.pages[0] = 0x10000000; /* 物理地址 1 */
vm.pages[1] = 0x20003000; /* 物理地址 2 */
vm.pages[2] = 0x30001000; /* 物理地址 3 */
vm.pages[3] = 0x0FFF8000; /* 物理地址 4 */
printf("vmalloc 分配 16KB:\n");
printf(" 虚拟地址: %p\n", vm.addr);
printf(" 大小: %lu 字节\n", vm.size);
printf(" 物理页数: %d\n\n", vm.nr_pages);
printf("物理页面(不连续):\n");
for (int i = 0; i < vm.nr_pages; i++) {
unsigned long va = (unsigned long)vm.addr + i * 4096;
printf(" VA 0x%lx → PA 0x%lx\n", va, vm.pages[i]);
}
printf("\n--- kmalloc vs vmalloc ---\n");
printf("kmalloc:\n");
printf(" 物理连续: 是\n");
printf(" 虚拟连续: 是\n");
printf(" 大小限制: 通常 < 128KB\n");
printf(" 速度: 快\n");
printf(" 用途: 设备驱动、小数据结构\n\n");
printf("vmalloc:\n");
printf(" 物理连续: 否\n");
printf(" 虚拟连续: 是\n");
printf(" 大大限制: 可以分配几 MB\n");
printf(" 速度: 较慢\n");
printf(" 用途: 大缓冲区、模块加载\n\n");
printf("--- vmalloc 的使用场景 ---\n");
printf("1. 分配大缓冲区(如网络缓冲区)\n");
printf("2. 加载内核模块\n");
printf("3. 分配大的数据结构\n");
printf("4. 不需要物理连续的场景\n");
return 0;
}道藏笔记
内核启示
分配虚拟连续但物理不连续的内存。
的工作原理:
- 在内核虚拟地址空间中找到连续区域
- 逐页分配物理页面(不连续)
- 修改页表,映射虚拟页面到物理页面
- 返回虚拟地址
vs :
- — 物理连续,快速,适合小块
- — 物理不连续,较慢,适合大块
的使用场景:
- 大缓冲区
- 内核模块加载
- 大数据结构
- 不需要物理连续的场景
的代价:
- 需要修改页表
- 不能用于 DMA
- TLB 压力更大
是"虚拟连续"的魔法——物理不连续,但看起来连续。
vmalloc 之试
需要虚拟地址连续但物理页不必连续的大块内核内存时,本章使用哪个接口?