Linux内核源码剖析之kmem_cache_create

写在前面: 

版本信息:

Linux内核2.6.24(大部分centos、ubuntu应该都在3.1+。但是2.6的版本适合学习,后续版本本质变化也不是很大)

一个操作系统对于内存的管理是非常的重要,关乎到整个系统的运行效率和内存最大使用率。内存管理从宏观来看,无非就是使内存达到最大使用率,减少内存的内/外碎片。而在Linux内核中使用伙伴算法(Buddy)管理一组页,使用Slab算法管理单独页,伙伴算法+Slab算法合理的避免内存的内/外碎片。那么今天介绍的kmem_cache_create方法就是Slab算法。

kmem_cache_create作用:

上文提及到内核使用Slab算法来切割页(内核默认页大小为4096字节),所以需要把4096字节切割成多少块,每块多大呢?这是一个内核需要考虑的问题,如果使用不恰当,会大大提升内存内碎片问题和内存使用率降低。接下来,我们看一个列子~

Linux内核源码剖析之kmem_cache_create_第1张图片

默认的前置条件:

页大小:4096Byte

Slab切割:每块64Byte

Slab切割块数:4096/64 = 2^12 / 2^6 = 2^6 = 64块

切割后每块64字节,一页总共又64块。此时,内核需要使用内存来开辟一个结构体,结构体大小为32字节,如下图:

Linux内核源码剖析之kmem_cache_create_第2张图片 

当在一块64字节的内存中开辟32字节的内存,此时会浪费32字节(这称为内存内碎片),此时我们考虑一下,如果这个32字节的结构体一直频繁的开辟,那么就会出现32字节 * N的内存浪费。如果说此32字节的结构体使用频率特别低,甚至只会使用几次,那么一点点的内存浪费是可以忽略的,但是使用频繁的话一定要做定制化或者特殊化处理,减少内存浪费。kmem_cache_create方法就是用来做特殊化处理的。

Linux内核源码剖析之kmem_cache_create_第3张图片 

假设说,Slab算法默认一块大小为64字节,而某个频繁使用的结构体为32字节,那我们把Slab算法默认块大小给修改成32字节不就完美了么,就不存在内存浪费问题了。接下来介绍kmem_cache_create方法。

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案例

在本文的最后给出内核中经常使用的结构体对于kmem_cache_create方法使用。

task_struct

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结构体 

socket

在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结构体来做空间开辟。

inode

在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结构体。

你可能感兴趣的:(深入理解Linux2.6内核,源码解读,linux,Linux内核)