内核空间内存管理

页:内核中内存管理的基本单位

尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU),把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。

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;需要动态映射这些内存。

区:内核把页划分在不同的区

由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存特定的物理地址上,所以不能将其用一些特定的任务。由于存在这种限制,所以内核根据页的相似特性把页划分为不同的区

  • ZONE_DMA——页能用DMA操作
  • ZONE_NORMAL——都是正常映射的页
  • ZONE_HIGHMEM——高端内存,并不能永久的映射到内核地址空间,物理内存不一定连续
  • 内核空间内存管理
    内核空间内存管理

如何分配和释放内存

页分配与释放

内核空间内存管理
内核空间内存管理

以字节为单位的分配与释放

内核空间内存管理
• kmalloc()内存分配最终总是调用__get_free_pages 来进行实际的分配
• kzalloc()先用 kmalloc() 申请空间, 再用memset()来初始化,所有申请的元素都被初始化为0。
• vmalloc()返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的,

伙伴系统

内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下,避免碎片的产生。
首先了解两个概念:
内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。
外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。
然后采用这样的处理方式:
底层把所有的空闲页框分为11个块链表,每个块链表中的结点分别是大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大的页框块包含1024个连续页框,对应4MB大小的连续内存。假设要申请一个256个页框的块,则先从结点为256个连续页框块的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找—分割—分配和转移。如果仍然没有,则返回错误。使用过的页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块,然后作为结点插入相应规格的链表中,如下图所示:
内核空间内存管理

slab分配器

伙伴系统分配内存时是基于页框为单位的,比较大。如果是几十个字节的小内存分配怎么办呢?此时就需要用SLAB机制。slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。也就是说:在内存中维护一个slab列表,列表项对应这各种数据结构大小的内存块;当有小对象申请内存时,直接从slab列表中找到对象类型的列表项,把相应大小的内存分配出去;对象用完后,释放掉对象并把对象所占的内存块归还到slab列表以供下一个同类型的对象使用。

slab分配器有以下三个基本目标:

• 1.减少伙伴算法在分配小块连续内存时所产生的内部碎片;
• 2.将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销。
• 3.通过着色技术调整对象以更好的使用硬件高速缓存;

普通高速缓存
slab分配器中kmem_cache是用来描述高速缓存的结构,因此它本身也需要slab分配器对其进行高速缓存。cache_cache变量保存着对高速缓存描述符的高速缓存。
专用高速缓存
内核为专用高速缓存的申请和释放提供了一套完整的接口,根据所传入的参数为具体的对象分配slab缓存。
• kmem_cache_create:创建高速缓存
• kmem_cache_destroy: 撤销高速缓存
• kmem_cache_alloc: 从高速缓存中返回一个指向对象的指针
• kmem_cache_free:释放一个对象

栈的静态分配

单页内核栈:每个进程的内核栈只有一页大小,这取决于编译时配置选项。
所解决的问题:
• 可以减少每个进程内存的消耗;
• 随着机器运行时间的增加,寻找两个未分配的、连续的页越来越困难,物理内存碎片化不断加重,那么给每个新进程分配虚拟内存的压力也增大;
• 每个进程的调用链在自己的内核栈中,当单页栈选项被**时,中断处理程序可获得自己的栈
任意函数必须尽量节省栈资源, 方法就是所有函数让局部变量所占空间之和不要超过几百字节。如果栈溢出,可能会对连着内核栈末端的thread_info结构产生很大的影响。

高端内存的映射

高端内存中的页不能永久地映射到内核地址空间。
• kmap:把给定page结构映射到内核地址空间;
这个函数可以睡眠,因此kmap()只能用在进程上下文中,不能用于中断上下文中。需要注意一点的是,当永久内核映射区没有空闲的页表项可供映射时,请求映射的进程会被阻塞,因此永久内核映射请求不能发生在中断和可延迟函数中。
当page位于低端内存,函数返回该页的虚拟地址
当page位于高端内存,建立一个永久映射,再返回地址
• kunmap: 永久映射的数量有限,应通过kunmap及时解除映射
• kmap_atomic: 临时映射
• kunmap_atomic: 解除临时映射
临时内核映射和永久内核映射相比,其最大的特点就是不会阻塞请求映射页框的进程,因此临时内核映射请求可以发生在中断和可延迟函数中。
临时映射可以在不能睡眠的地方,如中断处理程序中,因为获取映射时,绝对不会阻塞。它也禁止内核抢占,因为映射对每个处理器都是唯一的。