eBPF 对并发编程的支持是非常弱的
有一些讨论:
- https://lists.iovisor.org/g/iovisor-dev/topic/bpf_concurrency/74407447
- https://lwn.net/Articles/779120/
但是你甚至在 eBPF 的官方文档 https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md 里面几乎找不到任何关于并发的描述,除了 map.atomic_increment() 接口
但是这个借口也很奇怪。为了突出这个接口是原子的,线程安全的,难道其他接口就不是了吗?但好像又不是,因为 eBPF 里有大量的示例,并没有对 map 做特殊的并发控制,所以理论上可以认为,其他大部分 map 操作接口,也是线程安全的
线程安全验证
测试代码如下:
// calculate latency int trace_run(struct pt_regs *ctx, struct task_struct *prev) { u32 pid = bpf_get_current_pid_tgid(); u64 *tsp; if (pid <= 1) { return 0; } pid = 10000; u64 ts = bpf_ktime_get_ns(); start.update(&pid, &ts); #define __OP__ == tsp = start.lookup(&pid); if (tsp __OP__ 0) { dist.increment(bpf_log2l(1)); } tsp = start.lookup(&pid); if (tsp __OP__ 0) { dist.increment(bpf_log2l(1)); } /** 重复N次 **/ tsp = start.lookup(&pid); if (tsp __OP__ 0) { dist.increment(bpf_log2l(1)); } tsp = start.lookup(&pid); if (tsp __OP__ 0) { dist.increment(bpf_log2l(1)); } start.delete(&pid); return 0; }
这段代码,修改自 bbc 工具集自带的 runqlat.py
这段代码实际上是在构造一个冲突场景,来验证 trace_run 到底是并行执行还是串行执行的。代码的逻辑是在一个超高频触发的钩子(调度器进行上下文切换)函数里,先在 map 里插入一个固定key(pid)的元素,然后查询 N 次,每次查询如果 key 不存在,计数器就 +1,最后把 key 从 map 里面删除掉,如果:
- trance_run 是串行的,那么技术器永远都是 0,因为在一个串行执行的逻辑里,插入元素后,不管查询多少次,元素必然是存在的
- 如果 trace_run 是并行的,那么就存在一个条件,一定有一个 cpu,在另外一个 cpu 查询 key 是否存在的时候,执行到了函数结束的地方,把 key 从 map 里面删掉了
事实证明,trace_run 是并行执行的,并且中间 lookup 的次数越多,冲突的概率就越大
想想肯定是这样的,内核不可能保证同一个函数的hook点全部串行执行,不然效率得多差啊
既然确认了这一点,写 eBPF 程序的时候,就得自己做好并发控制了
并发编程
1. 原子操作
bcc 目前并没有统一的原子指令解决方案,由于bcc本身的运行环境很特殊,runtime是内核的一个虚拟机,其运行机制本身就有诸多限制,所以原子操作的支持也是很不完备的,各种坑
从目前了解到的信息来看,bcc支持的原子操作主要2种:
- eBPF 原子指令,这是最底层的,eBPF 虚拟机提供的指令级别的原子操作
- llvm 内置的原子操作,比如 __sync_and_fetch_add
- atomic_increment() API:这是 bcc 官方提供的 helper_function
1.1. eBPF 原子指令
大概21年春节的时候,google给社区提了个patch,开始正是为eBPF指令集引入原子操作
详细可以看这里:
https://lwn.net/Articles/842354/
支持的指令集包括:
- atomic[64][fetch]add
- atomic[64][fetch]and
- atomic[64][fetch]or
- atomic[64]_xchg
- atomic[64]_cmpxchg
但只支持64位的,不支持16位和32位,也不支持 Explicit memory barriers
目前来看,由于引入时间还没多久,还没看到 bcc 有相应的支持
1.2. llvm 内置的原子操作
由于bcc自带了一个 llvm 前端,因此可以使用一些 llvm 内置的原子操作函数。
注意:实际使用时最好自行确保函数语义是符合预期的,因为bcc编译有时候非常坑的,有些问题编译不出错的,但是运行结果不是你想像中的那样
llvm 内置的原子操作有两类:
- __sync_and_fetch_and_add / _sync_**_sub
- lock_xadd 系列
bcc 在 v0.21.0 版本引入 atomic_increment() 接口,其底层实现以来的就是 lock_xadd
https://github.com/iovisor/bcc/releases/tag/v0.21.0
另外 __sync_and_fetch_and_add 目前是有一个已知的llvm的bug
https://github.com/iovisor/bcc/issues/2654
在 clang 7 版本里面,__sync_and_fetch_and_add 函数理论上应该返回 add 之前的原值,但是实际在 ebpf 代码里面返回里被 add 的 delta 值,比如你每次加1,那这个函数就返回1,没有起到相应的作用
具体可以看看上面的 issue,有讨论,不知道新版本修复了没
不过据我实测,在机器上用 clang-7 编译器直接编译,是能返回正确的结果的,但是 ebpf 里面编译就不行,原因不明。如果有升级 clang 编译器可以解决这个问题的话,可以告知,谢谢,我懒得折腾了
2. 线程安全
由于上面我们已经实证,ebpf 代码是并行运行的,因此,并发情况下,如何保证 table 等数据结构的线程安全,就是个很重要的问题
对于bcc内置的table类数据结构,bcc 自身保证其线程安全,对于用户自定义的数据结构,bcc提供 bpf_spin_lock,具体可以参考
https://lwn.net/Articles/779120
2.1. bcc 内置函数的线程安全实现
xx
2.2. bpf_spin_lock 原理和使用
xx