【前言】
关于DPDK如果实现bypass内核的原理,在上一篇《【DPDK】谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动》中已经描述了在DPDK启动前做的准备工作,那么本篇文章将着重分析DPDK部分的职责,也就是从软件的的角度来分析在第一篇文章的基础上,如何做到真正的操作设备。
注意:
- 本篇文章将会更着重分析软件部分的实现,也就是分析代码实现;
- 同样,本篇会跨过中断部分与vfio部分,中断部分与vfio会在以后另开文章继续分析;
- 人能力以及水平有限,没办法保证没有疏漏,如有疏漏还请各路神仙进行指正,本篇内容都是本人个人理解,也就是原创内容。
- 另外在分析代码的过程中,为了防止一些无挂紧要的逻辑显得代码又臭又长,会对其中不重要或者与主要逻辑不相关的代码进行省略,包括且不限于,变量声明、部分不重要数据的初始化、异常处理、无关主要逻辑的模块函数调用等。
【1.DPDK的初始化】
再次回顾第一篇文章中的三个Questions:
Q:igb_uio/vfio-pci的作用是什么?为什么要用这两个驱动?这里的“驱动”和dpdk内部对网卡的“驱动”(dpdk/driver/)有什么区别呢?
Q:dpdk-devbinds是如何做到的将内核驱动解绑后绑定新的驱动呢?
Q:dpdk应用内部是如何操作pci设备的呢?是怎么让pci设备可以将数据包直接扔到用户态的呢?
其中第一个和第二个Questions便是DPDK应用启动前的前奏,其原理在第一篇文章已经阐述完毕,现在回到第三个Questions,DPDK应用内部是如何操作pci设备的。
回想DPDK应用的启动过程,以最标准的l3fwd应用启动为例,其启动的参数格式如下:
l3fwd [eal params] -- [config params]
参数分为两部分,第一部分为所有DPDK应用基本都要输入的参数,也就是eal参数,关于eal参数的解释可以看DPDK官方的doc:
https://doc.dpdk.org/guides/linux_gsg/linux_eal_parameters.html
其中,eal参数的作用主要是DPDK初始化时使用,阅读过DPDK example的源代码或在DPDK的基础上开发的应用,对一个函数应该颇为熟悉:
int rte_eal_init(int argc, char **argv)
其中eal参数便是给rte_eal_init进行初始化,指示DPDK应用“该怎么初始化”。
【2.准备工作】
在进行PCI的资源扫描之前有一些准备工作,这部分的工作不是在main函数中完成的,也更不是在rte_eal_init这个DPDK初始化函数中完成的,来到DPDK源代码中的drivers/bus/pci/pci_common.c文件中,在这个.c文件中的最后部分我们可以看到如下的代码:
struct rte_pci_bus rte_pci_bus = { .bus = { .scan = rte_pci_scan, .probe = rte_pci_probe, .find_device = pci_find_device, .plug = pci_plug, .unplug = pci_unplug, .parse = pci_parse, .dma_map = pci_dma_map, .dma_unmap = pci_dma_unmap, .get_iommu_class = rte_pci_get_iommu_class, .dev_iterate = rte_pci_dev_iterate, .hot_unplug_handler = pci_hot_unplug_handler, .sigbus_handler = pci_sigbus_handler, }, .device_list = TAILQ_HEAD_INITIALIZER(rte_pci_bus.device_list), .driver_list = TAILQ_HEAD_INITIALIZER(rte_pci_bus.driver_list), };
RTE_REGISTER_BUS(pci, rte_pci_bus.bus);
代码1.
如果看过内核代码,那么对这种“操作”应该会比较亲切,代码1中的操作是一种利用C语言实现类似于面向对象语言泛型的一种常见方式,例如C++。其中数据结构struct rte_pci_bus 可以看作一类总线的抽象,那么这个代码1中描述的便是PCI这种总线的实例。但是同样要注意一点,代码1中的struct rte_pci_bus rte_pci_bus这个变量的类型和变量名字长得他娘的一模一样....接下来可以看一下RTE_REGISTER_BUS这个奇怪的宏:
#define RTE_REGISTER_BUS(nm, bus) \ RTE_INIT_PRIO(businitfn_ ##nm, BUS) \ {\ (bus).name = RTE_STR(nm);\ rte_bus_register(&bus); \ } void rte_bus_register(struct rte_bus *bus) { RTE_VERIFY(bus); RTE_VERIFY(bus->name && strlen(bus->name)); /* A bus should mandatorily have the scan implemented */ RTE_VERIFY(bus->scan); RTE_VERIFY(bus->probe); RTE_VERIFY(bus->find_device); /* Buses supporting driver plug also require unplug. */ RTE_VERIFY(!bus->plug || bus->unplug); //将rte_bus结构插入至rte_bus_list链表中 TAILQ_INSERT_TAIL(&rte_bus_list, bus, next); RTE_LOG(DEBUG, EAL, "Registered [%s] bus.\n", bus->name); }
代码2.
可以看到RTE_REGISTER_BUS其实是一个宏函数,内部实现是rte_bus_register,而rte_bus_register内部做了两件事:
- 校验rte_bus结构中的方法以及属性,也就是参数的前置检查;
- 将rte_bus结构,也就是入参插入到rte_bus_list这个链表中;
那么这里我们可以初步得出一个结论:
- 调用RTE_REGISTER_BUS这个宏进行注册的总线(rte_bus)会被一个链表串起来做集中管理,以后想对某个bus调用对应的方法,只需要遍历这个链表然后找到想要操作的bus,再调用方法即可。那它的伪代码我们至少可以脑补出如代码3中描述的一样:
foreach list_node in list: if list_node is we want: list_node->method()
代码3.
但是RTE_REGISTER_BUS这个宏的出现至少带给我们如下几个问题:
- 这个宏里面实际上是一个函数,那这个函数是在哪调用的?
- 啥时候遍历这个链表然后执行rte_bus的方法(method)呢?
接下来便重点看这两个问题,先看第一个问题,这个函数是在哪调用的,通常我们看一个函数在哪调用的最常见的方法便是搜索整个项目,或用一些IDE自带的分析关联功能去找在哪个位置调用的这个宏,或这个函数,但是在RTE_REGISTER_BUS这个宏面前,没有任何一个地方调用这个宏。
还记得一个经典的问题么?
一个程序的启动过程中,main函数是最先执行的么?
在这里便可以顺便解答这个问题,再重新看代码2中的RTE_REGISTER_BUS这个宏,里面还夹杂着一个令人注意的宏,RTE_INIT_PRO,接下来为了便于分析,我们将宏里面的内容全部展开,见代码4.
/******展开前******/ /* 位于lib/librte_eal/common/include/rte_common.h */ #define RTE_PRIO(prio) \ RTE_PRIORITY_ ## prio #ifndef RTE_INIT_PRIO #define RTE_INIT_PRIO(func, prio) static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void) #endif #define _RTE_STR(x) #x #define RTE_STR(x) _RTE_STR(x) /* 位于lib/librte_eal/common/include/rte_bus.h */ #define RTE_REGISTER_BUS(nm, bus) \ RTE_INIT_PRIO(businitfn_ ##nm, BUS) \ {\ (bus).name = RTE_STR(nm);\ rte_bus_register(&bus); \ } /******展开后******/ /* 这里以RTE_REGISTER_BUS(pci, rte_pci_bus.bus)为例 */ #define RTE_REGISTER_BUS(nm, bus) \ static void __attribute__((constructor(RTE_PRIORITY_BUS), used)) businitfn_pci(void) { rte_pci_bus.bus.name = "pci" rte_bus_register(&rte_pci_bus.bus); }
代码4.
另外注意的一点是,这里如果想顺利展开,必须得知道在C语言中的宏中,出现“#”意味着什么:
- #:一个井号,代表着后续连着的字符转换成字符串,例如#BUS,那么在预编译完成后就会变成“BUS”
- ##:两个井号,代表着连接,这个地方通常可以用来实现C++中的模板功能,例如MY_##NAME,那么在预编译完成后就会变成MY_NAME
再次回到代码4中的代码,其中最令人值得注意的细节便是“__attribute__((constructor(RTE_PRIORITY_BUS), used))”,这个地方实际上使用GCC的属性将这个函数进行声明,我们可以查阅GCC的doc来看一下constructor这个属性是什么作用,以gcc 4.85为例,见图1:
- 链接: