Linux内存管理(三)物理内存管理(下)

Linux内存管理

Linux内存管理(一)Linux进程空间管理

Linux内存管理(二)物理内存管理(上)

Linux内存管理(三)物理内存管理(下)

Linux内存管理(四)用户态内存映射

Linux内存管理(五)内核态内存映射

Linux内存管理(三)物理内存管理(下)


前面讲的伙伴系统其管理的基本单位是页,一页有4K字节。但是如果我们需要更小的内存块,那物理内存应该如何分配呢?

一、缓存区数据结构

上一篇文章讲过,对于小块内存,会使用slub分配器,这篇文章就来解析它

在创建进程的时候,会调用 dup_task_struct,它会复制一个 task_struct 对象,需要先调用 alloc_task_struct_node 分配一个 task_struct 大小的内存

从下面这段代码中,可以看到 alloc_task_struct_node 会调用 kmem_cache_alloc_node,从 task_struct_cachep 中分配内存

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);

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);
}

kmem_cache 表示一个缓存区,其是内核一种内存管理的方法,其基本思想是分配几个连续的页,根据缓存对象的大小切分成小块内存

在系统初始化的时候,task_struct_cachep 会被 kmem_cache_create 创建,kmem_cache_create 中指定的是这个缓存区的名字叫 task_struct,每个内存块的大小为 arch_task_struct_size,即为 task_struct 的大小

有了这个缓存区,每次创建 task_struct 的时候,不必到内存里去分配,只需要调用 kmem_cache_alloc_node,从 task_struct_cachep 这个缓存区中看有没有直接可以使用的内存块

当进程结束的时候,task_struct 的内存也不必被释放,而是通过 kmem_cache_free,放到 task_struct_cachep 这个缓存区中,供下次使用

首先来看一下

struct kmem_cache {
	struct kmem_cache_cpu __percpu *cpu_slab;
	/* Used for retriving partial slabs etc */
	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;
	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;	/* Name (only for display!) */
	struct list_head list;	/* List of slab caches */
......
	struct kmem_cache_node *node[MAX_NUMNODES];
};


  • list:内核中不是只有一个 kmem_cache 对象,task_struct、mm_struct、fs_struct 等等都有它们对应的 kmem_cache 对象,这些对象都串在一个链表上,也就是 LIST_HEAD(slab_caches),list 就是链表节点

上面说过,对于缓存来说,起始就是分配几个连续的页,根据缓存对象的大小切分成小块内存

这里有三个 kmem_cache_order_objects 类型的变量。这里面的 order 表示2的 order 次方个页的内存块,objects 表示能够存放的缓存对象数量

最终内存会被变成下面这样子

img

对于每一项的结构,都是缓存对象后面跟着一个指向下一个空闲对象的指针,这样讲大内存块中所有的空闲对象连接成一个链表

  • size:表示每一项包含指针的大小
  • object_size:表示缓存对象的大小
  • offset:下一空闲对象指针在这一项里的偏移值

那么这些缓存对象哪些被分配了?哪些是空闲的?什么时候整个大内块都分配完了,需要向伙伴系统申请几页形成新的内存块呢?这些信息是谁在维护?

接下俩就介绍两个成员变量 kmem_cache_cpu 和 kmem_cache_node

img

在分配缓存块的时候,要分为两条路径,fast path 和 slow path,也就是快速通道普通通道

kmem_cache_cpu 是快速通道,kmem_cache_node 是普通通道。每次分配内存的时候,要先从 kmem_cache_cpu 中分配。如果 kmem_cache_cpu 中没有空闲的块,那就从 kmem_cache_node 里分配。如果还没有空闲的块,才去伙伴系统分配新的页

下面看一看 kmem_cache_cpu 是如何存放缓存块的

struct kmem_cache_cpu {
	void **freelist;	/* Pointer to next available object */
	unsigned long tid;	/* Globally unique transaction id */
	struct page *page;	/* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
	struct page *partial;	/* Partially allocated frozen slabs */
#endif
......
};

  • page:指向大内存块的第一个页,缓存块就是从里面分配的
  • freelist:指向大内存块里面第一个空闲的项
  • partial:也是指向大内存的第一个页,之所以叫 partial(部分),是因为它里面部分被分配了,部分空闲的。这是一个备选列表,当 page 满了,就会从这里查找

下面看看 kmem_cache_node 的定义

struct kmem_cache_node {
	spinlock_t list_lock;
......
#ifdef CONFIG_SLUB
	unsigned long nr_partial;
	struct list_head partial;
......
#endif
};

这里面也有一个 partial,是一个链表。这个存放的是部分空闲的大内存块。这是 kmem_cache_cpu 中 partial 的备选列表,如果那里没有,就来这里查找

二、分配过程

kmem_cache_alloc_node 会调用 slab_alloc_node

/*
 * Inlined fastpath so that allocation functions (kmalloc, kmem_cache_alloc)
 * have the fastpath folded into their functions. So no function call
 * overhead for requests that can be satisfied on the fastpath.
 *
 * The fastpath works by first checking if the lockless freelist can be used.
 * If not then __slab_alloc is called for slow processing.
 *
 * Otherwise we can simply pick the next object from the lockless free list.
 */
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;
......
	tid = this_cpu_read(s->cpu_slab->tid);
	c = raw_cpu_ptr(s->cpu_slab);
......
	object = c->freelist;
	page = c->page;
	if (unlikely(!object || !node_match(page, node))) {
		object = __slab_alloc(s, gfpflags, node, addr, c);
		stat(s, ALLOC_SLOWPATH);
	} 
......
	return object;
}

快速通道非常简单,取出 cpu_slab 也即 kmem_cache_cpu 的 free_list,如果可以取出空闲的项,那么就直接返回。如果没有空闲了,则只好进入普通通道,调用 __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;
......
redo:
......
	/* must check again c->freelist in case of cpu migration or IRQ */
	freelist = c->freelist;
	if (freelist)
		goto load_freelist;


	freelist = get_freelist(s, page);


	if (!freelist) {
		c->page = NULL;
		stat(s, DEACTIVATE_BYPASS);
		goto new_slab;
	}


load_freelist:
	c->freelist = get_freepointer(s, freelist);
	c->tid = next_tid(c->tid);
	return freelist;


new_slab:


	if (slub_percpu_partial(c)) {
		page = c->page = slub_percpu_partial(c);
		slub_set_percpu_partial(c, page);
		stat(s, CPU_PARTIAL_ALLOC);
		goto redo;
	}


	freelist = new_slab_objects(s, gfpflags, node, &c);
......
	return freeli


这里会继续尝试 kmem_cache_cpu 的 free_list,为什么呢?因为在分配内存的时候,进程可能被调度睡眠,在进程切换回来的时候,缓存区可能已经有空闲的内存块了,所以再检查一下。如果找到了,就跳到 load_freelist,指定下一个空闲的内存块,然后就可以返回了

如果 freelist 还没有,那么就跳到 new_slab,这里先去 kmem_cache_cpu 的 partial 里面去看,如果 partial 不为空,那么就讲 kmem_cache_cpu 的page,也即快速通道指向的那一大块内存,替换为 partial 指向的一大块内存,然后跳到 redo 重新尝试一下,这下应该是可以成功的

如果还真不行,那么就到了 new_slab_objects

static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
			int node, struct kmem_cache_cpu **pc)
{
	void *freelist;
	struct kmem_cache_cpu *c = *pc;
	struct page *page;


	freelist = get_partial(s, flags, node, c);


	if (freelist)
		return freelist;


	page = new_slab(s, flags, node);
	if (page) {
		c = raw_cpu_ptr(s->cpu_slab);
		if (c->page)
			flush_slab(s, c);


		freelist = page->freelist;
		page->freelist = NULL;


		stat(s, ALLOC_SLAB);
		c->page = page;
		*pc = c;
	} else
		freelist = NULL;


	return freelis

get_partial 会根据 node id,找到相应的 kmem_cache_node,然后调用 get_partial_node,开始在这个节点分配

/*
 * Try to allocate a partial slab from a specific node.
 */
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
				struct kmem_cache_cpu *c, gfp_t flags)
{
	struct page *page, *page2;
	void *object = NULL;
	int available = 0;
	int objects;
......
	list_for_each_entry_safe(page, page2, &n->partial, lru) {
		void *t;


		t = acquire_slab(s, n, page, object == NULL, &objects);
		if (!t)
			break;


		available += objects;
		if (!object) {
			c->page = page;
			stat(s, ALLOC_FROM_PARTIAL);
			object = t;
		} else {
			put_cpu_partial(s, page, 0);
			stat(s, CPU_PARTIAL_NODE);
		}
		if (!kmem_cache_has_cpu_partial(s)
			|| available > slub_cpu_partial(s) / 2)
			break;
	}
......
	return object;

acquire_slab 会从 kmem_cache_node 的 partial 链表中拿下一大块内存来,并且将 freelist(也就是第一块空闲的缓存块)赋值给t。并且当第一轮循环的时候,会将 kmem_cache_cpu 的page指向取下来的这一大块内存,返回的 object 就是这块内存里面的第一块缓存块 t

如果 kmem_cache_node 里面没有空闲的内存,这就说明原来的页里面都放满了,就要回到 new_slab_objects 函数,里面的 new_slab 函数会调用 allocate_slab

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
	struct page *page;
	struct kmem_cache_order_objects oo = s->oo;
	gfp_t alloc_gfp;
	void *start, *p;
	int idx, order;
	bool shuffle;


	flags &= gfp_allowed_mask;
......
	page = alloc_slab_page(s, alloc_gfp, node, oo);
	if (unlikely(!page)) {
		oo = s->min;
		alloc_gfp = flags;
		/*
		 * Allocation may have failed due to fragmentation.
		 * Try a lower order alloc if possible
		 */
		page = alloc_slab_page(s, alloc_gfp, node, oo);
		if (unlikely(!page))
			goto out;
		stat(s, ORDER_FALLBACK);
	}
......
	return page;
}

alloc_slab_page 分配页面。分配的时候,要按照 kmem_cache_order_objects 成员变量里的 order 来分配。如果分配不成功,那么说明内存紧张,换成 min 版本的 kmem_cache_order_objects

三、页面的换出

另一个物理内存管理要处理的事情就是页面的换出。每个进程都有自己的虚拟地址空间,虚拟地址空间都非常大,而不可能有这么多的物理内存。所以对于一些长时间不使用的页面,将其换出到磁盘,等到要使用的时候,将其换入到内存中,以此提高物理内存的使用率

什么情况会触发页面换出呢?

最常见的情况就是,分配物理内存的时候,发现没有地方了,那么就试图地回收一些

例如,申请一个页面的时候,会调用 get_page_from_freelist,调用链为 get_page_from_freelist -> node_reclaim -> __node_reclaim -> shrink_node,通过这个调用链可以看出,页面的换出是以节点为单位的

另一种情况是,就是内存管理系统主动去做的,就是内核线程 kswapd。这个线程是一个无限循环,如果内存不紧张,那么就睡眠,如果内存紧张,就唤醒,然后试图去换出一些页面

/*
 * The background pageout daemon, started as a kernel thread
 * from the init process.
 *
 * This basically trickles out pages so that we have _some_
 * free memory available even if there is no other activity
 * that frees anything up. This is needed for things like routing
 * etc, where we otherwise might have all activity going on in
 * asynchronous contexts that cannot page things out.
 *
 * If there are applications that are active memory-allocators
 * (most normal use), this basically shouldn't matter.
 */
static int kswapd(void *p)
{
	unsigned int alloc_order, reclaim_order;
	unsigned int classzone_idx = MAX_NR_ZONES - 1;
	pg_data_t *pgdat = (pg_data_t*)p;
	struct task_struct *tsk = current;


    for ( ; ; ) {
......
        kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
					classzone_idx);
......
        reclaim_order = balance_pgdat(pgdat, alloc_order, classzone_idx);
......
    }
}


这里的调用链是 balance_pgdat -> kswapd_shrink_node -> shrink_node,最后也是调用的 shrink_node,也是以节点为单位的,也就是每个节点都有一个 kswap 线程

shrink_node 调用的是 shrink_node_memcg。这里面有一个循环处理页面的列表

/*
 * This is a basic per-node page freer.  Used by both kswapd and direct reclaim.
 */
static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memcg,
			      struct scan_control *sc, unsigned long *lru_pages)
{
......
	unsigned long nr[NR_LRU_LISTS];
	enum lru_list lru;
......
	while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
					nr[LRU_INACTIVE_FILE]) {
		unsigned long nr_anon, nr_file, percentage;
		unsigned long nr_scanned;


		for_each_evictable_lru(lru) {
			if (nr[lru]) {
				nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
				nr[lru] -= nr_to_scan;


				nr_reclaimed += shrink_list(lru, nr_to_scan,
							    lruvec, memcg, sc);
			}
		}
......
	}
......

这里面有一个 lru 列表,可以想象的是所有的页面都挂在 LRU 列表上。LRU 是 Least Recent Use(最近最少使用)。这个列表里面会按照活跃度排序,这样就容易把不常使用的页表找出

内存页分为两类,一类是匿名页,和虚拟地址空间进行关联。一类是内存映射,不但和虚拟地址空间关联,还和文件关联

它们每一类都有两个列表,一类是 active,一类是 inactive。如果要换出内存,就从 inactive 列表中找出最不活跃的页表,换出到磁盘中

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
	LRU_UNEVICTABLE,
	NR_LRU_LISTS
};


#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)


static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
				 struct lruvec *lruvec, struct mem_cgroup *memcg,
				 struct scan_control *sc)
{
	if (is_active_lru(lru)) {
		if (inactive_list_is_low(lruvec, is_file_lru(lru),
					 memcg, sc, true))
			shrink_active_list(nr_to_scan, lruvec, sc, lru);
		return 0;
	}


	return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);

shrink_list 会先缩减活跃页面列表,再压缩不活跃的页面列表。对不活跃的页面列表进行缩减,shrink_inactive_list 就需要对页面进行回收;对于匿名页来讲,就需要分配 swap,将内存页写入文件系统中;对于内存映射关联了文件的,需要将修改的内容写回文件系统中

四、总结

结合上篇文章,对物理内存做一个总结

  • 物理内存分 NUMA 节点,分别进行管理
  • 每个 NUMA 节点分为多个区域
  • 每个区域有多个物理页面
  • 伙伴系统将多个连续的连续的页面作为一个大的内存块管理
  • kswapd 内核线程负责将页面换入和换出
  • slub allocator 负责将从伙伴系统申请到的大内存切分成小块内存,分配给其他系统

img

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102229048