内核空间内存管理
一、内核如何管理内存
1、页:内核中内存管理的基本单位
struct page
{
page_flags_t flags; 页标志符
atomic_t _count; 页引用计数
atomic_t _mapcount; 页映射计数
unsigned long private; 私有数据指针
struct address_space *mapping; 该页所在地址空间描述结构指针,用于内容为文件的页帧
pgoff_t index; 该页描述结构在地址空间radix树page_tree中的对象索引号即页号
struct list_head lru; 最近最久未使用struct slab结构指针链表头变量
void *virtual; 页虚拟地址
};
- flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在<linux/page-flags.h>
- _count:表示某一页被引用的次数,计数值为-1表示未被使用。
- virtual:即页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要动态映射这些内存。
尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU),把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高2
2、区:内核把页划分在不同的区
区 | 内容 | 范围 |
---|---|---|
ZONE_DMA | DMA直接访问的页 | <16M |
ZONE_NORMAL | 正常寻地址的页 | 16~896MB |
ZONE_HIGHEN | 动态映射的页 | >896MB |
- 执行DMA(直接内存访问)操作的内存必须从ZONE_DMA区分配
- ZONE_HIGHMEM:即 “高端内存”,不可永久的映射到虚拟地空间
二、如何分配和释放内存
- 页分配与释放
- get_zeroed_page:对于用户空间,这个方法能保障系统敏感数据不会泄露
- page_address: 把给定的页转换成逻辑地址
1、以字节为单位的分配与释放
- Kmalloc()内存分配最终总是调用__get_free_pages 来进行实际的分配
- kzalloc()先用 kmalloc() 申请空间, 再用memset()来初始化,所有申请的元素都被初始化为0。
- vmalloc()返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。
2、伙伴系统
把所有的空闲页框分为11个块链表,每个块链表中的结点分别是大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大的页框块包含1024个连续页框,对应4MB大小的连续内存。假设要申请一个256个页框的块,则先从结点为256个连续页框块的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找—分割—分配和转移。如果仍然没有,则返回错误。使用过的页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块,然后作为结点插入相应规格的链表中。
内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。
外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。
3、slab分配器
slab是Linux操作系统的一种内存分配机制。其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内存碎片,而且处理速度也太慢。slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内存碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。
slab分配器的作用:
- 对于频繁地分配和释放的数据结构,会缓存它;
- 频繁分配和回收比如导致内存碎片,为了避免,空闲链表的缓存会连续的存放,已释放的数据结构又会放回空闲链表,不会导致碎片;
- 让部分缓存专属单个处理器,分配和释放操作可以不加SMP锁;
- slab层把不同的对象划分为高速缓存组,每个高速缓存组都存放不同类型的对象,每个对象类型对应一个高速缓存。
普通高速缓存
slab分配器中kmem_cache是用来描述高速缓存的结构,因此它本身也需要slab分配器对其进行高速缓存。cache_cache变量保存着对高速缓存描述符的高速缓存。
专用高速缓存
内核为专用高速缓存的申请和释放提供了一套完整的接口,根据所传入的参数为具体的对象分配slab缓存。
- kmem_cache_create:创建高速缓存
- kmem_cache_destroy: 撤销高速缓存
- kmem_cache_alloc: 从高速缓存中返回一个指向对象的指针
- kmem_cache_free:释放一个对象
4、栈的静态分配
单页内核栈:每个进程的内核栈只有一页大小,这取决于编译时配置选项。
所解决的问题:
- 可以减少每个进程内存的消耗;
- 随着机器运行时间的增加,寻找两个未分配的、连续的页越来越困难,物理内存碎片化不断加重,那么给每个新进程分配虚拟内存的压力也增大;
- 每个进程的调用链在自己的内核栈中,当单页栈选项被**时,中断处理程序可获得自己的栈
任意函数必须尽量节省栈资源, 方法就是所有函数让局部变量所占空间之和不要超过几百字节。如果栈溢出,可能会对连着内核栈末端的thread_info结构产生很大的影响。
5、高端内存的映射
高端内存中的页不能永久地映射到内核地址空间。
- kmap:把给定page结构映射到内核地址空间;
这个函数可以睡眠,因此kmap()只能用在进程上下文中,不能用于中断上下文中。需要注意一点的是,当永久内核映射区没有空闲的页表项可供映射时,请求映射的进程会被阻塞,因此永久内核映射请求不能发生在中断和可延迟函数中。
当page位于低端内存,函数返回该页的虚拟地址
当page位于高端内存,建立一个永久映射,再返回地址
- kunmap: 永久映射的数量有限,应通过kunmap及时解除映射
- kmap_atomic: 临时映射
- kunmap_atomic: 解除临时映射
临时内核映射和永久内核映射相比,其最大的特点就是不会阻塞请求映射页框的进程,因此临时内核映射请求可以发生在中断和可延迟函数中。
临时映射可以在不能睡眠的地方,如中断处理程序中,因为获取映射时,绝对不会阻塞。它也禁止内核抢占,因为映射对每个处理器都是唯一的。
6、每个CPU数据
- alloc_percpu: 给系统的每个处理器分配一个指定类型对象的实例,以单字节对齐;
- free_percpu: 释放每个处理器的对象实例;
- get_cpu_var: 返回一个执行当前处理器数据的特殊实例,同时会禁止内核抢占
- put_cpu_var: 会重新**内核抢占
使用每个CPU数据好处:
- 减少了数据锁定,每个CPU访问自己CPU数据
- 大大减少缓存失效,失效往往发生在一个处理器操作某个数据,而其他处理器缓存了该数据,那么必须清理或刷新缓存。持续不断的缓存失效称为缓存抖动。
- 新的每个cpu接口
编译时期的cpu:
- 在编译时定义每个CPU变量::DEFINE PER CPU(type, name) ;
- 防范编译时警告:DECLARE PER_ CPU(type, name) ;其中type为cpu类型,name为cpu名称。
- 利用get_cpu _var()和put_ cpu _var()例程操作变量。
- 如果对获取其他处理器上的每个cpu数据,必须要给数据上锁,防止其他处理器抢占。
- 编译时每个cpu数据的例子不能在模块内使用。
运行时期的cpu:
- void *alloc_percpu(type) 一个宏
- void *_alloc_percpu() 对alloc_percpu宏的一个分装,给系统中的每个处理器分配一个指定类型对象的实例
- void free_percpu() 释放所有处理器上指定的每个CPU数据
7、使用每个CPU数据的原因
- 能够减少数据的锁定。按照每个处理器访问每个CPU的逻辑,就可以不需要任何锁。因为在这种逻辑下,本地处理器只会访问它自己的唯一数据。
- 能够大大减少缓存失效。在理想的情况下,处理器只访问自己的数据,不会对其他的处理器造成影响。percpu接口缓存一对齐所有数据,这样能够确保在访问一个处理器的数据时,不会将另一个处理器的数据带入同一个数据线上。分配函数选择
- 连续的物理页,使用低级页分配器 或kmalloc();
- 高端内存分配,使用alloc_pages(),返回page结构指针; 想获取地址指针,应使用kmap(),把高端内存映射到内核的逻辑地址空间;
- 仅仅需要虚拟地址连续页,使用vmalloc(),性能有所损失;
- 频繁创建和撤销大量数据结构,考虑建立slab高速缓存。