1. 页
内存管理的基本单位:Page。MMU(vaddr -> addr
)也用页进行管理。
内核用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; //由页缓存使用
pgoff_t index;
struct list_head lru;
void *virtual; //页虚拟地址
//...
}
注意,这里struct page
与物理页相关,而非与虚拟页相关。
2. 区
内核把页划分到不同的区内,对相似特性的页进行分组。
主要使用4种区:
-
ZONE_DMA
:包含的页执行DMA操作 -
ZOME_DMA32
:和ZONE_DMA
类似,但只能被32位设备访问 -
ZONE_NORMAL
:正常映射的页 -
ZONE_HIGHEM
:包含“高端内存”,不能永久映射到内核地址空间
32位x86机器上,内核空间分为:
ZONE_DMA
: <16MBZONE_NORMAL
: 16~896MBZONE_HIGHEM
: >896MB而x86-64上没有
ZONE_HIGHEM
。分配时,可从多个区中获取页,但不能同时在2个区中分配。
每个区用struct zone
表示,定义在<linux/mmzone.h>
中。(内包含一个自旋锁,但只保护结构,不保护整个区)
# 3. 获得页
获取页核心函数是:
struct page* alloc_page(gfp_t gfp_mask, unsigned int order)
,它可分配$2^{order}$个连续的物理页,返回第一个页的page
指针
若获取全为0的页,可调用:
unsigned long get_zeroed_page(unsigned int gfp_mask)
释放页是,可调用下面的函数,但要谨慎,只能释放属于你的页(内核完全信赖自己,若操作非法,内核就会挂起):
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
4. kmalloc()
从内核中申请一块内存,物理地址连续,和malloc()
相比,多了一个flag
参数:
void *kmalloc(size_t size, gfp_t flags)
,定义在<linux/slab.h>
其中flag
是一个gfp_mask
标志,定义在<linux/gfp.h>
,分为3类:
- 行为修饰符:指定分配时的行为
- 区修饰符:指定分配的区域
- 类型标识:结合上面两种修饰,配置内核内存分配
释放内存可调用:
-
void kfree(const void *ptr)
,只能释放kmalloc()
开辟的内存。若释放其它部分内存或释放已经释放的内存,会导致严重后果。kfree(NULL)
是安全的。
5. vmalloc()
可以分配虚拟地址连续的内存,物理地址无需连续(和malloc()
很像)。它定义于<linux/vmalloc.h>
:
void* vmalloc(unsigned long size)
释放调用void vfree(const void *addr)
。
两个函数可以睡眠,不能在中断上下文中调用。
6. slab层
Linux内核使用slab分配器,扮演通用数据结构缓存层,避免过多的内存申请和释放。
6.1. slab层设计
每个高速缓存对应一个对象类型,由若干个slab组成,slab由一个或多个连续物理页面组成。
slab三种状态:满、部分满或者空。
分配时,先从部分满的分配,再考虑空的slab,最后再考虑创建一个slab。
高速缓存使用struct kmem_cache
表示,包含3个链表:slabs_full
, slabs_partial
, slabs_empty
。
而slab描述符用struct slab
描述:
struct slab {
struct list_head list; // 满、部分满或空链表
unsigned long colouroff; // slab着色的偏移量
void *s_mem; // slab中第一个对象
unsigned int inuse; //已分配对象数
kmem_bufctl_t free; //第一个空闲对象
}
slab分配器可创建新的slab,实际上最终通过__get_free_pages()
低级内核页分配器进行的:
-
入口是
static void* kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
,分配大小是2的幂次方 -
释放则调用
kmem_freepages()
6.2. slab分配器的接口
高速缓存可通过下面的函数创建:
struct kmem_cache *kmem_cache_create(const char *name, //缓冲名
size_t size, //每个对象大小
size_t align, //第一个对象偏移
unsigned long flags,
void (*ctor)(void*));//构造函数
撤销可调用,但必须让里面所有slab为空,且之后不能再访问:
int kmem_cache_destroy(struct kmem_cache *cachep)
从缓存中分配和释放对象,可调用:
void *kmem_cache_alloc(struct kmem_cache *cachep, gft_t flags)
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
7. 栈上静态分配
内核栈空间小且固定(不能动态增长),大小通常与体系架构和编译选项有关。
7.1. 单页内核栈
2.6内核早期,栈空间只占用一个页。
好处在于:
- 减少内存消耗,
- 避免因为内存碎片的增多,难以寻找未分配的连续的页
7.2. 节省内核栈的内存
避免在内核栈上大量静态分配,否则栈会溢出,导致堆末端的内容被覆盖。
因此申请内存尽量动态分配,让函数所有的局部变量占用空间之和不超过几百字节。
8. 高端内存的映射
8.1. 永久映射
永久映射的高端内存页数是有限的,映射时,调用:
void *kmap(struct page *page)
不需要使用时,要解除映射:
void kumap(struct page *page)
函数可以睡眠,不能在中断上下文中使用。
8.2. 临时映射
内核可原子地将高端内存中的一个页映射到某个保留映射中,它可以用于不能睡眠的上下文中(如中断上下文),因此它不会阻塞。
建立和解除映射可调用:
void *kmap_atomic(struct page *page, enum km_type type)
void kunmap_atomic(void *kvaddr, emun km_type type)
9. 每个CPU的内存分配
2.6内核为了方便获取每个CPU的数据,引入新接口percpu
,定义在<linux/percpu.h>
9.1. 获取CPU
get_cpu()
:获得当前处理器ID,并禁止内核抢占put_cpu()
:激活内核抢占
9.2. 静态给每个CPU分配变量
DECLARE_PER_CPU(type, name)
:创建类型为type
,名字为name
的变量
然后可通过:
get_cpu_var(name)
:获得该name
变量,并禁止内核抢占put_cpu_var(name)
:完成,重新激活内核抢占
也可以获得其它处理器的变量,但不会禁止内核抢占和锁保护:
per_cpu(name, cpu)
9.3. 运行时给每个CPU分配变量
可通过下面的宏/函数进行分配和释放:
void *alloc_percpu(type)
(宏)void *__alloc_percpu(size_t size, size_t align)
void free_percpu(const void *)
然后依旧可以像9.2.中一样访问变量。
9.4. 使用每个CPU数据的原因
- 减少数据锁定(只需禁止内核抢占即可,代价小的多)
- 减少缓存失效
但注意,访问每个CPU数据时,不能睡眠,否则醒来可能到其它处理器上。
10. 分配函数的选择
- 若需要连续物理内存:
kmalloc()
- 若需要连续虚拟内存:
vmalloc()
,性能比kmalloc()
差些 - 若从高端内存进行分配:
alloc_pages()
&kmap()
- 若频繁创建和释放许多大的数据结构:建立slab高速缓存