在计算机系统的复杂架构中,内存就如同人体的血液,源源不断地为各个程序和进程输送着数据与指令,支撑着整个系统的高效运转。当我们在电脑上同时打开多个应用程序,或是运行大型游戏、进行复杂的数据处理时,内存的高效管理便成为了决定系统性能优劣的关键因素。
在 Linux 操作系统的庞大体系中,内核内存管理如同坚实的基石,支撑着整个系统的稳定运行。而在众多内存管理技术中,伙伴算法以其独特的魅力和强大的功能,成为了高效内存管理的一把利器。当我们深入探索 Linux 内核的奥秘时,伙伴算法就像是一位默默无闻却又无比可靠的守护者,精心地调配着内存资源,确保每一个程序都能在恰当的时机获得所需的内存空间,同时又能最大限度地减少内存碎片,提高内存的利用率。
那么,究竟什么是伙伴算法?它是如何在复杂的内核环境中发挥关键作用的呢?接下来,就让我们一同开启这场关于 Linux 内核内存伙伴算法的精彩之旅。
一、伙伴算法简介
伙伴算法,简单来说,是一种在操作系统内存管理中广泛应用的动态存储管理算法 。它如同一位精打细算的管家,对内存资源进行着巧妙的分配与回收。在 Linux 系统中,内存被划分成一个个大小固定的页框,而伙伴算法就围绕着这些页框展开工作。它将所有的空闲页框分组为 11 个块链表,每块链表分别包含大小为 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页框的页框块。这些不同大小的块链表,就像是一个个不同规格的 “资源仓库”,等待着被合理调配。
在Linux系统中,内存的分配与回收速率直接影响系统的存取效率。当内核频繁请求和释放不同大小的一组连续页框时,会导致许多外部空闲碎片,造成空间的浪费。使用伙伴算法可以有效地缓解该问题。伙伴关系机制是操作系统中的一种动态存储管理算法。在进行内存分配时,该算法通过不断平分较大的空闲内存块来获得较小的空闲内存块,直到获得所需要的内存块;在进行内存回收时,该算法尽可能地合并空闲块。
内存管理是应用程序通过硬件和软件协作访问内存的一种方法,当进程请求内存使用时,它给进程分配可用的内存;当进程释放内存时,回收相应的内存,同时负责跟踪系统中内存的使用状态。
在Linux系统中,首先将内存分为若干个节点,然后每个节点又可以分为1-3个区,每个区下又有若干个页。页是内存管理的基本单元。
当前存在的问题
当系统工作时,CPU最先访问的地址不是物理内存中的实地址,而是虚拟地址空间的虚地址。当请求分页时,首先在虚拟地址空间中分配一个虚拟空间,然后根据需要为此区间分配相应的物理页面并建立映射。
在分配空间时,我们首先想到的便是malloc函数。由于在实际情况中,操作系统必须能够在任意时刻申请和释放任意大小的内存,该函数的实现并不容易,导致的主要问题有延时问题和碎片问题。
延时问题指的是系统查找到可分配单元的时间变长,例如程序请求分配一个64KB的内存空间,系统查看64KB空间发现不全是空余的,于是查看65KB的空间,发现仍不能满足需求,直到查看80KB空间时,才满足了需求,这种方式请求次数多达17次,频繁操作时,非常耗时。
若系统以较大的定长空间来分配内存,在一定程度上可以节省时间,但带来的是碎片过多问题,由于每次用较大的空间进行分配,系统中出现大量碎片,导致内存浪费。严重者会导致内存无法完成分配,虽然仍有许多碎片空间。
基于此,系统需要一种能够高效分配内存,同时又能减少产生碎片的算法,伙伴算法能有效地解决该问题,如今已成为操作系统中的一种基础算法。
二、伙伴算法核心原理
2.1伙伴关系的奥秘
在伙伴算法的世界里,伙伴关系是其核心概念,如同连接各个内存块的纽带,贯穿于整个内存管理的过程。所谓伙伴关系,是指在内存分配过程中,当一个较大的内存块被分割时,由同一个大块内存分裂出来的两个大小相等的小块内存,它们之间就构成了伙伴关系 。这就好比一对双胞胎,它们在内存的 “家族” 中有着紧密的联系。
伙伴关系需要满足三个关键条件:一是两个内存块必须具有相同的大小,这是伙伴关系的基础,就像天平的两端,只有重量相等才能保持平衡;二是它们的物理地址是连续的,如同相邻的两座房子,紧密相连;三是这两个块必须是从同一个大块中分离出来的,这体现了它们的 “血缘关系”。例如,假设系统中有一个大小为 8 个页框的内存块,当它被分割成两个大小为 4 个页框的内存块时,这两个 4 页框的内存块就互为伙伴。它们大小相同,物理地址连续,且都来自于最初的 8 页框内存块,完全符合伙伴关系的定义。
伙伴关系在内存分配和回收过程中起着举足轻重的作用。在分配内存时,当系统找不到与请求大小完全匹配的空闲内存块时,就会从更大的内存块中进行分割,产生的伙伴内存块,一部分用于满足当前的分配需求,另一部分则作为空闲内存块保留下来,以备后续分配。
而在内存回收时,伙伴关系则成为了合并空闲内存块的关键依据。当一个内存块被释放时,系统会首先检查其伙伴内存块是否也处于空闲状态。如果伙伴内存块空闲,那么这两个伙伴就会合并成一个更大的内存块,重新回到空闲内存块的链表中。这样一来,通过伙伴关系的巧妙运用,系统能够有效地减少内存碎片的产生,提高内存的利用率,就像一位心灵手巧的裁缝,将零碎的布料巧妙地拼接起来,使其发挥更大的作用。
2.2分配过程的深度剖析
当系统中的进程发出内存分配请求时,伙伴算法便开始了它的高效运作。其分配过程可以分为以下几个关键步骤:
首先,伙伴算法会根据请求的内存大小,确定所需内存块的阶数。在伙伴算法中,内存块的大小是以 2 的幂次方来表示的,例如 1 页、2 页、4 页、8 页等,每一个不同的大小对应着不同的阶数。假设请求分配的内存大小为 n 页,那么系统会通过计算找到满足 2^k >= n 的最小 k 值,这个 k 值对应的阶数就是所需内存块的阶数。
接下来,系统会在相应阶数的空闲内存块链表中进行查找。如果该链表中有空闲的内存块,那么直接从链表中取出一个内存块分配给请求者,分配过程就此完成。例如,当请求分配 4 页内存时,系统会先确定其阶数为 2(因为 2^2 = 4),然后在阶数为 2 的空闲内存块链表中查找。若链表中有空闲块,就可以直接将其分配给进程。
然而,当相应阶数的空闲内存块链表为空时,分配过程就会变得复杂一些。此时,系统会向上查找更大阶数的空闲内存块链表。找到一个更大的空闲内存块后,将其分割成两个大小相等的子块,这两个子块就成为了伙伴关系。其中一个子块的大小与请求的内存大小相同,将其分配给请求者;另一个子块则插入到对应的空闲内存块链表中,以备后续分配使用。例如,当请求分配 4 页内存,但阶数为 2 的链表为空时,系统会查找阶数为 3(2^3 = 8)的链表。若找到一个 8 页的空闲内存块,就将其分割成两个 4 页的子块,一个分配给请求进程,另一个插入到阶数为 2 的空闲内存块链表中。
如果一直向上查找,直到最大阶数的空闲内存块链表都没有找到合适的内存块,那么分配过程就会失败,系统会返回错误信息,告知请求者内存分配失败。
为了更直观地理解,我们来看一个具体的例子。假设系统的内存被划分为多个页框,当前有一个进程请求分配 8 页内存。系统首先确定所需内存块的阶数为 3(2^3 = 8),然后在阶数为 3 的空闲内存块链表中查找。发现该链表为空,于是向上查找阶数为 4(2^4 = 16)的链表。在阶数为 4 的链表中找到一个 16 页的空闲内存块,将其分割成两个 8 页的子块,一个子块分配给请求进程,另一个子块插入到阶数为 3 的空闲内存块链表中,至此,分配过程完成。通过这个例子,我们可以清晰地看到伙伴算法在内存分配过程中的具体操作步骤和逻辑。
下面通过一个简单的例子来说明该算法分配过程:
假设要请求一个256(129~256)个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两等分,一般用作满足需求,另一半则插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果这样的块存在,内核就把1024个页框块的256个页框用作请求,然后剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错误信号。相关数据结构:
#define MAX_ORDER 11
struct zone {
……
struct free_area free_area[MAX_ORDER];
……
}
struct free_area {
struct list_head free_list;
unsigned long nr_free;//该组类别块空闲的个数
};
Zone结构体中的free_area数组的第k个元素,它保存了所有连续大小为2^k的空闲块,具体是通过将连续页的第一个页插入到free_list中实现的,连续页的第一个页的页描述符的private字段表明改部分连续页属于哪一个order链表。
(1)伙伴算法系统初始化
Linux内核启动时,伙伴算法还不可用,linux是通过bootmem来管理内存,在mem_init中会把bootmem位图中空闲的内存块插入到伙伴算法系统的free_list中。
调用流程如下:
mem_init----->__free_all_bootmem()—>free_all_bootmem()>free_all_bootmem_core(NODE_DATA(0))–>free_all_bootmem_core(pgdat)
//利用free_page 将页面分给伙伴管理器
free_all_bootmem
return(free_all_bootmem_core(NODE_DATA(0))); //#define NODE_DATA(nid) (&contig_page_data)
bootmem_data_t *bdata = pgdat->bdata;
page = virt_to_page(phys_to_virt(bdata->node_boot_start));
idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
map = bdata->node_bootmem_map;
for (i = 0; i < idx; )
unsigned long v = ~map[i / BITS_PER_LONG];
//如果32个页都是空闲的
if (gofast && v == ~0UL)
count += BITS_PER_LONG;
__ClearPageReserved(page);
order = ffs(BITS_PER_LONG) - 1;
//设置32个页的引用计数为1
set_page_refs(page, order)
//一次性释放32个页到空闲链表
__free_pages(page, order);
__free_pages_ok(page, order);
list_add(&page->lru, &list);
//page_zone定义如下return zone_table[page->flags >> NODEZONE_SHIFT];
//接收一个页描述符的地址作为它的参数,它读取页描述符的flags字段的高位,并通过zone_table数组来确定相应管理区描述符的地址,最终将页框回收到对应的管理区中
free_pages_bulk(page_zone(page), 1, &list, order);
i += BITS_PER_LONG;
page += BITS_PER_LONG;
//这32个页中,只有部分是空闲的
else if (v)
for (m = 1; m && i < idx; m<<=1, page++, i++)
if (v & m)
count++;
__ClearPageReserved(page);
set_page_refs(page, 0);
//释放单个页
__free_page(page);
else
i+=BITS_PER_LONG;
page += BITS_PER_LONG;
//释放内存分配位图本身
page = virt_to_page(bdata->node_bootmem_map);
for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++)
__ClearPageReserved(page);
set_page_count(page, 1);
__free_page(page);
(2)伙伴算法系统分配空间
page = __rmqueue(zone, order);
//从所请求的order开始,扫描每个可用块链表进行循环搜索。
for (current_order = order; current_order < MAX_ORDER; ++current_order)
area = zone->free_area + current_order;
if (list_empty(&area->free_list))
continue;
page = list_entry(area->free_list.next, struct page, lru);
//首先在空闲块链表中删除第一个页框描述符。
list_del(&page->lru);
//清楚第一个页框描述符的private字段,该字段表示连续页框属于哪一个大小的链表
rmv_page_order(page);
area->nr_free--;
zone->free_pages -= 1UL << order;
//如果是从更大的order链表中申请的,则剩下的要重新插入到链表中
return expand(zone, page, order, current_order, area);
unsigned long size = 1 << high;
while (high > low)
area--;
high--;
size >>= 1;
//该部分连续页面插入到对应的free_list中
list_add(&page[size].lru, &area->free_list);
area->nr_free++;
//设置该部分连续页面的order
set_page_order(&page[size], high);
page->private = order;
__SetPagePrivate(page);
__set_bit(PG_private, &(page)->flags)
return page;
(3)伙伴算法系统回收空间
free_pages_bulk
//linux内核将空间分为三个区,分别是ZONE_DMA、ZONE_NORMAL、ZONR_HIGH,zone_mem_map字段就是指向该区域第一个页描述符
struct page *base = zone->zone_mem_map;
while (!list_empty(list) && count--)
page = list_entry(list->prev, struct page, lru);
list_del(&page->lru);
__free_pages_bulk
int order_size = 1 << order;
//该段空间的第一个页的下标
page_idx = page - base;
zone->free_pages += order_size;
//最多循环10 - order次。每次都将一个块和它的伙伴进行合并。
while (order < MAX_ORDER-1)
//寻找伙伴,如果page_idx=128,order=4,则buddy_idx=144
buddy_idx = (page_idx ^ (1 << order));
buddy = base + buddy_idx;
/**
* 判断伙伴块是否是大小为order的空闲页框的第一个页。
* 首先,伙伴的第一个页必须是空闲的(_count == -1)
* 同时,必须属于动态内存(PG_reserved被清0,PG_reserved为1表示留给内核或者没有使用)
* 最后,其private字段必须是order
*/
if (!page_is_buddy(buddy, order))
break;
list_del(&buddy->lru);
area = zone->free_area + order;
//原先所在的区域空闲页减少
area->nr_free--;
rmv_page_order(buddy);
__ClearPagePrivate(page);
page->private = 0;
page_idx &= buddy_idx;
order++;
/**
* 伙伴不能与当前块合并。
* 将块插入适当的链表,并以块大小的order更新第一个页框的private字段。
*/
coalesced = base + page_idx;
set_page_order(coalesced, order);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;
2.3回收过程的全面解读
当进程使用完内存并将其释放时,伙伴算法的回收机制便开始发挥作用。内存回收过程是分配过程的逆过程,同样遵循着一定的规则和步骤:
- 首先,当一个内存块被释放时,系统会根据该内存块的大小确定其所属的阶数,然后将其插入到对应阶数的空闲内存块链表中。例如,释放一个大小为 4 页的内存块,系统会确定其阶数为 2,然后将该内存块插入到阶数为 2 的空闲内存块链表中。
- 接着,系统会检查刚插入的内存块的伙伴是否也在空闲链表中。如果伙伴内存块也处于空闲状态,那么就将这两个伙伴内存块合并成一个更大的内存块。合并后的内存块的阶数会增加 1,然后将其插入到新阶数对应的空闲内存块链表中。例如,释放的 4 页内存块在阶数为 2 的链表中找到了伙伴,它们合并成一个 8 页的内存块,阶数变为 3,然后将这个 8 页的内存块插入到阶数为 3 的空闲内存块链表中。
这个合并过程会持续进行,系统会不断检查合并后的内存块在更大阶数的链表中是否还有伙伴,直到不能合并或者已经合并至最大块为止。例如,合并后的 8 页内存块在阶数为 3 的链表中又找到了伙伴,它们再次合并成一个 16 页的内存块,阶数变为 4,然后将这个 16 页的内存块插入到阶数为 4 的空闲内存块链表中。如果在某个阶数的链表中没有找到伙伴,那么合并过程就会停止。
为了更好地理解回收过程,我们来看一个具体的实例:
假设系统中有一个进程释放了一个大小为 16 页的内存块,其阶数为 4。系统将这个 16 页的内存块插入到阶数为 4 的空闲内存块链表中。此时,检查发现其伙伴也在空闲链表中,于是将这两个 16 页的内存块合并成一个 32 页的内存块,阶数变为 5,然后将这个 32 页的内存块插入到阶数为 5 的空闲内存块链表中。接着,在阶数为 5 的链表中检查,发现没有伙伴,合并过程停止。通过这个实例,我们可以清楚地看到伙伴算法在内存回收过程中的详细操作流程,以及如何通过伙伴合并来减少内存碎片,提高内存的利用率。
三、伙伴算法的实现与数据结构
3.1关键数据结构解析
在伙伴算法的实现中,涉及到几个关键的数据结构,它们相互协作,共同完成内存的分配与回收任务。
struct zone 结构体是内存管理区的重要描述结构 ,它包含了系统中不同类型内存区域的相关信息。在 Linux 系统中,内存被划分为多个不同的区域,如 DMA 区域、Normal 区域和 HighMem 区域等,每个区域都有其独特的用途和特点。struct zone 结构体中的 free_area 数组是伙伴算法的核心数据结构之一,它保存了所有连续大小为 2^k 的空闲块,其中 k 表示分配阶数 。通过这个数组,系统可以快速地找到不同大小的空闲内存块,为内存分配和回收提供了便利。
struct free_area 结构体则是对空闲内存块的具体描述。它包含一个 free_list 链表,用于将相同大小的空闲内存块组织在一起,方便管理和查找。每个空闲内存块都通过其起始页框的 lru 域连接到 free_list 链表中,形成一个双向链表结构 。这样,在进行内存分配和回收时,系统可以通过遍历链表快速地找到合适的空闲内存块。nr_free 字段记录了该 free_area 中总共的空闲内存块的数量,通过这个字段,系统可以快速了解当前空闲内存的总量,以便进行合理的内存分配决策。
struct page 结构体是页描述符,用于描述系统中的每一个物理页。在伙伴算法中,每个内存块都是由多个连续的物理页组成,而 struct page 结构体则为这些物理页提供了详细的描述信息。lru 域作为链表节点,用于将物理页连接到 free_list 链表中,实现内存块的组织和管理。private 字段在伙伴算法中也有着重要的作用,它用于保存物理页所在内存块的分配阶数等相关信息,为内存分配和回收过程中的各种操作提供了必要的依据。
这些关键数据结构之间存在着紧密的联系。struct zone 结构体通过 free_area 数组与 struct free_area 结构体相连,从而管理不同大小的空闲内存块。而 struct free_area 结构体又通过 free_list 链表将相同大小的空闲内存块组织在一起,每个内存块的起始页框通过 lru 域连接到链表中,实现了内存块的有效管理。struct page 结构体则作为内存块的基本组成单位,通过其各个字段与其他数据结构相互关联,共同完成内存的分配与回收任务。这种紧密的联系和协作,使得伙伴算法能够高效地管理内存资源,确保系统的稳定运行。
3.2系统初始化流程
在 Linux 内核启动时,伙伴算法的初始化是一个至关重要的过程,它为后续的内存管理工作奠定了坚实的基础。初始化过程主要包括内存块的初始化和链表的构建等步骤。
在系统启动的早期阶段,内存是通过 bootmem 分配器进行管理的。当系统进入到 mem_init 阶段时,伙伴算法开始接管内存管理工作 。此时,系统会将 bootmem 位图中空闲的内存块插入到伙伴算法系统的 free_list 链表中,实现内存管理的交接。
具体的调用流程如下:从 start_kernel 函数开始,经过一系列的调用,最终到达 mem_init 函数。在 mem_init 函数中,会调用 __free_all_bootmem 函数,该函数进一步调用 free_all_bootmem 函数,然后通过 free_all_bootmem_core 函数,利用 free_page 函数将页面分给伙伴管理器 。在这个过程中,系统会遍历 bootmem_data_t 结构体,获取每次返回的页数,并将这些空闲内存块按照伙伴算法的规则插入到相应的 free_list 链表中。
除了插入空闲内存块,系统还会对 free_area 数组中的各个元素进行初始化。在 free_area_init_core 函数中,会对每个 free_area 结构体的 free_list 链表进行初始化,将其设置为空链表,并将 nr_free 字段初始化为 0 。这样,在初始化完成后,free_area 数组中的每个元素都处于初始状态,等待着后续的内存分配和回收操作。
在初始化过程中,系统还会对每个 pageblock 的起始页框对应的 struct zone 中的 pageblock_flags 代表的位图相关区域进行标记,将其标记为可移动的,表示该 pageblock 为可移动的 。这一操作在内核初始化伙伴系统时非常重要,它为后续的内存管理提供了重要的信息,确保系统能够正确地处理不同类型的内存块。通过这些初始化步骤,伙伴算法在 Linux 内核启动时完成了自身的准备工作,为系统的内存管理提供了可靠的支持。
3.3分配与回收的代码实现
以 Linux 内核代码为例,我们来深入分析伙伴算法内存分配和回收的具体代码实现,从而更直观地了解算法的实际运行机制。
在内存分配方面,内核中使用 alloc_pages 系列函数来实现基于伙伴算法的页框分配 。这些函数最终都会调用伙伴算法的入口函数 buffered_rmqueue 。在 buffered_rmqueue 函数中,首先会判断分配阶是否为 0 。如果分配阶为 0,说明请求的是单个页框,此时会启用 per-CPU 机制来分配物理内存,以提高分配效率。因为对于单个页框的分配,per-CPU 机制可以直接从每个 CPU 的本地缓存中获取空闲页框,避免了全局搜索的开销。如果分配阶不为 0,则调用 __rmqueue 函数进行内存分配。
__rmqueue 函数是内存分配的核心函数之一,它首先会调用 __rmqueue_smallest 函数,尝试从当前指定的分配阶到最高分配阶依次进行遍历 。在每次遍历的分配阶链表中,根据参数 migratetype 选择正确的迁移队列。如果在指定的迁移类型上分配失败后,再选用其他备用的迁移列表进行内存分配,该过程通过 __rmqueue_fallback 函数完成。通过这种方式,内核总是在竭尽全力保证满足分配内存的请求,确保系统的正常运行。
当在 __rmqueue_smallest 函数中选定一个页框块链表后,只要该链表不为空,就说明可以分配该分配阶对应的页框块 。此时,会通过 list_entry 函数将该页框块从链表上移除,然后将页框块首页框的 PG_buddy 标志删除,这标志着当前页框块已经不属于伙伴链表。并且将该首页框描述符中的 private 置 0,该字段中本来保存的是其所处页框块的分配阶。以上这个过程通过 rmv_page_order 函数完成。此外,还要更新页框块链表 nr_free 的值,以反映当前空闲内存块的数量变化。
在内存回收方面,当一个内存块被释放时,会调用 __free_pages 函数 。该函数首先会根据释放内存块的大小确定其所属的阶数,然后将其插入到对应阶数的空闲内存块链表中。接着,系统会检查刚插入的内存块的伙伴是否也在空闲链表中。如果伙伴内存块也处于空闲状态,那么就将这两个伙伴内存块合并成一个更大的内存块。合并后的内存块的阶数会增加 1,然后将其插入到新阶数对应的空闲内存块链表中。这个合并过程会持续进行,直到不能合并或者已经合并至最大块为止。在合并过程中,会调用 merge_free_area 等函数来完成内存块的合并和链表的调整操作,确保空闲内存块的有效管理和利用。
四、伙伴算法场景及优缺点
4.1伙伴算法的用途
管理物理内存,解决外碎片问题。
4.2满足以下条件的两个块称为伙伴
- 两个块具有相同的大小,记作b
- 它们的物理地址是连续的
- 第一块的第一个页框的物理地址是2*b*2^12的倍数
4.3伙伴算法管理结构
伙伴算法把物理内存分为11个组,第0、1、...10组分别包含2^0、2^1、...2^10个连续物理页面的内存。在zone结构中,有一个free_area数组,数组的每一个成员代表一个组,相关定义如下:
#define MAX_ORDER 11
struct zone {
...
struct free_area free_area[MAX_ORDER];
...
}
struct free_area {
struct list_head free_list;
/*该组类别块空闲的个数*/
unsigned long nr_free;
};
4.4伙伴算法的初始化和释放
(1)伙伴算法初始化过程
在start_kernel->mem_init-> free_all_bootmem_node->free_all_bootmem_core-> __free_pages_bootmem-> __free_pages->__free_pages_ok->free_one_page-> __free_one_page函数中,通过对每一个页面进行释放,从而完成对伙伴算法的初始化工作。
(2)伴算法的具体释放过程
伙伴算法释放的思想:当释放2^order页大小内存时,查看它的伙伴是否空闲,如果空闲就将伙伴从该组链表中删除,并且将这两个空闲的伙伴内存区域合并成一个更高阶的空闲内存区域,依次这样操作下去。
_free_one_page函数分析如下:
static inline void __free_one_page(struct page *page,
struct zone *zone, unsigned int order)
{
unsigned long page_idx;
int order_size = 1 << order;
int migratetype = get_pageblock_migratetype(page);
/*用PFN作为mem_map数组下标就可以索引到对应的page结构*/
page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
__mod_zone_page_state(zone, NR_FREE_PAGES, order_size);
/*这个循环主要查看当前释放块伙伴是否空闲,如果空闲则合并它们*/
while (order < MAX_ORDER-1) {
unsigned long combined_idx;
struct page *buddy;
/*找到释放块的伙伴*/
buddy = __page_find_buddy(page, page_idx, order);
/*判断释放块的伙伴是否空闲*/
if (!page_is_buddy(page, buddy, order))
break;
list_del(&buddy->lru);
zone->free_area[order].nr_free--;
rmv_page_order(buddy);
combined_idx = __find_combined_index(page_idx, order);
page = page + (combined_idx - page_idx);
page_idx = combined_idx;
order++;
}
set_page_order(page, order);
list_add(&page->lru,
&zone->free_area[order].free_list[migratetype]);
zone->free_area[order].nr_free++;
}
4.5伙伴算法优缺点
(1)显著优势
伙伴算法在 Linux 内存管理中展现出了诸多显著优势,这些优势使其成为内存管理的核心算法之一,为系统的高效稳定运行提供了有力保障。
首先,伙伴算法在减少内存碎片方面表现出色 。在传统的内存分配方式中,频繁的内存分配和释放操作容易导致内存碎片化,使得内存空间被分割成许多小块,这些小块可能因为太小而无法满足后续的内存分配需求,从而造成内存资源的浪费。而伙伴算法通过将内存块按照 2 的幂次方大小进行组织和管理,有效地减少了外部碎片的产生。在内存分配时,它总是从最接近请求大小的内存块中进行分配,如果没有完全匹配的内存块,就会从更大的内存块中进行分割,产生的伙伴内存块会被合理地管理和利用,避免了内存空间的碎片化。在内存回收时,通过伙伴合并机制,能够将相邻的空闲内存块合并成更大的内存块,进一步减少了内存碎片的数量,提高了内存的连续性。
其次,伙伴算法大大提高了内存利用率 。由于其合理的内存分配和回收策略,使得内存资源能够得到充分的利用。它能够根据进程的实际需求,精确地分配合适大小的内存块,避免了内存的过度分配和浪费。在分配内存时,它会尽量选择最接近请求大小的内存块进行分配,减少了内部碎片的产生。当进程释放内存时,通过伙伴合并机制,能够将空闲内存块合并成更大的内存块,使得这些内存块能够更好地满足后续的内存分配需求,从而提高了内存的整体利用率。
此外,伙伴算法还具有高效的分配和回收效率 。在内存分配过程中,通过对空闲内存块链表的快速查找和分割操作,能够迅速地找到合适的内存块并进行分配,大大缩短了内存分配的时间。在内存回收过程中,通过伙伴合并机制,能够快速地将空闲内存块合并成更大的内存块,并将其插入到相应的空闲内存块链表中,提高了内存回收的效率。这种高效的分配和回收机制,使得系统能够快速地响应进程的内存请求,提高了系统的整体性能。
(2)固有缺陷
尽管伙伴算法在内存管理中具有诸多优势,但它也并非完美无缺,存在一些固有的局限性。
首先,伙伴算法存在内部碎片问题 。由于伙伴算法按照 2 的幂次方大小来分配内存块,当请求的内存大小不是 2 的幂次方时,分配的内存块往往会比实际需求大,从而产生内部碎片。如果一个进程需要 10 字节的内存,而伙伴算法只能分配 16 字节的内存块,那么就会产生 6 字节的内部碎片。这些内部碎片虽然不会像外部碎片那样导致内存无法分配,但也会造成内存空间的浪费,降低了内存的实际利用率。
其次,伙伴算法的分配粒度受到限制 。它只能分配大小为 2 的幂次方的内存块,这在一定程度上限制了其灵活性。对于一些对内存大小要求非常精确的应用场景,伙伴算法可能无法提供最合适的内存块,从而影响应用的性能。在一些嵌入式系统中,由于内存资源非常有限,对内存的分配粒度要求非常高,伙伴算法的这种限制就可能会带来一些问题。
此外,伙伴算法在内存块合并时也存在一定的限制 。在回收内存时,只有当两个伙伴内存块都处于空闲状态时才能进行合并,如果其中一个伙伴内存块被占用,那么就无法进行合并,这可能会导致内存碎片的产生。一个大小为 8 页的内存块被释放,但其伙伴内存块被占用,那么这个 8 页的内存块就无法与伙伴合并成 16 页的内存块,只能以 8 页的空闲内存块的形式存在,这就可能会影响后续的内存分配效率。
五、伙伴算法的应用场景及优化策略
5.1应用场景
伙伴算法在 Linux 内核中有着广泛的应用场景,它为系统的高效运行提供了有力支持。在进程内存分配方面,伙伴算法发挥着关键作用。当一个新的进程被创建时,它需要申请一定的内存空间来存储其代码、数据和堆栈等信息。伙伴算法能够根据进程的需求,快速地分配合适大小的内存块,确保进程能够顺利启动和运行。在多进程并发运行的环境中,各个进程可能会随时申请或释放内存,伙伴算法能够高效地处理这些请求,保证内存资源的合理分配和利用,避免出现内存分配失败或内存碎片过多的情况,从而维持系统的稳定运行。
在设备驱动程序中,伙伴算法也有着不可或缺的应用。设备驱动程序需要与硬件设备进行交互,而这些交互往往需要大量的内存来存储设备数据、控制信息等。例如,在网络设备驱动中,需要分配内存来缓存网络数据包;在磁盘设备驱动中,需要分配内存来存储磁盘读写的数据。伙伴算法能够为设备驱动程序提供高效的内存分配服务,确保设备能够正常工作。当网络设备接收到大量的数据包时,伙伴算法能够迅速分配足够的内存来缓存这些数据包,保证网络通信的顺畅;当磁盘设备进行读写操作时,伙伴算法能够及时分配内存来存储数据,提高磁盘的读写效率。
在内存映射文件系统中,伙伴算法同样发挥着重要作用。内存映射文件系统允许将文件直接映射到内存中,使得对文件的访问就像对内存的访问一样高效。在这种情况下,伙伴算法负责分配内存来存储映射的文件数据。当用户打开一个大文件并进行频繁的读写操作时,内存映射文件系统通过伙伴算法分配内存,将文件数据映射到内存中,用户可以直接在内存中对文件进行操作,大大提高了文件访问的速度。同时,当文件操作完成后,伙伴算法又能及时回收内存,避免内存资源的浪费。
5.2优化策略
尽管伙伴算法在内存管理中表现出色,但为了进一步提升其性能,针对其局限性,研究者们提出了一系列优化策略。在改进分配策略方面,一种常见的优化方法是引入自适应分配策略 。传统的伙伴算法按照固定的 2 的幂次方大小来分配内存块,容易产生内部碎片。而自适应分配策略则可以根据实际请求的内存大小,更加灵活地选择分配的内存块。当请求的内存大小接近某个 2 的幂次方时,可以尝试分配稍大的内存块,但通过一定的机制来减少内部碎片的产生。可以对分配的内存块进行标记,记录其实际使用的内存大小,当该内存块被释放时,根据标记信息进行更加合理的合并操作,提高内存的利用率。
引入新的数据结构也是优化伙伴算法的重要方向之一 。例如,一些研究提出使用哈希表来辅助伙伴算法的内存分配和回收。哈希表可以快速地查找空闲内存块,提高分配和回收的效率。通过将空闲内存块的地址和大小等信息存储在哈希表中,当需要分配内存时,可以直接在哈希表中查找合适的内存块,避免了对链表的遍历,从而大大缩短了分配时间。在内存回收时,也可以通过哈希表快速地找到伙伴内存块,实现高效的合并操作。
此外,还可以结合其他内存管理技术来优化伙伴算法 。将伙伴算法与 slab 分配器相结合,对于一些频繁分配和释放的小对象,可以使用 slab 分配器来进行管理,而对于大内存块的分配和回收,则继续使用伙伴算法。这样可以充分发挥两种算法的优势,既减少了伙伴算法在处理小对象时的内部碎片问题,又利用了伙伴算法在管理大内存块时的高效性,从而提高整个内存管理系统的性能。通过这些优化策略的实施,伙伴算法能够在不同的应用场景中更好地发挥作用,为 Linux 内核的内存管理提供更加高效、可靠的支持。
六、伙伴算法在不同Linux版本中的演进
6.1版本变化概述
随着 Linux 内核的不断发展和演进,伙伴算法也在持续改进和优化,以适应日益复杂的系统需求和硬件环境。在早期的 Linux 版本中,伙伴算法的实现相对简单,主要侧重于基本的内存分配和回收功能。随着系统对内存管理性能要求的不断提高,伙伴算法在后续版本中经历了一系列的改进和扩展。
在 Linux 2.6 版本中,对伙伴算法进行了多项重要改进。引入了内存热插拔功能,使得系统能够在运行时动态地添加或移除内存模块。为了支持这一功能,伙伴算法在内存块的管理和链表操作上进行了相应的调整,确保在内存热插拔过程中内存分配和回收的稳定性和正确性。该版本还对伙伴算法的分配策略进行了优化,通过更合理地选择空闲内存块,提高了内存分配的效率,减少了内存碎片的产生。
到了 Linux 3.0 版本,伙伴算法进一步引入了一些新的特性和优化。引入了基于迁移类型的内存分配机制,将内存块分为不同的迁移类型,如不可移动、可回收和可移动等。在内存分配时,根据不同的迁移类型选择合适的内存块,提高了内存分配的灵活性和效率。该版本还对内存回收机制进行了改进,通过更智能地合并空闲内存块,减少了内存碎片的积累,提高了内存的利用率。
在 Linux 4.0 及之后的版本中,伙伴算法继续在性能优化和功能扩展方面进行演进。在一些高并发场景下,对伙伴算法的锁机制进行了优化,减少了锁竞争,提高了内存分配和回收的并发性能。还引入了一些新的数据结构和算法,如哈希表辅助的内存查找机制,进一步提高了内存分配和回收的速度。
6.2改进意义分析
这些在不同 Linux 版本中对伙伴算法的改进,对提升内存管理性能和效率具有重要意义。内存热插拔功能的引入,使得系统在运行时能够灵活地调整内存配置,满足不同的应用场景需求。在服务器环境中,当业务量增加时,可以动态添加内存模块,而伙伴算法能够正确地管理这些新增的内存,确保系统的稳定运行;当业务量减少时,又可以移除不必要的内存模块,节省能源消耗。
基于迁移类型的内存分配机制,有效地提高了内存分配的灵活性和效率。在现代操作系统中,不同类型的内存需求越来越多样化,通过将内存块分为不同的迁移类型,系统可以根据具体的需求选择最合适的内存块进行分配。对于一些不可移动的内核数据结构,分配不可移动类型的内存块,确保其在内存中的稳定性;对于一些可移动的用户数据,分配可移动类型的内存块,便于在内存管理过程中进行优化和调整。
对内存回收机制的改进,大大减少了内存碎片的积累,提高了内存的利用率。内存碎片的存在会导致内存空间的浪费,降低系统的性能。通过更智能地合并空闲内存块,伙伴算法能够及时地将相邻的空闲内存块合并成更大的内存块,使得内存空间更加连续,提高了内存的可分配性。在一些长时间运行的系统中,这种改进能够有效地避免内存碎片化问题的恶化,确保系统在长时间运行过程中始终保持良好的性能。
在高并发场景下对锁机制的优化以及引入新的数据结构和算法,显著提高了内存分配和回收的速度和并发性能。在多线程、多进程的高并发环境中,内存分配和回收操作频繁,如果锁竞争严重,会导致系统性能大幅下降。通过优化锁机制,减少了锁竞争,使得多个线程或进程能够更高效地同时进行内存操作。新的数据结构和算法的引入,如哈希表辅助的内存查找机制,能够快速地定位空闲内存块,大大缩短了内存分配和回收的时间,提高了系统的整体性能。这些改进使得 Linux 内核的内存管理更加高效、灵活和稳定,为系统的运行提供了有力的支持。