Linux内存管理源码分析

内核pwn学到UAF,发现又不太行了,虽说之前操作系统的知识没啥问题了,但是这里对于目前市面上的内存管理还是不了解,因此在这里再来浅浅分析一下,整体的数据部分,Linux采用node
、zone
、page
三级表示,接下来我们来分别叙述,这里若涉及到源码大家可以点击下面链接查看Linux内核相应版本查看
Linux 内核源码
本篇主要是个人跟随着arttnba3师傅:
arttnba3师傅个人博客
和cft56200_ln师傅:
cft56200_ln师傅博客
这两位大牛写的内存管理,a师傅的比较详细,c师傅比较简化,但是简化的前提是看明白了a师傅的部分博客知识(反正跳不开a师傅,他真的太细了
1. 数据结构部分
node节点
我们首先需要知道,对于内存访问架构来讲,一般CPU都可以分为以下两种方式:
- UMA(一致性内存访问,Uniform Memory Access),表示全局就一个
node
,且多个CPU通过1跟总线访问内存,且访问时间一致,类似SMP
- NUMA(非一致性内存访问,Not-Uniform Memory Access),每个CPU分配一块内存,存在多个
node
,且再不同情况下使用访问时间有所区别。

而node
的结构体是采用pglist_data
结构进行描述,定义在/include/linux/mmzone.h
,如下:
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn;
unsigned long node_present_pages;
unsigned long node_spanned_pages;
int node_id;
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd;
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;
int kswapd_failures;
#ifdef CONFIG_COMPACTION
int kcompactd_max_order;
enum zone_type kcompactd_highest_zoneidx;
wait_queue_head_t kcompactd_wait;
struct task_struct *kcompactd;
#endif
unsigned long totalreserve_pages;
#ifdef CONFIG_NUMA
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif
ZONE_PADDING(_pad1_)
spinlock_t lru_lock;
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
unsigned long first_deferred_pfn;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif
struct lruvec __lruvec;
unsigned long flags;
ZONE_PADDING(_pad2_)
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;
下面单独指出一些重要字段:
- node_zones:node_zones contains just the zones for THIS node. Not all of the zones may be populated, but it is the full list. It is referenced by this node's node_zonelists as well as other node's node_zonelists.说人话,他是一个
struct zone
类型的数组,包含了仅仅这个node
下的所有的zone
,这里注意并非所有zone
都被填充,但是他是已经被充满了,他被下面即将讲到的一个链表节点node_zonelists
和其他node
的node_zonelists
引用;
- node_zonelists:不标英语了,看着烦人,这里我直接写他的含义,他的定义是为了确定内存分配的时候对备用
zone
的搜索顺序,他同时可以包含非本node
的zone
,普遍他的第一个zone
链接的是本node
下的zone
数组第一个,其实这个struct zonelist
就是一个指向zone
的指针加上其他元素,我们可以看看他的数据结构,这里直接引用arttnba3
师傅的笔记,
如下:
/*
* 单次分配请求在一个 zonelist 上操作. 一个 zonelist 便是一组 zone 的列表,
* 其中第一个 zone 为分配的“目标”,而其他的 zone 为后备的zone,优先级降低。
*
* 为了提高 zonelist 的读取速度, 在 zonerefs 中包含正在被读取的 entry 的 zone index。
* 用来访问所给的 zoneref 结构体信息的帮助函数有:
*
* zonelist_zone() - 返回一个 struct zone 的指针作为 _zonerefs 中的一个 entry
* zonelist_zone_idx() - 返回作为 entry 的 zone 的 index
* zonelist_node_idx() - 返回作为 entry 的 node 的 index
*/
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
其中是一个struct zoneref
数组,接下来再看看其中的结构
struct zoneref {
struct zone *zone;
int zone_idx;
};
可以看到其就是一个指针而已
- nr_zones:记录了该
node
中所有可用的zone
数量
- node_start_pfn:
node
起始页的页框标号,这里的pfn
我们在之后讲解,这里可以理解为该node
所在的物理地址
- node_present_pages:
node
中物理页的总数量
- unsighnd long node_spanned_pages:
node
中物理页的总大小
- node_id:记录该
node
在系统中的标号,从0开始
知道了其中的一些数据结构,接下来我们了解一下node
的存储方式:我们可以在上面的网站中查找源码,在/arch/x86/mm/numa.c
中看到其中定义了一个pglist_data
的全局数组node_data[]
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
EXPORT_SYMBOL(node_data);
其中包含我们的所有node
,下面来一个好图,为啥大伙画图都这么专业捏

当我们知晓了node
节点的存储方式,我们需要另一个数组node_status
来描述对应node
节点的状态,他定义在/mm/page_alloc.c
当中,也是一个全局数组(我是真佩服写Linux的这一群大佬,这文件的分布情况跟我自己写的那个操作系统相比简直天壤之别阿)
nodemask_t node_states[NR_NODE_STATES] __read_mostly = {
[N_POSSIBLE] = NODE_MASK_ALL,
[N_ONLINE] = { { [0] = 1UL } },
#ifndef CONFIG_NUMA
[N_NORMAL_MEMORY] = { { [0] = 1UL } },
#ifdef CONFIG_HIGHMEM
[N_HIGH_MEMORY] = { { [0] = 1UL } },
#endif
[N_MEMORY] = { { [0] = 1UL } },
[N_CPU] = { { [0] = 1UL } },
#endif /* NUMA */
};
EXPORT_SYMBOL(node_states);
而我们的node_states
类型保存在/include/linux/nodemask.h
,这里仍然直接引用arttnba3
师傅
enum node_states {
N_POSSIBLE,
N_ONLINE,
N_NORMAL_MEMORY,
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY,
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_MEMORY,
N_CPU,
N_GENERIC_INITIATOR,
NR_NODE_STATES
};
说完node,我来绘个图吧,这里老抄作业好像体现不出自己真正学到了东西

我们将在之后一步一步慢慢完善这个图片
zone区域
同样的,先说其数据结构struct zone
,他位于/include/linux/mmzone.h
struct zone {
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
int pageset_high;
int pageset_batch;
#ifndef CONFIG_SPARSEMEM
unsigned long *pageblock_flags;
#endif
unsigned long zone_start_pfn;
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
#ifdef CONFIG_MEMORY_ISOLATION
unsigned long nr_isolate_pageblock;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_t span_seqlock;
#endif
int initialized;
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER];
unsigned long flags;
spinlock_t lock;
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark;
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
unsigned long compact_cached_free_pfn;
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif
#ifdef CONFIG_COMPACTION
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
bool compact_blockskip_flush;
#endif
bool contiguous;
ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
同样地,我们来了解其中比较重要的字段
- _watermark 水位线,一般表示剩余空闲页框,他又三个挡位,分别是
WMARK_MIN
,WMARK_LOW
,WMARK_HIGH
,他存放在_watermark
数组当中,进行内存分配的时候,分配器会根据当前水位来采取不同的措施,下面搞个图:

- lowmem_reserve:当本
zone
没有空闲块之后,会到别的zone
中进行分配,避免分配内存全分配在低端zone
,而我们不能保证这里分配的内存是可释放,或者最终会被释放的,出现低端zone
区域内存提前耗尽,而高端zone
区保留大量内存,因此声名该字段来保留一段内存,而这里的zone
区内存是其他zone
不能打扰的
- node:标识该
zone
所属node
,当然,这里只在NUMA
启动,UMA
中只有一个node
,不需要这个字段
- zone_pgdat:标识所属的
pglist_data
节点,同上面的node
-
pageset:由于目前都是多处理器CPU架构,因此对于临界区的同步互斥访问就是一个严重的问题,而防止出错的办法之一加锁解锁十分浪费资源,因此每个zone
当中都为每一个CPU准备一个单独的页面仓库,最开始buddy system
会首先将页面放置在各个CPU独自的页面仓库当中,需要进行分配的时候优先从其中分配,其类型结构体位于/include/linux/mmzone.h
struct per_cpu_pages {
int count;
int high;
int batch;
struct list_head lists[MIGRATE_PCPTYPES];
};
struct per_cpu_pageset {
struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
s8 expire;
u16 vm_numa_stat_diff[NR_VM_NUMA_STAT_ITEMS];
#endif
#ifdef CONFIG_SMP
s8 stat_threshold;
s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};
此结构是一个包括状态,他会被存放在每个CPU独立的.data..percpu
段当中,下面再再再次引用arttnba3
师傅的图,真的态🐂辣

看图好吧,这个order
起始就是伙伴系统中的对于不同大小页分配的请求大小

- flags:标识
zone
的状态
-
vm_stat:统计数据,这里是一个数组,而数组大小取决于定义的枚举类型,如下:
enum zone_stat_item {
NR_FREE_PAGES,
NR_ZONE_LRU_BASE,
NR_ZONE_INACTIVE_ANON = NR_ZONE_LRU_BASE,
NR_ZONE_ACTIVE_ANON,
NR_ZONE_INACTIVE_FILE,
NR_ZONE_ACTIVE_FILE,
NR_ZONE_UNEVICTABLE,
NR_ZONE_WRITE_PENDING,
NR_MLOCK,
NR_BOUNCE,
#if IS_ENABLED(CONFIG_ZSMALLOC)
NR_ZSPAGES,
#endif
NR_FREE_CMA_PAGES,
NR_VM_ZONE_STAT_ITEMS };
讲完一般结构,这里需要注意,虽说我们的node
节点中直接就是一个zone
数组,但他们之间是有区别的,此在/include/linux/mmzone.h
中有定义:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};
这里x86分别32位与64位都会有所区别,如下:
在32位中,zone
可以分为ZONE_DMA
、ZONE_NORMAL
、ZONE_HIGHMEM
,他们分别对应的起始和终止地址为
ZONE_DMA
:0~16MB
ZONE_NORMAL
:16~896MB
ZONE_HIGHMEM
:896~...MB
以上前两种类型是线性映射,也就是这里是直接映射的,也就是说存在虚拟地址就是物理地址的情形,后面的高端内存是不连续的
在64位中有所区别,zone
分为如下三种
ZONE_DMA
:0~16MB
ZONE_DMA32
:16~4GB
ZONE_NORMAL
:4GB~...
内核中取消了高端内存的概念,接着上面咱们画的图,这里我们把zone
补上

page页框
终于来到了咱们的页框,这里的page
对应的是物理页框而不是虚拟页,注意漏。
他对应的数据结构是struct page
,位于/include/linux/mm_types.h
如下:
struct page {
unsigned long flags;
union {
struct {
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
unsigned long private;
};
struct {
unsigned long dma_addr[2];
};
struct {
union {
struct list_head slab_list;
struct {
struct page *next;
#ifdef CONFIG_64BIT
int pages;
int pobjects;
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache;
void *freelist;
union {
void *s_mem;
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct {
unsigned long compound_head;
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
unsigned int compound_nr;
};
struct {
unsigned long _compound_pad_1;
atomic_t hpage_pinned_refcount;
struct list_head deferred_list;
};
struct {
unsigned long _pt_pad_1;
pgtable_t pmd_huge_pte;
unsigned long _pt_pad_2;
union {
struct mm_struct *pt_mm;
atomic_t pt_frag_refcount;
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
struct {
struct dev_pagemap *pgmap;
void *zone_device_data;
};
struct rcu_head rcu_head;
};
union {
atomic_t _mapcount;
unsigned int page_type;
unsigned int active;
int units;
};
atomic_t _refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
#if defined(WANT_PAGE_VIRTUAL)
void *virtual;
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
} _struct_page_alignment;
老样子,先解释关键字段
下面给出又一张十分详细的图,是由简·李奥师傅所作

-
flags:表示该页所处在的状态,定义于include/linux/page-flags.h
当中,他是一个枚举类型,如下:
enum pageflags {
PG_locked,
PG_referenced,
PG_uptodate,
PG_dirty,
PG_lru,
PG_active,
PG_workingset,
PG_waiters,
PG_error,
PG_slab,
PG_owner_priv_1,
PG_arch_1,
PG_reserved,
PG_private,
PG_private_2,
PG_writeback,
PG_head,
PG_mappedtodisk,
PG_reclaim,
PG_swapbacked,
PG_unevictable,
#ifdef CONFIG_MMU
PG_mlocked,
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
PG_uncached,
#endif
#ifdef CONFIG_MEMORY_FAILURE
PG_hwpoison,
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
PG_young,
PG_idle,
#endif
#ifdef CONFIG_64BIT
PG_arch_2,
#endif
__NR_PAGEFLAGS,
PG_checked = PG_owner_priv_1,
PG_swapcache = PG_owner_priv_1,
PG_fscache = PG_private_2,
PG_pinned = PG_owner_priv_1,
PG_savepinned = PG_dirty,
PG_foreign = PG_owner_priv_1,
PG_xen_remapped = PG_owner_priv_1,
PG_slob_free = PG_private,
PG_double_map = PG_workingset,
PG_isolated = PG_reclaim,
PG_reported = PG_uptodate,
};
这里采用的复用的手法,也就是说flags字段还容纳了其他元素,如下,结构划分位于/include/linux/page-flags-layout.h
当中
/*
* page->flags layout:
*
* There are five possibilities for how page->flags get laid out. The first
* pair is for the normal case without sparsemem. The second pair is for
* sparsemem when there is plenty of space for node and section information.
* The last is when there is insufficient space in page->flags and a separate
* lookup is necessary.
*
* No sparsemem or sparsemem vmemmap: | NODE | ZONE | ... | FLAGS |
* " plus space for last_cpupid: | NODE | ZONE | LAST_CPUPID ... | FLAGS |
* classic sparse with space for node:| SECTION | NODE | ZONE | ... | FLAGS |
* " plus space for last_cpupid: | SECTION | NODE | ZONE | LAST_CPUPID ... | FLAGS |
* classic sparse no space for node: | SECTION | ZONE | ... | FLAGS |
*/
可以看到在不同布局下他其实是可以用作指向归属的zone
和node
的
-
_mapcount:记录该页被页表映射的次数,初始值为-1,他是一个根据不同情况所采用的联合结构体,如果说他是被用户空间所映射,那么他会记录被映射的次数,但若是他没被映射到用户空间,页不是PageSlab
,那么他为page_type字段,它定义于/include/linux/page-flags.h
字段当中,如下:
#define PAGE_TYPE_BASE 0xf0000000
#define PAGE_MAPCOUNT_RESERVE -128
#define PG_buddy 0x00000080
#define PG_offline 0x00000100
#define PG_table 0x00000200
#define PG_guard 0x00000400
-
_refcount:用作该页在内核中的引用次数,初值为0,若大于0表示正在被使用,等于0表示空闲或将要被释放,内核函数get_page()
和put_page()
函数会来进行引用计数的增减,后者若引用计数器为1则会调用__put_single_page()
释放该页面
-
vitrual:指向物理页框对应虚拟地址(这里有点疑问那就是他被多个页表映射咋办捏,还是说每次切换进程的时候会刷新一下这里呢?)
说完数据结构,还记得上面flags
不同布局下对应的结构吗,linux一般提供了三种内存模型,定义在/include/asm-generic/memory_model.h

常用模型是sparsemem
,所以我们只了解他,中文翻译过来就是离散内存模型。在这个模型下,内存中会存在一个mem_section
类型的指针数组,而其中元素指向的mem_section
结构体中的section_mem_map
成员会指向一个struct page
类型的数组,它对应于一个连续的物理地址空间,如下图所示

其中mem_section
结构体的定义在/include/linux/mmzone.h
当中,如下:
struct mem_section {
unsigned long section_mem_map;
struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *page_ext;
unsigned long pad;
#endif
};
而我们的全局mem_section
数组存放着指向所有struct mem_section
结构体的指针,定义于/mm/sparse.c
当中:
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
____cacheline_internodealigned_in_smp;
#endif
咱们之前说到的数据结构都会使用PFN
进行表示物理地址,但实际上他并不是物理地址,而是对应的某一个page
的,而pfn
的含义就是page frame number
,他为每个物理页框所在位置都编了个号。而我们要通过PFN
找到page
或通过page
找到PFN
都需要这个mem_section
结构体中的section_mem_map
来实现。
2.伙伴系统
我们刚刚已经知道了,每个zone
中包含一个free_area
数组,其中就是一个个的双链表,且按照了buddy system
的order
进行管理,

而我们一个free_area
中其实并不只有一个双向链表,他是按照不同的migrate type
也就是迁移类型进行存放,主要是为了避免内存过于碎片化,如下图:

而这里的页面存在一个迁移类型,这决定了该页是否可以迁移,如下:
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES,
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE,
#endif
MIGRATE_TYPES
};
下面仍然是一个arttnba3
师傅所做的图

而free_area
中的结构中的nr_free
表示的是当前free_area
中空闲页面块的数量
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
1. 分配页框
内核中实现了几个函数接口来请求页框,最终都会调用__alloc_pages_nodemask
,如下图

其中核心的函数就是__alloc_pages_nodemask
,这里我们需要先知道gfp_mask
和alloc_flags
这两个标志
gfp_flags
- __GFP_DMA:请求在ZONE_DMA区域中分配页面;
- __GFP_HIGHMEM:请求在ZONE_HIGHMEM区域中分配页面;
- __GFP_MOVABLE:ZONE_MOVALBE可用时在该区域分配页面,同时表示页面分配后可以在内存压缩时进行迁移,也能进行回收;
- __GFP_RECLAIMABLE:请求分配到可恢复页面;
- __GFP_HIGH:高优先级处理请求;
- __GFP_IO:请求在分配期间进行 I/O 操作;
- __GFP_FS:请求在分配期间进行文件系统调用;
- __GFP_ZERO:请求将分配的区域初始化为 0;
- __GFP_NOFAIL:不允许请求失败,会无限重试;
- __GFP_NORETRY:请求不重试内存分配请求;
这里我是直接引用的cft56200_ln师傅的图

alloc_flags
- ALLOC_WMARK_MIN:仅在最小水位water mark及以上限制页面分配;
- ALLOC_WMARK_LOW:仅在低水位water mark及以上限制页面分配;
- ALLOC_WMARK_HIGH:仅在高水位water mark及以上限制页面分配;
- ALLOC_HARDER:努力分配,一般在gfp_mask设置了__GFP_ATOMIC时会使用;
- ALLOC_HIGH:高优先级分配,一般在gfp_mask设置了__GFP_HIGH时使用;
- ALLOC_CPUSET:检查是否为正确的 cpuset;
- ALLOC_CMA:允许从 CMA 区域进行分配
下面就是该核心函数的函数体部分,他位于/mm/page_alloc.c
当中,如下:
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;
gfp_t alloc_mask;
struct alloc_context ac = { };
if (unlikely(order >= MAX_ORDER)) {
WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
return NULL;
}
gfp_mask &= gfp_allowed_mask;
alloc_mask = gfp_mask;
if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
return NULL;
alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
alloc_mask = current_gfp_context(gfp_mask);
ac.spread_dirty_pages = false;
ac.nodemask = nodemask;
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp_mask, order) != 0)) {
__free_pages(page, order);
page = NULL;
}
trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);
return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);
上面函数概括为下面的步骤:
- 检测环境,准备分配
- 快速分配,调用
get_page_from_freelist()
- 慢速分配,调用
__alloc_pages_slowpath()
- 快慢均失败,考虑页面回收,杀死进程后再次尝试
其中准备函数prepare_alloc_pages()
是设定一下环境值且从指定参数node
中获取一个zonelist
,这里就不多讲了,直接来讲解快速分配函数get_page_from_freelist()
,他位于/mm/page_alloc.c
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:
no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
z = ac->preferred_zoneref;
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
ac->nodemask) {
struct page *page;
unsigned long mark;
if (cpusets_enabled() &&
(alloc_flags & ALLOC_CPUSET) &&
!__cpuset_zone_allowed(zone, gfp_mask))
continue;
if (ac->spread_dirty_pages) {
if (last_pgdat_dirty_limit == zone->zone_pgdat)
continue;
if (!node_dirty_ok(zone->zone_pgdat)) {
last_pgdat_dirty_limit = zone->zone_pgdat;
continue;
}
}
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 = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags,
gfp_mask)) {
int ret;
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
if (static_branch_unlikely(&deferred_pages)) {
if (_deferred_grow_zone(zone, order))
goto try_this_zone;
}
#endif
BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone;
if (node_reclaim_mode == 0 ||
!zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
continue;
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN:
continue;
case NODE_RECLAIM_FULL:
continue;
default:
if (zone_watermark_ok(zone, order, mark,
ac->highest_zoneidx, alloc_flags))
goto try_this_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;
} else {
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
if (static_branch_unlikely(&deferred_pages)) {
if (_deferred_grow_zone(zone, order))
goto try_this_zone;
}
#endif
}
}
if (no_fallback) {
alloc_flags &= ~ALLOC_NOFRAGMENT;
goto retry;
}
return NULL;
}
其功能就是首先遍历当前的zone
,判断当前zone
是否满足low water mark水位,若不满足则进行一次快速回收操作,再次检测水位情况,若还是不能满足,则遍历下一个zone
,然后采取同样的步骤,最后进入rmqueue
函数,这就是buddy system
的核心,过程可以简化看下图:

相比于代码,下图更加直观,之后我们来查看关键函数rmqueue()
,它位于/mm/page_alloc.c
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;
if (likely(order == 0)) {
if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
migratetype != MIGRATE_MOVABLE) {
page = rmqueue_pcplist(preferred_zone, zone, gfp_flags,
migratetype, alloc_flags);
goto out;
}
}
WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
spin_lock_irqsave(&zone->lock, flags);
do {
page = NULL;
if (order > 0 && alloc_flags & ALLOC_HARDER) {
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
if (page)
trace_mm_page_alloc_zone_locked(page, order, migratetype);
}
if (!page)
page = __rmqueue(zone, order, migratetype, alloc_flags);
} while (page && check_new_pages(page, order));
spin_unlock(&zone->lock);
if (!page)
goto failed;
__mod_zone_freepage_state(zone, -(1 << order),
get_pcppage_migratetype(page));
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
zone_statistics(preferred_zone, zone);
local_irq_restore(flags);
out:
if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) {
clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
wakeup_kswapd(zone, 0, 0, zone_idx(zone));
}
VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
return page;
failed:
local_irq_restore(flags);
return NULL;
}
有部分注释,我在上面中西合璧标注了一下,接下来先提醒大家伙,之前咱们讲解zone
上的一个字段per-cpu pageset
,他是为了放置条件竞争的问题,为每个cpu单独设置一个仓库用来为buddy system
进行迅速的分配,这里就是给出了buddy system
先从他里面调用的函数代码,总结为一下流程
- 若
order
为0,若没有开启CMA
|设置ALLOC_CMA
|迁移类型为MIGRATE_MOVABLE,则先从per-cpu pageset 中分配并且返回
- order >0 调用
__rmqueue_smallest()
分配
- 若未分配成功,这里不管order是否为0,调用
__rmqueue()
分配
- 结果检查,调用
check_new_pages()
,未通过则循环跳到第二步
我们一个一个关键函数来查看,首先是分配per_cpu_pageset
,也就是如下函数
rmqueue_pcplist()
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
struct zone *zone, gfp_t gfp_flags,
int migratetype, unsigned int alloc_flags)
{
struct per_cpu_pages *pcp;
struct list_head *list;
struct page *page;
unsigned long flags;
local_irq_save(flags);
pcp = &this_cpu_ptr(zone->pageset)->pcp;
list = &pcp->lists[migratetype];
page = __rmqueue_pcplist(zone, migratetype, alloc_flags, pcp, list);
if (page) {
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1);
zone_statistics(preferred_zone, zone);
}
local_irq_restore(flags);
return page;
}
主要是进行了一些同步互斥操作(开关中断),然后调用函数__rmqueue_pcplist
static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype,
unsigned int alloc_flags,
struct per_cpu_pages *pcp,
struct list_head *list)
{
struct page *page;
do {
if (list_empty(list)) {
pcp->count += rmqueue_bulk(zone, 0,
READ_ONCE(pcp->batch), list,
migratetype, alloc_flags);
if (unlikely(list_empty(list)))
return NULL;
}
page = list_first_entry(list, struct page, lru);
list_del(&page->lru);
pcp->count--;
} while (check_new_pcp(page));
return page;
}
这里先判定链表,若为空,则调用rmqueue_bulk()
函数,从zone
上拿到pages之后再进行unlink
,而rmqueue_bulk()
函数最终会调用__rmqueue()
static int rmqueue_bulk(struct zone *zone, unsigned int order,
unsigned long count, struct list_head *list,
int migratetype, unsigned int alloc_flags)
{
int i, alloced = 0;
spin_lock(&zone->lock);
for (i = 0; i < count; ++i) {
struct page *page = __rmqueue(zone, order, migratetype,
alloc_flags);
if (unlikely(page == NULL))
break;
if (unlikely(check_pcp_refill(page)))
continue;
list_add_tail(&page->lru, list);
alloced++;
if (is_migrate_cma(get_pcppage_migratetype(page)))
__mod_zone_page_state(zone, NR_FREE_CMA_PAGES,
-(1 << order));
}
__mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
spin_unlock(&zone->lock);
return alloced;
}
__rmqueue_smallest
该函数就是由order对应的free_area
中类型为migration type
的链表上进行分配,如果不够则向高order处请求,由于这里都是以2^order来进行分配,因此如果说我order为1,且这里不够的话,我们就转而order为2的链表,将其中的块对半拆下到低order中,其中向更高order分配是通过循环和脱链完成,而拆高阶的page是通过expand()
函数来进行的
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;
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;
del_page_from_free_list(page, zone, current_order);
expand(zone, page, order, current_order, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
而拆分函数expand
也比较简单
static inline void expand(struct zone *zone, struct page *page,
int low, int high, int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
high--;
size >>= 1;
VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);
if (set_page_guard(zone, &page[size], high, migratetype))
continue;
add_to_free_list(&page[size], zone, high, migratetype);
set_buddy_order(&page[size], high);
}
}
__rmqueue()
最开始我以为这个才是最终函数,但其实他不是,他反而还会调用__rmqueue_smallest()
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
unsigned int alloc_flags)
{
struct page *page;
if (IS_ENABLED(CONFIG_CMA)) {
if (alloc_flags & ALLOC_CMA &&
zone_page_state(zone, NR_FREE_CMA_PAGES) >
zone_page_state(zone, NR_FREE_PAGES) / 2) {
page = __rmqueue_cma_fallback(zone, order);
if (page)
goto out;
}
}
retry:
page = __rmqueue_smallest(zone, order, migratetype);
if (unlikely(!page)) {
if (alloc_flags & ALLOC_CMA)
page = __rmqueue_cma_fallback(zone, order);
if (!page && __rmqueue_fallback(zone, order, migratetype,
alloc_flags))
goto retry;
}
out:
if (page)
trace_mm_page_alloc_zone_locked(page, order, migratetype);
return page;
}
整体快速分配可以看下面这张图

我们了解完了快速分配,接下来就是慢速分配了,其中他的功能包括了内存碎片化的整理和回收,他的代码太长,我就也只贴一部分,如下:
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
page = __alloc_pages_direct_compact(gfp_mask, order,
alloc_flags, ac,
INIT_COMPACT_PRIORITY,
&compact_result);
......
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
......
}
其中内存碎片化也即是利用到迁移的知识,这里有两个关键函数,其中之一就是__alloc_pages_direct_compact
static struct page *
__alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
unsigned int alloc_flags, const struct alloc_context *ac,
enum compact_priority prio, enum compact_result *compact_result)
{
struct page *page;
unsigned int noreclaim_flag;
if (!order)
return NULL;
noreclaim_flag = memalloc_noreclaim_save();
*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
prio);
memalloc_noreclaim_restore(noreclaim_flag);
if (*compact_result <= COMPACT_INACTIVE)
return NULL;
count_vm_event(COMPACTSTALL);
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page) {
struct zone *zone = page_zone(page);
zone->compact_blockskip_flush = false;
compaction_defer_reset(zone, order, true);
count_vm_event(COMPACTSUCCESS);
return page;
}
count_vm_event(COMPACTFAIL);
cond_resched();
return NULL;
}
这里的函数也是迁移算法memory compaction
的代码实现,该算法可以简化为下面的流程

也就是分为两个链表,一个专门遍历空闲页,一个专门遍历使用页,注意这俩要分别维持链表,然后最后进行交换操作就实现了迁移过程,且记住这个迁移是需要page
本身是允许的才行,
在完成上述迁移操作后会再次尝试快速分配,这里的碎片化整理还有其他方式,但是我这里暂不区深究,先记录个图等我哪天想起来了再探索

而关于慢速分配还有个函数是__alloc_pages_direct_reclaim()
,他的作用主要是回收,而不是碎片整理
最后来个整体分配页框的函数流程图

暂未完工
一天下来怎么硕呢,感觉都是几位师傅的博客一口一口的喂饭,虽说自己理解了大致过程,但是对于源码的解读还是太粗了,这个系列还有释放页框和slub算法的源码实现,slub算法我再上一篇博客中已经讲解了大致原理了哦,这里还差一部分,