好久没更新了,有点惭愧。且最近在EBPF的原子性上也头疼了将近一周的时间,主要是通过测试不同的原子性方法,来兼容不同的环境。
这次将测试验证的过程记录下来,以避免以后踩到同样类型的坑里。
手里有三种环境见下表,为了让一套代码同时支持三种不同的环境,反复做了很多次试验,这里只记录成功的结果。
操作系统 | kernel 版本 | clang & LLVM 版本 |
---|---|---|
CentOS 7.9 | 5.10.0-1 | 9.0.1 |
CentOS 7.9 | 4.18.0-193 | 9.0.1 |
Ubuntu 23.04 | 6.2.0-35 | 15.0.7 |
本来是知道EBPF支持有限的锁的机制,但是考虑到以前经常从kernel中复制汇编代码,进行CAS(compare and swap)操作。
这里也是想优先使用一句话的原子CAS操作,来解决EBPF在多CPU访问一个Entry的互斥问题。
首先想到的是无锁化的原子操作,结果从其他地方复制过来的CAS汇编代码,在LLVM中均无法编译通过。
这里又想到了GCC及LLVM内建的__sync_系列函数: [ GCC内建 LLVM内建 ]
1static __u64 data = 10086;
2__u64 old = __sync_val_compare_and_swap(&data, 10086, 10010);
3bpf_printk("old: %d",old);
结果在6.2的环境中可以正常运行,在4.18及5.10环境中报错Cannot select: 0x55c89940f1e8: i64,ch = AtomicCmpSwap<(load store seq_cst seq_cst 8 on @data)>
。遂放弃该方法。
通过查阅LLVM的文档,发现__sync_val_compare_and_swap
该函数是LLVM在后边的版本才添加支持的。那么退而求其次,使用fetch-add
函数可以模拟原子锁的操作:
1static __u64 data = 10086;
2__u64 old = __sync_fetch_and_add(&data, 1);
3bpf_printk("old: %d", old);
这次可以编译通过了,且在 6.2 及5.10上正常运行,但是在4.18系统上加载报错:invalid argument: BPF_STX uses reserved fields
,及stx
指令访问了保留的字段。显而易见4.18版本还不支持fetch-add
类的操作。遂放弃。
又回到了起点,EBPF是支持bpf_spin_lock
操作的,那么只好使用此类函数实现原子性了:
1static __u64 data = 10086;
2static struct bpf_spin_lock lock;
3bpf_spin_lock(&lock);
4data++;
5bpf_spin_unlock(&lock);
6bpf_printk("after unlock: %d", data);
结果在6.20及5.10上可以正常运行,在4.18上加载报错:reference to "lock" in section SHN_COMMON: not supported
。遂放弃。
通过查看ebpf的文档,发现在低版本的kernel中,bpf_spin_lock
只支持在BPF_MAP_TYPE_HASH
和BPF_MAP_TYPE_ARRAY
的Value中实现。
经过反复验证,甚至到6.2的kernel也不支持BPF_MAP_TYPE_LRU_HASH
类型的MAP。
1static __u64 data = 10086;
2
3struct entry {
4 struct bpf_spin_lock lock;
5};
6
7struct {
8 __uint(type, BPF_MAP_TYPE_HASH);
9 __type(key, int);
10 __type(value, struct entry);
11 __uint(pinning, LIBBPF_PIN_BY_NAME);
12 __uint(max_entries, 16);
13 __uint(map_flags, BPF_F_NO_PREALLOC);
14} map001 SEC(".maps");
15
16void do_lock()
17{ // 4.18: OK
18 int k = 1;
19 struct entry *e = bpf_map_lookup_elem(&map001, &k);
20 if (e == NULL) {
21 struct entry ee;
22 memset(&ee, 0, sizeof(struct entry));
23 bpf_map_update_elem(&map001, &k, &ee, BPF_NOEXIST); // 忽略返回值
24
25 e = bpf_map_lookup_elem(&map001, &k);
26 }
27
28 if (e != NULL) {
29 bpf_spin_lock(&e->lock);
30 data++;
31 bpf_spin_unlock(&e->lock);
32 bpf_printk("after unlock: %d", data);
33 }
34}
终于,找到了一种方法,同时支持4.18、5.10、6.2的kernel。感兴趣的同学可以查看 示例代码
内核版本 | 4.18.0-193 | 5.10.0-1 | 6.2.0-35 |
---|---|---|---|
__sync_val_compare_and_swap | ❌ | ❌ | ✔️ |
__sync_fetch_and_add | ❌ | ✔️ | ✔️ |
static bpf_spin_lock | ❌ | ✔️ | ✔️ |
MAP-bpf_spin_lock | ✔️ | ✔️ | ✔️ |