19 年开始接触 dpdk 的时候,写了 dpdk-pdump 编译运行 这篇博客,最近做一个基于 ebpf 规则的抓包项目时深入研究了下 pdump 的实现原理,在本文中记录一下。
备注:本文源码版本摘自 dpdk 22.11-rc0
pdump
程序根据用户配置参数构建pdump_request
结构,然后通过 dpdk 多进程间通信机制将请求发送到 primary
进程,primary 进程接收到这个请求后解析请求并调用pdump
库提供的使能接口注册抓包回调,然后构造一条pdump_response
返回消息发给 pdump
程序。
pdump_rxtx_cbs
只在 libpdump 库代码内部使用,用于保存 pdump 程序注册的所有包过滤请求规则,在关闭 pdump 的时候会使用此结构来释放相关的数据。
此功能使用 dpdk 多进程间通信框架 mp_channel 实现,其特征如下:
dpdk primary 进程初始化时调用 rte_mp_action_register
注册一个以 mp_pdump 字符串标识的 pdump 消息处理 action 回调—— pdump_server
函数,当 mp 处理线程接收到 pdump 请求消息时调用 pdump_server 来完成 pdump 功能的使能与关闭。
pdump_server 的主要逻辑如下:
set_pdump_rxtx_cbs
函数使能、关闭抓包使能抓包的语义:
关闭抓包的语义:
根据配置找到对应的 pdump_rxtx_cbs 结构,根据此结构中保存的回调函数句柄移除某个接口的某个收、发队列的 pdump_rx、pdump_tx 收发包回调函数。
pdump 库通过注册 dpdk 收发包回调函数实现抓包,其语义为在配置的 mempool 中申请 mbuf,然后完成报文 copy,最后再将 mbuf 地址投递到配置的 ring 中。
dpdk primary 进程启动时并不会创建用于 pdump 抓包的 mempool、ring,这两个结构是在 pdump 进程中创建与销毁的,dpdk primary 进程会根据收到的 pdump_request 中的 mp 与 ring 字段配置抓包回调的参数,抓包回调中直接使用这两个数据结构地址。
这里有个问题:为什么 pdump 进程创建的 mempool 与 ring 的虚拟地址可以在 dpdk primary 进程中直接使用?进程的虚拟地址不是相互独立的吗?
上图为 dpdk primary 进程与 secondary 进程共享资源描述图,中间的 hugepage dpdk memory 是两个进程共享的大页内存,其映射的基地址在 priamry 进程中与 secondary 进程中是相同的,这样在大页内存上分配的内存其虚拟地址可以在 primary 进程与 secondary 进程中直接使用,底层的共享过程隐藏在 rte_eal_init 函数调用中。
上图右上角为 pdump 进程,它从 ring 中拿到 mbuf 后并不会直接调用 libpcap 接口保存报文,而是使用 dpdk pcap 虚拟网口,通过调用 rte_eth_tx_burst 向 pcap 虚拟网口发包来完成报文的保存过程。
这里存在一个问题:当 primary 进程运行后,接口的数目已经确定,pdump 要动态添加、删除 vdev port 该如何实现?
这一看似复杂的功能是通过调用如下两个接口实现的:
当 pdump 进程调用rte_eal_hotplug_add
接口时,此接口向 primary 进程发送一个接口热插拔消息,primary 进程调用 vdev bus 提供的 plug 方法完成新接口的创建并根据创建结果回复消息。当 vdev 成功创建后,pdump 进程通过调用 rte_eth_dev_get_port_by_name
函数找到此 vdev 接口的 port_id 来使用。
同理,当 pdump 进程退出时,它会调用 rte_eal_hotplug_remove
接口继续向 primary 进程发送接口热插拔消息,primary 进程调用 vdev bus 提供的 unplug 方法完成接口的销毁。
当 pdump 进程运行时,如果 primary 进程终止,pdump 进程需要检测到这一状态并退出。检测逻辑创建一个线程周期性设置一个定时事件回调函数,定时调用 rte_eal_primary_proc_alive
接口来检测 primary 进程是否存活。
rte_eal_primary_proc_alive
函数的实现有些小技巧,其代码如下:
145int
146rte_eal_primary_proc_alive(const char *config_file_path)
147{
148 int config_fd;
149
150 if (config_file_path)
151 config_fd = open(config_file_path, O_RDONLY);
152 else {
153 const char *path;
154
155 path = eal_runtime_config_path();
156 config_fd = open(path, O_RDONLY);
157 }
158 if (config_fd < 0)
159 return 0;
160
161 int ret = lockf(config_fd, F_TEST, 0);
162 close(config_fd);
163
164 return !!ret;
165}
上述代码核心逻辑为调用 lockf 来检查 rte_config 的文件锁是否被其它进程占有,可以想到 primary 进程是唯一占有 rte_config 文件锁的用户,当没有人占用就表示 primary 进程未在运行。
dpdk ebpf 过滤规则可以支持使用高级语言开发 ebpf 指令来实现过滤报文的功能,pdump 起初只支持全量抓包,当它与 dpdk ebpf 框架结合后能够实现抓取特定规则的报文。
pdump_request 中新增的 prm 结构字段将 pdump 与 dpdk ebpf 框架结合起来,主要的修改为在 pdump_copy 拷贝报文前调用 rte_bpf_exec_burst
函数,使用 ebpf 虚拟机执行 ebpf 规则过滤报文,然后只 copy 命中 ebpf 规则的报文即可。
目前 pdump 库已经支持 ebpf 规则过滤报文,可以调用rte_pdump_enable_bpf
来在 pdump 抓包的基础上注入 ebpf 规则。此接口会将 ebpf 指令码翻译为本地机器码,但是目前并没有执行 jit 翻译的机器码,而是执行了 ebpf 虚拟机。
需要注意的是 pdump 目前并不支持注册 ebpf 过滤规则。
pdump 支持抓取 single-segment 类型与 multi-segment 类型 mbuf 报文,single-segment 类型支持抓取的报文大小由 pdump 程序启动时配置的 mempool 中每个 mbuf 的 dataroom size 决定,dataroom size 缺省大小为 2048。
pdump 程序支持通过配置一个 “–multi” 参数来在多个 cpu 逻辑核上同时运行不同的抓包任务,同时 pdump 程序作为一个 secondary 进程,本身也支持多进程启动(不同进程的抓包目标不能相同)。多核 + 多进程的设计能够很好的利用多核优势,增强抓包能力。
pdump 当前支持 V1 与 V2 两个版本,主要区别在于 copy 报文的过程。V2 版本使用 pcappng 接口将报文 copy 为 pcappng 格式,V1 版本使用 dpdk mbuf copy 接口 copy 报文为原始报文格式。
pdump 定义了 rte_pdump_stats 结构来进行数据统计,此结构定义如下
234struct rte_pdump_stats {
235 uint64_t accepted; /**< Number of packets accepted by filter. */
236 uint64_t filtered; /**< Number of packets rejected by filter. */
237 uint64_t nombuf; /**< Number of mbuf allocation failures. */
238 uint64_t ringfull; /**< Number of missed packets due to ring full. */
239
240 uint64_t reserved[4]; /**< Reserved and pad to cache line */
241};
pdump 在大页上为所有收发包接口的收发包队列都创建一个rte_pdump_stats
结构,此统计在 primary 进程中被修改,在 secondary 进程中被读取用于显示,在大页上分配支持 seondary 进程中够获取此数据。
同时,此数据的增加使用 gcc 提供的原子函数完成,确保了数据的一致性。
pdump 看似简单,实则集成了 dpdk 内部众多子模块的功能,主要依赖的功能如下:
上述功能都是 dpdk 的基石,通过整合将这些基础的模块连接到一起实现更为复杂的功能。这一功能又可以成为新的基石,被其它的对象整合,这就是软件设计的魔力,值得学习与借鉴。