【前言】
关于DPDK如果实现bypass内核的原理,在上一篇《【DPDK】谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动》中已经描述了在DPDK启动前做的准备工作,那么本篇文章将着重分析DPDK部分的职责,也就是从软件的的角度来分析在第一篇文章的基础上,如何做到真正的操作设备。
注意:
【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内部做了两件事:
那么这里我们可以初步得出一个结论:
foreach list_node in list:
if list_node is we want:
list_node->method()
代码3.
但是RTE_REGISTER_BUS这个宏的出现至少带给我们如下几个问题:
接下来便重点看这两个问题,先看第一个问题,这个函数是在哪调用的,通常我们看一个函数在哪调用的最常见的方法便是搜索整个项目,或用一些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语言中的宏中,出现“#”意味着什么:
再次回到代码4中的代码,其中最令人值得注意的细节便是“__attribute__((constructor(RTE_PRIORITY_BUS), used))”,这个地方实际上使用GCC的属性将这个函数进行声明,我们可以查阅GCC的doc来看一下constructor这个属性是什么作用,以gcc 4.85为例,见图1: