探索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函数完成的。

相关文章
|
7天前
|
存储 监控 算法
解析公司屏幕监控软件中 C# 字典算法的数据管理效能与优化策略
数字化办公的时代背景下,企业为维护信息安全并提升管理效能,公司屏幕监控软件的应用日益普及。此软件犹如企业网络的 “数字卫士”,持续记录员工电脑屏幕的操作动态。然而,伴随数据量的持续增长,如何高效管理这些监控数据成为关键议题。C# 中的字典(Dictionary)数据结构,以其独特的键值对存储模式和高效的操作性能,为公司屏幕监控软件的数据管理提供了有力支持。下文将深入探究其原理与应用。
26 4
|
1月前
|
存储 算法 调度
基于和声搜索优化算法的机器工作调度matlab仿真,输出甘特图
本程序基于和声搜索优化算法(Harmony Search, HS),实现机器工作调度的MATLAB仿真,输出甘特图展示调度结果。算法通过模拟音乐家即兴演奏寻找最佳和声的过程,优化任务在不同机器上的执行顺序,以最小化完成时间和最大化资源利用率为目标。程序适用于MATLAB 2022A版本,运行后无水印。核心参数包括和声记忆大小(HMS)等,适应度函数用于建模优化目标。附带完整代码与运行结果展示。
|
7天前
|
算法 JavaScript 数据安全/隐私保护
基于GA遗传优化的最优阈值计算认知异构网络(CHN)能量检测算法matlab仿真
本内容介绍了一种基于GA遗传优化的阈值计算方法在认知异构网络(CHN)中的应用。通过Matlab2022a实现算法,完整代码含中文注释与操作视频。能量检测算法用于感知主用户信号,其性能依赖检测阈值。传统固定阈值方法易受噪声影响,而GA算法通过模拟生物进化,在复杂环境中自动优化阈值,提高频谱感知准确性,增强CHN的通信效率与资源利用率。预览效果无水印,核心程序部分展示,适合研究频谱感知与优化算法的学者参考。
|
1月前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于GA遗传优化TCN-GRU时间卷积神经网络时间序列预测算法matlab仿真
本项目基于MATLAB2022a开发,提供无水印算法运行效果预览及核心程序(含详细中文注释与操作视频)。通过结合时间卷积神经网络(TCN)和遗传算法(GA),实现复杂非线性时间序列的高精度预测。TCN利用因果卷积层与残差连接提取时间特征,GA优化超参数(如卷积核大小、层数等),显著提升模型性能。项目涵盖理论概述、程序代码及完整实现流程,适用于金融、气象、工业等领域的时间序列预测任务。
|
1月前
|
算法 定位技术 数据安全/隐私保护
基于遗传优化算法的多AGV栅格地图路径规划matlab仿真
本程序基于遗传优化算法实现多AGV栅格地图路径规划的MATLAB仿真(测试版本:MATLAB2022A)。支持单个及多个AGV路径规划,输出路径结果与收敛曲线。核心程序代码完整,无水印。算法适用于现代工业与物流场景,通过模拟自然进化机制(选择、交叉、变异)解决复杂环境下的路径优化问题,有效提升效率并避免碰撞。适合学习研究多AGV系统路径规划技术。
102 12
|
2月前
|
机器学习/深度学习 算法 JavaScript
基于GA遗传优化TCN时间卷积神经网络时间序列预测算法matlab仿真
本内容介绍了一种基于遗传算法优化的时间卷积神经网络(TCN)用于时间序列预测的方法。算法运行于 Matlab2022a,完整程序无水印,附带核心代码、中文注释及操作视频。TCN通过因果卷积层与残差连接学习时间序列复杂特征,但其性能依赖超参数设置。遗传算法通过对种群迭代优化,确定最佳超参数组合,提升预测精度。此方法适用于金融、气象等领域,实现更准确可靠的未来趋势预测。
|
2月前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于GA遗传优化TCN-LSTM时间卷积神经网络时间序列预测算法matlab仿真
本项目基于MATLAB 2022a实现了一种结合遗传算法(GA)优化的时间卷积神经网络(TCN)时间序列预测算法。通过GA全局搜索能力优化TCN超参数(如卷积核大小、层数等),显著提升模型性能,优于传统GA遗传优化TCN方法。项目提供完整代码(含详细中文注释)及操作视频,运行后无水印效果预览。 核心内容包括:1) 时间序列预测理论概述;2) TCN结构(因果卷积层与残差连接);3) GA优化流程(染色体编码、适应度评估等)。最终模型在金融、气象等领域具备广泛应用价值,可实现更精准可靠的预测结果。
|
7天前
|
算法 数据安全/隐私保护
基于GA遗传算法的悬索桥静载试验车辆最优布载matlab仿真
本程序基于遗传算法(GA)实现悬索桥静载试验车辆最优布载的MATLAB仿真(2022A版)。目标是自动化确定车辆位置,使加载效率ηq满足0.95≤ηq≤1.05且尽量接近1,同时减少车辆数量与布载时间。核心原理通过优化模型平衡最小车辆使用与ηq接近1的目标,并考虑桥梁载荷、车辆间距等约束条件。测试结果展示布载方案的有效性,适用于悬索桥承载能力评估及性能检测场景。
|
7天前
|
算法 机器人 数据安全/隐私保护
基于双向RRT算法的三维空间最优路线规划matlab仿真
本程序基于双向RRT算法实现三维空间最优路径规划,适用于机器人在复杂环境中的路径寻找问题。通过MATLAB 2022A测试运行,结果展示完整且无水印。算法从起点和终点同时构建两棵随机树,利用随机采样、最近节点查找、扩展等步骤,使两棵树相遇以形成路径,显著提高搜索效率。相比单向RRT,双向RRT在高维或障碍物密集场景中表现更优,为机器人技术提供了有效解决方案。
|
29天前
|
算法 安全 数据安全/隐私保护
基于AES的遥感图像加密算法matlab仿真
本程序基于MATLAB 2022a实现,采用AES算法对遥感图像进行加密与解密。主要步骤包括:将彩色图像灰度化并重置大小为256×256像素,通过AES的字节替换、行移位、列混合及轮密钥加等操作完成加密,随后进行解密并验证图像质量(如PSNR值)。实验结果展示了原图、加密图和解密图,分析了图像直方图、相关性及熵的变化,确保加密安全性与解密后图像质量。该方法适用于保护遥感图像中的敏感信息,在军事、环境监测等领域具有重要应用价值。

热门文章

最新文章