eBPF map 有诸多类型。如果想要理解它们的实现,该如何去阅读它们的源代码呢?

类型接口

在内核的源代码海洋中,接口的定义一般是:结构体的字段都是函数指针类型, 而结构体名称以 ops 结尾。譬如 bpf map 的接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ${KERNEL}/include/linux/bpf.h

/* map is generic key/value storage optionally accesible by eBPF programs */
struct bpf_map_ops {
    /* funcs callable from userspace (via syscall) */
    int (*map_alloc_check)(union bpf_attr *attr);
    struct bpf_map *(*map_alloc)(union bpf_attr *attr);
    void (*map_release)(struct bpf_map *map, struct file *map_file);
    void (*map_free)(struct bpf_map *map);
    int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key);
    void (*map_release_uref)(struct bpf_map *map);
    void *(*map_lookup_elem_sys_only)(struct bpf_map *map, void *key);
    int (*map_lookup_batch)(struct bpf_map *map, const union bpf_attr *attr,
                union bpf_attr __user *uattr);
    int (*map_lookup_and_delete_batch)(struct bpf_map *map,
                       const union bpf_attr *attr,
                       union bpf_attr __user *uattr);
    int (*map_update_batch)(struct bpf_map *map, const union bpf_attr *attr,
                union bpf_attr __user *uattr);
    int (*map_delete_batch)(struct bpf_map *map, const union bpf_attr *attr,
                union bpf_attr __user *uattr);

    /* funcs callable from userspace and from eBPF programs */
    void *(*map_lookup_elem)(struct bpf_map *map, void *key);
    int (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags);
    int (*map_delete_elem)(struct bpf_map *map, void *key);
    int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
    int (*map_pop_elem)(struct bpf_map *map, void *value);
    int (*map_peek_elem)(struct bpf_map *map, void *value);

    // ...
}

不同 map 类型会有对应的 struct bpf_map_ops 变量。它们的对应关系请查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ${KERNEL}/include/linux/bpf_types.h

// ...

BPF_MAP_TYPE(BPF_MAP_TYPE_ARRAY, array_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERCPU_ARRAY, percpu_array_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PROG_ARRAY, prog_array_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERF_EVENT_ARRAY, perf_event_array_map_ops)
#ifdef CONFIG_CGROUPS
BPF_MAP_TYPE(BPF_MAP_TYPE_CGROUP_ARRAY, cgroup_array_map_ops)
#endif
#ifdef CONFIG_CGROUP_BPF
BPF_MAP_TYPE(BPF_MAP_TYPE_CGROUP_STORAGE, cgroup_storage_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, cgroup_storage_map_ops)
#endif
BPF_MAP_TYPE(BPF_MAP_TYPE_HASH, htab_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERCPU_HASH, htab_percpu_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_LRU_HASH, htab_lru_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_LRU_PERCPU_HASH, htab_lru_percpu_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_LPM_TRIE, trie_map_ops)

// ...

类型的具体实现

这些 _ops 变量都在哪里呢?它们大部分都在目录 kernel/bpf/ 下。 譬如 BPF_MAP_TYPE_HASH 对应的 htab_map_ops 的源代码在 kernel/bpf/hashtab.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ${KERNEL}/kernel/bpf/hashtab.c

const struct bpf_map_ops htab_map_ops = {
    .map_meta_equal = bpf_map_meta_equal,
    .map_alloc_check = htab_map_alloc_check,
    .map_alloc = htab_map_alloc,
    .map_free = htab_map_free,
    .map_get_next_key = htab_map_get_next_key,
    .map_lookup_elem = htab_map_lookup_elem,
    .map_update_elem = htab_map_update_elem,
    .map_delete_elem = htab_map_delete_elem,
    .map_gen_lookup = htab_map_gen_lookup,
    .map_seq_show_elem = htab_map_seq_show_elem,
    BATCH_OPS(htab),
    .map_btf_name = "bpf_htab",
    .map_btf_id = &htab_map_btf_id,
    .iter_seq_info = &iter_seq_info,
};

通过类型跟接口的实现,便可快捷查找某个类型的具体实现。

内核对用户态提供的 API

这些具体的类型实现,该怎么跟用户态的 API 对接起来呢?

其实,对于 bpf 子系统而言,对用户态提供的系统调用 只有 1 个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ${KERNEL}/kernel/bpf/syscall.c

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
    // ...

    switch (cmd) {
    case BPF_MAP_CREATE:
        err = map_create(&attr);
        break;
    case BPF_MAP_LOOKUP_ELEM:
        err = map_lookup_elem(&attr);
        break;
    case BPF_MAP_UPDATE_ELEM:
        err = map_update_elem(&attr);
        break;
    case BPF_MAP_DELETE_ELEM:
        err = map_delete_elem(&attr);
        break;
    case BPF_MAP_GET_NEXT_KEY:
        err = map_get_next_key(&attr);
        break;
    case BPF_MAP_FREEZE:
        err = map_freeze(&attr);
        break;

    // ...
    }

    // ...
}

如此,便可一步一步地深入研究 bpf map 的具体实现。

内核源代码阅读工具

我现在使用的是 Sublime Text,因为它提供了开箱即用的定义跳转功能。 它的跳转功能比较弱,所以我使用全局搜索来辅助定义跳转; 我使用的全局搜索有两种方式,一是 Sublime Text 自带的,二是 ag + fzf 的命令行组合。

而 Visual Studio Code 用来写代码,就没用来阅读内核代码。

另外,对于 Windows 系统而言,推荐使用 SourceInsight 来阅读内核代码。

网络上也提供不上在线阅读源代码的工具,比如:

小结

使用趁手的工具去阅读源代码,事半功倍。