slab分配器,是内核为了达到高效利用内存的一种管理算法,它以牺牲一些内存空间的代价,收获了代码在时间上的利益.
1.slab的动机:
在操作系统动作过程中,经常会涉及到大量对象的重复生成、使用与释放.在LINUX系统中所用到的对象,比较典型的例子是inode、task_struct等.这些大量的常用的对象如果每次都要从无到有生成、投入使用、使用完再释放,类似这样的操作频率很高.而且,内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间.如果遵循传统的流程"生成->使用->释放",将是一个很耗时耗力的过程,而且很容易产生内存碎片.内核为优化这种情况,提供了"内存池"这样的一种策略.大量的常用的对象的基本的核心的骨架存放在内存池,当有需要用到这些对象的时候,直接从内存池上摘取,稍加初始化就可以了,这种策略的结果就是大大提高了效率,减少内存碎片的产生.其代价就是要浪费掉一些内存空间.slab分配器就是其中比较典型的策略.
2.slab的策略:
slab出现的目的就是为了实现内存的高效利用.那么,进一步的策略细则又是怎么样的呢?slab内存管理也是以树状形式去进行管理的.
cache_chain:
slab高速缓存树的根.它下一级就是slab高速缓存.即cache_chain每一个元素都是一个kmem_cache;
kmem_cache:
slab高速缓存.它下一级就是一定数量slab的集合.即kmem_cache所管理的slab集合有三种状态,分别是满、部分满、空,对应内核术语就是slabs_full、slabs_partial、slabs_empty;
slab:
每一个slab都是一段连续的内存区域.它才是用来承载数据的实体内存区域.它是slab分配器管理的最小单元.上述的cache_chain、kmem_cache只是一种组织方式,对实际的数据交互有任何的影响.因此,每一个slab根据其内部对象分配的多寡归属于其所属于的kmem_cache哪个区间(slabs_full、slabs_partial和slabs_empty).因此,slab分配器用一定的算法,根据slab内部对象分配出去的多寡,把当前slab分别移到slabs_full、slabs_partial还是slabs_empty.就像我们定义了一个指针数组(假设为pa),数组里面每个元素都是一个指针(假设为p),每个指针都指向一段分配出来的内存(假设为pm).pa就相当于cache_chain,p相当于kmem_cache,pm相当于slab.
page:
slab是一段连续的内存区域,这些内存区域就是由一个或多个page组成的.它只是标识此slab所占用的内存空间的大小,不是其成员对象;
object:
每一个slab内部存放的元素就是对象.也就是我们目标程序所需要到的目标数据结构.比如,struct niode.
因此,整个slab分配器的树状结构如下:
以内核中最常见的inode的操作为例.inode对应的slab高速缓存(kmem_cache)是inode_cachep,inode_cachep包含了多个slab,并根据slab内部对象分配的多寡进行归类(满、部分满、空闲三类).每一个slab里面又包含了多个我们要操作的目标数据结构.我们以后要用到struct inode的时候,直接从inode_cachep高速缓存里面摘取就可以了,这是一个很迅速的过程.
因此,我们要使用slab分配器,主要有下面几步:
申请高速缓存-->模块化高速缓存(存放我们自定义的目标对象数据并实现共性初始化)-->目标对象数据的操作-->目标对象的释放-->释放高速缓存
3.slab相关的API:
3-1.申请一个高速缓存:
函数原型:
struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, struct kmem_cache *, unsigned long), void (*dtor)(void*, struct kmem_cache *, unsigned long))
函数功能:
获取一块内存区域(高速缓存).
参数说明:
name:
高速缓存的名字.通过命令cat /proc/slabinfo可以看查当前系统的高速缓存信息.如下:
[root@seven ~]# cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> UDPLITEv6 0 0 704 11 2 : tunables 0 0 0 : slabdata 0 0 0 UDPv6 11 11 704 11 2 : tunables 0 0 0 : slabdata 1 1 0 TCPv6 12 12 1344 12 4 : tunables 0 0 0 : slabdata 1 1 0 isofs_inode_cache 0 0 376 21 2 : tunables 0 0 0 : slabdata 0 0 0 ext4_inode_cache 2075 2408 576 14 2 : tunables 0 0 0 : slabdata 172 17
可以看到ext4文件系统的节点i的高速缓存.
size:
高速缓存里面最基本元素(对象)所占内存空间的大小.
[注:]不是高速缓存的大小,而是里面目标对象的大小.这个目标对象就是我们以后用来操作的目标数据结构.
align:
页面第一个对象的偏移量,它可以用来确保已分配的对象进行某种特殊的对齐,最常用的就是0;
flags:
位掩码.控制高速缓存分配的行为.比如SLAB_HWCACHE_ALIGN标志要求所有对象跟高速缓存行对齐;
ctor:
高速缓存里面对象的"构造函数".
dtor:
高速缓存里面对象的"析构函数".
返回值:
成功则返回一个高速缓存区间,失败返回NULL.
3-2.获取一个目标数据结构:
通过函数kmem_cache_create()创建了一块高速缓存区,并把这个区规划成了大小相同的slab区域.每个slab区域存放的是具体对象数据的必须的基本骨架.相当于一池的水,已经被分割成一碗碗的量,我们需要的时候直接去端水就可以了,而不是用的时候再去手动勺."勺水"这个动作已经被kmem_cache_craete()处理了,我们后续的工作只是直接去端水--这个动作需要借助下面的内核函数kmem_cache_alloc():
函数原型:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
函数功能:
从既知的高速缓存区域里面获取一个目标数据结构对象的骨架.
参数说明:
cachep:
既知的高速缓存.
flags:
获取目标数据结构对象的方式,和kmalloc()函数的flags一样的意义.
3-3.释放一个目标数据结构:
和上述3-2动作相反,使用完一个目标数据结构需要把此目标数据结构返回给高速缓存,通过函数kmem_cache_free()实现.
函数原型:
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
函数功能:
把使用完的目标数据结构归还给其所属的高速缓存.
参数说明:
cachep:
目标数据结构对象所属的高速缓存.
objp:
指向目标数据结构对象的类型指针.
3-4.
如果整个高速存在对于我们来说已经没有存在意义的话,那么,可以通过函数kmem_cache_destroy()来实现.
函数原型:
void kmem_cache_destroy(struct kmem_cache *cachep)
函数功能:
释放一个高速缓存.
参数说明:
cachep:
通过函数kmem_cache_create()生成的相应的高速缓存.
4.slab的应用场景:
对于slab适配器多应用于文件系统、网络协议、scsi数据包和DMA.尤其是文件系统,因为在LINUX平台,在用户空间看来,"一切皆文件".对于文件节点的生成、操作、销毁是很频繁的,几乎每个文件系统都涉及了slab内存池,如ubi、ext4、yaffs等.
5.实例:
下面通过文件系统nfs为例看一下是如何使用slab内存池的.
回到上述2,我们使用slab内存池大致包括下面几大步:
申请高速缓存-->模块化高速缓存(存放我们自定义的目标对象数据并实现共性初始化)-->目标对象数据的操作-->目标对象的释放-->释放高速缓存
结合nfs实例fs/nfs/inode.c:
创建一个高速缓存:
static int __init nfs_init_inodecache(void) { nfs_inode_cachep = kmem_cache_create("nfs_inode_cache", sizeof(struct nfs_inode), 0, (SLAB_RECLAIM_ACCOUNT| SLAB_MEM_SPREAD), init_once, NULL); if (nfs_inode_cachep == NULL) return -ENOMEM; return 0; }
我们创建了一个名为nfs_inode_cache的高速缓存.它在文件系统加载的时候被调用:
/* * Initialize NFS */ static int __init init_nfs_fs(void) { int err; err = nfs_fs_proc_init(); if (err) goto out5; err = nfs_init_nfspagecache(); if (err) goto out4; err = nfs_init_inodecache(); if (err) goto out3; err = nfs_init_readpagecache(); if (err) goto out2; err = nfs_init_writepagecache(); if (err) goto out1; err = nfs_init_directcache(); if (err) goto out0; #ifdef CONFIG_PROC_FS rpc_proc_register(&nfs_rpcstat); #endif if ((err = register_nfs_fs()) != 0) goto out; return 0; out: #ifdef CONFIG_PROC_FS rpc_proc_unregister("nfs"); #endif nfs_destroy_directcache(); out0: nfs_destroy_writepagecache(); out1: nfs_destroy_readpagecache(); out2: nfs_destroy_inodecache(); out3: nfs_destroy_nfspagecache(); out4: nfs_fs_proc_exit(); out5: return err; }
获取一个目标数据结构:
struct inode *nfs_alloc_inode(struct super_block *sb) { struct nfs_inode *nfsi; nfsi = (struct nfs_inode *)kmem_cache_alloc(nfs_inode_cachep, GFP_KERNEL); if (!nfsi) return NULL; nfsi->flags = 0UL; nfsi->cache_validity = 0UL; #ifdef CONFIG_NFS_V3_ACL nfsi->acl_access = ERR_PTR(-EAGAIN); nfsi->acl_default = ERR_PTR(-EAGAIN); #endif #ifdef CONFIG_NFS_V4 nfsi->nfs4_acl = NULL; #endif /* CONFIG_NFS_V4 */ return &nfsi->vfs_inode; }
当我们需要操作一个nfs文件系统的nfs_inode时,直接从上述分配的nfs_inode_cachep摘取,然后再根据一些个性的需要实现初始化.如上述的nfsi.
[注:]所有的文件系统对上统一为vfs,从nfs的返回值也可见一斑!
释放一个目标数据结构:
void nfs_destroy_inode(struct inode *inode) { kmem_cache_free(nfs_inode_cachep, NFS_I(inode)); }
释放一个高速缓存:
static void nfs_destroy_inodecache(void) { kmem_cache_destroy(nfs_inode_cachep); }
它在文件系统卸载的时候被调用:
static void __exit exit_nfs_fs(void) { nfs_destroy_directcache(); nfs_destroy_writepagecache(); nfs_destroy_readpagecache(); nfs_destroy_inodecache(); nfs_destroy_nfspagecache(); #ifdef CONFIG_PROC_FS rpc_proc_unregister("nfs"); #endif unregister_nfs_fs(); nfs_fs_proc_exit(); }