在上一集 eBPF Talk: 动态或静态 tailcall 中,我们知道了 tailcall
有动态和静态之分。
对于动态 tailcall
而言,更新 PROG_ARRAY bpf map 时就比较简单,只需要更新一下 array->ptrs
数组即可。
但对于静态 tailcall
而言,更新 PROG_ARRAY bpf map 时,是如何更新那些 bpf prog 的汇编指令的呢?
内核函数调用栈
直接翻看内核源码:
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
|
__sys_bpf() // ${KERNEL}/kernel/bpf/syscall.c
|-->map_update_elem()
|-->bpf_map_update_value()
|-->bpf_fd_array_map_update_elem() { // ${KERNEL}/kernel/bpf/arraymap.c
ufd = *(u32 *)value;
new_ptr = map->ops->map_fd_get_ptr(map, map_file, ufd);
if (IS_ERR(new_ptr))
return PTR_ERR(new_ptr);
if (map->ops->map_poke_run) {
mutex_lock(&array->aux->poke_mutex);
old_ptr = xchg(array->ptrs + index, new_ptr);
map->ops->map_poke_run(map, index, old_ptr, new_ptr);
mutex_unlock(&array->aux->poke_mutex);
} else {
old_ptr = xchg(array->ptrs + index, new_ptr);
}
if (old_ptr)
map->ops->map_fd_put_ptr(old_ptr);
}
/
/
/
|-->prog_array_map_poke_run() { // ${KERNEL}/kernel/bpf/arraymap.c
list_for_each_entry(elem, &aux->poke_progs, list) {
struct bpf_jit_poke_descriptor *poke;
int i, ret;
aux = container_of(map, struct bpf_array, map)->aux;
for (i = 0; i < elem->aux->size_poke_tab; i++) {
poke = &elem->aux->poke_tab[i];
// ...
if (poke->tail_call.map != map ||
poke->tail_call.key != key)
continue;
old_bypass_addr = old ? NULL : poke->bypass_addr;
old_addr = old ? (u8 *)old->bpf_func + poke->adj_off : NULL;
new_addr = new ? (u8 *)new->bpf_func + poke->adj_off : NULL;
if (new) {
ret = bpf_arch_text_poke(poke->tailcall_target,
BPF_MOD_JUMP,
old_addr, new_addr);
BUG_ON(ret < 0 && ret != -EINVAL);
if (!old) {
ret = bpf_arch_text_poke(poke->tailcall_bypass,
BPF_MOD_JUMP,
poke->bypass_addr,
NULL);
BUG_ON(ret < 0 && ret != -EINVAL);
}
} else {
ret = bpf_arch_text_poke(poke->tailcall_bypass,
BPF_MOD_JUMP,
old_bypass_addr,
poke->bypass_addr);
BUG_ON(ret < 0 && ret != -EINVAL);
/* let other CPUs finish the execution of program
* so that it will not possible to expose them
* to invalid nop, stack unwind, nop state
*/
if (!ret)
synchronize_rcu();
ret = bpf_arch_text_poke(poke->tailcall_target,
BPF_MOD_JUMP,
old_addr, NULL);
BUG_ON(ret < 0 && ret != -EINVAL);
}
}
}
}
|
上面代码片段的关键地方在于:
array->ptrs
数组保存的是 bpf prog 的指针。
array->aux->poke_progs
链表保存的是 静态地 使用该 PROG_ARRAY bpf map 的 bpf prog 的 aux
信息。
- bpf prog 的
aux
里保存有 poke
数组。
poke
信息里保存有两个关键地址:tailcall_bypass
和 tailcall_target
。
bpf_arch_text_poke()
用于更新汇编指令。
上面代码片段的主要逻辑是:
- 更新
array->ptrs
数组。
- 遍历
array->aux->poke_progs
链表,拿到 bpf prog 的 aux
信息。
- 遍历
aux->poke_tab
数组,更新 poke
信息里的 tailcall_bypass
和 tailcall_target
地址上的汇编指令。
不过,poke
信息是何时添加到 array->aux->poke_progs
链表的呢?
添加 poke
信息
在 verifier 阶段,对 PROG_ARRAY bpf map 进行处理的时候,就会添加 poke
信息。
verifier 阶段较为复杂,在 JIT bpf prog 时生成 aux->poke_tab
数组;在 verifier 阶段后期的 do_misc_fixups()
函数中,会将 bpf prog 和 PROG_ARRAY bpf map 关联起来。
JIT bpf prog 时生成 aux->poke_tab
数组的代码如下:
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
|
bpf_check() // ${KERNEL}/kernel/bpf/verifier.c
|-->do_misc_fixups() {
| if (insn->imm == BPF_FUNC_tail_call)
| bpf_jit_add_poke_descriptor(prog, &desc) // ${KERNEL}/kernel/bpf/core.c
|
| /* Since poke tab is now finalized, publish aux to tracker. */
| for (i = 0; i < prog->aux->size_poke_tab; i++) {
| map_ptr = prog->aux->poke_tab[i].tail_call.map;
| // ...
| ret = map_ptr->ops->map_poke_track(map_ptr, prog->aux);
| // ...
| }
| }
| \
| \
| \
| |-->prog_array_map_poke_track() // ${KERNEL}/kernel/bpf/arraymap.c
| list_add_tail(&elem->list, &aux->poke_progs);
|-->fixup_call_args()
|-->jit_subprogs()
|-->bpf_int_jit_compile() // ${KERNEL}/arch/x86/net/bpf_jit_comp.c
|-->do_jit() {
case BPF_JMP | BPF_TAIL_CALL:
if (imm32)
emit_bpf_tail_call_direct(&bpf_prog->aux->poke_tab[imm32 - 1],
&prog, image + addrs[i - 1],
callee_regs_used,
bpf_prog->aux->stack_depth,
ctx);
else
emit_bpf_tail_call_indirect(&prog,
callee_regs_used,
bpf_prog->aux->stack_depth,
image + addrs[i - 1],
ctx);
break;
}
|-->emit_bpf_tail_call_direct() {
poke->tailcall_bypass = ip + (prog - start);
poke->adj_off = X86_TAIL_CALL_OFFSET;
poke->tailcall_target = ip + ctx->tail_call_direct_label - X86_PATCH_SIZE;
poke->bypass_addr = (u8 *)poke->tailcall_target + X86_PATCH_SIZE;
}
|
上面代码片段的主要处理逻辑:
- 分配
aux->poke_tab
数组。
- 将 bpf prog 和 PROG_ARRAY bpf map 关联起来。
- JIT bpf prog 时,更新
poke
信息。
小结
通过上面的分析,我们知道了 PROG_ARRAY bpf map 的实现细节:
- 在 verifier 阶段准备好 bpf prog 的
aux->poke_tab
数组。
- 在更新 PROG_ARRAY bpf map 时,更新
poke
信息里的 tailcall_bypass
和 tailcall_target
地址上的汇编指令。
尽管静态 tailcall
的实现细节繁多,相信你对静态 tailcall
有了更深的理解。
P.S. 从中学到了一点经验:控制逻辑让位于运行时;为了更高的运行时性能,带来更复杂的控制逻辑。
纸上得来终觉浅,绝知此事要躬行。弄个 demo 验证一下静态 tailcall
的实现细节:eBPF Talk: 共享的 tailcall PROG_ARRAY bpf map。