内存管理-49-alloc_pages-1-快速路径
基于 msm-5.4
一、简介
本文只介绍内存分配快速路径。
二、数据结构
1. struct alloc_context
struct alloc_context { struct zonelist *zonelist; nodemask_t *nodemask; struct zoneref *preferred_zoneref; int migratetype; enum zone_type high_zoneidx; bool spread_dirty_pages; };
此结构用于在分配过程中保存在分配函数(包括 alloc_pages 系列函数)之间传递的大多数不可变分配参数的结构。其 nodemask, migratetype 和 high_zoneidx 成员仅在 __alloc_pages_nodemask() 中初始化一次,之后永远不会改变。zonelist, preferred_zoneref 首先在 __alloc_pages_nodemask() 中为快速路径进行设置,可能稍后在 __alloc_pages_slowpath() 中更改。所有其他函数都通过 const 指针传递整个结构。
此结构主要在 prepare_alloc_pages() 中初始化。
成员介绍:
zonelist: 一次分配请求是针对一个区域列表进行操作。区域列表是zone的列表,第一个区域是分配的“目标”,其他zone是后备区域,优先级依次降低。
nodemask: cpuset使能时,指定可以从哪些node分配物理内存,嵌入式设备一般只有一个node. 如果没有指定,则在所有节点中进行分配.
preferred_zoneref: 在快速路径中指定要首先分配的区域,在慢路径中指定了zonelist中的第一个可用区域. //TODO:确认
migratetype: 本次分配要分配的页面的迁移类型。
high_zoneidx: 允许内存分配的最高zone。
spread_dirty_pages: 指定是否进行脏页的传播。
三、全局变量
1. gfp_allowed_mask
//(1<<25-1) 并清除第 ___GFP_NOLOCKDEP 位 #define __GFP_BITS_MASK (((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) & ~0x800000u) //(1<<25-1) 并清除第 ___GFP_NOLOCKDEP、__GFP_RECLAIM、__GFP_IO、__GFP_FS 位 #define GFP_BOOT_MASK (__GFP_BITS_MASK & ~(__GFP_RECLAIM|__GFP_IO|__GFP_FS)) gfp_t gfp_allowed_mask __read_mostly = GFP_BOOT_MASK; //page_alloc.c 不可FS、不可IO、不可回收。 start_kernel //main.c kernel_init //main.c init进程 kernel_init_freeable gfp_allowed_mask = __GFP_BITS_MASK; //表示调度程序初始化好了,可以进行阻塞分配了 enter_state //suspend.c 【】休眠路径中调用 hibernation_snapshot pm_restrict_gfp_mask saved_gfp_mask = gfp_allowed_mask; gfp_allowed_mask &= ~(__GFP_IO | __GFP_FS); //去掉IO和FS标志 enter_state //suspend.c 【】reseume路径中调用 hibernation_restore //hibernate.c pm_restore_gfp_mask //page_alloc.c 若右值非0才赋值 gfp_allowed_mask = saved_gfp_mask;
主要使用位置:
alloc_pages __alloc_pages_nodemask //page_alloc.c gfp_mask &= gfp_allowed_mask; //取交集,对分配掩码进行系统级过滤
gfp_allowed_mask 在早期启动期间设置为 GFP_BOOT_MASK, 以限制在启用中断之前哪些 GFP 标志可以被使用。一旦中断被启用,调度器就绪后时它被设置为 __GFP_BITS_MASK。在休眠期间,PM 使用它来避免在设备 suspend 后分配内存时进行 I/O。
通过它,可以对每次内存的分配掩码施加全局约束。
2. page_group_by_mobility_disabled
int page_group_by_mobility_disabled; //page_alloc.c
从 build_all_zonelists() 中的打印看,值是为false的. 默认是false, 后来也设置为false,即恒为false。如果此全局变量被设置为true,则所有内存都是不可移动的。
相关资料:
linux kernel内存碎片防治技术: http://www.wowotech.net/memory_management/memory-fragment.html
3. watermark_boost_factor
通过 /proc/sys/vm/watermark_boost_factor 可以修改此全局变量的值,boost_watermark() 中使用。它Boost的是High水位,单位是1/10000, 最终boost的结果存到 zone->watermark_boost 中,后者有会同时影响三个水位(Min、Low、High)的值, 见 min_wmark_pages()等宏。
get_page_from_freelist //page_alloc.c get_populated_pcp_list //page_alloc.c rmqueue //page_alloc.c rmqueue_bulk //page_alloc.c __rmqueue //page_alloc.c 快速路径中,分配不到内存时调用 __rmqueue_fallback steal_suitable_fallback //page_alloc.c boost_watermark(zone) max_boost = zone->_watermark[WMARK_HIGH] * watermark_boost_factor/10000; max_boost = max(pageblock_nr_pages, max_boost); zone->watermark_boost = min(zone->watermark_boost + pageblock_nr_pages, max_boost);
四、函数接口
相关接口主要定义在 include/linux/gfp.h 中。
1. 基于struct page进行分配物理页
struct page *alloc_pages(gfp_mask, order); //分配2^order个连续的物理页 void __free_pages(struct page *page, unsigned int order); struct page *alloc_page(gfp_mask); //只分配一个物理页 void __free_page(page);
2. 基于虚拟地址进行分配物理页
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); //分配并映射到虚拟地址 void free_pages(unsigned long addr, unsigned int order); unsigned long get_zeroed_page(gfp_t gfp_mask); //分配一个物理页并清0 void free_page(addr); //基于虚拟地址释放一个物理页
五、功能实现
alloc_pages()
基于页帧的分配函数
alloc_pages(gfp_mask, order) //gfp.h alloc_pages_node(0, gfp_mask, order) //gfp.h __alloc_pages_node(0, gfp_mask, order) //gfp.h __alloc_pages(gfp_mask, order, 0) //gfp.h __alloc_pages_nodemask(gfp_mask, order, 0, NULL) //page_alloc.c
1. __alloc_pages_nodemask()
/* * 这是按zone划分的伙伴分配器的核心函数。每个zone都有自己独立的伙伴系统。#### * * __alloc_pages: (gfp_mask, order, 0, NULL) */ struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask) { struct page *page; unsigned int alloc_flags = ALLOC_WMARK_LOW; //默认 WMARK_LOW gfp_t alloc_mask; /* 实际用于分配的 gfp_t,分配过程中可能会变化 */ struct alloc_context ac = { }; /* * 翻译: 后面假设了order值是合理的,因此如果超出范围,需尽早退出。 * order大于等于11就异常退出,决定buddy最大能分配内存2^10=4MB ###### */ if (unlikely(order >= MAX_ORDER)) { /* 若没有通过gfp标志指定不允许warn, 则打印一次栈回溯, 并返回NULL */ WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN)); return NULL; } /* * 这里对gfp分配标志有个过滤,取交集后保存到 alloc_mask 中。 * * gfp_allowed_mask 在早期启动期间设置为 GFP_BOOT_MASK, 以限制在启用中断之前哪些 GFP 标志可以被使用。 * 一旦中断被启用,系统运行时它会被设置为 __GFP_BITS_MASK。在休眠期间,PM 使用它来避免在设备suspend * 后分配内存时进行 I/O。 * * 对此时的分配掩码进行全局约束(这里是全局约束####,对快慢路径都有效,下面还有每进程约束####,只对慢速路径 * 有效) */ gfp_mask &= gfp_allowed_mask; //全局约束直接修改了用户的 gfp_mask alloc_mask = gfp_mask; /* * 做一些分配物理页前的准备工作,初始化 alloc_context; 若使能cpuset,指定只能从 * current->mems_allowed 列出的节点中分配内存####; 若gfp标志指定的是可移动类型的分配, * 则允许从CMA内存区分配。 */ if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags)) return NULL; /* 做脏区平衡,并决定优先从哪个zone中分配物理内存 */ finalise_ac(gfp_mask, &ac); /* * 翻译:禁止第一遍回退到碎片内存的类型,直到考虑到所有本地区域。 * 用于决定是否给分配标志或上 ALLOC_KSWAPD 标志。 */ alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask); /* * 【1】第一次分配尝试,是 fastpath 快速路径: 从伙伴系统空闲链表中获取page。若是申请 * 的是单个页,优先从cpu本地缓存去拿。 */ page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); if (likely(page)) goto out; /*---- 下面就要走内存分配慢速路径了 ----*/ /* * 翻译: 应用范围分配约束。这主要与 GFP_NOFS 和 GFP_NOIO 有关,也与 NOCMA 有关, * 对于来自特定上下文的所有分配请求,必须继承 GFP_NOFS 和 GFP_NOIO,这些上下文已由 * memalloc_no{fs,io}_{save,restore} 标记。###### * * 要走慢速分配路径,此时能否FS/IO要提前check好。 */ alloc_mask = current_gfp_context(gfp_mask); /* 上面 finalise_ac()中设置了,只用于快速路径分配内存,这里要走慢速路径了,直接设置为false */ ac.spread_dirty_pages = false; /* * 翻译: 如果为了优化快速路径的尝试使用了 &cpuset_current_mems_allowed 替换了原始节 * 点掩码(cpuset使能),则恢复原始节点掩码。也即慢速路径使用调用参数掩码NULL,无论cpuset * 是否使能。 */ if (unlikely(ac.nodemask != nodemask)) ac.nodemask = nodemask; /* * 【2】再次尝试分配,slowpath慢速路径: 对内存进行整理、迁移、压缩、交换,腾出大块连续物理内存。 * 这里面有trace打印,看来是可以安全加trace的 ##### */ page = __alloc_pages_slowpath(alloc_mask, order, &ac); out: /* Arm64嵌入式只有一个node, 这里一般是不成立的,先忽略 */ if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page && unlikely(__memcg_kmem_charge(page, gfp_mask, order) != 0)) { /* 若 page->_refcount 减1后为0了,则进行释放 */ __free_pages(page, order); page = NULL; } trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); return page; } EXPORT_SYMBOL(__alloc_pages_nodemask);
trace_mm_page_alloc() 打印内容,看起来很多不是 GFP_KERNEL 类型的分配:
UsbFfs-worker-5813 [000] .... 12138.030247: mm_page_alloc: page=ffffffff03ac0640 pfn=1519641 order=0 migratetype=0 gfp_flags=GFP_HIGHUSER|__GFP_NOWARN pool-3-thread-8-5771 [002] .... 12138.030903: mm_page_alloc: page=ffffffff03e13f80 pfn=1574142 order=0 migratetype=1 gfp_flags=GFP_HIGHUSER_MOVABLE|__GFP_ZERO|0x1000000
1.1 prepare_alloc_pages()
/* * alloc_pages: (gfp_mask, order, 0, NULL, &ac, &alloc_mask, &alloc_flags) * gfp_mask是约束后的,alloc_mask传参时等于gfp_mask, ac传参时全为0, alloc_flags传参时是 ALLOC_WMARK_LOW 。 * 后续 gfp_mask 是不变的,alloc_mask 会被子函数更改。 */ static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask, struct alloc_context *ac, gfp_t *alloc_mask, unsigned int *alloc_flags) { /* * 将gfp标志转换为 enum zone_type 类型的返回值. 返回值为 ((1<<10) >> (gfp_mask&0xf)) & 1, * 目前配置下无论gfp_mask传什么, 都返回0, 即 ZONE_NORMAL */ ac->high_zoneidx = gfp_zone(gfp_mask); /* 恒返回全局 contig_page_data.node_zonelists[0],数组只有一个元素,只能返回它 */ ac->zonelist = node_zonelist(preferred_nid, gfp_mask); ac->nodemask = nodemask; //NULL /* * 等效于 (gfp_mask & 0x18) >> 3 只保留gfp_mask中的bit3-bit4, 即只检测 ___GFP_RECLAIMABLE=0x10, * ___GFP_MOVABLE=0x08 这两个标志位, 用于获取gfp标志中的描述迁移特性的部分。 * 若是 GFP_KERNEL=0xcc0结果就是0,表示不可移动。其定义旁边所有标志中只有 GFP_HIGHUSER_MOVABLE、 * GFP_TRANSHUGE_LIGHT、GFP_TRANSHUGE 三个是可以移动的。 */ ac->migratetype = gfpflags_to_migratetype(gfp_mask); /* cpuset默认是使能的 */ if (cpusets_enabled()) { *alloc_mask |= __GFP_HARDWALL; /* 第一次进来走这里,此时还没初始化,这里让其指向 current->mems_allowed */ if (!ac->nodemask) ac->nodemask = &cpuset_current_mems_allowed; else *alloc_flags |= ALLOC_CPUSET; } /* * 当配置了 CONFIG_DEBUG_ATOMIC_SLEEP 选项时,如果在原子上下文(如spinlock、irq-handler等)中调 * 用 might_sleep(),它会打印出堆栈的回溯信息,帮助开发者定位问题。 * 如果没有开启调试选项,might_sleep() 函数实际上什么也不做,仅仅作为一个注解存在,提醒开发者注意 * 此函数可能会休眠。 * 这里从宏值上看可能会休眠的有:GFP_KERNEL、GFP_KERNEL_ACCOUNT、GFP_NOIO、GFP_NOFS、GFP_USER、 * GFP_HIGHUSER、GFP_HIGHUSER_MOVABLE、GFP_TRANSHUGE. * 不会休眠的有:GFP_ATOMIC、GFP_NOWAIT、GFP_DMA、GFP_DMA32、GFP_TRANSHUGE_LIGHT. */ might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); /* 默认不使能 CONFIG_FAIL_PAGE_ALLOC 故障注入功能恒返回false, 关于此配置感兴趣详见 fault-injection.rst */ if (should_fail_alloc_page(gfp_mask, order)) return false; /* * 若使能了CMA,且分配标志支持可移动, 且分配标志上或上了__GFP_CMA, 就可以在CMA内存区中分配 * 注: GFP_KERNEL 及其旁边所有分配标志都没包含__GFP_CMA, 需要额外或上。 */ if (IS_ENABLED(CONFIG_CMA) && ac->migratetype == MIGRATE_MOVABLE && (gfp_mask & __GFP_CMA)) *alloc_flags |= ALLOC_CMA; return true; }
1.2 finalise_ac()
此函数做脏区平衡,并决定优先从哪个zone中分配物理内存。
static inline void finalise_ac(gfp_t gfp_mask, struct alloc_context *ac) //page_alloc.c { /* 脏区平衡仅在快速路径中完成。GFP_KERNEL以及其周围的宏,都没有带此标志 */ ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE); /* * 首选zone用于统计,但至关重要的是,它还用作zonelist迭代器的起点。它可能会在忽略内存策略 * 的分配中被重置。 * 但是我们嵌入式只有一个zone, 只能返回我们的Normal Zone. */ ac->preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->high_zoneidx, ac->nodemask); }
1.3 alloc_flags_nofragment
/* alloc_pages: (ac.preferred_zoneref->zone, gfp_mask) 分别是Normal Zone和用户指定的分配标志 */ static inline unsigned int alloc_flags_nofragment(struct zone *zone, gfp_t gfp_mask) { unsigned int alloc_flags = 0; /* 用户传的标志除了 GFP_DMA、GFP_DMA32、GFP_TRANSHUGE_LIGHT、GFP_TRANSHUGE 不包含外,其它都包含 */ if (gfp_mask & __GFP_KSWAPD_RECLAIM) alloc_flags |= ALLOC_KSWAPD; return alloc_flags; }
此函数用于给大部分的分配类型或上 ALLOC_KSWAPD 标志。
1.4 get_page_from_freelist()
/* * __alloc_pages_nodemask: (alloc_mask, order, alloc_flags, &ac) * alloc_mask 就是增删后的 __GFP_ 开头的 gfp_mask, alloc_flags 则是 ALLOC_CPUSET, ALLOC_CMA, ALLOC_KSWAPD 这些标 * 志的位或。 */ static struct page * get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) { struct zoneref *z; struct zone *zone; struct pglist_data *last_pgdat_dirty_limit = NULL; bool no_fallback; retry: /* * 翻译: 扫描区域列表,寻找具有足够空闲空间的区域。另请参阅 kernel/cpuset.c 中的 __cpuset_node_allowed() * 注释。 * alloc_flags_nofragment() 中只有使能了 CONFIG_ZONE_DMA32 才会或上此标志位, 默认不使能, 这里恒为0. */ no_fallback = alloc_flags & ALLOC_NOFRAGMENT; /* 首先的zone, 若是有多个zone则zone_idx遍历顺序从高到低,我们只有一个Normal zone */ z = ac->preferred_zoneref; /* * 从prefer的zone开始遍历,ac->zonelist=contig_page_data.node_zonelists[0] 其 _zonerefs[3] 下标从小到大, * 对应的 zone_idx 从大到小(1-->0, 最后一个是NULL),进行遍历,只要有一个zone分配成功就直接退出了。 * 我们只有一个zone就忽略这个遍历吧。 */ for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) { struct page *page; unsigned long mark; /* 第一次分配,从快速路径进来: prepare_alloc_pages() 中并没有给或上 ALLOC_CPUSET 标志,恒不成立 */ if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) && !__cpuset_zone_allowed(zone, gfp_mask)) continue; /* * 快速路径: 几乎所有的分配标志都不会使它为真。 * 慢速路径:进入慢速路径前先直接将其设置为false了 * 所以这里可以先忽略脏页平衡。 */ if (ac->spread_dirty_pages) { /* zone_init_internals() 中将 zone->zone_pgdat = &contig_page_data */ if (last_pgdat_dirty_limit == zone->zone_pgdat) continue; /* 脏页回写,以后再看 */ if (!node_dirty_ok(zone->zone_pgdat)) { last_pgdat_dirty_limit = zone->zone_pgdat; continue; } } /* no_fallback 是恒为0的,不执行,这里可以先忽略 */ if (no_fallback && nr_online_nodes > 1 && zone != ac->preferred_zoneref->zone) { int local_nid; /* 如果移动到远程节点,则重试但允许碎片回退。局部性比避免碎片更重要。*/ local_nid = zone_to_nid(ac->preferred_zoneref->zone); if (zone_to_nid(zone) != local_nid) { alloc_flags &= ~ALLOC_NOFRAGMENT; goto retry; } } /* * 执行: mark = (zone->_watermark[alloc_flags & ALLOC_WMARK_MASK] + zone->watermark_boost) * 内存分配快速路径: 在 __alloc_pages_nodemask() 中初始化为 ALLOC_WMARK_LOW, 等效于 low_wmark_pages()。 */ mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK); /* * 翻译: 允许 high、atomic、harder 的0阶分配请求在最小水位检查时跳过 ->watermark_boost。这样做,请检查: * 1) ALLOC_WMARK_MIN - 允许在慢速路径中唤醒 kswapd。 * 2) ALLOC_HIGH - 允许高优先级请求。 * 3) ALLOC_HARDER - 允许 (__GFP_ATOMIC && !__GFP_NOMEMALLOC),以及其他。 * * 快速路径中 alloc_flags 只包含 ALLOC_WMARK_LOW, 因此恒不成立,不会执行。#### * 这里水位调低了,应该是可以让更多内存走快速路径被分配走。 * 慢速路径中不考虑水位的第二个判断可能成立,水位直接传 ALLOC_NO_WATERMARKS */ if (unlikely(!order && !(alloc_flags & ALLOC_WMARK_MASK) && (alloc_flags & (ALLOC_HARDER | ALLOC_HIGH)))) { mark = zone->_watermark[WMARK_MIN]; } /* * 此函数用于判断是否满足进入快速路径进行分配的条件: (1) 满足水位要求; (2) 有满足order请求的大块内存 * arg4返回 ac->preferred_zoneref->zone_idx。 */ if (!zone_watermark_fast(zone, order, mark, ac_classzone_idx(ac), alloc_flags, gfp_mask)) { int ret; /* 此处可保持快速路径更快,编译时生效断言 */ BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); //4 3 /* * 有这个标志就表示分配物理页时忽略水位,快速路径中不包含,慢速路径中可能包含,即使不满足 * 水位或大块内存要求也强行进行分配。 */ if (alloc_flags & ALLOC_NO_WATERMARKS) goto try_this_zone; /* * 不使能 CONFIG_NUMA 的话前者等于0恒成立,恒continue走了,下面不会执行到了, 也就是说Arm64 * 嵌入式设备不会进行快速内存回收动作。 */ if (node_reclaim_mode == 0 || !zone_allows_reclaim(ac->preferred_zoneref->zone, zone)) continue; ... } } try_this_zone: /* 快速路径分配内存 */ page = rmqueue(ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype); if (page) { prep_new_page(page, order, gfp_mask, alloc_flags); /* 如果这是高阶原子分配,则检查是否应为将来保留该页块 */ if (unlikely(order && (alloc_flags & ALLOC_HARDER))) reserve_highatomic_pageblock(page, zone, order); return page; } } //这里是遍历zone结束 return NULL; }
1.4.1 zone_watermark_fast()
/* * get_page_from_freelist: (zone, order, mark, ac_classzone_idx(ac), alloc_flags, gfp_mask) * alloc_flags 中有指定水位,mark表示水位的值。 */ static inline bool zone_watermark_fast(struct zone *z, unsigned int order, unsigned long mark, int classzone_idx, unsigned int alloc_flags, gfp_t gfp_mask) //page_alloc.c { /* 本zone的空闲页面个数 */ long free_pages = zone_page_state(z, NR_FREE_PAGES); //zone->vm_stat[NR_FREE_PAGES] long cma_pages = 0; #ifdef CONFIG_CMA //默认使能 /* * 如果用户分配时不允许使用 CMA 区域,则不要使用空闲的 CMA 页面。 * 此值有打印,即 cat /proc/zoneinfo 中的 nr_free_cma */ if (!(alloc_flags & ALLOC_CMA)) cma_pages = zone_page_state(z, NR_FREE_CMA_PAGES); //zone->vm_stat[NR_FREE_CMA_PAGES] #endif /* * 翻译: 仅针对 0 阶分配(分配单个页面)进行快速检查。如果失败,则需要考虑预留内存。有一种极端情况,即检 * 查通过但只有高阶原子预留是空闲的。如果调用者是 !atomic 的,那么它将毫无意义地搜索空闲列表。这种极端 * 情况会比较慢,但无害。 * * 这是针对0阶分配的快速检查。若不允许分配CMA内存还要从空闲页中剔除cma page,然后和水位+预留进行相比。 * 这说明CMA内存也是算在free_pages里面的。#### * 对于0阶分配,这是空闲内存是否充足的判断条件。#### * * cat /proc/zoneinfo 中的 protection: (0, 0) //zone->lowmem_reserve[0], zone->lowmem_reserve[1] */ if (!order && (free_pages - cma_pages) > mark + z->lowmem_reserve[classzone_idx]) return true; /* ---- 下面就是非0阶分配或空闲页面数量低于测试水位的情况了 ---- */ /* 如果空闲页高于'mark',则返回 true。这里面会根据 alloc_flag 去调整 mark 的值 */ if (__zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags, free_pages)) return true; /* * 翻译:检查min水位时,忽略 GFP_ATOMIC order-0 分配的水位boost。min水位是忽略boost的点, * 因此当低于low水位时,kswapd 会被唤醒。 * * 分配最初传入的是 ALLOC_WMARK_LOW, 不会成立。 * * 对于0阶原子分配,且有水位 boost,且分配标志是 WMARK_MIN,将水位调整到 min 水位,然后重新测试 */ if (unlikely(!order && (gfp_mask & __GFP_ATOMIC) && z->watermark_boost && ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) { mark = z->_watermark[WMARK_MIN]; return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags, free_pages); } return false; }
1.4.1.1 __zone_watermark_ok()
/* alloc_flags 最初来自 gfp_flags, 在分配路径中会变化 */ bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, int classzone_idx, unsigned int alloc_flags, long free_pages) { /* 对于快速路径,这里的mark是low水线的值,慢速路径可能不是 */ long min = mark; int o; /* 若有这两个标志,则表示需要尽力分配。见下文,这两个标志只在慢速路径中可能存在,快速路径中不存在 */ const bool alloc_harder = (alloc_flags & (ALLOC_HARDER|ALLOC_OOM)); /* 检查分配出去2^order个page之后的free pages是否满足水位mark的要求(free_pages 可能会变成负数 - 没关系) */ free_pages -= (1 << order) - 1; //TODO: 为啥要多个减1呀? /* * ALLOC_HIGH(0x20)整个内核中没有直接赋值它,但是会通过 __GFP_HIGH(0x20) 间接包含,包含后者的分配标志 * 只有 GFP_ATOMIC. 针对高优先级的分配,则击穿min水位,将min水线值降低一半. */ if (alloc_flags & ALLOC_HIGH) min -= min / 2; /* * 翻译: 如果调用者没有 ALLOC_HARDER 权限,则减去高原子预留。这将高估原子预留的大小,但可以避免一次搜索。 * * 若此次分配请求没有 alloc_harder 权限,则空闲页的数量还要减去高端原子预留; * 若有 alloc_harder 权限,则继续往下击穿水位: * ALLOC_HARDER: min是原来的3/8 * ALLOC_OOM: min是原来的2/8 */ if (likely(!alloc_harder)) { /* z->nr_reserved_highatomic 是内存分配过程中动态赋值的, 且没有任何文件节点导出这个成员的值 */ free_pages -= z->nr_reserved_highatomic; } else { /* * 翻译: OOM 受害者可以比普通的 ALLOC_HARDER 用户更加努力,因为它肯定会很快进入退出路径并释放内存。 * 它在释放路径期间进行的任何分配都将很小且短暂。 */ if (alloc_flags & ALLOC_OOM) min -= min / 2; //在已降低的基础上,再降低1/2 else min -= min / 4; //在已降低的基础上,再降低1/4 } #ifdef CONFIG_CMA /* 若此次分配不能使用CMA内存,还要从空闲页面中减去空闲的CMA页面 */ if (!(alloc_flags & ALLOC_CMA)) free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES); #endif /* * 翻译: 检查 0 阶分配请求的水位。如果不满足这些条件,那么高阶请求也无法继续,即使恰好有合适的页面可用。 * * 如果空闲页面free pages已经小于等于预留内存和限制水位(low或min)之和了,说明此次分配请求不满足wartmark * 要求,返回false */ if (free_pages <= min + z->lowmem_reserve[classzone_idx]) return false; /* 若没有触及水位限制,且是0阶分配,直接返回true, 表示满足分配请求。#### */ if (!order) return true; /* * 翻译: 对于高阶请求,从其order开始,检查至少一个合适的页面块是可用的. * * 对于高阶分配请求,虽然上面水位检查已经过了,但是不一定有大块内存供其使用,还需要继续判断一下。 */ for (o = order; o < MAX_ORDER; o++) { struct free_area *area = &z->free_area[o]; int mt; /* 若此order的空闲列表上没有页面了,则继续下一轮循环,去查看更高阶的空闲链表 */ if (!area->nr_free) continue; /* 此阶上有空闲内存块,在 UNMOVABLE, MOVABLE, RECLAIMABLE 链表上只要有一个链表不为空,就返回true */ for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { #ifdef CONFIG_CMA /* 这里主要是想先判断前三个迁移类型,下面延后判断CMA类型 */ if (mt == MIGRATE_CMA) continue; #endif /* 只是判断 !list_empty(&area->free_list[mt]) */ if (!free_area_empty(area, mt)) return true; } #ifdef CONFIG_CMA /* 如果此阶上只有CMA类型的空闲块,且用户指定了可分配CMA类型的内存,也返回true */ if ((alloc_flags & ALLOC_CMA) && !free_area_empty(area, MIGRATE_CMA)) { return true; } #endif /* * 若是harder分配,且高优先级原子分配的 HIGHATOMIC 内存链表不为空,也返回true。 * 但是cat /proc/pagetypeinfo 看高阶原子的都是0 */ if (alloc_harder && !list_empty(&area->free_list[MIGRATE_HIGHATOMIC])) return true; } /* 虽然水位满足,但在当前zone的free_area[]中没有找到可以满足当前order分配的内存块,返回false */ return false; }
此函数作用:判断分配是否满足水位和连续内存块的要求。若用户使用特权进行分配(通过 gfp_flags),或这是特权任务的分配(通过 current->flags), 则可击穿 min水位,使用 min 水位以下的内存。
针对 alloc_harder, 预留高阶原子内存,可能可以满足某些特殊场景的分配需求。#### 在 gfp_to_alloc_flags() 中对于 gfp_mask 包含 __GFP_ATOMIC 且不包含 __GFP_NOMEMALLOC 的原子分配,和非运行在硬中断中的RT任务的非原子分配,都会包含 ALLOC_HARDER 标记。对于RT任务,内存分配上也有倾向。####
1.4.2 rmqueue()
/* * 翻译: 从给定区域分配一个页面, 使用 pcplists 进行 order-0 分配。 * * get_page_from_freelist: (ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype) * 其中: ac->migratetype 是 prepare_alloc_pages()中根据参数 gfp_mask 来决定是分配哪种迁移类型, 之后整个分配流 * 程中都不在变动. */ static inline struct page *rmqueue(struct zone *preferred_zone, struct zone *zone, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags, int migratetype) { unsigned long flags; struct page *page; /* * 针对0阶分配,走到的PCP机制。PCP即per-cpu-page, 它是一个per-cpu变量,该变量中有一个单页面的链表, * 当系统需要单个物理页面时,直接从这个链表中分配物理页面,可以提高效率。分配单个页面有快速路径。#### * cat /proc/zoneinfo 可以看到pcp页面的情况。#### */ if (likely(order == 0)) { page = rmqueue_pcplist(preferred_zone, zone, gfp_flags, migratetype, alloc_flags); goto out; } /* * 翻译: 绝对不希望调用者尝试使用 __GFP_NOFAIL 分配大于 order-1 的页面。 * 即 NOFAIL 标志一次最大只能分配2个page #### */ WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1)); spin_lock_irqsave(&zone->lock, flags); //持 zone->lock 锁并关中断 #### do { page = NULL; /* * ALLOC_HARDER 表示尽力分配,一般在 gfp_mask 设置了 __GFP_ATOMIC 或RT任务分配内存时会使用。如果页面 * 分配失败,则尽可能分配 MIGRATE_HIGHATOMIC 类型的空闲页面。 * 若包含 ALLOC_HARDER 则优先级最高,优先从 MIGRATE_HIGHATOMIC 类型中分配。但是从 /proc/pagetypeinfo 中 * 看 HighAtomic 是没有内存的,这里必失败返回。#### */ if (alloc_flags & ALLOC_HARDER) { page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC); if (page) trace_mm_page_alloc_zone_locked(page, order, migratetype); } /* 若还没执行分配或上面分配失败了,若是可迁移类型且用户表示可使用CMA内存,优先从CMA内存中分配 #### */ if (!page && migratetype == MIGRATE_MOVABLE && gfp_flags & __GFP_CMA) page = __rmqueue_cma(zone, order, migratetype, alloc_flags); /* 若上面两个都没执行到或没分配到page, 则走这个默认路径 */ if (!page) page = __rmqueue(zone, order, migratetype, alloc_flags); /* * 如果没有分配到page退出、或分配到page且检查通过返回false退出,若分配到page但检查失败就继续尝试 #### * 检查机制见下一篇BK. */ } while (page && check_new_pages(page, order)); spin_unlock(&zone->lock); //这里就释放锁了,但是并没有开中断 if (!page) goto failed; /* 统计计数,在 /proc/vmstat 和 /proc/zoneinfo 中有进行打印,见 "nr_free_pages" 和 "nr_free_cma" */ __mod_zone_freepage_state(zone, -(1 << order), get_pcppage_migratetype(page)); /* 统计计数,在 /proc/vmstat 中有进行打印,见 "pgalloc_normal" */ __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); zone_statistics(preferred_zone, zone); //只有使能 CONFIG_NUMA 才有值,忽略不计 local_irq_restore(flags); //这里才开中断 #### out: /* 这里主要是优化内存外碎片。如果 &zone->flags 设置了 ZONE_BOOSTED_WATERMARK 标志位, * 就会唤醒 kswapd 线程回收内存。ZONE_BOOSTED_WATERMARK 在 fallback 流程里会被设置, * 说明此时页面分配器已经向备份空闲链表借用内存,有内存外碎片的可能。 */ /* Separate test+clear to avoid unnecessary atomics */ if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) { clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags); /* 唤醒kswapd线程 #### */ wakeup_kswapd(zone, 0, 0, zone_idx(zone)); } /* 默认不使能 CONFIG_DEBUG_VM, 恒不成立 */ VM_BUG_ON_PAGE(page && bad_range(zone, page), page); return page; failed: local_irq_restore(flags); return NULL; }
1.4.2.1 rmqueue_pcplist()
之后单独分析PCP机制。
1.4.2.2 __rmqueue_smallest()
/* * 作用: 浏览给定迁移类型的空闲列表,并从空闲列表中删除最小的可用页面块。 * rmqueue: (zone, order, MIGRATE_HIGHATOMIC) //alloc_flags 中包含 ALLOC_HARDER 的分配 * __rmqueue_cma_fallback: (zone, order, MIGRATE_CMA) //gfp_flags 中包含 __GFP_CMA 的分配 * */ 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; /* * 从参数 order 开始查找zone的指定迁移类型的空闲链表。如果当前的order中没有空闲对象, * 那么就会查找上一级order */ for (current_order = order; current_order < MAX_ORDER; ++current_order) { /* 此 order 对应的 free_area */ area = &(zone->free_area[current_order]); /* * 从此 free_area 的指定迁移类型的链表头部获取物理页. add_to_free_area()中通过 * page->lru 成员将其挂到 area->free_list[migratetype] */ page = get_page_from_free_area(area, migratetype); if (!page) continue; /* * 将 page 从 free_area 链表上摘下来,并清除 PG_buddy 标志位(清除是 page->page_type |= * PG_buddy,当从伙伴系统中分出去了要清除Buddy标志####). * 然后将 page->private=0,此order的页面块的个数减去1。 * 这里只是将空闲的对象摘出链表,真正分配的功能在下面expand()函数中实现。 */ del_page_from_free_area(page, area); /* 从摘取下来的页块中取出2^order大小,多出的部分头插法重新插入到空闲链表中 */ expand(zone, page, order, current_order, area, migratetype); /* 对于分出来的物理面块的首页执行 page->index = migratetype, 记录自己的迁移类型 */ set_pcppage_migratetype(page, migratetype); return page; } return NULL; }
1.4.2.2.1 expand()
/* __rmqueue_smallest: (zone, page, order, current_order, area, migratetype) * 其中 page 是从空闲链表上摘取的页面块的首页; order 是用户要分配的大小; current_order 是当前(更大的) * 阶数对于的页面块; area 与 current_order 对应 */ static inline void expand(struct zone *zone, struct page *page, int low, int high, struct free_area *area, int migratetype) { unsigned long size = 1 << high; /* * low是需要分配的order大小,high是实际分出来的order大小,若相等则不用拆分了,否则要拆分。 * 拆分分出来给用户的是最"左侧"的那个2^order大小的内存块。 */ while (high > low) { /* zone->free_area[MAX_ORDER] 是个数组,减去1是指向上一阶的 zone->free_area 结构 */ area--; /* 实际分配出来的order减1,也就是分成2份 */ high--; /* size大小也调为原来的1/2 */ size >>= 1; /* 默认不使能 CONFIG_DEBUG_VM, cond恒位0, 恒不会触发BUG() */ VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]); /* 若不使能 CONFIG_DEBUG_PAGEALLOC 恒返回false, 即使使能,默认配置下啥也不执行,直接返回false了 */ if (set_page_guard(zone, &page[size], high, migratetype)) continue; /* 分配出来的page其实是个数组的首地址####,头插法将多出来的挂回空闲链表上 #### */ add_to_free_area(&page[size], area, migratetype); /* * 设置挂回空闲链表的页块的 page->private=order 和 Buddy 标志(放回伙伴系统中要设置Buddy标志), #### * 即在空闲列表中页块的首页的private成员保存的才是其order值,TODO: 分配出来后有清空吗?########### */ set_page_order(&page[size], high); } }
1.4.2.3 __rmqueue_cma()
/* rmqueue: (zone, order, MIGRATE_MOVABLE, alloc_flags) */ static struct page *__rmqueue_cma(struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags) { struct page *page = 0; if (IS_ENABLED(CONFIG_CMA)) /* * alloc_contig_range()中分配过程中设置为1分配完后设置为0,为0表示此zone上没有正在执行的CMA分配流程。 * 若是有正在执行的分配流程,则直接返回NULL */ if (!zone->cma_alloc) page = __rmqueue_cma_fallback(zone, order); trace_mm_page_alloc_zone_locked(page, order, MIGRATE_CMA); return page; }
1.4.2.1 __rmqueue_cma_fallback()
static __always_inline struct page *__rmqueue_cma_fallback(struct zone *zone, unsigned int order) { return __rmqueue_smallest(zone, order, MIGRATE_CMA); }
1.4.2.4 __rmqueue()
/* 完成从伙伴分配器中删除元素的艰苦工作。在是有 zone->lock 的情况下调用 */ 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); /* 若是没有分配到,执行fallback,若fallback到了内存则返回true,然后再尝试一次重新分配 */ if (unlikely(!page) && __rmqueue_fallback(zone, order, migratetype, alloc_flags)) goto retry; trace_mm_page_alloc_zone_locked(page, order, migratetype); return page; }
1.4.2.4.1 __rmqueue_fallback()
/* __rmqueue: (zone, order, migratetype, alloc_flags) 其中 migratetype 变成 start_migratetype 了 */ static __always_inline bool __rmqueue_fallback(struct zone *zone, int order, int start_migratetype, unsigned int alloc_flags) { struct free_area *area; int current_order; int min_order = order; struct page *page; int fallback_mt; bool can_steal; /* * 翻译: 请勿从属于其他页面块(即 order < pageblock_order)的空闲列表中窃取页面。如果没有本地可用区域, * 则将重复区域列表,而无需 ALLOC_NOFRAGMENT。 */ /* 默认不使能 CONFIG_ZONE_DMA32, 它是0, 恒不执行 */ if (alloc_flags & ALLOC_NOFRAGMENT) min_order = pageblock_order; /* * 翻译: 在另一个列表中查找最大的可用页面。这大致相当于找到包含最多可用页面的页面块,但精确查找成本太高。 * * 从最大order开始向下查找, 尝试从其它迁移类型的内存中移动过来一个内存块。 */ for (current_order = MAX_ORDER - 1; current_order >= min_order; --current_order) { area = &(zone->free_area[current_order]); /* 返回-1表示没有找到可用的候选迁移类型的内存 */ fallback_mt = find_suitable_fallback(area, current_order, start_migratetype, false, &can_steal); if (fallback_mt == -1) continue; /* * 翻译: 如果我们无法从页块中窃取走所有的空闲页面,并且请求的迁移类型是可移动的。在这种情况下, * 最好窃取并拆分最小的可用页面,而不是最大的可用页面,因为即使下一个可移动分配落入到与当前页 * 面不同的页块,也不会造成永久性碎片。 * * 若 start_migratetype == MIGRATE_MOVABLE 条件下要满足 can_steal 为假的条件只有: * 窃取的不是一个完整的页块,并且是小于1/2页块大小的。这里还一口吃不下。 */ if (!can_steal && start_migratetype == MIGRATE_MOVABLE && current_order > order) goto find_smallest; goto do_steal; } return false; find_smallest: /* 满足上面条件且一口吃不下的话,又重新order由小到大进行遍历,找到一块满足大小的后备迁移类型的内存块即可 */ for (current_order = order; current_order < MAX_ORDER; current_order++) { area = &(zone->free_area[current_order]); fallback_mt = find_suitable_fallback(area, current_order, start_migratetype, false, &can_steal); if (fallback_mt != -1) break; } /* 这不应该发生——我们在寻找最大的页面时已经找到了合适的后备迁移类型的空闲内存 */ VM_BUG_ON(current_order == MAX_ORDER); do_steal: /* 将选定的后备内存从 current_order 对应的 area 的 area->free_list[fallback_mt] 中摘取下来 */ page = get_page_from_free_area(area, fallback_mt); steal_suitable_fallback(zone, page, alloc_flags, start_migratetype, can_steal); trace_mm_page_alloc_extfrag(page, order, current_order, start_migratetype, fallback_mt); return true; }
1.4.2.4.1.1 find_suitable_fallback()
/* * __rmqueue_fallback: (area, current_order, start_migratetype, false, &can_steal) * order从最大往下遍历, 每个order都调用一次这个函数 #### */ int find_suitable_fallback(struct free_area *area, unsigned int order, int migratetype, bool only_stealable, bool *can_steal) { int i; int fallback_mt; /* 此order没有空闲页,则直接退出 */ if (area->nr_free == 0) return -1; /* 首先初始化为不可偷其它迁移类型的 */ *can_steal = false; /* 遍历参数 migratetype 支持的所有fallback迁移类型 */ for (i = 0;; i++) { fallback_mt = fallbacks[migratetype][i]; //TODO: 贴上这个列表 /* 可fallback的列表遍历完了,退出 */ if (fallback_mt == MIGRATE_TYPES) break; /* 此order的此种迁移类型对应的空闲链表是空的,则继续遍历其它可fallback类型的空闲链表 */ if (free_area_empty(area, fallback_mt)) continue; /* 基于尽量减少内存碎片化来判断能否窃取这种类型,参数只有order和原迁移类型 */ if (can_steal_fallback(order, migratetype)) *can_steal = true; /* 这个传false的话,无论是否可以steal,只要遍历的迁移类型有内存,都返回可steal */ if (!only_stealable) return fallback_mt; /* only_stealable 传true的话,可以steal才返回可以steal */ if (*can_steal) return fallback_mt; } return -1; }
检查是否存在符合请求order的合适备用空闲页。如果 only_stealable 为真,则此函数仅在我们能够窃取所有空闲页的情况下才返回 fallback_mt。这将有助于减少由于同一页块中混合使用迁移类型页面而造成的碎片。
fallbacks[] 数组描述了当所需迁移类型的空闲列表耗尽时,fallback 查找的顺序。以 MIGRATE_TYPES 作为结尾。定义如下:
static int fallbacks[MIGRATE_TYPES][4] = { //page_alloc.c [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ };
1.4.2.4.1.1.1 can_steal_fallback()
static bool can_steal_fallback(unsigned int order, int start_mt) { /* * 翻译: 虽然下次检查会放宽顺序检查,但保留此顺序检查是有意为之。原因是,如果满足此条件, * 我们实际上可以窃取整个页块,但下面的检查并不能保证这一点,而且这只是启发式的,因此随时可能更改。 * * 若order对应的是一个完整的页面块(4M),则直接返回true。这有利于防治内存碎片化 */ if (order >= pageblock_order) return true; /* * build_all_zonelists()中判断总页面数小于6K个的时候,page_group_by_mobility_disabled 才为真, * 此时所有页面都是不可迁移的。这里恒为0 */ if (order >= pageblock_order / 2 || start_mt == MIGRATE_RECLAIMABLE || start_mt == MIGRATE_UNMOVABLE || page_group_by_mobility_disabled) return true; return false; }
翻译: 当我们在分配过程中回退到其他迁移类型时,会尝试从相同的页面块中窃取额外的空闲页面来满足后续分配,而不是污染多个页面块。
如果我们窃取的是相对较大的伙伴页面,则该页面块中很可能还有更多空闲页面,因此请尝试将它们全部窃取。对于可回收(reclaimable)和不可移动(unmovable)的分配,无论页面大小如何,我们都会进行窃取,因为这些分配污染可移动页面块所造成的碎片比从unmovable和reclaimable的页面块中窃取可移动页面块所造成的碎片更严重。
1.4.2.4.1.2 steal_suitable_fallback()
/* __rmqueue_fallback: (zone, page, alloc_flags, start_migratetype, can_steal) */ static void steal_suitable_fallback(struct zone *zone, struct page *page, unsigned int alloc_flags, int start_type, bool whole_block) { unsigned int current_order = page_order(page); //page->private struct free_area *area; int free_pages, movable_pages, alike_pages; int old_block_type; /* 获取选出来的待迁移page对应的页面块(4M)的迁移类型 */ old_block_type = get_pageblock_migratetype(page); /* 翻译: 由于竞争这是可能发生的,我们希望阻highatomic计数被破坏。*/ if (is_migrate_highatomic(old_block_type)) //return migratetype == MIGRATE_HIGHATOMIC; goto single_page; /* 若是一个完整的页面块,直接获取其所有权. 就是直接改整个页面块的迁移类型 */ if (current_order >= pageblock_order) { /* 调用 set_pageblock_migratetype(page, start_type) 直接设置页面块的迁移类型 */ change_pageblock_range(page, current_order, start_type); goto single_page; } /* * 翻译: 提升水位线以增加回收压力,从而降低未来发生回退的可能性。现在唤醒 kswapd,因为 * node可能整体处于平衡状态,kswapd 还没有被唤醒。 * * rmqueue()中清除此标志位并唤醒kswapd() */ if (boost_watermark(zone) && (alloc_flags & ALLOC_KSWAPD)) set_bit(ZONE_BOOSTED_WATERMARK, &zone->flags); /* 传参是 can_steal, 我们不被允许从整个页面块窃取 */ if (!whole_block) goto single_page; /* 迁移页面,返回迁移页面的数量 */ free_pages = move_freepages_block(zone, page, start_type, &movable_pages); /* * 翻译: 确定有多少页面与我们的分配兼容。对于可移动分配,它是我们刚刚获得的可移动页面的数量。 * 对于其他类型的分配,这有点棘手。 */ if (start_type == MIGRATE_MOVABLE) { alike_pages = movable_pages; } else { /* * 翻译: 如果我们将 RECLAIMABLE 或 UNMOVABLE 分配回退到 MOVABLE 页块,则应将所有不可移动页面视为兼容。 * 如果 UNMOVABLE 分配回退到 RECLAIMABLE 或反之,则应谨慎处理,因为我们无法区分不可移动页面的具体 * 迁移类型。 */ if (old_block_type == MIGRATE_MOVABLE) alike_pages = pageblock_nr_pages - (free_pages + movable_pages); else alike_pages = 0; } /* 翻译: 由于区域边界条件,移动整个块可能会失败 */ if (!free_pages) goto single_page; /* * 翻译: 如果块中有足够数量的页面是空闲的,或者具有与我们的分配相当的可迁移性,则回收整个块。 * * 将整个页块的迁移类型都设置为缺少内存的那个迁移类型。 */ if (free_pages + alike_pages >= (1 << (pageblock_order-1)) || page_group_by_mobility_disabled) set_pageblock_migratetype(page, start_type); return; single_page: /* 将选出的这块内存移动到缺内存的迁移类型对应的空闲链表上 */ area = &zone->free_area[current_order]; move_to_free_area(page, area, start_type); //list_move(&page->lru, &area->free_list[start_type]); }
翻译: 此函数实现了实际的窃取行为。如果 order 足够大,我们可以窃取整个页面块。如果 order 不够大,我们首先将此页面块中的空闲页面移动到我们的迁移类型 (migratetype),并确定该页面块中有多少个已分配的页面与迁移类型兼容。如果至少有一半页面空闲或兼容,我们可以更改页面块本身的迁移类型(migratetype),以便将来释放的页面能够被放入正确的空闲列表中。
1.4.2.4.1.2.1 boost_watermark()
static inline bool boost_watermark(struct zone *zone) { unsigned long max_boost; /* * 首先要满足用户配置了boost, 且有boost资格(剩余内存还较多)才可能返回true. * 通过 /proc/sys/vm/watermark_boost_factor 可以修改此全局变量的值,默认是15000; */ if (!watermark_boost_factor || !boost_eligible(zone)) return false; /* * 翻译: 不要在不太可能产生结果的区域内进行操作。在小型机器上,包括在小区域内运行 * 的 kdump 捕获内核,提高水位线可能会立即导致内存溢出。 */ if ((pageblock_nr_pages * 4) > zone_managed_pages(zone)) //zone->managed_pages return false; /* zone->_watermark[WMARK_HIGH] * watermark_boost_factor/10000 */ max_boost = mult_frac(zone->_watermark[WMARK_HIGH], watermark_boost_factor, 10000); /* * 翻译: * 如果碎片在启动初期就出现,高水位线可能尚未初始化,因此请勿提升。我们不会跳过 pageblock_nr_pages * 并提升水位线,因为过早分配失败意味着回收无济于事,甚至可能无法回收提升的水位线,从而导致系统挂起。 */ if (!max_boost) return false; /* 至少是1024了 */ max_boost = max(pageblock_nr_pages, max_boost); /* * 这里又取最小值,将 zone->watermark_boost 限定在: * [pageblock_nr_pages, zone->watermark_boost + pageblock_nr_pages] 之间 */ zone->watermark_boost = min(zone->watermark_boost + pageblock_nr_pages, max_boost); return true; }
这个boost是同时作用在min low high 上的。
1.4.2.4.1.2.1.1 boost_eligible()
static bool boost_eligible(struct zone *z) { unsigned long high_wmark, threshold; unsigned long reclaim_eligible, free_pages; high_wmark = z->_watermark[WMARK_HIGH]; /* * zone->vm_stat[NR_ZONE_INACTIVE_FILE] + zone->pageset[cpu]->vm_stat_diff[NR_ZONE_INACTIVE_FILE] + * zone->vm_stat[NR_ZONE_ACTIVE_FILE] + zone->pageset[cpu]->vm_stat_diff[NR_ZONE_ACTIVE_FILE] * 即所有文件页的数量之和。 */ reclaim_eligible = zone_page_state_snapshot(z, NR_ZONE_INACTIVE_FILE) + zone_page_state_snapshot(z, NR_ZONE_ACTIVE_FILE); /* zone->vm_stat[NR_FREE_PAGES] - zone->vm_stat[NR_FREE_CMA_PAGES] 即空闲页的数量 */ free_pages = zone_page_state(z, NR_FREE_PAGES) - zone_page_state(z, NR_FREE_CMA_PAGES); /* high_wmark + 2 * (high_wmark * watermark_boost_factor / 10000) 默认等于4倍的 high_wmark */ threshold = high_wmark + (2 * mult_frac(high_wmark, watermark_boost_factor, 10000)); /* * 翻译: 如果内存已经不足,则不要提升水位线,因为提升水位线只会让水位线在更长时间内处于更高的水平, * 从而迫使依赖水位线的其他用户做出意想不到的决定。如果内存如此低迷,正常模式下的 kswapd 应该会有所帮助。 * * 所有CPU的每CPU文件页数量之和 */ if (reclaim_eligible < threshold && free_pages < threshold) return false; return true; }
需要要有较多的空闲内核,对水线的boost才会生效。
1.4.2.4.1.2.2 move_freepages_block()
/* steal_suitable_fallback: (zone, page, start_type, &movable_pages) */ int move_freepages_block(struct zone *zone, struct page *page, int migratetype, int *num_movable) { unsigned long start_pfn, end_pfn; struct page *start_page, *end_page; /* 先对传出参数进行清0 */ if (num_movable) *num_movable = 0; /* page所在页面块的start/end pfn和页面块首page结构指针,将page规范到一个页块内 */ start_pfn = page_to_pfn(page); start_pfn = start_pfn & ~(pageblock_nr_pages-1); start_page = pfn_to_page(start_pfn); end_page = start_page + pageblock_nr_pages - 1; end_pfn = start_pfn + pageblock_nr_pages - 1; /* * 翻译: 不要跨越zone的边界 * * 若start_pfn不在zone管理的页面之间,则将start_page指向page(而不是页面块的首个page了), * 特别小的zone或zone前面的一些页面可能会出现。 */ if (!zone_spans_pfn(zone, start_pfn)) start_page = page; /* 若end_pfn不在zone管理的页面之间, 则直接返回。 */ if (!zone_spans_pfn(zone, end_pfn)) return 0; /* 将页面块迁移到迁移类型对应的freelist链表上,返回迁移的页面数 */ return move_freepages(zone, start_page, end_page, migratetype, num_movable); }
1.4.2.5 __mod_zone_freepage_state()
/* set_page_guard:(zone, -(1 << order), migratetype) */ static inline void __mod_zone_freepage_state(struct zone *zone, int nr_pages, int migratetype) { /* zone->vm_stat[NR_FREE_PAGES] - (1 << order); vm_zone_stat[NR_FREE_PAGES] - (1 << order) */ __mod_zone_page_state(zone, NR_FREE_PAGES, nr_pages); /* 如果 migratetype == MIGRATE_CMA,还要对 NR_FREE_CMA_PAGES 类型再减去一遍 */ if (is_migrate_cma(migratetype)) __mod_zone_page_state(zone, NR_FREE_CMA_PAGES, nr_pages); }
页面分配出去之后在这个函数中更新空闲页的统计计数,说明 NR_FREE_PAGES 对 NR_FREE_CMA_PAGES 是一种包含关系,前者包含后者。
vm_zone_stat[] 在 cat /proc/vmstat 中进行打印,见 "nr_free_pages" 和 "nr_free_cma" 两个域。
zone->vm_stat[] 在 cat /proc/zoneinfo 中进行打印,见 "nr_free_pages"/"pages free" 和 "nr_free_cma"
由于我们只有一个zone,这两个成员cat出来的值基本上是一致的。
1.4.2.6 __count_zid_vm_events()
/* rmqueue: (PGALLOC, page_zonenum(page), 1 << order) */ #define __count_zid_vm_events(item, zid, delta) \ __count_vm_events(item##_NORMAL - ZONE_NORMAL + zid, delta) //vmstat.h static inline enum zone_type page_zonenum(const struct page *page) { return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK; //63 1 } /* rmqueue: (PGALLOC_NORMAL - ZONE_NORMAL + zid, 1 << order) 其中 ZONE_NORMAL=0, zid=0 */ static inline void __count_vm_events(enum vm_event_item item, long delta) { raw_cpu_add(vm_event_states.event[item], delta); } DEFINE_PER_CPU(struct vm_event_state, vm_event_states) = {{0}}; //vmstat.c
这里应该是增加对 vm_event_states.event[PGALLOC_NORMAL] 的计数,它应该表示本CPU从Normal Zone中被分配出来的页帧数。TODO
init_mm_internals //vmstat.c 【】对应 /proc/vmstat 文件的 pgalloc_normal 域的值 vmstat_op.start //vmstat.c vmstat_start //vmstat.c all_vm_events //vmstat.c sum_vm_events //vmstat.c for_each_online_cpu(cpu) struct vm_event_state *this = &per_cpu(vm_event_states, cpu); for (i = 0; i < NR_VM_EVENT_ITEMS; i++) ret[i] += this->event[i];
1.4.3 prep_new_page()
/* 分配到物理页page之后调用 */ static void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags) { /* 设置分配出来的页的属性、owner、Poison等信息 */ post_alloc_hook(page, order, gfp_flags); /* &&前面恒返回假,&&后面返回true,一般是执行的. debug版本实测会执行 #### */ if (!free_pages_prezeroed() && want_init_on_alloc(gfp_flags)) /* 这里将所有分出来的内存清0 #### */ kernel_init_free_pages(page, 1 << order); /* 大于0阶的复合页的分配,有做特殊处理。TODO:后续看 */ if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order); /* * 翻译: 当需要 ALLOC_NO_WATERMARKS 来分配页面时,页面会被设置为 pfmemalloc。预期调用者正 * 在采取措施释放更多内存。调用者应避免将页面用于 !PFMEMALLOC 用途。 */ if (alloc_flags & ALLOC_NO_WATERMARKS) set_page_pfmemalloc(page); //page->index = -1UL; 正常情况下index是迁移类型 #### else clear_page_pfmemalloc(page); //page->index = 0; }
应该是若是使能了 CONFIG_INIT_ON_ALLOC_DEFAULT_ON 所有物理页在分配时都会被清0. 不使能的话,带 GFP_ZERO 标志的才会给自动清0。
1.4.3.1 post_alloc_hook()
inline void post_alloc_hook(struct page *page, unsigned int order, gfp_t gfp_flags) { /* 将新分配出来的页 page->private = 0, 在伙伴系统中时首页的是 order 值 */ set_page_private(page, 0); /* 将 page->_refcount 从0设置为1 */ set_page_refcounted(page); /* 默认不使能 HAVE_ARCH_ALLOC_PAGE, 是空实现 */ arch_alloc_page(page, order); /* * debug版本默认返回true, user版本默认不使能 CONFIG_DEBUG_PAGEALLOC 返回false, * 不会有这个invalid/valid设置 */ if (debug_pagealloc_enabled_static()) /* 将分配出来的页对应的虚拟地址范围的页表设置为valid */ kernel_map_pages(page, 1 << order, 1); /* 默认不使能 CONFIG_KASAN, 它是空实现,先忽略 */ kasan_alloc_pages(page, order); /* CONFIG_PAGE_POISONING debug版本默认使能user版本默认关闭,荼毒页面 */ kernel_poison_pages(page, 1 << order, 1); /* 初始化此page对应的 page_owner 结构,里面记录了分配者的pid、分配时间、order等信息。 * TODO: 下一篇BK讲解 */ set_page_owner(page, order, gfp_flags); }
分配出来的内存,先遍历所有字节检查是否有pageflip, 然后再将所有字节都清0。这样岂不是很占CPU. ####
1.4.4 reserve_highatomic_pageblock()
/* 如果没有包含具有合适order的空页面块,则保留一个页面块专门用于高阶原子分配 */ static void reserve_highatomic_pageblock(struct page *page, struct zone *zone, unsigned int alloc_order) { int mt; unsigned long max_managed, flags; /* 将保留的页面数量限制为 1 个页面块或大约此zone的 1%。检查容易发生竞争,但无害, 下面还会再检查一次 */ max_managed = (zone_managed_pages(zone) / 100) + pageblock_nr_pages; //zone->managed_pages if (zone->nr_reserved_highatomic >= max_managed) return; spin_lock_irqsave(&zone->lock, flags); //关中断持 zone->lock 锁 /* 持有zone->lock后再次检查,上面在锁外进行一次检查可以减少竞争 */ if (zone->nr_reserved_highatomic >= max_managed) goto out_unlock; /* * 获取参数page所在页面块的迁移特性,如果不是 HIGHATOMIC、ISOLATE、CMA 三种迁移类型,就将page * 所在页面块设置为高阶原子类型。 * TODO: 为啥 cat /proc/pagetypeinfo 恒为0呢? */ mt = get_pageblock_migratetype(page); if (!is_migrate_highatomic(mt) && !is_migrate_isolate(mt) && !is_migrate_cma(mt)) { /* 设置页面块的迁移类型为 HIGHATOMIC, */ zone->nr_reserved_highatomic += pageblock_nr_pages; set_pageblock_migratetype(page, MIGRATE_HIGHATOMIC); /* 将page所在的页面块迁移到 MIGRATE_HIGHATOMIC 这个迁移类型对应的freelist链表上 */ move_freepages_block(zone, page, MIGRATE_HIGHATOMIC, NULL); } out_unlock: spin_unlock_irqrestore(&zone->lock, flags); //开中断释放 zone->lock }
TODO: 只有一个zone,锁竞争会不会比较激烈?
1.5 current_gfp_context()
/* * 翻译: 将每任务的 gfp 上下文应用于给定的分配标志上。 * PF_MEMALLOC_NOIO 表示 GFP_NOIO * PF_MEMALLOC_NOFS 表示 GFP_NOFS * PF_MEMALLOC_NOCMA 表示不从 CMA 区域分配。 */ static inline gfp_t current_gfp_context(gfp_t flags) //sched/mm.h { /* 如果包含这些特殊标志,才需要处理 */ if (unlikely(current->flags & (PF_MEMALLOC_NOIO | PF_MEMALLOC_NOFS | PF_MEMALLOC_NOCMA))) { /* * NOIO implies both NOIO and NOFS and it is a weaker context * so always make sure it makes precedence * 翻译: NOIO 意味着 NOIO 和 NOFS,并且它是一个较弱的上下文,因此始终确保它优先 */ if (current->flags & PF_MEMALLOC_NOIO) flags &= ~(__GFP_IO | __GFP_FS); else if (current->flags & PF_MEMALLOC_NOFS) flags &= ~__GFP_FS; #ifdef CONFIG_CMA /* 不从CMA中分配内存,这里将 moveable 标志给去除了 #### */ if (current->flags & PF_MEMALLOC_NOCMA) flags &= ~__GFP_MOVABLE; #endif } return flags; }
若是noio,则IO和FS都要清除,若是只有nofs,则只清除FS, 若指定nocma,则清除MOVEABLE。执行这个的时候下一步就要进入内存分配慢速路径了,可能就要触发各种内存回收了,因此需要根据当前进程的gfp上下文(current->flags)将分配标志约束好。
六、调试接口
1. trace
见 https://www.cnblogs.com/hellokitty2/p/18928577
2. 调试选项
(1) _debug_guardpage_minorder
unsigned int _debug_guardpage_minorder; //page_alloc.c debug_guardpage_minorder_setup _debug_guardpage_minorder = res; pr_info("Setting debug_guardpage_minorder to %lu\n", res); early_param("debug_guardpage_minorder", debug_guardpage_minorder_setup); //有模块参数设置
使用位置:
kmem_cache_init //slub.c 若使能则将 slub_max_order = 0 init_debug_pagealloc //page_alloc.c 若此值不为0则将 _debug_guardpage_enabled=true set_page_guard //page_alloc.c 若order>=此值则直接返回,不设置guard debug_guardpage_minorder return _debug_guardpage_minorder;
相关资料:
Documentation/admin-guide/kernel-parameters.txt
debug_guardpage_minorder:
[KNL] 设置 CONFIG_DEBUG_PAGEALLOC 后,此参数允许控制伙伴分配器有意保持空闲(并因此受到保护)的页面order。较大的值会增加捕获随机内存损坏的概率,但会减少系统正常使用的内存量。最大可能值为 MAX_ORDER/2。将此参数设置为 1 或 2 应该足以识别大多数由内核或驱动程序代码中的错误导致的随机内存损坏问题,这些问题是由于 CPU 写入(或读取)随机内存位置时引起的。请注意,存在一类由硬件或固件错误或驱动程序对 DMA 编程不当(基本上是在总线级别写入内存并绕过 CPU MMU 时)引起的内存损坏问题,CONFIG_DEBUG_PAGEALLOC 无法检测到这些问题,因此此选项无助于追踪这些问题。
(2) _debug_guardpage_enabled
set_page_guard //page_alloc.c 如果为false则直接退出 clear_page_guard //page_alloc.c 如果为false则直接退出 debug_guardpage_enabled return _debug_guardpage_enabled;
和上面功能类似。
(3) _debug_pagealloc_enabled_early
bool _debug_pagealloc_enabled_early = IS_ENABLED(CONFIG_DEBUG_PAGEALLOC_ENABLE_DEFAULT); //page_alloc.c 默认使能 early_debug_pagealloc kstrtobool(buf, &_debug_pagealloc_enabled_early); early_param("debug_pagealloc", early_debug_pagealloc);
使用位置:
page_poisoning_enabled //page_poison.c 可能有助于poison的使能 init_debug_pagealloc //page_alloc.c 若没使能也不会使能 _debug_pagealloc_enabled 和 _debug_guardpage_enabled __kernel_map_pages //pageattr.c kernel_page_present //pageattr.c map_mem //mmu.c 若此值为真则 flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS; arch_add_memory //mmu.c mem_event_callback //mem-offline.c _copy_pte //hibernate.c debug_pagealloc_enabled //mm.h return IS_ENABLED(CONFIG_DEBUG_PAGEALLOC) && _debug_pagealloc_enabled_early;
资料:
Documentation/admin-guide/kernel-parameters.txt
[KNL] 设置 CONFIG_DEBUG_PAGEALLOC 后,此参数会在启动时启用该功能。默认情况下,该功能处于禁用状态,系统的工作方式与未使用 CONFIG_DEBUG_PAGEALLOC 构建的内核基本相同。注意:为了获取大多数 debug_pagealloc 错误报告,最好同时启用 page_owner 功能。on:启用该功能
(4) _debug_pagealloc_enabled
free_pages_prepare //page_alloc.c bulkfree_pcp_prepare //page_alloc.c free_pcp_prepare //page_alloc.c check_pcp_refill //page_alloc.c check_new_pcp //page_alloc.c post_alloc_hook //page_alloc.c free_unmap_vmap_area //vmalloc.c generic_online_page //memory_hotplug.c get_freepointer_safe //slub.c debug_pagealloc_enabled_static init_debug_pagealloc //【】使能位置 return _debug_pagealloc_enabled;
3. 调试配置
(1) CONFIG_INIT_ON_ALLOC_DEFAULT_ON
当启用该配置时,内核在分配内存(如通过kmalloc()、kmem_cache_alloc()等函数)后,自动将分配的内存区域初始化为零(即填充0x00字节)。这确保了新分配的内存不会包含任何残留数据。从根本上杜绝了未初始化内存泄漏的安全风险,是内核深度防御的关键一环。尽管引入轻微性能开销,但在现代安全威胁模型下,其收益显著高于成本。管理员应根据系统安全需求与性能容忍度进行权衡配置。
它默认使能,实测,debug版本关闭 CONFIG_INIT_ON_ALLOC_DEFAULT_ON 的条件下就不会分配的所有物理内存都被清0了,只有包含 __GFP_ZERO 标志的分配请求才会清0。若是使能此配置宏,则所有分配都会走清0动作。
在这加的测试补丁:
/* 默认使能此配置宏,会打印,关闭也会打印; 再加上&&判断后不会打印 */ static void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags) { post_alloc_hook(page, order, gfp_flags); if (!free_pages_prezeroed() && want_init_on_alloc(gfp_flags)) { kernel_init_free_pages(page, 1 << order); + if (debug_new_page_clear && !(gfp_flags & __GFP_ZERO)) { //若不加&&后面的会打印 + debug_new_page_clear = 0; + printk("HAM: new page cleared without __GFP_ZERO\n"); + if (static_branch_unlikely(&init_on_alloc)) { + printk("SFL_A: init_on_alloc is true\n"); + } + } + }
(2) CONFIG_INIT_ON_FREE_DEFAULT_ON
此宏在内存释放时进行初始化,而 INIT_ON_ALLOC 在分配时初始化,两者可独立启用。
七、总结
1. 若是分配单个物理页,则优先从pcp pages中快速分配,若 order>0 才会向伙伴系统分配。
2. 若分配的迁移类型的页块不足以应对本次请求,会根据数组中指定的次序依次从其它迁移类型挪一些页面过来。
posted on 2025-06-14 22:16 Hello-World3 阅读(156) 评论(0) 收藏 举报
浙公网安备 33010602011771号