物理页面分配之快速路径
Table of Contents
本文分析在理想情况下,也就是没有内存短缺时内核通过伙伴系统分配物理页面的流程。物理页面分配的接口包括alloc_pages,这个函数在成功分配时返回的是第一个页面的page数据结构。另外一类接口是__get_free_pages,返回的是内核空间的虚拟地址。本文主要以alloc_pages为入口,分析内核是如何在理想情况下经过快速路径去分配物理页面的,alloc_pages就是一个简单的宏定义,主要调用alloc_pages_noprof。
1. alloc_pages_noprof
原型:
struct page *alloc_pages_noprof(gfp_t gfp, unsigned int order)
作用:
分配2^order个连续页面,第一个物理页面自然对齐,所谓自然对齐,举个例子假如order为3,那么就会对齐到2^3*PAGE_SIZE的字节处。当在进程上下文时,会遵循NUMA分配策略。分配失败时返回NULL。
详细分析:
参数gfp的类型是gfp_t:
typedef unsigned int __bitwise gfp_t;
__bitwise主要是为了类型安全而存在,比如gfp_t类型的量不能和int类型的量进行运算和直接赋值(除非进行了类型强制转换),否则开启了Wall的编译选项时,编译器会报警告,它是编译器支持的一 个attribute。GFP标志主要用来指明内存应该如何分配,比如典型的由GFP推出内存应该在哪个zone中去分配。GFP这个缩写其实是get_free_pages,__开头这样的GFP标志比较底层,一般的用户应该使用GFP_KERNEL这样的由__开头的标志形成的组合。
该函数会根据当前上下文是进程上下文还是中断上下文,分不同的情况传入不同的参数pol(类型为mempolicy)去调用alloc_pages_mpol_noprof函数,alloc_pages_noprof定义如下:
struct page *alloc_pages_noprof(gfp_t gfp, unsigned int order) { struct mempolicy *pol = &default_policy; /* * No reference counting needed for current->mempolicy * nor system default_policy */ if (!in_interrupt() && !(gfp & __GFP_THISNODE)) pol = get_task_policy(current); return alloc_pages_mpol_noprof(gfp, order, pol, NO_INTERLEAVE_INDEX, numa_node_id()); }
在不处于中断上下文并且传入的GFP标志位没有__GFP_THISNODE时,内存的NUMA分配策略才会生效。那么如何判定是不是在中断上下文呢,__GFP_THISNODE究竟是什么意思呢,还有其它的NUMA策略标志吗?
判断是否在中断上下文中使用in_interrupt:
#define in_interrupt() (irq_count())
在没有定义CONFIG_PREEMPT_RT的情况下,irq_count被如下定义:
# define irq_count() (preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK))
preempt_count在通用头文件include/asm-generic/preempt.h与架构头文件arch/x86/include/asm/preempt.h中均有定义,但是一般是架构头文件优先使用,这种优先特性体现在Makefile中对头文件使用-I选项包含头文件的先后上,preempt_count在上述两个头文件中均有实现,但是-I选项只要找到第一个有实现的头文件即停止搜索:
LINUXINCLUDE := \ -I$(srctree)/arch/$(SRCARCH)/include \ -I$(objtree)/arch/$(SRCARCH)/include/generated \ $(if $(building_out_of_srctree),-I$(srctree)/include) \ -I$(objtree)/include \ $(USERINCLUDE) export KBUILD_CPPFLAGS NOSTDINC_FLAGS LINUXINCLUDE OBJCOPYFLAGS KBUILD_LDFLAGS
X86架构下,preempt_count被如下定义:
static __always_inline int preempt_count(void) { return raw_cpu_read_4(pcpu_hot.preempt_count) & ~PREEMPT_NEED_RESCHED; }
也就是说每个CPU都有一个4字节的int量preempt_count表征现在的抢占计数,这32个bit按如下划分:
PREEMPT_MASK: 0x000000ff SOFTIRQ_MASK: 0x0000ff00 HARDIRQ_MASK: 0x000f0000 NMI_MASK: 0x00f00000 PREEMPT_NEED_RESCHED: 0x80000000
也就是说最低8个bit(最低1个字节)用来计数抢占,低第二个字节用来表示软中断的计数,依次类推,那么NMI_MASK、HARDIRQ_MASK以及SOFTIRQ_MASK等各种MASK宏用来取出对应字段计数的,就可以按如下代码定出:
#define PREEMPT_BITS 8 #define SOFTIRQ_BITS 8 #define HARDIRQ_BITS 4 #define NMI_BITS 4 #define PREEMPT_SHIFT 0 #define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS) #define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS) #define NMI_SHIFT (HARDIRQ_SHIFT + HARDIRQ_BITS) #define __IRQ_MASK(x) ((1UL << (x))-1) #define PREEMPT_MASK (__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT) #define SOFTIRQ_MASK (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT) #define HARDIRQ_MASK (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT) #define NMI_MASK (__IRQ_MASK(NMI_BITS) << NMI_SHIFT)
所以回到前面irq_count的定义以及回答如何判定是不是在中断上下文中:只要不可屏蔽中断、硬中断以及软中断三者有其一即可认为当前处于中断上下文中。而一般在进入中断上下文时会对preempt_count相应的字段进行自增:
__irq_enter->preempt_count_add->__preempt_count_add
__GFP_THISNODE标志主要作用是表明从指定的节点上分配内存,禁止分配回退或使用其它策略,如果请求的节点没有足够的内存资源,那么分配将会失败,这种情况自然不需要考虑NUMA内存分配策略了。除了这个标志还有如下的一些移动和放置策略:
- __GFP_MOVABLE
表示页面是可移动的。这个标志通常用于那些可以在内存整理(compaction)过程中通过页面迁移移动的页面,或是可以被回收的页面。在内存管理中,标记为__GFP_MOVABLE的页面将被放置在特定的pageblocks中,这些pageblocks一般只包含可移动页面,以尽量减少外部碎片的问题。 - __GFP_RECLAIMABLE
主要用于slab分配。指定了SLAB_RECLAIM_ACCOUNT的slab分配使用该标志,这些页面可以通过shrinker机制回收。这使得slab分配的内存可以在系统需要时被回收,以便释放更多的内存资源。 - __GFP_WRITE
表示调用者打算修改页面内容,即页面将被“写脏”(dirty)。内核在分配这些页面时,会尽量将这些页面在本地节点之间进行分散分配,以避免所有脏页集中在同一个内存区域或节点,帮助实现公平的内存分配策略(fair zone allocation policy)。 - __GFP_HARDWALL
强制执行cpuset的内存分配策略。如果系统中存在cpuset配置(用于控制和隔离不同任务的内存使用),这个标志确保页面分配遵循cpuset的内存限制和隔离策略。 - __GFP_ACCOUNT
该标志表示分配的内存将被记账到kmemcg(Kernel Memory Control Group),即为分配的内存计入内核内存控制组。它用于限制和跟踪控制组(cgroup)中分配的内核内存资源。
当既不在中断上下文gfp参数也没有设置__GFP_THISNODE时,就会调用get_task_policy函数:
struct mempolicy *get_task_policy(struct task_struct *p) { struct mempolicy *pol = p->mempolicy; int node; if (pol) return pol; node = numa_node_id(); if (node != NUMA_NO_NODE) { pol = &preferred_node_policy[node]; /* preferred_node_policy is not initialised early in boot */ if (pol->mode) return pol; } return &default_policy; }
该函数首先获取当前进程的内存分配策略mempolicy,mempolicy可以被关联到一个进程,也可以关联到一个VMA。对于VMA关联的,优先考虑,然后才是进程关联。根据上面get_task_policy函数的定义,内核有一个默认的mempolicy叫default_policy,其定义如下:
static struct mempolicy default_policy = { .refcnt = ATOMIC_INIT(1), /* never free it */ .mode = MPOL_LOCAL, };
MPOL_LOCAL是NUMA内存策略的默认方式,所谓NUMA内存策略可以允许用户指定在特定节点上进行内存分配的优先级和方式,适用于不同的进程或VMA(虚拟内存区域)。这些策略可以用于优化多节点系统上的内存访问效率。具体有以下方式:
- interleave(交错方式MPOL_INTERLEAVE)
内存分配在指定的一组节点上交错进行,如果分配失败则会采用常规的回退策略。对于VMA分配,这种交错策略基于对象的偏移量(或匿名内存的映射偏移量);对于进程策略,则基于一个进程计数器进行分配。 - weighted interleave(加权交错MPOL_WEIGHTED_INTERLEAVE)
类似于interleave,但允许根据每个节点的权重分配内存。例如,nodeset(0,1)与权重(2,1)表示每在节点0上分配两页内存后,再在节点1上分配一页内存。 - bind(绑定MPOL_BIND)
只在指定的节点集合上分配内存,不采用回退策略。 - preferred(优先某个节点MPOL_PREFERRED)
首先尝试在指定节点上分配内存,若失败则使用常规回退策略。如果节点设置为NUMA_NO_NODE,则优先在本地CPU上分配内存。通常这类似于默认策略,但在VMA上设置时可以覆盖非默认的进程策略。 - preferred many(多个节点优先MPOL_PREFERRED_MANY)
与preferred类似,但允许指定多个优先节点,然后再进行回退。 - default(默认)
优先在本地节点上分配内存,或者在VMA上使用进程策略。这是Linux内核在NUMA系统上一直采用的默认行为。
另外,进程策略适用于该进程上下文中的大多数非中断内存分配,中断则不受策略影响,VMA策略只适用于该VMA中的内存分配。策略应用于系统的高区内存,而不应用于低区和GFP_DMA内存分配。对于共享内存(shmem/tmpfs),策略在所有用户之间共享,即使没有用户映射时也会记住该策略。
中断不会使用当前进程的内存策略,它们总是优先在本地CPU上分配内存。这种设计是为了在中断处理过程中尽可能减少延迟。
对于交错策略来说,在进程上下文中,不需要锁定机制,因为进程只会访问自身的状态,因此没有并发冲突。对于VMA的操作,mmap_lock的读锁(down_read)在一定程度上保护了这些操作,以确保内存映射的一致性。
内存策略mempolicy结构体的释放:内存策略对象通过引用计数来管理生命周期。mpol_put()函数会减少内存策略的引用计数,当引用计数降为零时,该内存策略对象会被释放。这种机制保证了对象只会在不再使用时被释放,避免了内存泄漏。
内存策略mempolicy结构体的复制:mpol_dup()函数用于分配一个新的内存策略,并将指定的内存策略复制到新的内存空间。新创建的内存策略对象的引用计数被初始化为1,表示当前调用者持有该引用。这允许多个内存策略对象彼此独立,同时保证每个对象的生命周期被正确管理。
回到get_task_policy函数,如果进程有内存分配策略mempolicy,则返回这个策略。如果进程没有内存策略,那么就会从系统的全局节点策略数组preferrred_node_policy中去获取内存策略,当然在系统启动早期preferred_node_policy里可能是没有数据的,所以需要判断pol->mode非零,因为preferred_node_policy的定义是static的(被初始化为0):
static struct mempolicy preferred_node_policy[MAX_NUMNODES];
MAX_NUMNODES定义了系统支持的最大NUMA节点数量:
#ifdef CONFIG_NODES_SHIFT #define NODES_SHIFT CONFIG_NODES_SHIFT #else #define NODES_SHIFT 0 #endif #define MAX_NUMNODES (1 << NODES_SHIFT)
而NODES_SHIFT的值依据不同的架构有不同的配置,这主要体现在比如arch/x86/Kconfig中有如下代码:
config NODES_SHIFT int "Maximum NUMA Nodes (as a power of 2)" if !MAXSMP range 1 10 default "10" if MAXSMP default "6" if X86_64 default "3" depends on NUMA help Specify the maximum number of NUMA Nodes available on the target system. Increases memory reserved to accommodate various tables.
这样在编译构建时会自动生成,比如CONFIG_NODES_SHIFT在自动生成的头文件include/generated/autoconf.h中被定义为10,那么MAX_NUMNODES = 1 << 10 = 1024。
get_task_policy中还使用了numa_node_id函数,在定义了CONFIG_USE_PERCPU_NUMA_NODE_ID时该函数定义如下:
#ifdef CONFIG_USE_PERCPU_NUMA_NODE_ID DECLARE_PER_CPU(int, numa_node); #ifndef numa_node_id /* Returns the number of the current Node. */ static inline int numa_node_id(void) { return raw_cpu_read(numa_node); } #endif
该值在cpu启动初始化的流程被初始化:
start_secondary->cpu_init->set_numa_node #ifndef set_numa_node static inline void set_numa_node(int node) { this_cpu_write(numa_node, node); } #endif
2. alloc_pages_mpol_noprof
原型:
struct page *alloc_pages_mpol_noprof(gfp_t gfp, unsigned int order, struct mempolicy *pol, pgoff_t ilx, int nid)
作用:
分配2^order个页面,第一个参数是内存分配标志gfp,最后一个参数nid是前面使用numa_node_id获得的该运行CPU所在的node。针对第四个参数ilx,在交错策略下时,ilx表明是否使用task_struct里的il_prev作为依据来选择内存分配的节点,为NO_INTERLEAVE_INDEX时表明使用task_struct:ilx_prev,而当通过get_vma_policy来获得一个有效的ilx值时就使用这个值来确定如何选择哪个节点来分配内存。第三个参数pol就是前面通过get_task_policy获得的内存策略,当然还有其它的调用路径通过get_vma_policy来获得内存策略。
详细分析:
alloc_pages_mpol_noprof主要分为三个部分来完成其功能:
- 第一部分是通过policy_nodemask函数获得在哪个(些)节点上分配内存,由nodemask_t类型的指针nodemask表示,并且返回在哪个目标节点上分配的节点号,由nid表示。
- 第二部分是针对不同的情况调用不同的分配函数,一种是针对MPOL_PREFERRED_MANY内存策略,调用alloc_pages_preferred_many分配内存,而针对大页内存分配的情况会做一些特殊处理,然后再调用__alloc_pages_node_noprof完成内存分配,最后一种情况就是通过__alloc_pages_noprof完成除前面两种特殊情况的“正常”页面分配。
- 第三部分是针对交错分配的方式,要更新一些统计信息。
以下继续针对这三部分的代码详细分析。
2.1. 第一部分
这部分代码确定分配的节点mask以及目标节点的节点号,代码如下:
nodemask_t *nodemask; nodemask = policy_nodemask(gfp, pol, ilx, &nid);
可以看到主要就是调用了policy_nodemask来确定nodemask以及nid,注意这里的nid是alloc_pages_mpol_noprof的最后一个参数nid的地址,nid的值通过numa_node_id获得代表当前运行CPU所在的节点,这里传入nid的地址,意味着policy_nodemask函数可能会修改nid的值。返回的nodemask表示可以在哪些节点上分配内存,而返回的nid是首选的节点。
policy_nodemask根据前面介绍的NUMA内存策略分几种case来给nodemask和nid赋予不同的值,其原型如下:
static nodemask_t *policy_nodemask(gfp_t gfp, struct mempolicy *pol, pgoff_t ilx, int *nid)
第一种case MPOL_PREFERRED:
nodemask_t *nodemask = NULL; switch (pol->mode) { case MPOL_PREFERRED: /* Override input node id */ *nid = first_node(pol->nodes); break;
这种情况只设置了优先使用分配内存的节点号,使用first_node去找到pol->nodes里第一个置位的bit的序号,从这里也可以看出pol->nodes这个成员表征了候选的可以用于分配的一些节点,只是说这些节点要按各种不同的策略去选择。
第二种case MPOL_PREFERRED_MANY:
case MPOL_PREFERRED_MANY: nodemask = &pol->nodes; if (pol->home_node != NUMA_NO_NODE) *nid = pol->home_node; break;
这种情况是有nodemask的,因为perferred many就是在一组优先的节点里分配,并且pol->home_node是有效值时,返回pol里的home_node号作为优先分配的节点。
第三种case MPOL_BIND:
case MPOL_BIND: /* Restrict to nodemask (but not on lower zones) */ if (apply_policy_zone(pol, gfp_zone(gfp)) && cpuset_nodemask_valid_mems_allowed(&pol->nodes)) nodemask = &pol->nodes; if (pol->home_node != NUMA_NO_NODE) *nid = pol->home_node; /* * __GFP_THISNODE shouldn't even be used with the bind policy * because we might easily break the expectation to stay on the * requested node and not break the policy. */ WARN_ON_ONCE(gfp & __GFP_THISNODE); break;
对于使用MPOL_BIND的内存策略,要限制内存策略不能应用到较低序号的zone,这个判断逻辑由apply_policy_zone函数实现,它的第二个参数,调用gfp_zone由gfp标志里去获得分配内存的目标zone号,也就是获得的目标zone号要大于apply_policy_zone里的一个特别的zone号,这时内存策略才生效。这里主要详细分析下gfp_zone的实现,它决定了内存从哪个zone去分配,较为关键。至于本case的代码较为简单。
在分析gfp_zone函数前,先介绍内核内存管理涉及到的各种类型的zone:
- ZONE_DMA
区域ZONE_DMA用于适应一些旧式设备,这些设备只能通过DMA(直接内存访问)访问低地址的内存(通常在16MB以下),部分旧设备只能访问某个范围内的物理内存地址。如果要在这些设备上进行DMA操作,内存必须分配在这个特定区域内,比如ISA总线设备或早期的DMA控制器。CONFIG_ZONE_DMA配置选项决定是否启用这个zone。 - ZONE_DMA32
区域ZONE_DMA32用于支持能够访问32位地址空间(4GB以下)但不能访问更高地址的设备。一些设备,如32位PCI设备,只能访问4GB以下的内存,因此分配时需要确保内存位于这个范围内。适用于需要低于4G地址范围的64位系统。CONFIG_ZONE_DMA32配置选项决定是否启用该zone。 - ZONE_NORMAL
区域ZONE_NORMAL包含可被大多数内核和用户进程直接访问的常规物理内存区域。在大多数情况下,DMA操作和内核直接访问都可以在ZONE_NORMAL中完成,这是标准的内存区域。所有常规的内存操作(除非设备或操作对内存有特殊要求)。 - ZONE_HIGHMEM
ZONE_HIGHMEM用于32位系统中超过直接寻址能力的高地址空间。它通过分页机制(如映射页表条目)来访问高于900MB的内存。32位系统只能直接寻址4GB的地址空间,对于大内存机器而言,这样的空间显得不足。因此,把高地址的物理内存放到ZONE_HIGHMEM中,使内核可以通过间接的方式访问它们。在内存大于4GB的32位系统上,通过页表映射方式访问超出寻址能力的内存区域。启用CONFIG_HIGHMEM后可用。 ZONE_MOVABLE
ZONE_MOVABLE主要用于可以迁移的页面,比如用户态内存或页缓存等。在内存卸载和巨页分配时,这个zone有助于提高成功率。通过将可以迁移的内存放在ZONE_MOVABLE,可以确保在需要时有一部分内存可以被迁移或回收,从而进行更灵活的内存管理。例如,大页分配和热插拔内存。用于可热插拔的内存、可移动页面的分配,以及减少不能迁移的分配对系统性能的影响。这种类型类似于ZONE_NORMAL,但是不可移动的页面却只能在ZONE_NORMAL。不过以下有一些情况会存在ZONE_MOVABLE有unmovable页面的情况:
- 长期钉住的页面是不可移动的,因此在ZONE_MOVABLE里不允许长期钉住页面,当页面先钉住然后通过page fault分配物理页面,这种情况没什么问题,页面会来自正确的zone(避开ZONE_MOVABLE),但是当页面被钉住时有可能地址空间里已经有页面位于ZONE_MOVABLE了,这时会将这些页面迁移到不同zone(避开ZONE_MOVABLE)中,迁移要是失败,钉住页面也会失败。
- memblock对于kernelcore/movablecore区域的设置可能会导致ZONE_MOVABLE里有unmovable的页面。
- ZONE_DEVICE
ZONE_DEVICE主要用于特殊的内存设备,如NVDIMM(非易失性双列直插内存模块)或其它直接映射到系统内存的设备。随着硬件的演进,现代系统需要支持直接映射到内存的硬件设备,如持久内存。这些设备可以直接与系统内存交互,通常是可持久化的。如使用非易失性存储器(如 NVDIMM)、显存映射等。启用CONFIG_ZONE_DEVICE后可用。
enum zone_type { ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_MOVABLE, ZONE_DEVICE, __MAX_NR_ZONES };
现在可以介绍gfp_zone函数了,gfp_zone函数的主要作用就是根据传入的gfp标志映射到不同类型的zone,也就是返回zone_type类型的枚举,这些类型前面刚介绍过了。现在看gfp_zone的实现:
static inline enum zone_type gfp_zone(gfp_t flags) { enum zone_type z; int bit = (__force int) (flags & GFP_ZONEMASK); z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & ((1 << GFP_ZONES_SHIFT) - 1); VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); return z; }
GFP_ZONE_TABLE这个宏类似于一个表(数组),它将不同的GFP标志组合映射到不同的zone_type:
#define GFP_ZONE_TABLE ( \ (ZONE_NORMAL << 0 * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT) \ | (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT) \ | (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT) \ | (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\ | (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\ )
这样定义出来这个GFP_ZONE_TABLE其实就是一个数字,只是这个数字按每GFP_ZONES_SHIFT个数目的bit 存储一个条目,这个条目通过传入的gfp标志索引,索引出来的是一个zone_type枚举类型的值。
这个宏其实定义了一种zone分配的回退机制,举个例子对于条目:
ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT
就是说当传入的GFP标志既设置了__GFP_MOVABLE又设置了__GFP_HIGHMEM,那么最多可以回退到ZONE_MOVABLE这种zone_type,又比如:
(OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT)
同时gfp标志同时设置了___GFP_MOVABLE和___GFP_DMA,那么最多可以回退到ZONE_DMA这种zone_type(假如配置了CONFIG_ZONE_DMA)。
那么可以观察出来,这里GFP_ZONE_TABLE映射出来的值都是往较低的zone_type中映射。
但是这里要提请读者注意,所谓“回退到”并不是指可以借用zone_type中较低zone的内存,而是较高的zone需要自己预留内存。
当然,有一些标志组合是无法满足的,比如同时设置了DMA+HIGHMEM,DMA本来就要求地址在低内存区,当然无法又在高地址区分配,所有这些搭配由GFP_ZONE_BAD宏表示。
GFP_ZONES_SHIFT在笔者的环境就是2,也就是说GFP_ZONE_TABLE中每2个比特保存一个条目。通过这种一个数字就实现表格映射的作用,实际也是一种性能优化,通过逻辑运算一个数字的方式是比查表数组访存要高效的。
在没有定义特殊的内存zone(比如DMA,HIGHMEM等)时,都会退化为ZONE_NORMAL:
#ifdef CONFIG_HIGHMEM #define OPT_ZONE_HIGHMEM ZONE_HIGHMEM #else #define OPT_ZONE_HIGHMEM ZONE_NORMAL #endif #ifdef CONFIG_ZONE_DMA #define OPT_ZONE_DMA ZONE_DMA #else #define OPT_ZONE_DMA ZONE_NORMAL #endif #ifdef CONFIG_ZONE_DMA32 #define OPT_ZONE_DMA32 ZONE_DMA32 #else #define OPT_ZONE_DMA32 ZONE_NORMAL #endif
再来看gpf_zone函数就简单了首先从flags中拿到低4bit的值:
enum zone_type z; int bit = (__force int) (flags & GFP_ZONEMASK);
为什么是低4bit呢,通过GFP_ZONEMASK的定义就知道了:
enum { ___GFP_DMA_BIT, ___GFP_HIGHMEM_BIT, ___GFP_DMA32_BIT, ___GFP_MOVABLE_BIT, ... }; #define BIT(nr) (UL(1) << (nr)) #define ___GFP_DMA BIT(___GFP_DMA_BIT) #define ___GFP_HIGHMEM BIT(___GFP_HIGHMEM_BIT) #define ___GFP_DMA32 BIT(___GFP_DMA32_BIT) #define ___GFP_MOVABLE BIT(___GFP_MOVABLE_BIT) #define __GFP_DMA ((__force gfp_t)___GFP_DMA) #define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) #define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) #define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ #define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
这里可以看到低4个bit其实就是zone修饰符,指明了哪些zone用于分配内存。之前介绍过gfp_t本质是个int,但它具有受限的取值,以限制某些int能进行的运算(范围),但是gfp_t不行。比如上面提到了gfp_t可能的四种值,一个完整的gfp_t值列表如下:
enum { ___GFP_DMA_BIT, ___GFP_HIGHMEM_BIT, ___GFP_DMA32_BIT, ___GFP_MOVABLE_BIT, ___GFP_RECLAIMABLE_BIT, ___GFP_HIGH_BIT, ___GFP_IO_BIT, ___GFP_FS_BIT, ___GFP_ZERO_BIT, ___GFP_UNUSED_BIT, /* 0x200u unused */ ___GFP_DIRECT_RECLAIM_BIT, ___GFP_KSWAPD_RECLAIM_BIT, ___GFP_WRITE_BIT, ___GFP_NOWARN_BIT, ___GFP_RETRY_MAYFAIL_BIT, ___GFP_NOFAIL_BIT, ___GFP_NORETRY_BIT, ___GFP_MEMALLOC_BIT, ___GFP_COMP_BIT, ___GFP_NOMEMALLOC_BIT, ___GFP_HARDWALL_BIT, ___GFP_THISNODE_BIT, ___GFP_ACCOUNT_BIT, ___GFP_ZEROTAGS_BIT, #ifdef CONFIG_KASAN_HW_TAGS ___GFP_SKIP_ZERO_BIT, ___GFP_SKIP_KASAN_BIT, #endif #ifdef CONFIG_LOCKDEP ___GFP_NOLOCKDEP_BIT, #endif #ifdef CONFIG_SLAB_OBJ_EXT ___GFP_NO_OBJ_EXT_BIT, #endif ___GFP_LAST_BIT };
所有这些gfp_t类型的值,有些是zone修饰符,有些是内存分配行为,比如___GFP_DIRECT_RECLAIM_BIT允许触发直接内存回收,后面的内存管理分析的系列文章还会遇到这些标志,再在具体的代码上下文分析更为形象。
继续分析gfp_zone函数:
z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & ((1 << GFP_ZONES_SHIFT) - 1);
将GFP_ZONE_TABLE右移bit * GFP_ZONES_SHIFT,这正是前面定义GFP_ZONE_TABLE时,存放在bit这种标志组合索引处的zone序号,它用来优先为bit这种gfp标志组合分配内存,注意是以2个比特为一个条目,所以最后做与运算,相当于只取最后两位。
回到policy_nodemask函数的第三种MPOL_BIND的情况,apply_policy_zone是说要将内存策略应用在较高点的区域,这个条件是本case下设置nodemask的必要条件,另一个必要条件是内存策略的pol->nodes要和当前进程允许的节点mask有交集(也就是task_struct:mems_allowed)。当然MPOL_BIND下也要将内存策略设置的优先node号带出并返回。
第四种case MPOL_INTERLEAVE:
根据前面的分析,这种情况就是内存分配在一组节点内交错循环分配:
case MPOL_INTERLEAVE: /* Override input node id */ *nid = (ilx == NO_INTERLEAVE_INDEX) ? interleave_nodes(pol) : interleave_nid(pol, ilx); break;
可以看到,分两种情况选择使用不同的函数来获得内存分配的优先节点,NO_INTERLEAVE_INDEX主要指示了要不要使用task_struct:il_prev来作为索引选择内存分配的节点,如果使用task_struct:il_prev(也就是ilx参数为-1)则通过interleave_nodes函数来获得内存分配的节点:
static unsigned int interleave_nodes(struct mempolicy *policy) { unsigned int nid; unsigned int cpuset_mems_cookie; /* to prevent miscount, use tsk->mems_allowed_seq to detect rebind */ do { cpuset_mems_cookie = read_mems_allowed_begin(); nid = next_node_in(current->il_prev, policy->nodes); } while (read_mems_allowed_retry(cpuset_mems_cookie)); if (nid < MAX_NUMNODES) current->il_prev = nid; return nid; }
do-while循环主要是处理并发情况,此处不讨论了。该函数主要使用next_node_in来获得合适的节点号,也就是在pol->nodes中找到第一个置位的bit的序号,并且是在current->il_prev之后,返回的nid小于MAX_NUMNODES时才会给出nid给调用者。
这里需要简单看下next_node_in的实现,该函数主要是要处理一种临界情况,那就是返回的node id的序号等于MAX_NUMNODES,就又要回到参数srcp的第一个置位的bit的序号:
#define next_node_in(n, src) __next_node_in((n), &(src)) static __always_inline unsigned int __next_node_in(int node, const nodemask_t *srcp) { unsigned int ret = __next_node(node, srcp); if (ret == MAX_NUMNODES) ret = __first_node(srcp); return ret; }
如果不使用task_struct:il_prev,就会调用interleave_nid来获得内存分配的节点号:
static unsigned int interleave_nid(struct mempolicy *pol, pgoff_t ilx) { nodemask_t nodemask; unsigned int target, nnodes; int i; int nid; nnodes = read_once_policy_nodemask(pol, &nodemask); if (!nnodes) return numa_node_id(); target = ilx % nnodes; nid = first_node(nodemask); for (i = 0; i < target; i++) nid = next_node(nid, nodemask); return nid; }
该函数以传入的ilx作为索引来选择内存分配的节点,首先ilx需要对pol->nodes里置位的节点数目进行取模得到target,然后通过first_node获得nodemask里第一个置位的节点号nid,最后从nid开始循环target次,找到低target个置位的比特的序号,返回。
第五种case MPOL_WEIGHTED_INTERLEAVE:
该种情况与case 4类似:
case MPOL_WEIGHTED_INTERLEAVE: *nid = (ilx == NO_INTERLEAVE_INDEX) ? weighted_interleave_nodes(pol) : weighted_interleave_nid(pol, ilx); break;
依据ilx参数的不同,而调用不同的函数确定nid并返回。当使用task_struct:il_prev时(也就是ilx无效为-1)调用weighted_interleave_nodes来确定分配内存的节点号,当ilx有效时,就会调用weighted_interleave_nid使用ilx来确定,下面依次分析。
首先是weighted_interleave_nodes函数:
static unsigned int weighted_interleave_nodes(struct mempolicy *policy) { unsigned int node; unsigned int cpuset_mems_cookie; retry: /* to prevent miscount use tsk->mems_allowed_seq to detect rebind */ cpuset_mems_cookie = read_mems_allowed_begin(); node = current->il_prev; if (!current->il_weight || !node_isset(node, policy->nodes)) { node = next_node_in(node, policy->nodes); if (read_mems_allowed_retry(cpuset_mems_cookie)) goto retry; if (node == MAX_NUMNODES) return node; current->il_prev = node; current->il_weight = get_il_weight(node); } current->il_weight--; return node; }
先抛开if条件里的逻辑不谈,每次进入weighted_interleave_nodes函数,就会返回current这个task_struct结构体里的il_prev,它就是记录了当前用于内存分配的节点号,每分配一次,current->il_weight就会递减,这相当于在il_prev这个内存节点上做了权重,分配il_weight次后就会将这个权重递减为1了。
现在分析if里的逻辑,进入这个if的条件是il_weight递减到0,或者前次使用的内存节点已经不在policy里允许的nodes时。
if里的逻辑主要是更新下一个使用的节点,由next_node_in获得,同时current:il_weight也要更新为当前这个节点的权重,这可以通过get_il_weight去获得:
static u8 get_il_weight(int node) { u8 *table; u8 weight; rcu_read_lock(); table = rcu_dereference(iw_table); /* if no iw_table, use system default */ weight = table ? table[node] : 1; /* if value in iw_table is 0, use system default */ weight = weight ? weight : 1; rcu_read_unlock(); return weight; }
从这个函数可以知道weight其实就是来自iw_table里对应节点的权重,它事先通过node_store函数去存放。总结来说,这个函数的功能就是达到前面介绍interleave分配的效果:在一组节点中交错分配内存,并且会按照一定的权重来分配,比如这里体现这点的就是每次进这个函数都会将current->il_weight权重递减,为非零本次分配内存的节点号不会改变,而当这个值递减为0,就会改变分配内存的节点号。
当ilx有效时,是weighted_interleave_nid函数用来确定分配内存的节点:
static unsigned int weighted_interleave_nid(struct mempolicy *pol, pgoff_t ilx) { nodemask_t nodemask; unsigned int target, nr_nodes; u8 *table; unsigned int weight_total = 0; u8 weight; int nid; nr_nodes = read_once_policy_nodemask(pol, &nodemask); if (!nr_nodes) return numa_node_id(); rcu_read_lock(); table = rcu_dereference(iw_table); /* calculate the total weight */ for_each_node_mask(nid, nodemask) { /* detect system default usage */ weight = table ? table[nid] : 1; weight = weight ? weight : 1; weight_total += weight; } /* Calculate the node offset based on totals */ target = ilx % weight_total; nid = first_node(nodemask); while (target) { /* detect system default usage */ weight = table ? table[nid] : 1; weight = weight ? weight : 1; if (target < weight) break; target -= weight; nid = next_node_in(nid, nodemask); } rcu_read_unlock(); return nid; }
该函数首先通过一个循环for_each_node_mask将pol->nodes里置上的节点的所有权重求和,然后ilx可以作为一个虚拟地址,其要对weight_total进行取余,这样ilx就会落在权重区间[0, weight_total]。 而后面的while循环实现了将余数target按权重落到相应的区间,其实现方式就是每次只要当前剩余target还不小于当前节点的权重,就会将当前target减去当前节点的权重。举个例子,系统有三个节点,权重依次为3,2,1那么中的权重为6,ilx作为虚拟地址比如可以为0-5,那么这六个地址按照比例一定是有3/6,2/6,1/6的概率分别在第一、二以及三个节点上完成内存分配请求,至于大于5的地址对weight_total取余数后一样符合这个加权概率。
最后这个policy_nodemask函数就是返回前面各个case算出的nodemask,注意nodemask是可以为NULL的,而nid通过最后一个参数带出:
return nodemask;
这样alloc_pages_mpol_noprof函数的第一部分policy_nodemask函数就介绍完了。
2.2. 第二部分
下面介绍alloc_pages_mpol_noprof函数的第二部分,本部分依据不同的情况而调用不同的分配函数。
情形一MPOL_PREFERRED_MANY
调用alloc_pages_preferred_many函数:
if (pol->mode == MPOL_PREFERRED_MANY) return alloc_pages_preferred_many(gfp, order, nid, nodemask);
static struct page *alloc_pages_preferred_many(gfp_t gfp, unsigned int order, int nid, nodemask_t *nodemask) { struct page *page; gfp_t preferred_gfp; /* * This is a two pass approach. The first pass will only try the * preferred nodes but skip the direct reclaim and allow the * allocation to fail, while the second pass will try all the * nodes in system. */ preferred_gfp = gfp | __GFP_NOWARN; preferred_gfp &= ~(__GFP_DIRECT_RECLAIM | __GFP_NOFAIL); page = __alloc_pages_noprof(preferred_gfp, order, nid, nodemask); if (!page) page = __alloc_pages_noprof(gfp, order, nid, NULL); return page; }
可以看到该种情形通过alloc_pages_preferred_many实现两阶段的页面分配,第一阶段先尝试在nodemask中指定的节点中分配内存,并且不打印分配失败相关的一些信息,也不允许进入直接页面回收, 如果这种分配方式可以成功分配出页面,就返回页面了。否则就会以NULL参数作为nodemask再次调用__alloc_pages_noprof。所以这里可以看到对于指定在某些节点分配内存的方式,先是尽力在这些节点上分配,分配失败时还是会尝试整个系统的节点都可以分配,这是一种回退,尽量保证分配成功有内存可用的方式。
情形二
该情形实际是对大页内存分配的一个优化,即保证页面在一个指定的固定节点进行分配:
if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) && /* filter "hugepage" allocation, unless from alloc_pages() */ order == HPAGE_PMD_ORDER && ilx != NO_INTERLEAVE_INDEX) { /* * For hugepage allocation and non-interleave policy which * allows the current node (or other explicitly preferred * node) we only try to allocate from the current/preferred * node and don't fall back to other nodes, as the cost of * remote accesses would likely offset THP benefits. * * If the policy is interleave or does not allow the current * node in its nodemask, we allocate the standard way. */ if (pol->mode != MPOL_INTERLEAVE && pol->mode != MPOL_WEIGHTED_INTERLEAVE && (!nodemask || node_isset(nid, *nodemask))) { /* * First, try to allocate THP only on local node, but * don't reclaim unnecessarily, just compact. */ page = __alloc_pages_node_noprof(nid, gfp | __GFP_THISNODE | __GFP_NORETRY, order); if (page || !(gfp & __GFP_DIRECT_RECLAIM)) return page; /* * If hugepage allocations are configured to always * synchronous compact or the vma has been madvised * to prefer hugepage backing, retry allowing remote * memory with both reclaim and compact as well. */ } }
首先判断是开启了透明大页并且分配的页面大小为2MB(笔者的环境amd64,HPAGE_PMD_ORDER = 21-12= 9,即分配2^9个页面,2MB大小的空间),而随后的条件ilx != NO_INTERLEAVE_INDEX、pol->mode != MPOL_INTERLEAVE以及pol->mode != MPOL_WEIGHTED_INTERLEAVE都是过滤掉交错分配的情况,因为交错 分配的本质要求就是要不固定节点分配,这和这个优化的要求:固定节点分配内存,是相矛盾的,最后 一个条件如果nodemask非空,就要求首选内存节点nid在这个nodemask中。这些条件任一不满足都会走 情形三的标准方式去分配内存。
__GFP_THISNODE指明内存分配应该在nid指明的节点上固定分配,调用者保证未来的内存访问来自nid的cpu,然后内存也是在nid节点分配的,这样本地内存访问的性能优势就得以体现。
情形三
该种情形其实没有太多分析的了,就是以前面算好的nodemask和nid,上层函数过来的gfp与order调用__alloc_pages_noprof函数即可:
page = __alloc_pages_noprof(gfp, order, nid, nodemask);
2.3. 第三部分
这部分代码主要是对numa页面的命中情况进行统计,分配出的页面如果确实和要求的nid一致,则增加zone:per_cpu_zonestats:vm_numa_event相应命中事件的计数:
if (unlikely(pol->mode == MPOL_INTERLEAVE) && page) { /* skip NUMA_INTERLEAVE_HIT update if numa stats is disabled */ if (static_branch_likely(&vm_numa_stat_key) && page_to_nid(page) == nid) { preempt_disable(); __count_numa_event(page_zone(page), NUMA_INTERLEAVE_HIT); preempt_enable(); } }
注意这种更新只会对MPOL_INTERLEAVE策略有效,另外numa stat统计信息也是可以不使能的。下面看下__count_numa_event函数:
static inline void __count_numa_event(struct zone *zone, enum numa_stat_item item) { struct per_cpu_zonestat __percpu *pzstats = zone->per_cpu_zonestats; raw_cpu_inc(pzstats->vm_numa_event[item]); }
可以清楚的看到,其就是增加了zone里percpu类型的变量per_cpu_zonestats的vm_numa_event数组里对应事件的计数。
这里需要分析下page_to_nid和page_zone这两个函数,这在现代常见的NUMA或SMP架构里是十分常见的。首先是page_to_nid,定义在mm.h里:
#ifdef NODE_NOT_IN_PAGE_FLAGS int page_to_nid(const struct page *page); #else static inline int page_to_nid(const struct page *page) { return (PF_POISONED_CHECK(page)->flags >> NODES_PGSHIFT) & NODES_MASK; } #endif
可以看到依据NODE_NOT_IN_PAGE_FLAGS宏是否定义而分两种版本的实现,如果它定义了,page_to_nid函数实现在sparse.c文件中,就不实现后面的版本了。这个宏其实区分了node id是否要放置在page:flags成员中,因为这个成员其实就是个unsigned long,也就是64个比特位,有可能是没有足够空间来容纳node id的:
#if NODES_SHIFT != 0 && NODES_WIDTH == 0 #define NODE_NOT_IN_PAGE_FLAGS 1 #endif
要想NODE_NOT_IN_PAGE_FLAGS有定义,那么NODES_SHIFT要非0,而NODES_WIDTH为0:
#ifdef CONFIG_NODES_SHIFT #define NODES_SHIFT CONFIG_NODES_SHIFT #else #define NODES_SHIFT 0 #endif
现代NUMA架构里一般CONFIG_NODES_SHIFT都是有值的,比如为10或3,这在前面的CONFIG_NODES_SHIFT有介绍,也就是说第一个条件一般是满足的。
下面就是考虑NODES_WIDTH的值:
#if ZONES_WIDTH + LRU_GEN_WIDTH + SECTIONS_WIDTH + NODES_SHIFT \ <= BITS_PER_LONG - NR_PAGEFLAGS #define NODES_WIDTH NODES_SHIFT #elif defined(CONFIG_SPARSEMEM_VMEMMAP) #error "Vmemmap: No space for nodes field in page flags" #else #define NODES_WIDTH 0 #endif
这里NR_PAGEFLAGS是用于表示页面状态(比如PG_locked、PG_dirty以及PG_uptodate等)占用多少比特位,这会在编译时确定,它其实就是__NR_PAGEFLAGS,在笔者的笔记本上它就是24。而在64位架构下BITS_PER_LONG一般就是64位。
ZONES_WIDTH定义如下:
#if MAX_NR_ZONES < 2 #define ZONES_SHIFT 0 #elif MAX_NR_ZONES <= 2 #define ZONES_SHIFT 1 #elif MAX_NR_ZONES <= 4 #define ZONES_SHIFT 2 #elif MAX_NR_ZONES <= 8 #define ZONES_SHIFT 3 #else #error ZONES_SHIFT "Too many zones configured" #endif #define ZONES_WIDTH ZONES_SHIFT
MAX_NR_ZONES其实就是__MAX_NR_ZONES,在笔者的环境它被定义为5,所以ZONES_SHIFT就是3,ZONES_WIDTH亦为3。LRU_GEN_WIDTH定义为3,LRU的一种优化实现会用到,参见内核文档multigen_lru.rst。
对于SECTIONS_WIDTH定义如下:
#if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP) #define SECTIONS_WIDTH SECTIONS_SHIFT #else #define SECTIONS_WIDTH 0 #endif
#ifdef CONFIG_SPARSEMEM #include <asm/sparsemem.h> #define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS) #else #define SECTIONS_SHIFT 0 #endif
CONFIG_SPARSEMEM用于支持物理地址内存布局不连续或稀疏分布的系统,物理地址空间可能存在多个“空洞”(没有内存的区域),这样的系统包括NUMA系统(多节点非均匀内存访问),高端服务器或具有大地址空间的系统。将物理内存按大块划分为多个section(默认大小如128MB,具体取决于SECTION_SIZE_BITS)。每个section的元数据存储在struct mem_section中,用来描述该section的物理内存状态。如果某个section内有物理内存,则分配struct page元数据;否则不分配,节省内存。通过CONFIG_SPARSEMEM,内核能够按section大粒度来管理物理内存,而不是逐页操作,从而减少元数据开销。与稀疏内存模型相对的是平坦(FLATMEM)内存模型,适用于物理内存布局连续且紧凑的系统,例如嵌入式设备或简单的单处理器系统。内存布局是线性的,没有大的稀疏区域。
CONFIG_SPARSEMEM这种实现需要明确知道section的数量,而section的数量(SECTIONS_WIDTH占用多少比特位)由SECTIONS_SHIFT决定,依赖物理地址空间和section大小。因此,在这种模式下:SECTIONS_WIDTH = SECTIONS_SHIFT,用于指定section的数量以及在数据结构中进行索引的宽度。
现代系统一般都是既配置了CONFIG_SPARSEMEM,也配置了CONFIG_SPARSEMEM_VMEMMAP(比如笔者的环境),所以SECTIONS_WIDTH为0。
那么在笔者的环境下就是:(3 + 3 + 0 + 10 = 16) <= (64 - 24 = 40)。也就是NODES_WIDTH有值为10,node id就在page:flags中。那么page_to_nid的实现就容易理解了,先将flags右移NODES_PGSHIFT位,这会保证node id所占的比特对齐到最低位,再与上NODES_MASK,只取node id所占的所有bit:
#define NODES_MASK ((1UL << NODES_WIDTH) - 1) #define NODES_PGSHIFT (NODES_PGOFF * (NODES_WIDTH != 0)) #define SECTIONS_PGOFF ((sizeof(unsigned long)*8) - SECTIONS_WIDTH) #define NODES_PGOFF (SECTIONS_PGOFF - NODES_WIDTH)
再用下图总结下page:flags的布局:
| [SECTION] | [NODE] | ZONE | [LAST_CPUPID] | ... | FLAGS |
可以看到,只有ZONE和FLAGS是必须的。并且从这个结构里也可以看出内存的一种层次架构,section里分多少node,节点里又分多少zone。
后面再来理解page_zone就简单了,它通过zone type返回当前节点对应的zone结构体:
static inline struct zone *page_zone(const struct page *page) { return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)]; }
page_to_nid前面已经介绍了,page_zonenum的实现类似:
static inline enum zone_type page_zonenum(const struct page *page) { ASSERT_EXCLUSIVE_BITS(page->flags, ZONES_MASK << ZONES_PGSHIFT); return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK; }
也是先将flags右移zone id所占的那些bit到最低位,然后和ZONES_MASK相与只取zone id所占的比特,另外pglist_data里node_zones成员表示这个节点所有的zone。注意这里有个ASSERT_EXCLUSIVE_BITS宏,它的主要作用是保证没有并发的写发生在(ZONES_MASK << ZONES_PGSHIFT)里设置的比特位上,其具体实现原理可以参考kcsan-checks.h。
到这里终于分析完了alloc_pages_mpol_noprof函数的三个部分,后面继续分析alloc_pages_mpol_noprof函数调用的一个关键函数:__alloc_pages_noprof。
3. __alloc_pages_noprof
原型:
struct page *__alloc_pages_noprof(gfp_t gfp, unsigned int order, int preferred_nid, nodemask_t *nodemask)
作用:
分配2^order个页面,preferred_nid是首选节点,nodemask是备用的节点mask,可以从里面选择。
详细分析:
从这个函数开始就进入伙伴系统的代码(page_alloc.c)了,之前一直是在NUMA内存分配策略的实现上(mempolicy.c)。该函数也分为三个部分来分析:
- 第一部分是确定alloc_context结构体里成员的值。
- 第二部分是真正的分配页面部分,包括快速和慢速路径,本文主要关心快速路径。
- 第三部分是依据条件更新统计信息到mem cgroup中。
下面逐一分析。
3.1. 第一部分
struct alloc_context { struct zonelist *zonelist; nodemask_t *nodemask; struct zoneref *preferred_zoneref; int migratetype; /* * highest_zoneidx represents highest usable zone index of * the allocation request. Due to the nature of the zone, * memory on lower zone than the highest_zoneidx will be * protected by lowmem_reserve[highest_zoneidx]. * * highest_zoneidx is also used by reclaim/compaction to limit * the target zone since higher zone than this index cannot be * usable for this allocation request. */ enum zone_type highest_zoneidx; bool spread_dirty_pages; };
它用于保存一些在内核内存管理内部使用的分配参数。nodemask、migratetype以及highest_zoneidx在分配流程中一旦初始化就不会改变。zonelist、preferred_zone以及highest_zoneidx第一次在快速路径中设置后,可能在后面的慢速路径中发生改变,可能会回退到其它zone中分配。
这里着重解释下highest_zoneidx参数,它通过gfp_zone函数获得,为zone的编号,就是一个zone_type枚举类型值。每个zone里都有一个lowmem_reserve成员:
long lowmem_reserve[MAX_NR_ZONES];
另外由于highest_zoneidx也限制了内存分配最高可用的zone,那么在慢速路径中针对高于highest_zoneidx的zone就没有必要做回收或压缩了,因为即使回收/压缩出空闲内存了也会因为内存位于高于highest_zoneidx的zone而不会使用,这相当于是内存回收/压缩的一种优化。
第一部分主要就是确定alloc_context参数:
struct page *page; unsigned int alloc_flags = ALLOC_WMARK_LOW; gfp_t alloc_gfp; /* The gfp_t that was actually used for allocation */ struct alloc_context ac = { }; /* * There are several places where we assume that the order value is sane * so bail out early if the request is out of bound. */ if (WARN_ON_ONCE_GFP(order > MAX_PAGE_ORDER, gfp)) return NULL; gfp &= gfp_allowed_mask; /* * Apply scoped allocation constraints. This is mainly about GFP_NOFS * resp. GFP_NOIO which has to be inherited for all allocation requests * from a particular context which has been marked by * memalloc_no{fs,io}_{save,restore}. And PF_MEMALLOC_PIN which ensures * movable zones are not used during allocation. */ gfp = current_gfp_context(gfp); alloc_gfp = gfp; if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac, &alloc_gfp, &alloc_flags)) return NULL; /* * Forbid the first pass from falling back to types that fragment * memory until all local zones are considered. */ alloc_flags |= alloc_flags_nofragment(zonelist_zone(ac.preferred_zoneref), gfp);
ALLOC_WMARK_LOW代表内存的水位线之一,每个zone都有几种水位线,用于控制空闲内存在不同的水位时,在内存回收方面采取不同程度的措施:
struct zone { /* Read-mostly fields */ /* zone watermarks, access with *_wmark_pages(zone) macros */ unsigned long _watermark[NR_WMARK]; ... }; enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, WMARK_PROMO, NR_WMARK }; /* The ALLOC_WMARK bits are used as an index to zone->watermark */ #define ALLOC_WMARK_MIN WMARK_MIN #define ALLOC_WMARK_LOW WMARK_LOW #define ALLOC_WMARK_HIGH WMARK_HIGH #define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */
ALLOC_WMARK_MIN对应的水位线是WMARK_MIN,这是最低的水位线,通常表示内存分配的最低安全阈值。当zone的可用页数低于此值时,分配内存需要特别严格的限制。如果内存低于这个水位线,系统可能会触发直接回收或触发OOM(Out-Of-Memory)杀手。保证系统维持最基本的内存可用性。通常用于高优先级的内存分配,避免耗尽内存。
ALLOC_WMARK_LOW对应的水位线是WMARK_LOW,是比ALLOC_WMARK_MIN高一点的阀值,用于防止内存过度分配导致系统进入紧急回收状态。如果zone的可用页数低于此水位线,但高于WMARK_MIN,后台内存回收机制(如kswapd)会被唤醒以补充内存。
ALLOC_WMARK_HIGH对应的水位线是 WMARK_HIGH,是最高的阈值。这是一个更宽松的水位线,当可用内存高于此值时,分配内存不需要触发任何回收机制。表示内存资源充足,分配可以直接进行。
ALLOC_NO_WATERMARKS不检查任何水位线。此标志用于绕过所有的水位线检查,通常在某些特殊情况下使用,比如高优先级的内存分配或紧急情况下的内存分配(如GFP_ATOMIC分配)。例如,在中断上下文或内存分配必须快速完成时。
alloc_gfp是在传进来的分配标志gfp的基础上在本次分配时还会添加一些标志,是实际用于内存分配的标志。
ac参数在alloc_context介绍过了。
接下来的WARN_ON_ONCE_GFP是当order大于MAX_PAGE_ORDER时打印警告,单次分配的内存,不能做到物理上有大于2^MAX_PAGE_ORDER个页面连续,所以这里做了拦截。这里主要想稍微详细的展开WARN_ON_ONCE_GFP宏:
#define WARN_ON_ONCE_GFP(cond, gfp) ({ \ static bool __section(".data.once") __warned; \ int __ret_warn_once = !!(cond); \ \ if (unlikely(!(gfp & __GFP_NOWARN) && __ret_warn_once && !__warned)) { \ __warned = true; \ WARN_ON(1); \ } \ unlikely(__ret_warn_once); \ })
可以看到,如果gfp里指定了__GFP_NOWARN,那么就不会打印警告,同时这里保证只打印一次的逻辑是在.data.once节里声明一个静态变量__warned,它初始为0,一旦满足条件通过WRAN_ON打印警告,同时全局静态变量__warned被改写为true,那么以后就再也不会打印警告了。
另外有一个相对通用的WARN_ON_ONCE实现。针对x86来说,一般配置了CONFIG_GENERIC_BUG、CONFIG_DEBUG_BUGVERBOSE以及宏__WARN_FLAGS,后者是个架构相关的宏,并且在绝大分架构上都有定义,它支持通过主动产生异常的方式,在异常句柄里去打印warning警告信息,对于x86架构来说就是ud2指令(否则就是warn_slowpath_fmt方式打印):
#define __WARN_FLAGS(flags) \ do { \ __auto_type __flags = BUGFLAG_WARNING|(flags); \ instrumentation_begin(); \ _BUG_FLAGS(ASM_UD2, __flags, ASM_REACHABLE); \ instrumentation_end(); \ } while (0)
这样WARN_ON_ONCE就可以通过__WARN_FLAGS方式实现:
#define WARN_ON_ONCE(condition) ({ \ int __ret_warn_on = !!(condition); \ if (unlikely(__ret_warn_on)) \ __WARN_FLAGS(BUGFLAG_ONCE | \ BUGFLAG_TAINT(TAINT_WARN)); \ unlikely(__ret_warn_on); \ }) #endif
可以看到这里传入了BUGFLAG_ONCE标志,__WARN_FLAGS的实现如下:
#define __WARN_FLAGS(flags) \ do { \ __auto_type __flags = BUGFLAG_WARNING|(flags); \ instrumentation_begin(); \ _BUG_FLAGS(ASM_UD2, __flags, ASM_REACHABLE); \ instrumentation_end(); \ } while (0)
_BUG_FLAGS宏的作用相当于是准备参数并通过ud2指令触发异常,这里就不贴代码了。最后异常的处理会来到__report_bug函数,里面有使用BUGFLAG_ONCE来保证只打印一次:
struct bug_entry *bug; const char *file; unsigned line, warning, once, done; if (!is_valid_bugaddr(bugaddr)) return BUG_TRAP_TYPE_NONE; bug = find_bug(bugaddr); if (!bug) return BUG_TRAP_TYPE_NONE; disable_trace_on_warning(); bug_get_file_line(bug, &file, &line); warning = (bug->flags & BUGFLAG_WARNING) != 0; once = (bug->flags & BUGFLAG_ONCE) != 0; done = (bug->flags & BUGFLAG_DONE) != 0; if (warning && once) { if (done) return BUG_TRAP_TYPE_WARN; /* * Since this is the only store, concurrency is not an issue. */ bug->flags |= BUGFLAG_DONE; }
可以看到,只要设置了BUGFLAG_ONCE和BUGFLAG_WARNING,就是要打印warning且只打印一次的话,就会将BUGFLAG_DONE标志置上给bug->flags。这个bug类型是bug_entry,它是从bug table里通过bugaddr作为索引找到的:
struct bug_entry *find_bug(unsigned long bugaddr) { struct bug_entry *bug; for (bug = __start___bug_table; bug < __stop___bug_table; ++bug) if (bugaddr == bug_addr(bug)) return bug; return module_find_bug(bugaddr); }
而bug_addr就是在_BUG_FLAGS宏里当作参数传入的,它一般就是WARN_ON_ONCE调用处。另外如果从内核的bug table里找不到,还可以从module的bug table里去找。
而一旦打印一次给bug->flags置上BUGFLAG_DONE后,下次再进入这个逻辑,就会满足if (done)的逻辑 直接返回而不进行后面真正的打印动作了。
这就是WARN_ON_ONCE实现只打印一次的方法,它主要是利用了bug_entry来保存打印情况,而WARN_ON_ONCE_GFP采取了另一个不同的办法来实现这种只打印一次的逻辑:在.data.section里声明一个静态变量__warned记录打印情况。
关于WARN/bug handle的更多细节实现,可以参考笔者其它文章。
回到__alloc_pages_noprof函数的第一部分继续看代码:
gfp &= gfp_allowed_mask; /* * Apply scoped allocation constraints. This is mainly about GFP_NOFS * resp. GFP_NOIO which has to be inherited for all allocation requests * from a particular context which has been marked by * memalloc_no{fs,io}_{save,restore}. And PF_MEMALLOC_PIN which ensures * movable zones are not used during allocation. */ gfp = current_gfp_context(gfp); alloc_gfp = gfp;
这一部分对本次内存分配实际使用的gfp再次进行了一些调整并给到alloc_gfp。gfp_allowed_mask主要是用于解决启动早期不能使用一些gfp标志的问题,它初始定义如下:
#define GFP_BOOT_MASK (__GFP_BITS_MASK & ~(__GFP_RECLAIM|__GFP_IO|__GFP_FS)) gfp_t gfp_allowed_mask __read_mostly = GFP_BOOT_MASK;
可以看到初始的时候,清除了这三个标志,意味着在启动阶段,这三个标志涉及到的功能内核还没有准备好,比如说磁盘IO或者文件操作还没有就绪。而在kernel_init_freeable函数里会解开这些限制:
static noinline void __init kernel_init_freeable(void) { /* Now the scheduler is fully set up and can do blocking allocations */ gfp_allowed_mask = __GFP_BITS_MASK; ... }
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))
__GFP_BITS_SHIFT就是最后一个gfp标志所占的比特,也就是所有gfp标志都可以使用了。
current_gfp_context主要是结合上Per-Process Flags,进程级别也可以设置gfp标志,存放于task_struct:flags域,比如进程可以设置PF_MEMALLOC_PIN标志,页面可以pin住,这时gfp flags需要清除__GFP_MOVABLE标志,也就是分配的页面是不能移动的了:
static inline gfp_t current_gfp_context(gfp_t flags) { unsigned int pflags = READ_ONCE(current->flags); if (unlikely(pflags & (PF_MEMALLOC_NOIO | PF_MEMALLOC_NOFS | PF_MEMALLOC_PIN))) { /* * NOIO implies both NOIO and NOFS and it is a weaker context * so always make sure it makes precedence */ if (pflags & PF_MEMALLOC_NOIO) flags &= ~(__GFP_IO | __GFP_FS); else if (pflags & PF_MEMALLOC_NOFS) flags &= ~__GFP_FS; if (pflags & PF_MEMALLOC_PIN) flags &= ~__GFP_MOVABLE; } return flags; }
接下来继续分析prepare_alloc_pages函数,该函数最终敲定alloc_context参数,较为关键:
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_gfp, unsigned int *alloc_flags) { ac->highest_zoneidx = gfp_zone(gfp_mask); ac->zonelist = node_zonelist(preferred_nid, gfp_mask); ac->nodemask = nodemask; ac->migratetype = gfp_migratetype(gfp_mask); if (cpusets_enabled()) { *alloc_gfp |= __GFP_HARDWALL; /* * When we are in the interrupt context, it is irrelevant * to the current task context. It means that any node ok. */ if (in_task() && !ac->nodemask) ac->nodemask = &cpuset_current_mems_allowed; else *alloc_flags |= ALLOC_CPUSET; } might_alloc(gfp_mask); if (should_fail_alloc_page(gfp_mask, order)) return false; *alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, *alloc_flags); /* Dirty zone balancing only done in the fast path */ ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE); /* * The preferred zone is used for statistics but crucially it is * also used as the starting point for the zonelist iterator. It * may get reset for allocations that ignore memory policies. */ ac->preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->highest_zoneidx, ac->nodemask); return true; }
第一行确定最高可以分配的zone_type(idx),通过gfp_zone得到,该函数(宏)在前面已经详细介绍了。 第二行代码比较关键:
ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
这里就要引入内核内存管理的一个关键数据结构了:zonelist。在NUMA系统里每个内存节点由pglist_data数据结构表示,该结构体里有一个zonelist类型的数组成员名为node_zonelists:
typedef struct pglist_data { ...; /* * node_zonelists contains references to all zones in all nodes. * Generally the first zones will be references to this node's * node_zones. */ struct zonelist node_zonelists[MAX_ZONELISTS]; ... } pg_data_t;
/* * One allocation request operates on a zonelist. A zonelist * is a list of zones, the first one is the 'goal' of the * allocation, the other zones are fallback zones, in decreasing * priority. * * To speed the reading of the zonelist, the zonerefs contain the zone index * of the entry being read. Helper functions to access information given * a struct zoneref are * * zonelist_zone() - Return the struct zone * for an entry in _zonerefs * zonelist_zone_idx() - Return the index of the zone for an entry * zonelist_node_idx() - Return the index of the node for an entry */ struct zonelist { struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; };
/* * This struct contains information about a zone in a zonelist. It is stored * here to avoid dereferences into large structures and lookups of tables */ struct zoneref { struct zone *zone; /* Pointer to actual zone */ int zone_idx; /* zone_idx(zoneref->zone) */ };
/* Maximum number of zones on a zonelist */ #define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
从上面这些代码就可以看出引入zonelist的缘由了,一个zonelist包含所有节点上的所有zone,一次分配就在一个zonelist上去操作(也就是所有的zone都是有可能去分配的)。zonelist里的zone按顺序依次向后具有较低的优先级用于分配内存,而排在最前面的zone就是最优先的zone要从里面分配内存。从MAX_ZONES_PER_ZONELIST宏的定义可以看出来所有内存其实就是按一种层次化组织,对于多节点的NUMA架构来说,就是分若干节点,每个节点又分若干zone。另外对于NUMA架构来说,node_zonelists成员实际分两条,一条是可以fallback的:ZONELIST_FALLBACK,也就是这里面有所有节点的所有zone,另外一条是:ZONELIST_NOFALLBACK,是内存分配标志gfp在指明了__GFP_THISNODE不允许回退时使用的zonelist,也就是只能从指定节点的zone里分配内存。
zoneref是具体存在一个zonelist里元素,它主要是加速访问zonelist,缓存下zone不用经常查阅zonelist这个表。
有了这些背景知识再来理解node_zonelist的实现就简单了,gfp_zonelist主要是看gfp标志里是否设置了__GFP_THISNODE依据不同的情况返回是fallback还是nofallback的zonelist索引:
static inline int gfp_zonelist(gfp_t flags) { #ifdef CONFIG_NUMA if (unlikely(flags & __GFP_THISNODE)) return ZONELIST_NOFALLBACK; #endif return ZONELIST_FALLBACK; }
返回的索引再加上node_zonelists就得到目标zonelist了,并存放到alloc_context的zonelist成员,后续会用来索引目标zone。
在继续分析prepare_alloc_pages的代码前,这里想简单的介绍下zonelist的初始化,详细的分析可以参见读者另外的文章。
对于多节点NUMA架构来说主要通过如下流程去初始化zonelist:
__build_all_zonelists->build_zonelists->build_zonelists_in_node_order->build_zonerefs_node->zoneref_set_zone
__build_all_zonelists里有如下代码循环针对所有的node去构建zonelist:
for_each_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); }
build_zonelists里有关键代码如下:
while ((node = find_next_best_node(local_node, &used_mask)) >= 0) { /* * We don't want to pressure a particular node. * So adding penalty to the first node in same * distance group to make it round-robin. */ if (node_distance(local_node, node) != node_distance(local_node, prev_node)) node_load[node] += 1; node_order[nr_nodes++] = node; prev_node = node; } build_zonelists_in_node_order(pgdat, node_order, nr_nodes);
就是说通过node_distance确定的节点间的距离,生成了一个node_order数组,这个数组里的元素是node的标号,数组里越靠后的元素表示的节点距离当前处理的节点越远,node_distance可以根据下层硬件的实际拓扑通过ACPI表传上来,这方面的细节参见笔者其它文章。node_order生成好了之后,它又作为参数调用了build_zonelists_in_node_order函数:
/* * Build zonelists ordered by node and zones within node. * This results in maximum locality--normal zone overflows into local * DMA zone, if any--but risks exhausting DMA zone. */ static void build_zonelists_in_node_order(pg_data_t *pgdat, int *node_order, unsigned nr_nodes) { struct zoneref *zonerefs; int i; zonerefs = pgdat->node_zonelists[ZONELIST_FALLBACK]._zonerefs; for (i = 0; i < nr_nodes; i++) { int nr_zones; pg_data_t *node = NODE_DATA(node_order[i]); nr_zones = build_zonerefs_node(node, zonerefs); zonerefs += nr_zones; } zonerefs->zone = NULL; zonerefs->zone_idx = 0; }
从这个函数可以看出填充zonerefs的一个顺序:就是按照node_order里指明的节点依次取出node作为参数,调用build_zonerefs_node,这样自然的效果就是距离当前处理节点(需要填充当前节点的node_zonelists)越近的节点,其里面的zone(s)排在zoneref(zonelist:_zonerefs)数组元素的前面。有个小细节提下,最后一个zonerefs的zone为NULL,它会作为first_zones_zonelist函数没有找到符合条件的zone时的返回值。
这里再稍微详细看下build_zonerefs_node函数的实现:
static int build_zonerefs_node(pg_data_t *pgdat, struct zoneref *zonerefs) { struct zone *zone; enum zone_type zone_type = MAX_NR_ZONES; int nr_zones = 0; do { zone_type--; zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zoneref_set_zone(zone, &zonerefs[nr_zones++]); check_highest_zone(zone_type); } } while (zone_type); return nr_zones; }
static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref) { zoneref->zone = zone; zoneref->zone_idx = zone_idx(zone); }
/* * zone_idx() returns 0 for the ZONE_DMA zone, 1 for the ZONE_NORMAL zone, etc. */ #define zone_idx(zone) ((zone) - (zone)->zone_pgdat->node_zones)
从zone_idx的实现知道,zone_type里的枚举类型也就是表示了zone的编号,比如ZONE_DMA的idx为0,ZONE_DMA32为1,依次类推。分析build_zonerefs_node函数的循环体可以知道,zonerefs数组里的元素,靠前的元素存放的zone其idx越大,因为zone_type的初始值为MAX_NR_ZONES,从较大的zone idx开始,针对某个具体的配置,这最终导致一个效果可能如下:
zone_type idx zonerefs[0] ZONE_MOVABLE 3 zonerefs[1] ZONE_NORMAL 2 zonerefs[2] ZONE_DMA32 1 zonerefs[3] ZONE_DMA 0
最后pgdat->node_zones成员表示了仅属于当前节点的所有zone,所以拿它作为基址偏移zone_type就可以得到相应zone_type的zone结构体了。
这里之所以需要详细理解这一点,因为后面的分配需要确定alloc_context: preferred_zoneref。
更多关于zone初始化的细节本文不再赘述了,参见笔者其它文章分析。
由上面初始化zonelist的简单分析也就可以更加深刻的理解前面代码里对zonelist的注释了:
the first one is the 'goal' of the allocation, the other zones are fallback zones, in decreasing priority.
回到prepare_alloc_pages继续分析,gfp_migratetype主要是用来从gfp标志中提取出页面的迁移类型,一种是可移动__GFP_MOVABLE,一种是可回收__GFP_RECLAIMABLE:
/* Convert GFP flags to their corresponding migrate type */ #define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) #define GFP_MOVABLE_SHIFT 3 static inline int gfp_migratetype(const gfp_t gfp_flags) { VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); BUILD_BUG_ON((___GFP_RECLAIMABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_RECLAIMABLE); BUILD_BUG_ON(((___GFP_MOVABLE | ___GFP_RECLAIMABLE) >> GFP_MOVABLE_SHIFT) != MIGRATE_HIGHATOMIC); if (unlikely(page_group_by_mobility_disabled)) return MIGRATE_UNMOVABLE; /* Group based on mobility */ return (__force unsigned long)(gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; } #undef GFP_MOVABLE_MASK #undef GFP_MOVABLE_SHIFT
这个函数前面先进行了一些检查,比如第一行的检查代表__GFP_RECLAIMABLE和__GFP_MOVABLE不能同时被设置,其它行实际是检查在这个enum中,满足第3位是___GFP_MOVABLE_BIT,第四位是___GFP_RECLAIMABLE_BIT,从0开始计数。
接下来的代码主要是处理cpuset内存资源组的限制,这种限制主要针对进程上下文并且没有设置nodemask的时候,cpuset_current_mems_allowed在设置了CONFIG_CPUSETS后,就是当前进程(current)的mems_allowed成员限制的node。
if (cpusets_enabled()) { *alloc_gfp |= __GFP_HARDWALL; /* * When we are in the interrupt context, it is irrelevant * to the current task context. It means that any node ok. */ if (in_task() && !ac->nodemask) ac->nodemask = &cpuset_current_mems_allowed; else *alloc_flags |= ALLOC_CPUSET; }
在should_fail_alloc_page主要用于处理错误注入。
__GFP_WRITE代表内存的申请者将会写页面,这样内存分配的时候就会尽量将其分散在不同zone中:
/* Dirty zone balancing only done in the fast path */ ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
/* * The preferred zone is used for statistics but crucially it is * also used as the starting point for the zonelist iterator. It * may get reset for allocations that ignore memory policies. */ ac->preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->highest_zoneidx, ac->nodemask);
通过first_zones_zonelist去获得目标zone,后面将使用这个目标zone去分配内存,ac->zonelist前面分析过了如何得到。获得的目标zone其序号应当是小于或等于第二个参数ac->highest_zoneidx的,如果第三个参数ac->nodemask有值,还应当保证分出来的zone在ac->nodemask指明的节点里,下面是其实现:
/** * first_zones_zonelist - Returns the first zone at or below highest_zoneidx within the allowed nodemask in a zonelist * @zonelist: The zonelist to search for a suitable zone * @highest_zoneidx: The zone index of the highest zone to return * @nodes: An optional nodemask to filter the zonelist with * * This function returns the first zone at or below a given zone index that is * within the allowed nodemask. The zoneref returned is a cursor that can be * used to iterate the zonelist with next_zones_zonelist by advancing it by * one before calling. * * When no eligible zone is found, zoneref->zone is NULL (zoneref itself is * never NULL). This may happen either genuinely, or due to concurrent nodemask * update due to cpuset modification. * * Return: Zoneref pointer for the first suitable zone found */ static inline struct zoneref *first_zones_zonelist(struct zonelist *zonelist, enum zone_type highest_zoneidx, nodemask_t *nodes) { return next_zones_zonelist(zonelist->_zonerefs, highest_zoneidx, nodes); }
/** * next_zones_zonelist - Returns the next zone at or below highest_zoneidx within the allowed nodemask using a cursor within a zonelist as a starting point * @z: The cursor used as a starting point for the search * @highest_zoneidx: The zone index of the highest zone to return * @nodes: An optional nodemask to filter the zonelist with * * This function returns the next zone at or below a given zone index that is * within the allowed nodemask using a cursor as the starting point for the * search. The zoneref returned is a cursor that represents the current zone * being examined. It should be advanced by one before calling * next_zones_zonelist again. * * Return: the next zone at or below highest_zoneidx within the allowed * nodemask using a cursor within a zonelist as a starting point */ static __always_inline struct zoneref *next_zones_zonelist(struct zoneref *z, enum zone_type highest_zoneidx, nodemask_t *nodes) { if (likely(!nodes && zonelist_zone_idx(z) <= highest_zoneidx)) return z; return __next_zones_zonelist(z, highest_zoneidx, nodes); }
可以看到一般最优可能的就是首选传进来的zone就满足条件了,也就是_zonerefs的第一个元素就满足条件,前面有介绍过,_zonerefs中排在前面的zone具有较大的zoneidx,这里也可以看到这些具有较大zoneidx的zone优先用于分配内存。如果不满足条件通过__next_zones_zonelist继续获取目标zone。在first_zones_zonelist里使用_zonerefs作为迭代寻找合适zoneref的开始位置。这里也可以看到要想往前递进zone,其实就是下次传入第一个参数z往前自加一就行,继续看__next_zones_zonelist函数:
/* Returns the next zone at or below highest_zoneidx in a zonelist */ struct zoneref *__next_zones_zonelist(struct zoneref *z, enum zone_type highest_zoneidx, nodemask_t *nodes) { /* * Find the next suitable zone to use for the allocation. * Only filter based on nodemask if it's set */ if (unlikely(nodes == NULL)) while (zonelist_zone_idx(z) > highest_zoneidx) z++; else while (zonelist_zone_idx(z) > highest_zoneidx || (zonelist_zone(z) && !zref_in_nodemask(z, nodes))) z++; return z; }
在这里看到了z自增的语句,假如没有给定目标节点的限制,那么就只需要循环到zoneref:zone_idx小于或等于highest_zoneidx就行。如果限定了node,那么还需要满足zone:node指明的节点号在参数nodes中被设置才行。这里zonelist里的zoneref有所有节点的所有zone,循环里是可能都用到的一直寻找到一个满足条件的zone。
下面接续分析:
/* * Forbid the first pass from falling back to types that fragment * memory until all local zones are considered. */ alloc_flags |= alloc_flags_nofragment(zonelist_zone(ac.preferred_zoneref), gfp);
这段代码主要就是禁止在第一次分配时造成内存碎片,细节不赘述了,比较简单。
到目前为止__alloc_pages_noprof函数的第一部分就介绍完了,它主要是确定分配用的参数结构体alloc_context。
3.2. 第二部分
第二部分主要就是分配页面的动作,这主要分两个路径,一是快速路径通过get_page_from_freelist,二是慢速路径通过__alloc_pages_slowpath,本文主要介绍快速路径的流程,慢速路径参见作者其它文章。当快速路径不能分配页面时,就会走慢速路径了:
/* First allocation attempt */ page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac); if (likely(page)) goto out; alloc_gfp = gfp; ac.spread_dirty_pages = false; /* * Restore the original nodemask if it was potentially replaced with * &cpuset_current_mems_allowed to optimize the fast-path attempt. */ ac.nodemask = nodemask; page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
可以看到当快速路径不能分配页面时,就会调整alloc_gfp,因为alloc_gfp在快速路径分配页面时可能施加了额外的限制,比如前面分析过的current_gfp_context里可能施加不能进行IO或文件系统相关的 操作,在prepare_alloc_pages里施加的__GFP_HARDWALL标志以严格遵循cpuset的策略,这些限制在慢速路径时都会解除,以提高内存分配的成功率。
另外spread_dirty_pages也是作为一种分散脏页面的限制在慢速路径也会解开。此外用于分配内存的ac->nodemask在prepare_alloc_pages里也可能因为cpuset内存组限制为cpuset_current_mems_allowed,在慢速路径时这种限制也没有了,用于分配内存的节点来自传进来的参数就可以了。
后面的节还会详细介绍快速路径的下一个核心函数get_page_from_freelist。
4. get_page_from_freelist
该函数会进一步进行快速页面的分配流程,也是分三个部分来分析这个函数。
- 第一部分是要从某个zone中分配内存前都需要进行的公共检测。
- 第二部分是检查水印并采取措施。
- 第三部分是进一步调用rmqueue函数去获取页面。
4.1. 第一部分
这里首先介绍一个宏循环,get_page_from_freelist函数的函数体基本都是在这个宏循环里完成,这个宏循环就是for_next_zone_zonelist_nodemask:
#define for_next_zone_zonelist_nodemask(zone, z, highidx, nodemask) \ for (zone = zonelist_zone(z); \ zone; \ z = next_zones_zonelist(++z, highidx, nodemask), \ zone = zonelist_zone(z))
这个宏传入四个参数,其中第一个参数zone是zone类型的指针,它会随着循环的进行而更新,get_page_from_freelist主要的逻辑就可以使用这个更新了的zone,循环退出的条件也就是zone为NULL时。zone通过zonelist_zone获得:
static inline struct zone *zonelist_zone(struct zoneref *zoneref) { return zoneref->zone; }
next_zones_zonelist函数前面已经分析过了。继续往下分析第一部分的代码:
struct zoneref *z; struct zone *zone; struct pglist_data *last_pgdat = NULL; bool last_pgdat_dirty_ok = false; bool no_fallback; retry: /* * Scan zonelist, looking for a zone with enough free. * See also cpuset_node_allowed() comment in kernel/cgroup/cpuset.c. */ 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;
static inline bool __cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask) { return cpuset_node_allowed(zone_to_nid(z), gfp_mask); }
bool cpuset_node_allowed(int node, gfp_t gfp_mask) { struct cpuset *cs; /* current cpuset ancestors */ bool allowed; /* is allocation in zone z allowed? */ unsigned long flags; if (in_interrupt()) return true; if (node_isset(node, current->mems_allowed)) return true; /* * Allow tasks that have access to memory reserves because they have * been OOM killed to get memory anywhere. */ if (unlikely(tsk_is_oom_victim(current))) return true; if (gfp_mask & __GFP_HARDWALL) /* If hardwall request, stop here */ return false; if (current->flags & PF_EXITING) /* Let dying task have memory */ return true; /* Not hardwall and node outside mems_allowed: scan up cpusets */ spin_lock_irqsave(&callback_lock, flags); rcu_read_lock(); cs = nearest_hardwall_ancestor(task_cs(current)); allowed = node_isset(node, cs->mems_allowed); rcu_read_unlock(); spin_unlock_irqrestore(&callback_lock, flags); return allowed; }
首先通过zone_to_nid去获得zone所在的节点号:
static inline int zone_to_nid(struct zone *zone) { return zone->node; }
zone的node成员表示了zone所在的节点号,在初始化的时候通过zone_set_nid去设置:
static inline void zone_set_nid(struct zone *zone, int nid) { zone->node = nid; }
可以看到cpuset_node_allowed函数是这段代码的主体逻辑。通过in_terrupt判断出当前是在中断上下文的话返回true,代表任何节点分配内存都可以,中断上下文使用内存主要满足可用性,所以所有节点都可以。另外zone所在的节点在当前进程设置的mems_allowed节点里时也返回true,这是优先判断的。如果当前进程是oom选中的victim进程时,也返回true进行内存分配,因为victim进程很快会被杀死而释放出更多的内存。
再往后判断gfp标志是否设置了__GFP_HARDWALL,这个标志被设置,就要求内存必须来自current->mems_allowed,但是前面的node_isset并没有返回,那这说明不能满足__GFP_HARDWALL的语义了,返回false,表示不能在当前的内存节点(zone所在的结点)进行内存分配。再往后正在退出的进程的情况,也是返回true可以在当前zone分配内存,因为它退出了自然就有内存了。
最后的情况才是检查cpuset内存资源组的设置,关于cpuset参见笔者其它文章。这里只是简单提一下,引用cgroup sub sys时通过的id是通过##拼起来的,比如如下使用的cpuset_cgrp_id:
/* Retrieve the cpuset for a task */ static inline struct cpuset *task_cs(struct task_struct *task) { return css_cs(task_css(task, cpuset_cgrp_id)); }
其拼接代码如下位置定义:
./include/linux/cgroup-defs.h:42:#define SUBSYS(_x) _x ## _cgrp_id
继续看get_page_from_freelist函数:
if (ac->spread_dirty_pages) { if (last_pgdat != zone->zone_pgdat) { last_pgdat = zone->zone_pgdat; last_pgdat_dirty_ok = node_dirty_ok(zone->zone_pgdat); } if (!last_pgdat_dirty_ok) continue; }
这段代码主要针对分配的页面用于page cache即将写时,需要做写脏页平衡。实际上就是当gfp_mask设置了__GFP_WRITE时,ac->spread_dirty_pages就会被设置。这里的逻辑就是当循环遍历到zone所在的节点(zone->zone_pgdat)发生变化时,和前一个分配内存的节点不一样时,对于新的这个用于分配内存的节点(zone->zone_pgdat),需要调用node_dirty_ok判断其是否已经超过了脏页写数量的限制,那么往下分析下这个函数的实现:
/** * node_dirty_ok - tells whether a node is within its dirty limits * @pgdat: the node to check * * Return: %true when the dirty pages in @pgdat are within the node's * dirty limit, %false if the limit is exceeded. */ bool node_dirty_ok(struct pglist_data *pgdat) { unsigned long limit = node_dirty_limit(pgdat); unsigned long nr_pages = 0; nr_pages += node_page_state(pgdat, NR_FILE_DIRTY); nr_pages += node_page_state(pgdat, NR_WRITEBACK); return nr_pages <= limit; }
该函数先通过node_dirty_limit函数获得当前节点可以写的最大脏页数量limit,然后又通过node_page_state来获得脏页和回写页的数量nr_pages,当它小于limit证明还有页面可以用于写,返回true。
/** * node_dirty_limit - maximum number of dirty pages allowed in a node * @pgdat: the node * * Return: the maximum number of dirty pages allowed in a node, based * on the node's dirtyable memory. */ static unsigned long node_dirty_limit(struct pglist_data *pgdat) { unsigned long node_memory = node_dirtyable_memory(pgdat); struct task_struct *tsk = current; unsigned long dirty; if (vm_dirty_bytes) dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE) * node_memory / global_dirtyable_memory(); else dirty = vm_dirty_ratio * node_memory / 100; if (rt_or_dl_task(tsk)) dirty += dirty / 4; /* * Dirty throttling logic assumes the limits in page units fit into * 32-bits. This gives 16TB dirty limits max which is hopefully enough. */ return min_t(unsigned long, dirty, UINT_MAX); }
在Linux系统中,内存写操作先写入缓存页,稍后再由内核的写回机制将数据从内存写到磁盘。这些缓存页称为脏页(dirty pages)。为了避免内存中脏页过多影响系统性能,内核通过vm.dirty_bytes或vm.dirty_ratio参数来限制脏页的数量或比例。这两参数可以通过以下两个命令去查看:
sysctl vm.dirty_bytes sysctl vm.dirty_ratio
在node_dirty_limit函数中可以看到,如果设置了vm_dirty_bytes这种绝对值指明了可以多少内存用于脏页缓存,那么当前节点会按其所有的内存node_memory占整个可脏写内存的比例去分担vm_dirty_bytes。否则,就是当前节点可写内存的20%用于写脏页缓存。对于用rt_or_dl_task判断出来当前进程是实时或者deadline进程的话,还可以在算出来的可脏写大小的基础上再增加25%的容量。这里想继续简单分析下rt_or_dl_task,因为这方面涉及到进程优先级的划分:
#define MAX_NICE 19 #define MIN_NICE -20 #define NICE_WIDTH (MAX_NICE - MIN_NICE + 1) /* * Priority of a process goes from 0..MAX_PRIO-1, valid RT * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority * values are inverted: lower p->prio value means higher priority. */ #define MAX_RT_PRIO 100 #define MAX_DL_PRIO 0 #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH) #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) /* * Convert user-nice values [ -20 ... 0 ... 19 ] * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ], * and back. */ #define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO) #define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO) static inline bool rt_or_dl_prio(int prio) { return unlikely(prio < MAX_RT_PRIO); } /* * Returns true if a task has a priority that belongs to RT or DL classes. * PI-boosted tasks will return true. Use rt_or_dl_task_policy() to ignore * PI-boosted tasks. */ static inline bool rt_or_dl_task(struct task_struct *p) { return rt_or_dl_prio(p->prio); }
从这一段代码可以看到,所有进程总的优先级可以从0到MAX_PRIO为140,默认的优先级DEFAULT_PRIO为120,并且优先级是倒转的,task_struct:prio的值越大,其优先级其实越小,task_struct:prio的值越小,其优先级越大。SCHED_NORMAL/SCHED_BATCH类的进程的优先级范围从100到139共四十个级别,这一般也就是用户态进程的级别,0-100是给实时或deadline类进程用的。对于SCHED_NORMAL/SCHED_BATCH的进程类来说,其还有nice值的概念,范围为[-20, 19],它和优先级之间可以通过NICE_TO_PRIO/PRIO_TO_NICE这样的宏进行转换。
回到node_dirty_limit函数,往下分析下怎样获取一个节点的可脏内存给node_memory量:
static unsigned long node_dirtyable_memory(struct pglist_data *pgdat) { unsigned long nr_pages = 0; int z; for (z = 0; z < MAX_NR_ZONES; z++) { struct zone *zone = pgdat->node_zones + z; if (!populated_zone(zone)) continue; nr_pages += zone_page_state(zone, NR_FREE_PAGES); } /* * Pages reserved for the kernel should not be considered * dirtyable, to prevent a situation where reclaim has to * clean pages in order to balance the zones. */ nr_pages -= min(nr_pages, pgdat->totalreserve_pages); nr_pages += node_page_state(pgdat, NR_INACTIVE_FILE); nr_pages += node_page_state(pgdat, NR_ACTIVE_FILE); return nr_pages; }
从这个node_dirtyable_memory函数的逻辑可以看到一个节点可以用于page cache的页面包括NR_FREE_PAGES空闲页面,以及NR_INACTIVE_FILE、NR_ACTIVE_FILE这种可以回收的页面,但是它们的和只是作为一个base value,node_dirty_limit函数的逻辑前面介绍了还要进行比例计算,并且这个比例用户态是可配的。
base value还要减去保留的页面数量totalreserve_pages,
该函数依次遍历节点里所有的zone,对于已经populated了的zone,都需要加上里面空闲页面的数量。前面也有提到过,pglist_data:node_zones就是包含属于本节点的所有zone,所以遍历基地址从它开始。那么什么叫populated了的zone呢:
/* Returns true if a zone has memory */ static inline bool populated_zone(struct zone *zone) { return zone->present_pages; }
zone里有几个表示各种类型页面数量的变量:managed_pages、spanned_pages以及present_pages。一般来说spanned_pages >= present_pages >= managed_pages,spanned_pages是zone所跨过的所有页面,包括内存空洞:
spanned_pages = zone_end_pfn - zone_start_pfn
而present_pages是在spanned_pages中除去空洞的地址范围,managed_pages是在present_pages中由buddy system管理的页面,就是managed_pages = present_pages - reserved_pages。
static inline unsigned long zone_page_state(struct zone *zone, enum zone_stat_item item) { long x = atomic_long_read(&zone->vm_stat[item]); #ifdef CONFIG_SMP if (x < 0) x = 0; #endif return x; }
每个zone都有一个atomic_long_t类型的数组vm_stat,它的大小是NR_VM_ZONE_STAT_ITEMS,统计了zone里各种页面状态的数量,比如空闲页面数量放在vm_stat的第零位(NR_FREE_PAGES)。
回到node_dirty_limit函数再看看它调用的global_dirtyable_memory:
/** * global_dirtyable_memory - number of globally dirtyable pages * * Return: the global number of pages potentially available for dirty * page cache. This is the base value for the global dirty limits. */ static unsigned long global_dirtyable_memory(void) { unsigned long x; x = global_zone_page_state(NR_FREE_PAGES); /* * Pages reserved for the kernel should not be considered * dirtyable, to prevent a situation where reclaim has to * clean pages in order to balance the zones. */ x -= min(x, totalreserve_pages); x += global_node_page_state(NR_INACTIVE_FILE); x += global_node_page_state(NR_ACTIVE_FILE); if (!vm_highmem_is_dirtyable) x -= highmem_dirtyable_memory(x); return x + 1; /* Ensure that we never return 0 */ }
这个函数和node_dirtyable_memory的逻辑类似,只是global_node_page_state读的是vm_node_stat这个全局统计量。node_dirty_ok函数就介绍完了,回到get_page_from_freelist函数继续分析,如果node_dirty_ok返回false,意味着当前zone对应的node已经达到了最高的脏页限制,跳过到下一个zone继续。
继续往下分析get_page_from_freelist函数:
if (no_fallback && nr_online_nodes > 1 && zone != zonelist_zone(ac->preferred_zoneref)) { int local_nid; /* * If moving to a remote node, retry but allow * fragmenting fallbacks. Locality is more important * than fragmentation avoidance. */ local_nid = zonelist_node_idx(ac->preferred_zoneref); if (zone_to_nid(zone) != local_nid) { alloc_flags &= ~ALLOC_NOFRAGMENT; goto retry; } }
这段代码的作用是,当前可能要切换节点分配内存了,但是还可以在清除ALLOC_NOFRAGMENT的情况下再尝试一次本地节点分配内存,因为本地性比碎片化更重要,本地性的内存访问性能较好。注意这里比较的基准是和preferred_zoneref这个zone所在的节点进行比较。goto retry实际上就是到整个for_next_zone_zonelist_nodemask循环重新开始一次了,只不过这次就没有ALLOC_NOFRAGMENT约束了。
再往下看第一部分的代码:
cond_accept_memory(zone, order);
对于Intel TDX这样的虚拟化平台,在虚拟机能够使用内存前必须先accepted它,这种机制可以阻止恶意的主机修改虚拟机的内存:
static bool cond_accept_memory(struct zone *zone, unsigned int order) { long to_accept; bool ret = false; if (!has_unaccepted_memory()) return false; if (list_empty(&zone->unaccepted_pages)) return false; /* How much to accept to get to promo watermark? */ to_accept = promo_wmark_pages(zone) - (zone_page_state(zone, NR_FREE_PAGES) - __zone_watermark_unusable_free(zone, order, 0) - zone_page_state(zone, NR_UNACCEPTED)); while (to_accept > 0) { if (!try_to_accept_memory_one(zone)) break; ret = true; to_accept -= MAX_ORDER_NR_PAGES; } return ret; }
该函数首先通过has_unaccepted_memory检测下是否还有unaccepted的内存,如果没有可以accepted的内存就直接返回false了。has_unaccepted_memory主要检测zone_with_unaccepted_pages这个量,它会在__free_unaccepted中增加,同时page:lru也会在这个函数中通过list_add_tail挂到zone:unaccepted_pages链表,以备后面可以accept这些内存。一旦accepted内存了,就可以在__accept_page中去减少zone_with_unaccepted_pages了。
如果有内存可以accept,则这些页面是挂在zone:unaccepted_pages链表上的,只有这个链表非空的时候才往下进行accepted内存的过程。
再往下的逻辑是计算需要accept多少内存可以达到promo watermark,首先计算promo watermark在哪个水位:
static inline unsigned long wmark_pages(const struct zone *z, enum zone_watermarks w) { return z->_watermark[w] + z->watermark_boost; } static inline unsigned long promo_wmark_pages(const struct zone *z) { return wmark_pages(z, WMARK_PROMO); }
可以看到这个promo watermark就是WMARK_PROMO水位再加上watermark_boot临时提高的水位。WMARK_PROMO是比较高的水位:
enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, WMARK_PROMO, NR_WMARK };
WMARK_MIN水位最低。
计算还需要accept多少内存其实就是看现在的空闲内存离promo_wmark_pages算出的水印还差多少内存。但是通过zone_page_state得到的空闲内存还需要排除一些内存再来看离目标内存水印位多远,比如对于__zone_watermark_unusable_free函数:
static inline long __zone_watermark_unusable_free(struct zone *z, unsigned int order, unsigned int alloc_flags) { long unusable_free = (1 << order) - 1; /* * If the caller does not have rights to reserves below the min * watermark then subtract the free pages reserved for highatomic. */ if (likely(!(alloc_flags & ALLOC_RESERVES))) unusable_free += READ_ONCE(z->nr_free_highatomic); #ifdef CONFIG_CMA /* If allocation can't use CMA areas don't use free CMA pages */ if (!(alloc_flags & ALLOC_CMA)) unusable_free += zone_page_state(z, NR_FREE_CMA_PAGES); #endif return unusable_free; }
一开始的(1 << order) - 1是处理对齐需要多少页面,然后要是调用者没有权限去分配预留的内存,那么不可用内存还需要加上nr_free_highatomic。允许分配预留内存(也就是允许分配低于WMARK_MIN水印的内存)的情况一共有四种:
/* Flags that allow allocations below the min watermark. */ #define ALLOC_RESERVES (ALLOC_NON_BLOCK|ALLOC_MIN_RESERVE|ALLOC_HIGHATOMIC|ALLOC_OOM)
计算出了需要accepted内存的量to_accept就在循环里通过try_to_accept_memory_one去接受内存了,只要有一次接受成功都会返回true,每次接受内存的order都是最大order值。具体的try_to_accept_memory_one函数参见作者其它文章分析,概况的说这个过程最终会调用free_one_page释放页面到伙伴系统。
回到get_page_from_freelist的第一部分继续分析:
/* * Detect whether the number of free pages is below high * watermark. If so, we will decrease pcp->high and free * PCP pages in free path to reduce the possibility of * premature page reclaiming. Detection is done here to * avoid to do that in hotter free path. */ if (test_bit(ZONE_BELOW_HIGH, &zone->flags)) goto check_alloc_wmark; mark = high_wmark_pages(zone); if (zone_watermark_fast(zone, order, mark, ac->highest_zoneidx, alloc_flags, gfp_mask)) goto try_this_zone; else set_bit(ZONE_BELOW_HIGH, &zone->flags);
这段代码主要是检查zone:flags是否设置了ZONE_BELOW_HIGH,这个水印表示zone的内存在高水位内存下。这时就需要进入check_alloc_wmark标签也就是第二部分进行内存回收了,通过high_wmark_pages去获得当前zone对应的高水位内存,注意这个都是要算上watermark_boost的:
static inline unsigned long wmark_pages(const struct zone *z, enum zone_watermarks w) { return z->_watermark[w] + z->watermark_boost; } static inline unsigned long high_wmark_pages(const struct zone *z) { return wmark_pages(z, WMARK_HIGH); }
zone:flags还可能有以下一些值:
enum zone_flags { ZONE_BOOSTED_WATERMARK, /* zone recently boosted watermarks. * Cleared when kswapd is woken. */ ZONE_RECLAIM_ACTIVE, /* kswapd may be scanning the zone. */ ZONE_BELOW_HIGH, /* zone is below high watermark. */ };
对于当前zone里的空想内存是否能达到mark这个内存水位,通过zone_watermark_fast来计算,如果达到就直接进入第三部分try_this_zone进行内存分配了。否则要设置当前当前zone:flags的ZONE_BELOW_HIGH,并进入到check_alloc_wmark也就是第二部分代码去回收内存,回收到足够的内存就可以到try_this_zone继续内存分配了。否则就要跳过这个zone,下次再遇到这个zone,若zone:flags还设置为ZONE_BELOW_HIGH,那么还要继续走第二部分代码check_alloc_wmark去回收内存,直到在free_unref_page_commit中调用clear_bit去清除这个bit。
下面继续详细分析下zone_watermark_fast函数:
static inline bool zone_watermark_fast(struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags, gfp_t gfp_mask) { long free_pages; free_pages = zone_page_state(z, NR_FREE_PAGES); /* * Fast check for order-0 only. If this fails then the reserves * need to be calculated. */ if (!order) { long usable_free; long reserved; usable_free = free_pages; reserved = __zone_watermark_unusable_free(z, 0, alloc_flags); /* reserved may over estimate high-atomic reserves. */ usable_free -= min(usable_free, reserved); if (usable_free > mark + z->lowmem_reserve[highest_zoneidx]) return true; } if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags, free_pages)) return true; /* * Ignore watermark boosting for __GFP_HIGH order-0 allocations * when checking the min watermark. The min watermark is the * point where boosting is ignored so that kswapd is woken up * when below the low watermark. */ if (unlikely(!order && (alloc_flags & ALLOC_MIN_RESERVE) && z->watermark_boost && ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) { mark = z->_watermark[WMARK_MIN]; return __zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags, free_pages); } return false; }
zone_page_state前面已经介绍过了,就是获得zone里空闲的页面有多少。然后的逻辑是对order为0也就是只分配一个页面的情况进行特殊处理,这可以理解为一种优化。在 if (!order)
的逻辑里,首
先通过__zone_watermark_unusable_free去获得不可用内存,然后现在剩余可用的内存free_pages里还要减去reserved,并且减去后需要大于当前传进来的水印与lowmem_reserve里保护内存的和,现在这个
水印前面介绍过就是WMARK_HIGH高水位。
如果order大于0,或者说order为0,但是没有返回true,都会进入到__zone_watermark_ok再次尝试:
bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags, long free_pages) { long min = mark; int o; /* free_pages may go negative - that's OK */ free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags); if (unlikely(alloc_flags & ALLOC_RESERVES)) { /* * __GFP_HIGH allows access to 50% of the min reserve as well * as OOM. */ if (alloc_flags & ALLOC_MIN_RESERVE) { min -= min / 2; /* * Non-blocking allocations (e.g. GFP_ATOMIC) can * access more reserves than just __GFP_HIGH. Other * non-blocking allocations requests such as GFP_NOWAIT * or (GFP_KERNEL & ~__GFP_DIRECT_RECLAIM) do not get * access to the min reserve. */ if (alloc_flags & ALLOC_NON_BLOCK) min -= min / 4; } /* * OOM victims can try even harder than the normal reserve * users on the grounds that it's definitely going to be in * the exit path shortly and free memory. Any allocation it * makes during the free path will be small and short-lived. */ if (alloc_flags & ALLOC_OOM) min -= min / 2; } /* * Check watermarks for an order-0 allocation request. If these * are not met, then a high-order request also cannot go ahead * even if a suitable page happened to be free. */ if (free_pages <= min + z->lowmem_reserve[highest_zoneidx]) return false; /* If this is an order-0 request then the watermark is fine */ if (!order) return true; /* For a high-order request, check at least one suitable page is free */ for (o = order; o < NR_PAGE_ORDERS; o++) { struct free_area *area = &z->free_area[o]; int mt; if (!area->nr_free) continue; for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { if (!free_area_empty(area, mt)) return true; } #ifdef CONFIG_CMA if ((alloc_flags & ALLOC_CMA) && !free_area_empty(area, MIGRATE_CMA)) { return true; } #endif if ((alloc_flags & (ALLOC_HIGHATOMIC|ALLOC_OOM)) && !free_area_empty(area, MIGRATE_HIGHATOMIC)) { return true; } } return false; }
该函数首先还是通过__zone_watermark_unusable_free去获得不可用内存,free_pages要减去它。然后
if (unlikely(alloc_flags & ALLOC_RESERVES))
里的逻辑主要是根据是否设置要使用保留内存标志来调整内存水线,如果可以使用保留内存,那么内存水线还可以往下调整,也就是可以使用更多的内存,不同的标志往下调的幅度不一样。
随后的代码:
if (free_pages <= min + z->lowmem_reserve[highest_zoneidx]) return false; /* If this is an order-0 request then the watermark is fine */ if (!order) return true;
这里其实再一次对order 0的情况进行里检测,前面的条件,其实就是除了高过水线和保留内存,还需要free_pages至少大1,因为order 0就是一个页面,前面的条件过了,并且后面的条件判断出是order0的情况就可以返回true了。
如果不是order 0的高阶内存分配的请求,后面的for循环里,从请求的order开始循环,依次向上到最大NR_PAGE_ORDERS这个order,这里面只要有一个order对应的free_area里有空闲的页面,就可以返回true了。
这里引出了free_area,需要介绍一点背景知识了。
Linux内核内存管理有2^order次方个连续页面形成页块大小这样的概念,一个单页面是内存分配的最小单位,而一个页块是连续的页面集合。NR_PAGE_ORDERS表示支持的页面块(page block)大小的种类, NR_PAGE_ORDERS = MAX_ORDER,通常定义为11。这意味着支持从order-0到order-10的块大小(即2^0到2^10个页面,每个块里面的页面都是连续的)。zone结构体里有一free_area数组,每个元素表示一个特定order的空闲块。每个free_area[order]管理的页面块大小是2^order个页面,注意这并不是说管理的总页面就是2^order个,它只是说一个页面块的大小。
free_area也是一个结构体,里面的nr_free表示当前特定order的页块数量(不是单页面的数量),这表示了当前order里所有的页块个数。每个free_area[order]中有nr_free个页块,每个页块的大小是2^order个页面,所以当前order的free_area总共管理的页面数量可以通过以下公式计算:页面数量 = nr_free * 2^order。
在free_area中还有一个链表数组成员free_list,当前order对应的nr_free这么多个页块又根据内核定义的迁移类型(MIGRATE_TYPES)进一步分类,按不同的类型挂入free_list链表数组中。
有了这些基础再理解上面对于高阶内存分配的循环检测逻辑就容易理解了,free_area_empty函数就是根据传进来的free_area去检查对应参数migratetype的链表是否不为空,不为空则代表可以分出一个页块(其阶大于或等于请求的order),注意这里检测到了MIGRATE_PCPTYPES:
enum migratetype { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA /* * MIGRATE_CMA migration type is designed to mimic the way * ZONE_MOVABLE works. Only movable pages can be allocated * from MIGRATE_CMA pageblocks and page allocator never * implicitly change migration type of MIGRATE_CMA pageblock. * * The way to use it is to change migratetype of a range of * pageblocks to MIGRATE_CMA which can be done by * __free_pageblock_cma() function. */ MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, /* can't allocate from here */ #endif MIGRATE_TYPES };
也就是MIGRATE_UNMOVABLE、MIGRATE_MOVABLE以及MIGRATE_RECLAIMABLE三个链表只要有一个里面有空闲页块就行。
free_area_empty定义如下:
static inline bool free_area_empty(struct free_area *area, int migratetype) { return list_empty(&area->free_list[migratetype]); }
可以看到其实现就是简单的判断链表非空。
回到zone_watermark_fast函数,最后的逻辑是针对order 0的情况并且ALLOC_MIN_RESERVE(__GFP_HIGH)高优先级内存分配,并且watermark_boost有值,且alloc_flags只是WMARK_MIN时,可以使用WMARK_MIN这个最低水位再次调用__zone_watermark_ok尝试去判断是否满足水位分配,相当于再次放宽了条件去检测。
回到get_page_from_freelist函数的第一部分,当zone_watermark_fast返回true时,表示当前内存充足,内存水位高于最高水位线,可以直接可以去往get_page_from_freelist函数的第三部分去分配内存了。否则就将zone->flags置上ZONE_BELOW_HIGH,表示当前zone的水位低于最高水位线,这样在释放页面的路径上就会减少pcp缓存的页面数量以达到最高水位线,另外需要进一步针对当前这次分配使用的水位做检测,去往get_page_from_freelist函数的第二部分。
4.2. 第二部分
进入第二部分代码的前提是当前处理的zone其水位没有达到WMARK_LOW指定的高水位位置。该部分代码的主要逻辑是依据本次分配指定的水位线(之前是直接看高水位),再重新调用zone_watermark_fast来看下水位线是否满足,因为本次分配可能指定了更低的水位线,比如WMARK_LOW或WMARK_MIN,这相当于放松条件再次尝试,如果还是没有满足的话就会尝试一些方法去补充内存,下面详细分析这段代码:
check_alloc_wmark: 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; if (cond_accept_memory(zone, order)) goto try_this_zone; /* * Watermark failed for this zone, but see if we can * grow this zone if it contains deferred pages. */ if (deferred_pages_enabled()) { if (_deferred_grow_zone(zone, order)) goto try_this_zone; } /* Checked here to keep the fast path fast */ BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); if (alloc_flags & ALLOC_NO_WATERMARKS) goto try_this_zone; if (!node_reclaim_enabled() || !zone_allows_reclaim(zonelist_zone(ac->preferred_zoneref), zone)) continue; ret = node_reclaim(zone->zone_pgdat, gfp_mask, order); switch (ret) { case NODE_RECLAIM_NOSCAN: /* did not scan */ continue; case NODE_RECLAIM_FULL: /* scanned but unreclaimable */ continue; default: /* did we reclaim enough */ if (zone_watermark_ok(zone, order, mark, ac->highest_zoneidx, alloc_flags)) goto try_this_zone; continue; } }
wmark_pages函数前面介绍过,只不过这次传入的参数是通过alloc_flags & ALLOC_WMARK_MASK去得到本次的分配水位线:
/* The ALLOC_WMARK bits are used as an index to zone->watermark */ #define ALLOC_WMARK_MIN WMARK_MIN #define ALLOC_WMARK_LOW WMARK_LOW #define ALLOC_WMARK_HIGH WMARK_HIGH #define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */ /* Mask to get the watermark bits */ #define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1)
前次调用使用的是WMARK_HIGH水位,本次可能会有更宽松的水位要求比如WMARK_MIN或WMARK_LOW。如果本次zone_watermark_fast依旧返回false,就会进入第二部分代码的主体了,依据一些条件看能不能尝试一些办法去回收内存。
首先是前面介绍过的cond_accept_memory函数,这里再次尝试看能否接受一些未accept的内存。
下一个方式是依据DEFERRED_STRUCT_PAGE_INIT是否配置,通过_deferred_grow_zone函数去增加可用内存。通常情况下,所有的 struct page 结构会在早期启动阶段由单线程进行初始化。在非常大的机器上,这可能会耗费相当长的时间。如果启用此选项,大型机器将在启动时仅加载一部分内存映射(memmap),然后通过并行的方式初始化剩余部分,相当于是说早期内存启动如果遇到内存不足的请求,还可以试试是不是还有deferred pages可以拿来用。
再往下的检测是如果本次分配可以允许不检查内存水印就直接跳到try_this_zone的第三部分去分配内存了。
最后一部分的逻辑是依据node_reclaim_mode的设置,看要不要通过node_reclaim进行内存节点回收。这需要满足两个条件:
if (!node_reclaim_enabled() || !zone_allows_reclaim(zonelist_zone(ac->preferred_zoneref), zone)) continue;
continue如果被执行了就是跳过当前zone了,所以要想执行到node_reclaim,就必须这两个条件都不满足。node_reclaim_enabled定义如下:
extern int node_reclaim_mode; static inline bool node_reclaim_enabled(void) { /* Is any node_reclaim_mode bit set? */ return node_reclaim_mode & (RECLAIM_ZONE|RECLAIM_WRITE|RECLAIM_UNMAP); }
就是只要node_reclaim_mode设置了任意的位,node_reclaim_enabled都会返回真了,取反当然就是假了。这个node_reclaim_mode支持用户态设置,是提供给用户态的一个接口:
#ifdef CONFIG_NUMA { .procname = "zone_reclaim_mode", .data = &node_reclaim_mode, .maxlen = sizeof(node_reclaim_mode), .mode = 0644, .proc_handler = proc_dointvec_minmax, .extra1 = SYSCTL_ZERO, }, #endif
第二个条件的函数zone_allows_reclaim如下:
int __read_mostly node_reclaim_distance = RECLAIM_DISTANCE; static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone) { return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <= node_reclaim_distance; }
node_distance是计算两个节点间距离的,关于这个函数的细节在笔者其它文章还会有详细分析。这里RECLAIM_DISTANCE被定义成30,也就是要能进行node_reclaim,还需要当前处理zone所在的节点和第一候选ac->preferred_zoneref这个zone所在的节点距离要小于30.
4.3. 第三部分
该部分本身的代码其实比较简单了,就是调用rmqueue去伙伴系统里拿内存,这个函数的分析又开到下一节去介绍了。
第三部分本身的代码如下:
try_this_zone: page = rmqueue(zonelist_zone(ac->preferred_zoneref), zone, order, gfp_mask, alloc_flags, ac->migratetype); if (page) { prep_new_page(page, order, gfp_mask, alloc_flags); /* * If this is a high-order atomic allocation then check * if the pageblock should be reserved for the future */ if (unlikely(alloc_flags & ALLOC_HIGHATOMIC)) reserve_highatomic_pageblock(page, order, zone); return page; } else { if (cond_accept_memory(zone, order)) goto try_this_zone; /* Try again if zone has deferred pages */ if (deferred_pages_enabled()) { if (_deferred_grow_zone(zone, order)) goto try_this_zone; } } } /* * It's possible on a UMA machine to get through all zones that are * fragmented. If avoiding fragmentation, reset and try again. */ if (no_fallback) { alloc_flags &= ~ALLOC_NOFRAGMENT; goto retry; } return NULL; }
rmqueue本节不介绍,假设rmqueue成功返回了page,那么prep_new_page会做一些准备工作:
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 (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order); /* * page is set pfmemalloc when ALLOC_NO_WATERMARKS was necessary to * allocate the page. The expectation is that the caller is taking * steps that will free more memory. The caller should avoid the page * being used for !PFMEMALLOC purposes. */ if (alloc_flags & ALLOC_NO_WATERMARKS) set_page_pfmemalloc(page); else clear_page_pfmemalloc(page); }
先看post_alloc_hook函数:
inline void post_alloc_hook(struct page *page, unsigned int order, gfp_t gfp_flags) { bool init = !want_init_on_free() && want_init_on_alloc(gfp_flags) && !should_skip_init(gfp_flags); bool zero_tags = init && (gfp_flags & __GFP_ZEROTAGS); int i; set_page_private(page, 0); set_page_refcounted(page); arch_alloc_page(page, order); debug_pagealloc_map_pages(page, 1 << order); /* * Page unpoisoning must happen before memory initialization. * Otherwise, the poison pattern will be overwritten for __GFP_ZERO * allocations and the page unpoisoning code will complain. */ kernel_unpoison_pages(page, 1 << order); /* * As memory initialization might be integrated into KASAN, * KASAN unpoisoning and memory initializion code must be * kept together to avoid discrepancies in behavior. */ /* * If memory tags should be zeroed * (which happens only when memory should be initialized as well). */ if (zero_tags) { /* Initialize both memory and memory tags. */ for (i = 0; i != 1 << order; ++i) tag_clear_highpage(page + i); /* Take note that memory was initialized by the loop above. */ init = false; } if (!should_skip_kasan_unpoison(gfp_flags) && kasan_unpoison_pages(page, order, init)) { /* Take note that memory was initialized by KASAN. */ if (kasan_has_integrated_init()) init = false; } else { /* * If memory tags have not been set by KASAN, reset the page * tags to ensure page_address() dereferencing does not fault. */ for (i = 0; i != 1 << order; ++i) page_kasan_tag_reset(page + i); } /* If memory is still not initialized, initialize it now. */ if (init) kernel_init_pages(page, 1 << order); set_page_owner(page, order, gfp_flags); page_table_check_alloc(page, order); pgalloc_tag_add(page, current, 1 << order); }
三个条件共同决定了是否要对分配出来的页面进行初始化:
bool init = !want_init_on_free() && want_init_on_alloc(gfp_flags) && !should_skip_init(gfp_flags);
want_init_on_free实现如下: static inline bool want_init_on_free(void) { return static_branch_maybe(CONFIG_INIT_ON_FREE_DEFAULT_ON, &init_on_free); }
启用init_on_free后,内核会在释放内存时将所有页面分配器和slab分配器的内存清零。这相当于在内核启动参数中添加init_on_free=1,这时默认开启此功能。可以通过在启动参数中设置init_on_free=0来禁用该功能。与init_on_alloc(在分配时清零内存)类似,init_on_free的目的是防止未初始化的堆内存问题。当启用init_on_free时,释放的内存会立刻被清零。这有效防止了多种与未初始化内存相关的漏洞,例如:堆内容泄露:清零后,即使后续程序意外访问到释放的内存,也不会看到之前的内容。内存取证和冷启动攻击:攻击者即使尝试从释放的内存中恢复数据,也无法获取实际内容。启用init_on_free会对性能产生负面影响,主要原因是清零操作会触碰到本来已经“冷”的内存区域,影响缓存效率。性能损耗范围:大多数情况下,性能影响约为3-5%。在某些特定的合成工作负载(如专门测试内存分配和释放的基准测试)中,性能损耗可能高达8%。如果系统需要加强对内存数据的保护(如防止敏感数据泄露或提高系统安全性),可以启用init_on_free。对性能敏感的场景下,则可能需要权衡利弊,选择关闭此功能。在笔者的环境里这个功能是关闭的。
继续看下一个条件:
DECLARE_STATIC_KEY_MAYBE(CONFIG_INIT_ON_ALLOC_DEFAULT_ON, init_on_alloc); #define static_branch_maybe(config, x) \ (IS_ENABLED(config) ? static_branch_likely(x) \ : static_branch_unlikely(x)) static inline bool want_init_on_alloc(gfp_t flags) { if (static_branch_maybe(CONFIG_INIT_ON_ALLOC_DEFAULT_ON, &init_on_alloc)) return true; return flags & __GFP_ZERO; }
这个配置打开时,所有页面分配器出来的页面或是slab分配器得到的内存在分配的时候都会被清零,大多数时候该功能对性能的影响在1%,某些专门合成的测试负载可能会有高达7%的性能损失。
这个配置在笔者的环境是打开的。最后一个条件主要是针对arm64环境的硬件内存标签功能,它依赖于ARMv8.5+提供的Memory Tagging Extension,本文不详细探究,总之第三个条件一般也是true,所以在笔者的环境就是init为true。
再往下set_page_private将page: private成员给赋值成0,因为这个成员可能表示之前使用者有意义的值,比如buffer head可以使用它,也可以表示一个swap_entry_t,当该页面在伙伴系统里,又可以存放页面的order信息,所以在分配完成返回给用户前,先将这个值清0。
set_page_refcounted函数又设置page: _refcount为1。在设置前判断当前页面应该不是tail page,因为tail page不能单独拿出来分配,操作tail page只能通过head page,如果一个页面是tail page,那么page: compound_head的bit 0被设置为1。
再往下是unpoision页面。poision/unposion页面是内核内存管理的一个安全功能,在释放页面时,以特定的毒化模式串去填充页面,而在分配页面时又会去验证是不是这样的毒化模式串,这样如果内存页面被非法的访问修改过,就会在分配页面检测毒化模式串时被发现。这个配置CONFIG_PAGE_POISONING在笔者的环境上是打开的。
kernel_unpoison_pages(page, 1 << order); static inline void kernel_unpoison_pages(struct page *page, int numpages) { if (page_poisoning_enabled_static()) __kernel_unpoison_pages(page, numpages); } void __kernel_unpoison_pages(struct page *page, int n) { int i; for (i = 0; i < n; i++) unpoison_page(page + i); } static void unpoison_page(struct page *page) { void *addr; addr = kmap_local_page(page); kasan_disable_current(); /* * Page poisoning when enabled poisons each and every page * that is freed to buddy. Thus no extra check is done to * see if a page was poisoned. */ check_poison_mem(page, kasan_reset_tag(addr), PAGE_SIZE); kasan_enable_current(); kunmap_local(addr); } static void check_poison_mem(struct page *page, unsigned char *mem, size_t bytes) { static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 10); unsigned char *start; unsigned char *end; start = memchr_inv(mem, PAGE_POISON, bytes); if (!start) return; for (end = mem + bytes - 1; end > start; end--) { if (*end != PAGE_POISON) break; } if (!__ratelimit(&ratelimit)) return; else if (start == end && single_bit_flip(*start, PAGE_POISON)) pr_err("pagealloc: single bit error\n"); else pr_err("pagealloc: memory corruption\n"); print_hex_dump(KERN_ERR, "", DUMP_PREFIX_ADDRESS, 16, 1, start, end - start + 1, 1); dump_stack(); dump_page(page, "pagealloc: corrupted page details"); }
可以看到这里针对每个页面都调用了memchr_inv函数来检查其里的内容是不是毒化模式串PAGE_POISON,它一般是0xaa,memchr_inv的作用就是检查自start起始,如果某个地址不是0xaa,就返回这个串的地 址了,判断完所有内容都是0xaa的话,就返回NULL。
中间关于tag/kasan的处理本文不详细描述了。接下来主要想介绍下set_page_owner函数,尽管CONFIG_PAGE_OWNER在笔者的环境上没有打开,但这个功能是一个用于跟踪页面被哪个进程使用的,对于调试可能有很大帮助。
如果开启了CONFIG_PAGE_OWNER就会定义上page_owner_ops这个操作集。
static struct page_ext_operations *page_ext_ops[] __initdata = { #ifdef CONFIG_PAGE_OWNER &page_owner_ops, #endif ... };
page_owner_ops里的init成员为函数init_page_owner,它会调用:
static_branch_enable(&page_owner_inited);
去使能page_owner_inited。而mm_core_init->page_ext_init->invoke_init_callbacks会对上述的init函数进行调用:
static void __init invoke_init_callbacks(void) { int i; int entries = ARRAY_SIZE(page_ext_ops); for (i = 0; i < entries; i++) { if (page_ext_ops[i]->init) page_ext_ops[i]->init(); } }
这样在打开了配置CONFIG_PAGE_OWNER时,page_owner_inited量就会使能。这样set_page_owner函数就可以调用__set_page_owner函数了:
static inline void set_page_owner(struct page *page, unsigned short order, gfp_t gfp_mask) { if (static_branch_unlikely(&page_owner_inited)) __set_page_owner(page, order, gfp_mask); }
如果开启了CONFIG_PAGE_OWNER,那么每个page关联到一个page_ext结构体,这个page_ext可以解释为一个地址,这样就可以在这个地址存储当前页面的一些额外的信息了,所谓extend就是这个意思。这个配置用于跟踪内核中每个页面的分配者和调用链。如果启用此特性,它会记录每个页面的分配和释放的调用堆栈,从而帮助开发者排查可能的内存泄漏或未释放的页面(例如裸调用 alloc_page(s) 而未释放)。
__set_page_owner函数里,通过page_ext_get->lookup_page_ext去获得当前页面对应的page_ext,所有的page_ext结构体在每个内存节点里有一个开始的基址base,就是pglist_data: node_page_ext成员,所谓获取page对应的page_ext,其实就是看当前页面对应当前节点起始页面的偏移是多少,取出相对于基址base偏移这么多的page_ext就行。而所有page所占的的page_ext结构体的空间,在alloc_node_page_ext函数里初始化的时候通过memblock_alloc_try_node去获得,并给到pglist_data: node_page_ext:
static int __init alloc_node_page_ext(int nid) { struct page_ext *base; unsigned long table_size; unsigned long nr_pages; nr_pages = NODE_DATA(nid)->node_spanned_pages; if (!nr_pages) return 0; /* * Need extra space if node range is not aligned with * MAX_ORDER_NR_PAGES. When page allocator's buddy algorithm * checks buddy's status, range could be out of exact node range. */ if (!IS_ALIGNED(node_start_pfn(nid), MAX_ORDER_NR_PAGES) || !IS_ALIGNED(node_end_pfn(nid), MAX_ORDER_NR_PAGES)) nr_pages += MAX_ORDER_NR_PAGES; table_size = page_ext_size * nr_pages; base = memblock_alloc_try_nid( table_size, PAGE_SIZE, __pa(MAX_DMA_ADDRESS), MEMBLOCK_ALLOC_ACCESSIBLE, nid); if (!base) return -ENOMEM; NODE_DATA(nid)->node_page_ext = base; total_usage += table_size; memmap_boot_pages_add(DIV_ROUND_UP(table_size, PAGE_SIZE)); return 0; }
memblock是内核里相对于伙伴系统的另一个内存管理基址,它在伙伴系统还未就绪的时候使用,关于它的详细分析见笔者另外的文章,总之page_ext所需的空间是在启动时事先通过memblock预分配好的。
有了page_ext,就可以通过get_page_owner去获得page_owner结构体了,page_ext可能不止被page_owner使用,page owner是page_ext的使用者之一(page owner这个功能就作为page ext的client),__set_page_owner->__update_page_owner_handle,在后者这个函数里,先通过get_page_owner将page_ext转换为page_owner结构体,然后就可以往里面存信息了,由于每个页面关联一个page_ext,page_ext又可以关联到page_owner,所以最终其实就记录了每个页面由谁持有,且__set_page_owner->save_stack还保存了调用栈用于分析:
noinline void __set_page_owner(struct page *page, unsigned short order, gfp_t gfp_mask) { struct page_ext *page_ext; u64 ts_nsec = local_clock(); depot_stack_handle_t handle; handle = save_stack(gfp_mask); page_ext = page_ext_get(page); if (unlikely(!page_ext)) return; __update_page_owner_handle(page_ext, handle, order, gfp_mask, -1, ts_nsec, current->pid, current->tgid, current->comm); page_ext_put(page_ext); inc_stack_record_count(handle, gfp_mask, 1 << order); }
static inline void __update_page_owner_handle(struct page_ext *page_ext, depot_stack_handle_t handle, unsigned short order, gfp_t gfp_mask, short last_migrate_reason, u64 ts_nsec, pid_t pid, pid_t tgid, char *comm) { int i; struct page_owner *page_owner; for (i = 0; i < (1 << order); i++) { page_owner = get_page_owner(page_ext); page_owner->handle = handle; page_owner->order = order; page_owner->gfp_mask = gfp_mask; page_owner->last_migrate_reason = last_migrate_reason; page_owner->pid = pid; page_owner->tgid = tgid; page_owner->ts_nsec = ts_nsec; strscpy(page_owner->comm, comm, sizeof(page_owner->comm)); __set_bit(PAGE_EXT_OWNER, &page_ext->flags); __set_bit(PAGE_EXT_OWNER_ALLOCATED, &page_ext->flags); page_ext = page_ext_next(page_ext); } }
这里可以看到各种信息被保存到了page_owner,比较典型的比如调用者的pid。关于page owner和page ext的更多细节可以参考笔者其它文章。到这里post_alloc_hook就介绍的差不多了。回到prep_new_page函数:
if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order);
如果gfp_flags设置了__GFP_COMP,那么就会调用prep_compound_page来处理head/tail page的逻辑。对于高阶的页面分配,可以组织这些连续的多个页面为复合页,高阶分配的第一页,称为head page,并设置了PG_head标志。除了head page以外的页称为tail pages,它们通过page->compound_head指向head page。第一个tail page的->compound_order字段记录了分配的阶数(order)。这意味着只有高阶分配(order > 0)的页才会被标记为compound page。tail page的compound_head的最低位(bit 0)表示PageTail() 状态(即是否是tail page),compound_head的其余部分存储了指向head page的指针。
高阶分配和compound page是有区别的,高阶多页分配是指分配2^order个连续的物理页(即高阶页)。分配高阶页时,是否启用compound page的逻辑,取决于分配时是否指定了__GFP_COMP标志。如果没有指定 __GFP_COMP,这些高阶页仅仅是分配了一块连续的物理内存,而没有特殊的结构或元数据(例如head/tail page)关联这些页。每个页只是独立的,和普通单页没有什么不同,内核无法识别它们属于同一个高阶分配。
void prep_compound_page(struct page *page, unsigned int order) { int i; int nr_pages = 1 << order; __SetPageHead(page); for (i = 1; i < nr_pages; i++) prep_compound_tail(page, i); prep_compound_head(page, order); }
先通过__SetPageHead设置head页面的PG_head标志。然后就是一个for循环开始对后面所有的页设置page.compund_head:
static __always_inline void set_compound_head(struct page *page, struct page *head) { WRITE_ONCE(page->compound_head, (unsigned long)head + 1); } static inline void prep_compound_tail(struct page *head, int tail_idx) { struct page *p = head + tail_idx; p->mapping = TAIL_MAPPING; set_compound_head(p, head); set_page_private(p, 0); }
这里利用了指针对齐到struct page的大小,所以指向page结构体的指针最低位是没有用途的,用来存放信息可以判断该页是否是tail page。
继续往下看pre_new_page:
if (alloc_flags & ALLOC_NO_WATERMARKS) set_page_pfmemalloc(page); else clear_page_pfmemalloc(page);
ALLOC_NO_WATERMARKS标志的目的是在系统内存非常低的情况下,确保关键路径上的内存分配优先级和正确性。表示允许分配内存超出系统设定的正常水位线(watermark),即允许突破最低水位限制。这通常发生在内存非常紧张的情况下,用于确保关键任务(如内核操作或内存回收操作)的内存需求可以被满足。
PFMEMALLOC是一个与页面关联的标志,表示该页面是通过ALLOC_NO_WATERMARKS分配的。分配这样的页面通常预期会被用于释放更多内存(如内存回收操作),而不是普通用途。这段代码判断分配标志是否包含ALLOC_NO_WATERMARKS如果内存分配时指定了ALLOC_NO_WATERMARKS,意味着当前内存极度不足,调用者可能需要确保分配到的页面用于内存回收或类似的关键任务,调用set_page_pfmemalloc(page),为页面设置PFMEMALLOC标志,否则调用clear_page_pfmemalloc(page),清除该标志。
调用者有责任确保标记了PFMEMALLOC的页面不会被用于非关键任务。这样可以避免稀缺的内存资源被滥用,从而帮助系统恢复到正常状态。在内存回收时,当系统内存不足时,内核的回收机制可能需要更多内存来完成清理操作,这些PFMEMALLOC页面可以确保分配到的内存优先用于回收任务。
以下几个接口常用来操作页面的PFMEMALLOC标志:
/* * Only to be called by the page allocator on a freshly allocated * page. */ static inline void set_page_pfmemalloc(struct page *page) { page->lru.next = (void *)BIT(1); } static inline void clear_page_pfmemalloc(struct page *page) { page->lru.next = NULL; } /* * Return true only if the page has been allocated with * ALLOC_NO_WATERMARKS and the low watermark was not * met implying that the system is under some pressure. */ static inline bool page_is_pfmemalloc(const struct page *page) { /* * lru.next has bit 1 set if the page is allocated from the * pfmemalloc reserves. Callers may simply overwrite it if * they do not need to preserve that information. */ return (uintptr_t)page->lru.next & BIT(1); }
可以看到其实际操作的是page:lru:next成员,而不是page:flags。这样prep_new_page函数就分析完了,又回到get_page_from_freelist函数:
/* * If this is a high-order atomic allocation then check * if the pageblock should be reserved for the future */ if (unlikely(alloc_flags & ALLOC_HIGHATOMIC)) reserve_highatomic_pageblock(page, order, zone);
对于高阶原子分配(ALLOC_HIGHATOMIC),需要提前预留一些内存到MIGRATE_HIGHATOMIC类型,可以看到这种情况一般是unlikely,尽管如此还是需要详细分析下,因为这个函数涉及页面类型以及页面迁移的代码,以下是reserve_highatomic_pageblock实现:
/* * Reserve the pageblock(s) surrounding an allocation request for * exclusive use of high-order atomic allocations if there are no * empty page blocks that contain a page with a suitable order */ static void reserve_highatomic_pageblock(struct page *page, int order, struct zone *zone) { int mt; unsigned long max_managed, flags; /* * The number reserved as: minimum is 1 pageblock, maximum is * roughly 1% of a zone. But if 1% of a zone falls below a * pageblock size, then don't reserve any pageblocks. * Check is race-prone but harmless. */ if ((zone_managed_pages(zone) / 100) < pageblock_nr_pages) return; max_managed = ALIGN((zone_managed_pages(zone) / 100), pageblock_nr_pages); if (zone->nr_reserved_highatomic >= max_managed) return; spin_lock_irqsave(&zone->lock, flags); /* Recheck the nr_reserved_highatomic limit under the lock */ if (zone->nr_reserved_highatomic >= max_managed) goto out_unlock; /* Yoink! */ mt = get_pageblock_migratetype(page); /* Only reserve normal pageblocks (i.e., they can merge with others) */ if (!migratetype_is_mergeable(mt)) goto out_unlock; if (order < pageblock_order) { if (move_freepages_block(zone, page, mt, MIGRATE_HIGHATOMIC) == -1) goto out_unlock; zone->nr_reserved_highatomic += pageblock_nr_pages; } else { change_pageblock_range(page, order, MIGRATE_HIGHATOMIC); zone->nr_reserved_highatomic += 1 << order; } out_unlock: spin_unlock_irqrestore(&zone->lock, flags); }
内核的内存管理,又将多个页面划分成了不同的迁移类型,比如MIGRATE_UNMOVABLE: 不可移动页面,MIGRATE_MOVABLE: 可移动页面(如用户空间页面),MIGRATE_RECLAIMABLE: 可回收页面(如文件缓存 页面),MIGRATE_HUGE: 巨页页面以及MIGRATE_CMA: 供连续内存分配器(CMA)使用的页面。通过将页面划分成不同的迁移类型,可以避免内存碎片化。具有相同迁移类型的页面组成在一起可以叫做一个pageblock(页块),这个pageblock具有pageblock_nr_pages个页面。一个页块具有阶pageblock_order,当然就有pageblock_nr_pages = 2^pageblock_order。
有了上面的背景再来分析reserve_highatomic_pageblock函数开头的判断,为high-order atomic分配保留的页面最多就是zone:managed_pages的百分之一。当百分之一的managed_pages还小于pageblock_nr_pages时,就不需要做预留页面了,因为最小也是预留一个pageblock。
在加锁前后都判断了zone->nr_reserved_highatomic >= max_managed,这种编程模式叫做double-checked locking,便于在条件满足时快速返回而不用获取锁。 继续往下看通过get_pageblock_migratetype获得了分配出来的page对应的迁移类型:
#define get_pageblock_migratetype(page) \ get_pfnblock_flags_mask(page, page_to_pfn(page), MIGRATETYPE_MASK)
/** * get_pfnblock_flags_mask - Return the requested group of flags for the pageblock_nr_pages block of pages * @page: The page within the block of interest * @pfn: The target page frame number * @mask: mask of bits that the caller is interested in * * Return: pageblock_bits flags */ unsigned long get_pfnblock_flags_mask(const struct page *page, unsigned long pfn, unsigned long mask) { unsigned long *bitmap; unsigned long bitidx, word_bitidx; unsigned long word; bitmap = get_pageblock_bitmap(page, pfn); bitidx = pfn_to_bitidx(page, pfn); word_bitidx = bitidx / BITS_PER_LONG; bitidx &= (BITS_PER_LONG-1); /* * This races, without locks, with set_pfnblock_flags_mask(). Ensure * a consistent read of the memory array, so that results, even though * racy, are not corrupted. */ word = READ_ONCE(bitmap[word_bitidx]); return (word >> bitidx) & mask; }
这里首先介绍下page_to_pfn这个宏,现代的Linux系统一般都开启了CONFIG_SPARSEMEM_VMEMMAP配置,这个配置优化了page_to_pfn/pfn_to_page的计算效率,这个配置的介绍如下:
SPARSEMEM_VMEMMAP uses a virtually mapped memmap to optimise pfn_to_page and page_to_pfn operations. This is the most efficient option when sufficient kernel resources are available.
开启这个配置时,page_to_pfn定义如下:
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap) #define page_to_pfn __page_to_pfn
从这个定义其实可以看到,vmemmap就存储了第一个page的虚拟地址,这点前面也有介绍过,但是vmemmap的设置没有介绍过,这里一并介绍下。对于x86架构来说,vmemmap的定义如下:
#define vmemmap ((struct page *)VMEMMAP_START)
对于开启了DYNAMIC_MEMORY_LAYOUT配置的情况,VMEMMAP_START定义如下:
# define VMEMMAP_START vmemmap_base
配置DYNAMIC_MEMORY_LAYOUT主要使得像vmemmap这样的地址可以随机化增加安全性,以下是这个配置的介绍:
This option makes base addresses of vmalloc and vmemmap as well as __PAGE_OFFSET movable during boot.
对于x86来说,vmemmap_base一般定义为如下:
unsigned long vmemmap_base __ro_after_init = __VMEMMAP_BASE_L4; EXPORT_SYMBOL(vmemmap_base);
这里可以看到,vmemmap_base使用了__ro_after_init修饰,它被如下定义:
#define __ro_after_init __section(".data..ro_after_init") #define __section(section) __attribute__((__section__(section)))
所以被__ro_after_init修饰的vmemmap_base最终就是放到了.data..ro_after_init这个节里,这是Linux内核划定的一个特殊的节,它的主要作用是在内核初始化完毕后,这个节里的数据不允许再更改,通过如下的调用路径设置了这个节的权限为只读:
start_kernel(line: 1099)->rest_init(callback)->kernel_init->mark_readonly->mark_rodata_ro
上面的流程相对于vmemmap_base被设置的流程是靠后的,也就是说只有被随机化设置后,才能更改.data..ro_after_init为只读。Linux内核开始初始化时,vmemmap_base的初始值就是__VMEMMAP_BASE_L4。在初始化时,配置了DYNAMIC_MEMORY_LAYOUT还会对vmemmap_base进行随机化设置,下面简单分析下这个随机化的过程。首先vmemmap_base的地址给到了kaslr_memory_region的base成员:
static __initdata struct kaslr_memory_region { unsigned long *base; unsigned long *end; unsigned long size_tb; } kaslr_regions[] = { { .base = &page_offset_base, .end = &direct_map_physmem_end, }, { .base = &vmalloc_base, }, { .base = &vmemmap_base, }, };
这里base成员有地址的,在内核启动初始化流程里都会被随机化,其设置是在kernel_randomize_memory函数里:
*kaslr_regions[i].base = vaddr;
该函数的调用按如下路径:
start_kernel(line: 918)->setup_arch->kernel_randomize_memory
可见其随机化设置是早于只读设置的。 介绍完了page_to_pfn的细节,现在分析下MIGRATETYPE_MASK:
#define PB_migratetype_bits 3 #define MIGRATETYPE_MASK ((1UL << PB_migratetype_bits) - 1)
就是描述页面迁移类型的共占3个bit,这里MIGRATETYPE_MASK是指只关心迁移类型的bit,其它bit不关心。迁移有哪些类型在migratetype枚举定义处介绍过。设置页面的迁移类型可以通过set_pageblock_migratetype接口。
介绍完了这些关联知识,可以看get_pfnblock_flags_mask的实现了。首先看get_pfnblock_flags_mask->get_pageblock_bitmap函数获得bitmap的实现:
/* Return a pointer to the bitmap storing bits affecting a block of pages */ static inline unsigned long *get_pageblock_bitmap(const struct page *page, unsigned long pfn) { #ifdef CONFIG_SPARSEMEM return section_to_usemap(__pfn_to_section(pfn)); #else return page_zone(page)->pageblock_flags; #endif /* CONFIG_SPARSEMEM */ }
该函数就是要获得一个bitmap,bitmap里存储了一些bit位,这些bit位可以影响整个page block范围。对于配置了CONFIG_SPARSEMEM的情况:首先使用__pfn_to_section来获得对应pfn页框号的mm_section 结构体:
static inline struct mem_section *__pfn_to_section(unsigned long pfn) { return __nr_to_section(pfn_to_section_nr(pfn)); }
pfn_to_section_nr是先要找到pfn对应的section number是多少:
#define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT) static inline unsigned long pfn_to_section_nr(unsigned long pfn) { return pfn >> PFN_SECTION_SHIFT; }
SECTION_SIZE_BITS在x86-64上一般就是27,也就是一个section就是128MB大,对pfn除以PFN_SECTION_SHIFT,相当于计算出这个pfn位于哪个section了,有了这个section number再通过__nr_to_section去找到对应这个pfn(或者section number)的mem_section结构体:
static inline struct mem_section *__nr_to_section(unsigned long nr) { unsigned long root = SECTION_NR_TO_ROOT(nr); if (unlikely(root >= NR_SECTION_ROOTS)) return NULL; #ifdef CONFIG_SPARSEMEM_EXTREME if (!mem_section || !mem_section[root]) return NULL; #endif return &mem_section[root][nr & SECTION_ROOT_MASK]; }
系统定义了一个全局的mem_section二维数组,里面存放了所有的mem_section结构体的地址,开启了SPARSEMEM_EXTREME配置时,mem_section的空间是动态分配的。首先要找到section number对应哪个root,也就是mem_section的一维:
#ifdef CONFIG_SPARSEMEM_EXTREME #define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section)) #else #define SECTIONS_PER_ROOT 1 #endif #define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT)
没有开启SPARSEMEM_EXTREME配置时,只有一个root mem_section,否则按每页作为一个root mem_section,那么一个页里能存放多少mem_section就是页的大小除以mem_section结构体的大小,这样__nr_to_section通过SECTION_NR_TO_ROOT先算出对应section number的root mem_section,通过root作为下标取了mem_section的一维后,又通过section number nr和SECTION_ROOT_MASK相于看看是某个root sections里的哪一个section,SECTION_ROOT_MASK定义如下:
#define SECTION_ROOT_MASK (SECTIONS_PER_ROOT - 1)
回到get_pageblock_bitmap里,有了mem_section结构体,section_to_usemap返回bitmap图就简单了:
static inline unsigned long *section_to_usemap(struct mem_section *ms) { return ms->usage->pageblock_flags; }
usage的初始化在激活section里完成:
static struct page * __meminit section_activate(int nid, unsigned long pfn, unsigned long nr_pages, struct vmem_altmap *altmap, struct dev_pagemap *pgmap) { ... if (!ms->usage) { usage = kzalloc(mem_section_usage_size(), GFP_KERNEL); if (!usage) return ERR_PTR(-ENOMEM); ms->usage = usage; } ... }
mem_section.usage这个成员的定义如下:
struct mem_section_usage { struct rcu_head rcu; #ifdef CONFIG_SPARSEMEM_VMEMMAP DECLARE_BITMAP(subsection_map, SUBSECTIONS_PER_SECTION); #endif /* See declaration of similar field in struct zone */ unsigned long pageblock_flags[0]; };
其中mem_section_usage_size里计算了usage本身的大小再加上pageblock_flags的大小:
size_t mem_section_usage_size(void) { return sizeof(struct mem_section_usage) + usemap_size(); }
static unsigned long usemap_size(void) { return BITS_TO_LONGS(SECTION_BLOCKFLAGS_BITS) * sizeof(unsigned long); }
#define SECTION_BLOCKFLAGS_BITS \ ((1UL << (PFN_SECTION_SHIFT - pageblock_order)) * NR_PAGEBLOCK_BITS)
从这里可以看到,首先是PFN_SECTION_SHIFT减去一个pageblock所占用的bit位数,再用1左移这么多位,表示一个mem section里可以用多少个pageblock,而每个pageblock需用NR_PAGEBLOCK_BITS这么多个 bit来表示迁移类型,和NR_PAGEBLOCK_BITS相乘就是整个mem section需要的表示迁移类型的比特数目。
回到get_pfnblock_flags_mask函数,现在get_pfnblock_flags_mask->get_pageblock_bitmap得到了对应page的pageblock的bitmap图,继续分析pfn_to_bitidx函数:
static inline int pfn_to_bitidx(const struct page *page, unsigned long pfn) { #ifdef CONFIG_SPARSEMEM pfn &= (PAGES_PER_SECTION-1); #else pfn = pfn - pageblock_start_pfn(page_zone(page)->zone_start_pfn); #endif /* CONFIG_SPARSEMEM */ return (pfn >> pageblock_order) * NR_PAGEBLOCK_BITS; }
第一行先将pfn落到一个section里,然后最后一行看这个pfn属于第几个pageblock,最后乘以每个pageblock占了多少个bit(NR_PAGEBLOCK_BITS),最后得到了对应该pfn在bit图中第几位就是bitidx。
回到get_pfnblock_flags_mask,得到了bitidx后,又除以BITS_PER_LONG,就是按每64位一个long来数,看看目标bit位在第几个unsigned long,最后是按unsigned long读取,返回目标pfn的迁移类型。
继续回到reserve_highatomic_pageblock函数,通过get_pageblock_migratetype得到迁移类型mt后,需要判断其是否是可合并的,只有mt小于MIGRATE_PCPTYPES才认为是可以合并的页面,满足条件的话,才是分两种情况来移动页面,一是本次分配得到的页面order小于页块order的情况通过move_freepages_block来移动页面:
static int move_freepages_block(struct zone *zone, struct page *page, int old_mt, int new_mt) { unsigned long start_pfn; if (!prep_move_freepages_block(zone, page, &start_pfn, NULL, NULL)) return -1; return __move_freepages_block(zone, start_pfn, old_mt, new_mt); }
prep_move_freepages_block函数如果最后两个参数有地址的话,会返回空闲的页面以及可移动页面的数量,但是此处调用是不关心这两个值的,这里只关心一个pageblock内开始的pfn是多少,有了start_pfn就可以调用__move_freepages_block了:
/* * Change the type of a block and move all its free pages to that * type's freelist. */ static int __move_freepages_block(struct zone *zone, unsigned long start_pfn, int old_mt, int new_mt) { struct page *page; unsigned long pfn, end_pfn; unsigned int order; int pages_moved = 0; VM_WARN_ON(start_pfn & (pageblock_nr_pages - 1)); end_pfn = pageblock_end_pfn(start_pfn); for (pfn = start_pfn; pfn < end_pfn;) { page = pfn_to_page(pfn); if (!PageBuddy(page)) { pfn++; continue; } /* Make sure we are not inadvertently changing nodes */ VM_BUG_ON_PAGE(page_to_nid(page) != zone_to_nid(zone), page); VM_BUG_ON_PAGE(page_zone(page) != zone, page); order = buddy_order(page); move_to_free_list(page, zone, order, old_mt, new_mt); pfn += 1 << order; pages_moved += 1 << order; } set_pageblock_migratetype(pfn_to_page(start_pfn), new_mt); return pages_moved; }
在循环的主体里,从pageblock的第一个pfn到最后一个pfn,依次通过move_to_free_list接口将这些pfn对应的page移动到新的迁移类型里,注意移动的步子是page.order里记录的1<<order这么多个页面,通过PageBuddy判断页面要由buddy管理(是空闲的)才可以移动:
#define PAGE_TYPE_OPS(uname, lname, fname) \ FOLIO_TYPE_OPS(lname, fname) \ static __always_inline int Page##uname(const struct page *page) \ { \ return data_race(page->page_type >> 24) == PGTY_##lname; \ } \ static __always_inline void __SetPage##uname(struct page *page) \ { \ if (Page##uname(page)) \ return; \ VM_BUG_ON_PAGE(data_race(page->page_type) != UINT_MAX, page); \ page->page_type = (unsigned int)PGTY_##lname << 24; \ } \ static __always_inline void __ClearPage##uname(struct page *page) \ { \ if (page->page_type == UINT_MAX) \ return; \ VM_BUG_ON_PAGE(!Page##uname(page), page); \ page->page_type = UINT_MAX; \ } /* * PageBuddy() indicates that the page is free and in the buddy system * (see mm/page_alloc.c). */ PAGE_TYPE_OPS(Buddy, buddy, buddy)
以上定义在include/linux/page-flags.h里,可以看到最后判断的其实是page.page_type成员的高8比特的内容,这个8个bit定义了页面的类型,这些类型包括:
/* * For pages that do not use mapcount, page_type may be used. * The low 24 bits of pagetype may be used for your own purposes, as long * as you are careful to not affect the top 8 bits. The low bits of * pagetype will be overwritten when you clear the page_type from the page. */ enum pagetype { /* 0x00-0x7f are positive numbers, ie mapcount */ /* Reserve 0x80-0xef for mapcount overflow. */ PGTY_buddy = 0xf0, PGTY_offline = 0xf1, PGTY_table = 0xf2, PGTY_guard = 0xf3, PGTY_hugetlb = 0xf4, PGTY_slab = 0xf5, PGTY_zsmalloc = 0xf6, PGTY_unaccepted = 0xf7, PGTY_mapcount_underflow = 0xff };
这里使用了buddy_order去获得该页面对应的order:
static inline unsigned int buddy_order(struct page *page) { /* PageBuddy() must be checked by the caller */ return page_private(page); } #define page_private(page) ((page)->private)
关于private前面其实有介绍,在buddy system里,它表示空闲页面的order,在释放页面时通过__free_one_page->set_buddy_order->set_page_private设置这个private值,得到了order后以它为参数调用move_to_free_list:
/* * Used for pages which are on another list. Move the pages to the tail * of the list - so the moved pages won't immediately be considered for * allocation again (e.g., optimization for memory onlining). */ static inline void move_to_free_list(struct page *page, struct zone *zone, unsigned int order, int old_mt, int new_mt) { struct free_area *area = &zone->free_area[order]; /* Free page moving can fail, so it happens before the type update */ VM_WARN_ONCE(get_pageblock_migratetype(page) != old_mt, "page type is %lu, passed migratetype is %d (nr=%d)\n", get_pageblock_migratetype(page), old_mt, 1 << order); list_move_tail(&page->buddy_list, &area->free_list[new_mt]); account_freepages(zone, -(1 << order), old_mt); account_freepages(zone, 1 << order, new_mt); }
该函数真正最后的移动页面到指定新的迁移类型new_mt的函数,从这个函数里可以看出来,zone按多个order划分了多个free_area,而每个free_area里又有多个链表free_list用于链接不同迁移类型的页面,所以这里最后是通过list_move_tail来将页面链入到free_area->free_list对应new_mt迁移类型的链表里,使用的是buddy_list这个list_head来代表这个页面。
最后这里还想分析下account_freepages函数,因为这个函数里体现了总体/部分的性能优化思想。下面是这个函数的实现:
static inline void account_freepages(struct zone *zone, int nr_pages, int migratetype) { lockdep_assert_held(&zone->lock); if (is_migrate_isolate(migratetype)) return; __mod_zone_page_state(zone, NR_FREE_PAGES, nr_pages); if (is_migrate_cma(migratetype)) __mod_zone_page_state(zone, NR_FREE_CMA_PAGES, nr_pages); else if (is_migrate_highatomic(migratetype)) WRITE_ONCE(zone->nr_free_highatomic, zone->nr_free_highatomic + nr_pages); }
account_freepages主要是通过__mod_zone_page_state修改NR_FREE_PAGES项里面记录的空闲页面的数量,对于是迁移到MIGRATE_HIGHATOMIC类型的页面,需要增加nr_free_highatomic量以反应增长了多少 个这个类型的页面,这里主要往下介绍__mod_zone_page_state函数:
/* * For use when we know that interrupts are disabled, * or when we know that preemption is disabled and that * particular counter cannot be updated from interrupt context. */ void __mod_zone_page_state(struct zone *zone, enum zone_stat_item item, long delta) { struct per_cpu_zonestat __percpu *pcp = zone->per_cpu_zonestats; s8 __percpu *p = pcp->vm_stat_diff + item; long x; long t; /* * Accurate vmstat updates require a RMW. On !PREEMPT_RT kernels, * atomicity is provided by IRQs being disabled -- either explicitly * or via local_lock_irq. On PREEMPT_RT, local_lock_irq only disables * CPU migrations and preemption potentially corrupts a counter so * disable preemption. */ preempt_disable_nested(); x = delta + __this_cpu_read(*p); t = __this_cpu_read(pcp->stat_threshold); if (unlikely(abs(x) > t)) { zone_page_state_add(x, zone, item); x = 0; } __this_cpu_write(*p, x); preempt_enable_nested(); }
zone里有per_cpu_zonestats,用于记录percpu里的各种页面状态的计数,item参数指明了是哪种类型的计数,而delta参数代表添加到该类的差量,stat_threshold表示了一个阀值,当加上本次的差量超过了阀值时,意味着页面类型统计计数需要转移到zone.vm_stat这个zone级别的量里去,同时更新全局的vm_node_stat统计计数:
static inline void zone_page_state_add(long x, struct zone *zone, enum zone_stat_item item) { atomic_long_add(x, &zone->vm_stat[item]); atomic_long_add(x, &vm_zone_stat[item]); }
统计计数转移后那么本次写到percpu里的统计计数就是0了,也就是x = 0,如果不转移那么是多少就写入多少了。
这就是前面提到的总体/部分性能优化,就是每次需要更新计数时,不必每次都去争抢全局的量,大部分都是在更新percpu里的值。
set_pfnblock_flags_mask和前面介绍过的get_pfnblock_flags_mask总体逻辑类似,这里只是介绍下最后的设置动作:
word = READ_ONCE(bitmap[word_bitidx]); do { } while (!try_cmpxchg(&bitmap[word_bitidx], &word, (word & ~mask) | flags));
mask是传进来的参数MIGRATETYPE_MASK,表示关心哪些bit位,这里先从bitmap读出来word,然后和~mask相与,相当于是先清除关心的哪些位的值,因为这些位上的值要被设置成新的flags。
然后回到reserve_highatomic_pageblock函数,下一种情况就是本次分得页面的order是大于pageblock的order,那么这种情况更改页面的迁移属性就是以整个pageblock为单位的:
static void change_pageblock_range(struct page *pageblock_page, int start_order, int migratetype) { int nr_pageblocks = 1 << (start_order - pageblock_order); while (nr_pageblocks--) { set_pageblock_migratetype(pageblock_page, migratetype); pageblock_page += pageblock_nr_pages; } }
可以看到先算出本次page对应的order里包含多少个pageblock,然后把这些pageblock的迁移类型都更改成对应migratetype参数类型的。
以上就是对reserve_highatomic_pageblock的分析,回到get_page_from_freelist可以看到对reserve_highatomic_pageblock的调用其实是一个较低概率发生的事情(unlikely),这里对其详细分析主要是为了介绍页面的移动、页面迁移类型的管理以及页面按mem section分组等知识。
继续往下分析get_page_from_freelist,就是如果没有分配到页面(page为NULL),那么就会尝试一些手段看能否获得一些内存,比如前面提到过的accepted内存。
以上就是对get_page_from_freelist的全部分析。
5. rmqueue
下面开始分析rmqueue函数,rmqueue主要调用两个函数,一是rmqueue_pcplist函数用于分配order小于3的情况,从per-cpu缓存的页面里去分配,另外一个函数是rmqueue_buddy是针对order大于3的情况,从伙伴分配器里直接分配,下面先整体介绍rmqueue函数,再分两节介绍这两个函数:
/* * Allocate a page from the given zone. * Use pcplists for THP or "cheap" high-order allocations. */ /* * Do not instrument rmqueue() with KMSAN. This function may call * __msan_poison_alloca() through a call to set_pfnblock_flags_mask(). * If __msan_poison_alloca() attempts to allocate pages for the stack depot, it * may call rmqueue() again, which will result in a deadlock. */ __no_sanitize_memory 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) { struct page *page; if (likely(pcp_allowed_order(order))) { page = rmqueue_pcplist(preferred_zone, zone, order, migratetype, alloc_flags); if (likely(page)) goto out; } page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags, migratetype); out: /* Separate test+clear to avoid unnecessary atomics */ if ((alloc_flags & ALLOC_KSWAPD) && unlikely(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; }
该函数本身比较简单,先是按两种情况各自调用相应的接口去分配页面,最后如果zone->flags设置了ZONE_BOOSTED_WATERMARK标志,就会唤醒kswapd线程以回收内存。这个标志是临时提高了水位线,提前触发kswapd进行回收,以避免后续更严重的内存紧缺情况,因为内存回收往往伴随着I/Oswap out或drop cache),提前回收可以降低突发I/O延迟,保证系统流畅运行。
5.1. rmqueue_pcplist
pcp(per-cpu page)机制主要为了减少对zone->lock的争用,提前缓存部分页面到pcp链表里,这样分配的时候只需要从per-cpu的缓存页面拿,而不需要动zone,rmqueue_pcplist的实现如下:
/* Lock and remove page from the per-cpu list */ static struct page *rmqueue_pcplist(struct zone *preferred_zone, struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags) { struct per_cpu_pages *pcp; struct list_head *list; struct page *page; unsigned long __maybe_unused UP_flags; /* spin_trylock may fail due to a parallel drain or IRQ reentrancy. */ pcp_trylock_prepare(UP_flags); pcp = pcp_spin_trylock(zone->per_cpu_pageset); if (!pcp) { pcp_trylock_finish(UP_flags); return NULL; } /* * On allocation, reduce the number of pages that are batch freed. * See nr_pcp_free() where free_factor is increased for subsequent * frees. */ pcp->free_count >>= 1; list = &pcp->lists[order_to_pindex(migratetype, order)]; page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list); pcp_spin_unlock(pcp); pcp_trylock_finish(UP_flags); if (page) { __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); zone_statistics(preferred_zone, zone, 1); } return page; }
pcp_spin_trylock实际上拿的就是per_cpu_pages:lock这个自选锁了,而不是zone:lock。list指明应该从哪个链表上摘取页面,per_cpu_pages:lists成员是个链表数组:
struct per_cpu_pages { spinlock_t lock; /* Protects lists field */ int count; /* number of pages in the list */ int high; /* high watermark, emptying needed */ int high_min; /* min high watermark */ int high_max; /* max high watermark */ int batch; /* chunk size for buddy add/remove */ u8 flags; /* protected by pcp->lock */ u8 alloc_factor; /* batch scaling factor during allocate */ #ifdef CONFIG_NUMA u8 expire; /* When 0, remote pagesets are drained */ #endif short free_count; /* consecutive free count */ /* Lists of pages, one per migrate type stored on the pcp-lists */ struct list_head lists[NR_PCP_LISTS]; } ____cacheline_aligned_in_smp;
这个链表数组有NR_PCP_LISTS个成员:
/* * One per migratetype for each PAGE_ALLOC_COSTLY_ORDER. Two additional lists * are added for THP. One PCP list is used by GPF_MOVABLE, and the other PCP list * is used by GFP_UNMOVABLE and GFP_RECLAIMABLE. */ #ifdef CONFIG_TRANSPARENT_HUGEPAGE #define NR_PCP_THP 2 #else #define NR_PCP_THP 0 #endif #define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1)) #define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP)
可以看到对于0-3的每个阶都有MIGRATE_PCPTYPES(3)个链表,对于透明大页打开的情况,额外还有两个链表在数组的最后。
这里简单介绍下____cacheline_aligned_in_smp这个属性,它其实就是编译器的对齐属性:
#define CONFIG_X86_L1_CACHE_SHIFT 6 /* L1 cache line size */ #define L1_CACHE_SHIFT (CONFIG_X86_L1_CACHE_SHIFT) #define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT) #ifndef SMP_CACHE_BYTES #define SMP_CACHE_BYTES L1_CACHE_BYTES #endif #ifndef ____cacheline_aligned #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) #endif
所以在x86架构上一般就是对齐到64B,也就是一个cache line的大小,这个对齐属性主要是为了解决伪共享问题,假如一个结构体跨缓存行了,由于缓存是以一行64B为单位同步的,一个CPU写了缓存行会引 起另外一个CPU在该缓存上的数据也无效,产生MESI同步协议开销,所以对齐到缓存行并控制结构体的大小在一个缓存行内就可以避免这种伪共享问题。
回到rmqueue_pcplist函数,通过order_to_pindex得到对应迁移类型和order的list链表,随后可以从这个链表里分配页面:
static inline unsigned int order_to_pindex(int migratetype, int order) { bool __maybe_unused movable; #ifdef CONFIG_TRANSPARENT_HUGEPAGE if (order > PAGE_ALLOC_COSTLY_ORDER) { VM_BUG_ON(order != HPAGE_PMD_ORDER); movable = migratetype == MIGRATE_MOVABLE; return NR_LOWORDER_PCP_LISTS + movable; } #else VM_BUG_ON(order > PAGE_ALLOC_COSTLY_ORDER); #endif return (MIGRATE_PCPTYPES * order) + migratetype; }
由于每个order都有MIGRATE_PCPTYPES这么多个链表,所以将分配请求的order乘以每个order多少个迁移类型,再加上目标迁移类型,得到在哪个链表上去分配页面,如果是大于PAGE_ALLOC_COSTLY_ORDER,则只按是否可移动选择最后两个链表之一。有了链表继续调用__rmqueue_pcplist去分配页面。
后面继续分析__rmqueue_pcplist函数,现在先看成功分配了的两个统计函数,一是__count_zid_vm_events:
#define __count_zid_vm_events(item, zid, delta) \ __count_vm_events(item##_NORMAL - ZONE_NORMAL + zid, delta) static inline void __count_vm_events(enum vm_event_item item, long delta) { raw_cpu_add(vm_event_states.event[item], delta); }
这里是对PGALLOC事件项调用__count_vm_events去增加调用分配内存的计数,实际操作的是一个全局的percpu量vm_event_states:
DEFINE_PER_CPU(struct vm_event_state, vm_event_states) = {{0}}; EXPORT_PER_CPU_SYMBOL(vm_event_states);
配置CONFIG_VM_EVENT_COUNTERS控制了是否启用VM(虚拟内存)事件计数器,该配置可以在/proc/vmstat中显示各种VM相关的事件统计信息,默认一般是开启的。具体有哪些事件,可以查看vm_event_iterm枚举类型的定义。
zone_statistics统计的信息也可以在/proc/vmstat里查看的,它就主要是关注NUMA_HIT、NUMA_MISS等这种numa信息的内存分配情况,细节代码不再分析了。
rmqueue_pcplist本身就分析完毕了,后面继续分析它调用的核心子函数__rmqueue_pcplist。
5.1.1. __rmqueue_pcplist
该函数的实现如下:
/* Remove page from the per-cpu list, caller must protect the list */ static inline struct page *__rmqueue_pcplist(struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags, struct per_cpu_pages *pcp, struct list_head *list) { struct page *page; do { if (list_empty(list)) { int batch = nr_pcp_alloc(pcp, zone, order); int alloced; alloced = rmqueue_bulk(zone, order, batch, list, migratetype, alloc_flags); pcp->count += alloced << order; if (unlikely(list_empty(list))) return NULL; } page = list_first_entry(list, struct page, pcp_list); list_del(&page->pcp_list); pcp->count -= 1 << order; } while (check_new_pages(page, order)); return page; }
该函数首先会检查list链表里是否还有页面,如果没有页面是空链表了,就会通过下节将要介绍的rmqueue_bulk去buddy分配器(系统)里批量的移动一些页面到list链表里。如果list链表里有页面,那么就直接使用list_first_entry去获取链表上的第一个页面,然后使用list_del从list链表上删除这个页面,注意pcp链表里的页面通过page:pcp_list链入,而buddy系统里的页面通过page:buddy_list链入,per_cpu_pages:count是按单个页面计数方式的空闲页面数量。
分配出来的页面还要通过check_new_pages进行检验,如果检验返回true代表这个页面不能使用(页面是bad的有问题),还要返回继续重新分配,is_check_pages_enabled一般是判断check_pages_enabled这个量,大概率是不满足,所以一般也就不检验了,假如要检验,也就是检查page未被映射(_mapcount==-1)、page未被任何进程或内核组件引用(ref_count==0)等条件,如果这些检查没有通过就会在check_new_page_bad->bad_page里打印具体什么原因。
下面分析rmqeue_bulk,在分析rmqueue_bulk前,需要分析其第三个参数batch的来源,其通过nr_pcp_alloc函数而得到,它代表了一次从buddy分配器里批量的移动多少页面到pcp链表,nr_pcp_alloc实现如下:
static int nr_pcp_alloc(struct per_cpu_pages *pcp, struct zone *zone, int order) { int high, base_batch, batch, max_nr_alloc; int high_max, high_min; base_batch = READ_ONCE(pcp->batch); high_min = READ_ONCE(pcp->high_min); high_max = READ_ONCE(pcp->high_max); high = pcp->high = clamp(pcp->high, high_min, high_max); /* Check for PCP disabled or boot pageset */ if (unlikely(high < base_batch)) return 1; if (order) batch = base_batch; else batch = (base_batch << pcp->alloc_factor); /* * If we had larger pcp->high, we could avoid to allocate from * zone. */ if (high_min != high_max && !test_bit(ZONE_BELOW_HIGH, &zone->flags)) high = pcp->high = min(high + batch, high_max); if (!order) { max_nr_alloc = max(high - pcp->count - base_batch, base_batch); /* * Double the number of pages allocated each time there is * subsequent allocation of order-0 pages without any freeing. */ if (batch <= max_nr_alloc && pcp->alloc_factor < CONFIG_PCP_BATCH_SCALE_MAX) pcp->alloc_factor++; batch = min(batch, max_nr_alloc); } /* * Scale batch relative to order if batch implies free pages * can be stored on the PCP. Batch can be 1 for small zones or * for boot pagesets which should never store free pages as * the pages may belong to arbitrary zones. */ if (batch > 1) batch = max(batch >> order, 2); return batch; }
前几行先将pcp->high钳制在pcp->high_min以及pcp->high_max之间。随后如果order为0,也就是只分配一个页面的情况,需要适当扩大批量分配的页面,右移alloc_factor,这样单个页面的情况不会频繁的打扰buddy分配器。然后判断只要还有空间增加批量分配的最大大小pcp->high,就将它增大。
总之该函数就是调整批量从伙伴系统拿多少页面过来到pcp链表上。现在可以看rmqueue_bulk的实现了:
/* * Obtain a specified number of elements from the buddy allocator, all under * a single hold of the lock, for efficiency. Add them to the supplied list. * Returns the number of new pages which were placed at *list. */ static int rmqueue_bulk(struct zone *zone, unsigned int order, unsigned long count, struct list_head *list, int migratetype, unsigned int alloc_flags) { unsigned long flags; int i; spin_lock_irqsave(&zone->lock, flags); for (i = 0; i < count; ++i) { struct page *page = __rmqueue(zone, order, migratetype, alloc_flags); if (unlikely(page == NULL)) break; /* * Split buddy pages returned by expand() are received here in * physical page order. The page is added to the tail of * caller's list. From the callers perspective, the linked list * is ordered by page number under some conditions. This is * useful for IO devices that can forward direction from the * head, thus also in the physical page order. This is useful * for IO devices that can merge IO requests if the physical * pages are ordered properly. */ list_add_tail(&page->pcp_list, list); } spin_unlock_irqrestore(&zone->lock, flags); return i; }
参数count代表需要分配多少次,每次按2^order个页面分配,参数list是调用者提供的将分配出来的页面加入到哪个列表上,这里可以看到buddyallocator需要拿到zone->lock自旋锁才能到buddy里去分配页面。
该函数实现如下:
/* * Do the hard work of removing an element from the buddy allocator. * Call me with the zone->lock already held. */ 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)) { /* * Balance movable allocations between regular and CMA areas by * allocating from CMA when over half of the zone's free memory * is in the CMA area. */ 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) return page; } } page = __rmqueue_smallest(zone, order, migratetype); if (unlikely(!page)) { if (alloc_flags & ALLOC_CMA) page = __rmqueue_cma_fallback(zone, order); if (!page) page = __rmqueue_fallback(zone, order, migratetype, alloc_flags); } return page; }
如果配置了CMA(Contiguous Memory Allocator)并且CMA页面多到一定程度,是可以从cma类型的页面里拿出来到pcp链表的,这里考虑没有开启这个配置的情况。该函数首先经由__rmqueue_smallest从目标migratetype里分配页面,如果没有能分配出来页面,就需要fallback(回退)到其它类型再次尝试。
/* * 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 < NR_PAGE_ORDERS; ++current_order) { area = &(zone->free_area[current_order]); page = get_page_from_free_area(area, migratetype); if (!page) continue; page_del_and_expand(zone, page, order, current_order, migratetype); trace_mm_page_alloc_zone_locked(page, order, migratetype, pcp_allowed_order(order) && migratetype < MIGRATE_PCPTYPES); return page; } return NULL; }
该函数从传入的order开始向上找到一个order里面有参数指明的迁移类型的页面,就通过page_del_and_expand将高阶order(curent_order)的页面向低阶扩展。zone里按多个order分不同的free_area,而free_area又按迁移类型分不同的链表挂页面,假如现在得到了非空的page,下面就要调用page_del_and_expand进行页面扩展:
static __always_inline void page_del_and_expand(struct zone *zone, struct page *page, int low, int high, int migratetype) { int nr_pages = 1 << high; __del_page_from_free_list(page, zone, high, migratetype); nr_pages -= expand(zone, page, low, high, migratetype); account_freepages(zone, -nr_pages, migratetype); }
先看下__del_page_from_free_list函数:
static inline void __del_page_from_free_list(struct page *page, struct zone *zone, unsigned int order, int migratetype) { VM_WARN_ONCE(get_pageblock_migratetype(page) != migratetype, "page type is %lu, passed migratetype is %d (nr=%d)\n", get_pageblock_migratetype(page), migratetype, 1 << order); /* clear reported state and update reported page count */ if (page_reported(page)) __ClearPageReported(page); list_del(&page->buddy_list); __ClearPageBuddy(page); set_page_private(page, 0); zone->free_area[order].nr_free--; }
该函数首先通过__del_page_from_free_list将页面从原来order的迁移类型链表里删除,这里删除是通过page:buddy_list成员,因为往buddy系统里挂入页面也是通过这个成员。__ClearPageBuddy的定义在前面介绍PAGE_TYPE_OPS宏已经涉及到了,它清的其实是page:page_type成员里的某个比特。随后调用set_page_private将page:private设置为0,因为只有buddy allocator需要通过page:private记录页面的阶,返回给其它模块(pcp list)时不需要了。最后就是对应order的nr_free需要递减,这个nr_free里记录的单位是按2^order个页面每单位。
__del_page_from_free_list删除页面完毕后,就可以调用exapnd函数来扩展高阶页面到低阶页面了。
/* * The order of subdivision here is critical for the IO subsystem. * Please do not alter this order without good reasons and regression * testing. Specifically, as large blocks of memory are subdivided, * the order in which smaller blocks are delivered depends on the order * they're subdivided in this function. This is the primary factor * influencing the order in which pages are delivered to the IO * subsystem according to empirical testing, and this is also justified * by considering the behavior of a buddy system containing a single * large block of memory acted on by a series of small allocations. * This behavior is a critical factor in sglist merging's success. * * -- nyc */ static inline unsigned int expand(struct zone *zone, struct page *page, int low, int high, int migratetype) { unsigned int size = 1 << high; unsigned int nr_added = 0; while (high > low) { high--; size >>= 1; VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]); /* * Mark as guard pages (or page), that will allow to * merge back to allocator when buddy will be freed. * Corresponding page table entries will not be touched, * pages will stay not present in virtual address space */ if (set_page_guard(zone, &page[size], high)) continue; __add_to_free_list(&page[size], zone, high, migratetype, false); set_buddy_order(&page[size], high); nr_added += size; } return nr_added; }
expand函数由当前high参数指明的order阶向下扩展页面,也就是把2^order这么多个页面依次挪到(high - 1),(high - 2)…low这些阶对应的迁移类型的链表上去,最后的low这个阶也就是本次分配页面的阶。这里可以看下__add_to_free_list的实现:
/* Used for pages not on another list */ static inline void __add_to_free_list(struct page *page, struct zone *zone, unsigned int order, int migratetype, bool tail) { struct free_area *area = &zone->free_area[order]; VM_WARN_ONCE(get_pageblock_migratetype(page) != migratetype, "page type is %lu, passed migratetype is %d (nr=%d)\n", get_pageblock_migratetype(page), migratetype, 1 << order); if (tail) list_add_tail(&page->buddy_list, &area->free_list[migratetype]); else list_add(&page->buddy_list, &area->free_list[migratetype]); area->nr_free++; }
还是操作buddy_list成员加到其它链表,注意expand扩展完了,除了最后low这个order对应的页面(它在返回时要加入到pcp链表),其它order的页面还是在buddy系统里,只是换到了低阶的链表里去,在buddy系统里,自然要通过set_buddy_order设置页面的order阶。
如果前面的__rmqueue_smallest没能获得页面,这说明所有order对应migratetype的链表里都没有空闲页面,这时可以考虑调用__rmqueue_fallback去其它迁移类型尝试:
/* * Try finding a free buddy page on the fallback list and put it on the free * list of requested migratetype, possibly along with other pages from the same * block, depending on fragmentation avoidance heuristics. Returns true if * fallback was found so that __rmqueue_smallest() can grab it. * * The use of signed ints for order and current_order is a deliberate * deviation from the rest of this file, to make the for loop * condition simpler. */ static __always_inline struct page * __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; /* * Do not steal pages from freelists belonging to other pageblocks * i.e. orders < pageblock_order. If there are no local zones free, * the zonelists will be reiterated without ALLOC_NOFRAGMENT. */ if (order < pageblock_order && alloc_flags & ALLOC_NOFRAGMENT) min_order = pageblock_order; /* * Find the largest available free page in the other list. This roughly * approximates finding the pageblock with the most free pages, which * would be too costly to do exactly. */ for (current_order = MAX_PAGE_ORDER; current_order >= min_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) continue; /* * We cannot steal all free pages from the pageblock and the * requested migratetype is movable. In that case it's better to * steal and split the smallest available page instead of the * largest available page, because even if the next movable * allocation falls back into a different pageblock than this * one, it won't cause permanent fragmentation. */ if (!can_steal && start_migratetype == MIGRATE_MOVABLE && current_order > order) goto find_smallest; goto do_steal; } return NULL; find_smallest: for (current_order = order; current_order < NR_PAGE_ORDERS; 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; } /* * This should not happen - we already found a suitable fallback * when looking for the largest page. */ VM_BUG_ON(current_order > MAX_PAGE_ORDER); do_steal: page = get_page_from_free_area(area, fallback_mt); /* take off list, maybe claim block, expand remainder */ page = steal_suitable_fallback(zone, page, current_order, order, start_migratetype, alloc_flags, can_steal); trace_mm_page_alloc_extfrag(page, order, current_order, start_migratetype, fallback_mt); return page; }
该函数首先尝试从高的order(大的块)向下进行检查,看是否有满足条件的order的块可以偷取页面,因为回退拆分小的块会越拆越小,导致碎片化严重。但如果caller请求的migratetype是MIGRATE_MOVABLE的又不能偷取整个较大的块,就要避免污染大的页块,跳到find_smallest从最小的order块开始查找满足条件的pageblock。
判断是否能偷取块里页面的关键函数是find_suitable_fallback,该函数如果没有找到合适的migratetype就返回-1:
/* * Check whether there is a suitable fallback freepage with requested order. * If only_stealable is true, this function returns fallback_mt only if * we can steal other freepages all together. This would help to reduce * fragmentation due to mixed migratetype pages in one pageblock. */ int find_suitable_fallback(struct free_area *area, unsigned int order, int migratetype, bool only_stealable, bool *can_steal) { int i; int fallback_mt; if (area->nr_free == 0) return -1; *can_steal = false; for (i = 0; i < MIGRATE_PCPTYPES - 1 ; i++) { fallback_mt = fallbacks[migratetype][i]; if (free_area_empty(area, fallback_mt)) continue; if (can_steal_fallback(order, migratetype)) *can_steal = true; if (!only_stealable) return fallback_mt; if (*can_steal) return fallback_mt; } return -1; }
这里的可迁移类型一般就是三类,一是MIGRATE_MOVABLE,二是MIGRATE_UNMOVABLE,三是MIGRATE_RECLAIMABLE,针对每种类型,都定义了当发生回退到其它类型时,这些类型有个优先级数组,就是fallbacks数组:
/* * This array describes the order lists are fallen back to when * the free lists for the desirable migrate type are depleted * * The other migratetypes do not have fallbacks. */ static int fallbacks[MIGRATE_PCPTYPES][MIGRATE_PCPTYPES - 1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE }, };
定义这个优先级的一个原则就是,MIGRATE_UNMOVABLE尽量避免混入MIGRATE_MOVABLE的页面里去,这会导致原来MIGRATE_MOVABLE页面所在的块变得不能通过compaction机制去移动进而构成一个大的MOVABLE的块,也就是避免内存碎片化。
can_steal_fallback用于判断是否能偷取整个页面,这里only_stealable参数为false,所以并不关心can_steal_fallback的返回值,不过这里可以看下这个函数的实现:
/* * When we are falling back to another migratetype during allocation, try to * steal extra free pages from the same pageblocks to satisfy further * allocations, instead of polluting multiple pageblocks. * * If we are stealing a relatively large buddy page, it is likely there will * be more free pages in the pageblock, so try to steal them all. For * reclaimable and unmovable allocations, we steal regardless of page size, * as fragmentation caused by those allocations polluting movable pageblocks * is worse than movable allocations stealing from unmovable and reclaimable * pageblocks. */ static bool can_steal_fallback(unsigned int order, int start_mt) { /* * Leaving this order check is intended, although there is * relaxed order check in next check. The reason is that * we can actually steal whole pageblock if this condition met, * but, below check doesn't guarantee it and that is just heuristic * so could be changed anytime. */ if (order >= pageblock_order) return true; /* * Movable pages won't cause permanent fragmentation, so when you alloc * small pages, you just need to temporarily steal unmovable or * reclaimable pages that are closest to the request size. After a * while, memory compaction may occur to form large contiguous pages, * and the next movable allocation may not need to steal. Unmovable and * reclaimable allocations need to actually steal pages. */ if (order >= pageblock_order / 2 || start_mt == MIGRATE_RECLAIMABLE || start_mt == MIGRATE_UNMOVABLE || page_group_by_mobility_disabled) return true; return false; }
一就是分配的order本来就比较大,大于pageblock_order,这自然是满足条件的,也就是多个整的pageblock都被偷取了,自然不会存在一个pageblock里混入其它迁移类型的情况,另外一种就是尽管order较小(小于pageblock order但大于pageblock order的一半),但由于caller请求的迁移类型不是MOVABLE的,这种情况下也把整个pageblock交给调用者拿去fallback,这同样是为了避免MOVABLE的pageblock里混入其它两种迁移类型的页面引起碎片化(不能通过移动页面整理出大片的连续空间了)。
回到__rmqueue_fallback函数,假设现在已经找到了回退的类型,首先通过get_page_from_area从空闲链表里去把页面摘取出来,随后通过stral_suitable_fallback执行真正的偷取动作:
/* * This function implements actual steal behaviour. If order is large enough, we * can claim the whole pageblock for the requested migratetype. If not, we check * the pageblock for constituent pages; if at least half of the pages are free * or compatible, we can still claim the whole block, so pages freed in the * future will be put on the correct free list. Otherwise, we isolate exactly * the order we need from the fallback block and leave its migratetype alone. */ static struct page * steal_suitable_fallback(struct zone *zone, struct page *page, int current_order, int order, int start_type, unsigned int alloc_flags, bool whole_block) { int free_pages, movable_pages, alike_pages; unsigned long start_pfn; int block_type; block_type = get_pageblock_migratetype(page); /* * This can happen due to races and we want to prevent broken * highatomic accounting. */ if (is_migrate_highatomic(block_type)) goto single_page; /* Take ownership for orders >= pageblock_order */ if (current_order >= pageblock_order) { unsigned int nr_added; del_page_from_free_list(page, zone, current_order, block_type); change_pageblock_range(page, current_order, start_type); nr_added = expand(zone, page, order, current_order, start_type); account_freepages(zone, nr_added, start_type); return page; } /* * Boost watermarks to increase reclaim pressure to reduce the * likelihood of future fallbacks. Wake kswapd now as the node * may be balanced overall and kswapd will not wake naturally. */ if (boost_watermark(zone) && (alloc_flags & ALLOC_KSWAPD)) set_bit(ZONE_BOOSTED_WATERMARK, &zone->flags); /* We are not allowed to try stealing from the whole block */ if (!whole_block) goto single_page; /* moving whole block can fail due to zone boundary conditions */ if (!prep_move_freepages_block(zone, page, &start_pfn, &free_pages, &movable_pages)) goto single_page; /* * Determine how many pages are compatible with our allocation. * For movable allocation, it's the number of movable pages which * we just obtained. For other types it's a bit more tricky. */ if (start_type == MIGRATE_MOVABLE) { alike_pages = movable_pages; } else { /* * If we are falling back a RECLAIMABLE or UNMOVABLE allocation * to MOVABLE pageblock, consider all non-movable pages as * compatible. If it's UNMOVABLE falling back to RECLAIMABLE or * vice versa, be conservative since we can't distinguish the * exact migratetype of non-movable pages. */ if (block_type == MIGRATE_MOVABLE) alike_pages = pageblock_nr_pages - (free_pages + movable_pages); else alike_pages = 0; } /* * If a sufficient number of pages in the block are either free or of * compatible migratability as our allocation, claim the whole block. */ if (free_pages + alike_pages >= (1 << (pageblock_order-1)) || page_group_by_mobility_disabled) { __move_freepages_block(zone, start_pfn, block_type, start_type); return __rmqueue_smallest(zone, order, start_type); } single_page: page_del_and_expand(zone, page, order, current_order, block_type); return page; }
该函数所谓的steal页面,其实就是把目标页面page的迁移类型改成caller请求的start_type,同时返回分配好的page。
首先通过get_pageblock_migratetype获得当前目标返回页面的迁移类型block_type,如果准备使用的页面的迁移类型是MIGRATE_HIGHATOMIC,则直接跳转到single_page逻辑,而不尝试去占用整个pageblock。这是因为MIGRATE_HIGHATOMIC页面需要保持一定的可用性,不能随意更改其迁移类型,否则可能影响系统的关键分配路径。进入single_page逻辑后,只会尝试拆分所需的order大小的页面,而不会影响整个pageblock,确保MIGRATE_HIGHATOMIC类型的页面不会被整体替换或迁移。
如果current_order大于等于pageblock_order,直接窃取整个pageblock,current_order >= pageblock_order说明这个page占据了整个pageblock,所以直接:
1,从free list中删除该page。
2,修改pageblock的migratetype以适应start_type(就是修改为新的start_type类型)。
3,调用expand()处理剩余的页,如果current_order > order,就分割页块。
4,更新zone的freepage计数。
5,返回page以满足分配。
接下来可能会增加回收压力,设置ZONE_BOOSTED_WATERMARK标志以在rmqueue函数里判断这个标志被设置进而唤醒kswapd回收更多的内存,稍后会详细分析这个boost_watermark函数。
如果whole_block == false,就不尝试整块窃取,直接进入单页处理。如果whole_block允许,尝试窃取整个pageblock,当然这还需要接下来的一些逻辑判断到底能不能窃取整个块。
其中一个重要条件是prep_move_freepages_block的返回值,如果prep_move_freepages_block返回false,代表不能窃取整个块,去往single_page只摘取满足本次的页面,这个函数前面遇到过,里面的逻辑也较为简单,此处不再分析。
继续往下的逻辑:
/* * Determine how many pages are compatible with our allocation. * For movable allocation, it's the number of movable pages which * we just obtained. For other types it's a bit more tricky. */ if (start_type == MIGRATE_MOVABLE) { alike_pages = movable_pages; } else { /* * If we are falling back a RECLAIMABLE or UNMOVABLE allocation * to MOVABLE pageblock, consider all non-movable pages as * compatible. If it's UNMOVABLE falling back to RECLAIMABLE or * vice versa, be conservative since we can't distinguish the * exact migratetype of non-movable pages. */ if (block_type == MIGRATE_MOVABLE) alike_pages = pageblock_nr_pages - (free_pages + movable_pages); else alike_pages = 0; }
如果请求的页面迁移类型是MIGRATE_MOVABLE,那么要fallback到MIGRATE_UNMOVABLE或MIGRATE_RECLAMIMABLE,这样的block里面不会有多余的页面变成MIGRATE_MOVABLE,否则会导致MIGRATE_UNMOVABLE/MIGRATE_RECLAIMABLE变少,使得将来fallback进MIGRATE_MOVABLE变得更容易,也就是非movable的页面变少了需要去movable的页面拿,这种情况是要避免的,相当于moveable页面变得 没法移动了。
继续往下看,如果现在的block_type是MIGRATE_MOVABLE,就是将要拆一个moveable的块给非moveable的页面用,这时要看看这个块里还有没有不可移动的页面,数量赋给alike_pages,这样所有不可移动的页面就是free_pages和alike_pages之和,如果free_pages + alike_pages足够多,整个pageblock会被改为start_type(即MIGRATE_UNMOVABLE或MIGRATE_RECLAIMABLE),这样可以保证,避免继续尝试将pageblock作为MIGRATE_MOVABLE处理(因为已经有太多非MOVABLE页面),保证未来的MOVABLE分配不会再误进入这个pageblock,防止MOVABLE页面被UNMOVABLE污染,这就是如下代码的逻辑:
/* * If a sufficient number of pages in the block are either free or of * compatible migratability as our allocation, claim the whole block. */ if (free_pages + alike_pages >= (1 << (pageblock_order-1)) || page_group_by_mobility_disabled) { __move_freepages_block(zone, start_pfn, block_type, start_type); return __rmqueue_smallest(zone, order, start_type); }
最后的single_page不会更改整个pageblock的迁移类型,只取特定大小的页面,具体的动作page_del_and_expand前面分析过了。
下面想分析下boost_watermark函数:
static inline bool boost_watermark(struct zone *zone) { unsigned long max_boost; if (!watermark_boost_factor) return false; /* * Don't bother in zones that are unlikely to produce results. * On small machines, including kdump capture kernels running * in a small area, boosting the watermark can cause an out of * memory situation immediately. */ if ((pageblock_nr_pages * 4) > zone_managed_pages(zone)) return false; max_boost = mult_frac(zone->_watermark[WMARK_HIGH], watermark_boost_factor, 10000); /* * high watermark may be uninitialised if fragmentation occurs * very early in boot so do not boost. We do not fall * through and boost by pageblock_nr_pages as failing * allocations that early means that reclaim is not going * to help and it may even be impossible to reclaim the * boosted watermark resulting in a hang. */ if (!max_boost) return false; max_boost = max(pageblock_nr_pages, max_boost); zone->watermark_boost = min(zone->watermark_boost + pageblock_nr_pages, max_boost); return true; }
该函数主要是临时提高水位,用于短暂地提高WMARK_HIGH,从而影响内存回收的触发阈值,使得系统在内存回收(reclaim)过程中回收更多的页面,以改善高阶页面分配的成功率。尤其是balance_pgdat()在触发内存回收时会参考WMARK_HIGH,如果watermark_boost提高了WMARK_HIGH,那么kswapd线程就会更积极地回收内存,从而腾出更多的可用页面。boost_watermark()触发时,watermark_boost逐步增加(但不会超过max_boost)。在kswapd或direct reclaim过程中,当内存压力缓解,watermark_boost可能会被逐步清零,从而恢复正常的WMARK_HIGH。watermark_boost仅是临时性的,不会永久提高水位,在内存回收机制执行后,系统最终会回归正常状态。
具体的逻辑本函数较为简单就不分析细节了。
5.2. rmqueue_buddy
该函数只是列下其代码,具体的也不会分析了,因为其涉及到的__rmqueue_smallest和__rmqueue前面都分析过了:
static __always_inline struct page *rmqueue_buddy(struct zone *preferred_zone, struct zone *zone, unsigned int order, unsigned int alloc_flags, int migratetype) { struct page *page; unsigned long flags; do { page = NULL; spin_lock_irqsave(&zone->lock, flags); if (alloc_flags & ALLOC_HIGHATOMIC) page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC); if (!page) { page = __rmqueue(zone, order, migratetype, alloc_flags); /* * If the allocation fails, allow OOM handling and * order-0 (atomic) allocs access to HIGHATOMIC * reserves as failing now is worse than failing a * high-order atomic allocation in the future. */ if (!page && (alloc_flags & (ALLOC_OOM|ALLOC_NON_BLOCK))) page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC); if (!page) { spin_unlock_irqrestore(&zone->lock, flags); return NULL; } } spin_unlock_irqrestore(&zone->lock, flags); } while (check_new_pages(page, order)); __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); zone_statistics(preferred_zone, zone, 1); return page; }
以上,就是Linux内核内存管理快速分配一个页面的流程。