Linux内存管理源码剖析(三)

一、小块内存分配

前面剖析了Linux分配大块内存的机制,也清楚了物理内存的组织机制,本次先来讲述小块内存的分配机制。

从fork创建一个进程开始讨论

经过在系统调用表sys_call_table中的查找之后会调用系统调用sys_fork,sys_fork又调用_do_fork函数,主要做两件事,第一复制父进程的task_struct结构,第二唤醒这个新创建的进程:

SYSCALL_DEFINE0(fork){//linux-4.13.16\kernel\fork.c
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
long _do_fork(unsigned long clone_flags,    unsigned long stack_start,
	      unsigned long stack_size,     int  *parent_tidptr,
	      int  *child_tidptr,   unsigned long tls){
	struct task_struct *p;
	。。。
    /* copy_process复制进程结构*/
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();
    。。。
		wake_up_new_task(p);/*wake_up_new_task唤醒进程*/
}

今天的探讨重点是copy_process函数如何复制一个不算占用太多内存的task_struct结构:

dup_task_struct(current, node);
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
	struct task_struct *tsk;
	unsigned long *stack;
	struct vm_struct *stack_vm_area;
	int err;

	if (node == NUMA_NO_NODE)
		node = tsk_fork_get_node(orig);
	tsk = alloc_task_struct_node(node);
	if (!tsk)
		return NULL;

copy_process函数会在底层调用dup_task_struct复制task_struct对象,dup_task_struct函数调用alloc_task_struct_node分配为task_struct结构分配内存。继续向下走发现它调用kmem_cache_alloc_node函数:

static struct kmem_cache *task_struct_cachep;
	task_struct_cachep = kmem_cache_create("task_struct",
			arch_task_struct_size, align,
			SLAB_PANIC|SLAB_NOTRACK|SLAB_ACCOUNT, NULL);
	//kmem_cache是一个静态全局变量,在系统初始化时被创建
static inline struct task_struct *alloc_task_struct_node(int node){
	return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}
static inline void free_task_struct(struct task_struct *tsk)
{
	kmem_cache_free(task_struct_cachep, tsk);
}

注释已经解释的够清楚了,在系统初始化时会为我们创建一个叫做task_struct_cachep的缓冲区,这个缓冲区里面都是一个个的大小刚好等于task_struct结构体大小的内存块,即每一个task_struct结构块的大小为arch_task_struct_size。
故有了这样一个机制之后我们每次为新的进程分配内存时就调用kmem_cache_alloc_node在这个缓冲区里面去找,当进程结束调用free_task_struct释放task_struct结构体时也是调用kmem_cache_free函数直接放回这个缓冲区即可,并不真正的去内存申请。

struct kmem_cache

通过上面的简单分析之后可以发现对于task_struct结构体我们有task_struct_cachep这个缓存块为它作为一个缓存池,那么不难想象对其他结构说不上应该也有类似的机制,以便提高系统的效率,故基于这点来瞅一瞅缓存相关的数据结构struct kmem_cache,这个缓存结构是我们使用slub技术分配时的缓存结构:

//linux-4.13.16\include\linux\slub_def.h
struct kmem_cache {
	struct kmem_cache_cpu  *cpu_slab;//一个NUMA节点上只有一个,快速分配通道
	unsigned long flags;
	unsigned long min_partial;
	int size;		/* The size of an object including meta data */
	int object_size;	/* The size of an object without meta data */
	int offset;		/* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
	int cpu_partial;	/* Number of per cpu partial objects to keep around */
#endif
	struct kmem_cache_order_objects oo;

	/* Allocation and freeing of slabs */
	struct kmem_cache_order_objects max; //即空闲的大小为2的order次方的大块
	struct kmem_cache_order_objects min;
	gfp_t allocflags;	/* gfp flags to use on each alloc */
	int refcount;		/* Refcount for slab cache destroy */
	void (*ctor)(void *);

	const char *name;	/*缓存块的名字,如task_struct */
	struct list_head list;	/*系统各缓存块链表 */
	struct kmem_cache_node *node[MAX_NUMNODES];//一个NUMA节点上只有一个,慢速分配通道
};

根据这个结构所在的头文件我们也可以知道它是为小块内存分配而生的结构了。
需要注意的是虽然是小块内存分配,其实就是将这个缓存维护的大块内存分为一个个的小块交给相关的数据结构去使用。当内核申请分配小块缓存时会先从kmem_cache_cpu快速分配若分配失败则从kmem_cache_node中分配,最后实在不行就去伙伴系统分配大块至当前缓冲区然后重新分配。
快慢通道
如上图所示所示, kmem_cache_node说他是慢速分配通道是因为它管理的是部分空闲大内存块,只有当前面已经分配好了的快速通道管理页面中已经无表项的时候才会到这部分缓存里面来分配。
在明白了这点之后来看一看前面用于分配task_struct结构体的相关操作,继续向下看kmem_cache_alloc_node函数,其会调用slab_alloc_node:

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
		gfp_t gfpflags, int node, unsigned long addr)
{
	void *object;
	struct kmem_cache_cpu *c;
	struct page *page;
	unsigned long tid;

	s = slab_pre_alloc_hook(s, gfpflags);
	if (!s)
		return NULL;
redo:
    ...
		c = raw_cpu_ptr(s->cpu_slab);  //获得快通道入口
    ...
	object = c->freelist; //快通道中的第一个空闲块
	page = c->page;//空闲页
	if (!object || !node_match(page, node)) {
		object = __slab_alloc(s, gfpflags, node, addr, c);
		stat(s, ALLOC_SLOWPATH);
	} 
	...
	return object;
}

上面先看当前节点的快通道里面有没有符合条件的缓存块,若有则直接返回否则调用 ___slab_alloc函数查看备用部分还有空闲块吗?

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
			  unsigned long addr, struct kmem_cache_cpu *c)
{
	void *freelist;
	struct page *page;
	page = c->page;
	if (!page)
		goto new_slab;
redo:
    ...
	freelist = c->freelist;
	if (freelist)//再次检查一遍看是否有空闲页,有可能这段时间有其他进程释放了
		goto load_freelist;

	freelist = get_freelist(s, page);

	if (!freelist) //若仍没有空闲的就跳到new_slab中
	    。。。
		goto new_slab;

load_freelist://有空闲的了就返回空闲的
    。。。
	return freelist;
new_slab:

	if (slub_percpu_partial(c)) {   //他从自己的partial(备用)中查看是否有空闲的
		page = c->page = slub_percpu_partial(c);
		slub_set_percpu_partial(c, page);//若有的话把它挂在快速通道指向的freelist指针上
		stat(s, CPU_PARTIAL_ALLOC);
		goto redo;//再试一次
	}
    //若仍然没有就要调用下面的函数到普通通道中去借它的partial内存了
	freelist = new_slab_objects(s, gfpflags, node, &c);
    。。。
	return freelist;
}

new_slab_objects函数先在缓存块task_struct_cachep 的普通通道kmem_cache_node中看是否还有空闲块可以给快速通道分配,若有则刚好,若没有的话就将调用相关函数去分配缓存块中struct kmem_cache_order_objects max与 struct kmem_cache_order_objects min两个成员上挂着的当前节点的空闲大块了。具体函数不在分析,没意义。

二、页面换出

进程的设计思想决定了内存使用的方式,小内存不可能同时满足所有进程使用,故需要一种特定的算法来讲lru的物理页面换出,在Linux中有一个内核线程Kswapd就是负责做这件事情的。对于内存页面分为匿名页与内存映射。匿名页和虚拟地址管理,而内存映射虚拟地址关联也和文件管理关联。他们每一个都分为active与inactive两个列表,当Kswapd选择页面换出时会先缩减活跃页面,然后在压缩不活跃列表,不同的是不活跃列表中被压缩的页面会被回收。对于被回收的匿名页,需要分配swap,将内存页写入文件系统;对于内存映射的页面会将对文件的修改写回到文件中。

三、总结

  • 物理内存分为多个NUMA节点,分别进行管理。
  • 每个NUMA节点分为多个用途不同的区域,如内核态直接映射区等。
  • 每个区域分为好多个页面。
  • 对于分配连续的多个物理页面组成的大块使用伙伴系统进行分配。
  • slab allocator 负责将从伙伴系统申请的大块内存切分成一个个的小块进行分配,如用作缓冲区对象等。
  • 内核线程Kswapd负责内存物理页面的换入换出工作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章