本文介绍内核如何给自己分配物理内存并管理。对应《深入》第8章。
在《深入》第2章“内存寻址”(或者是我博客中的这篇文章,点这里)中,已经介绍了内核如何给自己分配1G的线性地址的。但是物理内存的分配及管理恐怕更复杂而且更有必要:内存分配和释放的速度(由内存管理的算法决定)影响内核的工作效率。
首先介绍概念:
页框:通俗的说就是物理内存上的单元。请注意与“页”的区别。页有两重意思,不过最多的用法是指线性地址的单元。所以一个是实际物理内存的单元,一个是线性地址/虚拟内存的单元。在大多数情况下,页框大小等于页的大小,为4KB,使得一个页框恰好可以容纳一个页的数据。
(1)为什么要进行内存管理?
内存管理的目的总体上说无非是两点:提高时间效率和提高空间效率。时间效率也就是尽量使寻找到空闲内存、分配和释放这块内存的时间更短。空间效率就是指尽量能找到合适大小的空间,并减少内存空间浪费。
关于空间效率,举个简单的例子:切蛋糕。当我们拿到一块完整的蛋糕的话,如果想每个人都吃到完整的一块,那么我们当然不能没有计划的切块。虽然总量是不变的,但是如果随便乱切,横七竖八,势必导致最后剩下的蛋糕都是碎块,那么后面吃蛋糕的同学必然只能把小块小块的蛋糕凑成一大块来吃。势必很不爽啊。。。
(2)关键数据结构:页描述符
如果需要对一个东西进行管理,那么必须要有负责管理的数据结构,这个数据结构中有各个字段,用来提供不同的管理功能。举例子:比如为了保护内核的内存不被用户进程使用,就必须使用一个标志位;用户进程在读写内存时,首先就要检查这个标志位,然后才能读写。
所以内核使用了“页描述符”这个数据结构,页描述符的类型是page,长度为32字节(是字节哦),所有的页描述符放在一个数组mem_map中。我个人觉得好像应该叫“页框描述符”比较好。
(3)为什么要使用内存管理区?
内存本来在物理上是一个实体,并不分区的。有人会问:这样的一个整体进行处理不是很简洁么?为什么要分区呢?
但是由于以下的两种约束,我们迫不得已给自己增加了负担,将内存分区来管理。
约束一:DMA。这个名词不再解释,请自行google。在进行DMA数据传输时,DMA控制器只能对16M的内存进行寻址,所以被迫无奈,只好将这16M的物理内存固定划分给DMA了。
约束二:线性地址有限,无法直接大的物理内存。所以,超出896M的物理内存必须被区别对待。
所以,最终,物理内存被分配成3个管理区:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
(4)为什么要使用伙伴系统(Buddy system)算法?
使用伙伴系统的直接目的就是为了防止物理内存“外碎片”的产生。关于外碎片,简而言之就是虽然我们一直分配或者释放连续的内存,但是结果就是我们的内存变得很不连续,充满了大大小小的碎片或者是洞,以至于我们无法再分配足够大的一块连续的内存。(回忆下我们刚才说过的蛋糕的例子)
下面举例说明。假设我们要分配一块256个连续页框的内存。
首先考虑一种不好的分配算法作为对比,例如:将所有的空闲连续页框链在一个链表中,我们在分配空间时选取遇到的第一个大于256个连续页框(这个可能很随机,可能是1024个页框,也可能是512个,也可能恰好是256个;而且我们不能奢望在链表的开头遇到大于256个的连续页框,如果链表很长。。。Orz)。假设我们遇到了空闲的1024个页框。这时候我们面临两个选择:
(a)把这1024个页框都分配出去。这显然是一种巨大的浪费。
(b)把这1024个页框切开,将256个连续页框脱离链表,然后链表上剩下了768个连续页框的块。这其实和外碎片没有什么区别(因为链表后面很可能有更适合256的连续页框,比如257个或者258个连续页框),最终会导致这个链表被分割的乱七八糟,以至于再也找不到一块合适的连续空间。
(5)伙伴系统
所以,Linux使用了著名的伙伴系统来处理这个问题。那么下面用实例说明伙伴系统是如何改进的。最后我们再总结一下伙伴系统的思想。
伙伴系统的基础是11个连续页框链表,第一个链表上只存储所有空闲的1个页框,第二个链表上只储存所有空闲的2个连续页框,以此类推。这11个链表将不同大小的连续内存块链在不同的链表上,这首先节省了遍历一个很长的链表的时间,可是使我们直奔最符合需求的连续内存。如果是需要256个连续页框,我们可以直接找到第9个链表,取出一个即可。但又出现了几个问题:(q1)如果我们需要254个连续页框呢?(q2)如果我们已经没有了256个的连续页框,即256个连续页框的链表已经空了呢?
下面介绍如何妥善处理剩余部分,也就是解决问题q1。我们只需要254个连续页框时,我们把这个256个连续页框从链表上切除,但是只分配出去254个页框,剩余的两个连续页框链入属于它的链表,也就是存储所有两个连续页框的链表。这样我们看到,我们一个页框都没有浪费。
有人可能会问,如果需要258个连续页框怎么办(258>256)?这个问题其实与q1等价,但是此时最符合需求的是512个连续页框,但是剩下的部分只是要切割多次。但我们无需证明最后一个页框都不会浪费(因为至少最后都会链到1个页框的链表里)。q2其实与这种情况很类似,仍然要去更大的一级链表中寻找。
那么伙伴系统与开始介绍的那种不好的算法到底有什么本质区别呢?
简而言之,伙伴系统直奔最符合需求的连续内存,然后它妥善的处理了剩余部分。首先我们使用了空间换时间的算法:维护这11个块链表的开销,相对于一个好的内存分配算法根本算不上什么。其次,伙伴系统是类似“贪心”的。它寻找当前认为最好的。最后,它把分配后剩余的部分妥善的移动到它们最应该去的地方。
(6)使用的函数
分配:alloc_pages()/alloc_page()/__alloc_pages()
释放:__free_pages()/free_pages()/free_page()
==============================
下面插入两个概念,内存区(memory area)与内碎片。内存区这个词很容易让人产生误会,它不是指整个内存,而是指连续的任意长度的物理内存区。而内碎片是相对于外碎片讲的,外碎片是以页框为单位的,在页框之外;而内碎片就是在页框内部的碎片。产生内碎片的原因其实与外碎片一致:虽然我们所有的分配和释放都是连续的,但最后结果却是不连续的物理内存。
===============================
(7)为什么要使用slab分配器?
我们先不管slab是什么,先考虑为什么要使用它:它的出现就是为了解决内碎片的问题。
当然,有人肯定会想,使用伙伴系统解决内碎片不是一样么?只不过把单位从页框改为字节。
早期Linux确实是这么做的。但是遇到了一些问题(详见《深入》p.324),以我的理解就是:当分配和释放的粒度变细时,被迫需要考虑更多的因素,比如说需要考虑到数据类型。所以伙伴系统在处理内碎片时效率并不是最好的。
于是采用了具有面向对象思想(考虑到数据类型,就已经是一种面向对象的思想了)的slab系统。
(8)slab系统的思想的。。。呃 猜测
这里,限于篇幅和个人能力有限,就不详细介绍slab系统了。我的理解是:slab系统提供了一种快速查找并分配特定类型的数据(面向对象)的细粒度连续内存空间的方法。因为它使用了下图所示的这种层级结构,让我不由得联想到了数据结构中的B树(B树也是用来快速查找,减少树的层数的,当然也付出了一定的空间复杂度;当然我还不知道我这种联想合理不合理)。
(9)slab所使用的函数
分配和释放slab:kmem_cache_alloc()/kmem_cache_free()
分配和释放对象:kmalloc()/kfree()。
======================================
参考资料:
这里有一篇IBM关于slab的技术文章:Linux slab 分配器剖析。