众所周知,物理内存的管理对于一个操作系统性能的重要性,那么著名的 Linux 是如何有效地管理起物理内存的呢。这里将作一个详尽的分析。
        内存管理最重要的两个指标莫过于:1。减少碎片,提高利用率;2. 分配和释放的速度要快。
        提到内存碎片,分为外碎片和内碎片两种。所谓的外碎片就是,当内存频繁申请和释放后,出现了很多空洞,但是它们不是连续的,当下次将要分配一块内存时,虽然空闲内存总量大于所需分配的大小,但由于这些空洞不是连续的,导致无法满足需求,这是很常见的问题;所谓内碎片就是,假设内存分配都是按照一定单位分配,比如 4K,当用户只需要 512 字节的内存大小时,由于分配了 4K 出去,导致 4K-512 的内存没有被利用,也无法再次分配,从而导致的内存的浪费。
        本文的重点伙伴系统就是为解决外碎片而实现的算法。当然它的效率也是极其高的。
        常规的物理内存管理算法无非利用位图或者链表来记录空闲内存块的状态,当需要分配时,扫描整条空闲链表,然后找到一块仅大于所需内存的块返回,这里仅大于是指,最好能找到一块刚好大于所以的内存块,那么就意味着,需要对空闲内存块进行排序,分配还好,每次释放时,查找合适的位置都要进行很多运算。这时自然能想到,对空闲内存进行散列归类,即按照一定规格大小分成多条链表,当进行分配和释放时,就可以直接找到相应的链表了,进而提升了效率。这样分配是提升了效率,但是释放时依旧很慢,因为内存释放时,如果相邻的内存也是空闲的,应该尽量合并成更大的内存,记录到更大内存的链表中,那么,如何能找到能合并的内存,就需要一个合适的算法了。伙伴系统就是这样一个算法。
        首先说一下,Linux 管理物理内存,有一个重要的数据结构,就是 mem_map,它是一个 page 类型的数组,每个元素代表一个物理页,由 32 个字节组成,记录了所有有关该物理页框的状态。这样想知道某一页框的信息就非常的方便了。如同前面所说,伙伴系统把内存大小分类为 11 种不同规格的容量,分别为 2 的 n 次方个页框的大小,这样,当所需一块内存时,很容易就确定,从哪种规格里查找空闲页。然后待解决的就是快速查找能够合并的内存块的大小。伙伴系统规定,满足以下条件的两块内存为伙伴:
        1. 两个块具有相同的大小,记作 b;
        2. 它们的物理地址是连续的;
        3. 第一块的第一个页框的物理地址是 (2 x b x 页大小)的倍数;

        前两条规定好理解,也就是两块相同大小,并且连续的内存块才有可能是伙伴,因为这样,两块合起来就可以很顺利地添加到更上一级的空闲链表中。第三条规定了,在满足前两个条件的情况下,还需要,第一块的物理地址的限定,举个例子。

 +---------------------------------------+
 | a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7 |
 +---------------------------------------+
 |   b0    |    b1   |    b2   |    b3   |
 +---------------------------------------+
 |        c0         |        c1         |
 +---------------------------------------+

        假设,a0 - a7 代表物理页,那么满足条件的伙伴为 (a0, a1), (a2, a3),(a4, a5), (a6, a7); 然而 a0 和 a1 可以合并为 b0, 以此类推,(b0, b1), (b2, b3) 也是伙伴, (c0, c1) 也是伙伴。除此之外其它两两之间均不为伙伴,如 a1 和 a2 虽然满足前两个条件,但不满足第三个条件,b1 和 b2 也是,当不为伙伴的两个块即使空闲,且连续,在伙伴系统中它们也是不能合并的。这里显然,也有少许的碎片存的,但设想,其实这样的情况是极少发生的,因为如果 a0 在使用,a3 在使用,这一般发生在 a1 是在 a3 被分配之后才被释放的,虽然此时 a2 也被释放了,a1 和 a2 无法合并,但下次再分配该级别大小的内存时,就会首先找到 a1,不会再向 a3 后面的空闲内存中寻找,这样仅有这种极少的浪费,其实也是可以被接受的。
        之所以会有第一种规定,因为这样,查找一个块的伙伴时就很容易定位到它的伙伴的大小,再加上第二条,就很容易知道,是向前还是向后查找它的伙伴,如果没有第三条,那两面两条也将没有意义,假设上图中, b2 块被释放时,再向前找与 b1 大小相同的块将没有意义,因为如果 b1 空闲,把 b2 和 b1 进行了合并,或者没有前两条的约束,让 b2 与 a3 进而合并,虽然可以合并到上一级中成为更大的块,由于 b2 抢了 b0 或者 a2 的伙伴,那么 b0 空闲时,就无数与其它块进行合并了,并且合并的块可能也不再规整,从而无法进行散列,慢慢就形成了碎片,所以 b2 只有唯一的伙伴 b3 ,它也只会等到 b3 被释放时,两块进行合并进而升级,这样,每一块都只将有唯一的伙伴,除非伙伴不被释放,不然它们总是可以合并。
        整个结构如图:

  +-------+     +------+      +------+
  |   1   |---->|  a1  |----->|  a2  |
  +-------+     +------+      +------+
  |   2   |
  +-------+     +------+
  |   4   |-----|  c1  |
  +-------+     +------+
  |  ...  |

        此时运行时的情形就很容易被分析出来了,每个级别中被挂入空闲链表的数据其实非常的少,在极端情况下,a0-a7 都被申请,但只释放了 a1, a3, a5, a7,此时才会出现空间,但在频繁的申请和释放的使用中,如果前面有空闲块,会首先被满足,所以出现空洞的实际情况比较少,当一个块被释放时,只需要简单的运算就可以确定它和它的伙伴能否合并成更大的块,以此类型,尽量地形成更大的连续空间以满足系统的需求。
        只所以说合并变的简单,看一下代码就知道了。
static inline struct page *
__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order)
{
	unsigned long buddy_idx = page_idx ^ (1 << order);

	return page + (buddy_idx - page_idx);
}
        page_idx 为页框号,即 a0 之类的序号,order 为级别,即 2 的 order 次方个页框,page_idx 与 (1 << order)做异或,那么如果 page_idx 中相应的位为 1,相当于 page_idx - 2^order,因为互为伙伴的两个块的第一个块,它的地址,肯定为 (2 x b x 页大小),b 为 2 的 order 次方,因为都以页大小为单位,所以相当于 2 x 2^order,为 2 ^ (order + 1),那么它的第 2 ^ order 位肯定为 0,如果为 1 那么说明它是第二块,此时应该找第一块,所以要减去相应级别的大小,同理,如果为 0,说明它是第 1 块,此时应找第二块,所以要加上同级别的大小。
        综上所述,伙伴系统的原理就很清楚了,由于有不同级别块大小的散列,使查找能满足块的空闲内存非常的迅速,再加上查找合并块的高效算法,使合并查找非常高效。这就是伙伴系统!

Logo

更多推荐