GPU Buddy 与内核 Buddy / SLUB 深度对比分析
目录
- 概览
- 内核 Buddy (page_alloc) 深度解析
- SLUB 深度解析
- GPU Buddy 深度解析
- 三者的层次关系
- 关键差异对比表
概览
Linux 内核中有三个不同的”buddy”或分配器概念,它们管理不同层面的内存:
| 分配器 |
管理对象 |
层次 |
源码位置 |
| 内核 Buddy |
物理 RAM 页框 |
最底层,管理物理内存 |
mm/page_alloc.c |
| SLUB |
内核堆对象 (kmalloc/kmem_cache_alloc) |
中间层,基于内核 Buddy |
mm/slub.c |
| GPU Buddy |
GPU 显存地址空间 |
独立层,只借用 SLUB 存元数据 |
drivers/gpu/buddy.c |
内核 Buddy (page_alloc) 深度解析
管理对象
管理系统物理 RAM 页框(struct page),每个页面典型大小为 4KB。这是 Linux 内核最底层的物理内存分配器。
数据结构
每个内存 zone 维护一个 free_area 数组,按 order 和迁移类型组织空闲页:
1 2 3 4 5 6 7 8
| struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; };
struct free_area free_area[NR_PAGE_ORDERS];
|
关键设计:链表而非红黑树。 每个 free_list[migratetype] 是一条双向链表,空闲页通过 page->buddy_list 挂入链表。伙伴信息直接编码在 struct page 中:
1 2 3 4 5 6
| static inline void set_buddy_order(struct page *page, unsigned int order) { set_page_private(page, order); __SetPageBuddy(page); }
|
分配路径
1 2 3 4 5
| alloc_pages(gfp, order) → __alloc_pages() → get_page_from_freelist() → rmqueue() → __rmqueue_smallest()
|
分配策略是从请求 order 的 free_list 中取一个空闲页,如果该 order 为空则向更大 order 搜索,找到后分裂大块。始终使用 preferred migratetype 的链表,优先从链表头部取(FIFO)。
释放与合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| static inline void __free_one_page(struct page *page, unsigned long pfn, struct zone *zone, unsigned int order, int migratetype, fpi_t fpi_flags) { while (order < MAX_PAGE_ORDER) { buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn); if (!buddy) goto done_merging;
if (migratetype != buddy_mt && (!migratetype_is_mergeable(migratetype) || !migratetype_is_mergeable(buddy_mt))) goto done_merging;
__del_page_from_free_list(buddy, zone, order, buddy_mt); } done_merging: __add_to_free_list(page, zone, order, migratetype, to_tail); }
|
合并逻辑:循环向上,通过 find_buddy_page_pfn() 根据 pfn 和 order 位运算找到伙伴页,检查是否为 free 且迁移类型兼容,满足则合并并继续向上一级。
碎片化控制
内核 Buddy 有最完善的碎片化控制:
- 多迁移类型:UNMOVABLE、MOVABLE、RECLAIMABLE、CMA、HIGHATOMIC 等,每种类型独立链表
- pageblock 粒度:以
pageblock_order 为单位分组页面,不同迁移类型按 pageblock 隔离,防止不可移动页面永久碎片化可移动区域
- **compaction (内存压缩)**:
mm/compaction.c 通过 fragmentation_index() 评估碎片程度,触发 kcompactd 迁移页面生成大块连续内存
- 暴露接口:
/proc/buddyinfo 显示每个 zone 每个 order 的空闲块数,debugfs/extfrag/ 输出碎片化指标
最大 Order
1 2 3
| #ifndef CONFIG_FORCE_MAX_ZONEORDER #define MAX_PAGE_ORDER 10 #endif
|
这意味着内核 Buddy 能分配的最大连续物理内存为 PAGE_SIZE << 10(典型 4MB)。
SLUB 深度解析
管理对象
管理内核堆对象 — kmalloc() 和 kmem_cache_alloc() 的后端。SLUB 是对象分配器,不是块分配器。
数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct slab_sheaf { union { struct rcu_head rcu_head; struct list_head barn_list; struct { unsigned int capacity; bool pfmemalloc; }; }; struct kmem_cache *cache; unsigned int size; int node; void *objects[]; };
struct slub_percpu_sheaves { local_trylock_t lock; struct slab_sheaf *main; struct slab_sheaf *spare; struct slab_sheaf *rcu_free; };
|
每个 kmem_cache 有:
- Per-CPU sheaf 缓存:
main/spare/rcu_free 三块,实现几乎无锁的快速路径
- Per-node 链表:partial(部分空闲 slab)、full(全满 slab)
分配路径
1 2 3 4 5 6 7
| kmalloc(size, GFP_KERNEL) → slab_alloc() → slab_alloc_node() → 尝试从 per-CPU main sheaf 取对象 (无锁快速路径) → 若 main 为空,从 spare 交换过来 (local_trylock) → 若 spare 也为空,从 node partial list 补充 (spinlock) → 若 partial list 为空,调用 new_slab() 从内核 Buddy 申请新 slab
|
slab 来源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static inline struct kmem_cache_order_objects oo_make(unsigned int order, unsigned int size) { struct kmem_cache_order_objects x = { (order << OO_SHIFT) + order_objects(order, size) }; return x; }
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 & ((1 << OO_SHIFT) - 1); }
|
SLUB 从内核 Buddy 申请 page 作为 slab,再用 oo_objects() 将 slab 切成等大的对象槽位。SLUB 自己不实现 buddy 逻辑,它借用内核 Buddy 作为底层页来源。
释放路径
1 2 3 4 5
| kfree(ptr) → slab_free() → 尝试加入 per-CPU main sheaf (无锁快速路径) → 若 main 已满,将其变为 spare,新 sheaf 作为 main → 若 slab 变得全空,释放 slab 归还内核 Buddy
|
SLUB 不合并对象 — 释放只将对象指针还给同 slab 的 freelist,不会将相邻对象合并成大对象。
GPU Buddy 深度解析
管理对象
管理 GPU 显存地址空间 — 抽象的字节地址范围,不是系统物理内存。分配器以 u64 字节偏移量寻址。
核心数据结构
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
| struct gpu_buddy_block { u64 header; struct gpu_buddy_block *left; struct gpu_buddy_block *right; struct gpu_buddy_block *parent; void *private; union { struct rb_node rb; struct list_head link; }; struct list_head tmp_link; unsigned int subtree_max_alignment; };
struct gpu_buddy { struct rb_root **free_trees; struct gpu_buddy_block **roots; unsigned int n_roots; unsigned int max_order; u64 chunk_size; u64 size; u64 avail; u64 clear_avail; };
|
关键设计:增广红黑树而非链表。 每个 order 有 clear 和 dirty 两棵红黑树,按 block offset 排序,并通过 subtree_max_alignment 增广以支持高效对齐查找。
1 2 3 4 5 6 7 8
| #define GPU_BUDDY_HEADER_OFFSET GENMASK_ULL(63, 12) #define GPU_BUDDY_HEADER_STATE GENMASK_ULL(11, 10) #define GPU_BUDDY_ALLOCATED (1 << 10) #define GPU_BUDDY_FREE (2 << 10) #define GPU_BUDDY_SPLIT (3 << 10) #define GPU_BUDDY_HEADER_CLEAR GENMASK_ULL(9, 9) #define GPU_BUDDY_HEADER_ORDER GENMASK_ULL(5, 0)
|
一个 64-bit header 字段编码了 block 的完整状态信息,无需像内核 Buddy 依赖 struct page 的标志位。
元数据分配:借用 SLUB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static struct kmem_cache *slab_blocks;
slab_blocks = KMEM_CACHE(gpu_buddy_block, 0);
block = kmem_cache_zalloc(slab_blocks, GFP_KERNEL);
kmem_cache_free(slab_blocks, block);
mm->free_trees = kmalloc_array(GPU_BUDDY_MAX_FREE_TREES, sizeof(*mm->free_trees), GFP_KERNEL); mm->free_trees[i] = kmalloc_array(mm->max_order + 1, sizeof(struct rb_root), GFP_KERNEL);
mm->roots = kmalloc_array(mm->n_roots, sizeof(struct gpu_buddy_block *), GFP_KERNEL);
|
GPU Buddy 从内核借用了三种内存:
kmem_cache: 每个 gpu_buddy_block 节点(约 72 字节)
kmalloc_array: 红黑树根指针数组(struct rb_root *[])
kmalloc_array: 根块指针数组(struct gpu_buddy_block *[])
它管理的 GPU 显存本身不经过内核页分配器,只是用内核内存来”记账”。
初始化
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
| int gpu_buddy_init(struct gpu_buddy *mm, u64 size, u64 chunk_size) { size = round_down(size, chunk_size); mm->size = size; mm->avail = size; mm->clear_avail = 0; mm->chunk_size = chunk_size; mm->max_order = ilog2(size) - ilog2(chunk_size);
mm->free_trees = kmalloc_array(GPU_BUDDY_MAX_FREE_TREES, ...); for_each_free_tree(i) { mm->free_trees[i] = kmalloc_array(mm->max_order + 1, ...); for (j = 0; j <= mm->max_order; ++j) mm->free_trees[i][j] = RB_ROOT; }
mm->n_roots = hweight64(size); mm->roots = kmalloc_array(mm->n_roots, ...);
do { order = ilog2(size) - ilog2(chunk_size); root_size = chunk_size << order; root = gpu_block_alloc(mm, NULL, order, offset); mark_free(mm, root); mm->roots[root_count] = root; offset += root_size; size -= root_size; } while (size); }
|
非 2 的幂大小的处理:当 size 不是 2 的幂时(例如 6GB = 4GB + 2GB),hweight64(size) 返回二进制中 1 的个数(6GB = 0x180000000,1 个 1),n_roots 就是子树根的数量。每个 root 是最大的 2 的幂块,恰好填满剩余空间。
例如 size=0x180000000 (6GB):
- 第一个 root:
1 << floor(log2(6GB)) = 4GB, offset=0
- 第二个 root:
1 << floor(log2(2GB)) = 2GB, offset=4GB
分配路径
1 2 3 4 5 6
| int gpu_buddy_alloc_blocks(struct gpu_buddy *mm, u64 start, u64 end, u64 size, u64 min_block_size, struct list_head *blocks, unsigned long flags)
|
三种分配策略,由 flags 控制:
GPU_BUDDY_RANGE_ALLOCATION: 范围受限分配 (__gpu_buddy_alloc_range_bias)
- 从根块开始 DFS 遍历,找到包含在
[start, end) 范围内的空闲块
- 沿途分裂过大的块
偏移对齐分配 (gpu_buddy_offset_aligned_allocation): 当 size < min_block_size 且 size 为 2 的幂时
- 利用增广红黑树的
subtree_max_alignment 字段高效搜索对齐块
- 优先右子树(高地址),找到满足对齐约束的块
自由分配 (alloc_from_freetree): 无范围限制
- 从优先红黑树(clear 或 dirty)中搜索
- 支持
GPU_BUDDY_TOPDOWN_ALLOCATION(从高地址分配)
- 优先树耗尽后 fallback 到另一棵树
- 找到的块若阶数大于请求阶数,循环分裂
分配完成后支持 GPU_BUDDY_TRIM_DISABLE 控制的自动 Trim:将未使用的多余空间归还。
分裂逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static int split_block(struct gpu_buddy *mm, struct gpu_buddy_block *block) { unsigned int block_order = gpu_buddy_block_order(block) - 1; u64 offset = gpu_buddy_block_offset(block);
block->left = gpu_block_alloc(mm, block, block_order, offset); block->right = gpu_block_alloc(mm, block, block_order, offset + (mm->chunk_size << block_order)); mark_split(mm, block);
if (gpu_buddy_block_is_clear(block)) { mark_cleared(block->left); mark_cleared(block->right); clear_reset(block); }
mark_free(mm, block->left); mark_free(mm, block->right); }
|
分裂一个 order=n 的空闲块时:
- 从
kmem_cache 分配两个新 gpu_buddy_block 作为 left/right
- 父块标记为 SPLIT,从空闲红黑树移除
- 左右子块标记为 FREE,插入对应红黑树
- 若父块原本是 clear,子块继承 clear,父块清除 clear 标志
释放与合并
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
| static unsigned int __gpu_buddy_free(struct gpu_buddy *mm, struct gpu_buddy_block *block, bool force_merge) { struct gpu_buddy_block *parent;
while ((parent = block->parent)) { buddy = __get_buddy(block);
if (!gpu_buddy_block_is_free(buddy)) break;
if (!force_merge) { if (gpu_buddy_block_is_clear(block) != gpu_buddy_block_is_clear(buddy)) break;
if (gpu_buddy_block_is_clear(block)) mark_cleared(parent); }
rbtree_remove(mm, buddy); gpu_block_free(mm, block); gpu_block_free(mm, buddy); block = parent; }
mark_free(mm, block); }
|
合并逻辑与内核 Buddy 类似但有关键差异:
- clear/dirty 屏障:两个伙伴 clear 状态不同时阻止合并(
force_merge=false)。这是 GPU 特有的优化 — 清零和未清零内存不能混并。
- 归还到 kmem_cache:合并后子节点 block 对象归还到 SLUB,而非释放”物理页面”。
强制合并 (__force_merge)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| static int __force_merge(struct gpu_buddy *mm, u64 start, u64 end, unsigned int min_order) { for_each_free_tree(tree) { for (i = min_order - 1; i >= 0; i--) { iter = rb_last(&mm->free_trees[tree][i]); while (iter) { block = rbtree_get_free_block(iter); buddy = __get_buddy(block); if (gpu_buddy_block_is_free(buddy)) { gpu_buddy_assert(gpu_buddy_block_is_clear(block) != gpu_buddy_block_is_clear(buddy)); __gpu_buddy_free(mm, block, true); if (order >= min_order) return 0; } } } } }
|
当正常分配路径失败时,__force_merge 被调用以强制合并 clear 和 dirty 伙伴,打破 clear/dirty 碎片化,生成更大的块。
增广红黑树与对齐分配
1 2 3 4 5
| RB_DECLARE_CALLBACKS_MAX(static, gpu_buddy_augment_cb, struct gpu_buddy_block, rb, unsigned int, subtree_max_alignment, gpu_buddy_block_offset_alignment);
|
红黑树按 offset 排序,增广属性 subtree_max_alignment 记录了子树中块的最大对齐值(__ffs64(offset))。这使得可以 O(log n) 搜索满足对齐约束的空闲块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static struct gpu_buddy_block * gpu_buddy_find_block_aligned(struct gpu_buddy *mm, ...) { while (rb) { block = rbtree_get_free_block(rb); if (right_node && gpu_buddy_subtree_can_satisfy(right_node, alignment)) { rb = right_node; continue; } if (gpu_buddy_block_offset_alignment(block) >= alignment) return block; if (left_node && gpu_buddy_subtree_can_satisfy(left_node, alignment)) { rb = left_node; continue; } break; } }
|
这是 GPU Buddy 独有的特性 — 内核 Buddy 只支持按页边界对齐,不支持任意 2 的幂对齐。
三者的层次关系
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
| ┌─────────────────────────────────┐ │ 用户程序 │ └───────────────┬─────────────────┘ │ ┌─────────────────────────────────────┼──────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ kmalloc() │ │ GPU 驱动 │ │ kmem_cache_alloc │ │ (通用堆分配) │ │ (i915/amdgpu等) │ │ (专用对象缓存) │ └───────┬───────┘ └────────┬────────┘ └────────┬─────────┘ │ │ │ ▼ ▼ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ SLUB (mm/slub.c) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │main sheaf │ │spare sheaf │ │rcu_free │ per-CPU 无锁缓存 │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────┘ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ kmem_cache_node │ per-node 链表 │ │ │ (partial/full slab)│ │ │ └─────────┬───────────┘ │ └──────────────────────────┼────────────────────────────────────────────────┘ │ new_slab() / free_slab() ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ 内核 Buddy (mm/page_alloc.c) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ zone->free_area[order].free_list[MIGRATE_TYPES] │ │ │ └──────────────────────────────────────────────────────────┘ │ │ 管理:物理 RAM 页框 (struct page) │ └───────────────────────────────────────────────────────────────────────────┘
╔═══════════════════════════════════╗ ║ GPU Buddy (drivers/gpu/buddy.c) ║ ║ ║ ║ ┌─────────────────────────────┐ ║ ║ │ free_trees[clear/dirty] │ ║ ║ │ [order].rb_root (RB树) │ ║ ║ └─────────────────────────────┘ ║ ║ 管理:GPU 显存地址空间 (u64) ║ ║ ║ ║ 元数据来源: ║ ║ gpu_buddy_block ──► kmem_cache ║ ║ RB 树根数组 ──► kmalloc ║ ╚═══════════════════════════════════╝
|
关键路径:
- 用户申请内核内存 →
kmalloc() → SLUB per-CPU sheaf → (if miss) → 内核 Buddy 申请 slab page → 切成对象返回
- GPU 驱动申请显存 →
gpu_buddy_alloc_blocks() → 红黑树查找 → 分裂 → 返回 block → 驱动据此操作 GPU 显存
- GPU Buddy 申请元数据 →
kmem_cache_zalloc(slab_blocks) → SLUB → gpu_buddy_block 节点
关键差异对比表
| 维度 |
内核 Buddy (page_alloc) |
SLUB (slab allocator) |
GPU Buddy |
| 源码位置 |
mm/page_alloc.c |
mm/slub.c |
drivers/gpu/buddy.c |
| 管理对象 |
物理 RAM 页框 |
内核堆对象 |
GPU 显存地址空间 |
| 分配器类型 |
块/页分配器 (binary buddy) |
对象/slab 分配器 |
块/页分配器 (binary buddy) |
| 分配粒度 |
1 页 (4KB),order 0..MAX_ORDER |
8B ~ 8KB+,由 cache object_size 决定 |
chunk_size 字节 (>=4KB),order 0..n |
| 空闲块结构 |
双向链表 free_list[MIGRATE_TYPES] |
per-CPU sheaf + per-node partial 链表 |
增广红黑树 free_trees[2][order] |
| 伙伴合并 |
__free_one_page() 循环向上,检查迁移类型 |
不合并 — 对象回到 slab freelist |
__gpu_buddy_free() 循环向上,检查 clear/dirty |
| 合并屏障 |
迁移类型不兼容 |
N/A |
clear/dirty 不匹配(可 force_merge 打破) |
| 分裂 |
__rmqueue_smallest() 向上搜索后向下分裂 |
N/A |
split_block() 从 kmem_cache 分配左右子节点 |
| 元数据位置 |
struct page 内嵌在 mem_map 中 |
kmem_cache + struct slab(在 page 元数据区) |
独立 kmem_cache 分配 gpu_buddy_block |
| 对齐支持 |
仅页对齐 (4KB) |
可配置 per-cache align |
增广 RB 树支持任意 2 的幂对齐 |
| 分配策略 |
preferred migratetype 链表头部取 |
per-CPU 无锁 sheaf |
clear 优先 + top-down + 范围约束 |
| 碎片化控制 |
多迁移类型 + pageblock 隔离 + compaction |
无(依赖底层 Buddy) |
clear/dirty 双树 + force_merge + top-down |
| 锁粒度 |
zone spinlock (IRQ-safe) |
local_trylock (per-CPU) + node spinlock |
外部 — 调用者提供 mutex |
| NUMA 感知 |
有(per-zone, per-node) |
有(per-node partial, per-CPU) |
无 |
| 存储依赖 |
管理的页本身就是物理内存 |
从内核 Buddy 申请 slab page |
只从内核 SLUB 申请元数据,显存独立管理 |
| 最大分配 |
MAX_PAGE_ORDER (10, ~4MB 典型) |
KMALLOC_MAX_SIZE (通常 8KB, 大分配走页分配器) |
GPU_BUDDY_MAX_ORDER=51 即 chunk_size << 51 |
GPU Buddy 为什么用红黑树而不是链表?
内核 Buddy 的 free_list 链表方案在以下条件成立时运作良好:
- 伙伴通过
pfn ^ (1 << order) 位运算直接算出
- 物理页是连续的线性地址空间
- 不需要按地址范围搜索(分配时不指定地址)
GPU Buddy 需要的能力链表无法满足:
- 范围约束分配(
GPU_BUDDY_RANGE_ALLOCATION):需要在 [start, end) 范围内找到空闲块
- 对齐分配(
gpu_buddy_offset_aligned_allocation):需要搜索满足 __ffs64(offset) >= alignment 的块
- Top-down 分配:需要从最高地址开始分配
红黑树按 offset 排序 + subtree_max_alignment 增广,使得上述操作都是 O(log n)。链表只能做 O(n) 线性扫描。
GPU Buddy 与内核 Buddy 的关键行为差异
1. 元数据与数据分离
内核 Buddy 中 struct page 嵌入在 mem_map[] 数组中,与管理的物理内存共存。每个 struct page 约 64 字节,直接对应一个物理页。
GPU Buddy 的 gpu_buddy_block 从 kmem_cache 独立分配,与管理的显存完全分离。显存大小与 block 节点数量无直接耦合。
2. 状态机
1 2 3 4 5 6 7 8 9 10
| 内核 Buddy 状态机(简化): PageBuddy flag + page->private(order) → free PageBuddy unset → allocated
GPU Buddy 状态机: FREE → 在红黑树中,可分配 ALLOCATED → 已分配,从红黑树移除 SPLIT → 已分裂为左右子节点,从红黑树移除 FREE+clear → 在 clear 树中 FREE+
|
GPU Buddy 多了一个 SPLIT 状态和 clear/dirty 属性。
3. Clear/Dirty 双树
这是 GPU 特有的优化。GPU 显存清零(memset to 0)是昂贵的操作。通过维护两棵红黑树:
CLEAR_TREE:内容已清零的空闲块
DIRTY_TREE:内容不确定的空闲块
分配时优先使用 CLEAR_TREE 可避免 GPU 清零开销。释放时通过 GPU_BUDDY_CLEARED 标志告知块已清零。
总结
|
内核 Buddy |
SLUB |
GPU Buddy |
| 核心 |
管理物理页,链表+迁移类型 |
管理堆对象,per-CPU 无锁缓存 |
管理显存地址,增广红黑树 |
| 借用内核 |
—(最底层) |
从内核 Buddy 拿 slab page |
从 SLUB 拿 block 节点 |
| 产出 |
连续物理页框 |
固定大小堆对象 |
GPU 显存地址范围 |
| 独有特性 |
迁移类型隔离、compaction |
per-CPU sheaf、kfree_rcu 批量 |
clear/dirty 双树、对齐分配、范围约束、force_merge |