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 的次数越多,冲突的概率就越大