【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:

圖1.GCC文檔中關於constructor屬性的描述

  其實GCC文檔中已經說的很明白了,constructor會在main函數調用前而被調用,並且如果程序中如果出現了多個用GCC的constructor屬性聲明的函數,可以利用優先級對其進行排序,當然在這里,優先級數值越大的constructor優先級越小,運行的順序越靠后。

  • P.S. RTE_REGISTER_BUS展開時,另一個”used“的函數聲明比較常見,就是告訴編譯器,這個函數有用,別給老子報警(通常我們編譯時在gcc的CFLAGS中加上-Wall -Werror的參數時,一個你沒有使用的函數,gcc在編譯的時候會直接爆出一個error,”xxx define but not used“,這個used就是用來對付這種警告/錯誤的,一般在內聯匯編函數上用的比較多)

  那到了這里,第一個問題的答案已經逐漸明了

  1. 這個宏里面實際上是一個函數,那這個函數是在哪調用的?
    • 答:RTE_REGISTER_BUS內部的函數被gcc用constructor屬性進行了聲明,因此會在main函數被調用之前而運行,也就是在main函數被調用之前,rte_bus就已經加入到全局的”bus“鏈表中了。

  接下來再看第二個問題,”啥時候遍歷這個鏈表然后執行rte_bus的方法(method)呢?“,答案在dpdk的初始化函數rte_eal_init中。

【2.資源的掃描】

  在准備工作完成后,我們現在有了一個全局鏈表,這個鏈表中存儲着一個個總線的實例,也就是”struct rte_bus“結果,那么此時這個全局鏈表可以看作一個管理結構,想要完成對應的任務,只需要遍歷這個鏈表就可以了。

  來到DPDK的初始化函數rte_eal_init函數,這個函數調用非常復雜, 而且涉及的模塊眾多,根本沒有辦法進行一次性全面的分析,但是好處是我們只需要找到我們關注的地方即可,見代碼5:

int
rte_eal_init(int argc, char **argv)
{
    ......;//初始化的模塊過多,並且無關,直接忽略
    if (rte_bus_scan()) {
        rte_eal_init_alert("Cannot scan the buses for devices");
        rte_errno = ENODEV;
        rte_atomic32_clear(&run_once);
        return -1;
    }
    ......;//初始化的模塊過多,並且無關,直接忽略
}

/* 掃描事先注冊好的全局總線鏈表,調用scan方法進行掃描 */
int
rte_bus_scan(void)
{
    int ret;
    struct rte_bus *bus = NULL;
    //遍歷總線鏈表
    TAILQ_FOREACH(bus, &rte_bus_list, next) {
        //調用某一總線的scan函數鈎子
        ret = bus->scan();
        if (ret)
            RTE_LOG(ERR, EAL, "Scan for (%s) bus failed.\n",
                bus->name);
    }

    return 0;
}

代碼5.

  在代碼5中的代碼中,rte_eal_init函數調用了rte_bus_scan函數,而rte_bus_scan函數是一段非常簡單的代碼,功能就是是對總線進行掃描,然后調用事先注冊好的某一總線實例的scan函數鈎子,那么回到代碼1.中,我們來看一下PCI總線的scan函數是什么,答案便是rte_pci_scan函數,那么接下來的任務便是進入rte_pci_scan函數,看了下PCI這種總線的掃描函數做了哪些事情。

/*
 * PCI總線的掃描函數
*/
int
rte_pci_scan(void)
{
    ......//變量聲明,省略
    //1.打開/sys/bus/pci/devices/目錄
    dir = opendir(rte_pci_get_sysfs_path());
    ......//異常處理,省略
    //2.接下來的內容便是掃描devices目錄下所有的PCI地址目錄
    while ((e = readdir(dir)) != NULL) {
        if (e->d_name[0] == '.')
            continue;
        ......//格式化字符串,省略
        //3.掃描某個PCI地址目錄
        if (pci_scan_one(dirname, &addr) < 0)
            goto error;
    }
    ......//異常處理,資源釋放,省略
}

代碼6.

  其中代碼6的邏輯非常簡單,就是進入/sys/bus/pci/devices目錄掃描目錄下所有的PCI設備,然后再進入PCI設備的目錄下掃描PCI設備的資源,如圖2所示。

 

圖2.rte_pci_scan的原理

  進入pci_scan_one函數后,便開始對這個PCI設備目錄中的每一個文件進行讀取,拿到對應的信息,在第一篇文章中也提到過,內核會將PCI設備的信息通過文件系統這種特殊的接口暴露給用戶態,供用戶態程序讀取,那么pci_scan_one的邏輯便如圖3所示。

圖3.pci_scan_one的函數執行邏輯

  可以看到圖3中pci_scan_one函數的執行邏輯,其實同樣非常簡單,就是將PCI設備目錄下的sysfs進行讀取、解析。這11步中值得注意的有3步,分別是第9、第10以及第11步,接下來將重點觀察這3步的內容,先從第9步說起。

  其實第9步調用pci_parse_sysfs_resource函數執行的內容就是去解析/sys/bus/pci/devices/0000:81:00.0/resource這個文件,之前在第一篇文章中也提到過,這個resource文件中包含着PCI BAR的信息,其中有分為三列,第一列為PCI BAR的起始地址,第二列為PCI BAR的終止地址,第三列為PCI BAR的標識,那么這個函數便是用於解析resource文件,拿到對應的PCI BAR信息,見代碼7.

/*
 * 解析[pci_addr]/resource文件
 * @param filename resource文件所在的目錄,例如/sys/bus/pci/devices/0000:81:00.0/resource
 * @param dev PCI設備的實例
*/
static int
pci_parse_sysfs_resource(const char *filename, struct rte_pci_device *dev)
{
    ......//變量聲明,省略
    //1.open resource文件
    f = fopen(filename, "r");
    ......//異常處理,省略
    //2.遍歷6個PCI BAR,關於PCI BAR的數量與作用在上一篇文章中已經闡述
    for (i = 0; i<PCI_MAX_RESOURCE; i++) {
        ......//異常處理,省略
        //3.解析某一行PCI BAR的字符串,拿到PCI BAR的起始地址、結束地址以及標識
        if (pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr,
                &end_addr, &flags) < 0)
            goto error;
        //4.只要Memory BAR,把信息拿到,存至數據結構中,至於為什么只需要Memory BAR,在上一篇文章中已經闡述完畢
        if (flags & IORESOURCE_MEM) {
            dev->mem_resource[i].phys_addr = phys_addr;
            dev->mem_resource[i].len = end_addr - phys_addr + 1;
            dev->mem_resource[i].addr = NULL;
        }
    }
    ......//異常處理,資源釋放,省略
}

代碼7.

  可以看到,pci_parse_sysfs_resource函數內部的執行邏輯同樣非常簡單,就是解析resource文件,把Memory類型的PCI BAR信息提去並拿出來(這里注意,關於為什么只拿Memory類型的PCI BAR在上一篇文章中已經闡述),那么圖3中的步驟9的作用便分析完畢,接下來看圖3中的步驟10.

圖3中的步驟10主要是拿到當前PCI設備用的驅動類型,但是是怎么拿到的呢?答案也很簡單,看軟連接的鏈接信息就可以得知,見圖4.所以說這個pci_get_kernel_driver_by_path的函數名命名可以說是非常到位了。

 

圖4.pci_get_kernel_driver_by_path的實現原理

  那么至此,圖3中的步驟10的原理也闡述完畢,接着看步驟11,步驟11主要涉及數據結構的關系,其中rte_pci_bus這個結構體象征着PCI總線,而同樣已知的是,一條總線上會掛在一些數量的總線設備,舉個例子,PCI總線上會有一些PCI設備,那么這些PCI設備的抽象類型便是rte_pci_devices類型,那么這也同樣是一個包含的關系,即rte_pci_bus這個結構從概念上是包含rte_pci_devices這個類型的,所以在rte_pci_bus這個結構上有一個devices_list鏈表用來集中管理總線上的設備,數據結構關系可以如圖5所示。

圖5.rte_pci_bus與rte_pci_device數據結構關系圖

  可以看到在圖5中,rte_pci_device這個結構被串在了rte_pci_bus結構中的devices_list這個鏈表中,同樣需要值得注意的是rte_pci_device這個結構體對象,其中很多的成員屬性在這里先說一下

  • TAILQ_ENTRY(rte_pci_device) next:這個對象就是一個鏈表結構,用來將前后的rte_pci_device串起來,方便管理,沒什么實際意義;
  • struct rte_device device:struct rte_device這個結構體象征着設備的一些通用信息,舉個例子,不管是什么類型設備,PCI設備還是啥SDIO設備,他們都具有“名字”、“驅動”這些共性的特征,那么關於這些共性特征描述便抽象成struct rte_device這個結構體類型;
  • struct rte_pci_addr addr:struct rte_pci_addr這個結構體象征着一個PCI地址,舉個例子,0000:81:00.1,這便是一個PCI設備的地址,並且實際上這個地址是由四部分組成的,第一個部分叫做"domain",也就是第一個冒號之前的4個數字0000,第二個部分叫做“bus”,也就是第一個冒號和第二個冒號之間的2個數字81,第三個部分叫做“device_id”,在第二個冒號和最后一個句號之間的2個數字00,最后一個部分也就是第四個部分叫做"function",也就是最后一個句號之后的1個數字1。但是關於PCI地址為啥這么分,本人也不知道...;
  • struct rte_pci_id id:struct rte_pci_id這個結構體象征着PCI驅動的一些ID號,包括之前反復提過的class id、vendor id、device id、subsystem vendor id以及subsystem device id;
  • struct rte_mem_resource mem_resource[6]:這個是重點,mem_resource,類似於內核中的resource結構體,里面存着解析完resource文件后的PCI BAR信息,也就是圖3中的步驟9將resource文件中的信息提去后存志這個mem_resource對象中;
  • struct rte_intr_handle intr_handle:中斷句柄,本篇文章不包含中斷相關內容,關於中斷的原理解析會放到以后的文章中介紹;
  • struct rte_pci_driver driver:這個也是重點,描述這個PCI設備用的是何種驅動,但是這里需要注意的是,這里的驅動可不是指的內核中的那些驅動,也不是指的igb_uio/vfio-pci,而是指的DPDK的用戶態PMD驅動;
  • max_vfs:這個主要是與sriov相關,指的是這個PCI設備最大能虛擬出幾個VF,sriov是網絡虛擬化領域中常用的一種技術;
  • kdrv:內核驅動,但是也要注意,只要不是igb_uio/vfio-pci驅動,其他的驅動一律變成UNKNOWN,比如現在的網卡是一個內核的ixgbe驅動,DPDK應用不關心,它只關心是不是igb_uio/vfio-pci驅動,所以一律賦值為UNKNOWN;
  • vfio_req_intr_handle:這個同樣是重點,vfio驅動的中斷句柄,但是本篇文章不涉及中斷,也不涉及vfio,關於這兩個地方以后會專門開文章來介紹。

  至此,DPDK啟動過程中,PCI資源的掃描任務就此完成,在這一階段完成后,可以得到一個非常重要的結論:

  • 掃描的PCI設備資源、屬性信息全部被存到了圖5中的rte_pci_bus.device_list這個鏈表中

  那么根據這個結論,也可以推導出接下來要做什么事情,那便是去遍歷這個device_list,對每一個PCI設備做接管、初始化工作。

【3.PCI設備加載PMD驅動】

  接下來便是核心的地方,根據第二章的描述,現在已經將每一個PCI設備掃描完成,拿到了關鍵的信息,接下來便是怎么根據這些信息來完成PMD驅動的加載。再次回到rte_eal_init這個DPDK初始化的關鍵函數。

 1 int rte_eal_init(int argc, char **argv)
 2 {
 3     ......//其他模塊初始化,省略 
 4     //1.掃描總線,第二章已經分析完畢
 5     if (rte_bus_scan()) {
 6         ......//異常處理,省略
 7     }
 8     ......//其他模塊初始化,省略
 9     //2.總線探測
10     if (rte_bus_probe()) {
11     ......//異常處理省略
12     }
13     .......//其他處理,省略
14 }

代碼8.

  第二個關鍵函數便是rte_bus_probe函數,這個函數就是負責將總線數據結構上的設備進行驅動的加載,進入rte_bus_scan的函數邏輯。

//總線掃描函數
int rte_bus_probe(void)
{
    int ret;
    struct rte_bus *bus, *vbus = NULL;
    //1.遍歷rte_bus_list鏈表,拿到事先注冊的所有rte_pci_bus數據結構
    TAILQ_FOREACH(bus, &rte_bus_list, next) {
        if (!strcmp(bus->name, "vdev")) {
            vbus = bus;
            continue;
        }
        //2.調用總線數據結構的probe鈎子函數,對於pci設備來說,那么就是rte_pci_probe函數
        ret = bus->probe();
        if (ret)
            RTE_LOG(ERR, EAL, "Bus (%s) probe failed.\n",
                bus->name);
    }
    ......//省略
    return 0;
}

代碼9.

  可以看到rte_bus_probe函數的實現邏輯同樣非常簡單,見代碼1中的rte_pci_bus對象的注冊,可以看到probe這個函數鈎子就是rte_pci_bus這個結構中的rte_pci_probe函數,那么接下來便可以着重分析PCI總線的probe函數,也就是rte_pci_probe函數。

//PCI總線的探測函數
int rte_pci_probe(void)
{
    ......//初始化,變量聲明,省略
    //1.遍歷rte_pci_bus的device_list鏈表,拿到每一個PCI設備對象
    FOREACH_DEVICE_ON_PCIBUS(dev) {
        probed++;

        devargs = dev->device.devargs;
        //對PCI設備對象調用pci_probe_all_drivers函數,這里的決策是要么探測所有,要么根據白名單進行選擇性探測,在DPDK初始化時可以指定白名單參數,對指定的PCI設備進行探測
        if (probe_all)
            ret = pci_probe_all_drivers(dev);
        else if (devargs != NULL &&
            devargs->policy == RTE_DEV_WHITELISTED)
            ret = pci_probe_all_drivers(dev);
        ......//異常處理,省略
    }
    return (probed && probed == failed) ? -1 : 0;
}

//用PMD驅動對pci設備進行掛載
static int
pci_probe_all_drivers(struct rte_pci_device *dev)
{
    ......//異常處理、變量聲明,省略
    //1.遍歷事先注冊好的驅動鏈表,注意這里的PMD驅動的注冊原理與總線的注冊邏輯類似,可以自行分析
    FOREACH_DRIVER_ON_PCIBUS(dr) {
        //2.拿驅動去探測設備,這里的邏輯是事先注冊的驅動挨個探測一遍,匹配和過濾的規則在函數內部里實現
        rc = rte_pci_probe_one_driver(dr, dev);
        ......//異常處理,省略
        return 0;
    }
    return 1;
}

代碼10.

  接着再進入rte_pci_probe_one_driver,看PCI設備如何關聯上對應的PMD驅動,再如何加載驅動的,代碼分析見代碼11.

static int
rte_pci_probe_one_driver(struct rte_pci_driver *dr,
             struct rte_pci_device *dev)
{
    ......//參數檢查,變量初始化,省略
    //1.對PCI設備和驅動進行匹配,道理也很簡單,一個I350的卡不可能給他上i40e的驅動
    if (!rte_pci_match(dr, dev))
        /* Match of device and driver failed */
        return 1;

    //2.看這個設備是否是在黑名單參數里,如果在,那就跳過,類似於白名單,在DPDK初始化時可以指定黑名單
    if (dev->device.devargs != NULL &&
        dev->device.devargs->policy ==
            RTE_DEV_BLACKLISTED) {
        return 1;
    }
    //3.檢查numa節點的有效性
    if (dev->device.numa_node < 0) {
        ......//異常處理,省略
    }
    //4.檢查設備是否已經加載過驅動,都加載了那還加載個屁,接着跳過
    already_probed = rte_dev_is_probed(&dev->device);
    if (already_probed && !(dr->drv_flags & RTE_PCI_DRV_PROBE_AGAIN)) {
        return -EEXIST;
    }
    //5.邏輯到了這里,那么設備是已經確認了沒有加載驅動,並且已經和驅動配對成功,那么進行指針賦值
    if (!already_probed)
        dev->driver = dr;
    //6.驅動是否需要PCI BAR資源映射,對於大多數驅動,ixgbe、igb、i40e等驅動,都是需要進行重新映射的,不映射拿不到PCI BAR
    if (!already_probed && (dr->drv_flags & RTE_PCI_DRV_NEED_MAPPING)) {
        //7.調用rte_pci_map_device對設備進行PCI BAR資源映射
        ret = rte_pci_map_device(dev);
        ......//異常處理,省略
    }

    //8.調用驅動的probe函數進行驅動的加載
    ret = dr->probe(dr, dev);
        ......//異常處理,省略
    return ret;
}

代碼11.

  代碼11分析了rte_pci_probe_one_driver函數的執行邏輯,到這里,我們重新梳理一下從rte_eal_init函數到rte_pci_probe_one_driver的函數調用流程以及邏輯流程,見圖6與圖7.

圖6.rte_eal_init中PCI設備的掃描到加載函數調用過程

  在進入PMD驅動具體的加載函數前,先說一下圖6中的粉色框標識的函數rte_pci_device_map,這個函數執行了重要的PCI BAR映射邏輯,因此這個函數屬於一個重要的關鍵函數,所以先分析一下rte_pci_device_map這個函數的實現,見代碼12.

//對PCI設備進行映射,這里實際說的比較籠統,起始是對PCI設備的PCI BAR資源進行映射到用戶空間,讓應用程序可以訪問、操作以及配置PCI BAR
int rte_pci_map_device(struct rte_pci_device *dev)
{
    switch (dev->kdrv) {
    case RTE_KDRV_VFIO:
#ifdef VFIO_PRESENT
    //如果是VFIO驅動接管,則進入pci_vfio_map_resource,也就是進入vfio的邏輯來映射資源
    if (pci_vfio_is_enabled())
        ret = pci_vfio_map_resource(dev);
#endif
        break;
    case RTE_KDRV_IGB_UIO:
    case RTE_KDRV_UIO_GENERIC:
        //如果是uio驅動,那么就進入pci_uio_map_resource,也就是進入uio的邏輯來映射資源
        if (rte_eal_using_phys_addrs()) {
            ret = pci_uio_map_resource(dev);
        }
        break;
    }
    ......//異常處理,省略
}
//uio驅動框架下的映射PCI設備資源
int pci_uio_map_resource(struct rte_pci_device *dev)
{
    ......//參數檢查、變量初始化,省略
    //1.申請uio資源
    ret = pci_uio_alloc_resource(dev, &uio_res);
    //2.對6個PCI BAR進行映射
    for (i = 0; i != PCI_MAX_RESOURCE; i++) {
        //跳過無效BAR
        phaddr = dev->mem_resource[i].phys_addr;
        if (phaddr == 0)
            continue;
        //其實對於intel的網卡,只有BAR0 & BAR1能進行映射,其中在64bit的工作模式下,BAR 0和BAR 1被歸為同一個PCI BAR,這里的原理可以看上一篇文章
        //3.調用pci_uio_map_resource_by_index函數對具體的一塊PCI BAR進行映射
        ret = pci_uio_map_resource_by_index(dev, i,
                uio_res, map_idx);
        map_idx++;
    }

    uio_res->nb_maps = map_idx;

    TAILQ_INSERT_TAIL(uio_res_list, uio_res, next);
    ......//異常處理,省略
}
//PCI設備對某個BAR進行映射
int pci_uio_map_resource_by_index(struct rte_pci_device *dev, int res_idx,
        struct mapped_pci_resource *uio_res, int map_idx)
{
    ......//變量初始化、異常處理,省略
    
    if (!wc_activate || fd < 0) {
        ......//字符串處理,拿到resource0..N的文件路徑,舉個例子/sys/bus/pci/devices/0000:81:00.0/resource0

        //1.對resource0..N開始open
        fd = open(devname, O_RDWR);
        ......//異常處理,省略
    }
    //2.對這個resource0..N進行映射
    mapaddr = pci_map_resource(pci_map_addr, fd, 0,
            (size_t)dev->mem_resource[res_idx].len, 0);
    ......//異常處理,省略
    //3.對映射完成的空間進行長度累加,從這里可以看出,如果要映射多個PCI BAR,dpdk會讓這些映射后的虛擬空間是連續的
    pci_map_addr = RTE_PTR_ADD(mapaddr,
            (size_t)dev->mem_resource[res_idx].len);
    //4.賦值,其中最重要的就是這個mapaddr,這個指針內部的地址,就是PCI BAR映射到用戶空間的虛擬地址,最終這個地址會被保存在mem_resource結構中的addr至真中
    maps[map_idx].phaddr = dev->mem_resource[res_idx].phys_addr;
    maps[map_idx].size = dev->mem_resource[res_idx].len;
    maps[map_idx].addr = mapaddr;
    maps[map_idx].offset = 0;
    strcpy(maps[map_idx].path, devname);
    dev->mem_resource[res_idx].addr = mapaddr;
    ......//異常處理,省略
}

/*
 * 對resource0..N資源進行映射
 * @param requested_addr 請求的地址,告訴從哪個虛擬地址開始映射,主要是為了讓多個PCI BAR的情況下,映射后的虛擬地址是連續的,這樣方便管理
 * @param fd resource0..N文件的文件描述符
 * @param offset 偏移,注意,映射PCI設備的資源文件resource0..N,這里的偏移必須是0,關於為什么是0,Linux Kernel Doc有規定,可以見上一篇文章
 * @param size 映射的空間大小,這個可以通過PCI BAR的結束地址 - PCI BAR的起始地址 + 1計算出來
 * @param additional_flags 控制標識,為0
 * @return 成功返回映射后的虛擬地址,失敗返回NULL
*/
void *
pci_map_resource(void *requested_addr, int fd, off_t offset, size_t size,
         int additional_flags)
{
    void *mapaddr;

    //1.映射PCI resource0..N文件
    mapaddr = mmap(requested_addr, size, PROT_READ | PROT_WRITE,
            MAP_SHARED | additional_flags, fd, offset);
    ......//異常處理,省略
    return mapaddr;
}

代碼12.

  代碼12由於涉及到4個函數,並且關系是強相關的,拆解后不利於分析,因此插入到一塊代碼區域中,顯得有些長,但是非常重要。其中代碼12的函數調用流程如圖7所示。

圖7.PCI BAR資源映射的函數調用關系

  其中PCI BAR的資源映射函數rte_pci_map_device至少告訴我們這么幾個信息:

  1. DPDK拿到PCI BAR不是通過UIO驅動拿到的,而是直接通過Kernel對用戶空間的接口,也就是通過sysfs拿到的,具體就是映射resource0..N文件。這里在上一篇文章中已經介紹過;
  2. 映射后的虛擬地址已經存至了rte_pci_device->mem_resouce[PCI_BAR_INDEX].add指針中。

  那么至此,rte_bus_probe這個總線掛載函數的內部執行流程已經分析完畢,同樣也拿到了關鍵的資源,也就是PCI BAR映射到用戶空間后的地址,通過這個地址,便拿到了寄存器的基地址,接下來對PCI設備的配置以及操作只需要將這個基地址 + 寄存器地址偏移,即可拿到寄存器地址,便可以對其進行讀寫。在進一步分析之前,我會先給出rte_bus_scan函數執行的邏輯圖,請注意的是,函數執行的邏輯圖會從宏觀上闡述執行的邏輯,所以會忽略函數調用的維度,關於函數調用關系的維度,見圖6以及圖7即可。接下來rte_bus_probe函數的執行邏輯圖請見圖8.

圖8.rte_bus_probe函數的執行邏輯

  說完了rte_bus_probe的函數執行邏輯,再來完善一下圖5的數據結構關系,完善后見圖8。

圖8.圖5數據結構的完善

  但是到了這里還沒有結束,接下來便進入PMD驅動的加載函數。

【4.PMD驅動的加載】

  第四章將着重以ixgbe驅動為例,講解PMD驅動是如何加載的,先進入ixgbe的probe函數,也就是圖8中的eth_ixgb_pci_probe函數。

tatic int
eth_ixgbe_pci_probe(struct rte_pci_driver *pci_drv __rte_unused,
        struct rte_pci_device *pci_dev)
{
    ......//初始化以及其他處理,省略

    retval = rte_eth_dev_create(&pci_dev->device, pci_dev->device.name,
        sizeof(struct ixgbe_adapter),
        eth_dev_pci_specific_init, pci_dev,
        eth_ixgbe_dev_init, NULL);
    ......//其他處理,省略
    return 0;
}

代碼13.

可以看到eth_ixgbe_pci_probe的主要處理還是非常簡單的,就是調用rte_eth_dev_create去創建PMD驅動,那么接着進入rte_eth_dev_create函數進行分析,見代碼14.這個函數較為重要,會重點分析

/*
 * 創建PMD驅動
 * @param device[in] rte_pci_device->rte_device,在圖8中已經說明為設備的通用信息結構
* @param name[in] 設備名
* @param priv_data_size 私有數據的大小,這個私有數據很重要,可以理解指的就是PMD驅動,因為每個網卡的信息都可能不一樣,所以將這些私有數據打成一個void *來實現泛型
* @param ethdev_bus_specific_init 一個函數指針,為eth_dev_bus_specific_init函數,這個函數有BUG,在multiprocess模型下,此BUG已被本人解決並提交了patch,目前已經被intel社區采納,在20.02版本以后修復,BUG可以看這篇文章https://www.cnblogs.com/jungle1996/p/12191070.html * @param bus_init_params 就是rte_pci_device結構,這個結構在圖8中已經說明為PCI設備的描述結構
* @param ethdev_init 函數指針,為PMD驅動初始化函數,在ixgbe這個驅動下為eth_ixgbe_dev_init
* @param init_param PMD驅動初始化的參數,一般為NULL
*/ int __rte_experimental rte_eth_dev_create(struct rte_device *device, const char *name, size_t priv_data_size, ethdev_bus_specific_init ethdev_bus_specific_init, void *bus_init_params, ethdev_init_t ethdev_init, void *init_params) { ......//變量聲明,參數檢查,省略 //1.拿到ete_eth_dev結構,對於不同的類型的進程拿到方法不一樣,至於為啥這樣,是因為這個結構中的一些屬性來自於共享內存,
//因此對於secondary進程需要attach到primary進程中的共享內存中,拿到這些共享內存數據。
if (rte_eal_process_type() == RTE_PROC_PRIMARY) {
//2.申請內存,得到rte_eth_dev結構,注意這個結構並不是來自於共享內存,而是這個結構中的一些屬性來自於共享內存,這個結構只是一個local變量
//但是請注意,在這個函數的內部實現中,已經拿到了共享內存地址,並賦值至rte_eth_dev->data這個指針 ethdev
= rte_eth_dev_allocate(name);
//3.如果指定了有私有數據,那就申請這個私有數據 if (priv_data_size) { ethdev->data->dev_private = rte_zmalloc_socket( name, priv_data_size, RTE_CACHE_LINE_SIZE, device->numa_node); ......//異常處理,省略 } } else {
//4.由於secondary進程的權限比較低,沒有掌控內存的權限,因此關鍵數據只能通過attach到primary暴露的共享內存中,拿到關鍵數據
//其實這個地方主要是要拿到rte_eth_dev->data這個指針指向的共享內存(因為這里面有PCI BAR映射后的地址) ethdev
= rte_eth_dev_attach_secondary(name); ......//異常處理,省略 } //5.指針賦值,沒啥說的,就是讓PMD驅動也可以通過device來拿到PCI設備的信息 ethdev->device = device; //6.調用eth_dev_bus_specific_init函數,這個函數內部有BUG,請注意 if (ethdev_bus_specific_init) { retval = ethdev_bus_specific_init(ethdev, bus_init_params); ......//異常處理,省略 } //7.調用PMD驅動的初始化,對PMD驅動進行初始化,在xigbe驅動下為eth_ixgbe_dev_init retval = ethdev_init(ethdev, init_params); ......//異常處理,省略 rte_eth_dev_probing_finish(ethdev); }

代碼14.

  可以看到,代碼14中的rte_eth_dev_create函數還是比較重要的,可以說是銜接PCI設備與PMD驅動的接口層函數。所以懶得看代碼中的注釋的可以直接看圖9給出的rte_eth_dev_create的函數內部流程圖。見圖9.

圖9.rte_eth_dev_create函數的執行流程

  分析完rte_eth_dev_create函數后,便自然的進入了PMD驅動的初始化函數,接下來會以ixgbe這種驅動進行分析,那么在ixgbe驅動下,初始化函數為eth_ixgbe_dev_init。

  接下來不會全面分析,因為對於驅動而言,他的初始化邏輯是巨他媽的長的...分析這部分代碼,我們只需要記住我們的初衷即可,我們的初衷即為:

dpdk應用內部是如何操作pci設備的呢?是怎么讓pci設備可以將數據包直接扔到用戶態的呢?

  我們之前也闡述了,為了實現這個初衷,我們一定要不惜一切代價讓PMD驅動拿到PCI BAR,然后通過PCI BAR去操作寄存器,並且同過第2章和第3章的分析,我們其實已經拿到了PCI BAR,通過mmap映射resource0..N這個內核通過sysfs開放的接口,現在這個PCI BAR經過映射后的虛擬機地址正在rte_pci_device->mem_resource[idx].addr中沉睡,我們的任務只不過是讓PMD驅動結構拿到這個地址而已,換而言之,其實就是等號左右賦值一下就可以完成,那么我們來看eth_ixgbe_dev_init函數,見圖10.

 

圖10.PCI BAR虛擬地址的賦值

#define IXGBE_DEV_PRIVATE_TO_HW(adapter)\
    (&((struct ixgbe_adapter *)adapter)->hw)

/*
 * ixgbe驅動的初始化函數
 * @param eth_dev[in] PMD驅動描述結構
*/
static int
eth_ixgbe_dev_init(struct rte_eth_dev *eth_dev, void *init_params __rte_unused)
{
    ......//無關邏輯,省略
    //1.將PMD驅動中的私有空間進行轉換成ixgbe_adapter結構,再拿到ixgbe_adatper其中的hw屬性,注意這個變量的內存位於共享內存中,因此secondary也是拿得到的,這就是secondary為啥可以讀網卡寄存器狀態,因此secondary其實是可以通過共享內存拿到PCI BAR的
    struct ixgbe_hw *hw =
        IXGBE_DEV_PRIVATE_TO_HW(eth_dev->data->dev_private);
    ......//無關邏輯,省略
    //2.掛鈎子函數,給ixgbe這個PMD驅動指定收發包函數
    eth_dev->dev_ops = &ixgbe_eth_dev_ops;
    eth_dev->rx_pkt_burst = &ixgbe_recv_pkts;
    eth_dev->tx_pkt_burst = &ixgbe_xmit_pkts;
    eth_dev->tx_pkt_prepare = &ixgbe_prep_pkts;

    if (rte_eal_process_type() != RTE_PROC_PRIMARY) {
        ......//secondary進程的相關邏輯,省略
    }
    ......//拷貝PCI設備信息
    rte_eth_copy_pci_info(eth_dev, pci_dev);

    //3.最重要的一步,拿到PCI BAR以及設備號還有廠商號
    //至此,PMD驅動成功拿到經過映射到進程虛擬空間的PCI BAR
    hw->device_id = pci_dev->id.device_id;
    hw->vendor_id = pci_dev->id.vendor_id;
    hw->hw_addr = (void *)pci_dev->mem_resource[0].addr;
    hw->allow_unsupported_sfp = 1;

    //4.其他部分的初始化工作,先暫時省略

    return 0;
}

代碼15.

  經過代碼15所示,我們可以看到在eth_ixgbe_dev_init這個函數中,PMD驅動已經拿到了經過mmap映射后的在進程用戶空間的PCI BAR地址,接下來對PCI設備的配置,通過這個PCI BAR + 寄存器地址偏移拿到寄存器地址,便可以對寄存器進行讀寫、配置。到這里我們先暫停一下腳步,過一下數據結構之間的關系。見圖11.

 

 

  從圖11中可以看出,一番操作后,PCI BAR已經被ixgbe_adapter-hw指針指向,接下來想拿到PCI BAR只需要對rte_dev->data->dev_private調用IXGBE_DEV_PRIVATE_TO_HW即可拿到PCI BAR。而且還要注意的是由於rte_dev->data指針指向的空間為共享內存,因此PCI BAR實際上也在共享內存中,這也就是Secondary進程可以讀取網卡的寄存器配置以及狀態,就是因為Seconday進程實際上可以通過共享內存拿到PCI BAR,然后想讀寄存器信息以及狀態,和primary進程相同,只需要PCI BAR + 寄存器地址偏移拿到寄存器地址,便可以實現對寄存器狀態信息的讀取。

  但是這還沒有結束,我們還差最后一個問題沒有解決,那便是,DPDK怎么讓PCI設備把包直接扔到的用戶態,這部分將會放在本系列的第三章中講解。

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM