第十二章 内存管理

内存管理

1. 内核如何管理内存

1.1 页

内核把物理页作为内存管理的基本单位,MMU通常以(page)为单位进行处理,页表页表就是这个意思

内核用struct page结构代表系统中的每个物理页:linux/mm_types.h

1
2
3
4
5
6
7
8
9
10
struct page {
unsigned long flags;
atomic_t _count;
atomic_t mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};
  • flags域用来存放页的状态,定义在

1.2

硬件有一些限制,比如某些页位于内存中的特定物理地址上,所以不能将其用于一些特定的任务。因此内核又把页划分为不同的区(zone),内核使用区将相似属性的页进程分组。

Linux必须处理如下两种硬件引起的内存寻址问题:

  • 一些硬件只能在某些特定内存地址上执行DMA
  • 一些体系结构的内存的物理寻址范围比虚拟寻址范围大,导致一些内存不能永久映射到内核空间

Linux因此将页分成四种区:

  • ZONE_DMA 包含能执行DMA的页
  • ZONE_DMA32 支持32位设备
  • ZONE_NORMAL 所以能正常映射的页
  • ZONE_HIGHEM 包含”高端内存“,其中的页不能永久映射到内核地址空间

区由结构体struct zone表示:linux/mmzone.h,初始化在:

1.3 获取页API

内核提供一种请求内存的底层机制,以页为单位分配内存,相关接口定义在linux/gfp.h

  • struct page* alloc_pages(gfp_t gfp_mask, unsigned int order):分配(1<<order)个连续的物理页,返回指向第一页页结构的指针
  • void page_address(struct page page):转换为逻辑地址
  • __get_free_pages(gfp_mask, order):分配(1<<order)页,返回指向第一页逻辑地址的指针
  • alloc_page(gfp_mask)
  • __get_free_page(gfp_mask)
  • get_zeroed_page(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)

2. kmalloc()

内核提供kmalloc()函数来分配以字节为单位的内存,调用它可以获得以字节为单位的一块内核内存

kmalloc()在linux/slab.h

  • void *kmalloc(size_t size, gfp_t flags):分配在物理上连续的大小为size的内存块

2.1 gfp_mask标志

  • 行为修饰符:表示内核应当如何分配所需内存
  • 区修饰符:表示从哪儿分配内存
  • 类型:组合行为修饰符和区修饰符,得到一个类型标志,方便使用

这些标志声明在:linxu/gfp.h

标志 修饰符 描述
GFP_ATOMIC __GFP_HIGH 用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 0 与GFP_ATOMIC类似,但是调用不会退给紧急内存池,增加了分配失败的可能性
GFP_NOIO __GFP_WAIT 这种分配可以阻塞,但不会启动磁盘I/O。这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,有递归风险
GFP_NOFS __GFP_WAIT|__GFP_IO 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作。这个标志在不能再启动另一个文件系统操作时,用在文件系统代码中
GFP_KERNEL __GFP_WAIT |__GFP_IO | __GFP_FS 常用的方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中
GFP_USER __GFP_WAIT|__GFP_IO|__GFP_FS 常用方式,可能会阻塞。这个标志用于用户空间分配内存
GFP_HIGHUSER __GFP_WAIT|__GFP_IO|
__GFP_FS|__GFP_HIGHMEM
从ZONE_HIGHMEM进行分配,可能会阻塞。这个标志也用于为用户空间进程分配内存
GFP_DMA __GFP_DMA 从ZONE_DMA进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志,通常与以上的某个标志组合

2.2 kfree()

kfree声明在中:

1
void kfree(const void *ptr);

kfree()释放由kmalloc()分配的内存块,注意分配和回收要配对使用,调用kfree(NULL)是安全的。

2.3 vmalloc()

vmalloc()类似于kmalloc(),只是vmalloc()分配的虚拟地址连续,但是物理地址可能不连续。这是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内连续,但是不能保证其在物理内存也连续。kmalloc()确保页在物理地址上连续。
vmallo()声明在linux/vmalloc.h,定义在mm/vmalloc.c,函数可能睡眠:

1
2
void* vmalloc(unsigned long size);
void vfree(const void *add);

3. slab分配器

内存的分配和释放是非常普遍的,频繁的分配和释放消耗很大,Linux提供了slab分配器管理内存。关于slab分配器的原则:

  • 频繁使用的数据结构也会频繁分配和释放,因此应该缓存它们
  • 频繁分配和释放会导致内存碎片,因此空闲链表的缓存应该连续存放。
  • 回收的对象可以立刻投入下一次分配,因此,提供了频繁的分配释放的性能
  • 如果知道对象大小、页大小和总的高速缓存大小,分配器可以做出更明智的决策
  • 如果让部分缓存专属于单个处理器,那么分配和释放就可以不加SMP锁
  • 如果分配器与NUMA相关,它就可以从相同的内存节点为请求者分配
  • 对存放的对象进行着色,防止多个对象映射到相同的高速缓存行(cache line)

学习资料:

每个高速缓存使用kmem_cache结构表示,包含三个链表:slabs_full、slabs_partial、slabs_empty,slab实现的文件:linux/slab.hmm
/slab.c

slab申请的内存释放:

  • 当可用内存变得紧缺时,系统试图释放出更多内存以供使用
  • 当高速缓存显式被撤销时

4. 在栈上静态分配

内核对进程的栈有限制:内核栈小而且固定。

进程的内核栈通常是两页的大小,32位和64位体系结构的页为4Kb和8kb,所以它们的内核栈大小分别为8kb和16kb。

可能设置单页的内核栈,此时中断处理程序不和内核进程共享同一个栈了,内核为它提供自己的中断栈。

5. 高端内存映射

高端内存中的页不能永久映射到内核地址空间,因此通过alloc_pages()以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。

永久映射
要映射一个给定的page结构到内核地址空间,可以使用kmap()函数,定义在linux/highmem.h

1
2
void* kmap(struct page *page);
void kunmap(struct page *page);

临时映射
当必须创建一个映射而当前上下文不能睡眠时,内核提供了临时映射(原子映射),通过一组保留的映射完成。

1
2
void* kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *kvaddr, enum Km_type type);

km_type描述了临时映射的目的(v3.5之前)