vmalloc: 不连续物理内存分配与 vmap
参考
概览
vmalloc 的核心是在 vmalloc 区域中找到合适的 hole,hole 是虚拟地址连续的;然后逐页分配内存来从物理上填充 hole。
vmalloc 的 gfp_maks 和逐页分配就决定了它的属性:
- 可能睡眠:不能从中断上下文中调用,或其他不允许阻塞情况下调用。
- 虚拟地址连续:要建立页表,开销比 kmalloc 大
- 物理地址不连续:要多次分配页面,开销比 kmalloc 大
- size 对齐到页:不适合小内存分配
分配得到的虚拟地址在虚拟地址空间布局中的“vmalloc/ioremap space”区域,[ffffc90000000000, ffffe8ffffffffff]
32 TB。
关键流程
- 在 vmalloc/ioremap space 区域分配一个连续虚拟地址空间
- 从 Buddy System 多次分配 order 为 0 的页面,这些页面不连续。
- 通过 vmap 机制,连续的虚拟地址映射到不连续的物理页面。
vmap
vmalloc 基于 vmap,先来介绍 vmap。
在内核的的 vmalloc 区域中,选取一段连续的虚拟地址区域,映射到 page 数组代表的不连续物理页面,返回虚拟地址。
void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot);
vmap 应用场景
per_cpu 的 hardirq stack
代码路径:
start_kernel->init_IRQ->irq_init_percpu_irqstack->map_irq_stack
dma-buf。
详见 dma-buf
sudo bpftrace -e 'kfunc:vmlinux:__vmalloc_node_range_noprof { @[kstack] = count(); }'
@[
bpf_prog_6deef7357e7b4530_sd_fw_ingress+6685
bpf_prog_6deef7357e7b4530_sd_fw_ingress+6685
bpf_trampoline_6442530470+111
__vmalloc_node_range_noprof+9
# dup_task_struct alloc_thread_stack_node
copy_process+3049
kernel_clone+189
__do_sys_clone3+228
do_syscall_64+130
entry_SYSCALL_64_after_hwframe+118
]: 35
2
3
4
5
6
7
8
9
10
11
12
13
14
数据结构
static LIST_HEAD(free_vmap_area_list);
/* 空闲的未被使用的 vmalloc 区域 */
static struct rb_root free_vmap_area_root = RB_ROOT;
/* 用于描述一段虚拟地址的区域 */
struct vmap_area {
unsigned long va_start;
unsigned long va_end;
/* 属于以下 3 个红黑树之一:
- free_vmap_area_root。表明未被使用的区域。
- vmap_node 的 busy。表明已被分配,正在被使用
- vmap_node 的 lazy。表明该区域未被使用,处于正在 lazy 释放的阶段。
*/
struct rb_node rb_node;
/* 同 rb_node,属于 3 个链表之一,free_vmap_area_list、busy、lazy */
struct list_head list;
union {
unsigned long subtree_max_size;
struct vm_struct *vm; /* 指向一个 vm_struct 单向链表 */
};
unsigned long flags; /* mark type of vm_map_ram area */
};
/* 管理虚拟地址和物理页之间的关系 */
struct vm_struct {
struct vm_struct *next; /* XXX 串成一个单向链表,干啥的? */
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
#ifdef CONFIG_HAVE_ARCH_HUGE_VMALLOC
unsigned int page_order;
#endif
unsigned int nr_pages;
phys_addr_t phys_addr;
const void *caller;
};
/* 该结构体用于大锁化小锁,降低锁争用,后文会分析 */
static struct vmap_node {
/* Simple size segregated storage. */
struct vmap_pool pool[MAX_VA_SIZE_PAGES];
spinlock_t pool_lock;
bool skip_populate;
/* 已经被分配,正在被使用的 vmap_area */
struct rb_list busy;
/* vunmap 的 vmap_area 会先挂在这颗红黑树上,等待被释放 */
struct rb_list lazy;
/*
* Ready-to-free areas.
*/
struct list_head purge_list;
struct work_struct purge_work;
unsigned long nr_purged;
};
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
vmap 代码分析
vmap()
struct vm_struct *area = get_vm_area_caller()->__get_vm_area_node()
BUG_ON(in_interrupt()); /* 不能在中断上下文使用 */
struct vm_struct *area = kzalloc_node()
/* 分配一个 vmap_area */
struct vmap_area *va = alloc_vmap_area(..., area)
/* 优先从 vmap_pool 分配 vmap_area */
va = node_alloc();
/* 否则从 slab 分配 */
if (!va) va = kmem_cache_alloc_node(vmap_area_cachep);
/* 如果不是从 vmap_pool 分配的,还需要这一步分配一个虚拟地址出来 */
__alloc_vmap_area()
find_vmap_lowest_match() /* 深度遍历红黑树,得到合适的 va(并没有从红黑树中取下) */
va_alloc()->va_clip() /* 修改红黑树中的那个 va,剪掉我们要使用的那一段区域 */
/* 将 vmap_area 放进 vmap_node 的 busy 红黑树和链表 */
struct vmap_node *vn = addr_to_node(va->va_start);
vmap_pages_range(area->addr, .., pages)
return area->addr;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来分析 vmap 演进历史中几个比较重要的 patch。
[patch] mm: rewrite vmap layer - Nick Piggin
db64fe02258f1507e13fe5212a989922323685ce mm: rewrite vmap layer
存在的问题:
- vmap 最大的问题是 vunmap。目前需要一个 global kernel TLB flush,在大多数架构上,是一个广播给所有 CPU 的用于 flush cache 的 IPI,而且需要用一个全局锁。随着 CPU 数量增加,有伸缩性问题。
- 另一个问题是,整个 vmap 用了一个读写锁,而这个读写锁很少读并行,大多数时候是在 fast path 进行写。
解决方式:
- lazy TLB unmapping。在 vunmap 后,不会立即 flush TLB。而是在某次 TLB flush 时,同时 flush 多个 vunmap 的地址。XEN 和 PAT 不会这样做,原因我懒得看。
- 虚拟地址的额外信息保存在红黑树里,提升算法可伸缩性。
- 对于小的 vmap,使用 per-cpu 的分配器,避免全局锁。
[PATCH v3 00/11] Mitigate a vmap lock contention v3 - Uladzislau Rezki (Sony)
挑几个比较重要的进行分析。
d093602919ad mm: vmalloc: remove global vmap_area_root rb-tree
大锁化小锁,减少锁争用。
引入
struct vmap_node
,对于不同范围内的虚拟地址,通过addr_to_node(va)
函数,使用不同的struct vmap_node
,从而减少锁争用。 将原先全局的vmap_area_root
红黑树(用于记录已被使用的 vmap_area),改为 per-vmap_node 的busy
红黑树。 将原先全局的vmap_area_lock
锁,改为 per-vmap_node 的锁。282631cb2447 mm: vmalloc: remove global purge_vmap_area_root rb-tree
大锁化小锁,减少锁争用。
将原先全局的
purge_vmap_area_root
红黑树(用于记录。。。),改为 per-vmap_node 的lazy
红黑树。 将原先全局的purge_vmap_area_lock
锁,改为了 per-vmap_node 的锁。减少了锁争用。72210662c5a2 mm: vmalloc: offload free_vmap_area_lock lock
减少对
free_vmap_area_lock
锁的争用。在每个
struct vmap_node
内新增struct vmap_pool
数组,将不同大小的空闲的struct vmap_area
放在不同的池子里进行缓存。在
alloc_vmap_area()
时,只有对应的 vmap_pool 空了时,才会通过kmem_cache_alloc_node()
从 SLUB 中分配struct vmap_area
。如果成功从 vmap_pool 中分配了,虽说分配时需要占用 per-vmap_node 的 pool_lock,但后面不需要__alloc_vmap_area()
,因此省去了对free_vmap_area_lock
这个大锁的争用。用于 lazy TLB vunmap 的
__purge_vmap_area_lazy()
函数,会从 lazy 链表中的 va 移动到 purge_list,如果需要移除的 va 较多,则使用 work queue 处理,否则直接就地purge_vmap_node()
将 va 放回 vmap_pool,如果 vmap_pool 满了,则放回全局的 free_vmap_area_root 红黑树和 free_vmap_area_list 链表。53becf32aec1 mm: vmalloc: support multiple nodes in vread_iter
8e1d743f2c26 mm: vmalloc: support multiple nodes in vmallocinfo
8f33a2ff3072 mm: vmalloc: set nr_nodes based on CPUs in a system
此时才会真正地用上多个 vmap_node
7679ba6b36db mm: vmalloc: add a shrinker to drain vmap pools
注册了一个 shrinker,用于在必要时缩小 vmap pool
这是一个很典型的内存管理中锁的典型优化案例,其他案例:
vmalloc
接口在 include/linux/vmalloc.h
vmalloc()
默认使用 GFP_KERNEL
如果想指定 gfp_mask,应使用 __vmalloc()
vmalloc_huge()
如果可以,会分配大页。
最终都会调用到核心函数 __vmalloc_node_range_noprof()
关于 _noprof
见buddy system。
__vmalloc_node_range_noprof
struct vm_struct *area = __get_vm_area_node()
/* 分配页面并 vmap */
__vmalloc_area_node()
vm_area_alloc_pages()->alloc_pages_bulk_array_mempolicy_noprof()
alloc_pages_bulk_noprof() /* 分配 0 阶页面 */
vmap_pages_range()->..->vmap_small_pages_range_noflush()
2
3
4
5
6
7
可以看到,与 vmap 的流程相比,vmalloc 只多出了一个 alloc_pages_bulk_noprof()
分配内存的动作,该函数详见 buddy system。
想水 patch 了
struct vmap_area
的注释里的vmap_area_root
现在已经无了