第六十九章:Slab
元婴后期涉及内核源码:
一
林小源在伙伴系统的领地边缘发现了一条小路。
小路蜿蜒向下,通向一个地下的工坊。他沿着石阶走下去,听到里面传来叮叮当当的声响——像是有人在敲打什么东西。
工坊很大,灯光昏暗。四面墙上凿满了整齐的小格子,每个格子里都放着一个形状相同的物件。有些格子是空的,有些格子里的物件散发着微弱的光。
"这是什么地方?"
"Slab。"一个声音从工坊深处传来。
林小源循声望去。一个矮胖的工匠从阴影中走出来,手里拿着一把精细的刻刀。他的围裙上沾满了碎屑,手指上布满了老茧。他的眼睛很小,但极其专注,像是在丈量什么东西的尺寸。
"Slab 分配器。"工匠说,"小块内存的分配器。"
他走到墙边,从一个格子里取出一个物件。那是一个规则的立方体,边长约 64 字节——林小源不知道自己怎么知道这个尺寸的,但他就是知道。
"伙伴系统只能分配 2 的幂次大小的块——最小 4KB。"工匠说,"但内核经常需要分配几十字节、几百字节的小东西。task_struct、inode、dentry——这些东西不需要整页。如果每次都分配 4KB,剩下的全浪费了。"
他把立方体放在工作台上,拿起刻刀,在上面刻了几道纹路。
"所以我把一整页划分成固定大小的小块。"他说,"分配的时候,从空闲列表里取一个。释放的时候,放回去。简单,快速,不浪费。"
林小源看着墙上那些格子。他注意到每面墙上的格子大小不同——有的很小,有的稍大一些。每面墙上都挂着一个标签:8、16、32、64、128、256……
"那些数字是什么?"
"大小类。"工匠说,"每个大小类有自己的缓存。你需要 64 字节,就从 64 字节的墙上去取。需要 128 字节,就从 128 字节的墙上去取。"
他用刻刀敲了敲工作台。台上的一块页面——一整块 4KB 的物理页面——裂成了 64 个小立方体,整整齐齐地排列着。
"一块 4KB 的页面,可以切成 64 个 64 字节的对象。"他说,"比分配整页高效多了。"
二
林小源在工坊里转了一圈,渐渐发现了更多细节。
有些格子里的物件不是新的——上面有使用过的痕迹,有磨损,有划痕。但它们被放回了格子里,等待下一次被取出。
"那些旧物件——"
"复用。"工匠说,"这是 Slab 最精妙的地方。"
他从一个格子里取出一个旧物件,递给林小源。物件上刻着 的字样,边缘有些磨损。
"这是一个被释放的 task_struct。"工匠说,"一个进程退出了,它的 task_struct 被释放了。但我不销毁它——我把它放回原来的格子里。下一个进程创建的时候,我直接把这个旧的取出来,清空数据,就能用了。"
"不需要重新分配?"
"不需要。"工匠说,"内存已经分配好了,对象的大小也对。只需要清空数据就行。这比向伙伴系统申请新页面快得多。"
林小源看着那个旧物件。它的形状完整,结构完好,只是里面的数据被清空了。就像一间被清空的房间——墙壁还在,门还在,只是家具换了。
"这就是对象复用。"工匠说,"频繁分配和释放的对象——进程、文件、目录——都可以复用。不需要每次都从头分配,只需要把旧的取出来重新用。"
他把那个旧物件放回格子里,轻轻拍了拍它。
"好好休息。"他低声说,"很快就会有人来接你了。"
三
林小源正要离开,工匠叫住了他。
"你知道 吗?"
"知道。"林小源说,"内核中最常用的内存分配函数。"
"你知道它是怎么工作的吗?"
林小源摇了摇头。
工匠走到墙上,指着那些大小类的标签。
"当内核调用 kmalloc(128) 的时候,"他说,"Slab 会去找 128 字节的缓存。如果那个缓存里有空闲对象——直接取出来,O(1)。如果没有——从伙伴系统申请新的页面,切成 128 字节的对象,再分配。"
"所以 kmalloc 是——"
"是一个智能的分拣器。"工匠说,"它根据你请求的大小,选择最合适的 Slab 缓存。请求 8 字节,就用 8 字节的缓存。请求 2000 字节——最接近的是 2048 字节的缓存,就用那个。"
林小源看着墙上那些格子。每个格子都是一个 Slab 缓存,每个缓存都有自己的大小类和空闲列表。kmalloc 就像一个经验丰富的采购员——知道每种材料在哪里,知道哪个格子有存货,知道哪个格子需要补货。
"但 kmalloc 分配的内存是物理连续的。"工匠突然说,"如果你需要很大的一块——比如几 MB——kmalloc 可能分配不出来。因为伙伴系统找不到那么大的连续物理页面。"
"那怎么办?"
工匠笑了。他的笑容很短,像是在说"那就是另一个故事了"。
"你去找 vmalloc。"他说,"它能做 kmalloc 做不到的事。"
/*
* Slab 分配器的工作原理:
*
* 1. 从伙伴系统分配一整页(或多页)
* 2. 把页面划分成固定大小的"对象"
* 3. 分配时,从空闲列表中取出一个对象
* 4. 释放时,把对象放回空闲列表
*
* Slab 的好处:
* - 减少内部碎片(小对象不需要分配整页)
* - 分配/释放非常快(O(1))
* - 对象缓存(频繁分配/释放的对象可以复用)
*
* kmalloc 的大小类:
* 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096...
* 每个大小类有自己的 slab 缓存
*/
struct slab_object {
int in_use;
char data[64];
};
struct slab_cache {
const char *name;
int object_size;
int objects_per_slab;
struct slab_object objects[8];
int nr_free;
};
void *slab_alloc(struct slab_cache *cache) {
for (int i = 0; i < cache->objects_per_slab; i++) {
if (!cache->objects[i].in_use) {
cache->objects[i].in_use = 1;
cache->nr_free--;
return &cache->objects[i];
}
}
return NULL; /* slab 已满 */
}
void slab_free(struct slab_cache *cache, void *ptr) {
struct slab_object *obj = (struct slab_object *)ptr;
obj->in_use = 0;
cache->nr_free++;
}
printf("=== Slab — 小块内存的分配器 ===\n\n");
/* 创建 slab 缓存 */
struct slab_cache task_cache = {
.name = "task_struct",
.object_size = 64,
.objects_per_slab = 8,
.nr_free = 8,
};
memset(task_cache.objects, 0, sizeof(task_cache.objects));
printf("Slab 缓存: %s\n", task_cache.name);
printf("对象大小: %d 字节\n", task_cache.object_size);
printf("每 slab 对象数: %d\n", task_cache.objects_per_slab);
printf("空闲对象: %d\n\n", task_cache.nr_free);
/* 分配对象 */
printf("--- 分配对象 ---\n");
void *objs[4];
for (int i = 0; i < 4; i++) {
objs[i] = slab_alloc(&task_cache);
printf("分配对象 %d: %p (空闲: %d)\n",
i, objs[i], task_cache.nr_free);
}
/* 释放对象 */
printf("\n--- 释放对象 ---\n");
slab_free(&task_cache, objs[1]);
printf("释放对象 1: %p (空闲: %d)\n", objs[1], task_cache.nr_free);
slab_free(&task_cache, objs[3]);
printf("释放对象 3: %p (空闲: %d)\n", objs[3], task_cache.nr_free);
printf("\n--- Slab vs 伙伴系统 ---\n");
printf("伙伴系统:\n");
printf(" - 分配 2^n 页\n");
printf(" - 最小 4KB\n");
printf(" - 适合大块内存\n\n");
printf("Slab:\n");
printf(" - 分配固定大小的对象\n");
printf(" - 最小 8 字节\n");
printf(" - 适合小块内存\n");
printf(" - 对象可以复用\n");
printf("\n--- kmalloc 的大小类 ---\n");
int sizes[] = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 };
for (int i = 0; i < 10; i++)
printf(" %d 字节\n", sizes[i]);#include <stdio.h>
#include <string.h>
/*
* Slab 分配器的工作原理:
*
* 1. 从伙伴系统分配一整页(或多页)
* 2. 把页面划分成固定大小的"对象"
* 3. 分配时,从空闲列表中取出一个对象
* 4. 释放时,把对象放回空闲列表
*
* Slab 的好处:
* - 减少内部碎片(小对象不需要分配整页)
* - 分配/释放非常快(O(1))
* - 对象缓存(频繁分配/释放的对象可以复用)
*
* kmalloc 的大小类:
* 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096...
* 每个大小类有自己的 slab 缓存
*/
struct slab_object {
int in_use;
char data[64];
};
struct slab_cache {
const char *name;
int object_size;
int objects_per_slab;
struct slab_object objects[8];
int nr_free;
};
void *slab_alloc(struct slab_cache *cache) {
for (int i = 0; i < cache->objects_per_slab; i++) {
if (!cache->objects[i].in_use) {
cache->objects[i].in_use = 1;
cache->nr_free--;
return &cache->objects[i];
}
}
return NULL; /* slab 已满 */
}
void slab_free(struct slab_cache *cache, void *ptr) {
struct slab_object *obj = (struct slab_object *)ptr;
obj->in_use = 0;
cache->nr_free++;
}
int main() {
printf("=== Slab — 小块内存的分配器 ===\n\n");
/* 创建 slab 缓存 */
struct slab_cache task_cache = {
.name = "task_struct",
.object_size = 64,
.objects_per_slab = 8,
.nr_free = 8,
};
memset(task_cache.objects, 0, sizeof(task_cache.objects));
printf("Slab 缓存: %s\n", task_cache.name);
printf("对象大小: %d 字节\n", task_cache.object_size);
printf("每 slab 对象数: %d\n", task_cache.objects_per_slab);
printf("空闲对象: %d\n\n", task_cache.nr_free);
/* 分配对象 */
printf("--- 分配对象 ---\n");
void *objs[4];
for (int i = 0; i < 4; i++) {
objs[i] = slab_alloc(&task_cache);
printf("分配对象 %d: %p (空闲: %d)\n",
i, objs[i], task_cache.nr_free);
}
/* 释放对象 */
printf("\n--- 释放对象 ---\n");
slab_free(&task_cache, objs[1]);
printf("释放对象 1: %p (空闲: %d)\n", objs[1], task_cache.nr_free);
slab_free(&task_cache, objs[3]);
printf("释放对象 3: %p (空闲: %d)\n", objs[3], task_cache.nr_free);
printf("\n--- Slab vs 伙伴系统 ---\n");
printf("伙伴系统:\n");
printf(" - 分配 2^n 页\n");
printf(" - 最小 4KB\n");
printf(" - 适合大块内存\n\n");
printf("Slab:\n");
printf(" - 分配固定大小的对象\n");
printf(" - 最小 8 字节\n");
printf(" - 适合小块内存\n");
printf(" - 对象可以复用\n");
printf("\n--- kmalloc 的大小类 ---\n");
int sizes[] = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 };
for (int i = 0; i < 10; i++)
printf(" %d 字节\n", sizes[i]);
return 0;
}道藏笔记
内核启示
Slab 分配器是内核分配小块内存的核心机制。
Slab 的工作原理:
- 从伙伴系统分配一整页
- 把页面划分成固定大小的对象
- 分配时从空闲列表取出对象
- 释放时把对象放回空闲列表
Slab 的优势:
- 减少内部碎片
- 分配/释放 O(1)
- 对象复用(频繁分配/释放的对象可以复用)
的大小类:
- 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096
- 根据请求大小选择合适的 Slab 缓存
Slab 是小块内存的"对象池"——复用是效率的关键。
Slab 之试
小块内核内存不适合直接按页分配,本章常用的分配接口是什么?