Linux源码分析: 12 内核是如何实现内存管理的

Linux内核设计与实现 12—— 内存管理

前言:内核内存管理存在的困难

在内核里分配内存存在着以下这些困难:

  • 从根本上讲,内核本身不能像用户空间那样奢侈地使用内存;
  • 内核不支持简单便捷的内存分配方式;
  • 内核难以处理内存分配错误。

因此,内存管理十分重要,本文通过研究Linux内核源代码以及相关资料,对内核如何进行内存管理给出了详细介绍。

12.1 数据结构介绍:页与区

12.1.1 页

内核把物理页(page)作为内存管理的基本单位。从虚存的角度来看,页就是最小单位。

内核中用struct page结构表示系统中的每个物理页(<linux/mm_types.h>):

struct page {
    unsigned long flags;			// 存放页的状态,是否为脏页,是否被锁存等
    atomic_t _count;	    		// 存放页的引用次数,为-1代表未被引用
    atomic_t _mapcount;				// 被映射引用的次数
    unsigned long private;			// 作为私有数据
    struct address_space *mapping;   // 当一个页被页缓存使用时,mapping域指向和这个页相关的地址空间
    pgoff_t index;
    struct list_head lru;
    void *virtual;				    // 页在虚拟内存中的地址
}

12.1.2 区

有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。因此,内核把页划分为不同的区(zone),并利用区对具有相似特性的页进行分组。Linux必须处理如下两种由于硬件缺陷所引发的内存寻址问题:

  • 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问);
  • 一些体系结构中的内存物理寻址范围远大于虚拟寻址范围,这导致内存不能永久地映射在内核空间上。

因为存在这些制约条件,Linux主要使用了四种区(<linux/mmzone.h>)来解决问题:

  • ZONE_DMA——这个区包含的页能用来执行DMA操作;
  • ZONE_DMA32——与前一个类似,不同点在于该区中的页面只能被32位设备访问;
  • ZONE_NORMAL——这个区包含能够正常映射的页;
  • ZONE_HIGHEM——“高端内存”,又称动态映射页,其中的页不能永久映射到内核地址空间中。

每个区都用struct zone表示(<linux/mmzone.h>),该结构体很大,以下展示的是最重要的三个域:

struct zone {
    ...
    /* lock域是一个自旋锁,它防止该结构被并发访问 */
    spinlock_t lock;
    /* watermark数组持有该区的最小值、最低和最高水位值,内核使用水位为每个内存区设置合适的内存消耗基准 */
    unsigned long watermark[NR_WMARK];
    /* name域是一个以NULL结束的字符串表示这个区的名字,它在内核启动期间被初始化 */
    const char *name;
    ...
}

12.2 内核管理内存的接口:以页为单位

12.2.1 如何获得页

内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有接口都以页为单位分配内存,核心函数是(<linux/gfp.h>):

static inline struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order,
		struct zonelist *zonelist)
{
	return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

该函数分配2order(1 << order)个连续的物理页,并返回一个指向第一个页的page结构体的指针;如果出错,则返回NULL。

其他的页分配方法列表如下:

标志描述
alloc_page(gfp_mask)只分配一页,返回指向页结构的指针
alloc_pages(gfp_mask, order)分配2^order个页,返回指向第一页页结构的指针
__get_free_page(gfp_mask)只分配一页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask, order)分配2^order个页,返回指向第一页逻辑地址的指针
get_zeroed_page(gfp_mask)只分配一页,同时让内容填充为0(报障系统安全),返回指向其逻辑地址的指针

12.2.2 如何释放页

我们可以根据需要,调用不同的函数来释放页:

void __free_pages(struct page *page, unsigned int order)	// 根据指针进行删除
void free_pages(unsigned long addr, unsigned int order)		// 根据逻辑地址进行删除(多页)
void free_page(unsigned long addr)						  // 根据逻辑地址进行删除(单页)

需要注意的是,释放页的时候需要谨慎,如果传递了错误的page指针、地址或者order值,都可能导致系统的崩溃。

12.3 内核管理内存的接口:以字节为单位

12.3.1 如何分配字节单位的内存:kmalloc()

如果需要以字节为单位分配内存,内核提供的是kmalloc()接口(<linux/slab.h>):

void * kmalloc(size_t size,gfp_t flags)

它与malloc()函数非常类似,唯一的区别是多了一个flags参数,该函数返回一个指向内存块的指针,该内存在物理上是连续的,如果出错,则返回NULL。

与malloc()类似,在调用之后,我们必须检查返回的是不是NULL,如果是,要适当地处理错误,举例如下:

struct node *p;

p = kmalloc(sizeof(struct node), GFP_KERNEL);
if (!p) {
	/* 处理错误…… */
}

12.3.2 如何释放字节单位的内存:kfree()

正如malloc()与free()对应,kmalloc()与kfree()也一一对应,而在使用kfree()时,也需要注意,必须释放的是kmalloc()分配的内存,如果想要释放的内存已经被释放了,或者释放了属于内核其他部分的内存,这同样会导致相当严重的后果。

void kfree(const void *ptr)

12.3.3 gfp_mask标志

在前面,我们注意到在各种分配函数中,都出现了分配器标志,这些标志可以分成三类:

  • 行为修饰符:表示内核应当如何分配所需内存;
  • 区修饰符:表示从哪儿分配内存(从前面介绍的四个区中选择);
  • 类型:组合了行为修饰符和区修饰符,所以指定一个类型标志就可以了。

而在实际使用中,绝大多数情况都是使用GFP_KERNEL或者GFP_ATOMIC,二者的描述如下:

  • GFP_ATOMIC:用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方;
  • GFP_KERNEL:一种尽力而为的常规分配方式,常用于获得调用者所需的内存。

12.3.4 更接近malloc()的分配方式:vmalloc()

内核还提供了一种分配方式,vmalloc(),它相对于kmalloc(),更接近于malloc()的分配方式,因为它分配的内存虚拟地址是连续的,而物理地址则无需连续。同样,它有一个对应的释放函数vfree()(mm/vmalloc.c):

void *vmalloc(unsigned long size)
void vfree(const void *)

实际上使用的更多的还是kmalloc(),这是出于性能的考虑,因为vmalloc()分配的空间在物理上是不连续的,所以必须对这些页进行一对一地映射,这会导致比直接内存映射大得多的TLB抖动。因此,vmalloc()仅在不得已的时候使用,一般是为了获得大块内存的时候。

12.4 对空闲链表的改进:slab层

空闲链表是一个包含可供使用的、和以及分配好的数据结构块。当代码需要一个新的数据结构实例是,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。当不需要这个数据结构的实例时,再把它放回空闲链表。而不是释放它。这使得空闲链表相当于对象的高速缓存,即快速地存储频繁使用的对象类型。

但是内核,空闲链表存在一个问题,那就是不能进行全局控制。为了弥补这一缺陷,使代码更加健壮,Linux内核提供了slab层。

12.4.1 slab层的设计思路

可以把slab层划分为三级结构,高速缓存——slab——对象,上层对应多个下层:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMqRTsMa-1655262639950)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220615110942852.png)]

slab描述符struct slab用来描述slab(mm/slab.c):

/*
 * struct slab
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.
 * Slabs are chained into three list: fully used, partial, fully free slabs.
 */
struct slab {
	struct list_head list;	/* 满、部分满或空链表 */
	unsigned long colouroff;	/* slab着色的偏移量 */
	void *s_mem;		/* including colour offset */
	unsigned int inuse;	/* num of objs active in slab */
	kmem_bufctl_t free;	/* slab中第一个空闲对象 */
	unsigned short nodeid;
};

slab描述符要么在slab之外另外分配,要么就放在slab自身开始的地方。如果slab狠下,或者slab内部有足够的空间容纳slab描述符,那么描述符就存放在slab里面。

slab分配器可以创建新的slab,创建函数如下:

/*
 * Interface to system's page allocator. No need to hold the cache-lock.
 *
 * If we requested dmaable memory, we will get it. Even if we
 * did not request dmaable memory, we might get it, but that
 * would be relatively rare and ignorable.
 */
// 第一个参数指向高速缓存
// 第二个参数是传给__get_free_pages()的标志
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参数上
	flags |= cachep->gfpflags;
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		flags |= __GFP_RECLAIMABLE;

    // 通过alloc_pages_exact_node为告诉缓存分配足够的内存
	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);
}

忽略掉与NUMA相关的代码,核心的函数如下:

// 第一个参数指向高速缓存
// 第二个参数是传给__get_free_pages()的标志
static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
{
	struct page *page;
	/*
	 * Nommu uses slab's for process anonymous memory allocations, and thus
	 * requires __GFP_COMP to properly refcount higher order allocations
	 */
	flags |= __GFP_COMP;
	// 把高速缓存需要的缺省标志加到flags参数上
	flags |= cachep->gfpflags;
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		flags |= __GFP_RECLAIMABLE;

    // 通过alloc_pages_exact_node为告诉缓存分配足够的内存
	page = alloc_pages_exact_node(nodeid, flags | __GFP_NOTRACK, cachep->gfporder);
	if (!page)	// 无效页,直接返回NULL
		return NULL;
	...
	return page_address(page);
}

当然,slab层的关键就是避免频繁分配和释放页,所以slab层只会在极少数的情况下执行上述操作。

12.4.2 slab分配器的接口

一个新的高速缓存通过以下函数创建:

struct kmem_cache * kmem_cache_create {
        const char *name,           // 高速缓存名称
        size_t size,                // 每个元素大小
        size_t align,               // 第一个对象的偏移,确保页内对齐
        unsigned long flags,        // 可选设置项
        void (*ctor)(void *);      // 构造函数,一般为null
}

同样的,在创建了一个高速缓存之后,我们也可以通过下面这对函数进行高速缓存对象的分配以及释放:

/**
 * kmem_cache_alloc - Allocate an object
 * @cachep: The cache to allocate from.
 * @flags: See kmalloc().
 *
 * Allocate an object from this cache.  The flags are only relevant
 * if the cache has no available objects.
 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
	void *ret = __cache_alloc(cachep, flags, __builtin_return_address(0));

	trace_kmem_cache_alloc(_RET_IP_, ret,
			       obj_size(cachep), cachep->buffer_size, flags);

	return ret;
}

/**
 * kmem_cache_free - Deallocate an object
 * @cachep: The cache the allocation was from.
 * @objp: The previously allocated object.
 *
 * Free an object which was previously allocated from this
 * cache.
 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
	unsigned long flags;

	local_irq_save(flags);
	debug_check_no_locks_freed(objp, obj_size(cachep));
	if (!(cachep->flags & SLAB_DEBUG_OBJECTS))
		debug_check_no_obj_freed(objp, obj_size(cachep));
	__cache_free(cachep, objp);
	local_irq_restore(flags);

	trace_kmem_cache_free(_RET_IP_, objp);
}

12.5 内存管理中存在的问题与解决方案

12.5.1 问题1:内核栈的静态分配不足

不同于用户空间的内存分配,内存的静态分配在内核中显得比较困难,因为内核栈很小,而且固定。栈溢出时悄无声息,但势必会引起严重的问题,因为内核对于栈的管理能力较弱,一旦溢出,就有可能覆盖掉紧邻堆栈末端的东西。

解决这个问题,主要有以下两种措施:

  • 给每个进程分配一个固定大小的小栈,这样不但可以减少内存的消耗,而且内核也无须负担太重的栈管理任务。

  • 减少静态分配,多采用动态分配。

12.5.2 问题2:高端内存页无法永久映射

根据定义,高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不会获得逻辑地址。

12.5.2.1 永久映射

要映射一个给定的page结构到内核地址空间,可以使用kmap()(<linux/highmem.h>),和它对应的解除映射的函数为kunmap(),因为允许永久映射的数量有限,所以在使用过程我们应该及时解除映射:

static inline void *kmap(struct page *page)
{
	might_sleep();
	return page_address(page);
}

static inline void kunmap(struct page *page)
{
}
12.5.2.2 临时映射

kmap()函数因为可以睡眠,所以只能用在进程上下文中。

当必须创建一个映射而当前上下文又不能睡眠时,内核提供了临时映射,也叫原子映射:

static inline void *kmap_atomic(struct page *page, enum km_type idx)
{
	pagefault_disable();	// 使页错误失效,保证操作的原子性
	return page_address(page);
}

参数type是下列枚举类型之一,这些枚举类型描述了临时映射的目的(<asm/kmap_types.h>):

// enum类型指示了建立临时映射的目的
enum km_type {
KMAP_D(0)	KM_BOUNCE_READ,
KMAP_D(1)	KM_SKB_SUNRPC_DATA,
KMAP_D(2)	KM_SKB_DATA_SOFTIRQ,
KMAP_D(3)	KM_USER0,
KMAP_D(4)	KM_USER1,
KMAP_D(5)	KM_BIO_SRC_IRQ,
KMAP_D(6)	KM_BIO_DST_IRQ,
KMAP_D(7)	KM_PTE0,
KMAP_D(8)	KM_PTE1,
KMAP_D(9)	KM_IRQ0,
KMAP_D(10)	KM_IRQ1,
KMAP_D(11)	KM_SOFTIRQ0,
KMAP_D(12)	KM_SOFTIRQ1,
KMAP_D(13)	KM_SYNC_ICACHE,
KMAP_D(14)	KM_SYNC_DCACHE,
/* UML specific, for copy_*_user - used in do_op_one_page */
KMAP_D(15)	KM_UML_USERCOPY,
KMAP_D(16)	KM_IRQ_PTE,
KMAP_D(17)	KM_NMI,
KMAP_D(18)	KM_NMI_PTE,
KMAP_D(19)	KM_TYPE_NR
};

这个函数不会阻塞,因此可以用在中断上下文和其他不能重新调度的地方。

12.5.3 问题3:CPU数据的分配问题

对于多处理机的操作系统,不同的处理机访问内存页的数据是唯一的,而在访问过程中有可能出现互斥和抢占的问题。

为了方便创建和操作每个CPU数据,内核采用了percpu的操作接口,这简化了创建和操作每个CPU的数据。

在运行时实现对每个CPU变量的动态分配(<linux/percpu.c>):

/**
 * __alloc_percpu - allocate dynamic percpu area
 * @size: size of area to allocate in bytes
 * @align: alignment of area (max PAGE_SIZE)
 *
 * Allocate percpu area of @size bytes aligned at @align.  Might
 * sleep.  Might trigger writeouts.
 *
 * CONTEXT:
 * Does GFP_KERNEL allocation.
 *
 * RETURNS:
 * Percpu pointer to the allocated area on success, NULL on failure.
 */
// 第一个参数指分配的实际字节数
// 第二个参数指分配时要按多少字节对齐
void __percpu *__alloc_percpu(size_t size, size_t align)
{
	return pcpu_alloc(size, align, false);
}
EXPORT_SYMBOL_GPL(__alloc_percpu);

同样的,可以相应的调用free_percpu()来释放处理器上指定的percpu数据。

__alloc_percpu()返回了一个指针,通过对指针进行操作,可以进一步修改处理器的数据:

void *percpu_ptr;
unsigned long *foo;

percpu_ptr = alloc_percpu(unsigned long);
if (!percpu_ptr)
	/* 内存分配错误 */

foo = get_cpu_var(percpu_ptr);	// 返回一个指向处理器的指针的拷贝
/* 操作foo ... */
put_cpu_var(percpu_ptr);	// 完成操作:重新激活内核抢占

get_cpu_var()和put_cpu_var()是两个宏,其中get_cpu_var()会禁止内核抢占;而put_cpu_var()会重新激活内核抢占。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值