给以下 bpf maps 增加了 BPF_F_CPUBPF_F_ALL_CPUS flag 支持:

  • BPF_MAP_TYPE_PERCPU_ARRAY
  • BPF_MAP_TYPE_PERCPU_HASH
  • BPF_MAP_TYPE_LRU_PERCPU_HASH
  • BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE

这 2 个 flag 的作用:

  • lookup_elem() 时,可以通过 BPF_F_CPU + cpu 查询指定 CPU 上的值。
  • update_elem() 时,可以通过 BPF_F_CPU + cpu 更新指定 CPU 上的值。
  • update_elem() 时,可以通过 BPF_F_ALL_CPUS 使用一个 value 更新所有 CPU 上的值。

背景

Alexei 和 Andrii 在 review “bpf: Introduce global percpu data” 补丁集时,为了规避 lskel 里 per-CPU maps M:N 问题:在 M 核机器上缓存了长度为 M 的值,在 N 核机器上 update_elem() 时使用了长度为 M 的值,如果 N > M ,则会有问题;提出新增 2 个 flag 的 想法。

实现

利用 flags 参数,如果是 BPF_F_CPU,则将 cpu 嵌入到 flags 高 32 比特。

因此,在内核里,lookup_elem() 时从 flags 里拿到 cpu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// kernel/bpf/arraymap.c

int bpf_percpu_array_copy(struct bpf_map *map, void *key, void *value, u64 map_flags)
{
        // ...
        size = array->elem_size;
        rcu_read_lock();
        pptr = array->pptrs[index & array->index_mask];
        if (map_flags & BPF_F_CPU) {
                cpu = map_flags >> 32;
                copy_map_value(map, value, per_cpu_ptr(pptr, cpu));
                check_and_init_map_value(map, value);
                goto unlock;
        }
        // ...
unlock:
        rcu_read_unlock();
        return 0;
}

update_elem() 时,也从 flags 里拿到 cpu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// kernel/bpf/arraymap.c

int bpf_percpu_array_update(struct bpf_map *map, void *key, void *value,
                            u64 map_flags)
{
        // ...
        size = array->elem_size;
        rcu_read_lock();
        pptr = array->pptrs[index & array->index_mask];
        if (map_flags & BPF_F_CPU) {
                cpu = map_flags >> 32;
                ptr = per_cpu_ptr(pptr, cpu);
                copy_map_value(map, ptr, value);
                bpf_obj_free_fields(array->map.record, ptr);
                goto unlock;
        }
        // ...
unlock:
        rcu_read_unlock();
        return 0;
}

使用 BPF_F_ALL_CPUS update_elem() 时,复用一个 value:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// kernel/bpf/arraymap.c

int bpf_percpu_array_update(struct bpf_map *map, void *key, void *value,
                            u64 map_flags)
{
        // ...
        for_each_possible_cpu(cpu) {
                ptr = per_cpu_ptr(pptr, cpu);
                val = (map_flags & BPF_F_ALL_CPUS) ? value : value + size * cpu;
                copy_map_value(map, ptr, val);
                bpf_obj_free_fields(array->map.record, ptr);
        }
unlock:
        rcu_read_unlock();
        return 0;
}

用法

libbpf 为例。

使用 BPF_F_ALL_CPUS update_elem():

1
2
3
4
// tools/testing/selftests/bpf/prog_tests/percpu_alloc.c

        flags = BPF_F_ALL_CPUS;
        err = bpf_map__update_elem(map, keys, key_sz, values, value_sz, flags);

使用 BPF_F_CPU + cpu update_elem():

1
2
3
4
// tools/testing/selftests/bpf/prog_tests/percpu_alloc.c

        flags = (u64)cpu << 32 | BPF_F_CPU;
        err = bpf_map__update_elem(map, keys, key_sz, values, value_sz, flags);

使用 BPF_F_CPU + cpu lookup_elem():

1
2
3
4
// tools/testing/selftests/bpf/prog_tests/percpu_alloc.c

        flags = (u64)cpu << 32 | BPF_F_CPU;
        err = bpf_map__lookup_elem(map, keys, key_sz, values, value_sz, flags);

题外

顺手修复了给 PERCPU HASH maps update_elem() 时泄漏 特殊字段 的问题。

即,如果在 value 里使用了 bpf_timer 这类字段时,update_elem() 不会释放已有的 bpf_timer 字段;要等到 value 被删除、或者整个 map 被销毁时,才会被释放。

小结

其实,该补丁集历经了半年多、一共 13 个版本才合入社区。

在这个过程中,逐渐熟悉了社区的代码品味;期间,也经历了社区 AI review 的建设过程。

痛苦中成长,也收获满满。