Linux kernel学习-内存管理

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-management/

接着之前的 Linux kernel 学习步伐,来到极其重要的内存管理部分,继续本文内容,需要先了解内存寻址的基础知识,见之前的 [内存寻址] 博文。

1、内存页及内存区域:

正如之前所说,Linux kernel 使用物理页作为内存管理的基本单位,其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成,系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页,它的定义在 include/linux/mm_types.h 头文件中:

struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	atomic_t _count;		/* Usage count, see below. */
	union {
		atomic_t _mapcount;	/* Count of ptes mapped in mms,
					 * to show when page is mapped
					 * & limit reverse map searches.
					 */
		struct {		/* SLUB */
			u16 inuse;
			u16 objects;
		};
	};
	union {
	    struct {
		unsigned long private;		/* Mapping-private opaque data:
					 	 * usually used for buffer_heads
						 * if PagePrivate set; used for
						 * swp_entry_t if PageSwapCache;
						 * indicates order in the buddy
						 * system if PG_buddy is set.
						 */
		struct address_space *mapping;	/* If low bit clear, points to
						 * inode address_space, or NULL.
						 * If page mapped as anonymous
						 * memory, low bit is set, and
						 * it points to anon_vma object:
						 * see PAGE_MAPPING_ANON below.
						 */
	    };
#if USE_SPLIT_PTLOCKS
	    spinlock_t ptl;
#endif
	    struct kmem_cache *slab;	/* SLUB: Pointer to slab */
	    struct page *first_page;	/* Compound tail pages */
	};
	union {
		pgoff_t index;		/* Our offset within mapping. */
		void *freelist;		/* SLUB: freelist req. slab lock */
	};
	struct list_head lru;		/* Pageout list, eg. active_list
					 * protected by zone->lru_lock !
					 */
	/*
	 * 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 */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
	unsigned long debug_flags;	/* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
	/*
	 * kmemcheck wants to track the status of each byte in a page; this
	 * is a pointer to such a status block. NULL if not tracked.
	 */
	void *shadow;
#endif
};

其中的 flags 用于表示页的状态(是否为脏或者被锁定等),_count 即为页的引用计数,kernel 一般使用 page_count 宏调用 atomic_read 函数原子的读取此值,page_count 返回 0 表示此页可用。如果一个页被作为 page cache 使用,则 page 的 mapping 字段指向映射的 inode 的 address_space 对象,如果页被作为私有数据(作为 buffer_heads 缓冲、buddy 系统等),则 private 常包含对应的信息。注意其中的 virtual 字段为页的虚拟地址,结合之前的知识,对于高端内存来说,其并没有被固定映射到 kernel 地址空间中,因此如果 virtual 字段为 NULL,则表示此页必须被动态映射。

kernel 使用 page 结构记录系统中的所有页,因此 struct page 的大小应该要尽量小以减少内存占用,另外 kernel 必须知道页是否空闲,如果不空闲则拥有者是谁。

由于实际硬件限制,Linux kernel 不可能使用全部的物理内存,kernel 为此将内存划分为不同的区域,一个区域中的内存属性应该也相同。kernel 中常见的内存区域有 ZONE_DMA(可用于 DMA 的页)、ZONE_DMA32(与 ZONE_DMA 类似,但只对 32 位设备可用)、ZONE_NORMAL、ZONE_HIGHMEM(并没有被固定映射的高端内存区域),这些内存区域一般都是硬件相关的,例如在 x86 架构下,ZONE_DMA 的范围为 0MB - 16MB,ZONE_HIGHMEM 为高于 896MB 的物理内存,而在 x86_64 架构下 ZONE_HIGHMEM 则为空。需要注意的是内存的分配不会跨域这些不同的内存区域。内存区域在 kernel 中由 struct zone 结构来表示,其中的 name 字段即为内存区域名称。

2、获取页:

分配和释放内存是 Linux kernel 中极其重要又用的极多的接口。先看看 kernel 提供的直接获取以内存页面为单位的 alloc_pages 函数:

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

此函数是最基本的用于分配大小为 2^order 并且连续的物理页的函数,其返回分配到的第一个页面的 page 指针。

来看看比较重要的 gfp_t 类型的 gfp_mask 值:

gfp_t 实际上就是 unsigned int 类型,gfp_mask 常用于指定行为方式、区域方式、类型等信息。常见的行为方式标志有:__GFP_WAIT(标志分配器可以睡眠,明显不适用于中断上下文中)、__GFP_IO(分配器可以启动磁盘 I/O)等。区域方式指定内存从哪里分配,对应的就有:__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM(从高端内存或普通内存中分配)。类型标志则用于简化分配时的指定操作,常见的有:GFP_ATOMIC(高优先级并不可睡眠,常用于中断、中断下半部、持有自旋锁等环境中)、GFP_NOIO(表示分配可中断但不可以发起 I/O 操作)、GFP_NOFS(分配时不可发起文件 I/O 操作)、GFP_KERNEL(最常见的分配标志,常用于可以睡眠的进程上下文中)、GFP_USER(用于分配内存给用户进程)、GFP_DMA 等。

需要注意的是对 __get_free_pages 和 kmalloc 函数(下面会分别说明)不能指定 __GFP_HIGHMEM 标志,因为它们都是直接返回的虚拟地址,而非 page 结构指针,如果指定了 __GFP_HIGHMEM,则他们可能分配到的内存并没有被映射到 kernel 地址空间,因此这样得不到虚拟地址。只有 alloc_page 函数可以分配高端内存,这个限制在下面的 __get_free_pages 函数的实现中可以看到。

使用 page_address 函数可以将 page 指针转换为虚拟地址(非物理地址)。实际使用中经常会用到 __get_free_pages 函数直接在分配页时直接得到虚拟地址,其参数为 alloc_pages 完全一样,看看它的实现就一目了然了:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}

另外 kernel 还 “好心” 的提供了两个只分配一个页的函数:alloc_page 和 __get_free_page,可以想象只是把 order 参数设为 0 而已。你可以使用 get_zeroed_page 函数分配一个页并自动清零(gfp_mask 指定 __GFP_ZERO)。

对应的释放页可以用 __free_pages(page 指针为参数)、free_pages(虚拟地址为参数)、free_page(只释放一个页)这些函数。

下面是常用的分配非整数倍页大小的内存的函数。首先是最常用的 kmalloc 函数:

void *kmalloc(size_t size, gfp_t flags)

kmalloc 用于分配最少指定的 size 字节大小的内存(实际分配的可能比 size 多),这与用户空间的 malloc 函数很相似,但需要注意的是 kmalloc 分配的内存物理地址是连续的,这非常重要。

相应的释放内存函数是 kfree:

void kfree(const void *objp)

kfree 用于释放 kmalloc 分配的内存,注意如果使用 kfree 在不是的 kmalloc 分配的内存地址或者已经 kfree 过的地址上,都可能导致 kernel 出错。

紧接着就是大名鼎鼎的 vmalloc 函数了。它与 kmalloc 类似,但它分配的内存只是虚拟连续的而物理地址却不一定连续,这也类似于用户空间的 malloc 函数的效果。vmalloc 由于需要做页表转换之类的操作,性能比 kmalloc 差,而且 vmalloc 得到的页还必须由单独的页来做映射,对 TLB 缓存的效率也会有影响(有关 TLB 缓存参考之前的文章 [内存寻址]),由于这些原因,vmalloc 在 kernel 中用到的机会并不是很多,其常用于分配大量的内存,常见的一个例子就是内核模块的代码就是通过 vmalloc 加载到 kernel 中的。vmalloc 的原型为:

void * vmalloc(unsigned long size)

与之对应的,使用 vfree 释放分配的内存。另外 vmalloc 和 vfree 都是可以睡眠的,因此它们对中断上下文是不适用的。

3、Slab分配器:

Slab 也是 Linux kernel 中非常重要的组成部分,它用于简化内存的分配和释放,它相当于一个可用内存列表,里面包含一堆已经分配好的数据结构,当 kernel 需要分配一个数据结构时,可以直接从这个可用内存列表中取出而节省分配的时间,不需要的时候又可以还给这个列表而不需要释放,因此这个列表用于缓存经常访问的某种类型的数据。为了统一管理和释放,Linux kernel 引入 Slab 分配器作为通用的数据结构缓存层给经常访问的数据结构使用。需要说明的是 kmalloc 就是在 Slab 分配器基础上实现的。

这里简单对 Slab 分配器做个介绍,有关其细节请参考这篇 PDF 文档:

The Slab Allocator: An Object-Caching Kernel Memory Allocator

Slab 层将不同的对象划分到名为 cache 的不同组中,每个组存储不同类型的数据,也就是每种数据类型都有一个 cache。每个 cache 然后被划分为多个 slab,slab 由一个或多个连续的物理页组成(通常只有一个页),每个 slab 又包含一些数量的对象,也就是实际缓存的数据。每个 slab 的状态可以是这三个中的一个:满、部分满、空。当 kernel 请求一个新对象时,优先从状态为 部分满 的 slab 中取,如果没有则从状态为 空 的 slab 中分配,如果没有状态为 空 的 slab 了就创建一个,可以看到这种策略可以相对的减少内存碎片。

kernel 中常用到的 struct inode 结构就是一个典型的例子,它在 VFS 等地方被用到的非常多,因此 kernel 中增加一个名为 inode_cachep 的 cache 用于缓存 inode 结构。

每个 cache 由 kmem_cache 结构来表示,它的 struct kmem_list3 *nodelists[MAX_NUMNODES] 类型字段即为该 cache 包含的所有 slab。每个 slab 由 struct slab 结构来表示,看看 kmem_list3 和 slab 结构的定义:

struct slab {
	struct list_head list;
	unsigned long colouroff;
	void *s_mem;		/* including colour offset */
	unsigned int inuse;	/* num of objs active in slab */
	kmem_bufctl_t free;
	unsigned short nodeid;
};

struct kmem_list3 {
	struct list_head slabs_partial;	/* partial list first, better asm code */
	struct list_head slabs_full;
	struct list_head slabs_free;
	unsigned long free_objects;
	unsigned int free_limit;
	unsigned int colour_next;	/* Per-node cache coloring */
	spinlock_t list_lock;
	struct array_cache *shared;	/* shared per node */
	struct array_cache **alien;	/* on other nodes */
	unsigned long next_reap;	/* updated without locking */
	int free_touched;		/* updated without locking */
};

多个 slab 可以分别链接到 kmem_list3 的 满(slabs_full)、部分满(slabs_partial)、空(slabs_free)3 个链表中。

Slab 分配器调用 kmem_getpages 函数分配新的 slab(关于 cache 的创建下面会提到),kmem_getpages 会调用 __get_free_pages 函数分配所需的内存用于保持 cache,因此 kmem_getpages 一般在当 部分满(partial) 和 空(free)slab 的情况下调用,来看看它的实现:

static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
{
	struct page *page;
	int nr_pages;
	int i;

#ifndef CONFIG_MMU
	/*
	 * Nommu uses slab's for process anonymous memory allocations, and thus
	 * requires __GFP_COMP to properly refcount higher order allocations
	 */
	flags |= __GFP_COMP;
#endif

	flags |= cachep->gfpflags;
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		flags |= __GFP_RECLAIMABLE;

	page = alloc_pages_exact_node(nodeid, flags | __GFP_NOTRACK, cachep->gfporder);
	if (!page)
		return NULL;

	nr_pages = (1 << cachep->gfporder);
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		add_zone_page_state(page_zone(page),
			NR_SLAB_RECLAIMABLE, nr_pages);
	else
		add_zone_page_state(page_zone(page),
			NR_SLAB_UNRECLAIMABLE, nr_pages);
	for (i = 0; i < nr_pages; i++)
		__SetPageSlab(page + i);

	if (kmemcheck_enabled && !(cachep->flags & SLAB_NOTRACK)) {
		kmemcheck_alloc_shadow(page, cachep->gfporder, flags, nodeid);

		if (cachep->ctor)
			kmemcheck_mark_uninitialized_pages(page, nr_pages);
		else
			kmemcheck_mark_unallocated_pages(page, nr_pages);
	}

	return page_address(page);
}

第一个参数 cachep 为需要分配页的 cache,cachep->gfporder 指定要分配的大小,上面的代码中对于 NUMA 架构做了必要的处理。

kmem_getpages 分配的内存通过 kmem_freepages 释放,它调用 free_pages 释放页,kmem_freepages 一般在系统检测到内存不足时调用或者在销毁 cache 时显示调用。

下面重点来看看 Slab 分配器如何使用。

使用 kmem_cache_create 函数创建新的 cache,其定义为:

struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))

第一个 name 参数指定 cache 的名称,size 为 cache 中对象的大小,align 为对象的对齐(一般为 0),flags 控制 cache 的行为,最后一个参数 ctor 为对象的构造函数,cache 分配新页时会调用此构造函数,现在一般将 ctor 值设为 NULL。

cache 的标志可以是下面常用几种标志的 OR 值:

SLAB_HWCACHE_ALIGN:对 cache 中的每个对象做对齐处理,对齐之后可以提高 cache line 的访问性能,但由于要浪费内存空间,因此一般只在对性能有很高要求的场合使用;
SLAB_POISON:以固定的值填充 slab(默认 0xa5a5a5a5);
SLAB_PANIC:如果分配失败,kernel 直接 panic;
SLAB_CACHE_DMA :指定 Slab 层在 ZONE_DMA 上分配每个 slab。

kmem_cache_create 如果成功返回 struct kmem_cache 结构指针,注意由于 kmem_cache_create 函数可能会睡眠,因此不能在中断上下文中使用。

使用 kmem_cache_destroy 函数销毁 kmem_cache_create 返回的 cache,此函数一般在模块退出时调用,你也可以在很多模块的初始化中找到 kmem_cache_create。同样由于会睡眠,kmem_cache_destroy 也不能在中断上下文中使用。

cache 被创建之后,就可以调用 kmem_cache_alloc 函数从 cache 中取得对象,其定义为:

void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

此函数直接返回对象的指针,如果 cache 中所有 slab 都没有空闲的对象了,Slab 层就需要调用 kmem_getpages 获取新的页。

如果一个对象不再需要使用了,可以调用 kmem_cache_free 将其回收到 slab 中:

void kmem_cache_free(struct kmem_cache *cachep, void *objp)

需要注意的就是 kmem_cache_free 和 kmem_cache_destroy 不能混淆。

4、高端内存映射:

由于高端内存不是被固定映射到 kernel 地址空间中,因此 alloc_pages 函数使用时如果指定了 __GFP_HIGHMEM 标志,则它返回的 page 很可能没有有效的虚拟地址。

使用 kmap 函数可以将一个 page 固定的映射到 kernel 地址空间中:

void *kmap(struct page *page)

注意此函数对高端内存和低端内存都是适用的,如果 page 在低端内存,则直接返回页的虚拟地址,否则需要创建内存映射,由于 kmap 可能会睡眠,因此不能在中断上下文中使用。

被映射的高端内存不需要时应使用 kunmap 函数删除映射。

另外对于不能睡眠的进程环境,Linux kernel 又提供了临时的高端内存映射方法。kernel 可以原子地映射一个高端内存页到 kernel 中的保留映射集中的一个,此保留映射集也是专门用于中断上下文等不能睡眠的地方映射高端内存页的需要。临时高端内存映射函数为 kmap_atomic,看看它在 x86 下的实现:

void *kmap_atomic_prot(struct page *page, enum km_type type, pgprot_t prot)
{
	enum fixed_addresses idx;
	unsigned long vaddr;

	/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
	pagefault_disable();

	if (!PageHighMem(page))
		return page_address(page);

	debug_kmap_atomic(type);

	idx = type + KM_TYPE_NR*smp_processor_id();
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
	BUG_ON(!pte_none(*(kmap_pte-idx)));
	set_pte(kmap_pte-idx, mk_pte(page, prot));

	return (void *)vaddr;
}

void *kmap_atomic(struct page *page, enum km_type type)
{
	return kmap_atomic_prot(page, type, kmap_prot);
}

kmap_atomic 实际调用 kmap_atomic_prot 实现临时映射,kmap_atomic_prot 中同样会先做判断,如果要映射的页不在高端内存则直接返回虚拟地址,然后根据 type 和当前处理器 ID 计算得到 fixmap 的索引,并调用 __fix_to_virt 将 fixmap 索引转换为虚拟地址,有关 fixmap 机制见之前的 [内存寻址] 博文。

kmap_atomic 函数的 type 参数用于临时映射的用途,此函数会禁用内核抢占,因为临时映射是和每个处理器相关的,它是直接调用 pagefault_disable 函数禁止 page fault handler,其中会自动禁用内核抢占,看看 pagefault_disable 的实现:

static inline void pagefault_disable(void)
{
	inc_preempt_count();
	/*
	 * make sure to have issued the store before a pagefault
	 * can hit.
	 */
	barrier();
}

临时高端内存映射可以使用 kunmap_atomic 函数删除,它会启用内核抢占,同时它也不会睡眠,需要注意的是此次的临时高端内存映射在下一次临时映射高端内存时就会无效。

本文中如果有任何问题,欢迎提出指正哦,玩的开心~~~ ^_^

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*