本文目的在于分析Linux内存管理机制中的伙伴系统。内核版本为2.6.31。
1. 伙伴系统的概念

在系统运行过程中,经常需要分配一组连续的页,而频繁的申请和释放内存页会导致内存中散布着许多不连续的页,这样,当某一时刻要申请一块较大的连续内存时,虽然系统内存余量足够,即很多页是空闲的,但找不到一大块连续的内存供使用。

Linux内核中使用伙伴系统(buddy system)算法来管理内存页。它把所有的空闲页放到11个链表中,每个链表分别管理大小为1,2,4,8,16,32,64,128,256,512,1024个页的内存块。当系统需要分配内存时,就可以从buddy系统中获取。例如,要申请一块包含4个页的连续内存,就直接从buddy系统中管理4个页连续内存的链表中获取。当系统释放内存时,则将释放的内存放回buddy系统对应的链表中,如果释放内存后发现有两块相邻的内存又可以合并为一个更高阶的内存块,例如释放4个页,而恰好相邻的内存也为4个页的空闲内存,则合并这两块内存并放到buddy系统管理8个连续页的链表中。同样的,如果系统需要申请3个页的连续内存,则只能在4个页的链表中获取,剩下的一个页被放到buddy系统中管理1个页的链表中。

buddy分配器分配的最小单位是一个页。要分配小于一页的内存需要用到slab分配器,而slab是基于buddy分配器的。

struct zone {
       ......
       struct free_area   free_area[MAX_ORDER];
       ......
    }____cacheline_internodealigned_in_smp;

struct zone的free_area[]数组成员存放了各阶的空闲内存列表,数组下标可取0~MAX_ORDER-1,(MAX_ORDER=11)。所以,每个阶(order)的内存链表使用struct free_area结构来记录。

struct free_area {
       struct list_head  free_list[MIGRATE_TYPES];
       unsigned long     nr_free;
    };

struct free_area有两个成员,free_list[]是不同migrate type(迁移类型)页链表的数组(我们先不关注什么是迁移类型,后面会讲到),每种迁移类型都是一个struct page的链表,由每个struct page的page->lru连起来。nr_free表示这个order空闲页的数量,例如,阶为2的连续页块共有3个,则nr_free=3,实际上这个阶的空闲页数为(2^2)*3=12。
2. per-cpu的冷热页链表

struct zone结构有一个pageset[]成员:

struct zone {
       ......
    struct per_cpu_pageset  pageset[NR_CPUS];
    ......
    }____cacheline_internodealigned_in_smp;
     
    struct per_cpu_pageset {
       struct per_cpu_pages pcp;
    } ____cacheline_aligned_in_smp;
     
    struct per_cpu_pages {
       int count;    /* number ofpages in the list */
       int high;     /* highwatermark, emptying needed */
       int batch;    /* chunk sizefor buddy add/remove */
       struct list_head list;   /*the list of pages */
    };

为了方便,我下文将per cpu pageset简称为pcp。

pageset[]数组用于存放per cpu的冷热页。当CPU释放一个页时,如果这个页仍在高速缓存中,就认为它是热的,之后很可能又很快被访问,于是将它放到pageset列表中,其他的认为是冷页。pageset中的冷热页链表元素数量是有限制的,由per_cpu_pages的high成员控制,毕竟如果热页太多,实际上最早加进来的页已经不热了。

在CPU释放一个页的时候,不会急着释放到buddy系统中,而是会先试图将页作为热页或冷页放到pcp链表中,直到超出数量限制。而释放多个页时则直接释放到buddy系统中。

per_cpu_pages的count成员表示链表中页的数量。batch表示有时需要从伙伴系统中拿一些页放到冷热页链表中时,一次拿多少个页。list成员是冷热页链表,越靠近表头的越热。

一般情况下,当内核想申请一个页的内存时,就先从CPU的冷热页链表中申请。但是,有时直接申请冷页会更合理一些,因为有时cache中的页肯定是无效的,所以内核在申请内存页时提供了一个标记GPF_COLD来指明要申请冷页。

注意,冷热页分配只针对分配和回收一个页的时候,多个页则直接操作buddy。
3. alloc_pages_node()在buddy系统上分配页

static inline struct page* alloc_pages_node(int nid, gfp_t gfp_mask,
                         unsigned int order)
    {
       /* Unknown node is current node */
       if (nid < 0)
           nid = numa_node_id();
     
       return __alloc_pages(gfp_mask, order, node_zonelist(nid,gfp_mask));
    }

这个函数的三个参数为:

nid:节点id,UMA系统为0。

gfp_mask:GFP(get free page)掩码,在include/linux/gfp.h中定义。

order:分配阶,如分配4个页,则order=2。

node_zonelist()返回节点的zone备用列表,即NODE_DATA(nid)->node_zonelists[]。

该函数最终调用__alloc_pages_nodemask()做实际的分配工作,这个函数的注释为“This is the 'heart' of the zoned buddy allocator”。这个函数根据gpf_flags寻找合适的zone,然后调用函数get_page_from_freelist()进行接下来的工作,这个函数简化后的实现如下:

static struct page *
    get_page_from_freelist(gfp_tgfp_mask, nodemask_t *nodemask, unsigned int order,
           struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
           struct zone *preferred_zone, int migratetype)
    {
       struct zoneref *z;
       struct page *page = NULL;
       int classzone_idx;
       struct zone *zone;
     
       /* 只有ZONE_NORMAL,所以都返回0 */
       classzone_idx = zone_idx(preferred_zone);
     
    /*
        * Scan zonelist, lookingfor a zone with enough free.
        * See alsocpuset_zone_allowed() comment in kernel/cpuset.c.
        */
       for_each_zone_zonelist_nodemask(zone, z, zonelist,
                         high_zoneidx, nodemask) {
           /* 如果没有设置NO_WATERMARKS */
           if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
              unsigned long mark;
              int ret;
     
              /* 获得设置的水印位 */
              mark = zone->watermark[alloc_flags &ALLOC_WMARK_MASK];
              /* 判断是否超出了水印设置的限制 */
              if (zone_watermark_ok(zone,order, mark,
                     classzone_idx, alloc_flags))
                  goto try_this_zone;
           }
     
    try_this_zone:
           page = buffered_rmqueue(preferred_zone,zone, order,
                         gfp_mask, migratetype);
           if (page)
              break;
       }
       return page;
    }

其中调用的函数主要就两个:

zone_watermark_ok()用来判断水印限制(zone-> watermark[]),如果要分配的order超出了水印限制,说明系统中可用内存页不够了,不能继续分配。

/*
     * Return 1 if free pages are above 'mark'.This takes into account the order
     * of the allocation.
     */
    int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
                 int classzone_idx,int alloc_flags)
    {
       /* free_pages my go negative - that's OK */
       long min = mark;
       long free_pages = zone_page_state(z, NR_FREE_PAGES) - (1 <<order) + 1; /* 为毛加1 */
       int o;
     
       if (alloc_flags & ALLOC_HIGH) /* 如果有ALLOC_HIGH,就降低限制 */
           min -= min / 2;
       if (alloc_flags & ALLOC_HARDER) /* 如果有ALLOC_HARDER,就再降低限制 */
           min -= min / 4;
     
       /* 如果分配完,剩余的小于限制了 */
       if (free_pages <= min + z->lowmem_reserve[classzone_idx])
           return 0;
     
       /* 如果分配完,比order小的伙伴拥有的页数占多数,也不行。 */
       for (o = 0; o < order; o++) {
           /* At the next order, this order's pages become unavailable */
           free_pages -= z->free_area[o].nr_free << o;
     
           /* Require fewer higher order pages to be free */
           min >>= 1;
     
           if (free_pages <= min)
              return 0;
       }
       return 1;
    }

注意,在init_per_zone_wmark_min()函数中初始化了每个zone的水印以及lowmem_reserve等限制。这个函数被module_init()了,并且内嵌编到内核,所以在系统启动时自动执行。

buffered_rmqueue()进行实际的分配页的工作。

static inline
    struct page *buffered_rmqueue(structzone *preferred_zone,
              struct zone *zone, int order, gfp_t gfp_flags,
              int migratetype)
    {
       unsigned long flags;
       struct page *page;
       int cold = !!(gfp_flags & __GFP_COLD);/* 是否设置COLD位 */
       int cpu;
     
    again:
       cpu  = get_cpu();
       if (likely(order == 0)) { /* 如果需要分配一页 */
           /* 在pcp上分配 */
       } else { /* 如果需要分配多页 */
           /* 在buddy上分配 */
       }
     
       __count_zone_vm_events(PGALLOC, zone, 1 << order);
       zone_statistics(preferred_zone, zone); /* 更新统计数据. */
       local_irq_restore(flags);
       put_cpu();
     
       VM_BUG_ON(bad_range(zone, page));
       /* 其他工作 */
       if (prep_new_page(page, order, gfp_flags))
           goto again;
       return page;
     
    failed:
       local_irq_restore(flags);
       put_cpu();
       return NULL;
    }

在分配页的时候分为两种情况,如果只需分配一页,则直接在pcp上进行。如果需要分配多页,则在buddy系统上分配。

申请一个页,在pcp上分配:

1.   通过&zone_pcp(zone,cpu)->pcp获取pcp链表。

2.   如果pcp->count==0,即pcp链表为空,则使用rmqueue_bulk()函数在buddy上获取batch个单页放到pcp链表中,并将这些页从buddy上移除,同时更新zone的vm_stat统计数据。

3.   根据gfp_flags有没有GPF_COLD标志,判断如果需要分配冷页就从pcp链表的末尾取一个页,如果需要热页就从链表头取一个页,获得的页赋值给page。

4.   将page从pcp链表中删除:list_del(&page->lru),同时pcp->count--。

申请多个页,在buddy上分配:

1.   如果设置有__GFP_NOFAIL标记,并且order>1,则给出警告。

2.   使用__rmqueue()分配2^order个页。

3.   更新zone的vm_stat统计数据。

在buddy上申请2^order个页都是通过__rmqueue()函数完成的,它主要分三步工作:

1.   调用__rmqueue_smallest()在指定zone的free_area[order]上特定migratetype链表上尝试分配2^order个页,如果order阶没有足够内存,就尝试在order+1阶的特定migratetype链表上分配2^order个页,依次类推直到分配到想要的页。

2.   将被申请的页从buddy系统上清除。同时,申请之后可能buddy系统需要重新调整,例如,本来想分配2^1=2个页,而buddy已经没有2个页的伙伴了,所以在2^2=4个页的伙伴上申请,那申请完剩下的两个页需要从free_area[2]上删除,并且放到free_area[1]链表中,这个工作是由expand()完成的。

3.   如果在当前zone的buddy上特定migratetype的链表中没有分配成功,并且migratetype != MIGRATE_RESERVE,就使用__rmqueue_fallback()在备用列表中分配。

在这里我们需要说明struct page的三个成员:

lru:链表节点,在存放struct page的链表中都是以lru为节点的,如buddy和pcp中的链表。

private:这个成员有多重意思,我们在这里看到,如果page在buddy系统中,private就是这个页所在free_area的阶数。如果page在pcp冷热页链表中,private就是migratetype。

flags:如果页在buddy系统中,PG_buddy标记就会被设置,否则被清除。
4. 释放页

释放页的接口为__free_pages(),它的参数为第一个页的指针page,以及order。

void __free_pages(structpage *page, unsigned int order)
    {
       if (put_page_testzero(page)) {
           if (order == 0)
              free_hot_page(page);
           else
              __free_pages_ok(page,order);
       }
    }

如果释放一个页,则先尝试添加到pcp中,超过pcp限制再往buddy系统中添加。如果释放多个页,则通过__free_pages_ok()释放。

将页回收至buddy系统中的接口为__free_one_page()。就是一个查找page idx和合并原有buddy的过程。

static inline void__free_one_page(struct page *page,
           struct zone *zone, unsigned int order,
           int migratetype)
    {
       unsigned long page_idx;
     
       if (unlikely(PageCompound(page)))
           if (unlikely(destroy_compound_page(page, order)))
              return;
     
       VM_BUG_ON(migratetype == -1);
     
       /* 由mem_map得到页的index */
       page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
     
       /* 这句判断的意思是page_idx必须是order对齐的? */
       VM_BUG_ON(page_idx & ((1 << order) - 1));
       VM_BUG_ON(bad_range(zone, page));
     
       /* 从order阶开始尝试合并 */
       while (order < MAX_ORDER-1) {
           unsigned long combined_idx;
           struct page *buddy;
     
           /* 找到和当前page同阶的buddy的理论位置 */
           buddy = __page_find_buddy(page, page_idx, order);
          
           /* 看page和buddy能否合并,不能就结束了 */
           if (!page_is_buddy(page, buddy, order))
              break;
          
           /* 可以合并,则把buddy释放 */
           /* Our buddy is free, merge with it and move up one order. */
           list_del(&buddy->lru);
           zone->free_area[order].nr_free--;
           rmv_page_order(buddy);
     
           /* page和buddy合并,起始index就是page或buddy的index */
           combined_idx = __find_combined_index(page_idx, order);
           page = page + (combined_idx - page_idx);
           page_idx = combined_idx;
     
           /* 继续看能否再往高阶合并 */
           order++;
       }
     
    /* 合并完成,设置最终buddy的order并添加到响应order数组的链表中。 */
       set_page_order(page, order);
       list_add(&page->lru,
           &zone->free_area[order].free_list[migratetype]);
       zone->free_area[order].nr_free++;
    }

这段代码逻辑很清晰。注意,只有相邻地址的buddy才能合并,所以实际上待释放page的buddy的页的index是可以计算出来的:

/*
     * Locate the struct page for both the matchingbuddy in our
     * pair (buddy1) and the combined O(n+1) pagethey form (page).
     *
     * 1) Any buddy B1 will have an order O twin B2which satisfies
     * the following equation:
     *     B2= B1 ^ (1 << O)
     * For example, if the starting buddy (buddy2)is #8 its order
     * 1 buddy is #10:
     *     B2= 8 ^ (1 << 1) = 8 ^ 2 = 10
     *
     * 2) Any buddy B will have an order O+1 parentP which
     * satisfies the following equation:
     *     P= B & ~(1 << O)
     *
     * Assumption: *_mem_map is contiguous at leastup to MAX_ORDER
     */
    static inline struct page *
    __page_find_buddy(structpage *page, unsigned long page_idx, unsigned int order)
    {
       unsigned long buddy_idx = page_idx ^ (1<< order);
     
       return page + (buddy_idx - page_idx);
    }

合并两个buddy得到合并后buddy的起始页index的函数:

static inline unsigned long
    __find_combined_index(unsignedlong page_idx, unsigned int order)
    {
       return (page_idx & ~(1 <<order));
    }

判断是否可以合并的函数:

/*
     * This function checks whether a page is free&& is the buddy
     * we can do coalesce a page and its buddy if
     * (a) the buddy is not in a hole &&
     * (b) the buddy is in the buddy system&&
     * (c) a page and its buddy have the same order&&
     * (d) a page and its buddy are in the samezone.
     *
     * For recording whether a page is in the buddysystem, we use PG_buddy.
     * Setting, clearing, and testing PG_buddy isserialized by zone->lock.
     *
     * For recording page's order, we usepage_private(page).
     */
    static inline intpage_is_buddy(struct page *page, struct page *buddy,
                                int order)
    {
       /* buddy的页的index是否合法。 */
       if (!pfn_valid_within(page_to_pfn(buddy)))
           return 0;
     
       /* 是否属于同一个zone。 */
       if (page_zone_id(page) != page_zone_id(buddy))
           return 0;
     
       /* 目标buddy必须设置了PG_buddy标记。并且和page是同order的。 */
       if (PageBuddy(buddy) && page_order(buddy) == order) {
           VM_BUG_ON(page_count(buddy) != 0);
           return 1;
       }
       return 0;
    }

原文:https://blog.csdn.net/jasonchen_gbd/article/details/44023801

Linux内存管理 - buddy系统的更多相关文章

  1. Linux 内存管理与系统架构设计

    Linux 提供各种模式(比如,消息队列),但是最著名的是 POSIX 共享内存(shmem,shared memory). Linux provides a variety of schemes ( ...

  2. Linux内存管理原理

    本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址又叫线性地址.linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻 ...

  3. Linux内存管理原理【转】

    转自:http://www.cnblogs.com/zhaoyl/p/3695517.html 本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址 ...

  4. Windows内存管理和linux内存管理

    windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...

  5. 伙伴系统之避免碎片--Linux内存管理(十六)

    1 前景提要 1.1 碎片化问题 分页与分段 页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位,它含有一组意义相对完整的信 ...

  6. 伙伴系统之伙伴系统概述--Linux内存管理(十五)

    在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, ...

  7. 启动期间的内存管理之bootmem_init初始化内存管理–Linux内存管理(十二)

    1. 启动过程中的内存初始化 首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在init/main.c?v=4.7, line 479 其代码很复杂, 我们只截取 ...

  8. 启动期间的内存管理之初始化过程概述----Linux内存管理(九)

    在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检 ...

  9. Linux内存管理 (25)内存sysfs节点解读

    1. General 1.1 /proc/meminfo /proc/meminfo是了解Linux系统内存使用状况主要接口,也是free等命令的数据来源. 下面是cat /proc/meminfo的 ...

随机推荐

  1. linux 命令(Ubuntu)

    1. 我们可以使用下列的命令压缩一个目录: # zip -r archive_name.zip directory_to_compress 2. 下面是如果解压一个zip文档: # unzip arc ...

  2. 从零开始使用vue-cli搭建一个vue项目及注意事项

    一.安装node.js 1.根据电脑的自行下载node.js安装包http://nodejs.cn 2.点击安装,按照正常的的一路点击下去 3.验证安装是否成功,按键win+r,输入cmd打开命令行工 ...

  3. python_文件 处理

    一.字符编码 内存固定使用unicode编码 数据最先产生于内存中,是unicode格式,要想传输需要转成bytes格式 # unicode -------> enconde( u t f - ...

  4. Spring集成Quartz的3种方式

    1.使用xml配置方式 Maven依赖 <properties> <!-- spring版本号 --> <spring.version>4.2.2.RELEASE& ...

  5. Maven 错误 Failure to transfer ...was cached in the local repository...

    Maven 错误 Failure to transfer ...was cached in the local repository... 我解决的时候多了两步才解决 1. mvn clean ins ...

  6. 使用Xshell连接服务器

    转载原地址:http://www.server110.com/linux/201308/830.html 1)关于Xshell 网上更多的资料里提到的SSH客户端是putty,因为简单.开源.免费.但 ...

  7. ThinkPHP 统计数据(数字字段)更新 setInc 与 setDec 方法

    ThinkPHP 统计数据更新 ThinkPHP 内置了对统计数据(数字字段)的更新方法: setInc():将数字字段值增加 setDec():将数字字段值减少 setInc() ThinkPHP ...

  8. 关于微信小程序登录授权

    小程序的API接口文档写的很清晰,现在理一遍思路. 前端通过wx.login()获取code ,把code发给后台,后台返回openid,再获取用户的授权信息(这里先判断是否授权,授权过的就直接进入小 ...

  9. winfrom C#树勾选等

    AfterCheck /// <summary> /// 树勾选 /// </summary> /// <param name="sender"> ...

  10. Linux 命令-1

    1.除了/之外,所有的字符都合法,有些字符最好不要用 2.以.开头的文件将被隐藏.和window的区别 3.大小写敏感 4.命令格式: 命令 -选项 参数   例如: ls -la /etc  ,选项 ...