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


    【前言】

      关于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:

    • 链接:
  • 相关阅读:
    Baklib|SaaS产品,实现企业流程数字化
    【推荐算法系列十七】:GBDT+LR 排序算法
    灰度发布、蓝绿发布、滚动发布
    【C++】函数重载 ① ( 函数重载概念 | 函数重载判断标准 - 参数个数 / 类型 / 顺序 | 返回值不是函数重载判定标准 )
    预训练模型的多任务主动学习
    keepalived实现nginx负载均衡机高可用
    git冲突时拉取并覆盖本地代码
    uniapp图片加水印
    最强大脑(1)
    九联UNT413A_S905L3A__AI语音_默认打开ADB_完美线刷固件包【可救砖】
  • 原文地址:https://blog.csdn.net/lingshengxiyou/article/details/127855846