linux内存源码分析 - 伙伴系统(初始化和申请页框)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/

  之前的文章已经介绍了伙伴系统,这篇我们主要看看源码中是如何初始化伙伴系统、从伙伴系统中分配页框,返回页框于伙伴系统中的。

  我们知道,每个管理区都有自己的伙伴系统管理属于这个管理区的页框,这也说明了,在伙伴系统初始化时,管理区必须要已经存在(初始化完成)了。在管理区描述符(struct zone)中,struct free_area就专门用于描述伙伴系统的。在一个管理区中,伙伴系统一共维护着包含1,2,4,8,16,...,512,1024个连续页框的链表,当我需要从此管理区分配8个页框时,伙伴系统会从包含8个连续页框的链表取出一个结点(一个结点就是一个连续的8个页框),然后分配给我。这时如果8个连续页框的链表为空,则会从16个连续页框的链表取出一个结点将其分为两个8个连续页框的结点,并把它们放入8页框的链表,然后再分配其中一个结点给我。而当回收页框时,会尝试与前后连续页框组成更大的页框块,加入到更高级的页框页表中,比如,我释放了8个页框,伙伴系统会尝试将我释放这连续的8个页框与前8个页框或者后8个页框合并,一直尝试合并到不能合并为止,并加入相应的链表中。

  而注意的是,在系统中,需要向伙伴系统申请页框时,页框的数量都是以2的次方计算的,也就是申请页框的数量一定是1,2,4,8,16,32,64......1024这些数中的一个。释放时也是这样的道理,不过一般而言,会从伙伴系统中申请页框操作最频繁的模块可能就是slab/slub和建立进程的线性区了。

/* 内存管理区描述符 */
struct zone {

    ........

    /* 实现每CPU页框高速缓存,里面包含每个CPU的单页框的链表 */
    struct per_cpu_pageset __percpu *pageset;

    ........

    /* 对应于伙伴系统中MIGRATE_RESEVE链的页块的数量 */
    int            nr_migrate_reserve_block;

    ........

    /* 标识出管理区中的空闲页框块,用于伙伴系统 */
    /* MAX_ORDER为11,分别代表包含大小为1,2,4,8,16,32,64,128,256,512,1024个连续页框的链表,具体见下面 */
    struct free_area    free_area[MAX_ORDER];

    ......

}

/* 伙伴系统的一个块,描述1,2,4,8,16,32,64,128,256,512或1024个连续页框的块 */
struct free_area {
    /* 指向这个块中所有空闲小块的第一个页描述符,这些小块会按照MIGRATE_TYPES类型存放在不同指针里 */
    struct list_head    free_list[MIGRATE_TYPES];
    /* 空闲小块的个数 */
    unsigned long        nr_free;
};

/* 每CPU高速缓存描述符 */
struct per_cpu_pageset {
    /* 核心结构,高速缓存页框结构 */
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

struct per_cpu_pages {
    /* 当前CPU高速缓存中页框个数 */
    int count;        /* number of pages in the list */
    /* 上界,当此CPU高速缓存中页框个数大于high,则会将batch个页框放回伙伴系统 */
    int high;        /* high watermark, emptying needed */
    /* 在高速缓存中将要添加或被删去的页框个数,当链表中页框数量多个上界时会将batch个页框放回伙伴系统,当链表中页框数量为0时则从伙伴系统中获取batch个页框 */
    int batch;        /* chunk size for buddy add/remove */

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    /* 页框的链表,如果需要冷高速缓存,从链表尾开始获取页框,如果需要热高速缓存,从链表头开始获取页框 */
    struct list_head lists[MIGRATE_PCPTYPES];
};

  在各个页框块链表中又有几个以MIGRATE_TYPES区分的小链表,MIGRATE_TYPES类型如下:

  • MIGRATE_UNMOVABLE:页框内容不可移动,在内存中位置必须固定,无法移动到其他地方,核心内核分配的大部分页面都属于这一类。
  • MIGRATE_RECLAIMABLE:页框内容可回收,不能直接移动,但是可以回收,因为还可以从某些源重建页面,比如映射文件的数据属于这种类别,kswapd会按照一定的规则,周期性的回收这类页面。
  • MIGRATE_MOVABLE:页框内容可移动,属于用户空间应用程序的页属于此类页面,它们是通过页表映射的,因此我们只需要更新页表项,并把数据复制到新位置就可以了,当然要注意,一个页面可能被多个进程共享,对应着多个页表项。
  • MIGRATE_PCPTYPES:用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目。
  • MIGRATE_CMA: 预留一段的内存给驱动使用,但当驱动不用的时候,伙伴系统可以分配给用户进程用作匿名内存或者页缓存。而当驱动需要使用时,就将进程占用的内存通过回收或者迁移的方式将之前占用的预留内存腾出来,供驱动使用。
  • MIGRATE_ISOLATE:不能从这个链表分配页框,因为这个链表专门用于NUMA结点移动物理内存页,将物理内存页内容移动到使用这个页最频繁的CPU。
/* 这几个链表主要用于反内存碎片 */
enum {
    MIGRATE_UNMOVABLE,         /* 页框内容不可移动,在内存中位置必须固定,无法移动到其他地方,核心内核分配的大部分页面都属于这一类。 */
    MIGRATE_RECLAIMABLE,         /* 页框内容可回收,不能直接移动,但是可以回收,因为还可以从某些源重建页面,比如映射文件的数据属于这种类别,kswapd会按照一定的规则,周期性的回收这类页面。 */
    MIGRATE_MOVABLE,             /* 页框内容可移动,属于用户空间应用程序的页属于此类页面,它们是通过页表映射的,因此我们只需要更新页表项,并把数据复制到新位置就可以了
                                 * 当然要注意,一个页面可能被多个进程共享,对应着多个页表项。
                                 */
    MIGRATE_PCPTYPES,             /* 用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目 */
    MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    MIGRATE_CMA,                   /* 预留一段的内存给驱动使用,但当驱动不用的时候,伙伴系统可以分配给用户进程用作匿名内存或者页缓存。而当驱动需要使用时,就将进程占用的内存通过回收或者迁移的方式将之前占用的预留内存腾出来,供驱动使用。 */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,            /* 不能从这个链表分配页框,因为这个链表专门用于NUMA结点移动物理内存页,将物理内存页内容移动到使用这个页最频繁的CPU */
#endif
    MIGRATE_TYPES
};

  很简单地看出,相应类型的页要从相应的链表中获取,而它们之间也是有一定的优先级顺序的:

static int fallbacks[MIGRATE_TYPES][4] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
#ifdef CONFIG_CMA
    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
    [MIGRATE_CMA]         = { MIGRATE_RESERVE }, /* Never used */
#else
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
#endif
    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, /* Never used */
#ifdef CONFIG_MEMORY_ISOLATION
    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, /* Never used */
#endif
};

  以MIGRATE_RECLAIMABLE为例,如果我需要申请这种页框,当然会优先从这类页框的链表中获取,如果没有,我会依次尝试从MIGRATE_UNMOVABLE -> MIGRATE_MOVABLE -> MIGRATE_RESERVE链进行分配。

  之前的文章也有提到页描述符,它用于描述一个物理页框,所有的页描述符保存在mem_map数组中,通过页框号可以直接获取它对应的页描述符(以页框号做mem_map数组下标获取的就是对应的页描述符),通过页描述符我们可以确定物理页框所在位置,所以在伙伴系统中,保存在链表中的结点,就是这组连续页框的首个页框的页描述符。

初始化伙伴系统

  在初始化伙伴系统之前,所有的node和zone的描述符都已经初始化完毕,同时物理内存中所有的页描述符页相应的初始化为了MIGRATE_MOVABLE类型的页。

  初始化过程中首先将所有管理区的伙伴系统链表置空,这些工作处于start_kernel() -> setup_arch() -> x86_init.paging.pagetable_init()中进行:

//以下代码处于free_area_init_core()函数中

/* 将管理区ZONE的伙伴系统置空 */
static void __meminit zone_init_free_lists(struct zone *zone)
{
    unsigned int order, t;

    for (order = 0; order < MAX_ORDER; order++)
        for (type = 0; type < MIGRATE_TYPES; type++) {
                INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
                zone->free_area[order].nr_free = 0;
            }
}

void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{
        ........
        /* 该区所有页都设置为MIGRATE_MOVABLE */
        if ((z->zone_start_pfn <= pfn) && (pfn < zone_end_pfn(z)) && !(pfn & (pageblock_nr_pages - 1)))
            set_pageblock_migratetype(page, MIGRATE_MOVABLE);    

        ........
}

  

初始化高端内存区

  在伙伴系统的初始化过程中,X86下高端内存和低端内存的伙伴系统初始化是分开来进行的,顺序是从高端内存区到低端内存区,在初始化伙伴系统之前,系统中的node、zone、page都已经初始化完成了,基本上伙伴系统的初始化就是将所有不用的页框释放回伙伴系统中。我们先看看高端内存区的伙伴系统初始化,主要在set_highmem_pages_init(void)函数中:

/* 所有高端内存管理区初始化,将所有node的所有zone的managed_pages置为0,并将他们的页框回收到页框分配器中 */
void __init set_highmem_pages_init(void)
{
    struct zone *zone;
    int nid;

    /* 将所有node的所有zone的managed_pages置为0,即将所有管理区的所管理页数量设置为0 */
    reset_all_zones_managed_pages();

    /* 遍历所有管理区,这里只初始化高端内存区 */
    for_each_zone(zone) {
        unsigned long zone_start_pfn, zone_end_pfn;

        /* 如果不是高端内存区,则下一个 */
        /* 判断方法: 当前zone描述符地址 - 所属node的zone描述符数组基地址 == 高端内存区偏移量 */
        if (!is_highmem(zone))
            continue;

        /* 该管理区开始页框号 */
        zone_start_pfn = zone->zone_start_pfn;
        /* 该管理区结束页框号 */
        zone_end_pfn = zone_start_pfn + zone->spanned_pages;

        /* 该管理区所属的node结点号 */
        nid = zone_to_nid(zone);
        printk(KERN_INFO "Initializing %s for node %d (%08lx:%08lx)\n",
                zone->name, nid, zone_start_pfn, zone_end_pfn);

        /* 将start_pfn到end_pfn中所有页框回收,并放入页框分配器 */
        add_highpages_with_active_regions(nid, zone_start_pfn,
                 zone_end_pfn);
    }
}

  我们具体看add_highpages_with_active_regions函数:

/* 将start_pfn到end_pfn中所有页框回收,并放入页框分配器 */
void __init add_highpages_with_active_regions(int nid,
             unsigned long start_pfn, unsigned long end_pfn)
{
    phys_addr_t start, end;
    u64 i;

    /* 遍历所有memblock.memory,此结构有e820传递的数据初始化成,每个node会是其中一个这种结构 */
    for_each_free_mem_range(i, nid, &start, &end, NULL) {
        /* 修正后第一个页框号,因为有可能页框号不在node上 */
        unsigned long pfn = clamp_t(unsigned long, PFN_UP(start),
                        start_pfn, end_pfn);
        /* 修正后最后一个页框号 */
        unsigned long e_pfn = clamp_t(unsigned long, PFN_DOWN(end),
                          start_pfn, end_pfn);
        for ( ; pfn < e_pfn; pfn++)
            if (pfn_valid(pfn))
                /* 这里会将页框回收到页框分配器中 */
                free_highmem_page(pfn_to_page(pfn));
    }
}

  继续

void free_highmem_page(struct page *page)
{

    __free_reserved_page(page);
    /* 系统中总页数量++ */
    totalram_pages++;
    /* 页所属的管理区的managed_pages++ */
    page_zone(page)->managed_pages++;
    /* 高端内存页数量++ */
    totalhigh_pages++;
}

static inline void __free_reserved_page(struct page *page)
{
    ClearPageReserved(page);
    /* page->_count = 1 */
    init_page_count(page);
    /* 释放到伙伴系统中,之后给出详细代码 */
    __free_page(page);
}

  到这里整个高端内存区的伙伴系统就初始化完成了,关于__free_page()函数我们之后会说明,这个函数是伙伴系统的是否单个页框的API。

初始化低端内存区(ZONE_DMA、ZONE_NORMAL)

  在系统初始化阶段会先启用一个bootmem分配器,此分配器是专门用于启动阶段的,一个bootmem分配器管理着一个node结点的所有内存,也就是在numa架构中多个node有多个bootmem,他们被链入bdata_list链表中保存。而伙伴系统的初始化就是将bootmem管理的所有物理页框释放到伙伴系统中去。

  我们先看看bootmem的struct bootmem_data结构:

/* bootmem分配器结点(管理着一整块连续内存,可以管理一个node中所有的物理内存),启动时使用 */
typedef struct bootmem_data {
    /* 此块内存开始页框号 */
    unsigned long node_min_pfn;
    /* 此块内存结束页框号,如果是32位系统下此保存的是 ZONE_NORMAL最后一个页框号 */
    unsigned long node_low_pfn;
    /* 指向位图内存区,node中所有ZONE_HIGHMEM之前的页框都在这里面有一个位,每次需要分配内存时就会扫描找出一个空闲页框,空洞的内存也会占用位,不过空洞的内存应该设置为已分配 */
    void *node_bootmem_map;
    /* 上次分配距离末尾的偏移量 */
    unsigned long last_end_off;
    unsigned long hint_idx;
    /* 链入bdata_list结构链表 */
    struct list_head list;
} bootmem_data_t;

  bootmem分配器核心就是node_bootmem_map这个位图,每一位代表这个node的一个页,当需要分配时就会扫描这个位图,然后获取一段物理页框进行分配,一般都会从开始处向后进行分配,并没有什么特殊的算法在其中。而伙伴系统初始化时页会根据这个位图,将位图中空闲的页释放回到伙伴系统中,而已经分配出去的页则不会在初始化阶段释放回伙伴系统,不过有可能会在系统运行过程中释放回伙伴系统中,我们具体看看是如何实现初始化的:

/* 释放所有启动后不需要的内存到页框分配器 */
unsigned long __init free_all_bootmem(void)
{
    unsigned long total_pages = 0;
    /* 系统会为每个node分配一个这种结构,这个管理着node中所有页框,可以叫做bootmem分配器 */
    bootmem_data_t *bdata;

    /* 设置所有node的所有zone的managed_pages = 0,该函数在启动时只会调用一次,如果初始化高端内存的伙伴系统时调用过,这里就不会再次调用了 */
    reset_all_zones_managed_pages();

    /* 遍历所有需要释放的启动内存数据块 */
    list_for_each_entry(bdata, &bdata_list, list)
        /* 释放bdata启动内存块中所有页框到页框分配器 */
        total_pages += free_all_bootmem_core(bdata);

    /* 所有内存页数量 */
    totalram_pages += total_pages;

    /* 返回总共释放的页数量 */
    return total_pages;
}

  继续,主要看free_all_bootmem_core()函数:

/* 释放bdata启动内存块中所有页框到页框分配器 */
static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata)
{
    struct page *page;
    unsigned long *map, start, end, pages, count = 0;

    /* 此bootmem没有位图,也就是没有管理内存 */
    if (!bdata->node_bootmem_map)
        return 0;

    /* 此bootmem的位图 */
    map = bdata->node_bootmem_map;
    /* 此bootmem包含的开始页框 */
    start = bdata->node_min_pfn;
    /* 此bootmem包含的结束页框 */
    end = bdata->node_low_pfn;

    bdebug("nid=%td start=%lx end=%lx\n",
        bdata - bootmem_node_data, start, end);

    /* 释放 bdata->node_min_pfn 到 bdata->node_low_pfn 之间空闲的页框到伙伴系统 */
    while (start < end) {
        unsigned long idx, vec;
        unsigned shift;

        /* 一次循环检查long所占位数长度的页框数量(32或64) */
        idx = start - bdata->node_min_pfn;
        shift = idx & (BITS_PER_LONG - 1);

        /* 做个整理,因为有可能start并不是按long位数对其的,有可能出现在了vec的中间位数 */
        vec = ~map[idx / BITS_PER_LONG];

        if (shift) {
            vec >>= shift;
            if (end - start >= BITS_PER_LONG)
                vec |= ~map[idx / BITS_PER_LONG + 1] <<
                    (BITS_PER_LONG - shift);
        }

        /* 如果检查的这一块内存块全是空的,则一次性释放 */
        if (IS_ALIGNED(start, BITS_PER_LONG) && vec == ~0UL) {
            /* 这一块长度的内存块都为空闲的,计算这块内存的order,如果这块内存块长度是8个页框,那order就是3(2的3次方) */
            int order = ilog2(BITS_PER_LONG);

            /* 从start开始,释放2的order次方的页框到伙伴系统 */
            __free_pages_bootmem(pfn_to_page(start), order);
            /* count用来记录总共释放的页框 */
            count += BITS_PER_LONG;
            /* 开始位置向后移动 */
            start += BITS_PER_LONG;
        } else {
            /* 内存块中有部分是页框是空的,一页一页释放 */
            unsigned long cur = start;

            start = ALIGN(start + 1, BITS_PER_LONG);
            while (vec && cur != start) {
                if (vec & 1) {
                    /* 获取页框描述符,页框号实际上就是页描述符在mem_map的偏移量 */
                    page = pfn_to_page(cur);
                    /* 将此页释放到伙伴系统 */
                    __free_pages_bootmem(page, 0);
                    count++;
                }
                vec >>= 1;
                ++cur;
            }
        }
    }

  在__free_pages_bootmem()中也是调用了__free_pages()将页释放到伙伴系统中:

void __init __free_pages_bootmem(struct page *page, unsigned int order)
{
    /* 需要释放的页数量 */
    unsigned int nr_pages = 1 << order;
    struct page *p = page;
    unsigned int loop;

    /* 预取指令,该指令用于把将要使用到的数据从内存提前装入缓存中,以减少访问主存的指令执行时的延迟 */
    prefetchw(p);
    for (loop = 0; loop < (nr_pages - 1); loop++, p++) {
        /* 预取下一个页描述符 */
        prefetchw(p + 1);
        __ClearPageReserved(p);
        /* 设置page->_count = 0 */
        set_page_count(p, 0);
    }
    __ClearPageReserved(p);
    set_page_count(p, 0);

    /* 管理区的managed_pages加上这些页数量 */
    page_zone(page)->managed_pages += nr_pages;
    /* 将首页框的_count设置为1,代表被使用,因为被使用的页框才能够释放 */
    set_page_refcounted(page);
    /* 释放到管理区的伙伴系统 */
    __free_pages(page, order);
}

  到这里,高端内存和低端内存的初始化就已经完成了。所以未使用的页框都已经放入伙伴系统中供伙伴系统进行管理。之后我们会详细说明申请页框和释放页框的相关的操作。

申请页框

  对于申请单个页框而言,系统会从每CPU高速缓存维护的单页框链表中进行分配,而对于申请多个页框,系统则从伙伴系统中进行分配,可以说每CPU高速缓存算是伙伴系统中的一小部分,专门用于分配单个页框,因为系统希望尽量让那些刚释放掉的单页框分配出去,这样可以有效地提高缓存命中率,因为释放掉的页框可能还处于缓存中,而刚分配的页框一般都会马上使用,系统就不用对这些页框换进换出缓存了。因为每个CPU都有自己的高速缓存,所以这个结构就叫每CPU高速缓存。

  伙伴系统提供了多个接口供其他模块申请页框使用,如下:

  • struct page * alloc_pages (gfp_mask, order):向伙伴系统请求连续的2的order次方个页框,返回第一个页描述符。
  • struct page * alloc_page (gfp_mask):相当于struct page * alloc_pages(gfp_mask, 0)。
  • void * __get_free_pages (gfp_mask, order):该函数类似于alloc_pages(),但返回第一个所分配页的线性地址。
  • void * __get_free_page (gfp_mask):相当于void * __get_free_pages (gfp_mask, 0)。

  对于gfp_mask掩码,有如下这些:

/* 允许内核对等待空闲页框的当前进程进行阻塞 */
#define __GFP_WAIT    ((__force gfp_t)___GFP_WAIT)
/* 允许内核访问保留的页框池 */
#define __GFP_HIGH    ((__force gfp_t)___GFP_HIGH)
/* 允许内核在低端内存页上执行IO传输 */
#define __GFP_IO    ((__force gfp_t)___GFP_IO)
/* 如果清0,则不允许内核执行依赖于文件系统的操作 */
#define __GFP_FS    ((__force gfp_t)___GFP_FS)
/* 所请求的页框可能是"冷"的 */
#define __GFP_COLD    ((__force gfp_t)___GFP_COLD)
/* 一次内存分配失败将不会产生警告 */
#define __GFP_NOWARN    ((__force gfp_t)___GFP_NOWARN)
#define __GFP_REPEAT    ((__force gfp_t)___GFP_REPEAT)
/* 同上 */
#define __GFP_NOFAIL    ((__force gfp_t)___GFP_NOFAIL)
/* 一次分配失败后不再重试 */
#define __GFP_NORETRY    ((__force gfp_t)___GFP_NORETRY)
#define __GFP_MEMALLOC    ((__force gfp_t)___GFP_MEMALLOC)
/* 属于扩展页的页框 */
#define __GFP_COMP    ((__force gfp_t)___GFP_COMP)
/* 任何返回的页框必须被填满0 */
#define __GFP_ZERO    ((__force gfp_t)___GFP_ZERO)  

  而这些类型进行一些组合,会产生如下的一些掩码:

/* 原子分配,分配期间不会进行阻塞 */
#define GFP_ATOMIC    (__GFP_HIGH)
#define GFP_NOIO    (__GFP_WAIT)
#define GFP_NOFS    (__GFP_WAIT | __GFP_IO)
#define GFP_KERNEL    (__GFP_WAIT | __GFP_IO | __GFP_FS)
#define GFP_TEMPORARY    (__GFP_WAIT | __GFP_IO | __GFP_FS | \
             __GFP_RECLAIMABLE)
#define GFP_USER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_HIGHUSER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
             __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (__GFP_WAIT | __GFP_IO | __GFP_FS | \
                 __GFP_HARDWALL | __GFP_HIGHMEM |                  __GFP_MOVABLE)
#define GFP_IOFS    (__GFP_IO | __GFP_FS)
#define GFP_TRANSHUGE    (GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
             __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN |              __GFP_NO_KSWAPD)

  好了,我们主要看申请页框的接口,这些接口最后都会调用到__alloc_pages_nodemask()函数,这里我只用alloc_pages()函数进行讲解,我们先看从alloc_pages()如何到__alloc_pages_nodemask()函数的:

/* 分配页框
 * gfp_mask: 标志
 * order: 需求2的次方个数页框
 */
#define alloc_pages(gfp_mask, order) \
        alloc_pages_node(numa_node_id(), gfp_mask, order)

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

    /* 根据node号获取此node相应的zonelist,因为如果此node上没法分配出多余的内存,会从zonelist的其他node的zone中分配 */
    return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order,
        struct zonelist *zonelist)
{
        /* 最后调用到的函数 */
    return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

  在node中会有多个zonelist,这个zonelist的作用是将所有的zone按相对于此node的优先级进行排序,链表头4个就是此node的ZONE_MOVABLE、ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA,之后是排在此node之后的node的这4个管理区。然后再是排在此node之前的node的这4个管理区。在分配时就按照这个顺序,直到分配出相应数量的页框为止。

  在同一个node中的管理区中分配也有一定的顺序,当我需要从高端内存区申请内存时,系统会按照 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 顺序为我尝试分配,而当我需要从ZONE_NORMAL区申请内存时,系统会按照ZONE_NORMAL -> ZONE_DMA 顺序为我尝试分配,当我需要从ZONE_DMA区申请内存时,系统只会从ZONE_DMA区为我分配。在NUMA架构里也是一样,只会在其他node上的相应的管理区中分配内存。

  接下来我们要着重说明__alloc_pages_nodemask()函数。

__alloc_pages_nodemask()

  伙伴系统的页框分配方式主要有两种:快速分配和慢速分配。

  • 快速分配:会根据zonelist链表的优先级顺序,从相应zone的伙伴系统中分配连续页框。
  • 慢速分配:在快速分配失败之后执行,会唤醒kswapd内核线程,进行页框的回收。然后再尝试进行连续页框的分配。

  伙伴系统分配可用页框给申请者时,首先会根据zonelist对每个可用的zone进行快速分配,成功则返回第一个页框的页描述符,如果所有zone的快速分配都不成功,则会zonelist中的zone进行慢速分配,慢速分配中会进行内存压缩和唤醒kswapd线程(会导致阻塞)进行内存的回收工作,之后再尝试继续分配。我们先看看__alloc_pages_nodemask()源码:

struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist, nodemask_t *nodemask)
{
    enum zone_type high_zoneidx = gfp_zone(gfp_mask);
    struct zone *preferred_zone;
    struct zoneref *preferred_zoneref;
    struct page *page = NULL;
    /* 从gfp_mask中获取选定的页框类型,当中只会检查__GFP_MOVABLE和__GFP_RECLAIMABLE */
    int migratetype = gfpflags_to_migratetype(gfp_mask);
    unsigned int cpuset_mems_cookie;
    /* 这个需要注意一下,之后分配是会根据这个flags进行一定的操作 */
    int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
    int classzone_idx;

    gfp_mask &= gfp_allowed_mask;

    lockdep_trace_alloc(gfp_mask);

    /* 如果设置了__GFP_WAIT,就检查当前进程是否需要调度,如果要则会进行调度 */
    might_sleep_if(gfp_mask & __GFP_WAIT);

    /* 检查gfp_mask和order是否符合要求,就是跟fail_page_alloc里面每一项对比检查 */
    if (should_fail_alloc_page(gfp_mask, order))
        return NULL;

    /* 检查结点的管理区链表是否为空 */
    if (unlikely(!zonelist->_zonerefs->zone))
        return NULL;

    /* 如果使能了CMA,选定的页框类型是可迁移的页框,就在标志上加上ALLOC_CMA */
    if (IS_ENABLED(CONFIG_CMA) && migratetype == MIGRATE_MOVABLE)
        alloc_flags |= ALLOC_CMA;

retry_cpuset:
    /* 这里只是表明在这个顺序锁中是个读者 */
    cpuset_mems_cookie = read_mems_allowed_begin();

    /* 获取链表中第一个管理区,这个管理区用于之后的统计 */
    preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
                nodemask ? : &cpuset_current_mems_allowed,
                &preferred_zone);
    if (!preferred_zone)
        goto out;

    /* 获取的管理区的类型的偏移量,0是 ZONE_DMA , 1是 ZONE_NORMAL , 2是 ZONE_HIGHMEM , 3是 ZONE_MOVABLE */
    classzone_idx = zonelist_zone_idx(preferred_zoneref);

    /* 第一次尝试分配页框,第一次是快速分配 */
    /* 遍历zonelist,尝试获取2的order次方个连续的页框 */
    page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
            zonelist, high_zoneidx, alloc_flags,
            preferred_zone, classzone_idx, migratetype);

    if (unlikely(!page)) {
        /* 如果没有分配到所需连续的页框,这里会尝试第二次分配,第二次是慢速分配 */
        gfp_mask = memalloc_noio_flags(gfp_mask);
        /* 如果之前没有分配成功,这里尝试进入慢速分配,在这个函数中,会尝试唤醒页框回收线程,然后再进行分配 */
        page = __alloc_pages_slowpath(gfp_mask, order,
                zonelist, high_zoneidx, nodemask,
                preferred_zone, classzone_idx, migratetype);
    }

    trace_mm_page_alloc(page, order, gfp_mask, migratetype);

out:
    /* 如果都没有分配成功,这里会不停地尝试分配,直到成功 */
    if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
        goto retry_cpuset;
    /* 返回第一个页描述符 */
    return page;
}

   快速分配的函数是get_page_from_freelist(),我们看看:

/* 快速分配页框,从管理区链表中遍历所有管理区,获取指定连续的页框数 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
        struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
        struct zone *preferred_zone, int classzone_idx, int migratetype)
{
    struct zoneref *z;
    struct page *page = NULL;
    struct zone *zone;
    nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
    int zlc_active = 0;        /* set if using zonelist_cache */
    int did_zlc_setup = 0;        /* just call zlc_setup() one time */
    bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                (gfp_mask & __GFP_WRITE);
    int nr_fair_skipped = 0;
    bool zonelist_rescan;

zonelist_scan:
    zonelist_rescan = false;

    /* 遍历结点中的管理区,如果要求从高端内存分配,则顺序为ZONE_HighMen -> ZONE_NORMAL -> ZONE_DMA
     * 如果没要求从高端内存分配,则顺序为ZONE_NORMAL -> ZONE_DMA     * 在nodemask中屏蔽掉的node不会进行遍历
     */
    for_each_zone_zonelist_nodemask(zone, z, zonelist,
                        high_zoneidx, nodemask) {
        unsigned long mark;

        if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
            !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;

        /* 检查此管理区是否属于该CPU所允许分配的管理区 */
        if (cpusets_enabled() &&
            (alloc_flags & ALLOC_CPUSET) &&
            !cpuset_zone_allowed_softwall(zone, gfp_mask))
                continue;

        /* 公平分配,这里我还不太清楚,有没有大神帮忙说明一下 */
        if (alloc_flags & ALLOC_FAIR) {
            if (!zone_local(preferred_zone, zone))
                break;
            if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                nr_fair_skipped++;
                continue;
            }
        }

        /* 如果需要考虑到zone的脏页数量的情况,如果此zone在内存中有过多的脏页,则跳过此zone */
        if (consider_zone_dirty && !zone_dirty_ok(zone))
            continue;

        /* 选择阀值,阀值保存在管理区的watermark中,分别有alloc_min alloc_low alloc_high三种,选择任何一种都会要求分配后空闲页框数量不能少于阀值,默认是alloc_low */
        mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];

        /* 根据阀值查看管理区中是否有足够的空闲页框,空闲内存数量保存在 zone->vm_stat[NR_FREE_PAGES],这里的检查算法是: 当前空闲内存减去需要申请的内存之后,空闲内存是否高于阀值,高于则允许分配 */
        if (!zone_watermark_ok(zone, order, mark,
                       classzone_idx, alloc_flags)) {
            /* 没有足够的空闲页框 */
            int ret;

            BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
            /* 如果分配标志中有 ALLOC_NO_WATERMARKS标志,代表无视阀值,直接分配 */
            if (alloc_flags & ALLOC_NO_WATERMARKS)
                goto try_this_zone;

            if (IS_ENABLED(CONFIG_NUMA) &&
                    !did_zlc_setup && nr_online_nodes > 1) {
                /* NUMA系统中如果使用了zlc(zonelist_cache),则取出此zonelist允许的node列表 */
                allowednodes = zlc_setup(zonelist, alloc_flags);
                zlc_active = 1;
                did_zlc_setup = 1;
            }

            if (zone_reclaim_mode == 0 ||
                !zone_allows_reclaim(preferred_zone, zone))
                goto this_zone_full;

            if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;

            /* 尝试回收zone区的一些可回收页(文件映射使用的页)和一些slab,这里面也挺复杂的,以后的文章分析 */
            ret = zone_reclaim(zone, gfp_mask, order);
            switch (ret) {
            case ZONE_RECLAIM_NOSCAN:      /* 没有进行回收 */
                continue;
            case ZONE_RECLAIM_FULL:         /* 没有找到可回收的页面 */
                continue;
            default:
                /* 回收到了一些页,这里继续检查阀值是否足够分配连续页框,足够则跳到 try_this_zone 尝试分配 */
                if (zone_watermark_ok(zone, order, mark,
                        classzone_idx, alloc_flags))
                    goto try_this_zone;

                /* 没有回收到足够的内存,如果阀值是min,则跳到 this_zone_full 标记此区已满,因为min是这三个阀值当中最小的阀值 */
                if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
                    ret == ZONE_RECLAIM_SOME)
                    goto this_zone_full;

                continue;
            }
        }

try_this_zone:
        /* 尝试从这个管理区获取连续页框,这个管理区有足够的页框分配 */
        page = buffered_rmqueue(preferred_zone, zone, order,
                        gfp_mask, migratetype);
        if (page)
            break;
this_zone_full:
        if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
            /* 在zonelist的zonelist_cache中标记此node为满状态 */
            zlc_mark_zone_full(zonelist, z);
    }

    /* 分配到了连续页框 */
    if (page) {
        /* 如果分配时有 ALLOC_NO_WATERMARKS 标记则记录到页描述符中 */
        page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
        /* 将页描述符返回 */
        return page;
    }

    /* 如果第一次ALLOC_FAIR分配没有能够分配到内存,第二次尝试非ALLOC_FAIR分配 */
    if (alloc_flags & ALLOC_FAIR) {
        alloc_flags &= ~ALLOC_FAIR;
        if (nr_fair_skipped) {
            zonelist_rescan = true;
            reset_alloc_batches(preferred_zone);
        }
        if (nr_online_nodes > 1)
            zonelist_rescan = true;
    }

    if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
        /* 禁止zonelist_cache,zonelist_cache用于快速扫描的,它标记着所有zone中哪个zone有空闲内存哪个zone没有,扫描时就跳过这些zone */
        zlc_active = 0;
        zonelist_rescan = true;
    }

    /* 跳回去,尝试再次扫描一遍zonelist,这里最多只会进行一次再次扫描,因为第二次就不会把 zonelist_rescan 设置为true了 */
    if (zonelist_rescan)
        goto zonelist_scan;

    return NULL;
}

  在快速分配中,如果条件允许会遍历两次zonelist中的zone,整个快速分配的流程是:从zonelist中取出一个zone,检查此zone标志判断是否可通过此zone分配内存,如果 zone的空闲内存 - 需要申请的内存 < 阀值(默认是alloc_low) ,伙伴系统则会将zone的一些页进行回收(主要回收那些映射文件数据的页框和slab的页框),然后再次判断阀值和空闲内存与申请内存大小直接的关系,如果 zone的空闲内存 - 需要申请的内存 > 阀值,则调用buffered_rmqueue()函数从此zone中的分配内存,否则,选取下一个zone继续执行这段操作。当zonelist中的所有zone都遍历完成后,还是没有分配到内存,如果条件允许会再次遍历一遍。

  对于zone_reclaim函数这里就不详细进行分析了(因为自己也没有细看),我们记得在伙伴系统中有一个每CPU高速缓存,里面保存着以migratetype分类的单页框的双向链表,当申请内存者只需要一个页框时,内核会从每CPU高速缓存中相应类型的单页框链表中获取一个页框交给申请者,这样的好处是,但释放单个页框时会放入每CPU高速缓存链表,如果这时有需要申请单个页框,就把这个刚刚释放的页框交付出去,因为这个页框可能还存在于cache中,处理时就可直接处理cache而不用把这个页框再放入cache中,提高了cache的命中率,这样的页框就称为“热”页。每CPU高速缓存维护的这些所有类型的单页框双向链表时,把刚释放的页框从链表头插入,申请“热”页时就从链表头拿出页框,申请“冷”页时则从链表位拿出。我们看看buffered_rmqueue():

static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, int migratetype)
{
    unsigned long flags;
    struct page *page;
    bool cold = ((gfp_flags & __GFP_COLD) != 0);

again:
    if (likely(order == 0)) {
        /* 这里是只需要分配一个页框,会从每CPU高速缓存中分配 */
        struct per_cpu_pages *pcp;
        struct list_head *list;

        local_irq_save(flags);
        /* 获取此zone的每CPU高速缓存 */
        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        /* 获取需要的类型的页框的高速缓存链表,高速缓存中也区分migratetype类型的链表,链表中保存的页框对应的页描述符 */
        list = &pcp->lists[migratetype];
        if (list_empty(list)) {
            /* 如果当前migratetype的每CPU高速缓存链表中没有空闲的页框,从伙伴系统中获取batch个页框加入到这个链表中,batch保存在每CPU高速缓存描述符中,在rmqueue_bulk中是每次要1个页框,要batch次,也就是这些页框是离散的 */
            pcp->count += rmqueue_bulk(zone, 0,
                    pcp->batch, list,
                    migratetype, cold);
            if (unlikely(list_empty(list)))
                goto failed;
        }

        if (cold)
            /* 需要冷的高速缓存,则从每CPU高速缓存的双向链表的后面开始分配 */
            page = list_entry(list->prev, struct page, lru);
        else
            /* 需要热的高速缓存,则从每CPU高速缓存的双向链表的前面开始分配,因为释放时会从链表头插入,所以链表头是热的高速缓存 */
            page = list_entry(list->next, struct page, lru);
        /* 从每CPU高速缓存链表中拿出来 */
        list_del(&page->lru);
        pcp->count--;
    } else {
        /* 需要多个页框,从伙伴系统中分配,但是申请多个页框时是有可能会发生失败的情况的,而分配时又表明__GFP_NOFAIL不允许发生失败,所以这里给出一个警告 */
        if (unlikely(gfp_flags & __GFP_NOFAIL)) {
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        /* 从伙伴系统中获取连续页框,返回第一个页的页描述符 */
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        if (!page)
            goto failed;
        /* 统计,减少zone的free_pages数量统计,因为里面使用加法,所以这里传进负数 */
        __mod_zone_freepage_state(zone, -(1 << order),
                      get_freepage_migratetype(page));
    }

    __mod_zone_page_state(zone, NR_ALLOC_BATCH, -(1 << order));
    if (atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]) <= 0 &&
        !test_bit(ZONE_FAIR_DEPLETED, &zone->flags))
        set_bit(ZONE_FAIR_DEPLETED, &zone->flags);

    __count_zone_vm_events(PGALLOC, zone, 1 << order);
    /* 统计 */
    zone_statistics(preferred_zone, zone, gfp_flags);
    local_irq_restore(flags);

    VM_BUG_ON_PAGE(bad_range(zone, page), page);
    /* 检查所有分配的连续页框是否为空闲页 */
    if (prep_new_page(page, order, gfp_flags))
        goto again;
    /* 返回第一个页描述符 */
    return page;

failed:
    /* 分配失败 */
    local_irq_restore(flags);
    return NULL;
}

  对于每CPU高速缓存,有可能出现的情况就是链表为空(太多人申请1个页框),这时候每CPU高速缓存会向伙伴系统连续申请batch次单个页框放入自己的链表中,这里发生在rmqueue_bulk()中。而如果一次申请多个页框,则直接从伙伴系统中获取,也就是__rmqueue()函数。而对于rmqueue_bulk()来说,其核心函数也是__rmqueue(),因为rmqueue_bulk()中也是调用了batch次__rmqueue()获取batch个单页框。

/* 从伙伴系统中获取2的order次方个页框,返回第一个页框的描述符 */
static struct page *__rmqueue(struct zone *zone, unsigned int order,
                        int migratetype)
{
    struct page *page;

retry_reserve:
    /* 直接从migratetype类型的链表中获取了2的order次方个页框,具体见后面 */
    page = __rmqueue_smallest(zone, order, migratetype);

    /* 如果page为空,没有在需要的migratetype类型中分配获得页框,说明当前需求类型(migratetype)的页框没有空闲,会根据fallback数组中定义好的优先级从其他类型的页框中获取页框 */
    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
        /* 根据fallbacks数组从其他migratetype类型的链表中获取内存,具体见后面 */
        page = __rmqueue_fallback(zone, order, migratetype);

         /* 从其他类型的空闲页框链表中也没有获得页框,设定为默认类型的页框,重试一次 */
        if (!page) {
            /* 定义从页框属性为MIGRATE_RESERVE的空闲链表中查找 */
            migratetype = MIGRATE_RESERVE;
            /* 重试尝试从MIGRATE_RESERVE类型的链表中找出空闲内存 */
            goto retry_reserve;
        }
    }

    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;

    /* 循环遍历这层之后的空闲链表 */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        area = &(zone->free_area[current_order]);
        /* 如果当前空闲链表为空,则从更高一级的链表中获取空闲页框 */
        if (list_empty(&area->free_list[migratetype]))
            continue;
        /* 获取空闲链表中第一个结点所代表的连续页框 */
        page = list_entry(area->free_list[migratetype].next,
                            struct page, lru);
        /* 将页框从空闲链表中删除 */
        list_del(&page->lru);
        /* 将首页框的private设置为0 */
        rmv_page_order(page);
        area->nr_free--;
        /* 如果从更高级的页框的链表中分配,这里会将多余的页框放回伙伴系统的链表中,比如我们只需要2个页框,但是这里是从8个连续页框的链表分配给我们的,那其他6个就要拆分为2和4个分别放入链表中 */
        expand(zone, page, order, current_order, area, migratetype);
        /* 设置页框的类型 */
        set_freepage_migratetype(page, migratetype);
        return page;
    }

    return NULL;
}

/* 根据fallbacks数组中定义的优先级,从其他migratetype类型的链表中获取连续页框,返回第一个页框的页描述符 */
static inline struct page *
__rmqueue_fallback(struct zone *zone, unsigned int order, int start_migratetype)
{
    struct free_area *area;
    unsigned int current_order;
    struct page *page;
    int migratetype, new_type, i;

    for (current_order = MAX_ORDER-1;
                current_order >= order && current_order <= MAX_ORDER-1;
                --current_order) {     /* 遍历不同order的链表,如果需要分配2个连续页框,则会遍历2,4,8,16,32,64,128,256,512,1024这几个链表 */
        for (i = 0;; i++) {      /* 遍历order链表中对应fallbacks优先级的类型链表 */

            /* 根据fallbacks和i获取migratetype */
            migratetype = fallbacks[start_migratetype][i];

            /* 这里不能分配MIGRATE_RESERVE类型的内存,这部分内存是保留使用,最后其他的migratetype都没有内存可分配才会分配MIGRATE_RESERVE类型的内存 */
            if (migratetype == MIGRATE_RESERVE)
                break;

            area = &(zone->free_area[current_order]);
            /* 链表为空,说明这个链表页没有内存 */
            if (list_empty(&area->free_list[migratetype]))
                continue;

            /* 有空余的内存,即将分配 */
            /* 从链表中获取第一个节点,但是注意,这里分配的内存可能大于我们需要的数量(从其他order链表中获取的连续页框),之后会调用expand把多余的放回去 */
            page = list_entry(area->free_list[migratetype].next,
                    struct page, lru);
            area->nr_free--;

            /* 一般情况下 new_type = migratetype  */
            new_type = try_to_steal_freepages(zone, page,
                              start_migratetype,
                              migratetype);

            /* 从伙伴系统中拿出来 */
            list_del(&page->lru);
            /* 设置page->_mapcount = -1 并且 page->private = 0 */
            rmv_page_order(page);

            /* 如果有多余的页框,则把多余的页框放回伙伴系统中 */
            expand(zone, page, order, current_order, area,
                   new_type);

            /* 设置获取的页框的类型为新的类型 */
            set_freepage_migratetype(page, new_type);

            trace_mm_page_alloc_extfrag(page, order, current_order,
                start_migratetype, migratetype, new_type);

            return page;
        }
    }

    return NULL;
}

  整个快速分配大概就是这样,如果申请1个页框则会从每CPU高速缓存分配,如果申请的是多个页框则从伙伴系统中分配。如果都不成功,那就进入慢速分配了。

  对于慢速分配,我也只做一个抛砖引玉,里面涉及的流程既长又复杂,涉及到内存压缩(同步和非同步)、kswapd线程唤醒。这里就不做详细说明了,如果要需要的朋友,可以跟我说,我看看要不要单独拿一篇出来进行说明。

/* 慢速分配页框 */
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
    struct zonelist *zonelist, enum zone_type high_zoneidx,
    nodemask_t *nodemask, struct zone *preferred_zone,
    int classzone_idx, int migratetype)
{
    const gfp_t wait = gfp_mask & __GFP_WAIT;
    struct page *page = NULL;
    int alloc_flags;
    unsigned long pages_reclaimed = 0;
    unsigned long did_some_progress;
    enum migrate_mode migration_mode = MIGRATE_ASYNC;
    bool deferred_compaction = false;
    int contended_compaction = COMPACT_CONTENDED_NONE;

    /* order不能大于11 */
    if (order >= MAX_ORDER) {
        WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
        return NULL;
    }

    /* 调用者指定了GFP_THISNODE标志,表示不能进行内存回收
     * 上层调用者应当在指定了GFP_THISNODE失败后,使用其他标志进行分配
     */
    if (IS_ENABLED(CONFIG_NUMA) &&
        (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
        goto nopage;

restart:
    /* 如果调用者的标志没有禁止kswapd线程标志,则会唤醒这个线程用于页框回收,这里会唤醒所有node结点中的kswap线程,每个node都有一个自己的kswap线程 */
    if (!(gfp_mask & __GFP_NO_KSWAPD))
        wake_all_kswapds(order, zonelist, high_zoneidx,
                preferred_zone, nodemask);

    /* 根据传入标志确定其他的一些标志 */
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

    /* 如果不受CPUSET的限制,则找出优先用于分配的管理区 */
    if (!(alloc_flags & ALLOC_CPUSET) && !nodemask) {
        struct zoneref *preferred_zoneref;
        preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
                NULL, &preferred_zone);
        classzone_idx = zonelist_zone_idx(preferred_zoneref);
    }

rebalance:
    /* 在唤醒回收线程之后再次尝试获取页框 */
    page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist,
            high_zoneidx, alloc_flags & ~ALLOC_NO_WATERMARKS,
            preferred_zone, classzone_idx, migratetype);

    if (page)
        goto got_pg;

    if (alloc_flags & ALLOC_NO_WATERMARKS) {
        /* 这里就是还是没有获取到,尝试忽略阀值再次进行获取页框 */
        zonelist = node_zonelist(numa_node_id(), gfp_mask);

        /* 尝试获取页框,这里不调用zone_watermark_ok(),也就是忽略了阀值,使用管理区预留的页框 */
        page = __alloc_pages_high_priority(gfp_mask, order,
                zonelist, high_zoneidx, nodemask,
                preferred_zone, classzone_idx, migratetype);
        if (page) {
            goto got_pg;
        }
    }

    /* 还是没有分配到,如果调用者不希望等待获取内存,就返回退出 */
    if (!wait) {
        WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL);
        goto nopage;
    }

    /* 调用者本身就是内存回收进程,不能执行后面的内存回收流程,是为了防止死锁 */
    if (current->flags & PF_MEMALLOC)
        goto nopage;

    if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL))
        goto nopage;

    /* 通过压缩看能否有多余的页框,通过页面迁移实现,第一次调用是非同步的,第二次是同步的,具体的还要细看 */
    page = __alloc_pages_direct_compact(gfp_mask, order, zonelist,
                    high_zoneidx, nodemask, alloc_flags,
                    preferred_zone,
                    classzone_idx, migratetype,
                    migration_mode, &contended_compaction,
                    &deferred_compaction);
    if (page)
        goto got_pg;

    if ((gfp_mask & GFP_TRANSHUGE) == GFP_TRANSHUGE) {
        if (deferred_compaction)
            goto nopage;

        if (contended_compaction == COMPACT_CONTENDED_LOCK)
            goto nopage;

        if (contended_compaction == COMPACT_CONTENDED_SCHED
            && !(current->flags & PF_KTHREAD))
            goto nopage;
    }

    if ((gfp_mask & GFP_TRANSHUGE) != GFP_TRANSHUGE ||
                        (current->flags & PF_KTHREAD))
        migration_mode = MIGRATE_SYNC_LIGHT;

    /* 进行内存回收,然后在里面继续分配,返回分配到的内存的第一个页框 */
    page = __alloc_pages_direct_reclaim(gfp_mask, order,
                    zonelist, high_zoneidx,
                    nodemask,
                    alloc_flags, preferred_zone,
                    classzone_idx, migratetype,
                    &did_some_progress);
    if (page)
        goto got_pg;

    /* 还是没有分配到内存 */
    if (!did_some_progress) {
        /* 如果是文件系统操作,并且不允许重试,就是这次一定要分配到内存 */
        if (oom_gfp_allowed(gfp_mask)) {
            if (oom_killer_disabled)
                goto nopage;

            if ((current->flags & PF_DUMPCORE) &&
                !(gfp_mask & __GFP_NOFAIL))
                goto nopage;
            /* 杀死其他进程后再尝试 */
            page = __alloc_pages_may_oom(gfp_mask, order,
                    zonelist, high_zoneidx,
                    nodemask, preferred_zone,
                    classzone_idx, migratetype);
            if (page)
                goto got_pg;

            /* 分配禁止失败 */
            if (!(gfp_mask & __GFP_NOFAIL)) {
                /* 要求的数量太多,没办法 */
                if (order > PAGE_ALLOC_COSTLY_ORDER)
                    goto nopage;
                /* 是从DMA区域要内存,实在没太多内存 */
                if (high_zoneidx < ZONE_NORMAL)
                    goto nopage;
            }

            goto restart;
        }
    }

    /* 回收到了一部分,这里检查是否继续尝试回收 */
    pages_reclaimed += did_some_progress;
    if (should_alloc_retry(gfp_mask, order, did_some_progress,
                        pages_reclaimed)) {
        /* 需要,这里会阻塞一段时间,然后重试 */
        wait_iff_congested(preferred_zone, BLK_RW_ASYNC, HZ/50);
        goto rebalance;
    } else {
        /* 不需要重试,这次再次压缩获取内存,这里是同步方式 */
        page = __alloc_pages_direct_compact(gfp_mask, order, zonelist,
                    high_zoneidx, nodemask, alloc_flags,
                    preferred_zone,
                    classzone_idx, migratetype,
                    migration_mode, &contended_compaction,
                    &deferred_compaction);
        if (page)
            goto got_pg;
    }

nopage:
    /* 没有分配到内存 */
    warn_alloc_failed(gfp_mask, order, NULL);
    return page;
got_pg:
    /* 分配到了 */
    if (kmemcheck_enabled)
        kmemcheck_pagealloc_alloc(page, order, gfp_mask);

    return page;
}
时间: 2024-11-06 07:13:00

linux内存源码分析 - 伙伴系统(初始化和申请页框)的相关文章

linux内存源码分析 - 内存回收(整体流程)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文件页进行的.对于匿名页,内存回收过程中会筛选出一些不经常使用的匿名页,将它们写入到swap分区中,然后作为空闲页框释放到伙伴系统.而对于文件页,内存回收过程中也会筛选出一些不经常使用的文件页,如果此文件页中保存的内容与磁盘中文件对应内容一致,说明此文件页是一个干净的文件页,就不需要进行回写,直接将此

(转)linux内存源码分析 - 内存回收(整体流程)

http://www.cnblogs.com/tolimit/p/5435068.html------------linux内存源码分析 - 内存回收(整体流程) 概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文件页进行的.对于匿名页,内存回收过程中会筛选出一些不经常使用的匿名页,将它们写入到swap分区中,然后作为空闲页框释放到伙伴系统.而对于文件页,内存回收过程中也会筛选出一些不经常使用的文件页,如果此文件页中保存的内容与磁盘中

linux内存源码分析 - SLAB分配器概述

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请者,而且我们知道也可页框大小为4K(也可设置为4M),这时候就会有个问题,如果我只需要1KB大小的内存,页框分配器也不得不分配一个4KB的页框给申请者,这样就会有3KB被白白浪费掉了.为了应对这种情况,在页框分配器上一层又做了一层SLAB层,SLAB分配器的作用就是从页框分配器中拿出一些页框,专门把

linux内存源码分析 - SLUB分配器概述

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ SLUB和SLAB的区别 首先为什么要说slub分配器,内核里小内存分配一共有三种,SLAB/SLUB/SLOB,slub分配器是slab分配器的进化版,而slob是一种精简的小内存分配算法,主要用于嵌入式系统.慢慢的slab分配器或许会被slub取代,所以对slub的了解是十分有必要的. 我们先说说slab分配器的弊端,我们知道slab分配器中每个node结点有三个链表,分别是空闲slab链表,部分空sla

linux内存源码分析 - 内存压缩(同步关系)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 最近在看内存回收,内存回收在进行同步的一些情况非常复杂,然后就想,不会内存压缩的页面迁移过程中的同步关系也那么复杂吧,带着好奇心就把页面迁移的源码都大致看了一遍,还好,不复杂,也容易理解,这里我们就说说在页面迁移过程中是如何进行同步的.不过首先可能没看过的朋友需要先看看linux内存源码分析 - 内存压缩(一),因为会涉及里面的一些知识. 其实一句话可以概括页面迁移时是如何进行同步的,就是:我要开始对这

linux内存源码分析 - 页表的初始化

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 本文章中系统我们假设为x86下的32位系统,暂且不分析64位系统的页表结构. linux分页 linux下采用四级分页,一个线性地址会分为5个偏移量用于寻址,具体看图: 虽然有四级,但并不是每一级都会用到,在linux中,对于硬件体系的不同可能会用到二级页表,三级页表,四级页表中的其中一个,如下: 64位系统:使用四级分页或三级分页,跟硬件有关. 未开启PAE(物理地址扩展)的32位系统:只使用二级分页,页上

linux内存源码分析 - 内存回收(匿名页反向映射)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 看完了内存压缩,最近在看内存回收这块的代码,发现内容有些多,需要分几块去详细说明,首先先说说匿名页的反向映射,匿名页主要用于进程地址空间的堆.栈.还有私有匿名共享内存(用于有亲属关系的进程),这些匿名页所属的线性区叫做匿名线性区,这些线性区只映射内存,不映射具体磁盘上的文件.匿名页的反向映射对匿名页的回收起到了很大的作用.为了进行内存回收,内核为每个zone管理区的内存页维护了5个LRU链表(最近最少使

linux内存源码分析 - 内存回收(lru链表)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 对于整个内存回收来说,lru链表是关键中的关键,实际上整个内存回收,做的事情就是处理lru链表的收缩,所以这篇文章就先说说系统的lru链表. 内存回收的核心思想,就是如果一些数据能够保存到磁盘,在内存不足时就把这些数据写到磁盘中,这样这些数据占用的内存页就可以作为空闲内存页给予系统使用了. 当内存不足时,系统就必须要将一些页框回收,而哪些页框可以回收呢,之前我们有说过,属于内核的大部分页框是不能够进行回

Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7)【转】

原文地址:Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinaunix.net/uid-25909619-id-4938395.html 前面粗略分析start_kernel函数,此函数中基本上是对内存管理和各子系统的数据结构初始化.在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就是创建并启动内核线