Concurrency management in BPF

eBPF 对并发编程的支持是非常弱的

有一些讨论:

  1. https://lists.iovisor.org/g/iovisor-dev/topic/bpf_concurrency/74407447
  2. 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 里面删除掉,如果:

  1. trance_run 是串行的,那么技术器永远都是 0,因为在一个串行执行的逻辑里,插入元素后,不管查询多少次,元素必然是存在的
  2. 如果 trace_run 是并行的,那么就存在一个条件,一定有一个 cpu,在另外一个 cpu 查询 key 是否存在的时候,执行到了函数结束的地方,把 key 从 map 里面删掉了

事实证明,trace_run 是并行执行的,并且中间 lookup 的次数越多,冲突的概率就越大