BPF_MAP_TYPE_PERCPU_ARRAY未零初始化?创建后部分CPU值异常咨询
我尝试用BPF_MAP_TYPE_PERCPU_ARRAY统计SK_SKB钩子处接收的数据包数量,但创建映射后立即用bpftool map dump查看,发现部分CPU对应的值并未被零初始化。
映射定义如下:
struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, int); __type(value, __u64); __uint(pinning, LIBBPF_PIN_BY_NAME); } request_count_map SEC(".maps");
bpftool map dump输出:
$ sudo bpftool map dump id 287 [{ "key": 0, "values": [{ "cpu": 0, "value": 0 },{ "cpu": 1, "value": 94322157119214 },{ "cpu": 2, "value": 140730068914171 },{ "cpu": 3, "value": 94322160575280 },{ "cpu": 4, "value": 94322160592312 },{ "cpu": 5, "value": 94322160592512 },{ "cpu": 6, "value": 80 },{ "cpu": 7, "value": 0 },{ "cpu": 8, "value": 0 },{ "cpu": 9, "value": 140730068932048 },{ "cpu": 10, "value": 0 },{ "cpu": 11, "value": 0 },{ "cpu": 12, "value": 0 },{ "cpu": 13, "value": 0 },{ "cpu": 14, "value": 0 },{ "cpu": 15, "value": 0 },{ "cpu": 16, "value": 80 },{ "cpu": 17, "value": 0 },{ "cpu": 18, "value": 0 },{ "cpu": 19, "value": 140730068932048 } ] } ]
根据内核文档和man手册的描述,数组类型的映射元素应该在创建时预分配并零初始化,但实际结果不符。
补充:我在加载时尝试手动更新映射,代码如下:
int index = 0; __u64 value = 0; // 获取映射文件描述符 reqcnt_map_fd = bpf_obj_get(reqcnt_map_filename); // 更新映射值 err = bpf_map_update_elem(respcnt_map_fd, &index, &value, BPF_ANY); if (err) { goto fail; }
但执行后部分CPU的值依然不是0,这是为什么?
问题根源
你遇到的问题是用户态更新percpu数组映射时的参数错误:
对于BPF_MAP_TYPE_PERCPU_ARRAY这类percpu类型的映射,用户态调用bpf_map_update_elem时,value参数不能是单个值,而必须是一个包含所有CPU对应值的数组。内核会将数组中的每个元素依次赋值给对应CPU的存储位置。
你的代码中只传递了单个__u64 value = 0,内核会读取该变量之后的栈内存作为其他CPU的值(因为内核预期接收的是长度为CPU数量 * sizeof(__u64)的内存块),这些栈内存是未初始化的垃圾数据,所以最终部分CPU显示出随机的非零值。
解决方法
正确初始化用户态数组:
先获取系统的CPU数量,然后创建一个对应长度的数组,将所有元素初始化为0,再传递给bpf_map_update_elem:#include <unistd.h> #include <sys/sysinfo.h> int main() { int index = 0; int cpu_count = get_nprocs_conf(); // 获取系统总CPU数 __u64 *values = calloc(cpu_count, sizeof(__u64)); // 全部初始化为0 int reqcnt_map_fd = bpf_obj_get(reqcnt_map_filename); int err = bpf_map_update_elem(reqcnt_map_fd, &index, values, BPF_ANY); free(values); if (err) { // 错误处理 return -1; } return 0; }利用BPF程序初始化(更推荐):
不需要在用户态手动初始化,而是在BPF程序中第一次访问映射时,通过bpf_map_lookup_elem获取指针并初始化。因为percpu数组的每个CPU存储区在第一次被BPF程序访问时,内核会确保其被零初始化(文档描述的零初始化实际是针对BPF程序访问的场景,用户态直接dump未被访问过的CPU存储区可能会看到未初始化的内存):SEC("sk_skb") int sk_skb_handler(struct __sk_buff *skb) { __u64 *count = bpf_map_lookup_elem(&request_count_map, &(int){0}); if (count) { (*count)++; } return 0; }当每个CPU上的BPF程序第一次执行到这段代码时,对应CPU的存储区会被自动零初始化,后续统计就是正确的。
额外说明
内核文档中提到的“零初始化”,更多是指BPF程序访问时的保证。对于用户态来说,percpu数组中未被任何CPU的BPF程序访问过的存储区,其内存可能保留着之前的垃圾数据,这是正常的内核内存分配行为。所以更可靠的方式是让BPF程序自己完成初始化,或者在用户态正确传递全CPU的零值数组。
内容的提问来源于stack exchange,提问作者diviquery

