【Linux】内核学习笔记(十一)——内存管理

本篇是《Linux内核学习笔记》系列的第十一篇,我们将开始学习内核中至为重要的子系统——内存管理(mm)。

用户空间里要获取内存,用一个malloc就完事了,不是吗?

然而这简洁接口的背后,是内核复杂的虚拟化和封装在做支撑。从根本上来说,内核本身不能奢侈地使用内存,因为它必须提供更好的服务。内核和用户空间是不同的,不存在简单便捷的分配方式。比如,内核一般不能睡眠;它就是至高无上的管理者,没有别的软件能为它承担责任。此外,内核还要应对任何可能的内存分配错误,决不是像malloc一样返回NULL指针那么轻松。

接下来我们会学习Linux上的管理机制。假定读者已具有虚拟内存等前置知识的基础。

1. 基本概念

1.1. 页page

物理页是内核进行内存管理的基本单位,这是它的定义。page结构体包括很多复杂的联合体,我们做一点折叠和删减,看看里面都有什么。

struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */

	/*
	 * Five words (20/40 bytes) are available in this union.
	 * WARNING: bit 0 of the first word is used for PageTail(). That
	 * means the other users of this union MUST NOT use the bit to
	 * avoid collision and false-positive PageTail().
	 */
	union {
		struct {	/* Page cache and anonymous pages */
		};
		struct {	/* page_pool used by netstack */
		};
		struct {	/* slab, slob and slub */
		};
		struct {	/* Tail pages of compound page */
		};
		struct {	/* Second tail page of compound page */
		};
		struct {	/* Page table pages */
		};
		struct {	/* ZONE_DEVICE pages */
		};

		/** @rcu_head: You can use this to free a page by RCU. */
		struct rcu_head rcu_head;
	};

	union {		/* This union is 4 bytes in size. */
		/*
		 * If the page can be mapped to userspace, encodes the number
		 * of times this page is referenced by a page table.
		 */
		atomic_t _mapcount;

		/*
		 * If the page is neither PageSlab nor mappable to userspace,
		 * the value stored here may help determine what this page
		 * is used for.  See page-flags.h for a list of page types
		 * which are currently stored here.
		 */
		unsigned int page_type;

		unsigned int active;		/* SLAB */
		int units;			/* SLOB */
	};

	/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
	atomic_t _refcount;

	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */


} _struct_page_alignment;

好了,接下来删去所有无关的联合体,突出我们将讨论的重点。

struct page {
	unsigned long flags;
	/* Page cache and anonymous pages */
	struct list_head lru;
	struct address_space *mapping;
	pgoff_t index;
	unsigned long private;
	atomic_t _refcount;
	void *virtual;
} _struct_page_alignment;

flags<linux/page-flags.h>定义,包括各种页状态。_refcount存放了页被引用的次数,应采用对应的封装函数进行检查,不可直接访问该域。

注意,page代表物理页。换言之,如果一个物理页框上的内容被换到磁盘里去了,那么同一个page结构体指示的内存会在不同时刻存放不同的页。它的作用仅在于描述物理内存本身,每一个页框都有一个page结构体与之对应。

1.2. 区zone

实际上,内存的页之间彼此也不一样,有的可能要做一些奇怪的任务,例如由于硬件缺陷,导致只能使用指定地址进行DMA。或者,有些体系结构的物理地址寻址空间比虚拟地址寻址空间还大很多,这样就有内存无法映射到内存空间。为了解决这个问题,Linux引入了“区”的概念。

enum zone_type {

	ZONE_DMA,

	ZONE_DMA32,

	ZONE_NORMAL,

	ZONE_HIGHMEM,

	ZONE_MOVABLE,

	ZONE_DEVICE,

	__MAX_NR_ZONES
};

这些都是Linux中区的可能类型。Linux把页划为区,形成不同的内存池,从而进行按用途分配。如果没必要,Linux会尽量为一般用途的内存分配ZONE_NORMAL类型的页,但内存实在不够的话也会从别的可用区拿内存过来。

应该注意的是,x64机器上其实没有ZONE_HIGHMEM这种东西,从直观上也不太能理解会有容量比64位寻址空间更大的内存器。

zone是一个硕大的结构体,定义于<linux/mmzone.h>,这里不再贴代码了。系统中有几个区,就有几个zone结构体,可以知道它的数量是相当有限。这里只介绍几个重要的字段:

  • lock是自旋锁,防止结构被并发访问,但不保护驻留在区中的所有页。
  • _watermark数组包含该区的最小值、最低和最高水位值,内核使用水位为每个区设置合适的内存消耗基准。
  • name是区的名字,定义于mm/page_alloc.c,就是DMA、Normal、HighMem这些。

2. 获得、释放页

2.1. 接口

你已经知道什么是页了,快来尝试获取一个页吧!

很容易想到获取和释放页是内存管理中最底层的部分。看看定义:

static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)

它分配2^order个连续的物理页,并返回指向第一个页的page结构体;出错则返回NULL。如果只要一个页,使用封装函数alloc_page就好,非常神奇。gfp_t类型的参数我们后面会讲。

如果需要全0页(例如返回给用户空间的页),可以使用get_zeroed_page函数,它直接返回已分配页的逻辑地址。

如要释放页,可以使用:

extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

不过要切记,只能释放属于自己的页。内核完全信赖自己,错误的行为很可能导致系统崩溃。

2.2. 实现

TBD

3. 获得、释放字节

获取页的封装函数太低级了,更多时候我们需要获取一个固定的字节数。内核为此提供了一组方便的接口。

3.1. kmalloc

static __always_inline void *kmalloc(size_t size, gfp_t flags)

和用户空间的malloc基本是一样的,错误时返回NULL。

3.1.1. gfp_t

gfp,get free page。这些flags是一组标志,大致可以分为行为修饰符、区修饰符和类型。它们声明于<linux/gfp.h>,不过<linux/slab.h>也包含了该头文件,问一般不需要直接引用它。

行为修饰符可以指定如何分配内存,例如:__GFP_ATOMIC指示分配器不能睡眠,__GFP_NOFAIL指示无限重复分配直至成功,等等。区修饰符指定内存区应该从何处分配,例如__GFP_DMA表示从DMA区分配,什么都不指定就是从正常的NORMAL区分配。

类型标志则将行为修饰符和区修饰符封装于其中,我们在真正使用的时候一般只使用这些封装的标志。例如,GFP_ATOMIC需要使用在各种不能睡眠的地方,GFP_USER用于为用户空间分配内存,而GFP_KERNEL则是最广为使用的首选标志,它需要用于睡眠安全的进程上下文中(比如未持有自旋锁等),内核会尽力而为进行内存分配。还有些标志用于禁止启动文件系统I/O或磁盘I/O,它们用于底层的文件系统或块I/O代码中,防止引起无限的链式反应。GFP_DMA表示分配器必须从DMA区分配内存,一般要和GFP_KERNEL或GFP_ATOMIC联合使用。

#define GFP_ATOMIC	(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_USER	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA		__GFP_DMA

3.1.2. 实现

TBD

3.2. kfree

有借有还,再借不难。

void kfree(const void *);

不过切记只能释放由kmalloc分配出来的内存块。重复释放,或者释放不属于自己的内存,或者释放并非由kmalloc分配的内存,都可能导致严重后果。但这其中有一个例外,就是kfree(NULL)

3.3. vmalloc/vfree

kmalloc保证分配到的内存空间具有连续的物理地址(虚拟地址当然也是连续的)。vmalloc只保证虚拟地址连续,这些页映射到的页框很可能不连续,不过只有游离于内存管理单元以外的硬件才要求必须有连续的物理内存,进程完全可以使用物理上不连续的虚拟内存。

然而,很多内核代码却使用kmalloc获取内存,因为虚拟地址必须经由页表项才能计算出物理地址,如果都能走TLB的话还算好,但如果vmalloc得到的内存映射到了很多页,有可能导致TLB抖动,导致访存性能大幅降低。此外,vmalloc可能会睡眠,因此不能再任何不允许阻塞的情况下使用。

另一方面,vmalloc不要求内存在物理上是连续的。连续的大块内存是一个很高的要求,因此往往这个时候需要vmalloc。

vfree和kfree类似,用于回收由vmalloc分配的内存。

4. 高速缓存——slab层

freelist中存放了很多已经分配好的数据结构块。需要的时候代码会从中抓取一个块来使用,不需要的时候就还回去,从而避免了频繁的内存分配和回收。为了弥补这一缺陷,Linux提供了slab层,它扮演了通用数据结构缓存层的角色。slab分配器能够减少频繁分配和释放带来的代价,并根据硬件设置进行优化。

4.1. slab设计

首先是高速缓存。slab把不同的对象划分为所谓的高速缓存组,每个高速缓存组存放不同类型的对象,每种对象类型对应一个高速缓存。例如一个高速缓存可用于存放进程描述符,另一个可以存放inode,等等。

然后,这些高速缓存又被划成一个一个slab,通常每个slab只占一页,不过也有可能是物理上连续的多页。

每个slab都包含一些对象成员,这些对象就是被缓存的数据结构。刚创建的时候slab是空的,随着不断的使用而逐渐变满。当需要一个对象时,优先从半满的slab中取一个出来,如果没有半满slab,就找空slab,要是空slab都没有就只好创建一个新的slab了。对象使用完毕后,就归还给slab。

4.2. slab分配器接口

上面的名词会有些抽象,接下来我们将介绍更为具体的接口。

高速缓存是kmem_cache结构体,定义于<linux/slub_def.h>。使用如下方法创建一个kmem_cache

struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
			unsigned int align, slab_flags_t flags,
			void (*ctor)(void *));

name是高速缓存的名字;size是高速缓存中每个元素的大小;align表示每个slab中第一个对象的偏移量,用于满足特定的页对齐要求,一般来说设成0就可以了;flags用于控制高速缓存的行为;ctor是高速缓存的构造函数,只有在新页追加到高速缓存时才会调用,一般设成NULL即可。

要撤销高速缓存,相对应地使用:

void kmem_cache_destroy(struct kmem_cache *);

注意以上这两个函数都可能导致睡眠,所以需要注意什么,不用我再说了吧。

创建高速缓存之后,使用该函数从高速缓存获取对象,或者释放对象:

void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags) __assume_slab_alignment __malloc;
void kmem_cache_free(struct kmem_cache *, void *);

申请获取对象时,如果高速缓存没有空闲的对象,slab层会调用kmem_getpages获取新的页,以创建新的slab。

4.3. slab使用实例

代码取自kernel/fork.c。首先设置一个全局变量:

static struct kmem_cache *task_struct_cachep;

在内核初始化期间,该变量会被初始化,成为进程描述符的高速缓存。

void __init fork_init(void)
{
	...
	int align = max_t(int, L1_CACHE_BYTES, ARCH_MIN_TASKALIGN);
	unsigned long useroffset, usersize;

	/* create a slab on which task_structs can be allocated */
	task_struct_whitelist(&useroffset, &usersize);
	task_struct_cachep = kmem_cache_create_usercopy("task_struct",
			arch_task_struct_size, align,
			SLAB_PANIC|SLAB_ACCOUNT,
			useroffset, usersize, NULL);
	...
}

SLAB_PANIC指示分配器必须成功获取内存,否则panic。这一点是有必要的,因为如果内核拿不到进程描述符,那谁也别想跑起来;而放在函数调用后再判断是否为空显得太臃肿。

每次fork肯定会创建一个新的进程描述符的,其对应的调用栈为:

kmem_cache_alloc(s, flags) // node is ignored because NUMA is disabled
kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node)
alloc_task_struct_node(node)
dup_task_struct(current, node)
copy_process(NULL, trace, NUMA_NO_NODE, args)
kernel_clone(args)
...

当进程的一生最终结束之后,其进程描述符被回收:

kmem_cache_free(task_struct_cachep, tsk)
free_task_struct(tsk)
...

参考资料

Slab Allocator – kernel.org

发表评论

您的电子邮箱地址不会被公开。