Post content
我越来越觉得 bpf 射区对 percpu 的一些文档和用例是错误的,但我还不能确定,如果有老师能指出我的错误,我愿意 _ _ _ _ 🙏 如果稍微用 bpftrace <v0.41 用得多一点,可以看到这种报错 Looks like the BPF stack limit of 512 bytes is exceeded. Please move large on stack variables into BPF per-cpu array map. 这是由于 bpf 的 stack 限制是 512 bytes,如果要在栈上分配巨型 buff (结构体、字符串)就会报错。解决办法正如最后一句提醒所说,可以把巨型 buff 放到 per-cpu map 里,这是 bpf 社区里著名的最佳实践了,大概是这个意思: // sizeof(struct big) == 512, verifier decline! - struct big big = {}; // define a percpu_map whose entry value is of size 512, verifier pass! + struct big *big = bpf_map_lookup_elem(&percpu_map, &ZERO); 在 bpftrace 维护者 dxu 的博客里专门讲了这个问题,需要 ast 解析出全部的大字符串的总长度然后把字符串扔进 percpu map: https://dxuuu.xyz/big-strings.html 在 dylan 维护的 bpf doc 里,bpf 并发这一页也说 percpu map 是解决并发问题的好手段,简单直观,唯一的问题是 at cost of memory: https://docs.ebpf.io/linux/concepts/concurrency/ 在 cilium/cilium 里大量使用 percpu map 在 tailcall 之间传递数据,如这里 xdp meta: https://github.com/cilium/cilium/blob/v1.19.0-pre.1/bpf/include/bpf/ctx/xdp.h#L403 在 cilium/pwru 里我为了解决 __sync_fetch_and_add() 在 5.10 内核上无法运行的问题使用 percpu map 来模拟 sync_fetch_and_add(): https://github.com/cilium/pwru/blob/v1.0.10/bpf/kprobe_pwru.c#L414 在我很喜欢的工具 sockdump 里 (用来观测 unixsock stream) ,手动维护 percpu map 来临时存储 buff 再推给用户态: https://github.com/mechpen/sockdump/blob/d40ec77e960d021861220bc14a273c5dcad13160/sockdump.py#L40 在内核 bpf 子系统 tailcall 维护者 Leon 的 bpfsnoop 里也使用 percpu map 来临时存储 bpfsnoop_fgraph_tailcall_data: https://github.com/bpfsnoop/bpfsnoop/blob/v0.5.3/bpf/bpfsnoop_fgraph.c#L67 在 cilium latest doc (v1.18) 里说,bpf 程序运行时不会被内核抢占,所以请放心把 percpu 用做全局变量、规避 bpf stack 限制: https://docs.cilium.io/en/latest/reference-guides/bpf/toolchain/index.html However, there is a work-around in that the program can simply use a BPF map of type BPF_MAP_TYPE_PERCPU_ARRAY with just a single slot of arbitrary value size. This works, because during execution, BPF programs are guaranteed to never get preempted by the kernel and therefore can use the single map entry as a scratch buffer for temporary data, for example, to extend beyond the stack limitation. This also functions across tail calls, since it has the same guarantees with regards to preemption. 然而,我怀疑上面所有的资料里,只有一处是正确的,那就是 cilium/cilium 在 xdp 里使用 percpu 在 tailcall 之间传递数据,因为上面的场景里只有 native xdp / tc bpf 可以盖棺定论地说,软中断关闭,抢占调度禁止,可以安全使用 percpu map entry。 在其他的一般场景,比如 kprobe on unix_stream_sendmsg(), 由于 >v5.11 的内核正确地实现了 migrate_disable() [1],普通 bpf prog 只禁止了 cpu migration,但没有禁止 preempt[2],所以可能出现 prog1 运行到一半,已经在修改 percpu buffer 了,然后被抢占,在相同的 cpu 上运行 prog2,也修改 percpu buffer,最后切回 prog1 的时候 percpu buffer 已经被玩烂,救不回来了。 [1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=74d862b682f51e45d25b95b1ecf212428a4967b0 [2] https://lwn.net/Articles/812503/ 我目前的改法是使用 ringbuf_reserve 来获取一个大 buffer,用起来感觉还可以,就是不灵活,size 必须是常数,我最爱的变长 msg ringbuf 从此就只能说再见了。。。 👋 因此我之前写的这条 post 千万不要读,基本是错的,完全没有考虑中断抢占的情况: https://t.me/c/1459082815/752 (更多惊喜见昨天的 lwn,想象一下 bpf_spin_lock 遇上 preempt 会发生什么😊: https://lwn.net/SubscriberLink/1039374/986a9adb75d63ae0/) 软件工程太勃大茎深了😋