8.2. 后备缓存
最后更新于:2022-04-01 02:58:03
## 8.2. 后备缓存
一个设备驱动常常以反复分配许多相同大小的对象而结束. 如果内核已经维护了一套相同大小对象的内存池, 为什么不增加一些特殊的内存池给这些高容量的对象? 实际上, 内核确实实现了一个设施来创建这类内存池, 它常常被称为一个后备缓存. 设备驱动常常不展示这类的内存行为, 它们证明使用一个后备缓存是对的, 但是, 有例外; 在 Linux 2.6 中 USB 和 SCSI 驱动使用缓存.
Linux 内核的缓存管理者有时称为" slab 分配器". 因此, 它的功能和类型在 <linux/slab.h> 中声明. slab 分配器实现有一个 kmem_cache_t 类型的缓存; 使用一个对 kmem_cache_create 的调用来创建它们:
~~~
kmem_cache_t *kmem_cache_create(const char *name, size_t size,
size_t offset,
unsigned long flags,
void (*constructor)(void *, kmem_cache_t *,
unsigned long flags), void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
~~~
这个函数创建一个新的可以驻留任意数目全部同样大小的内存区的缓存对象, 大小由 size 参数指定. name 参数和这个缓存关联并且作为一个在追踪问题时有用的管理信息; 通常, 它被设置为被缓存的结构类型的名子. 这个缓存保留一个指向 name 的指针, 而不是拷贝它, 因此驱动应当传递一个指向在静态存储中的名子的指针(常常这个名子只是一个文字字串). 这个名子不能包含空格.
offset 是页内的第一个对象的偏移; 它可被用来确保一个对被分配的对象的特殊对齐, 但是你最可能会使用 0 来请求缺省值. flags 控制如何进行分配并且是下列标志的一个位掩码:
SLAB_NO_REAP
设置这个标志保护缓存在系统查找内存时被削减. 设置这个标志通常是个坏主意; 重要的是避免不必要地限制内存分配器的行动自由.
SLAB_HWCACHE_ALIGN
这个标志需要每个数据对象被对齐到一个缓存行; 实际对齐依赖主机平台的缓存分布. 这个选项可以是一个好的选择, 如果在 SMP 机器上你的缓存包含频繁存取的项. 但是, 用来获得缓存行对齐的填充可以浪费可观的内存量.
SLAB_CACHE_DMA
这个标志要求每个数据对象在 DMA 内存区分配.
还有一套标志用来调试缓存分配; 详情见 mm/slab.c. 但是, 常常地, 在用来开发的系统中, 这些标志通过一个内核配置选项被全局性地设置
函数的 constructor 和 destructor 参数是可选函数( 但是可能没有 destructor, 如果没有 constructor ); 前者可以用来初始化新分配的对象, 后者可以用来"清理"对象在它们的内存被作为一个整体释放回给系统之前.
构造函数和析构函数会有用, 但是有几个限制你必须记住. 一个构造函数在分配一系列对象的内存时被调用; 因为内存可能持有几个对象, 构造函数可能被多次调用. 你不能假设构造函数作为分配一个对象的一个立即的结果而被调用. 同样地, 析构函数可能在以后某个未知的时间中调用, 不是立刻在一个对象被释放后. 析构函数和构造函数可能或不可能被允许睡眠, 根据它们是否被传递 SLAB_CTOR_ATOMIC 标志(这里 CTOR 是 constructor 的缩写).
为方便, 一个程序员可以使用相同的函数给析构函数和构造函数; slab 分配器常常传递 SLAB_CTOR_CONSTRUCTOR 标志当被调用者是一个构造函数.
一旦一个对象的缓存被创建, 你可以通过调用 kmem_cache_alloc 从它分配对象.
~~~
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
~~~
这里, cache 参数是你之前已经创建的缓存; flags 是你会传递给 kmalloc 的相同, 并且被参考如果 kmem_cache_alloc 需要出去并分配更多内存.
为释放一个对象, 使用 kmem_cache_free:
~~~
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
~~~
当驱动代码用完这个缓存, 典型地当模块被卸载, 它应当如下释放它的缓存:
~~~
int kmem_cache_destroy(kmem_cache_t *cache);
~~~
这个销毁操作只在从这个缓存中分配的所有的对象都已返回给它时才成功. 因此, 一个模块应当检查从 kmem_cache_destroy 的返回值; 一个失败指示某类在模块中的内存泄漏(因为某些对象已被丢失.)
使用后备缓存的一方面益处是内核维护缓冲使用的统计. 这些统计可从 /proc/slabinfo 获得.
### 8.2.1. 一个基于 Slab 缓存的 scull: scullc
是时候给个例子了. scullc 是一个简化的 scull 模块的版本, 它只实现空设备 -- 永久的内存区. 不象 scull, 它使用 kmalloc, scullc 使用内存缓存. 量子的大小可在编译时和加载时修改, 但是不是在运行时 -- 这可能需要创建一个新内存区, 并且我们不想处理这些不必要的细节.
scullc 使用一个完整的例子, 可用来试验 slab 分配器. 它区别于 scull 只在几行代码. 首先, 我们必须声明我们自己的 slab 缓存:
~~~
/* declare one cache pointer: use it for all devices */
kmem_cache_t *scullc_cache;
~~~
slab 缓存的创建以这样的方式处理( 在模块加载时 ):
~~~
/* scullc_init: create a cache for our quanta */
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* no ctor/dtor */
if (!scullc_cache)
{
scullc_cleanup();
return -ENOMEM;
}
~~~
这是它如何分配内存量子:
~~~
/* Allocate a quantum using the memory cache */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, scullc_quantum);
}
~~~
还有这些代码行释放内存:
~~~
for (i = 0; i < qset; i++)
if (dptr->data[i])
kmem_cache_free(scullc_cache, dptr->data[i]);
~~~
最后, 在模块卸载时, 我们不得不返回缓存给系统:
~~~
/* scullc_cleanup: release the cache of our quanta */
if (scullc_cache)
kmem_cache_destroy(scullc_cache);
~~~
从 scull 到 scullc 的主要不同是稍稍的速度提升以及更好的内存使用. 因为量子从一个恰好是合适大小的内存片的池中分配, 它们在内存中的排列是尽可能的密集, 与 scull 量子的相反, 它带来一个不可预测的内存碎片.
### 8.2.2. 内存池
在内核中有不少地方内存分配不允许失败. 作为一个在这些情况下确保分配的方式, 内核开发者创建了一个已知为内存池(或者是 "mempool" )的抽象. 一个内存池真实地只是一类后备缓存, 它尽力一直保持一个空闲内存列表给紧急时使用.
一个内存池有一个类型 mempool_t ( 在 <linux/mempool.h> 中定义); 你可以使用 mempool_create 创建一个:
~~~
mempool_t *mempool_create(int min_nr,
mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn,
void *pool_data);
~~~
min_nr 参数是内存池应当一直保留的最小数量的分配的对象. 实际的分配和释放对象由 alloc_fn 和 free_fn 处理, 它们有这些原型:
~~~
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
~~~
给 mempool_create 最后的参数 ( pool_data ) 被传递给 alloc_fn 和 free_fn.
如果需要, 你可编写特殊用途的函数来处理 mempool 的内存分配. 常常, 但是, 你只需要使内核 slab 分配器为你处理这个任务. 有 2 个函数 ( mempool_alloc_slab 和 mempool_free_slab) 来进行在内存池分配原型和 kmem_cache_alloc 和 kmem_cache_free 之间的感应淬火. 因此, 设置内存池的代码常常看来如此:
~~~
cache = kmem_cache_create(. . .);
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache);
~~~
一旦已创建了内存池, 可以分配和释放对象,使用:
~~~
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
~~~
当内存池创建了, 分配函数将被调用足够的次数来创建一个预先分配的对象池. 因此, 对 mempool_alloc 的调用试图从分配函数请求额外的对象; 如果那个分配失败, 一个预先分配的对象(如果有剩下的)被返回. 当一个对象被用 mempool_free 释放, 它保留在池中, 如果对齐预分配的对象数目小于最小量; 否则, 它将被返回给系统.
一个 mempool 可被重新定大小, 使用:
~~~
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
~~~
这个调用, 如果成功, 调整内存池的大小至少有 new_min_nr 个对象. 如果你不再需要一个内存池, 返回给系统使用:
~~~
void mempool_destroy(mempool_t *pool);
~~~
你编写返回所有的分配的对象, 在销毁 mempool 之前, 否则会产生一个内核 oops.
如果你考虑在你的驱动中使用一个 mempool, 请记住一件事: mempools 分配一块内存在一个链表中, 对任何真实的使用是空闲和无用的. 容易使用 mempools 消耗大量的内存. 在几乎每个情况下, 首选的可选项是不使用 mempool 并且代替以简单处理分配失败的可能性. 如果你的驱动有任何方法以不危害到系统完整性的方式来响应一个分配失败, 就这样做. 驱动代码中的 mempools 的使用应当少.