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負責內存物理頁面的換入換出工作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章