版本信息:
Linux内核2.6.24(大部分centos、ubuntu应该都在3.1+。但是2.6的版本适合学习,后续版本本质变化也不是很大)
一个操作系统对于内存的管理是非常的重要,关乎到整个系统的运行效率和内存最大使用率。内存管理从宏观来看,无非就是使内存达到最大使用率,减少内存的内/外碎片。而在Linux内核中使用伙伴算法(Buddy)管理一组页,使用Slab算法管理单独页,伙伴算法+Slab算法合理的避免内存的内/外碎片。那么今天介绍的kmem_cache_create方法就是Slab算法。
上文提及到内核使用Slab算法来切割页(内核默认页大小为4096字节),所以需要把4096字节切割成多少块,每块多大呢?这是一个内核需要考虑的问题,如果使用不恰当,会大大提升内存内碎片问题和内存使用率降低。接下来,我们看一个列子~
默认的前置条件:
页大小:4096Byte
Slab切割:每块64Byte
Slab切割块数:4096/64 = 2^12 / 2^6 = 2^6 = 64块
切割后每块64字节,一页总共又64块。此时,内核需要使用内存来开辟一个结构体,结构体大小为32字节,如下图:
当在一块64字节的内存中开辟32字节的内存,此时会浪费32字节(这称为内存内碎片),此时我们考虑一下,如果这个32字节的结构体一直频繁的开辟,那么就会出现32字节 * N的内存浪费。如果说此32字节的结构体使用频率特别低,甚至只会使用几次,那么一点点的内存浪费是可以忽略的,但是使用频繁的话一定要做定制化或者特殊化处理,减少内存浪费。kmem_cache_create方法就是用来做特殊化处理的。
假设说,Slab算法默认一块大小为64字节,而某个频繁使用的结构体为32字节,那我们把Slab算法默认块大小给修改成32字节不就完美了么,就不存在内存浪费问题了。接下来介绍kmem_cache_create方法。
include/linux/slab.h 文件中,定义了kmem_cache_create方法的原型。
// const char * 此slab缓存的名字
// size_t 大小
// size_t 对齐大小
// unsigned long 标志位
// void (*)(struct kmem_cache *, void *) 回调初始化函数
struct kmem_cache *kmem_cache_create(const char *, size_t, size_t,
unsigned long,
void (*)(struct kmem_cache *, void *));
mm/slab.c 文件中,实现了kmem_cache_create方法。
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags,
void (*ctor)(struct kmem_cache *, void *))
{
………… // 省略Debug信息
………… // 省略标志位判断信息,如果需要详细的标志位判断请自行看源码。
cachep = kmem_cache_zalloc(&cache_cache, GFP_KERNEL);
// 设置参数
cachep->colour_off = cache_line_size();
if (cachep->colour_off < align)
cachep->colour_off = align;
cachep->colour = left_over / cachep->colour_off;
cachep->slab_size = slab_size;
cachep->flags = flags;
cachep->gfpflags = 0;
if (CONFIG_ZONE_DMA_FLAG && (flags & SLAB_CACHE_DMA))
cachep->gfpflags |= GFP_DMA;
cachep->buffer_size = size;
cachep->reciprocal_buffer_size = reciprocal_value(size);
cachep->ctor = ctor;
cachep->name = name;
// 添加到链表中。
list_add(&cachep->next, &cache_chain);
return cachep;
}
EXPORT_SYMBOL(kmem_cache_create);
此方法篇幅特别长,不过都是大部分都是Debug信息和标志位的判断,为了篇幅的简单,这里做一部分的忽略,当然标志位肯定会影响到参数,如果需要详细的标志位判断请自行看源码。
在kmem_cache_create方法中,会创建kmem_cache结构体,此结构体承载着Slab算法的数据,当然最终kmem_cache_create方法传入的参数会添加到kmem_cache结构体中,并且kmem_cache结构体在内核中使用双向链表链起来,方便后续开辟空间时使用。
在本文的最后给出内核中经常使用的结构体对于kmem_cache_create方法使用。
task_struct结构体对于内核来说太常见了,进程/线程的承载体,一个正常运行的服务器中进程/线程的数量都是几千上万,频繁的开辟和销毁进程/线程,所以task_struct结构体肯定是非常平凡使用,所以对于Slab而言,肯定要适配与task_struct结构体,所以肯定会使用kmem_cache_create方法。
kernel/fork.c 文件中fork_init方法。
void __init fork_init(unsigned long mempages)
{
task_struct_cachep =
kmem_cache_create("task_struct", sizeof(struct task_struct),
ARCH_MIN_TASKALIGN, SLAB_PANIC, NULL);
………… // 省略其他与Slab无关代码
}
再看一下开辟空间时,如何使用。
// task_struct_cachep结构体是初始化调用kmem_cache_create方法返回的kmem_cache结构体。
# define alloc_task_struct() kmem_cache_alloc(task_struct_cachep, GFP_KERNEL)
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;
tsk = alloc_task_struct();
if (!tsk)
return NULL;
………… //省略与开辟空间无关代码
return tsk;
}
在开辟task_struct结构体时,使用task_struct_cachep结构体,task_struct_cachep结构体是初始化过程中调用kmem_cache_create方法返回的kmem_cache结构体
在Linux内核中,网络通信都离不开最上层的socket暴露层,所以socket结构体肯定会非常频繁的使用。
net/socket.c文件中init_inodecache方法。
static int init_inodecache(void)
{
// Linux一切皆文件,socket也需要满足VFS的规范。
sock_inode_cachep = kmem_cache_create("sock_inode_cache",
sizeof(struct socket_alloc),
0,
(SLAB_HWCACHE_ALIGN |
SLAB_RECLAIM_ACCOUNT |
SLAB_MEM_SPREAD),
init_once);
if (sock_inode_cachep == NULL)
return -ENOMEM;
return 0;
}
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};
这里做一点说明,Linux一切皆文件,socket也需要满足VFS虚拟文件的规范。所以这里传入的结构体是socket_alloc,而不是socket。socket_alloc结构体包含了socket结构体和inode结构体,是为了满足VFS虚拟文件规范~!
static struct inode *sock_alloc_inode(struct super_block *sb)
{
struct socket_alloc *ei;
// 使用slab算法开辟空间
// sock_inode_cachep是初始化调用kmem_cache_create方法返回的kmem_cache结构体。
ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
if (!ei)
return NULL;
init_waitqueue_head(&ei->socket.wait);
// 初始化工作
ei->socket.fasync_list = NULL;
ei->socket.state = SS_UNCONNECTED;
ei->socket.flags = 0;
ei->socket.ops = NULL;
ei->socket.sk = NULL;
ei->socket.file = NULL;
return &ei->vfs_inode;
}
当开辟socket结构体时(这里是socket_alloc,因为满足VFS,做了包装,内部还是socket),会使用到 kmem_cache_create方法返回的kmem_cache结构体来做空间开辟。
在Linux中一切皆文件,所以众多模块需要接入VFS虚拟文件的规范,所以inode结构体是非常非常使用频繁,所以一定要做适配。
fs/inode.c中inode_init方法。
void __init inode_init(void)
{
// slab对于inode结构体的优化。
inode_cachep = kmem_cache_create("inode_cache",
sizeof(struct inode),
0,
(SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
SLAB_MEM_SPREAD),
init_once);
………… // 省略其他与slab无关的代码。
}
再看一下开辟空间时,如何使用。
static struct inode *alloc_inode(struct super_block *sb)
{
static const struct address_space_operations empty_aops;
static struct inode_operations empty_iops;
static const struct file_operations empty_fops;
struct inode *inode;
// 如果当前实现VFS规范的super_block传入了alloc_inode 的函数就使用传入的(上文socket就实现了)
if (sb->s_op->alloc_inode)
inode = sb->s_op->alloc_inode(sb);
else
// 使用默认的inode的slab开辟
inode = (struct inode *) kmem_cache_alloc(inode_cachep, GFP_KERNEL);
// 省略与开辟inode无关代码。
return inode;
}
与上面的例子一摸一样。
针对性的创建slab使用 kmem_cache_create方法,该方法返回kmem_cache结构体, 在开辟内存时,使用kmem_cache_alloc,并传入kmem_cache结构体。