在研究 eBPF Talk: trace tailcall 程序?NO! 时,产生了疑惑:

  1. 静态 tailcall 使用的 PROG_ARRAY bpf map 能在多个进程之间共享吗?

TL;DR 能。且在更新 PROG_ARRAY bpf map 时,会更新静态 tailcall 的所有 bpf prog。

设计 demo

为了验证上述疑惑,我设计了如下 demo:

  1. tailcallPROG_ARRAY bpf map pin 到 /sys/fs/bpf/tailcall-shared_progs
  2. 在一个进程里更新该 PROG_ARRAY bpf map。
  3. 在另一个进程里也更新该 PROG_ARRAY bpf map。
  4. 尝试在 XDP 里使用该 PROG_ARRAY bpf map。

demo 效果如下:

 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
# 在第一个终端里运行
# 在第一个进程里更新 **PROG_ARRAY** bpf map
# ./tailcall-shared -1
2023/08/06 16:39:58 Prepared tailcall(handle_new_connection1)
2023/08/06 16:39:58 Attached kprobe(tcp_connect)
2023/08/06 16:39:58 Listening events...
2023/08/06 16:40:03 new tcp connection: 192.168.64.2:36654 -> 142.251.10.101:80 (handle_new_connection1)
2023/08/06 16:40:07 new tcp connection: 192.168.64.2:49594 -> 74.125.130.102:80 (handle_new_connection1)

# 在第二个终端里运行
# 在第二个进程里更新 **PROG_ARRAY** bpf map
# 此后,第一个终端里不会有新的 tcp 连接 log
# ./tailcall-shared -2
2023/08/06 16:40:49 Prepared tailcall(handle_new_connection2)
2023/08/06 16:40:49 Attached kprobe(inet_csk_complete_hashdance)
2023/08/06 16:40:49 Listening events...
2023/08/06 16:40:52 new tcp connection: 192.168.64.2:46898 -> 142.251.12.138:80 (handle_new_connection2)
2023/08/06 16:40:53 new tcp connection: 192.168.64.2:44094 -> 142.251.12.139:80 (handle_new_connection2)

# 在第三个终端里运行
# 尝试在 XDP 里使用该 **PROG_ARRAY** bpf map
# ./tailcall-shared -x
2023/08/06 16:41:22 Failed to load bpf xdp: field XdpEntry: program xdp_entry: load program: invalid argument
load program: invalid argument:
    processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 0

demo 源代码

P.S. demo 源代码:GitHub - Asphaltt/learn-by-example

使用的 bpf 代码如下:

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(key_size, 4);
    __uint(value_size, 4);
    __uint(max_entries, 1);
} progs SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} socks SEC(".maps");

SEC("kprobe/handle_new_connection1")
int handle_new_connection1(struct pt_regs *ctx)
{
    __u32 key = 0;
    struct sock **skp = bpf_map_lookup_and_delete(&socks, &key);
    if (!skp)
        return 0;

    bpf_printk("tcpconn, handle_new_connection1\n");

    struct sock *sk = *skp;
    __handle_new_connection(ctx, sk, PROBE_TYPE_FENTRY, 0);

    return 0;
}

SEC("kprobe/handle_new_connection2")
int handle_new_connection2(struct pt_regs *ctx)
{
    __u32 key = 0;
    struct sock **skp = bpf_map_lookup_and_delete(&socks, &key);
    if (!skp)
        return 0;

    bpf_printk("tcpconn, handle_new_connection2\n");

    struct sock *sk = *skp;
    __handle_new_connection(ctx, sk, PROBE_TYPE_FEXIT, 0);

    return 0;
}

SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx)
{
    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM1(ctx);

    __u32 key = 0;
    bpf_map_update_elem(&socks, &key, &sk, BPF_ANY);

    bpf_tail_call_static(ctx, &progs, 0);

    return 0;
}

SEC("kprobe/inet_csk_complete_hashdance")
int k_icsk_complete_hashdance(struct pt_regs *ctx)
{
    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM2(ctx);

    __u32 key = 0;
    bpf_map_update_elem(&socks, &key, &sk, BPF_ANY);

    bpf_tail_call_static(ctx, &progs, 0);

    return 0;
}
  1. kprobe tcp 连接的两个关键函数。
  2. 有两个用于更新 PROG_ARRAY bpf map 的 bpf prog。

Go 的代码比较啰嗦,就不贴了,直接跳转原文去查看源代码吧。

demo 结论

通过 demo 验证了静态 tailcallPROG_ARRAY bpf map 能在多个进程之间共享。

即,在第二个进程更新 PROG_ARRAY bpf map 时,第一个进程里所使用的 bpf prog 也被更新了。

通过 demo,印证了 eBPF Talk: 更新 tailcall PROG_ARRAY bpf map 所学到的知识。

你也可以通过 eBPF Talk: 动态或静态 tailcall 中 gdb 的方式查看 bpf prog 的汇编指令,来验证这一结论。