去年学习 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)等
加载成功后的程序,然后就可以进行挂载了。
挂载
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_put 检查 cgroup 上是否存在相同挂载类型的程序,如果存在,则进行替换。
- static_branch_inc 如果不存在,则将
cgroup_bpf_enabled_key
计数器中,该挂载类型的计数 +1。
cgroup_bpf_enabled_key
特定类型 cgroup BPF 程序的计数器。!!! 在运行时,会用到该计数器。
到此,我们已经成功将程序挂载到 cgroup 的 sock_ops 上。
套接字操作 sock_ops
套接字的操作很多,这里以连接建立过程中服务端 accept
操作为例。
依然是从系统调用 accept
开始。
do_accept 此处
ops->accept()
中的 ops 对应着 proto_ops inet_stream_ops 有状态的 socket(如 TCP) 的相关操作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_CGROUP_RUN_PROG_SOCK_OPS 执行挂载类型为
BPF_CGROUP_SOCK_OPS
的 BPF 程序。
- BPF_CGROUP_RUN_PROG_SOCK_OPS 执行挂载类型为
- tcp_v4_rcv 此时第一次握手刚开始,sock(套接字在内核协议栈这层的体现) 的状态还是
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 程序的执行了,不多做赘述,有兴趣的看这里的 分析。
关注"云原生指北"公众号
(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。