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——对象,上层对应多个下层:
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()会重新激活内核抢占。