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]]]
  • nnMG: 为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)完整版

相关推荐
2035去旅行11 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
陈希12312 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡12 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.16 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道16 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.19 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落20 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨21 小时前
WSL2的安装与使用
linux·wsl
微小的鱼21 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng21 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器