Skip to content

第六十九章:Slab

元婴后期

涉及内核源码:

林小源在伙伴系统的领地边缘发现了一条小路。

小路蜿蜒向下,通向一个地下的工坊。他沿着石阶走下去,听到里面传来叮叮当当的声响——像是有人在敲打什么东西。

工坊很大,灯光昏暗。四面墙上凿满了整齐的小格子,每个格子里都放着一个形状相同的物件。有些格子是空的,有些格子里的物件散发着微弱的光。

"这是什么地方?"

"Slab。"一个声音从工坊深处传来。

林小源循声望去。一个矮胖的工匠从阴影中走出来,手里拿着一把精细的刻刀。他的围裙上沾满了碎屑,手指上布满了老茧。他的眼睛很小,但极其专注,像是在丈量什么东西的尺寸。

"Slab 分配器。"工匠说,"小块内存的分配器。"

他走到墙边,从一个格子里取出一个物件。那是一个规则的立方体,边长约 64 字节——林小源不知道自己怎么知道这个尺寸的,但他就是知道。

"伙伴系统只能分配 2 的幂次大小的块——最小 4KB。"工匠说,"但内核经常需要分配几十字节、几百字节的小东西。task_struct、inode、dentry——这些东西不需要整页。如果每次都分配 4KB,剩下的全浪费了。"

他把立方体放在工作台上,拿起刻刀,在上面刻了几道纹路。

"所以我把一整页划分成固定大小的小块。"他说,"分配的时候,从空闲列表里取一个。释放的时候,放回去。简单,快速,不浪费。"

林小源看着墙上那些格子。他注意到每面墙上的格子大小不同——有的很小,有的稍大一些。每面墙上都挂着一个标签:8163264128256……

"那些数字是什么?"

"大小类。"工匠说,"每个大小类有自己的缓存。你需要 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 做不到的事。"


c
/*
 * 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]);

道藏笔记

内核启示

Slab 分配器是内核分配小块内存的核心机制。

Slab 的工作原理:

  1. 从伙伴系统分配一整页
  2. 把页面划分成固定大小的对象
  3. 分配时从空闲列表取出对象
  4. 释放时把对象放回空闲列表

Slab 的优势:

  • 减少内部碎片
  • 分配/释放 O(1)
  • 对象复用(频繁分配/释放的对象可以复用)

的大小类:

  • 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096
  • 根据请求大小选择合适的 Slab 缓存

Slab 是小块内存的"对象池"——复用是效率的关键。


破关试炼

Slab 之试

小块内核内存不适合直接按页分配,本章常用的分配接口是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道