【DPDK】谈谈DPDK如何实现bypass内核的原理 其二 DPDK部分的实现

本文详细介绍了DPDK如何通过bypass内核来操作PCI设备,从DPDK初始化、资源扫描到加载PMD驱动的过程。首先,文章回顾了DPDK启动前的准备工作,包括PCI设备与UIO驱动的角色。接着,分析了DPDK应用如何操作PCI设备,通过RTE_REGISTER_BUS宏注册总线,并在rte_eal_init中进行资源扫描。在资源扫描阶段,文章阐述了PCI总线扫描、PCI BAR的映射和PMD驱动加载的过程。最后,文章以ixgbe驱动为例,展示了PMD驱动如何获取并使用PCI BAR来配置和操作设备寄存器。
摘要由CSDN通过智能技术生成

【前言】

  关于DPDK如果实现bypass内核的原理,在上一篇《【DPDK】谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动》中已经描述了在DPDK启动前做的准备工作,那么本篇文章将着重分析DPDK部分的职责,也就是从软件的的角度来分析在第一篇文章的基础上,如何做到真正的操作设备。

注意:

  1. 本篇文章将会更着重分析软件部分的实现,也就是分析代码实现;
  2. 同样,本篇会跨过中断部分与vfio部分,中断部分与vfio会在以后另开文章继续分析;
  3. 人能力以及水平有限,没办法保证没有疏漏,如有疏漏还请各路神仙进行指正,本篇内容都是本人个人理解,也就是原创内容。
  4. 另外在分析代码的过程中,为了防止一些无挂紧要的逻辑显得代码又臭又长,会对其中不重要或者与主要逻辑不相关的代码进行省略,包括且不限于,变量声明、部分不重要数据的初始化、异常处理、无关主要逻辑的模块函数调用等。

【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内部做了两件事:

  1. 校验rte_bus结构中的方法以及属性,也就是参数的前置检查;
  2. 将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这个宏的出现至少带给我们如下几个问题:

  1. 这个宏里面实际上是一个函数,那这个函数是在哪调用的?
  2. 啥时候遍历这个链表然后执行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:

  • 链接:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值