抽丝剥茧:从 Linux 源码探索 eBPF 的实现
去年学习 eBPF,分享过 几篇 eBPF 方面的学习笔记,都是面向 eBPF 的应用。为了准备下一篇文章,这次决定从 Linux 源码入手,深入了解 eBPF 的工作原理。因此这篇又是一篇学习笔记,假如你对 eBPF 的工作原理也感兴趣,不如跟随我的脚步一起。文章中若有任何问题,请不吝赐教。
这里不会再对 eBPF 进行过多的介绍,可以参考我的另一篇 使用 eBPF 技术实现更快的网络数据包传输,结合 追踪 Kubernetes 中的数据包 可以了解 eBPF 的基本内容以及其在网络加速方面的应用。
接下来我们还是使用 eBPF sockops 中的程序 bpf_sockops 为例, 配合 Linux v6.8 源码探索 eBPF 的工作原理。
BPF 程序操作
在 load.sh 脚本中,完成了程序的加载和挂载操作,下面的命令使用 bpftool 分别完成 BPF 程序的加载和挂载。
#load
sudo bpftool prog load bpf_sockops.o "/sys/fs/bpf/bpf_sockop"
#attach
sudo bpftool cgroup attach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockop"
这里 bpftool 是对内核函数 bpf()
封装的命令行工具,用于管理和操作 BPF 程序与 Map。
加载
sudo bpftool prog load bpf_sockops.o "/sys/fs/bpf/bpf_sockop"
命令 bpftool prog load
将 bpf_sockops.o
加载到路径 /sys/fs/bpf/bpf_sockop
中。
bpftool 对 BPF 程序的加载是由调用 bpf()
指定命令 BPF_PROG_LOAD
并传入 加载选项bpf_prog_load_opts
来完成的:
syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))
- syscall bpf() bpf 系统函数
- __sys_bpf 执行 bpf 命令 BPF_PROG_LOAD
- bpf_prog_load 为程序分配内存、初始化、检查证书、运行 verifier、创建文件描述符(fd)等
- __sys_bpf 执行 bpf 命令 BPF_PROG_LOAD
加载成功后的程序,然后就可以进行挂载了。
挂载
sudo bpftool cgroup attach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockop"
命令 bpftool cgroup attach
将加载(pin 到文件系统中)的程序 /sys/fs/bpf/bpf_sockop
挂载到 cgroup /sys/fs/cgroup/unified/
,挂载的类型为 sock_ops
。这个 sock_ops
是 bpftool 所使用的库 libbpf
定义,也被是 ELF 部件名,对应着 BPF 程序类型 BPF_PROG_TYPE_SOCK_OPS
,挂载类型 为 BPF_CGROUP_SOCK_OPS
。
在 eBPF 编程中,ELF(Executable and Linkable Format)文件用于存储编译后的 eBPF 程序和相关数据。ELF 文件由多个部分(sections)组成,每个部分包含不同类型的信息,比如程序代码、符号表、调试信息等。
libbpf 类型 sock_ops
=> BPF 程序类型 BPF_PROG_TYPE_SOCK_OPS
=> 挂载类型 BPF_CGROUP_SOCK_OPS
,对应到程序 bpf_sockops.c
中部件名(__section
)为 sockops
的代码块。
关于 sock_ops
挂载点:
sock_ops
通常指的是在 Linux 内核中处理套接字操作的一系列函数和操作。
sock_ops
具体可以包括一系列的操作,如创建套接字、绑定套接字到特定地址和端口、监听来自其他套接字的连接请求、接受连接请求、发送和接收数据、以及关闭套接字等。这些操作通常通过一组预定义的 API 来提供,例如 POSIX 套接字 API,它定义了一系列函数,如socket()
、bind()
、listen()
、accept()
、send()
、recv()
和close()
等,供应用程序调用。
这次 bpftool 是通过 bpf()
执行执行 BPF_PROG_ATTACH
并传入 挂载选项 bpf_prog_attach_opts
来完成的。
syscall(__NR_bpf, BPF_PROG_ATTACH, &attr, sizeof(attr))
- syscall bpf() bpf 系统函数
- bpf_prog_attach
- cgroup_bpf_prog_attach
- cgroup_bpf_prog_attach
- __cgroup_bpf_attach
- bpf_prog_put 检查 cgroup 上是否存在相同挂载类型的程序,如果存在,则进行替换。
- static_branch_inc 如果不存在,则将
cgroup_bpf_enabled_key
计数器中,该挂载类型的计数 +1。
- __cgroup_bpf_attach
- cgroup_bpf_prog_attach
- cgroup_bpf_prog_attach
- bpf_prog_attach
cgroup_bpf_enabled_key
特定类型 cgroup BPF 程序的计数器。!!! 在运行时,会用到该计数器。
到此,我们已经成功将程序挂载到 cgroup 的 sock_ops 上。
套接字操作 sock_ops
套接字的操作很多,这里以连接建立过程中服务端 accept
操作为例。
依然是从系统调用 accept
开始。
- accept
- __sys_accept4_file
- do_accept 此处
ops->accept()
中的 ops 对应着 proto_ops inet_stream_ops 有状态的 socket(如 TCP) 的相关操作- inet_stream_ops.accept
- inet_accept
sk1->sk_prot->accept()
这里的sk_prot
提供了 TCP 协议proto tcp_prot
的具体操作- tcp_prot.accept
- inet_csk_accept 开始处理三次握手,调用 TCP 协议的实现来处理。inet_init 注册了
IPPROTO_TCP
也就是 TCP 协议的实现,也就是 net_protocol tcp_protocol,其handler
为tcp_v4_rcv
。- tcp_v4_rcv 此时第一次握手刚开始,sock(套接字在内核协议栈这层的体现) 的状态还是
TCP_LISTEN
- tcp_v4_do_rcv 在连接成功建立前,每次握手都会对状态进行处理。
- tcp_rcv_state_process 我们直接看最后一次握手,也就是收到客户端的 ACK,完成与客户端连接的建立。
- tcp_init_transfer sock 的状态被设置为
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
,开始进行数据传输。- bpf_skops_established
- BPF_CGROUP_RUN_PROG_SOCK_OPS 执行挂载类型为
BPF_CGROUP_SOCK_OPS
的 BPF 程序。
- BPF_CGROUP_RUN_PROG_SOCK_OPS 执行挂载类型为
- bpf_skops_established
- tcp_init_transfer sock 的状态被设置为
- tcp_rcv_state_process 我们直接看最后一次握手,也就是收到客户端的 ACK,完成与客户端连接的建立。
- tcp_v4_rcv 此时第一次握手刚开始,sock(套接字在内核协议栈这层的体现) 的状态还是
- inet_accept
- inet_stream_ops.accept
- do_accept 此处
- __sys_accept4_file
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
是 socket.accept() 接受连接请求并完成连接建立的操作符,也是众多 sock_ops
操作符 中的一个。这些操作符,可以被看作是 事件 Event,程序的触发则是由事件驱动的。例如:
- 如果客户端发起连接请求并完成三次握手后的操作符是
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
; - 套接字进入监听状态时的操作符是
BPF_SOCK_OPS_TCP_LISTEN_CB
; - 数据被确认
BPF_SOCK_OPS_DATA_ACK_CB
- TCP 状态改变
BPF_SOCK_OPS_STATE_CB
最后就是 BPF 程序的执行了,不多做赘述,有兴趣的看这里的 分析。