linux cma分配与释放详解

1. 概述

Contiguous Memory Allocator, CMA,连续内存分配器,用于分配连续的大块内存。 平时设备不用时又要把内存给系统用,最大化利用内存。

CMA分配器,会Reserve一片物理内存区域:

  1. 设备驱动不用时,内存Buddy系统将该区域用于分配和管理Moveable类型页面;
  2. 设备驱动使用时,用于连续内存分配,此时已经分配的Moveable页面需要进行迁移;

此外,CMA分配器还可以与DMA子系统集成在一起,使用DMA的设备驱动程序无需使用单独的CMA API

本文通过奔跑吧linux的qemu进行实际演示,内核版本:kernel5.0

2. CMA数据结构

2.1 struct cma

使用struct cma来描述一个CMA区域:

c 复制代码
struct cma {
        //CMA区域物理地址的起始page frame number(页帧号)
        unsigned long   base_pfn;
        //CMA区域的页面数量
        unsigned long   count;
        //描述cma区域页面的分配情况,1表示已分配,0为空闲。
        unsigned long   *bitmap;
        //表示bitmap中一个bit所代表的页面数量(2^order_per_bit)
        unsigned int order_per_bit; 
        struct mutex    lock;
#ifdef CONFIG_CMA_DEBUGFS
        struct hlist_head mem_head;
        spinlock_t mem_head_lock;
#endif
        const char *name;
};

cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。具体内存管理的单位和struct cma中的order_per_bit成员相关,如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照(2^1)个page组成的block来分配和释放,以此类推。

3. CMA主要流程分析

3.1 CMA初始化流程

系统初始化过程需要先创建CMA区域,创建方法有:dts的reserved memory或者通过命令行参数。

  1. 通过dts的reserved memory方式,物理内存的描述放置在dts中配置,比如:
c 复制代码
linux,cma {
        compatible = "shared-dma-pool";
        /*alloc-ranges = <0x0 0x00000000 0x0 oxffffffff>;*/
        reusable;
        alignment = <0x0 400000>;
        size = <0x0 0x800000>;
        linux,cma-default;
};
  • linux,cma:为CMA 区域名称。
  • compatible:须为"shared-dma-pool"。
  • resuable:表示 cma 内存可被 buddy 系统使用。
  • size:表示cma区域的大小,单位为字节
  • alignment:指定 CMA 区域的地址对齐大小。
  • linux,cma-default:属性表示当前 cma 内存将会作为默认的cma pool 用于cma 内存的申请。

QEMU 内置了多款的开发板,其中 QEMU Virt 开发板模拟一款通用的 ARM 开发板,包括内存布局、中断分配、 CPU 配置、时钟配置等信息,这些信息目前都实现在 QEMU 的源代码中,具体文件是 QEMU 源代码: hw/arm/virt.c。所以无法通过修改dts进行实际演示,于是通过第二种方法。

  1. 通过命令行参数

可以通过在启动kernel时传到kernel command参数,格式如下:

less 复制代码
cma=nn[MG]@[start[MG][-end[MG]]]
  • nn[MG]: 为cma区域大小单位是M或者G,例如启动一个64M大小的CMA为:cma=64MB
  • 如果需要指定特定物理地址范围,则后续需要跟@标识符
  • @标识符后面start为起始物理地址
  • end为结束物理地址,start和end都非必选项。

在qemu run_debian_arm64.sh中修改kernel_arg="noinitrd nokaslr cma=8MB"

在/proc/meminfo中可查看cma内存的大小:

在系统启动过程中,内核对上面描述的dtb文件进行解析,从而完成内存信息注册,调用流程为:

c 复制代码
setup_arch
       arm64_memblock_init
              early_init_fdt_scan_reserved_mem
                   __reserved_mem_init_node
// drivers/of/of_reserved_mem.c
static int __init __reserved_mem_init_node(struct reserved_mem *rmem)
{
        extern const struct of_device_id __reservedmem_of_table[];
        const struct of_device_id *i;
        //遍历__reservedmem_of_table section中的内容
        for (i = __reservedmem_of_table; i < &__rmem_of_table_sentinel; i++) {
                reservedmem_of_init_fn initfn = i->data;
                const char *compat = i->compatible;
                //检查到dts中有compatible匹配(CMA这里为"shared-dma-pool")
                if (!of_flat_dt_is_compatible(rmem->fdt_node, compat))
                        continue;
                //进一步执行对应的initfn,此时initfn是rmem_cma_setup()
                if (initfn(rmem) == 0) {
                        pr_info("initialized node %s, compatible id %s\n",
                                rmem->name, compat);
                        return 0;
                }
        }
        return -ENOENT;
}

RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

#define RESERVEDMEM_OF_DECLARE(name, compat, init)                      \
        _OF_DECLARE(reservedmem, name, compat, init, reservedmem_of_init_fn)

#define _OF_DECLARE(table, name, compat, fn, fn_type)                   \
        static const struct of_device_id __of_table_##name              \
                __used __section(__##table##_of_table)                  \
                 = { .compatible = compat,                              \
                     .data = (fn == (fn_type)NULL) ? fn : fn  }

__reserved_mem_init_node会遍历__reservedmem_of_table section中的内容,检查到dts中有compatible匹配(CMA这里为"shared-dma-pool")就进一步执行对应的initfn。通过RESERVEDMEM_OF_DECLARE定义的都会被链接到__reservedmem_of_table这个section段中,最终会调到使用RESERVEDMEM_OF_DECLARE定义的函数,如下rmem_cma_setup:

c 复制代码
//用dtb中解析出来的地址信息来初始化CMA,用来创建和初始化struct cma
tatic int __init rmem_cma_setup(struct reserved_mem *rmem)
{
        phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
        phys_addr_t mask = align - 1;
        unsigned long node = rmem->fdt_node;
        struct cma *cma;
        int err;

        if (!of_get_flat_dt_prop(node, "reusable", NULL) ||
            of_get_flat_dt_prop(node, "no-map", NULL))
                return -EINVAL;

        if ((rmem->base & mask) || (rmem->size & mask)) {
                pr_err("Reserved memory: incorrect alignment of CMA region\n");
                return -EINVAL;
        }
        //cma_init_reserved_mem 从保留内存块里面获取一块地址为base,大小为size的内存
        err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &cma);
        if (err) {
                pr_err("Reserved memory: unable to setup CMA region\n");
                return err;
        }
        /* Architecture specific contiguous memory fixup. */
        dma_contiguous_early_fixup(rmem->base, rmem->size);
        //如果dts指定了linux,cma-default,则将dma_contiguous_set_default指向这个CMA区域,
        //使用dma_alloc_contiguous从CMA分配内存时,默认会从该区域划分。
        //dma_contiguous_default_area = cma;
        if (of_get_flat_dt_prop(node, "linux,cma-default", NULL))
                dma_contiguous_set_default(cma);

        rmem->ops = &rmem_cma_ops;
        rmem->priv = cma;

        pr_info("Reserved memory: created CMA memory pool at %pa, size %ld MiB\n",
                &rmem->base, (unsigned long)rmem->size / SZ_1M);

        return 0;
}
c 复制代码
int __init cma_init_reserved_mem(phys_addr_t base, phys_addr_t size,
                                 unsigned int order_per_bit,
                                 const char *name,
                                 struct cma **res_cma)
{
        struct cma *cma;
        phys_addr_t alignment;
        
        /* Sanity checks */
        if (cma_area_count == ARRAY_SIZE(cma_areas)) {
                pr_err("Not enough slots for CMA reserved regions!\n");
                return -ENOSPC;
        }
        if (!size || !memblock_is_region_reserved(base, size))
                return -EINVAL;
        /* ensure minimal alignment required by mm core */
        alignment = PAGE_SIZE <<
                        max_t(unsigned long, MAX_ORDER - 1, pageblock_order);
        /* alignment should be aligned with order_per_bit */
        if (!IS_ALIGNED(alignment >> PAGE_SHIFT, 1 << order_per_bit))
                return -EINVAL;
        if (ALIGN(base, alignment) != base || ALIGN(size, alignment) != size)
                return -EINVAL;
        /*
         * Each reserved area must be initialised later, when more kernel
         * subsystems (like slab allocator) are available.
         */
        cma = &cma_areas[cma_area_count];
        if (name) {
                cma->name = name;
        } else {
                cma->name = kasprintf(GFP_KERNEL, "cma%d\n", cma_area_count);
                if (!cma->name)
                        return -ENOMEM;
        }               
        cma->base_pfn = PFN_DOWN(base);
        cma->count = size >> PAGE_SHIFT;
        cma->order_per_bit = order_per_bit;
        *res_cma = cma;
        cma_area_count++;
        totalcma_pages += (size / PAGE_SIZE);

        return 0;
}

执行到此, CMA和其它的保留内存是一样的,都是放在 memblock.reserved 中,这部分保留内存一样没能被 Buddy 系统用到。前面讲过为了提升内存利用率,还需要将CMA这部分内存标记后归还给 Buddy系统,供 Buddy作为可移动页面提供给APP或内核内存申请,由cma_init_reserved_areas来实现。

c 复制代码
//mm/cma.c
static int __init cma_init_reserved_areas(void)
{
        int i;

        for (i = 0; i < cma_area_count; i++) {
                int ret = cma_activate_area(&cma_areas[i]);

                if (ret)
                        return ret;
        }

        return 0;
}
core_initcall(cma_init_reserved_areas);

static int __init cma_activate_area(struct cma *cma)
{
        //cma_bitmap_maxno计算Bitmap需要多少内存
        int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);
        unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;
        //i变量表示该CMA eara有多少个pageblock
        unsigned i = cma->count >> pageblock_order;
        struct zone *zone;
        //CMA区域由一个bitmap来管理所有page的状态,一个bit位代表2^order_per_bit个page
        cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL);

        if (!cma->bitmap)
                return -ENOMEM;

        WARN_ON_ONCE(!pfn_valid(pfn));
        zone = page_zone(pfn_to_page(pfn));
        //遍历该CM区域中的所有的pageblock
        do {
                unsigned j;
                //确保CMA区域中的所有page都是在一个zone内
                base_pfn = pfn;
                for (j = pageblock_nr_pages; j; --j, pfn++) {
                        WARN_ON_ONCE(!pfn_valid(pfn));
                        if (page_zone(pfn_to_page(pfn)) != zone)
                                goto not_in_zone;
                }
                //最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA,将页面添加到伙伴系统中并更新zone管理的页面总数。
                init_cma_reserved_pageblock(pfn_to_page(base_pfn));
        } while (--i);

        mutex_init(&cma->lock);
        return 0;
not_in_zone:
        pr_err("CMA area %s could not be activated\n", cma->name);
        kfree(cma->bitmap);
        cma->count = 0;
        return -EINVAL;
}

/* Free whole pageblock and set its migration type to MIGRATE_CMA. */
void __init init_cma_reserved_pageblock(struct page *page)
{
        unsigned i = pageblock_nr_pages;
        struct page *p = page;

        do {
                //将页面已经设置的reserved标志位清除掉
                __ClearPageReserved(p);
                //设置page的_refcount为0
                set_page_count(p, 0);
        } while (++p, --i);
        //将migratetype设置为MIGRATE_CMA
        set_pageblock_migratetype(page, MIGRATE_CMA);
        //循环调用__free_pages函数,将CMA区域中所有的页面都释放到buddy系统中,每次不超过2^MAX_ORDER - 1个page
        if (pageblock_order >= MAX_ORDER) {
                i = pageblock_nr_pages;
                p = page;
                do {
                        //设置page的_refcount为1
                        set_page_refcounted(p);
                        __free_pages(p, MAX_ORDER - 1);
                        p += MAX_ORDER_NR_PAGES;
                } while (i -= MAX_ORDER_NR_PAGES);
        } else {
                set_page_refcounted(page);
                __free_pages(page, pageblock_order);
        }
        //更新伙伴系统管理的内存数量
        adjust_managed_page_count(page, pageblock_nr_pages);
}

void adjust_managed_page_count(struct page *page, long count)
{
        atomic_long_add(count, &page_zone(page)->managed_pages);
        totalram_pages_add(count);
#ifdef CONFIG_HIGHMEM
        if (PageHighMem(page))
                totalhigh_pages_add(count);
#endif
}

执行到此,后续这部分CMA内存就可以为buddy所申请。在伙伴系统中migratetype为movable并且分配flag带CMA,可以从CMA分配内存。

c 复制代码
// mm/page_alloc.c
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
                                                unsigned int alloc_flags)
{
        struct page *page;

retry:
        page = __rmqueue_smallest(zone, order, migratetype);
        if (unlikely(!page)) {
                //从CMA分配内存
                if (migratetype == MIGRATE_MOVABLE)
                        page = __rmqueue_cma_fallback(zone, order);

                if (!page && __rmqueue_fallback(zone, order, migratetype,
                                                                alloc_flags))
                        goto retry;
        }

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

static __always_inline struct page *__rmqueue_cma_fallback(struct zone *zone,
                                        unsigned int order)
{
        return __rmqueue_smallest(zone, order, MIGRATE_CMA);
}

/*
 * Go through the free lists for the given migratetype and remove
 * the smallest available page from the freelists
 */
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                                                int migratetype)
{
        unsigned int current_order;
        struct free_area *area;
        struct page *page;

        /* Find a page of the appropriate size in the preferred list */
        for (current_order = order; current_order < MAX_ORDER; ++current_order) {
                area = &(zone->free_area[current_order]);
                page = list_first_entry_or_null(&area->free_list[migratetype],
                                                        struct page, lru);
                if (!page)
                        continue;
                list_del(&page->lru);
                rmv_page_order(page);
                area->nr_free--;
                expand(zone, page, order, current_order, area, migratetype);
                set_pcppage_migratetype(page, migratetype);
                return page;
        }

        return NULL;
}

3.2 CMA分配流程

c 复制代码
//mm/cma.c
struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align,
                       bool no_warn)
{
        unsigned long mask, offset;
        unsigned long pfn = -1;
        unsigned long start = 0;
        unsigned long bitmap_maxno, bitmap_no, bitmap_count;
        size_t i;
        struct page *page = NULL;
        int ret = -ENOMEM;

        if (!cma || !cma->count)
                return NULL;

        pr_debug("%s(cma %p, count %zu, align %d)\n", __func__, (void *)cma,
                 count, align);

        if (!count)
                return NULL;
        //bitmap中的page已经按order_per_bit对齐,align_order大于order_per_bit则需要按照align_order对齐,这里返回align_order对齐的mask
        mask = cma_bitmap_aligned_mask(cma, align);
        //base_pfn按align_order对齐后,按照order_per_bit的offset
        offset = cma_bitmap_aligned_offset(cma, align);
        //根据CMA区域的页面数量,获取bitmap最大的可用bit数
        bitmap_maxno = cma_bitmap_maxno(cma);
        //将申请的page count按order_per_bit向上对齐,计算要多少个bit
        bitmap_count = cma_bitmap_pages_to_bits(cma, count);
        //要申请的bit count大于最大可用bit,不能满足要求
        if (bitmap_count > bitmap_maxno)
                return NULL;
        //在位图中查找下一个连续的bitmap_count个零位区域的偏移量(offset)
        for (;;) {
                mutex_lock(&cma->lock);
                bitmap_no = bitmap_find_next_zero_area_off(cma->bitmap,
                                bitmap_maxno, start, bitmap_count, mask,
                                offset);
                //没有找到满足条件的零位区域,则返回大于等于bitmap_maxno的值
                if (bitmap_no >= bitmap_maxno) {
                        mutex_unlock(&cma->lock);
                        break;
               }
               //将要分配的页面的对应bitmap先置位为1,表示已经分配了
                bitmap_set(cma->bitmap, bitmap_no, bitmap_count);
                mutex_unlock(&cma->lock);

                pfn = cma->base_pfn + (bitmap_no << cma->order_per_bit);
                mutex_lock(&cma_mutex);
                //使用alloc_config_range来进行内存回收与迁移,最后将这块干净的连续内存返回给调用者使用
                ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
                                     GFP_KERNEL | (no_warn ? __GFP_NOWARN : 0));
                mutex_unlock(&cma_mutex);
                if (ret == 0) {
                        page = pfn_to_page(pfn);
                        break;
                }
                //分配失败则清除掉bitmap
                cma_clear_bitmap(cma, pfn, count);
                if (ret != -EBUSY)
                        break;
                pr_debug("%s(): memory range at %p is busy, retrying\n",
                         __func__, pfn_to_page(pfn));
                /* try again with a bit different memory target */
                start = bitmap_no + mask + 1;
        }

        trace_cma_alloc(pfn, page, count, align);
        ......
        pr_debug("%s(): returned %p\n", __func__, page);
        return page;
}

继续看cma_alloc流程的alloc_config_range要干哪些事情:

简而言之,目的就是想从一块"脏的"连续内存块(已经被各种类型的内存使用),得到一块干净的连续内存块,要么是回收掉,要么是迁移走,最后将这块干净的连续内存返回给调用者使用,如下图:

c 复制代码
int alloc_contig_range(unsigned long start, unsigned long end,
                       unsigned migratetype, gfp_t gfp_mask)
{
        unsigned long outer_start, outer_end; 
        unsigned int order;
        int ret = 0;
        /* 
         * struct compact_control结构体:
         * .nr_migratepages: 要迁移的页面数
         * .order: 要迁移的页面的页框数的指数(即2的幂),初始值为 -1
         * .zone: 起始页框所属的内存区域,由 start 所指向的页框计算而来
         * .mode: 内存迁移的模式,这里是同步模式(MIGRATE_SYNC)
         * .ignore_skip_hint: 是否忽略跳过提示。这里设置为 true
         * .no_set_skip_hint: 是否禁止设置跳过提示。这里设置为 true
         * .gfp_mask: GFP 内存分配标志,由当前进程的 GFP 上下文 gfp_mask 决定
        */
        struct compact_control cc = {
                .nr_migratepages = 0,
                .order = -1,
                .zone = page_zone(pfn_to_page(start)),
                .mode = MIGRATE_SYNC, 
                .ignore_skip_hint = true,
                .no_set_skip_hint = true,
                .gfp_mask = current_gfp_context(gfp_mask),
        };
        INIT_LIST_HEAD(&cc.migratepages);
        //将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。
        //因为buddy系统不会从 MIGRATE_ISOLATE 迁移类型的pageblock 分配页面,可以防止在cma分配过程中,这些页面又被人从Buddy分走。
        ret = start_isolate_page_range(pfn_max_align_down(start),
                                       pfn_max_align_up(end), migratetype, 0);
        if (ret)
                return ret;
        //将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用
        ret = __alloc_contig_migrate_range(&cc, start, end);
        if (ret && ret != -EBUSY)
                goto done;
        ret =0;
        //回收lru链表
        lru_add_drain_all();
        //回收per-cpu pages,回收过程需要先将放在PCP缓存页面再归还给Buddy。
        drain_all_pages(cc.zone);

        order = 0;
        outer_start = start;
        while (!PageBuddy(pfn_to_page(outer_start))) {
                if (++order >= MAX_ORDER) {
                        outer_start = start;
                        break;
                }
                outer_start &= ~0UL << order;
        }

        if (outer_start != start) {
                order = page_order(pfn_to_page(outer_start));
                if (outer_start + (1UL << order) <= start)
                        outer_start = start;
        }
       /* Make sure the range is really isolated. */
        if (test_pages_isolated(outer_start, end, false)) {
                pr_info_ratelimited("%s: [%lx, %lx) PFNs busy\n",
                        __func__, outer_start, end);
                ret = -EBUSY;
                goto done;
        }

        /* Grab isolated pages from freelists. */
        outer_end = isolate_freepages_range(&cc, outer_start, end);
        if (!outer_end) {
                ret = -EBUSY;
                goto done;
        }

        /* Free head and tail (if any) */
        if (start != outer_start)
                free_contig_range(outer_start, start - outer_start);
        if (end != outer_end)
                free_contig_range(end, outer_end - end);

done:
        //pageblock的迁移类型从 MIGRATE_ISOLATE 恢复为 MIGRATE_CMA
        undo_isolate_page_range(pfn_max_align_down(start),
                                pfn_max_align_up(end), migratetype);
        return ret;
}
3.2.1 start_isolate_page_range函数
c 复制代码
//mm/page_isolation.c
//将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。
//#define pageblock_nr_pages      (1UL << pageblock_order)
//非HUGETLB  pageblock_order = MAX_ORDER - 1
int start_isolate_page_range(unsigned long start_pfn, unsigned long end_pfn, unsigned migratetype, int flags)
{
        unsigned long pfn;
        unsigned long undo_pfn;
        struct page *page;

        BUG_ON(!IS_ALIGNED(start_pfn, pageblock_nr_pages));
        BUG_ON(!IS_ALIGNED(end_pfn, pageblock_nr_pages));

        for (pfn = start_pfn;
             pfn < end_pfn;
             pfn += pageblock_nr_pages) {
                //判断页框号是否有效,返回第一个有效的page
                page = __first_valid_page(pfn, pageblock_nr_pages);
                if (page &&
                    set_migratetype_isolate(page, migratetype, flags)) {
                        undo_pfn = pfn;
                        goto undo;
                }
        }
        return 0;
undo:
        for (pfn = start_pfn;
             pfn < undo_pfn;
             pfn += pageblock_nr_pages) {
                struct page *page = pfn_to_online_page(pfn);
                if (!page)
                        continue;
                unset_migratetype_isolate(page, migratetype);
        }

        return -EBUSY;
}

static inline struct page *
__first_valid_page(unsigned long pfn, unsigned long nr_pages)
{       
        int i;

        for (i = 0; i < nr_pages; i++) {
                struct page *page; 
                //给定的pfn是否有效
                if (!pfn_valid_within(pfn + i))
                        continue;
                //只有当页面处于在线状态时,才返回有效页框号的页面
                page = pfn_to_online_page(pfn + i);
                if (!page)
                        continue;
                return page;
        }
        return NULL;
}
#define pfn_valid_within(pfn) pfn_valid(pfn)
int pfn_valid(unsigned long pfn)
{
        //左移操作符 `<<` 来将页框号 `pfn` 转换为对应的物理地址 `addr`
        phys_addr_t addr = pfn << PAGE_SHIFT;

        if ((addr >> PAGE_SHIFT) != pfn)
                return 0;
#ifdef CONFIG_SPARSEMEM
        //页框号对应的内存区域号是否超出了系统支持的内存区域数量 `NR_MEM_SECTIONS`
        if (pfn_to_section_nr(pfn) >= NR_MEM_SECTIONS)
                return 0;
        if (!valid_section(__nr_to_section(pfn_to_section_nr(pfn))))
                return 0;
#endif
        //检查给定的物理地址是否属于内存映射区域
        return memblock_is_map_memory(addr);
}
EXPORT_SYMBOL(pfn_valid);

static int set_migratetype_isolate(struct page *page, int migratetype, int isol_flags)
{
        struct zone *zone;
        unsigned long flags, pfn;
        struct memory_isolate_notify arg;
        int notifier_ret;
        int ret = -EBUSY;

        zone = page_zone(page);

        spin_lock_irqsave(&zone->lock, flags);

        //如果指定页面已经被标记为隔离状态,则跳转到标签 `out`
        if (is_migrate_isolate_page(page))
                goto out;
        //将页面转换为页框号,并赋值给 `pfn` 变量
        pfn = page_to_pfn(page);
        arg.start_pfn = pfn;
        arg.nr_pages = pageblock_nr_pages;
        arg.pages_found = 0;

        //函数通知内存隔离事件,并根据返回值进行相应的处理,将结果保存在 `notifier_ret` 变量中
        notifier_ret = memory_isolate_notify(MEM_ISOLATE_COUNT, &arg);
        notifier_ret = notifier_to_errno(notifier_ret);
        if (notifier_ret)
                goto out;
        //检查是否存在不可移动的页面
        if (!has_unmovable_pages(zone, page, arg.pages_found, migratetype, flags))
                ret = 0;
out:
        //如果 `ret` 为零,则更新相关的内存状态信息,并释放空闲页块
        if (!ret) {
                unsigned long nr_pages;
                int mt = get_pageblock_migratetype(page);
                //设置为MIGRATE_ISOLATE
                set_pageblock_migratetype(page, MIGRATE_ISOLATE);
                zone->nr_isolate_pageblock++;
                //将范围内的空闲页面移动到请求类型的空闲列表中
                //list_move(&page->lru,&zone->free_area[order].free_list[migratetype]);
                nr_pages = move_freepages_block(zone, page, MIGRATE_ISOLATE, NULL);
                //vmstat统计,将zone中NR_FREE_PAGES与NR_FREE_CMA_PAGES内存区域的空闲页面数量减少nr_pages
                __mod_zone_freepage_state(zone, -nr_pages, mt);
        }
        spin_unlock_irqrestore(&zone->lock, flags);
        if (!ret)
                //当系统需要申请内存时,优先从PCP缓存拿,用完了再从buddy批发。
                //释放时也优先放回该PCP缓存,缓存满了再放回buddy系统。
                //将所有 CPU 上的每个 per-cpu 页面重新放回伙伴分配器
                drain_all_pages(zone);
        return ret;
}
3.2.2 __alloc_contig_migrate_range函数

将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用。

c 复制代码
/* [start, end) must belong to a single zone. */
static int __alloc_contig_migrate_range(struct compact_control *cc,
                                        unsigned long start, unsigned long end)
{
        /* This function is based on compact_zone() from compaction.c. */
        unsigned long nr_reclaimed;
        unsigned long pfn = start;
        unsigned int tries = 0;
        int ret = 0;
        //CPU 的所有类型的pagevec放到对应类型的lru链表中
        migrate_prep();
        while (pfn < end || !list_empty(&cc->migratepages)) {
                if (fatal_signal_pending(current)) {
                        ret = -EINTR;
                        break;
                }

                if (list_empty(&cc->migratepages)) {
                        cc->nr_migratepages = 0;
                        //隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。
                        //这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。
                        pfn = isolate_migratepages_range(cc, pfn, end);
                        if (!pfn) {
                                ret = -EINTR;
                                break;
                        }
                        tries = 0;
                } else if (++tries == 5) {
                        ret = ret < 0 ? ret : -EBUSY;
                        break;
                }
                //对于干净的文件页,直接回收即可
                nr_reclaimed = reclaim_clean_pages_from_list(cc->zone,
                                                        &cc->migratepages);
                cc->nr_migratepages -= nr_reclaimed;
                //该函数是页面迁移在内核态的主要接口,它把可移动的物理页迁移到一个新分配的页面。
                ret = migrate_pages(&cc->migratepages, alloc_migrate_target,
                                    NULL, 0, cc->mode, MR_CONTIG_RANGE);
        }
        if (ret < 0) {
                putback_movable_pages(&cc->migratepages);
                return ret;
        }
        return 0;
}

migrate_prep()函数

c 复制代码
//CPU 的所有类型的pagevec放到对应类型的lru链表中
int migrate_prep(void)
{
        //清除LRU列表,以便页面可以被隔离
        lru_add_drain_all();
        return 0;
}
void lru_add_drain(void)
{
        lru_add_drain_cpu(get_cpu());
        put_cpu();
}
//每个cpu有一个lru_pvecs(DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs)) , lru_pvecs中有5个不同的pagevec,
//这5条链表分别是LRU_INACTIVE_ANON,LRU_ACTIVE_ANON,LRU_INACTIVE_FILE,LRU_ACTIVE_FILE,LRU_UNEVICTABLE
//每个pagevec可以暂存PAGEVEC_SIZE(15)个page。内核在不同地方将需要添加的不同类型页pagevec_add 到 pagevec中。
//将CPU 的所有类型的pagevec放到对应类型的lru链表中。
//要么"cpu"是当前CPU,并且已经禁用了抢占;要么"cpu"正在热拔插,已经失效。
void lru_add_drain_cpu(int cpu)
{
        //lru_add_pvec:原来不属于lru链表的,新加入进来的页
        struct pagevec *pvec = &per_cpu(lru_add_pvec, cpu);

        if (pagevec_count(pvec))
                //Add the passed pages to the LRU
                __pagevec_lru_add(pvec);
        //lru_rotate_pvecs:非活动页并且在非活动lru链表中,将这些页移动到非活动lru链表的末尾
        pvec = &per_cpu(lru_rotate_pvecs, cpu);
        if (pagevec_count(pvec)) {
                unsigned long flags;
                local_irq_save(flags);
                pagevec_move_tail(pvec);
                local_irq_restore(flags);
        }
        //lru_deactivate_file_pvecs:活动lru链表中的页,会强制清除PG_activate和PG_referenced,
        //并加入到非活动lru链表的链表表头中,这些页一般从活动lru链表中的尾部拿出来的
        pvec = &per_cpu(lru_deactivate_file_pvecs, cpu);
        if (pagevec_count(pvec))
                pagevec_lru_move_fn(pvec, lru_deactivate_file_fn, NULL);

        pvec = &per_cpu(lru_lazyfree_pvecs, cpu);
        if (pagevec_count(pvec))
                pagevec_lru_move_fn(pvec, lru_lazyfree_fn, NULL);

        activate_page_drain(cpu);
}

void __pagevec_lru_add(struct pagevec *pvec)
{
        pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}

//对每个pvec中的每个page执行move_fn
static void pagevec_lru_move_fn(struct pagevec *pvec,
        void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
        void *arg)
{
        int i;
        struct pglist_data *pgdat = NULL;
        struct lruvec *lruvec;
        unsigned long flags = 0;
        //每个pvec中的每个page
        for (i = 0; i < pagevec_count(pvec); i++) {
                struct page *page = pvec->pages[i];
                struct pglist_data *pagepgdat = page_pgdat(page);

                if (pagepgdat != pgdat) {
                        if (pgdat)
                                spin_unlock_irqrestore(&pgdat->lru_lock, flags);
                        pgdat = pagepgdat;
                        spin_lock_irqsave(&pgdat->lru_lock, flags);
                }

                lruvec = mem_cgroup_page_lruvec(page, pgdat);
                (*move_fn)(page, lruvec, arg);
        }
        if (pgdat)
                spin_unlock_irqrestore(&pgdat->lru_lock, flags);
        release_pages(pvec->pages, pvec->nr);
        pagevec_reinit(pvec);
}

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
                                 void *arg)
{
        enum lru_list lru;
        int was_unevictable = TestClearPageUnevictable(page);

        VM_BUG_ON_PAGE(PageLRU(page), page);
        //设置page的LRU标志位
        SetPageLRU(page);
        smp_mb();
        //可回收的page
        if (page_evictable(page)) {
                lru = page_lru(page);
                update_page_reclaim_stat(lruvec, page_is_file_cache(page),
                                         PageActive(page));
                if (was_unevictable)
                        count_vm_event(UNEVICTABLE_PGRESCUED);
        } else {
                //不可回收的page
                lru = LRU_UNEVICTABLE;
                ClearPageActive(page);
                SetPageUnevictable(page);
                if (!was_unevictable)
                        count_vm_event(UNEVICTABLE_PGCULLED);
        }
        //添加page到lru链表
        add_page_to_lru_list(page, lruvec, lru);
        trace_mm_lru_insertion(page, lru);
}

static __always_inline void add_page_to_lru_list(struct page *page,
                                struct lruvec *lruvec, enum lru_list lru)
{
        //更新lru size,添加page到lru链表
        update_lru_size(lruvec, lru, page_zonenum(page), hpage_nr_pages(page));
        list_add(&page->lru, &lruvec->lists[lru]);
}

isolate_migratepages_range()函数

c 复制代码
//mm/compaction.c
//隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。
//这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。
unsigned long
isolate_migratepages_range(struct compact_control *cc, unsigned long start_pfn,
                                                        unsigned long end_pfn)
{       
        unsigned long pfn, block_start_pfn, block_end_pfn;

        /* Scan block by block. First and last block may be incomplete */
        pfn = start_pfn;
        block_start_pfn = pageblock_start_pfn(pfn);
        if (block_start_pfn < cc->zone->zone_start_pfn)
                block_start_pfn = cc->zone->zone_start_pfn;
        block_end_pfn = pageblock_end_pfn(pfn);
 
        for (; pfn < end_pfn; pfn = block_end_pfn,
                                block_start_pfn = block_end_pfn,
                                block_end_pfn += pageblock_nr_pages) {
                        
                block_end_pfn = min(block_end_pfn, end_pfn);
                if (!pageblock_pfn_to_page(block_start_pfn,
                                        block_end_pfn, cc->zone))
                        continue;
                //将单个页块pageblock中所有可迁移页面隔离
                //隔离给定范围 [low_pfn, end_pfn) 中所有可迁移的页面。这个范围被期望位于同一个页块内。
                //如果有致命信号待处理,返回 0,否则返回第一个未扫描的页面的 PFN,
                //它可能小于、等于或大于 end_pfn。
                //页面被隔离到 cc->migratepages 列表(不需要为空),并相应地更新 cc->nr_migratepages 字段。
                pfn = isolate_migratepages_block(cc, pfn, block_end_pfn, ISOLATE_UNEVICTABLE);
                if (!pfn)
                        break;      
                if (cc->nr_migratepages == COMPACT_CLUSTER_MAX)
                        break;
        }       
        return pfn;
}

reclaim_clean_pages_from_list()函数

c 复制代码
//mm/vmscan.c
//对于干净的文件页,直接回收即可
unsigned long reclaim_clean_pages_from_list(struct zone *zone,
                                            struct list_head *page_list)
{                               
        struct scan_control sc = {
                .gfp_mask = GFP_KERNEL,
                .priority = DEF_PRIORITY,
                .may_unmap = 1,
        };              
        unsigned long ret;
        struct page *page, *next;
        LIST_HEAD(clean_pages);
                
        list_for_each_entry_safe(page, next, page_list, lru) {
                if (page_is_file_cache(page) && !PageDirty(page) &&
                    !__PageMovable(page)) {
                        ClearPageActive(page);
                        list_move(&page->lru, &clean_pages);
                }
        }
        //回收page,returns the number of reclaimed pages
        ret = shrink_page_list(&clean_pages, zone->zone_pgdat, &sc,
                        TTU_IGNORE_ACCESS, NULL, true);
        list_splice(&clean_pages, page_list);
        mod_node_page_state(zone->zone_pgdat, NR_ISOLATED_FILE, -ret);
        return ret;
}

migrate_pages()函数

该函数是页面迁移在内核态的主要接口,它把可移动的物理页迁移到一个新分配的页面。

系统要使用CMA区域的内存,内存上的页面必须是可迁移的,这样子当设备要使用CMA时页面才能迁移走,那么哪些页面可以迁移呢?有两种类型:

  1. LRU上的页面,LRU链表上的页面为用户进程地址空间映射的页面,如匿名页和文件页,都是从buddy分配器migrate type为movable的pageblock上分来的。

  2. 非LRU上,但是是movable页面。非LRU的页面通常是为kernel space分配的page,要实现迁移需要驱动实现page->mapping->a_ops中的相关方法。比如我们常见的zsmalloc内存分配器的页面就支持迁移。

如下图,migrate_pages()无非是要分配一个新的页面,断开旧页面的映射关系,重新简历映射到新的页面,并且要拷贝旧页面的内容到新页面、新页面的struct page属性要和旧页面设置得一样,最后释放旧的页面。

c 复制代码
//mm/migrate.c
//caller: migrate_pages(&cc->migratepages, alloc_migrate_target, NULL, 0, cc->mode, MR_CONTIG_RANGE);
/*
 * from: 准备迁移页面的链表
 * get_new_page:申请新页面函数的指针
 * putnew_page:释放新页面函数的指针
 * private:传给get_new_page的参数,CMA这里没有使用到传NULL
 * mode:迁移模式,CMA的迁移模式会设置为MIGRATE_SYNC。迁移过程可能发生阻塞,若需要迁移某个page正在writeback或locked会等待它完成
 * reason:迁移原因,记录是什么功能触发了迁移的行为。因为内核许多路径都需要用migrate_pages来迁移比如还有内存规整、热插拔等。
 *         CMA传递的为MR_CONTIG_RANG,表示调用alloc_contig_range()分配连续内存。
 
  
migrate_mode迁移模式主要有以下几个:

-   MIGRATE_ASYNC //异步迁移,过程中不会发生阻塞。
-   MIGRATE_SYNC_LIGHT //轻度同步迁移,允许大部分的阻塞操作,唯独不允许脏页的回写操作。
-   MIGRATE_SYNC //同步迁移,迁移过程会发生阻塞,若需要迁移的某个page正在writeback或被locked会等待它完成
-   MIGRATE_SYNC_NO_COPY //同步迁移,但不等待页面的拷贝过程。页面的拷贝通过回调migratepage(),过程可能会涉及DMA

migrate reason 用于说明迁移原因:

-   MR_COMPACTION //内存规整导致的迁移
-   MR_MEMORY_FAILURE //当内存出现硬件问题(ECC校验失败等)时触发的页面迁移。 参考memory-failure.c
-   MR_MEMORY_HOTPLUG //内存热插拔导致的迁移
-   MR_SYSCALL //应用层主动调用migrate_pages()或move_pages()触发的迁移。
-   MR_MEMPOLICY_MBIND //调用mbind系统调用设置memory policy时触发的迁移
-   MR_NUMA_MISPLACED //numa balance触发的页面迁移(node之间)
-   MR_CONTIG_RANGE //调用alloc_contig_range()为CMA或HugeTLB分配连续内存时触发的迁移(和compact相关)。

 */

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;
                //遍历 from链表,对每个page调用unmap_and_move来实现迁移处理。page2是page在from中的下一个页
                list_for_each_entry_safe(page, page2, from, lru) {
retry:
                        cond_resched();

                        if (PageHuge(page))
                                rc = unmap_and_move_huge_page(get_new_page,
                                                put_new_page, private, page,
                                                pass > 2, mode, reason);
                        else
                                //调用get_new_page分配一个新页面,然后使用__unmap_and_move迁移页面到这个新分配的页面中
                                rc = unmap_and_move(get_new_page, put_new_page,
                                                private, page, pass > 2, mode,
                                                reason);
                        //返回值处理 
                        switch(rc) {
                        case -ENOMEM:
                                if (PageTransHuge(page) && !PageHuge(page)) {
                                        lock_page(page);
                                        rc = split_huge_page_to_list(page, from);
                                        unlock_page(page);
                                        if (!rc) {
                                                list_safe_reset_next(page, page2, lru);
                                                goto retry;
                                        }
                                }
                                nr_failed++;
                                goto out;
                        case -EAGAIN:
                                retry++;
                                break;
                        case MIGRATEPAGE_SUCCESS:
                                nr_succeeded++;
                                break;
                        default:
                                nr_failed++;
                                break;
                        }
                }
        }
        nr_failed += retry;
        rc = nr_failed;
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;
}
c 复制代码
static ICE_noinline 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,
                                   enum migrate_reason reason)
{
        int rc = MIGRATEPAGE_SUCCESS;
        struct page *newpage;

        if (!thp_migration_supported() && PageTransHuge(page))
                return -ENOMEM;
        //get_new_page分配一个新页面,具体见alloc_migrate_target()
        newpage = get_new_page(page, private);
        if (!newpage)
                return -ENOMEM;
        //如果页的refcount == 1,说明此页必定是非文件页而且没有进程映射了此页,此页可以直接释放掉(???)
        if (page_count(page) == 1) {
                /* page was freed from under us. So we are done. */
                ClearPageActive(page);
                ClearPageUnevictable(page);
                if (unlikely(__PageMovable(page))) {
                        lock_page(page);
                        if (!PageMovable(page))
                                __ClearPageIsolated(page);
                        unlock_page(page);
                }
                if (put_new_page)
                        put_new_page(newpage, private);
                else
                        put_page(newpage);
                goto out;
        }
        //将page取消映射,并把page的数据复制到newpage中、
        rc = __unmap_and_move(page, newpage, force, mode);
        if (rc == MIGRATEPAGE_SUCCESS)
                set_page_owner_migrate_reason(newpage, reason);

out:
        if (rc != -EAGAIN) {
                //已迁移的页面将删除所有引用并将被释放。未迁移的页面将保留其引用并将被恢复。
                list_del(&page->lru);
                if (likely(!__PageMovable(page)))
                        mod_node_page_state(page_pgdat(page), NR_ISOLATED_ANON +
                                        page_is_file_cache(page), -hpage_nr_pages(page));
        }

        //如果迁移成功,则释放在隔离期间占用的引用。否则,将页面恢复到正确的列表,除非我们想重试。
        if (rc == MIGRATEPAGE_SUCCESS) {
                put_page(page);
                if (reason == MR_MEMORY_FAILURE) {
                        //有意将 PG_HWPoison 设置在刚释放的页面上。虽然这听起来有些奇怪,但目前这是 HWPoison 标志的工作方式。
                        if (set_hwpoison_free_buddy_page(page))
                                num_poisoned_pages_inc();
                }
        } else {
                //迁移不成功,把旧页还原
                if (rc != -EAGAIN) {
                        if (likely(!__PageMovable(page))) {
                                putback_lru_page(page);
                                goto put_new;
                        }

                        lock_page(page);
                        if (PageMovable(page))
                                putback_movable_page(page);
                        else
                                __ClearPageIsolated(page);
                        unlock_page(page);
                        put_page(page);
                }
put_new:
                //迁移不成功,将新页放回到空闲页链表中,上面put_new_page为NULL,则调用put_page
                if (put_new_page)
                        put_new_page(newpage, private);
                else
                        put_page(newpage);
        }

        return rc;
}
c 复制代码
static __always_inline int __PageMovable(struct page *page)
{                
        return ((unsigned long)page->mapping & PAGE_MAPPING_FLAGS) ==
                                PAGE_MAPPING_MOVABLE;
}

static int __unmap_and_move(struct page *page, struct page *newpage,
                                int force, enum migrate_mode mode)
{
        int rc = -EAGAIN;
        int page_was_mapped = 0;
        struct anon_vma *anon_vma = NULL;
        bool is_lru = !__PageMovable(page); (???为什么moveable取反就是在lru上)
        //尝试获取old page的页面锁PG_locked
        if (!trylock_page(page)) {
                //force == 0 或者 对于MIGRATE_ASYNC模式的为异步迁移 拿不到锁就直接跳过此页面。
                if (!force || mode == MIGRATE_ASYNC)
                        goto out;
                //force == 1 && mode != MIGRATE_ASYNC
                //表示当前进程正在进行内存分配操作
                if (current->flags & PF_MEMALLOC)
                        goto out;
                //CMA迁移模式为MIGRATE_SYNC,这里一定使用lock_page一定要等到锁。
                lock_page(page);
        }
        //处理正在回写的页面,根据迁移模式判断是否等待页面回写完成。
        //MIGRATE_SYNC_LIGHT和MIGRATE_ASYNC不等待,cma迁移模式为MIGRATE_SYNC,会调用wait_on_page_writeback()函数等待页面回写完成。
        if (PageWriteback(page)) {
                switch (mode) {
                case MIGRATE_SYNC:
                case MIGRATE_SYNC_NO_COPY:
                        break;
                default:
                        rc = -EBUSY;
                        goto out_unlock;
                }
                if (!force)
                        goto out_unlock;
                wait_on_page_writeback(page);
        }

        //对于匿名页,为了防止迁移过程anon_vma数据结构被释放了,需要使用page_get_anon_vma增加anon_vma->refcount引用计数。
        if (PageAnon(page) && !PageKsm(page))
                anon_vma = page_get_anon_vma(page);

        //获取new page的页面锁PG_locked,正常情况都能获取到。
        if (unlikely(!trylock_page(newpage)))
                goto out_unlock;
        //判断这个页面是否属于非LRU页面
        if (unlikely(!is_lru)) {
                //如果页面为非LRU页面,则通过调用move_to_new_page来处理,该函数会回调驱动的miratepage函数来进行页面迁移。
                rc = move_to_new_page(newpage, page, mode);
                goto out_unlock_both;
        }

        //通过page_mapped()判断是否有用户PTE映射了该页面。
        //如果有则调用try_to_unmap(),通过反向映射机制解除old page所有相关的PTE。
        if (!page->mapping) {
                VM_BUG_ON_PAGE(PageAnon(page), page);
                if (page_has_private(page)) {
                        try_to_free_buffers(page);
                        goto out_unlock_both;
                }
        } else if (page_mapped(page)) {
                /* Establish migration ptes */
                VM_BUG_ON_PAGE(PageAnon(page) && !PageKsm(page) && !anon_vma,
                                page);
                //try to remove all page table mappings to a page
                try_to_unmap(page,
                        TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
                page_was_mapped = 1;
        }
        //调用move_to_new_page,拷贝old page的内容和struct page属性数据到new page。
        //对于LRU页面 move_to_new_page是通过调用migrate_page做了两件事:复制struct page的属性和页面内容。
        if (!page_mapped(page))
                rc = move_to_new_page(newpage, page, mode);
        //对页表进行迁移:remove_migration_ptes通过反向映射机制建立new page到进程的映射关系。
        if (page_was_mapped)
                remove_migration_ptes(page,
                        rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
        //迁移完成释放old、new页面的PG_locked,当然对于匿名页我们也要put_anon_vma减少的anon_vma->refcount引用计数
out_unlock_both:
        unlock_page(newpage);
out_unlock:
        /* Drop an anon_vma reference if we took one */
        if (anon_vma)
                put_anon_vma(anon_vma);
        unlock_page(page);
out:
        //如果迁移成功,则减少新页面的引用计数,但不会释放页面,因为新页面的所有者已增加引用计数器。
        //如果该页面是LRU页面,还将将页面添加到LRU列表中。
        if (rc == MIGRATEPAGE_SUCCESS) {
                if (unlikely(!is_lru))
                        put_page(newpage);
                else
                        putback_lru_page(newpage);
        }

        return rc;
}

如果有page被映射到多个虚拟地址,可以通过Rmap Walk机制来遍历所有的VMA,并最终调用回调函数来取消映射。

c 复制代码
/*
 * rmap_walk_control: 用于控制特定需求的 rmap 遍历
 *
 * arg: 传递给 rmap_one() 和 invalid_vma()
 * rmap_one: 对已映射页面的每个 vma 上执行
 * done: 用于检查遍历终止条件
 * anon_lock: 通过优化方式获取 anon_lock,而不是使用默认设置
 * invalid_vma: 用于跳过不感兴趣的 vma
 */
struct rmap_walk_control {
        void *arg;
        bool (*rmap_one)(struct page *page, struct vm_area_struct *vma,
                                        unsigned long addr, void *arg);
        int (*done)(struct page *page);
        struct anon_vma *(*anon_lock)(struct page *page);
        bool (*invalid_vma)(struct vm_area_struct *vma, void *arg);
};

/**
 * try_to_unmap - 尝试将页面的所有页表映射删除
 * @page: 要取消映射的页面
 * @flags: action and flags
 *
 * 尝试删除映射到此页面的所有页表项,调用者必须持有页面锁。
 */
bool try_to_unmap(struct page *page, enum ttu_flags flags)
{
        struct rmap_walk_control rwc = {
                .rmap_one = try_to_unmap_one, //完成页表映射移除工作
                .arg = (void *)flags,
                .done = page_mapcount_is_zero,
                .anon_lock = page_lock_anon_vma_read,
        };
        if ((flags & (TTU_MIGRATION|TTU_SPLIT_FREEZE))
            && !PageKsm(page) && PageAnon(page))
                rwc.invalid_vma = invalid_migration_vma;

        if (flags & TTU_RMAP_LOCKED)
                rmap_walk_locked(page, &rwc);
        else
                //完成页表映射移除工作,内部调用try_to_unmap_one()
                rmap_walk(page, &rwc);

        return !page_mapcount(page) ? true : false;
}
c 复制代码
//mm/migrate.c
//非LRU和LRU页面都是通过move_to_new_page来复制页面
static int move_to_new_page(struct page *newpage, struct page *page,
                                enum migrate_mode mode)
{
        struct address_space *mapping;
        int rc = -EAGAIN;
        bool is_lru = !__PageMovable(page);

        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(!PageLocked(newpage), newpage);

        mapping = page_mapping(page);
        //是lru页面
        if (likely(is_lru)) {
                //回调驱动的miratepage函数来进行页面迁移
                if (!mapping)
                        //复制struct page的属性和页面内容
                        rc = migrate_page(mapping, newpage, page, mode);
                else if (mapping->a_ops->migratepage)
                        //大多数页面都有映射,大多数文件系统都提供了 migratepage 回调函数
                        //匿名页面是交换空间的一部分,其也有自己的 migratepage 回调函数
                        rc = mapping->a_ops->migratepage(mapping, newpage,
                                                        page, mode);
                else
                        rc = fallback_migrate_page(mapping, newpage,
                                                        page, mode);
        } else {
                //对于非LRU页面,它可能会在隔离步骤后被释放。在这种情况下,我们不应该尝试迁移。
                VM_BUG_ON_PAGE(!PageIsolated(page), page);
                if (!PageMovable(page)) {
                        rc = MIGRATEPAGE_SUCCESS;
                        __ClearPageIsolated(page);
                        goto out;
                }
                //迁移
                rc = mapping->a_ops->migratepage(mapping, newpage,
                                                page, mode);
                WARN_ON_ONCE(rc == MIGRATEPAGE_SUCCESS &&
                        !PageIsolated(page));
        }

        //在释放页面之前,需要清除旧的 pagecache 页面的 `page->mapping` 指针。
        //但是,为了统计目的,需要保留页面的 `PageAnon` 标志不变。
        if (rc == MIGRATEPAGE_SUCCESS) {
                if (__PageMovable(page)) {
                        VM_BUG_ON_PAGE(!PageIsolated(page), page);

                        //进行页面迁移操作时,需要在 `page_lock`(页面锁)下清除 `PG_movable` 标志位。
                        //这样可以确保在页面正在被处理的过程中,任何compactor都不会尝试迁移此页面。
                        __ClearPageIsolated(page);
                }

                //对于匿名(anonymous)页面和可移动的(movable)页面,其 `page->mapping` 指针会在 `free_pages_prepare` 中进行清除操作,因此在这里不需要再次重置。
                //这样可以保持页面标记为匿名页面(PageAnon),以确保其正常工作。
                if (!PageMappingFlags(page))
                        page->mapping = NULL;
        }
out:
        return rc;
}

int migrate_page(struct address_space *mapping,
                struct page *newpage, struct page *page,
                enum migrate_mode mode)
{
        int rc;
        BUG_ON(PageWriteback(page));    /* Writeback must be complete */
        //先检查page的refcount是否符合预期,符合后之后会复制页面的映射数据,比如page->index、page->mapping以及PG_swapbacked
        rc = migrate_page_move_mapping(mapping, newpage, page, mode, 0);

        if (rc != MIGRATEPAGE_SUCCESS)
                return rc;

        if (mode != MIGRATE_SYNC_NO_COPY)
                //page页面内容的复制
                migrate_page_copy(newpage, page);
        else
                //migrate_page_states用来复制页面的flag,如PG_dirty,PG_XXX等标志位,也是属于struct page属性的复制。
                migrate_page_states(newpage, page);
        return MIGRATEPAGE_SUCCESS;
}

3.3 CMA释放流程

cma_release释放CMA内存的代码很简单,就是把页面重新free给Buddy,清除掉cma的bitmap分配标识:

c 复制代码
/**
 * cma_release() - release allocated pages
 * @cma:   Contiguous memory region for which the allocation is performed.
 * @pages: Allocated pages.
 * @count: Number of allocated pages.
 *
 *  此函数释放由 alloc_cma() 分配的内存。
 *  当提供的页面不属于连续区域时,返回 false;否则返回 true。
 */
bool cma_release(struct cma *cma, const struct page *pages, unsigned int count)
{
        unsigned long pfn;
        if (!cma || !pages)
                return false;
        pr_debug("%s(page %p)\n", __func__, (void *)pages);
        pfn = page_to_pfn(pages);
        if (pfn < cma->base_pfn || pfn >= cma->base_pfn + cma->count)
                return false;

        VM_BUG_ON(pfn + count > cma->base_pfn + cma->count);
        //把页面重新free给Buddy
        free_contig_range(pfn, count);
        //清除掉cma的bitmap分配标识
        cma_clear_bitmap(cma, pfn, count);
        trace_cma_release(pfn, pages, count);

        return true;
}

4.参考资料

1. CMA技术原理分析--内核工匠

2. Linux内存管理之CMA -- LoyenWang

3. Linux内存管理之RMAP -- LoyenWang

4. 宋宝华:论Linux的页迁移(Page Migration)完整版

相关推荐
TravisBytes29 分钟前
linux 系统是如何收发数据包
linux·运维·服务器
ice___Cpu2 小时前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
z202305082 小时前
linux 之0号进程、1号进程、2号进程
linux·运维·服务器
狐心kitsune3 小时前
erlang学习:Linux常用命令1
linux·学习·erlang
DREAM依旧3 小时前
《深入了解 Linux 操作系统》
linux
阿赭ochre4 小时前
Linux环境变量&&进程地址空间
linux·服务器
Iceberg_wWzZ4 小时前
数据结构(Day14)
linux·c语言·数据结构·算法
可儿·四系桜4 小时前
如何在多台Linux虚拟机上安装和配置Zookeeper集群
linux·服务器·zookeeper
Flying_Fish_roe4 小时前
linux-软件包管理-包管理工具(Debian 系)
linux·运维·debian
大广-全栈开发5 小时前
centos 7 安装gitlab
linux·git·centos