伙伴系统是内核用来管理物理内存的一种算法(需要注意的是它是用来管理物理内存的,而不是映射后的虚拟内存),在物理内存中会除了内核和一些特殊用途的内存外,其余的空闲内存就会交给内核内存管理系统统一管理和分配。
如果没有伙伴系统就会出现内存断断续续的情况,如:
假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。
伙伴系统就是为了缓解这种碎片化(注意:是缓解!!!),它把管理起来的内存分为了不同的组,总共11组,每个组中的内存块大小都是一样的,都是2的幂次个物理页。
这十一组的大小分别是20,21,22,23……2^10,而一个内存页一般是4KB,所以大小就是4KB,8KB,16KB,32KB……4MB,内核就使用11个链表来表示11个不同大小的内存块。
在linux中我们把最小内存块称为page,大小一般为4kb,每个page都有两个状态:
1:free 表示未分配 2:allocated 表示已分配
而链表则是freelist,也就是11个freelist,也就是:
0号freeList :每个节点对应1个free page; 1号freeList :每个节点对应2个连续free page; ... n号freeList : 每个节点包含 2^n个连续free page,
链表对应的freePage是不会相交的,不会存在两个节点对应的freePage有重复的状态系统初始时,因为所有page都是free状态,所以系统会把所有page分配到这些链表里,作为初始设置。
当分配内存时,会优先从大小匹配的内存链表中查找是否有空闲的内存,当发现对应大小的内存都被使用完毕后,就会向大一倍的内存链表去寻找空闲内存。
但并不是直接去使用更大内存链表中的内存块,而是把内存块分为相同的两份,一份给请求者使用,另一份则给下一级,也就是对应内存块的上的内存链表上。
如:现在需要分配一个大小为8K内存,但是在对应内存块的内存链表上已经没有了,那么伙伴系统就会去16K内存链表上去查找一个空闲内存块,并分为两个8K的内存块。一个给申请内存块的使用,另一个则加入到8K的内存链表中去进行管理。当然如果16K的也没有空闲内存块了,就会去更高一级的32KB中去查询有没有空间的内存块,会把32K分成一个16K和两个8K,16K放到16K的内存块中管理,两个8K的,一个返回给申请者,一个8K的放到8K的内存块中管理。
在Linux中可以通过 echo m > /proc/sysrq-trigger来观察buddy状态和/proc/buddyinfo也可以观察到。
每一个区域都有一个struct free_area用来表示该区域的page block,在include/linux/mmzone.h;
在zone中实例化
struct zone {
……
struct free_area free_area[MAX_ORDER];
……
}
而MAX_ORDER值为11,正好对应了11个freelist
当释放内存时,会扫描对应大小的内存块链表,查看是否存在地址能够连续在一起的内存块,如果发现有,那么就合并两个内存块放置到更大一级的内存块链表上,以此类推。比如我们释放8KB大小的内存,那么会从对应的链表扫描是否有能够合并的内存块,如果有另一个8KB大小的内存和我们使用的内存地址连续,那么就合并它们组成一个16KB大小的内存块,然后接着扫描16KB大小的内存块链表,继续查找合并的可能,以此类推下去。
四. 优缺点优点:
较好的解决了外部碎片问题
只要申请的块不超过1024个页面(4M),内核就尽量分配连续的页面
针对大内存分配设计
缺点:
合并的要求太过严格,只能是满足伙伴关系的块才能合并。一个很小的块往往会阻碍一个大块的合并,一个系统中,对内存块的分配,大小是随机的,一片内存中仅一个小的内存块没有释放,旁边两个大的就不能合并
浪费问题:伙伴系统是按2的幂次方大小进行分配内存块,如果所需内存大小不是2的幂次方,就会有部分页面浪费,有时候还很严重。
拆分和合并开销很大



