内核中分配内存不像在用户态那么容易。内核一般不能睡眠,而且处理内存分配错误对内核来说也并非易事。由于这类因素存在,导致内核中获取内存要比在用户控件复杂很多。本文参考《Linux 内核设计与实现》,大致梳理一下Linux内核的内存管理。简单总结下内存管理的基本单位—页,内存管理方式—按区划分,以及避免内存碎片的机制—slab 层等。
页
内核把 物理页 作为内存管理的基本单位,用struct page
结构表示系统中的每个 物理页。1
2
3
4
5
6
7
8
9
10
11/*定义在 <linux/mm_types.h> */
struct page{
unsigned long flag;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void* virtual;
};
flag
用来存储页的状态,包括是不是脏页,是不是被锁定在内存中等等。_count
用来存放引用计数。当引用计数变为-1时说明当前页未被引用,在新的分配中可以使用之。(内核应通过static int page_count(struct page* page)
来检查引用计数)virtual
是页的虚拟地址,通常情况下就是页在虚拟内存中的地址。
必须要理解的是page
结构与物理页相关,并非虚拟页。
区
由于硬件限制,内核不能对所有页一视同仁。有些页位于内存中特定物理地址上,因而不能将其用于一些特定的任务。由于存在这类限制,所以内核把页划分为不同的区(ZONE)。
为解决这些问题:
- 一些硬件只能用某些特定的内存地址来执行 DMA
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样就有一些内存不能永久地映射到内核空间。
x86-32上的区
区 | 描述 | 物理内存 |
---|---|---|
ZONE_DMA | DMA 使用的页 | <16MB |
ZONE_NORMAL | 正常可寻址的页 | 16~896MB |
ZONE_HIGHMEM | 动态映射的页 | >896MB |
Linux 主要使用了四种区:
- ZONE_DMA,这个区包含的页可用来执行 DMA
- ZONE_DMA32,与 ZONE_DMA 相似但只能被32位设备访问
- ZONE_NORMAL,这个区包含的页能正常映射
- ZONE_HIGHMEM,这个区包含『高端内存』,其中的页不能永久地映射到内核地址空间。
区的实际使用和分布与体系结构有关。有的体系结构在任何地址上都能执行 DMA(此时ZONE_DMA 就为空,ZONE_NORMAL 可以直接用于分配)。ZONE_HIGHMEM 也差不多,在所有内存都能被直接映射的体系结构上,ZONE_HIGHMEM 为空。因而一般规则是前两个区各取所需后,剩余的就由 ZONE_NORMAL 独享。
函数接口
获得页
内核提供了一种请求内存的底层机制,并提供了接口。所有接口都以页为单位。
gfp_t标志
- GFP_WAIT 分配器可以睡眠
- GFP_HIGH 分配器可以访问紧急事件缓冲池
- GFP_IO 分配器可以启动磁盘 I/O
- GFP_FS 分配器可以启动文件系统 I/O
- GFP_COLD 分配器应该使用高速缓存中将要淘汰的页
- GFP_NOWARN 失败时不打印警告
- GFP_REPEAT 失败时重复进行分配,这次分配依然存在失败可能
- GFP_NOFALL 失败时重复分配,直到成功
- GFP_NORETRY 失败时绝不会重新分配
- GFP_NO_GROW 由 slab 内部使用
- GFP_COMP 添加混合页元数据,在 hugetlb 的代码内部使用
1 | struct page* alloc_pages(gfp_t gfp_mask, unsigned int order) |
该函数分配 2order 个连续的物理页并返回一个指针,指向第一个页的
struct page
。可以通过下面这个函数把给定的页转换成它的逻辑地址。1 | void* page_address(struct page* page) |
还有一些接口
1 | unsigned long _get_free_pages(gfp_t gft_mask, unsigned int order)//与 alloc_pages 作用相同,只是它返回的是第一页的逻辑地址。 |
释放页
释放页需要谨慎,只能释放属于自己的页。传递了错误的 struct page
或地址,用错了 order 值都可能导致系统崩溃。内核是完全信任自己的。1
2
3void _free_pages(struct page* page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr);
kmalloc()
1 | void* kmalloc(size_t size, gfp_t flags);//获取以字节为单位的一块内核内存 |
vmalloc()
kmalloc()
确保页在物理地址上是连续的(虚拟地址当然也是)。vmalloc()
只确保页在虚拟地址空间内连续。1
2void* vmalloc(unsigned long size);
void vfree(const void* addr);
slab 层
为了减小数据频繁申请释放带来的开销,开发人员常会用到空闲链表来做缓存。在需要新的数据块时从链表中取一个,不需要时再放回去而不是释放掉。然而在内核中面临一个问题是不能全局控制。内存变得紧缺时,内核无法通知每个空闲链表收缩空间释放内存。实际上,内核根本不知道存在空闲链表。为了弥补这一缺陷使得代码更加稳固,Linux 内核提供了 slab 层(slab 分配器)来扮演缓存层的角色。
slab 分配器把不同对象划分为高速缓存组,每个缓存组都存放不同类型的对象,每种对象类型对应一个高速缓存。如一个高速缓存用于存放task_strcut
,另一个用于存放struct inode
。高速缓存又划分为 slab, slab 由一个或多个物理上连续的页组成。slab 处于三种状态:
- Empty ,空
- Partial, 部分满
- Full,满
内核的某一部分需要新对象时,先从部分满的 slab 中分配。若没有部分满的 slab,就从空的 slab 中分配。若没有空的 slab,就要新建一个 slab 了。1
2
3
4
5
6
7struct slab{
struct list_head list;//满、半满、或空的链表
unsigned long colouroff;
void* s_mem;
unsigned int inuse; // slab 中分配的对象个数
kmem_bufctl_t free; // 第一个空闲对象
};
参考 《Linux 内核设计与实现》