linux内存源码分析 - 内存压缩

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

 

概述

  本文章最好结合linux内存管理源码分析 - 页框分配器与linux内存源码分析 -伙伴系统(初始化和申请页框)一起看,会涉及里面的一些知识。

  我们知道内存是以页框为单位,每个页框大小默认是4K(大页除外),而在系统运行时间长后就会出现内存碎片,内存碎片的意思就是一段空闲页框中,会有零散的一些正在使用的页框,导致此段页框被这些正在使用的零散页框分为一小段一小段连续页框,这样当需要大段连续页框时就没办法分配了,这些空闲页框就成了一些碎片,不能合并起来作为一段大的空闲页框使用,如下图:

linux内存源码分析 - 内存压缩_第1张图片

  白色的为空闲页框,而有斜线的为已经在使用的页框,在这个图中,空闲页框都是零散的,它们没办法组成一块连续的空闲页框,它们只能单个单个进行分配,当内核需要分配连续页框时则没办法从这里分配。为了解决这个问题,内核实现了内存压缩功能,其原理很简单,就是从这块内存区段的前面扫描可移动的页框,从内存区段后面向前扫描空闲的页框,两边扫描结束后,将可移动的页框放入到空闲页框中,最后最理想的结果就如下图:

  这样移动之后就把前面的页框整理为了一大段连续的物理页框了,当然这只是理想情况,因为并不是所有页框都可以进行移动,像内核使用的页框大部分都不能够移动,而用户进程的页框大部分是可以移动了,之后会分析源码看看系统是如何实现的。

  现在再来说说什么时候会进行内存压缩。它会在两个地方调用到:

  • 内核从伙伴系统中获取连续页框,但是连续页框又不足时。
  • 因为内存短缺导致kswapd被唤醒时,在进行内存回收之后会进行内存压缩。

  而内存压缩是一个相当耗费资源的事情,它并不会经常会执行,即使因为内存短缺导致代码中经常调用到内存压缩函数,它也会根据调用次数选择性地忽略一些执行请求。

  系统判定是否执行内存压缩的标准是

  • 在分配页框过程中,管理区显示是有足够的空闲页框供于本次分配的,但是伙伴系统链表中又没有连续页框段用于本次分配。原因就是过多分散的空闲页框,它们没办法组成一块连续页框存放在伙伴系统的链表中。
  • 在kswapd唤醒后会对zone的页框阀值进行检查,如果可用页框少于高阀值则会进行内存回收,每次进行内存回收之后会进行内存压缩。

  而实际上无论唤醒kswapd执行内存压缩还是连续页框不足执行内存压缩,它们的入口都是alloc_pages()函数,因为kswapd不是间断性自动唤醒,而是在分配页框时页框不足的情况下被主动唤醒,在内存足够的情况下,kswapd是不会被唤醒的,而分配页框的函数入口就是alloc_pages(),会在此函数里面判断页框是否足够。

 

伙伴系统(补充)

  在之前的文章有描述伙伴系统,这里需要对其进行更详细的补充,才能够继续往下说明内存压缩。首先,在内核里页框被分为以下几种类型:

/* 这几个链表主要用于反内存碎片 */
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
};

  在内存压缩中,可以移动的页框只有MIGRATE_MOVABLE与MIGRATE_CMA这两种类型的页框,而MIGRATE_RECLAIMABLE是内存回收时的目标类型。

  在每个内存管理区的伙伴系统中,都维护着以这些类型区分开的空闲页框段链表,如下:

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

  ....../* 标识出管理区中的空闲页框块,用于伙伴系统 */
    /* 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;
};

  而在每个管理区的伙伴系统初始化过程中,会把所有空闲页框都设置为MIGRATE_MOVABLE类型,并放入到对应管理区的free_list[MIGRATE_MOVABLE]这个链表中,而根据释放页框函数(见linux内存源码分析 - 伙伴系统(释放页框))可以看出,在初始化过程中,释放的所有页框几乎都放到了free_area[10].free_list[MIGRATE_MOVABLE]这个链表中来,其他链表几乎为空。在这里思考一下,在初始化伙伴系统完成后,除了内核正在使用的页框(这些页框类型为MIGRATE_UNMOVABLE),其他所有空闲页框都被设置为可移动页框(MIGRATE_MOVABLE),其他类型的页框几乎都为空。再去看看内核分配页框函数,里面提到了一个fallbacks:

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_MOVABLE。我们注意这一行

[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },

  而当我们映射一个文件时,申请页框时会表明我们需要申请MIGRATE_RECLAIMABLE类型的页框,但是这种类型的页框空缺的,伙伴系统会先去MIGRATE_UNMOVABLE类型的链表中获取一段连续空闲页框,如果MIGRATE_UNMOVABLE类型也为空缺的,会再往下,从MIGRATE_MOVABLE类型获取一段连续页框,这种类型在初始化之后是最多的,在获取这段连续页框成功后,会把这段页框设置为我们需要的MIGRATE_RECLAIMABLE类型。这整个过程对整个内存压缩有着至关重要的关系,说到这个从其他类型的free_list中获取页框,就必须要说到pageblock,pageblock中对内存压缩来说最重要的就两个变量:

#ifdef CONFIG_HUGETLB_PAGE

#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE

/* Huge page sizes are variable */
extern int pageblock_order;

#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

/* Huge pages are a constant size */
#define pageblock_order        HUGETLB_PAGE_ORDER

#endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

#else /* CONFIG_HUGETLB_PAGE */

/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order        (MAX_ORDER-1)

#endif /* CONFIG_HUGETLB_PAGE */

#define pageblock_nr_pages    (1UL << pageblock_order)

  pageblock_nr_pages和pageblock_order,pageblock_order有两种情况:2的pageblock_order次方等于大页大小,或者pageblock_order=MAX_ORDER。也就是一个pageblock的大小要么等于大页的大小,要么等于一个free_list[10]链表中一个1024个连续页框的大小。在默认不是用大页的情况下,一个pageblock就是1024个连续页框大小,内核会将内存简单地分为一个一个pageblock,每个pageblock为1024个连续页框,0~1023为一个pageblock,1024~2047为一个pageblock。当从其他类型页框的free_list中拿去一段连续页框时,是以一个pageblock为单位的,也就是说,这个pageblock中的所有页框都会被设置为我们需要的页框类型,如下图:

linux内存源码分析 - 内存压缩_第2张图片

  这里只用了order为10的连续空闲页框链表说例子,实际上用order为其他的也一样,因为转移的最小单位为一个pageblock。而如果在一个pageblock中有部分页已经被使用的情况,这种情况是有可能发生的,因为在检索目标类型的free_are[ORDER]时,ORDER是从大到小进行搜索的,如果ORDER=10的时候没有空闲的连续页框供于使用,则会到ORDER=9的free_list[目标类型]进行查找,这样的话就有可能导致一个pageblokc中可能有512个页框是处于使用中的,而另外512个是空闲的。在这种情况下, 如果此块pageblock被移动到新类型的页框数量大于等于此块pageblock的页框总数量的一半,则会把pageblock设置为新的类型,否则不设置。这样就会有一种情况,就是当一些页正在使用时,这些页所属的pageblock被移动到了其他类型的伙伴系统中,会导致pageblock的类型与正在使用的页的类型不一致。内核为了解决这种情况,会在页释放的时候检查释放的页是否与所属pageblock的类型一致,不一致则将释放页的类型更改为与pageblock一致的类型。

  从以上总结出来,当从不同类型的伙伴系统中获取连续页框时,是一pageblock为单位,而每个pageblock标记了这块pageblock都属于同一种类型,即使有些正在使用的页不能在pageblock移动时立即更改类型,但这些页也会在被释放时检查是否与所属pageblock一致,并进行更改。这里我们具体看看代码,我们从__rmqueue函数进行分析,如果对之前流程不太清楚的,可以看看linux内存源码分析 -伙伴系统(初始化和申请页框)。

/* 从伙伴系统中获取2的order次方个页框,返回第一个页框的描述符
 * zone: 管理区描述符
 * order: 需要页面的2的次方数
 * migratetype: 从此类型中获取,这时传入的时需求的页框类型
 */
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);

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

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

 

  再进入到__rmqueue_fallback中看看:

/* Remove an element from the buddy allocator from the fallback list */
/* 根据fallbacks数组中定义的优先级,从其他migratetype类型的链表中获取连续页框,返回第一个页框的页描述符 
 * start_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;

    /* Find the largest possible block of pages in the other list */
    for (current_order = MAX_ORDER-1;
                current_order >= order && current_order <= MAX_ORDER-1;
                --current_order) {     /* 遍历不同order的链表,如果需要分配2个连续页框,则会遍历1024,512,256,128,64,32,16,8,4,2,1这几个链表,注意这里是倒着遍历的 */
        for (i = 0;; i++) {      /* 遍历order链表中对应fallbacks优先级的类型链表 */

            /* 根据fallbacks和i获取migratetype,start_migratetype是申请页框时需要的类型 */
            /*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 }, 
             *#else
             *    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
             *#endif
             *    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, 
             *#ifdef CONFIG_MEMORY_ISOLATION
             *    [MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, 
             *#endif
             *};
             */
            migratetype = fallbacks[start_migratetype][i];

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

            /* 当前order的链表,current_order从10 ~ order */
            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--;

            /* 在当前start_migratetype中没有足够的页进行分配时,则会从migratetype获取order或者pageblock相等数量的页框放到start_migratetype中的order链表中,返回获取的页框本来所属的类型  
             * 代码中不建议把过低order数量的页框进行移动,最小移动单位是一个pageblock,它的大小是1024个页框或者一个大页的大小。如果order过小则可能不会移动
             */
            new_type = try_to_steal_freepages(zone, page,
                              start_migratetype,
                              migratetype);

            /* 从伙伴系统中拿出来,因为在try_to_steal_freepages已经将新的页框放到了需要的start_mirgatetype的链表中
             * 并且此order并不一定是所需要order的上级,因为order是倒着遍历了,比如我们需要32个MIGRATE_UNMOVABLE页框,但是移动的是1024个MIGRATE_MOVABLE页框到MIGRATE_UNMOVABLE的order=10的链表中。
             */
            list_del(&page->lru);
            /* 设置page->_mapcount = -1 并且 page->private = 0 */
            rmv_page_order(page);

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

            /* 设置获取的页框的类型为新的类型,因为在try_to_steal_freepages()中cma类型是直接返回的,而其他类型都会在里面被设置,page->index = new_type
             * 到这里,page已经是一个2^oder连续页框的内存段,之后就把它返回到申请者就好
             */
            set_freepage_migratetype(page, new_type);

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

            return page;
        }
    }

    return NULL;
}

 

  最后看看try_to_steal_freepages()函数:

/* 从page所在的伙伴系统中拿出page所在这段pageblock块,将这个pageblock从旧了类型移动到新的类型链表中,order不变,返回移动的页数量 
 * start_type是我们需要的migratetype,fallback_type是当前遍历到的migratetype
 */
static int try_to_steal_freepages(struct zone *zone, struct page *page,
                  int start_type, int fallback_type)
{
    /* page是当前遍历到的migratetype当中order页的首页描述符,并不是我们需要的migratetype中的页
     * order是当前遍历到的migratetype当中order,并不是当前需要分配的order
     *
     */
    int current_order = page_order(page);

    /*
     * When borrowing from MIGRATE_CMA, we need to release the excess
     * buddy pages to CMA itself. We also ensure the freepage_migratetype
     * is set to CMA so it is returned to the correct freelist in case
     * the page ends up being not actually allocated from the pcp lists.
     */
    if (is_migrate_cma(fallback_type))
        return fallback_type;

    /* 如果当前需要的order值大于默认一个内存块的order值(这个值为MAX_ORDER-1或者大页的大小),就算出需要多少块pageblock才能达到order,然后把这些pageblock都设置为start_type 
     * 这种情况发生在pageblock_order等于大页的大小,而内核配置了CONFIG_FORCE_ORDER,导致order >= pageblock_order
     */
    if (current_order >= pageblock_order) {
        /* 计算出pageblock的块数,然后将每一块都设置为需要的类型,这种情况下并没有把它们从旧类型的伙伴系统移到需要类型的伙伴系统中,在外面函数会将其移出来 */
        change_pageblock_range(page, current_order, start_type);
        return start_type;
    }

    /* 如果order大于pageblock_order的一半,或者类型是MIGRATE_RECLAIMABLE,或者内核关闭了页可迁移的特性,则从此页所属的mirgatetype和order链表中获取页框放到start_type中 
     * 如果oder小于pageblock_order / 2并且start_type != MIGRATE_RECLAIMABLE并且page_group_by_mobility_disabled == false,就不会移动页框。
     */
    if (current_order >= pageblock_order / 2 ||
        start_type == MIGRATE_RECLAIMABLE ||
        page_group_by_mobility_disabled) {
        int pages;

        /* 将这个pageblock从旧了类型移动到新的类型链表中,order不变,返回移动的页数量,但是已经在使用的页会被跳过,并且这些已经被使用的页不会被更改为新的类型 */
        pages = move_freepages_block(zone, page, start_type);

        /* 如果这块pageblock中的页数量大于pageblock的页数量的一半,则设置这块为新的migratetype类型,如果小于,则不会把此pageblock设置为新的类型,但是move_freepages_block移动的页已经设置为新的类型 
         * 对于这种情况,在这些正在使用的块被释放时,会被检查是否与所属pageblock的类型一致,不一致则会设置为一致
         */
        if (pages >= (1 << (pageblock_order-1)) ||
                page_group_by_mobility_disabled) {

            set_pageblock_migratetype(page, start_type);
            return start_type;
        }
    }
    /* 返回是从哪个migratetype中移动的页框 */
    return fallback_type;
}

 

  move_freepages_block():

/* 调整为一个pageblock,将这个pageblock从旧了类型移动到新的类型链表中,order不变 */
int move_freepages_block(struct zone *zone, struct page *page,
                int migratetype)
{
    unsigned long start_pfn, end_pfn;
    struct page *start_page, *end_page;

    /* 调整为pageblock对齐 */
    start_pfn = page_to_pfn(page);
    start_pfn = start_pfn & ~(pageblock_nr_pages-1);
    start_page = pfn_to_page(start_pfn);
    end_page = start_page + pageblock_nr_pages - 1;
    end_pfn = start_pfn + pageblock_nr_pages - 1;

    /* Do not cross zone boundaries */
    /* 检查开始页框和结束页框是否都处于zone中,如果不属于,则用page作为开始页框 */
    if (!zone_spans_pfn(zone, start_pfn))
        start_page = page;
    /* 如果结束页框不属于zone,则直接返回0 */
    if (!zone_spans_pfn(zone, end_pfn))
        return 0;

    /* 将这个pageblock从旧了类型移动到新的类型链表中,order不变,正在使用的页会被跳过 */
    return move_freepages(zone, start_page, end_page, migratetype);
}

  move_freepages():

/* 将此段页框移动到新的migratetype类型的伙伴系统链表中,正在使用的页会被跳过 */
int move_freepages(struct zone *zone,
              struct page *start_page, struct page *end_page,
              int migratetype)
{
    struct page *page;
    unsigned long order;
    int pages_moved = 0;

#ifndef CONFIG_HOLES_IN_ZONE
    /*
     * page_zone is not safe to call in this context when
     * CONFIG_HOLES_IN_ZONE is set. This bug check is probably redundant
     * anyway as we check zone boundaries in move_freepages_block().
     * Remove at a later date when no bug reports exist related to
     * grouping pages by mobility
     */
    VM_BUG_ON(page_zone(start_page) != page_zone(end_page));
#endif
    /* 遍历这组页框 */
    for (page = start_page; page <= end_page;) {
        /* Make sure we are not inadvertently changing nodes */
        VM_BUG_ON_PAGE(page_to_nid(page) != zone_to_nid(zone), page);

        /* 检查页框和页框号是否属于内存,如果不正确则跳过 */
        if (!pfn_valid_within(page_to_pfn(page))) {
            page++;
            continue;
        }

        /* 如果页框不在伙伴系统中则跳到下一页,通过判断page->_mapcount是否等于-128 */
        if (!PageBuddy(page)) {
            page++;
            continue;
        }

        /* 获取此页框的order号,保存在page->private中 */
        order = page_order(page);
        /* 从伙伴系统中拿出来,并放到新的migratetype类型中的order链表中 */
        list_move(&page->lru,
              &zone->free_area[order].free_list[migratetype]);
        /* 将这段空闲页框设置为新的类型page->index = migratetype */
        set_freepage_migratetype(page, migratetype);
        /* 跳过此order个页框数量 */
        page += 1 << order;
        /* 记录拿出来了多少个页框 */
        pages_moved += 1 << order;
    }
    /* 返回一共拿出来的页框 */
    return pages_moved;
}

 

 

内存压缩

  上面说到页分很多种类型,而对于内存压缩来说,只会正对三种类型进行压缩,分别是:MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_CMA。并且内存压缩是耗费一定的内存、CPU和IO的。

  内存压缩分为两种,同步内存压缩和异步内存压缩。通过看代码不太理解同步和异步有什么大的不同,大概来说,异步的情况下是不处理MIGRATE_RECLAIMABLE类型的页框的,只会处理MIGRATE_MOVABLE和MIGRATE_CMA两种类型,并且异步情况下有时候需要占有锁。

  先说一下内存压缩的算法,首先,内存压缩是以zone为单位的,而zone中又以pageblock为单位。在内存压缩开始前,会在zone的头和尾各设置一个指针,头指针从头向尾扫描可移动的页,而尾指针从尾向头扫描空闲的页,当他们相遇时终止压缩。下图就是简要的说明图:

初始时内存状态(默认所有正在使用的页框都为可移动):

从头扫描可移动页框:

linux内存源码分析 - 内存压缩_第3张图片

从尾扫描空闲页框:

linux内存源码分析 - 内存压缩_第4张图片

结果:

  但是实际情况并不是与上面图示的情况完全一致。头指针每次扫描一个符合要求的pageblock里的所有页框,当pageblock不为MIGRATE_MOVABLE、MIGRATE_CMA、MIGRATE_RECLAIMABLE时会跳过这些pageblock,当扫描完这个pageblock后有可移动的页框时,会变为尾指针以pageblock为单位向前扫描可移动页框数量的空闲页框,但是在pageblock中也是从开始页框向结束页框进行扫描,最后会将前面的页框内容复制到这些空闲页框中。

linux内存源码分析 - 内存压缩_第5张图片

  需要注意,扫描可移动页框是要先判断pageblock的类型是否符合,符合的pageblock才在里面找可移动的页框,当扫描了一个符合的pageblock后本次扫描可移动页框会停止,转到扫描空闲页框。而扫描空闲页框时也会根据pageblock进行扫描,只是从最后一个pageblock向前扫描,而在每个pageblock里面,也是从此pageblock开始页框向pageblock结束页框进行扫描。当需要的空闲页框数量=扫描到的一个pageblock中可移动的页框数量时,则会停止。当两个指针相会时会停止本次内存压缩。具体的我们看代码,内存压缩的代码主要函数是try_to_compact_pages(),在这个函数中,会对每个zone都进行内存压缩,但是内存压缩的前提是将内存压缩推迟到不能推迟的地步了,我的理解是,内存压缩比较耗费CPU和内存,内核不能频繁去做内存压缩,只有做个阀值,当管理区的内存压缩请求达到这个阀值后,才进行内存压缩,这样能避免掉频繁的内存压缩请求:

/* 尝试每个管理区进行内存压缩空闲出一些页 
 * order: 2的次方,如果是分配时调用到,这个就是分配时希望获取的order,如果是通过写入/proc/sys/vm/compact_memory文件进行强制内存压缩,order就是-1
 *
 */
unsigned long try_to_compact_pages(struct zonelist *zonelist,
            int order, gfp_t gfp_mask, nodemask_t *nodemask,
            enum migrate_mode mode, int *contended,
            struct zone **candidate_zone)
{
    enum zone_type high_zoneidx = gfp_zone(gfp_mask);
    /* 表示运行使用文件系统IO */
    int may_enter_fs = gfp_mask & __GFP_FS;
    /* 表示可以使用磁盘IO */
    int may_perform_io = gfp_mask & __GFP_IO;
    struct zoneref *z;
    struct zone *zone;
    int rc = COMPACT_DEFERRED;
    int alloc_flags = 0;
    int all_zones_contended = COMPACT_CONTENDED_LOCK; /* init for &= op */

    *contended = COMPACT_CONTENDED_NONE;

    /* Check if the GFP flags allow compaction */
    /* 如果order=0或者不允许使用文件系统IO和磁盘IO,则跳过本次压缩,因为不使用IO有可能导致死锁 */
    if (!order || !may_enter_fs || !may_perform_io)
        return COMPACT_SKIPPED;

#ifdef CONFIG_CMA
    /* 在启动了CMA的情况下,如果标记了需要的内存为MIGRATE_MOVABLE,则添加一个ALLOC_CMA标志 */
    if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
        alloc_flags |= ALLOC_CMA;
#endif
    /* Compact each zone in the list */
    /* 遍历管理区链表中所有的管理区 */
    for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx,
                                nodemask) {
        int status;
        int zone_contended;

        /* 检查管理区设置中是否需要跳过此次压缩 
         * 判断标准是:
         * zone->compact_considered是否小于1UL << zone->compact_defer_shift
         * 小于则推迟,并且zone->compact_considered++,也就是这个函数会主动去推迟此管理区的内存压缩
         */
        if (compaction_deferred(zone, order))
            continue;

        /* 进行管理区的内存压缩 */
        status = compact_zone_order(zone, order, gfp_mask, mode,
                            &zone_contended);
        rc = max(status, rc);
        /*
         * It takes at least one zone that wasn't lock contended
         * to clear all_zones_contended.
         */
        all_zones_contended &= zone_contended;

        /* If a normal allocation would succeed, stop compacting */
        /* 判断压缩后是否足够进行内存分配,如果足够,则不会对下个zone进行压缩了,直接跳出 */
        if (zone_watermark_ok(zone, order, low_wmark_pages(zone), 0,
                      alloc_flags)) {
            *candidate_zone = zone;
            /*
             * We think the allocation will succeed in this zone,
             * but it is not certain, hence the false. The caller
             * will repeat this with true if allocation indeed
             * succeeds in this zone.
             */
            /* 重新设置内存压缩计数器,让其归0,也就是重新计算内存压缩请求 */
            compaction_defer_reset(zone, order, false);
            /*
             * It is possible that async compaction aborted due to
             * need_resched() and the watermarks were ok thanks to
             * somebody else freeing memory. The allocation can
             * however still fail so we better signal the
             * need_resched() contention anyway (this will not
             * prevent the allocation attempt).
             */
            /* 异步情况下需要被调度时会设置 */
            if (zone_contended == COMPACT_CONTENDED_SCHED)
                *contended = COMPACT_CONTENDED_SCHED;

            goto break_loop;
        }

        /* 如果是同步压缩 */
        if (mode != MIGRATE_ASYNC) {
            /*
             * We think that allocation won't succeed in this zone
             * so we defer compaction there. If it ends up
             * succeeding after all, it will be reset.
             */
            /* 提高内存压缩计数器的阀值,zone的内存压缩计数器阀值 */
            defer_compaction(zone, order);
        }

        /*
         * We might have stopped compacting due to need_resched() in
         * async compaction, or due to a fatal signal detected. In that
         * case do not try further zones and signal need_resched()
         * contention.
         */
        if ((zone_contended == COMPACT_CONTENDED_SCHED)
                    || fatal_signal_pending(current)) {
            *contended = COMPACT_CONTENDED_SCHED;
            goto break_loop;
        }

        continue;
break_loop:
        /*
         * We might not have tried all the zones, so  be conservative
         * and assume they are not all lock contended.
         */
        all_zones_contended = 0;
        break;
    }

    /*
     * If at least one zone wasn't deferred or skipped, we report if all
     * zones that were tried were lock contended.
     */
    if (rc > COMPACT_SKIPPED && all_zones_contended)
        *contended = COMPACT_CONTENDED_LOCK;

    return rc;
}

 

  主要的是compact_zone_order(),这个函数里主要初始化一个struct compact_control结构体,此结构体用于控制此zone的内存压缩的流程,其组成如下:

struct compact_control {
    /* 扫描到的空闲页的页的链表 */
    struct list_head freepages;    /* List of free pages to migrate to */
    /* 扫描到的可移动的页的链表 */
    struct list_head migratepages;    /* List of pages being migrated */
    /* 空闲页链表中的页数量 */
    unsigned long nr_freepages;    /* Number of isolated free pages */
    /* 可移动页链表中的页数量 */
    unsigned long nr_migratepages;    /* Number of pages to migrate */
    /* 空闲页框扫描所在页框号 */
    unsigned long free_pfn;        /* isolate_freepages search base */
    /* 可移动页框扫描所在页框号 */
    unsigned long migrate_pfn;    /* isolate_migratepages search base */
    enum migrate_mode mode;        /* Async or sync migration mode */
    bool ignore_skip_hint;        /* Scan blocks even if marked skip */
    bool finished_update_free;    /* True when the zone cached pfns are
                     * no longer being updated
                     */
    bool finished_update_migrate;
    /* 申请内存时需要的页框的order值 */
    int order;            /* order a direct compactor needs */
    const gfp_t gfp_mask;        /* gfp mask of a direct compactor */
    /* 扫描的管理区 */
    struct zone *zone;
    /* 保存结果 */
    int contended;            /* Signal need_sched() or lock
                     * contention detected during
                     * compaction
                     */
};

 

  扫描到的可移动页和空闲页都会保存在这个结构体中的链表里。

  再继续看compact_zone_order()函数,里面主要调用了compact_zone():

static unsigned long compact_zone_order(struct zone *zone, int order,
        gfp_t gfp_mask, enum migrate_mode mode, int *contended)
{
    unsigned long ret;
    struct compact_control cc = {
        /* 压缩结束后空闲页框数量 */
        .nr_freepages = 0,
        /* 压缩结束后移动的页框数量 */
        .nr_migratepages = 0,
        .order = order,
        /* 表示需要移动的页框类型,有movable和reclaimable两种,可以同时设置 */
        .gfp_mask = gfp_mask,
        /* 管理区 */
        .zone = zone,
        /* 同步或异步 */
        .mode = mode,
    };
    /* 初始化一个空闲页框链表头 */
    INIT_LIST_HEAD(&cc.freepages);
    /* 初始化一个movable页框链表头 */
    INIT_LIST_HEAD(&cc.migratepages);

    /* 进行内存压缩 */
    ret = compact_zone(zone, &cc);

    VM_BUG_ON(!list_empty(&cc.freepages));
    VM_BUG_ON(!list_empty(&cc.migratepages));

    *contended = cc.contended;
    return ret;
}

  这里面又调用了compact_zone(),这个函数里首先会判断是否进行内存压缩,有三种情况:

  • COMPACT_PARTICAL: 此zone内存足够用于分配要求的2^order个页框,不用进行内存压缩。
  • COMPACT_SKIPPED: 此zone内存不足以进行内存压缩,判断条件是此zone的空闲页框数量少于 zone的低阀值 + (2 << order)。
  • COMPACT_CONTINUE: 此zone可以进行内存压缩。

  判断完能否进行内存压缩后,还需要判断是否在kswapd内核线程中进行内存压缩的,如果在kswapd中进行内存压缩,则会跳过一些标记了PB_migrate_skip的pageblock。而如果不是在kswapd中,则将此zone的所有pageblock都标记为不可跳过的(清除PB_migrate_skip)。之后会初始化两个扫描的起始页框号,也就是将扫描可移动页框的指针设置为zone的开始页框号,扫描空闲页框的指针设置为zone的结束页框号。之后如上面所说,循环扫描pageblock,对每个pageblock进行可移动页框的移动,如下:

/* 内存压缩主要实现函数 */
static int compact_zone(struct zone *zone, struct compact_control *cc)
{
    int ret;
    /* 管理区开始页框号 */
    unsigned long start_pfn = zone->zone_start_pfn;
    /* 管理区结束页框号 */
    unsigned long end_pfn = zone_end_pfn(zone);
    /* 获取可进行移动的页框类型(__GFP_RECLAIMABLE、__GFP_MOVABLE) */
    const int migratetype = gfpflags_to_migratetype(cc->gfp_mask);
    /* 同步还是异步 
     * 同步为1,异步为0
     */
    const bool sync = cc->mode != MIGRATE_ASYNC;

    /* 根据传入的cc->order判断此次压缩是否能够进行,主要是因为压缩需要部分内存,这里面会判断内存是否足够 */
    ret = compaction_suitable(zone, cc->order);
    switch (ret) {
    /* 内存足够用于分配,所以此次压缩直接跳过 */
    case COMPACT_PARTIAL:
    /* 内存数量不足以进行内存压缩 */
    case COMPACT_SKIPPED:
        /* Compaction is likely to fail */
        return ret;
    /* 可以进行内存压缩 */
    case COMPACT_CONTINUE:
        /* Fall through to compaction */
        ;
    }

    /* 如果不是在kswapd线程中,并且推迟次数超过了最大推迟次数则会执行这个if语句 */
    if (compaction_restarting(zone, cc->order) && !current_is_kswapd())
        /* 设置zone中所有pageblock都不能跳过扫描 */
        __reset_isolation_suitable(zone);

    /* 可移动页框扫描起始页框号,在__reset_isolation_suitable会被设置为zone中第一个页框 */
    cc->migrate_pfn = zone->compact_cached_migrate_pfn[sync];
    /* 空闲页框扫描起始页框号,这两个都是在__reset_isolation_suitable()中设置,此项被设置为zone中最后一个页框 */
    cc->free_pfn = zone->compact_cached_free_pfn;
    /* 检查cc->free_pfn,并将其重置到管理区最后一个完整pageblock的最后一块页框,
     * 有可能管理区的大小并不是pageblock的整数倍,最后一个pageblock不是完整的,就把这个页框块忽略,不进行扫描 
     */
    if (cc->free_pfn < start_pfn || cc->free_pfn > end_pfn) {
        cc->free_pfn = end_pfn & ~(pageblock_nr_pages-1);
        zone->compact_cached_free_pfn = cc->free_pfn;
    }
    /* 同上,检查cc->migrate_pfn,并整理可移动页框扫描的起始页框 */
    if (cc->migrate_pfn < start_pfn || cc->migrate_pfn > end_pfn) {
        cc->migrate_pfn = start_pfn;
        zone->compact_cached_migrate_pfn[0] = cc->migrate_pfn;
        zone->compact_cached_migrate_pfn[1] = cc->migrate_pfn;
    }

    trace_mm_compaction_begin(start_pfn, cc->migrate_pfn, cc->free_pfn, end_pfn);
    
    /* 将处于pagevec中的页都放回原本所属的lru中,这一步很重要 */
    migrate_prep_local();

    /* compact_finished用于判断是否已经完成内存压缩
     * 主要判断cc->free_pfn <= cc->migrate_pfn,并且没有发生错误,cc->contended中保存是否需要终止
     */
    while ((ret = compact_finished(zone, cc, migratetype)) ==
                        COMPACT_CONTINUE) {
        int err;

        /* 将可移动页(MOVABLE和CMA和RECLAIMABLE)从zone->lru隔离出来,存到cc->migratepages这个链表,一个一个pageblock进行扫描,当一个pageblock扫描成功获取到可移动页后就返回
         * 一次扫描最多32*1024个页框
         */
        /* 异步不处理RECLAIMABLE页 */
        switch (isolate_migratepages(zone, cc)) {
        case ISOLATE_ABORT:
            /* 失败,把这些页放回到lru或者原来的地方 */
            ret = COMPACT_PARTIAL;
            putback_movable_pages(&cc->migratepages);
            cc->nr_migratepages = 0;
            goto out;
        case ISOLATE_NONE:
            continue;
        case ISOLATE_SUCCESS:
            ;
        }

        /* 将隔离出来的页进行迁移,如果到这里,cc->migratepages中最多也只有一个pageblock的页框数量,并且这些页框都是可移动的 
         * 空闲页框会在compaction_alloc中获取
         * 也就是把隔离出来的一个pageblock中可移动页进行移动
         */
        err = migrate_pages(&cc->migratepages, compaction_alloc,
                compaction_free, (unsigned long)cc, cc->mode,
                MR_COMPACTION);

        trace_mm_compaction_migratepages(cc->nr_migratepages, err,
                            &cc->migratepages);

        /* All pages were either migrated or will be released */
        /* 设置所有可移动页框为0 */
        cc->nr_migratepages = 0;
        if (err) {
            /* 将剩余的可移动页框返回原来的位置 */
            putback_movable_pages(&cc->migratepages);
            /*
             * migrate_pages() may return -ENOMEM when scanners meet
             * and we want compact_finished() to detect it
             */
            if (err == -ENOMEM && cc->free_pfn > cc->migrate_pfn) {
                ret = COMPACT_PARTIAL;
                goto out;
            }
        }
    }

out:
    /* 将剩余的空闲页框放回伙伴系统 */
    cc->nr_freepages -= release_freepages(&cc->freepages);
    VM_BUG_ON(cc->nr_freepages != 0);

    trace_mm_compaction_end(ret);

    return ret;
}

  这里我们先看看内核是如何进行隔离可移动页框的。

 

隔离可移动页框

  隔离可移动页框是以pageblock为单位,主要函数为isolate_migratepages():

/* 从cc->migrate_pfn(保存的是扫描可移动页框指针所在的页框号)开始到第一个获取到可移动页框的pageblock结束,获取可移动页框,并放入到cc->migratepages */
static isolate_migrate_t isolate_migratepages(struct zone *zone,
                    struct compact_control *cc)
{
    unsigned long low_pfn, end_pfn;
    struct page *page;
    /* 保存同步/异步方式,只有异步的情况下能进行移动页框ISOLATE_ASYNC_MIGRATE */
    const isolate_mode_t isolate_mode =
        (cc->mode == MIGRATE_ASYNC ? ISOLATE_ASYNC_MIGRATE : 0);

    /*
     * Start at where we last stopped, or beginning of the zone as
     * initialized by compact_zone()
     */
    /* 扫描起始页框 */
    low_pfn = cc->migrate_pfn;

    /* Only scan within a pageblock boundary */
    /* 以1024对齐 */
    end_pfn = ALIGN(low_pfn + 1, pageblock_nr_pages);

    /*
     * Iterate over whole pageblocks until we find the first suitable.
     * Do not cross the free scanner.
     */
    for (; end_pfn <= cc->free_pfn;
            low_pfn = end_pfn, end_pfn += pageblock_nr_pages) {

        /*
         * This can potentially iterate a massively long zone with
         * many pageblocks unsuitable, so periodically check if we
         * need to schedule, or even abort async compaction.
         */
        /* 由于需要扫描很多页框,所以这里做个检查,执行时间过长则睡眠,一般是32个1024页框休眠一下,异步的情况还需要判断运行进程是否需要被调度 */
        if (!(low_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages))
                        && compact_should_abort(cc))
            break;

        /* 获取第一个页框,需要检查是否属于此zone */
        page = pageblock_pfn_to_page(low_pfn, end_pfn, zone);
        if (!page)
            continue;

        /* If isolation recently failed, do not retry */
        /* 获取页框的PB_migrate_skip标志,如果设置了则跳过这个1024个页框 */
        if (!isolation_suitable(cc, page))
            continue;

        /*
         * For async compaction, also only scan in MOVABLE blocks.
         * Async compaction is optimistic to see if the minimum amount
         * of work satisfies the allocation.
         */
        /* 异步情况,如果不是MIGRATE_MOVABLE或MIGRATE_CMA类型则跳过这段页框块 */
        /* 异步不处理RECLAIMABLE的页 */
        if (cc->mode == MIGRATE_ASYNC &&
            !migrate_async_suitable(get_pageblock_migratetype(page)))
            continue;

        /* Perform the isolation */
        /* 执行完隔离,将low_pfn到end_pfn中正在使用的页框从zone->lru中取出来,返回的是可移动页扫描扫描到的页框号
         * 而UNMOVABLE类型的页框是不会处于lru链表中的,所以所有不在lru链表中的页都会被跳过
         * 返回的是扫描到的最后的页
         */
        low_pfn = isolate_migratepages_block(cc, low_pfn, end_pfn,
                                isolate_mode);

        if (!low_pfn || cc->contended)
            return ISOLATE_ABORT;

        /*
         * Either we isolated something and proceed with migration. Or
         * we failed and compact_zone should decide if we should
         * continue or not.
         */
        /* 跳出,说明这里如果成功只会扫描一个pageblock */
        break;
    }
    /* 统计,里面会再次遍历cc中所有可移动的页,判断它们是RECLAIMABLE还是MOVABLE的页
     */
    acct_isolated(zone, cc);

    /* 可移动页扫描到的页框修正 */
    cc->migrate_pfn = (end_pfn <= cc->free_pfn) ? low_pfn : cc->free_pfn;

    return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE;
}

  注意上面代码里循环最后的break,当获取到可移动页框时,就break跳出循环。之后主要看isolate_migratepages_block(),里面是针对一个pageblock的扫描:

/* 将一个pageblock中所有可以移动的页框隔离出来 */
static unsigned long
isolate_migratepages_block(struct compact_control *cc, unsigned long low_pfn,
            unsigned long end_pfn, isolate_mode_t isolate_mode)
{
    struct zone *zone = cc->zone;
    unsigned long nr_scanned = 0, nr_isolated = 0;
    /* 待移动的页框链表 */
    struct list_head *migratelist = &cc->migratepages;
    struct lruvec *lruvec;
    unsigned long flags = 0;
    bool locked = false;
    struct page *page = NULL, *valid_page = NULL;

    /*
     * Ensure that there are not too many pages isolated from the LRU
     * list by either parallel reclaimers or compaction. If there are,
     * delay for some time until fewer pages are isolated
     */
    /* 检查isolated是否小于LRU链表的(inactive + active) / 2,超过了则表示已经将许多页框隔离出来 */
    while (unlikely(too_many_isolated(zone))) {
        /* async migration should just abort */
        if (cc->mode == MIGRATE_ASYNC)
            return 0;

        /* 进行100ms的休眠,等待设备没那么繁忙 */
        congestion_wait(BLK_RW_ASYNC, HZ/10);

        if (fatal_signal_pending(current))
            return 0;
    }
    /* 如果是异步调用,并且当前进程需要调度的话,返回真 */
    if (compact_should_abort(cc))
        return 0;

    /* Time to isolate some pages for migration */
    /* 遍历每一个页框 */
    for (; low_pfn < end_pfn; low_pfn++) {
        /*
         * Periodically drop the lock (if held) regardless of its
         * contention, to give chance to IRQs. Abort async compaction
         * if contended.
         */
        /* 这里会释放掉zone->lru_lock这个锁 */
        if (!(low_pfn % SWAP_CLUSTER_MAX)
            && compact_unlock_should_abort(&zone->lru_lock, flags,
                                &locked, cc))
            break;

        if (!pfn_valid_within(low_pfn))
            continue;
        /* 扫描次数++ */
        nr_scanned++;

        /* 根据页框号获取页描述符 */
        page = pfn_to_page(low_pfn);

        /* 设置valid_page */
        if (!valid_page)
            valid_page = page;

        /*
         * Skip if free. We read page order here without zone lock
         * which is generally unsafe, but the race window is small and
         * the worst thing that can happen is that we skip some
         * potential isolation targets.
         */
        /* 检查此页是否处于伙伴系统中,主要是通过page->_mapcount判断,如果在伙伴系统中,则跳过这块空闲内存 */
        if (PageBuddy(page)) {
            /* 获取这个页开始的order次方个页框为伙伴系统的一块内存 */
            unsigned long freepage_order = page_order_unsafe(page);

            /*
             * Without lock, we cannot be sure that what we got is
             * a valid page order. Consider only values in the
             * valid order range to prevent low_pfn overflow.
             */
            if (freepage_order > 0 && freepage_order < MAX_ORDER)
                low_pfn += (1UL << freepage_order) - 1;
            continue;
        }

        /*
         * Check may be lockless but that's ok as we recheck later.
         * It's possible to migrate LRU pages and balloon pages
         * Skip any other type of page
         */
        /* 以下处理此页不在伙伴系统中的情况,表明此页是在使用的页*/
        /* 如果页不处于lru中的处理,isolated的页是不处于lru中的,用于balloon的页也不处于lru中?
         * 可移动的页都会在LRU中,不在LRU中的页都会被跳过,这里就把UNMOVABLE进行跳过
         */
        if (!PageLRU(page)) {
            if (unlikely(balloon_page_movable(page))) {
                if (balloon_page_isolate(page)) {
                    /* Successfully isolated */
                    goto isolate_success;
                }
            }
            continue;
        }

        /*
         * PageLRU is set. lru_lock normally excludes isolation
         * splitting and collapsing (collapsing has already happened
         * if PageLRU is set) but the lock is not necessarily taken
         * here and it is wasteful to take it just to check transhuge.
         * Check TransHuge without lock and skip the whole pageblock if
         * it's either a transhuge or hugetlbfs page, as calling
         * compound_order() without preventing THP from splitting the
         * page underneath us may return surprising results.
         */
        /* 如果此页是透明大页的处理,也是跳过。透明大页是在系统活动时可以实时配置,不需要重启生效 */
        if (PageTransHuge(page)) {
            if (!locked)
                low_pfn = ALIGN(low_pfn + 1,
                        pageblock_nr_pages) - 1;
            else
                low_pfn += (1 << compound_order(page)) - 1;

            continue;
        }

        /*
         * Migration will fail if an anonymous page is pinned in memory,
         * so avoid taking lru_lock and isolating it unnecessarily in an
         * admittedly racy check.
         */
        /* 如果是一个匿名页,并且被引用次数大于page->_mapcount,则跳过此页,注释说此页很有可能被锁定在内存中不允许换出,但不知道如何判断的 */
        if (!page_mapping(page) &&
            page_count(page) > page_mapcount(page))
            continue;

        /* If we already hold the lock, we can skip some rechecking */
        /* 检查是否有上锁,这个锁是zone->lru_lock */
        if (!locked) {
            locked = compact_trylock_irqsave(&zone->lru_lock,
                                &flags, cc);
            if (!locked)
                break;
            /* 没上锁的情况,需要检查是否处于LRU中 */
            /* Recheck PageLRU and PageTransHuge under lock */
            if (!PageLRU(page))
                continue;
            /* 如果在lru中,检查是否是大页,做个对齐,防止low_pfn不是页首 */
            if (PageTransHuge(page)) {
                low_pfn += (1 << compound_order(page)) - 1;
                continue;
            }
        }

        lruvec = mem_cgroup_page_lruvec(page, zone);

        /* Try isolate the page */
        /* 将此页从lru中隔离出来 */
        if (__isolate_lru_page(page, isolate_mode) != 0)
            continue;

        VM_BUG_ON_PAGE(PageTransCompound(page), page);

        /* Successfully isolated */
        /* 如果在cgroup的lru缓冲区,则将此页从lru缓冲区中拿出来 */
        del_page_from_lru_list(page, lruvec, page_lru(page));

isolate_success:
        /* 隔离成功,此页已不处于lru中 */
        cc->finished_update_migrate = true;
        /* 将此页加入到本次压缩需要移动页链表中 */
        list_add(&page->lru, migratelist);
        /* 需要移动的页框数量++ */
        cc->nr_migratepages++;
        /* 隔离数量++ */
        nr_isolated++;

        /* Avoid isolating too much */
        /* COMPACT_CLUSTER_MAX代表每次内存压缩所能移动的最大页框数量 */
        if (cc->nr_migratepages == COMPACT_CLUSTER_MAX) {
            ++low_pfn;
            break;
        }
    }
if (unlikely(low_pfn > end_pfn)) low_pfn = end_pfn; /* 解锁 */ if (locked) spin_unlock_irqrestore(&zone->lru_lock, flags); /* 如果全部的页框块都扫描过了,并且没有隔离任何一个页,则标记最后这个页所在的pageblock为PB_migrate_skip,然后 * if (pfn > zone->compact_cached_migrate_pfn[0]) zone->compact_cached_migrate_pfn[0] = pfn; if (cc->mode != MIGRATE_ASYNC && pfn > zone->compact_cached_migrate_pfn[1]) zone->compact_cached_migrate_pfn[1] = pfn; * */ if (low_pfn == end_pfn) update_pageblock_skip(cc, valid_page, nr_isolated, true); trace_mm_compaction_isolate_migratepages(nr_scanned, nr_isolated); /* 统计 */ count_compact_events(COMPACTMIGRATE_SCANNED, nr_scanned); if (nr_isolated) count_compact_events(COMPACTISOLATED, nr_isolated); return low_pfn; }

  到这里,已经完成了从一个pageblock获取可移动页框,并放入struct compact_control中的migratepages链表中。从之前的代码看,当调用isolate_migratepages()将一个pageblock的可移动页隔离出来之后,会调用到migrate_pages()进行可移动页框的移动,之后就是详细说明此函数。

 

可移动页框的移动

  我们先看看migrate_pages()函数原型:

static inline int migrate_pages(struct list_head *l, new_page_t new,
        free_page_t free, unsigned long private, enum migrate_mode mode,
        int reason)

  比较重要的两个参数是

  • new_page_t new: 是一个函数指针,指向获取空闲页框的函数
  • free_page_t free: 也是一个函数指针,指向释放空闲页框的函数

  我们要先看看这两个指针指向的函数,这两个函数指针分别指向compaction_alloc()和compaction_free(),compaction_alloc()是我们主要分析的函数,如下:

static struct page *compaction_alloc(struct page *migratepage,
                    unsigned long data,
                    int **result)
{
    /* 获取cc */
    struct compact_control *cc = (struct compact_control *)data;
    struct page *freepage;

    /*
     * Isolate free pages if necessary, and if we are not aborting due to
     * contention.
     */
    /* 如果cc中的空闲页框链表为空 */
    if (list_empty(&cc->freepages)) {
        /* 并且cc->contended没有记录错误代码 */
        if (!cc->contended)
            /* 从cc->free_pfn开始向前获取空闲页 */
            isolate_freepages(cc);

        if (list_empty(&cc->freepages))
            return NULL;
    }
    /* 从cc->freepages链表中拿出一个空闲page */
    freepage = list_entry(cc->freepages.next, struct page, lru);
    list_del(&freepage->lru);
    cc->nr_freepages--;
    
    /* 返回空闲页框 */
    return freepage;
}

  代码很简单,主要还是里面的isolate_freepages()函数,在这个函数中,会从cc->free_pfn开始向前扫描空闲页框,但是注意以pageblock向前扫描,但是在pageblock内部是从前向后扫描的,最后遇到cc->migrate_pfn后或者cc->nr_freepages >= cc->nr_migratepages的情况下会停止,如下:

/* 隔离出空闲页框 */
static void isolate_freepages(struct compact_control *cc)
{
    struct zone *zone = cc->zone;
    struct page *page;
    unsigned long block_start_pfn;    /* start of current pageblock */
    unsigned long isolate_start_pfn; /* exact pfn we start at */
    unsigned long block_end_pfn;    /* end of current pageblock */
    unsigned long low_pfn;         /* lowest pfn scanner is able to scan */
    int nr_freepages = cc->nr_freepages;
    struct list_head *freelist = &cc->freepages;

    /* 获取开始扫描页框所在的pageblock,并且设置为此pageblock的最后一个页框或者管理区最后一个页框 */
    isolate_start_pfn = cc->free_pfn;
    block_start_pfn = cc->free_pfn & ~(pageblock_nr_pages-1);
    block_end_pfn = min(block_start_pfn + pageblock_nr_pages,
                        zone_end_pfn(zone));
    /* 按pageblock_nr_pages对齐,low_pfn保存的是可迁移页框扫描所在的页框号,但是这里有可能migrate_pfn == free_pfn */
    low_pfn = ALIGN(cc->migrate_pfn + 1, pageblock_nr_pages);


    /* 开始扫描空闲页框,从管理区最后一个pageblock向migrate_pfn所在的pageblock扫描
     * block_start_pfn是pageblock开始页框号
     * block_end_pfn是pageblock结束页框号
     */
    /* 循环条件, 
     * 扫描到low_pfn所在pageblokc或者其后一个pageblock,low_pfn是low_pfn保存的是可迁移页框扫描所在的页框号,并按照pageblock_nr_pages对齐。
     * 并且cc中可移动的页框数量多于cc中空闲页框的数量。由于隔离可移动页是以一个一个pageblock为单位的,所以刚开始时更多是判断cc->nr_migratepages > nr_freepages来决定是否结束
     * 当扫描到可移动页框扫描所在的pageblock后,则会停止
     */
    for (; block_start_pfn >= low_pfn && cc->nr_migratepages > nr_freepages;
                block_end_pfn = block_start_pfn,
                block_start_pfn -= pageblock_nr_pages,
                isolate_start_pfn = block_start_pfn) {
        unsigned long isolated;

        if (!(block_start_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages))
                        && compact_should_abort(cc))
            break;

        /* 检查block_start_pfn和block_end_pfn,如果没问题,返回block_start_pfn所指的页描述符,也就是pageblock第一页描述符 */
        page = pageblock_pfn_to_page(block_start_pfn, block_end_pfn,
                                    zone);
        if (!page)
            continue;

        /* Check the block is suitable for migration */
        /* 判断是否能够用于迁移页框
         * 判断条件1: 如果处于伙伴系统中,它所代表的这段连续页框的order值必须小于pageblock的order值
         * 判断条件2: 此pageblock必须为MIGRATE_MOVABLE或者MIGRATE_CMA类型,而为MIGRATE_RECLAIMABLE类型的pageblock则跳过
         */
        if (!suitable_migration_target(page))
            continue;

        /* If isolation recently failed, do not retry */
        /* 检查cc中是否标记了即使pageblock标记了跳过也对pageblock进行扫描,并且检查此pageblock是否被标记为跳过 */
        if (!isolation_suitable(cc, page))
            continue;

        /* Found a block suitable for isolating free pages from. */
        /* 扫描从isolate_start_pfn到block_end_pfn的空闲页框,并把它们放入到freelist中,返回此pageblock中总共获得的空闲页框数量 
         * 第一轮扫描可能会跳过,应该第一次isolate_start_pfn是等于zone最后一个页框的
         */
        isolated = isolate_freepages_block(cc, &isolate_start_pfn,
                    block_end_pfn, freelist, false);
        /* 统计freelist中空闲页框数量 */
        nr_freepages += isolated;

        /* 下次循环开始的页框 */
        cc->free_pfn = (isolate_start_pfn < block_end_pfn) ?
                isolate_start_pfn :
                block_start_pfn - pageblock_nr_pages;


        /* 设置cc->finished_update_free为true,即表明此次cc获取到了空闲页框链表 */
        if (isolated)
            cc->finished_update_free = true;

        /* 检查contended,此用于表明是否需要终止 */
        if (cc->contended)
            break;
    }

    /* split_free_page does not map the pages */
    /* 设置页表项,设置为内核使用 */
    map_pages(freelist);


    /* 保证free_pfn不超过migrate_pfn */
    if (block_start_pfn < low_pfn)
        cc->free_pfn = cc->migrate_pfn;

    cc->nr_freepages = nr_freepages;
}

 

   对于单个pageblock里的操作,就在isolate_freepages_block()中:

/* 扫描从start_pfn到end_pfn的空闲页框,并把它们放入到freelist中,返回此pageblock中总共获得的空闲页框数量 */
static unsigned long isolate_freepages_block(struct compact_control *cc,
                unsigned long *start_pfn,
                unsigned long end_pfn,
                struct list_head *freelist,
                bool strict)
{
    int nr_scanned = 0, total_isolated = 0;
    struct page *cursor, *valid_page = NULL;
    unsigned long flags = 0;
    bool locked = false;
    unsigned long blockpfn = *start_pfn;

    cursor = pfn_to_page(blockpfn);

    /* Isolate free pages. */
    /* 从pageblock的start向end进行扫描 */
    for (; blockpfn < end_pfn; blockpfn++, cursor++) {
        int isolated, i;
        /* 当前页框 */
        struct page *page = cursor;

        /*
         * Periodically drop the lock (if held) regardless of its
         * contention, to give chance to IRQs. Abort if fatal signal
         * pending or async compaction detects need_resched()
         */
        if (!(blockpfn % SWAP_CLUSTER_MAX)
            && compact_unlock_should_abort(&cc->zone->lock, flags,
                                &locked, cc))
            break;

        nr_scanned++;
        /* 检查此页框号是否正确 */
        if (!pfn_valid_within(blockpfn))
            goto isolate_fail;

        /* valid_page是开始扫描的页框 */
        if (!valid_page)
            valid_page = page;
        /* 检查此页是否在伙伴系统中,不在说明是正在使用的页框,则跳过 */
        if (!PageBuddy(page))
            goto isolate_fail;

        /*
         * If we already hold the lock, we can skip some rechecking.
         * Note that if we hold the lock now, checked_pageblock was
         * already set in some previous iteration (or strict is true),
         * so it is correct to skip the suitable migration target
         * recheck as well.
         */
        /* 获取锁 */
        if (!locked) {
            locked = compact_trylock_irqsave(&cc->zone->lock,
                                &flags, cc);
            if (!locked)
                break;

            /* Recheck this is a buddy page under lock */
            if (!PageBuddy(page))
                goto isolate_fail;
        }

        /* Found a free page, break it into order-0 pages */
        /* 将page开始的连续空闲页框拆分为连续的单个页框,返回数量,order值会在page的页描述符中,这里有可能会设置pageblock的类型 */
        isolated = split_free_page(page);
        /* 更新总共获得的空闲页框 */
        total_isolated += isolated;
        /* 将isolated数量个单个页框放入freelist中 */
        for (i = 0; i < isolated; i++) {
            list_add(&page->lru, freelist);
            page++;
        }

        /* If a page was split, advance to the end of it */
        /* 跳过这段连续空闲页框,因为上面把这段空闲页框全部加入到了freelist中 */
        if (isolated) {
            blockpfn += isolated - 1;
            cursor += isolated - 1;
            continue;
        }

isolate_fail:
        if (strict)
            break;
        else
            continue;

    }

    /* Record how far we have got within the block */
    *start_pfn = blockpfn;

    trace_mm_compaction_isolate_freepages(nr_scanned, total_isolated);

    if (strict && blockpfn < end_pfn)
        total_isolated = 0;

    /* 如果占有锁则释放掉 */
    if (locked)
        spin_unlock_irqrestore(&cc->zone->lock, flags);

    /* Update the pageblock-skip if the whole pageblock was scanned */
    /* 扫描完了此pageblock,但是total_isolated为0的话,则标记此pageblock为跳过 */
    if (blockpfn == end_pfn)
        update_pageblock_skip(cc, valid_page, total_isolated, false);

    /* 统计 */
    count_compact_events(COMPACTFREE_SCANNED, nr_scanned);
    if (total_isolated)
        count_compact_events(COMPACTISOLATED, total_isolated);
    /* 返回总共获得的空闲页框 */
    return total_isolated;
}

 

  这里结束就是从一个pageblock中获取到了空闲页框,最后会返回在此pageblock中总共获得的空闲页框数量。这里看完了compaction_alloc()中的调用过程,再看看compaction_free()的调用过程,这个函数主要用于当从compaction_alloc()获取一个空闲页框用于移动时,但是因为某些原因失败,就要把这个空闲页框重新放回cc->freepages链表中,实现也很简单:

/* 将page释放回到cc中 */
static void compaction_free(struct page *page, unsigned long data)
{
    struct compact_control *cc = (struct compact_control *)data;

    list_add(&page->lru, &cc->freepages);
    cc->nr_freepages++;
}

 

  看完了compaction_alloc()和compaction_free(),现在看最主要的函数migrate_pages()此函数就是用于将隔离出来的可移动页框进行移动到空闲页框中,在里面每进行一个页框的处理前,都会先判断当前进程是否需要调度,然后再进行处理:

/* 将from中的页移到新页上,新页会在get_new_page中获取
 * from = &cc->migratepages
 * get_new_page = compaction_alloc
 * put_new_page = compaction_free
 * private = (unsigned long)cc
 * mode = cc->mode
 * reson = MR_COMPACTION
 */
int migrate_pages(struct list_head *from, new_page_t get_new_page,
        free_page_t put_new_page, unsigned long private,
        enum migrate_mode mode, int reason)
{
    int retry = 1;
    int nr_failed = 0;
    int nr_succeeded = 0;
    int pass = 0;
    struct page *page;
    struct page *page2;
    /* 获取当前进程是否允许将页写到swap */
    int swapwrite = current->flags & PF_SWAPWRITE;
    int rc;

    /* 如果当前进程不支持将页写到swap,要强制其支持 */
    if (!swapwrite)
        current->flags |= PF_SWAPWRITE;

    for(pass = 0; pass < 10 && retry; pass++) {
        retry = 0;

        /* page是主要遍历的页,page2是page在from中的下一个页 */
        list_for_each_entry_safe(page, page2, from, lru) {
            /* 如果进程需要调度,则调度 */
            cond_resched();

            if (PageHuge(page))
                /* 大页处理
                 * 这里看得不太懂,不知道怎么保证从from中拿出来的页就一定是大页
                 */
                rc = unmap_and_move_huge_page(get_new_page,
                        put_new_page, private, page,
                        pass > 2, mode);
            else
                /* 此页为非大页处理 */
                rc = unmap_and_move(get_new_page, put_new_page,
                        private, page, pass > 2, mode);

            /* 返回值处理 */
            switch(rc) {
            case -ENOMEM:
                goto out;
            case -EAGAIN:
                /* 重试 */
                retry++;
                break;
            case MIGRATEPAGE_SUCCESS:
                /* 成功 */
                nr_succeeded++;
                break;
            default:
                /*
                 * Permanent failure (-EBUSY, -ENOSYS, etc.):
                 * unlike -EAGAIN case, the failed page is
                 * removed from migration page list and not
                 * retried in the next outer loop.
                 */
                nr_failed++;
                break;
            }
        }
    }
    rc = nr_failed + retry;
out:
    /* 统计 */
    if (nr_succeeded)
        count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded);
    if (nr_failed)
        count_vm_events(PGMIGRATE_FAIL, nr_failed);
    trace_mm_migrate_pages(nr_succeeded, nr_failed, mode, reason);

    /* 恢复PF_SWAPWRITE标记 */
    if (!swapwrite)
        current->flags &= ~PF_SWAPWRITE;

    return rc;
}

  大页的情况下有些我暂时还没看懂,这里就先分析常规页的情况,常规页的情况的处理主要是umap_and_move()函数,具体看函数实现吧,如下:

/* 从get_new_page中获取一个新页,然后将page取消映射,并把page的数据复制到新页上 */
static int unmap_and_move(new_page_t get_new_page, free_page_t put_new_page,
            unsigned long private, struct page *page, int force,
            enum migrate_mode mode)
{
    int rc = 0;
    int *result = NULL;
    /* 获取一个空闲页,具体见compaction_alloc() */
    struct page *newpage = get_new_page(page, private, &result);

    if (!newpage)
        return -ENOMEM;

    /* 如果此页在期间被释放为空闲页了,则跳过此页 */
    if (page_count(page) == 1) {
        /* page was freed from under us. So we are done. */
        goto out;
    }
    /* 此页是透明大页则跳过 */
    if (unlikely(PageTransHuge(page)))
        if (unlikely(split_huge_page(page)))
            goto out;

    /* 将page取消映射,并把page的数据复制到newpage中 */
    rc = __unmap_and_move(page, newpage, force, mode);

out:
    if (rc != -EAGAIN) {
        /*
         * A page that has been migrated has all references
         * removed and will be freed. A page that has not been
         * migrated will have kepts its references and be
         * restored.
         */
        /* 迁移成功,将旧的page放回到LRU链表中,为什么放入lru链表,因为旧的page已经是一个空闲的page了 */
        list_del(&page->lru);
        dec_zone_page_state(page, NR_ISOLATED_ANON +
                page_is_file_cache(page));
        /* 在这里会把新页放回到原来的地方 */
        putback_lru_page(page);
    }

    /*
     * If migration was not successful and there's a freeing callback, use
     * it.  Otherwise, putback_lru_page() will drop the reference grabbed
     * during isolation.
     */
    /* 迁移不成功 */
    if (rc != MIGRATEPAGE_SUCCESS && put_new_page) {
        ClearPageSwapBacked(newpage);
        /* 将新页放回到cc的空闲页链表中,具体见compaction_free() */
        put_new_page(newpage, private);
    } else if (unlikely(__is_movable_balloon_page(newpage))) {
        /* drop our reference, page already in the balloon */
        /* 如果新页是属于balloon使用的页,则放回到相应地方 */
        put_page(newpage);
    } else
        /* 在这里会把新页放回到原来的地方 */
        putback_lru_page(newpage);

    if (result) {
        if (rc)
            *result = rc;
        else
            *result = page_to_nid(newpage);
    }
    return rc;
}

 

  在看_umap_and_move():

static int __unmap_and_move(struct page *page, struct page *newpage,
                int force, enum migrate_mode mode)
{
    int rc = -EAGAIN;
    int remap_swapcache = 1;
    struct anon_vma *anon_vma = NULL;

    if (!trylock_page(page)) {
        /* 异步一定需要锁 */
        if (!force || mode == MIGRATE_ASYNC)
            goto out;

        /*
         * It's not safe for direct compaction to call lock_page.
         * For example, during page readahead pages are added locked
         * to the LRU. Later, when the IO completes the pages are
         * marked uptodate and unlocked. However, the queueing
         * could be merging multiple pages for one bio (e.g.
         * mpage_readpages). If an allocation happens for the
         * second or third page, the process can end up locking
         * the same page twice and deadlocking. Rather than
         * trying to be clever about what pages can be locked,
         * avoid the use of lock_page for direct compaction
         * altogether.
         */
        if (current->flags & PF_MEMALLOC)
            goto out;

        lock_page(page);
    }

    /* 此页需要写回到磁盘 */
    if (PageWriteback(page)) {
        /*
         * Only in the case of a full synchronous migration is it
         * necessary to wait for PageWriteback. In the async case,
         * the retry loop is too short and in the sync-light case,
         * the overhead of stalling is too much
         */
        if (mode != MIGRATE_SYNC) {
            rc = -EBUSY;
            goto out_unlock;
        }
        if (!force)
            goto out_unlock;
        /* 等待此页回写 */
        wait_on_page_writeback(page);
    }
    /*
     * By try_to_unmap(), page->mapcount goes down to 0 here. In this case,
     * we cannot notice that anon_vma is freed while we migrates a page.
     * This get_anon_vma() delays freeing anon_vma pointer until the end
     * of migration. File cache pages are no problem because of page_lock()
     * File Caches may use write_page() or lock_page() in migration, then,
     * just care Anon page here.
     */
    /* 匿名页并不使用于共享内存 */
    if (PageAnon(page) && !PageKsm(page)) {
        /*
         * Only page_lock_anon_vma_read() understands the subtleties of
         * getting a hold on an anon_vma from outside one of its mms.
         */
        /* 获取此页所在的vma */
        anon_vma = page_get_anon_vma(page);
        if (anon_vma) {
            /*
             * Anon page
             */
        } else if (PageSwapCache(page)) {
            /*
             * We cannot be sure that the anon_vma of an unmapped
             * swapcache page is safe to use because we don't
             * know in advance if the VMA that this page belonged
             * to still exists. If the VMA and others sharing the
             * data have been freed, then the anon_vma could
             * already be invalid.
             *
             * To avoid this possibility, swapcache pages get
             * migrated but are not remapped when migration
             * completes
             */
            remap_swapcache = 0;
        } else {
            goto out_unlock;
        }
    }

    /* balloon使用的页 */
    if (unlikely(isolated_balloon_page(page))) {
        /*
         * A ballooned page does not need any special attention from
         * physical to virtual reverse mapping procedures.
         * Skip any attempt to unmap PTEs or to remap swap cache,
         * in order to avoid burning cycles at rmap level, and perform
         * the page migration right away (proteced by page lock).
         */
        rc = balloon_page_migrate(newpage, page, mode);
        goto out_unlock;
    }

    /*
     * Corner case handling:
     * 1. When a new swap-cache page is read into, it is added to the LRU
     * and treated as swapcache but it has no rmap yet.
     * Calling try_to_unmap() against a page->mapping==NULL page will
     * trigger a BUG.  So handle it here.
     * 2. An orphaned page (see truncate_complete_page) might have
     * fs-private metadata. The page can be picked up due to memory
     * offlining.  Everywhere else except page reclaim, the page is
     * invisible to the vm, so the page can not be migrated.  So try to
     * free the metadata, so the page can be freed.
     */
    if (!page->mapping) {
        VM_BUG_ON_PAGE(PageAnon(page), page);
        if (page_has_private(page)) {
            try_to_free_buffers(page);
            goto out_unlock;
        }
        goto skip_unmap;
    }

    /* Establish migration ptes or remove ptes */
    /* umap此页 */
    try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);

skip_unmap:
    /* 将page的内容复制到newpage中,会进行将newpage重新映射到page所属进程的pte中 */
    if (!page_mapped(page))
        rc = move_to_new_page(newpage, page, remap_swapcache, mode);

    if (rc && remap_swapcache)
        remove_migration_ptes(page, page);

    /* Drop an anon_vma reference if we took one */
    if (anon_vma)
        put_anon_vma(anon_vma);

out_unlock:
    unlock_page(page);
out:
    return rc;
}

  整个内存压缩源码到这就不再往下分析了,这里面基本上已经把可移动页移到了空闲页上,并且进行了重新映射,然后旧的可移动页放回到cpu的单页框链表中,然后会慢慢放回到伙伴系统中。

 

你可能感兴趣的:(linux内存源码分析 - 内存压缩)