Linux 内存管理

内核中分配内存不像在用户态那么容易。内核一般不能睡眠,而且处理内存分配错误对内核来说也并非易事。由于这类因素存在,导致内核中获取内存要比在用户控件复杂很多。本文参考《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_DMADMA 使用的页<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
2
3
unsigned long _get_free_pages(gfp_t gft_mask, unsigned int order)//与 alloc_pages 作用相同,只是它返回的是第一页的逻辑地址。
struct page* alloc_page(gfp_t gfp_mask);//申请一页,相当于 order = 0 (2^0 = 1)
struct long _get_free_page(gfp_t gfp_mask);


释放页

释放页需要谨慎,只能释放属于自己的页。传递了错误的 struct page或地址,用错了 order 值都可能导致系统崩溃。内核是完全信任自己的。

1
2
3
void _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
2
void* 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
7
struct slab{
struct list_head list;//满、半满、或空的链表
unsigned long colouroff;
void* s_mem;
unsigned int inuse; // slab 中分配的对象个数
kmem_bufctl_t free; // 第一个空闲对象
};

参考 《Linux 内核设计与实现》