探索Linux内核内存伙伴算法:优化系统性能的关键技术!

简介: 探索Linux内核内存伙伴算法:优化系统性能的关键技术!

通常情况下,一个高级操作系统必须要给进程提供基本的、能够在任意时刻申请和释放任意大小内存的功能,就像malloc 函数那样,然而,实现malloc 函数并不简单,由于进程申请内存的大小是任意的,如果操作系统对malloc 函数的实现方法不对,将直接导致一个不可避免的问题,那就是内存碎片。

内存碎片就是内存被分割成很小很小的一些块,这些块虽然是空闲的,但是却小到无法使用。随着申请和释放次数的增加,内存将变得越来越不连续。最后,整个内存将只剩下碎片,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框就可能无法满足,所以减少内存浪费的核心就是尽量避免产生内存碎片。

一、简介

在Linux系统中,内存的分配与回收速率直接影响系统的存取效率。当内核频繁请求和释放不同大小的一组连续页框时,会导致许多外部空闲碎片,造成空间的浪费。使用伙伴算法可以有效地缓解该问题。伙伴关系机制是操作系统中的一种动态存储管理算法。在进行内存分配时,该算法通过不断平分较大的空闲内存块来获得较小的空闲内存块,直到获得所需要的内存块;在进行内存回收时,该算法尽可能地合并空闲块。

内存管理是应用程序通过硬件和软件协作访问内存的一种方法,当进程请求内存使用时,它给进程分配可用的内存;当进程释放内存时,回收相应的内存,同时负责跟踪系统中内存的使用状态。

在Linux系统中,首先将内存分为若干个节点,然后每个节点又可以分为1-3个区,每个区下又有若干个页。页是内存管理的基本单元。

当前存在的问题

当系统工作时,CPU最先访问的地址不是物理内存中的实地址,而是虚拟地址空间的虚地址。当请求分页时,首先在虚拟地址空间中分配一个虚拟空间,然后根据需要为此区间分配相应的物理页面并建立映射。

在分配空间时,我们首先想到的便是malloc函数。由于在实际情况中,操作系统必须能够在任意时刻申请和释放任意大小的内存,该函数的实现并不容易,导致的主要问题有延时问题和碎片问题。

延时问题指的是系统查找到可分配单元的时间变长,例如程序请求分配一个64KB的内存空间,系统查看64KB空间发现不全是空余的,于是查看65KB的空间,发现仍不能满足需求,直到查看80KB空间时,才满足了需求,这种方式请求次数多达17次,频繁操作时,非常耗时。

若系统以较大的定长空间来分配内存,在一定程度上可以节省时间,但带来的是碎片过多问题,由于每次用较大的空间进行分配,系统中出现大量碎片,导致内存浪费。严重者会导致内存无法完成分配,虽然仍有许多碎片空间。

基于此,系统需要一种能够高效分配内存,同时又能减少产生碎片的算法,伙伴算法能有效地解决该问题,如今已成为操作系统中的一种基础算法。

640.png

[内核资料领取,](https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2)   [Linux内核源码学习地址。](https://ke.qq.com/course/4032547?flowToken=1044435)

二、伙伴算法原理

Linux 便是采用这著名的伙伴系统算法来解决外部碎片的问题。把所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。对1024 个页框的最大请求对应着 4MB 大小的连续RAM 块。每一块的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16个页框的块,其起始地址是 16 * 2^12 (2^12 = 4096,这是一个常规页的大小)的倍数。

下面通过一个简单的例子来说明该算法的工作原理:

假设要请求一个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链表。

伙伴算法系统初始化

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);

伙伴算法系统分配空间

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;

伙伴算法系统回收空间

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++;


内存分配

下面通过一个例子说明内存分配的过程:

现内存总容量为16KB,用户请求分配4KB大小的内存空间,且规定最小的内存分配单元是2KB。于是位图分为8个区域,用1表示已分配,用0表示未分配,则初始位图和空闲链表如图所示。从上到下依次是位图、内存块、空闲链表。

由于需要分配4KB内存,数显到链表中4KB位置进行查看,发现为空,于是继续向后查找8KB位置,发现仍为空,直到到达链表尾16KB位置不为空。16KB块分裂成两个8KB的块,其中一块插入到链表相应位置,另一块继续分裂成两个4KB的块,其中一个交付使用,另一个插入到链表中,结果如下图所示。

内存回收

内存回收是内存分配的逆过程,假设以上存储要释放4KB内存,首先到链表中4KB位置查看是否有它的“伙伴”,发现该位置不为空,于是合并成一个8KB的块,继续寻找它的“伙伴”,然后合并成一个16KB的块,插入链表中。若在查找过程中没有发现“伙伴”,则直接插入到链表中,然后将位图中的标记清零,表示内存可用。

优缺点分析

伙伴算法采用2的幂次方进行分配内存块,可以避免把大的内存块拆分的过小,更重要的是可以加快分配和释放速度,但如果所需要的空间不是2的整数次幂,则会产生许多内部碎片。

分配和合并采用链表和位图操作,操作方便,但是开销比较大。

一个很小的块往往会阻碍一个大块的合并,一个系统中,对内存块的分配,大小是随机的,一片内存中仅一个小的内存块没有释放,旁边两个大的内存块就不能合并。

三、伙伴算法场景

3.1伙伴算法的用途

管理物理内存,解决外碎片问题。

3.2满足以下条件的两个块称为伙伴

  • 两个块具有相同的大小,记作b
  • 它们的物理地址是连续的
  • 第一块的第一个页框的物理地址是2*b*2^12的倍数

3.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;
};

由此可知伙伴算法管理结构如下图所示:

3.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++;
}

3.5伙伴算法的内存分配

1) 伙伴算法内存分配的过程

当内核收到alloc_pages系列函数的分配请求时,首先需要确定是从哪一个节点上分配,然后再确定需要从节点的哪一个zone上分配,最后再根据伙伴算法,确定从zone的哪一个free_area数组成员上分配。在内存不足的情况下,还要回收内存,如果内存还是不够,还要调度kswapd把必要的内存存储到交换分区中。内存分配模块总是试图从代价最小的节点上分配,而对zone的确定则根据alloc_pages()系列函数的gfp_mask用以下规则确定:

  • 如果gfp_mask参数设置了__GFP_DMA位,则只能从ZONE_DMA中分配。
  • 如果gfp_mask参数设置了__GFP_HIGHMEM位,则能够从ZONE_DMA、ZONE_NORMAL、ZONE__HIGHMEM中分配。
  • 如果__GFP_DMA和__GFP_HIGHMEM都没有设置,则能够从ZONE_DMA和ZONE_NORMAL中分配。

当然,如果没有指定__GFP_DMA标志,则会尽量避免使用ZONE_DMA区的内存,只有当指定的区内存不足,而ZONE_DMA区又有充足的内存时,才会从ZONE_DMA中分配。

2)伙伴算法的具体内存分配过程

伙伴算法内存分配的思想

举例说明:假设请求分配4个页面,根据该算法先到第2(2^2=4)个组中寻找空闲块,如果该组没有空闲块就到第3(2^3=8)个组中寻找,假设在第3个组中找到空闲块,就把其中的4个页面分配出去,剩余的4个页面放到第2个组中。如果第三个组还是没有空闲块,就到第4(2^4=16)个组中寻找,如果在该组中找到空闲块,把其中的4个页面分配出去,剩余的12个页面被分成两部分,其中的8个页面放到第3个组,另外4个页面放到第2个组...依次类推。

alloc_pages系列函数最终会调用__rmqueue函数从free_area中取出内存页面,__rmqueue函数具体分析如下:

//返回申请到的内存空间首页的struct page结构指针
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
    struct free_area * area;
    unsigned int current_order;
    struct page *page;
    /*查询zone中从order到MAX_ORDER-1的各指数值对应的空闲内存区域*/
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
    /*连续2^current_order页空闲内存区域的描述结构指针*/
    area = zone->free_area + current_order;
    /*如果该空闲内存区域为空,则继续查看2^(current_order+1)页空闲内存区域*/
    if (list_empty(&area->free_list))
        continue;
    /*得到该空闲内存区域的首页struct page结构指针*/
    page = list_entry(area->free_list.next, struct page, lru);
    /*将page所指的连续2^current_order页的空闲区域从其所在的链表中删除*/
    list_del(&page->lru);
    rmv_page_order(page);
    area->nr_free--;
    __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
    /*
     *如果分配的内存比要申请的内存大,则需要把大块剩余的那一部分
     *分解并放到对应的队列中去
     */
    expand(zone, page, order, current_order, area);
    return page;
    }
    return NULL;
}

这个函数根据这个函数根据order从最合适的free_area队列中分配,如果不成功就从更大的块中找,找到一个合适的块后把它摘下来,最后需要把大块剩余的那一部分分解并放到对应的队列中去,这个工作是expand函数完成的。

相关文章
|
9天前
|
机器学习/深度学习 算法
基于改进遗传优化的BP神经网络金融序列预测算法matlab仿真
本项目基于改进遗传优化的BP神经网络进行金融序列预测,使用MATLAB2022A实现。通过对比BP神经网络、遗传优化BP神经网络及改进遗传优化BP神经网络,展示了三者的误差和预测曲线差异。核心程序结合遗传算法(GA)与BP神经网络,利用GA优化BP网络的初始权重和阈值,提高预测精度。GA通过选择、交叉、变异操作迭代优化,防止局部收敛,增强模型对金融市场复杂性和不确定性的适应能力。
139 80
|
6天前
|
存储 缓存 监控
局域网屏幕监控系统中的Python数据结构与算法实现
局域网屏幕监控系统用于实时捕获和监控局域网内多台设备的屏幕内容。本文介绍了一种基于Python双端队列(Deque)实现的滑动窗口数据缓存机制,以处理连续的屏幕帧数据流。通过固定长度的窗口,高效增删数据,确保低延迟显示和存储。该算法适用于数据压缩、异常检测等场景,保证系统在高负载下稳定运行。 本文转载自:https://www.vipshare.com
100 66
|
2天前
|
机器学习/深度学习 算法
基于遗传优化的双BP神经网络金融序列预测算法matlab仿真
本项目基于遗传优化的双BP神经网络实现金融序列预测,使用MATLAB2022A进行仿真。算法通过两个初始学习率不同的BP神经网络(e1, e2)协同工作,结合遗传算法优化,提高预测精度。实验展示了三个算法的误差对比结果,验证了该方法的有效性。
|
5天前
|
机器学习/深度学习 数据采集 算法
基于PSO粒子群优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目展示了基于PSO优化的CNN-GRU-SAM网络在时间序列预测中的应用。算法通过卷积层、GRU层、自注意力机制层提取特征,结合粒子群优化提升预测准确性。完整程序运行效果无水印,提供Matlab2022a版本代码,含详细中文注释和操作视频。适用于金融市场、气象预报等领域,有效处理非线性数据,提高预测稳定性和效率。
|
2天前
|
监控 算法 JavaScript
基于 Node.js Socket 算法搭建局域网屏幕监控系统
在数字化办公环境中,局域网屏幕监控系统至关重要。基于Node.js的Socket算法实现高效、稳定的实时屏幕数据传输,助力企业保障信息安全、监督工作状态和远程技术支持。通过Socket建立监控端与被监控端的数据桥梁,确保实时画面呈现。实际部署需合理分配带宽并加密传输,确保信息安全。企业在使用时应权衡利弊,遵循法规,保障员工权益。
16 7
|
6天前
|
机器学习/深度学习 算法 索引
单目标问题的烟花优化算法求解matlab仿真,对比PSO和GA
本项目使用FW烟花优化算法求解单目标问题,并在MATLAB2022A中实现仿真,对比PSO和GA的性能。核心代码展示了适应度计算、火花生成及位置约束等关键步骤。最终通过收敛曲线对比三种算法的优化效果。烟花优化算法模拟烟花爆炸过程,探索搜索空间,寻找全局最优解,适用于复杂非线性问题。PSO和GA则分别适合快速收敛和大解空间的问题。参数调整和算法特性分析显示了各自的优势与局限。
|
8天前
|
存储 算法 Python
文件管理系统中基于 Python 语言的二叉树查找算法探秘
在数字化时代,文件管理系统至关重要。本文探讨了二叉树查找算法在文件管理中的应用,并通过Python代码展示了其实现过程。二叉树是一种非线性数据结构,每个节点最多有两个子节点。通过文件名的字典序构建和查找二叉树,能高效地管理和检索文件。相较于顺序查找,二叉树查找每次比较可排除一半子树,极大提升了查找效率,尤其适用于海量文件管理。Python代码示例包括定义节点类、插入和查找函数,展示了如何快速定位目标文件。二叉树查找算法为文件管理系统的优化提供了有效途径。
40 5
|
6天前
|
存储 监控 算法
企业内网监控系统中基于哈希表的 C# 算法解析
在企业内网监控系统中,哈希表作为一种高效的数据结构,能够快速处理大量网络连接和用户操作记录,确保网络安全与效率。通过C#代码示例展示了如何使用哈希表存储和管理用户的登录时间、访问IP及操作行为等信息,实现快速的查找、插入和删除操作。哈希表的应用显著提升了系统的实时性和准确性,尽管存在哈希冲突等问题,但通过合理设计哈希函数和冲突解决策略,可以确保系统稳定运行,为企业提供有力的安全保障。
|
8月前
|
缓存 Linux 测试技术
安装【银河麒麟V10】linux系统--并挂载镜像
安装【银河麒麟V10】linux系统--并挂载镜像
2223 0
|
8月前
|
关系型数据库 MySQL Linux
卸载、下载、安装mysql(Linux系统centos7)
卸载、下载、安装mysql(Linux系统centos7)
255 0