内存管理
在内核中分配内存不像在其他地方分配内存那么容易。造成这种局面的因素很多,根本原因是内核本身不能像用户空间那样奢侈地使用内存。
1.页
内核把物理页作为内存管理的基本单位。内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位。体系结构不同,支持的页大小也不同。内核用struct page
结构表示每个物理页:
struct page {
unsigned long flags;
atomic_t count;
unsigned int mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};
对上面重要变量说明:
-
flag
的每一位单独表示一个状态,标志定义在<linux/page-flags.h>
-
count
存放页的引用次数,为0则是空闲页 -
virtual
是页的虚拟地址
2.区
由于硬件限制,内核对页不能一视同仁。有些页位于内存特定的物理地址上,不能用于一些特定的任务,因此内核把页划分为不同的区(zone)。Linux必须处理如下两种由于硬件缺陷而引起的内存寻址问题:
- 一些硬件只能用某些特定内存来执行DMA(直接内存访问)
- 一些体系结构的内存物理寻址范围比虚拟寻址范围大的多,因此部分内存永远无法映射到内核空间
因此Linux主要存在四种区:
- ZONE_DMA,包含的页可以执行DMA
- ZONE_DMA32,和ZONE_DMA不同在于,这些页面只能被32位设备访问,某些体系下该区比ZONE_DMA更大
- ZONE_NORMAL,能够正常映射的页
- ZONE_HIGHMEM,不能永久被映射到内核空间地址的区
每个区都用struct zone
表示,定义在<linux/mmzone.h>
:
struct zone {
spinlock_t lock;
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
unsigned long protection[MAX_NR_ZONES];
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long nr_active;
unsigned long nr_inactive;
int all_unreclaimable;
unsigned long pages_scanned;
struct free_area free_area[MAX_ORDER];
wait_queue_head_t * wait_table;
unsigned long wait_table_size;
unsigned long wait_table_bits;
struct per_cpu_pageset pageset[NR_CPUS];
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_pfn;
char *name;
unsigned long spanned_pages;
unsigned long present_pages;
};
其中,lock
是自旋锁防止该结构被并发访问;watermark
数组持有该区的最小值、最低和最高水位值;name
是以NULL结尾的区名字,三个区名字为DMA,Normal和HighMem。
3.获得页
前面了解了页和区的概念,下面讲述如何请求和释放页。
请求页
标志 | 描述 |
---|---|
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,返回指向逻辑地址的指针 |
释放页
释放页需要谨慎,只能释放属于你的页。传递了错误的struct page
或地址,,用了错误的order
值都可能导致系统崩溃。
例如释放8个页:
free_pages(page, 3)
可以看到释放过程与C语言的释放内存很相似的。
4.kmalloc()
上述的方法是对以页为单位的连续物理页,而以字节为单位的分配,内核提供的函数是kmalloc()
。使用方法和malloc()
类似,只是多了一个flags
参数,其在<linux/slab.h>
中声明:
void * kmalloc(size_t size, gfp_t flags)
与kmalloc()
对应的函数就是kfree()
,kfree()
声明于<linux/slab.h>
中:
void kfree(const void *ptr)
5.vmalloc()
vmalloc()
和kmalloc()
工作方式类似,但是kmalloc()
使用的连续的物理地址。vmalloc()
使用非连续的物理地址,该函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。
大多数情况下,一般硬件设备需要使用连续的物理地址,而软件可以使用非连续的物理地址,但是大多数情况,为了性能提升,内核往往用kmalloc()
更多。
vmalloc()
函数声明在<linux/vmalloc.h>
中,定义在<mm/vmalloc.c>
中。用法和用户空间的malloc()
相同:
void * vmalloc(unsigned long size)
释放通过vmalloc()
所获得的内存,使用下面函数:
void vfree(const void *addr)
6.slab层
分配和释放数据结构是所有内核中最常用操作之一。为了便于数据的频繁分配和回收,编程人员常常会用到空闲链表。空闲链表包含可供使用的、已经分配好的数据结构块。当代名需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据存放进去。不需要这个数据结构的实例时,就放回空闲链表,而不是释放它。空闲链表相对于对象的高速缓存——快速存储频繁使用的对象类型(这个策略简直是awesome!)。
没有免费的蛋糕,对于空闲链表存在的主要问题是无法全局控制。当内存紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小,以便释放部分内存。实际上,内核根本就不知道任何空闲链表。因此未来弥补这个缺陷,Linux内核提供了slab层
(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。对于slab分配器设计需要考虑一下几个原则:
- 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
- 频繁分配和回收必然会导致内存碎片。为了避免这种情况,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,不会导致碎片。
- 回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
- 如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
- 对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。
slab层
把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存,例如一个高速缓存用于task_struct
,一个用于struct inode
。kmalloc()接口建立在slab层上,使用了一组通用高速缓存。这些缓存又被分为slabs,slab由一个或多个物理上连续的页组成,一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构,每个slab处于三种状态之一:满,部分满,空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。下图给出高速缓存,slab及对象之间的关系:
每个缓存都使用kmem_catche
结构表示,结构中包含3个链表。这些链表包含高速缓存所有的slab。slab描述符struct slab
用来描述每个slab
:
struct slab {
struct list_head list; /*满,部分满或空链表*/
unsigned long colouroff; /*slab着色的偏移量*/
void *s_mem; /*在slab中的第一个对象*/
unsigned int inuse; /*已分配的对象数*/
kmem_bufctl_t free; /*第一个空闲对象*/
};
slab层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。
7.栈上的静态分配
在前面讨论的分配例子,不少可以分配到栈上。用户空间可以奢侈地负担很大的栈,而且栈空间还可以动态增长,相反内核空间不能——栈小而固定。给每个进程分配一个固定小栈,可以减小内存消耗和栈管理任务负担。
进程的内核栈大小既依赖体系结构,也和编译时的选项有关。在任何一个函数中,都必须尽量节省栈资源。让函数所有局部变量之后不要超过几百字节(栈上分配大量的静态分配是不理智的),栈溢出就会覆盖掉临近堆栈末端的数据。首先就是前面讲的thread_info
。
8.每个CPU使用数据
支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般而言,每个CPU的数据存放在一个数组内,数组中的每一项对应着系统上一个存在的处理器,安装当前处理器号就能确定这个数组的当前元素。
在Linux中引入了新的操作接口称为percpu
,头文件<linux/percpu.h>
声明了所有接口操作例程,可以在文件mm/slab.c
和<asm/percpu.h>
找到定义。
使用每个CPU数据的好处是:
- 减少了数据锁定
- 大大减少了缓存失效,一个CPU操作另一个CPU的数据时,必须清理另一个CPU的缓存并刷新,存在不断的缓存失效。持续不断的缓存失效称为缓存抖动。
这种方式的唯一安全要求就是禁止内核抢占,同时注意进程在访问每个CPU数据过程中不能睡眠——否则,唤醒之后可能已经到其他处理器上了。