1. 程式人生 > >【DPDK】談談DPDK如何實現bypass核心的原理 其一 PCI裝置與UIO驅動

【DPDK】談談DPDK如何實現bypass核心的原理 其一 PCI裝置與UIO驅動

【前言】

  隨著網路的高速發展,對網路的效能要求也越來越高,DPDK框架是目前的一種加速網路IO的解決方案之一,也是最為流行的一套方案。DPDK通過bypass核心協議棧與核心驅動,將驅動的工作從核心態移至使用者態,並利用polling mode的執行緒工作模式加速網路I/O使得網路IO效能出現大幅度的增長。

  在使用DPDK的時候,我們常常會說提到用DPDK來接管網絡卡以達到bypass核心驅動以及核心協議棧的操作,本篇文章將主要分析DPDK是如何實現的bypass核心驅動來實現所謂的“接管網絡卡”的功能。

注意:

  1. 本篇文章會涉及一些pci裝置的內容,但是不會重點講解pci裝置,pci裝置中的某些規則就是這麼設計的,並沒有具體原因。
  2. 本篇部分原理的講解會以Q&A的方式拖出,因為DPDK bypass核心的這部分涉及的知識維度比較多,沒有辦法按照線性的思路講解。
  3. 本人能力以及水平有限,沒辦法保證沒有疏漏,如有疏漏還請各路神仙進行指正,本篇內容都是本人個人理解,也就是原創內容。
  4. 由於內容過多,本篇文章會著重基礎的將PCI以及igb_uio相關的知識與分析,以便於不光是從DPDK本身,而是全面的瞭解DPDK如果做到的bypass核心驅動,另外關於DPDK的程式碼部分實現將會放在後續文章中放出,另外還有DPDK的中斷模式以及vfio也會在後續的文章中依次發出(先開個坑,立個flag)

【1.談一談使用】

  通常啟動一個基於dpdk開發的應用,都需要幾步準備來完成。

  1. 首先需要插入igb_uio/vfio-pci這兩個驅動中的一個,接下來會以igb_uio為例講解(因為簡單...vfio還是有點複雜的...vfio的解析會放在以後的文章中放出)。
  2. 其次需要執行dpdk-devbinds.py這個dpdk官方給出的py指令碼,以此來完成核心驅動到igb_uio/vfio的接管。接管之後,再次執行dpdk-devbinds可以很明顯的看到驅動從ixgbe轉為了igb_uio。請見圖1.
  3. 執行dpdk應用,以-p引數指定要接管的網口,例如-c 0x03,那麼接管的網口便是port 0和port 1.

 

圖1.接管前後pci裝置驅動發生的變化

 

  那麼經過上述三個操作,至少腦子裡會產生這麼幾個問題:

  Q:igb_uio/vfio-pci的作用是什麼?為什麼要用這兩個驅動?這裡的“驅動”和dpdk內部對網絡卡的“驅動”(dpdk/driver/)有什麼區別呢?

  Q:dpdk-devbinds是如何做到的將核心驅動解綁後繫結新的驅動呢?

  Q:dpdk應用內部是如何操作pci裝置的呢?是怎麼讓pci裝置可以將資料包直接扔到使用者態的呢?

  這三個問題,實際上也是我當初在研究這一部分是遇到的三個問題。首先我們先來看第一個問題。

【問題一:igb_uio/vfio-pci是什麼?】

  我們會以igb_uio驅動為例進行講解。這裡其實很難一步講清楚igb_uio的作用以及實現原理,所以接下來的講解還是會以Q&A和“挖坑式”的方式進行逐步將原理展現給各位看官面前。先說說操作一個外設,最先想到的是什麼呢?如果有過微控制器等嵌入式外設開發的朋友肯定會冒出這樣的一個想法

我得配置這個外設,為此我需要找到它的暫存器,但是找到它的暫存器前提是我得先拿到基地址才行,接下來通過基地址+暫存器偏移就能找到暫存器所在的地址,然後就可以配置了

  所以第一個任務便是我們要拿到”基地址“,首先有必要先科普一下pci裝置的基地址。因此我必須得掏出一張圖,即描述pci配置空間的一張圖,如果圖2所示。

圖2.pci裝置的配置空間

  圖2為pci配置空間的分佈圖,在圖中,0x0010 ~ 0x0028這24個位元組中,分佈著6個PCI BAR(base address register),也就是最最重要的“基地址”,那這裡有人可能會想問“這個圖和我們有關係麼?這個圖中的空間在哪?我們該怎麼解析?”,答案是“無關”,這些圖中的資訊事實上在系統啟動時,就已經被解析完成了,以檔案系統的方式供使用者態程式取讀取。但是這裡其實有這樣的一個問題:

PCI裝置為啥有6個BAR,而不是3個、8個?這些BAR都有啥區別?實際訪問暫存器的時候以哪一個BAR為基準呢?

  其實解釋這個問題,是一件簡單而又不簡單的事情。簡單是因為pci裝置規定就是有6個bar空間,而不簡單是因為不知道為什麼規定6個bar空間。那麼這些BAR又有什麼區別呢?這裡要引用一下stackoverflow上面一位老哥說的話,見圖3.(這裡其實我之前也一直不太明白,因為國內的很多論壇帖子都是千篇一律...很難篩選出自己想要的資訊...)

圖3.不同BAR空間的區別之StackOverflow

  其實關鍵就是藍色的那句話,即”6個槽(BAR)允許裝置以不同的目的提供不同的區域“,根據這個線索,我們來看一下intel 82599這款經典的10G網絡卡的datasheet中9.3.6中的解釋。見圖4.

 

圖4.intel 82599 datasheet中關於不同pci bar的劃分

  可以看到這款經典網絡卡(其實intel的卡基本都是這麼分的)主要將6個pci bar分成了三塊區域:

  • Memory BAR : 記憶體BAR,Memory BAR標誌著這塊BAR空間位於記憶體空間,通過mmap對映後可以直接訪問。
  • I/O BAR : IO BAR空間,I/O BAR標誌著這塊BAR空間位於IO空間,對其的訪問不能像Memory BAR那樣對映之後就可以隨心所欲訪問,IO BAR必須通過專門的操作來進行讀寫。
  • MSI-X BAR : 這個BAR空間主要是用來配置MSI -X 中斷向量。

  那麼這裡可能有人會問,一共不是6個BAR空間麼?這裡只分了3個區域,那麼每個區域分多少呢?這裡請注意的是關於圖3中6個PCI BAR,每個PCI BAR都是32位的,但是像82599這種工作在64位的網絡卡,其實就只有三個BAR。BAR0 BAR1為Memory BAR,BAR2 BAR3為I/O BAR,BAR4 BAR5為MSI-X BAR。這裡我們可以對照一款低端網絡卡I350的datasheet,見圖5.

圖6.I350網絡卡datasheet中關於BAR分佈的描述

 

   從圖6可以看到,對於I350這種低端的千兆網絡卡,可以將其配置位工作在32位還是64位模式下,但是對於82599這種萬兆10g的卡,就沒那麼多選擇餘地了,只能工作在64位模式下,因此回到圖3中,我們可以根據intel 82599的datasheet來得知intel的64bit網絡卡的bar分佈是長什麼樣子的,如圖7.

圖7.intel 82599網絡卡的BAR分佈

  所以PCI配置空間的規範結合intel的I350和82599這兩款網絡卡的datasheet進行分析,我們可以得出這樣的一個結論:”PCI有6個BAR是規範,6個BAR的區別和作用取決於具體的PCI外設,需要檢視datasheet才能給出答案“。

  說完6個BAR的作用以及分佈,接下來還有個問題,實際訪問PCI BAR的時候以哪一個BAR為基準呢?這裡主要有疑問的地方會出現在Memory BAR還是I/O BAR。因為需要搞清楚這兩者的區別,才能真正判斷在哪個BAR寫配置。關於IO BAR和Memory BAR的區別首先需要科普一下,在x86體系架構下,記憶體的編址情況。接下來進入科普時間。

  其實這裡是比較晦澀難懂的,首先我們得知道,為什麼會出現I/O空間和外設空間?在討論區別之前我們可以看一張圖,看看I/O空間和Memory空間長什麼樣子,這裡可以看寶華叔經典的《Linux裝置驅動開發詳解》的第11章部分,這裡我就簡單的說一下,x86下的I/O空間和Memory空間到底長啥樣子。見圖8.

圖8.I/O空間與記憶體空間,來自寶華叔的《Linux裝置驅動開發詳解》中第11章

  另外需要注意的時,非x86體系架構下,例如ARM、PowerPC這些架構下,所有的外設和主存(RAM)都會進行統一的編址,所以kernel可以像訪問正常的記憶體空間一樣訪問內設。而x86體系架構下,外設是進行獨立編址的,如圖8所示,因此也就出現了IO空間和Memory空間的區別。(其實可以將RAM看成一種”專門用來記憶體對映的IO裝置“)。另外我們從圖8還可一看到另外一個資訊,那就是訪問外設其實可以有兩種方式,一種是通過I/O空間用專有的指令進行訪問,另外一種便是訪問記憶體空間,而訪問記憶體空間就相對而言容易的多,也隨便的多,那麼為什麼外設會同時擁有兩個空間呢?這裡是由於外設通常會自帶“儲存器”。另外寶華叔還特地提到了如下一句話:

訪問外設可以通過訪問記憶體空間,而訪問外設其實可以不必通過IO空間,也間接說明了IO空間實際上不是訪問裝置所必要的,而記憶體空間才是必要的

  這裡常常還有一個容易懵逼的概念,叫做“I/O埠”和”I/O記憶體“(趁著說DPDK,這裡就把這些基礎的概念依次科普一下),首先訪問I/O空間是必須通過一些專有指令進行訪問的,通過獨特的in、out指令進行訪問,埠號表示了外設的暫存器地址。Intel語法中的in、out指令格式如下:

IN 累加器, {埠號 | DX}
OUT {埠號 | DX}, 累加器

  這兩個指令實際上不需要知道是什麼意思,只需要知道訪問I/O空間需要獨特的in、out指令來訪問暫存器地址,這些暫存器地址就像“開放了埠”一樣供cpu訪問,因此稱為“I/O埠”。而I/O記憶體便是正常訪問記憶體空間的I/O裝置所在的暫存器地址。簡而言之,通過I/O指令通過I/O埠來訪問I/O空間的外設暫存器;通過記憶體對映後通過I/O記憶體訪問記憶體空間的外設暫存器,在這裡所謂的I/O埠或者I/O記憶體可以理解為一種“通道”,主語是“CPU”,謂語是“訪問”,賓語是”外設暫存器“,而I/O埠則是“狀語”。並且實際上,在現在的計算機體系架構下,已經不再推薦通過I/O埠的方式取訪問暫存器了,而是推薦採用IO記憶體的方式。

  經歷了上面的關於PCI BAR、IO空間、記憶體空間、IO埠、IO記憶體的科普,接下來我們迴歸DPDK的驅動託管流程。上面的科普說到了一個關鍵就是“訪問暫存器實際上可以I/O記憶體的方式取訪問記憶體空間的外設暫存器,而不必通過I/O埠的方式訪問位於I/O埠的外設暫存器”。補充了這些關鍵的基本知識後,我們再梳理一下可以得到哪些關鍵性的結論:

  1. PCI有6個BAR,6個BAR的不同劃分跟pci裝置設計有關,intel的網絡卡有Memory Bar、IO Bar還有MSI-X Bar。
  2. 這些Bar,想操作暫存器的話,不必通過I/O Bar,通過Memory Bar即可,也就是intel網絡卡中的Bar0空間。

  知道要訪問哪一塊Bar後,接下來就要想辦法拿到BAR空間供使用者態訪問。

【4.如何拿到BAR?】

  如何拿到BAR,關於這個問題,可以通過閱讀DPDK的原始碼來解決,接下來不會系統性的分析DPDK是如何在啟動階段掃描PCI裝置,這裡會留到以後新開一篇文章闡述,接下來的分析將會從程式碼中的某一點出發進行分析。

  進入DPDK原始碼中的drivers/bus/pci/linux/pci.c中的函式,上程式碼:

#define PCI_MAX_RESOURCE 6
/*
 * pci掃描檔案系統下的resource檔案
 * @param filename 通常為/sys/bus/pci/devices/[pci_addr]/resource檔案
 * @param dev[out] dpdk中對一個pci裝置的抽象
*/
static int
pci_parse_sysfs_resource(const char *filename, struct rte_pci_device *dev)
{
    FILE *f;
    char buf[BUFSIZ];
    int i;
    uint64_t phys_addr, end_addr, flags;

    f = fopen(filename, "r"); //先開啟resource檔案,resource檔案是一個只讀檔案,任何的寫操作都會被忽略掉
    if (f == NULL) {
        RTE_LOG(ERR, EAL, "Cannot open sysfs resource\n");
        return -1;
    }
    //掃描6次,為什麼是6次,在之前已經提到,PCI最多有6個BAR
    for (i = 0; i<PCI_MAX_RESOURCE; i++) {

        if (fgets(buf, sizeof(buf), f) == NULL) {
            RTE_LOG(ERR, EAL,
                "%s(): cannot read resource\n", __func__);
            goto error;
        }
        //掃描resource檔案拿到BAR
        if (pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr,
                &end_addr, &flags) < 0)
            goto error;
        //如果是Memory BAR,則進行記錄
        if (flags & IORESOURCE_MEM) {
            dev->mem_resource[i].phys_addr = phys_addr;
            dev->mem_resource[i].len = end_addr - phys_addr + 1;
            /* not mapped for now */
            dev->mem_resource[i].addr = NULL;
        }
    }
    fclose(f);
    return 0;

error:
    fclose(f);
    return -1;
}

/*
 * 掃描pci resource檔案中的某一行
 * @param line 某一行
 * @param len 長度,為第一個引數字串的長度
 * @param phys_addr[out] PCI BAR的起始地址,這個地址要mmap才能用
 * @param end_addr[out] PCI BAR的結束地址
 * @param flags[out] PCI BAR的標誌
*/
int
pci_parse_one_sysfs_resource(char *line, size_t len, uint64_t *phys_addr,
    uint64_t *end_addr, uint64_t *flags)
{
    union pci_resource_info {
        struct {
            char *phys_addr;
            char *end_addr;
            char *flags;
        };
        char *ptrs[PCI_RESOURCE_FMT_NVAL];
    } res_info;
    //字串處理
    if (rte_strsplit(line, len, res_info.ptrs, 3, ' ') != 3) {
        RTE_LOG(ERR, EAL,
            "%s(): bad resource format\n", __func__);
        return -1;
    }
    errno = 0;
    //字串處理,拿到PCI BAR起始地址、PCI BAR結束地址、PCI BAR標誌
    *phys_addr = strtoull(res_info.phys_addr, NULL, 16);
    *end_addr = strtoull(res_info.end_addr, NULL, 16);
    *flags = strtoull(res_info.flags, NULL, 16);
    if (errno != 0) {
        RTE_LOG(ERR, EAL,
            "%s(): bad resource format\n", __func__);
        return -1;
    }

    return 0;
}

程式碼1.

  可以看到這段程式碼的邏輯非常簡單,就是掃描某個pci裝置的resource檔案獲得PCI BAR。也就是/sys/bus/pci/[pci_addr]/resource這個檔案,接下來讓我們看一下這個檔案長什麼樣子,見圖9.

圖9.pci目錄下的resource檔案

  可以看到resource檔案內部的特點,前6行為PCI裝置的6個BAR,每行共3列,其中第1列為PCI BAR的起始地址,第2列為PCI BAR的終止地址,第3列為PCI BAR的標識。圖中的例子是ixgbe驅動的intel 82599網絡卡,之前在第3節也說過,對於82599這張卡工作在64bit模式,前兩個BAR為Memory BAR,中間兩個BAR為IO BAR,最後兩個BAR為MSI-X BAR,因此實際上只有第一行是對我們有用的。通過讀取resource檔案便完成了BAR的獲取。另外PCI目錄下還有很多其他關於PCI裝置的資訊,見圖10.

圖10.PCI裝置目錄內容

 

   這張圖中的目錄結構和圖2是不是有些眼熟呢?沒錯這些檔案起始就是系統在啟動時根據PCI裝置資訊自動進行處理並建立的。

  • config: PCI配置空間,二進位制,可讀寫;
  • device: PCI裝置ID,只讀。很重要;
  • driver: 為PCI裝置採用的驅動目錄的軟連線,真正的目錄位於/sys/bus/pci/drivers/目錄下,可以看圖10中顯示這個PCI裝置採用的是核心ixgbe驅動;
  • enable: 裝置是否正常使能,可讀寫;
  • irq: 被分到的中斷號,只讀;
  • local_cpulist: 這個網絡卡的記憶體空間位於和同處於一個NUMA節點上的cpu有哪些,列表方式呈現,只讀。舉個例子,比如網絡卡的記憶體空間位於numa node 0,cpu 1-6同樣位於numa node0,那麼讀取這個檔案的內容便是:1-6。重要,因為跨numa節點訪問記憶體會帶來極大的效能開銷。
  • local_cpu: 與local_cpulist的作用相同,不過是以掩碼的方式給出,例如1-6號cpu和pci裝置處於同一個numa節點,那麼掩碼便是0x7E(0111 1110)。重要,重要程度等價於local_cpulist。
  • numa_node: 只讀,告訴這個PCI裝置屬於哪一個numa節點。重要,會影響效能。
  • resource: BAR空間記錄檔案,只讀,任何寫操作將會被忽略,通常有三列組成,第一列為PCI BAR起始地址,第二列為PCI BAR終止地址,第三列為這個PCI BAR的標識,見圖9.
  • resource0..N: 某一個PCI BAR空間,二進位制,只讀,可以對映,如果使用者態程式向操作PCI裝置必須通過mmap這個resource0..N,也就意味著這個檔案是可以mmap的。重要。
  • sriov_numfs: 只讀,虛擬化常用的技術,sriov透傳技術,可以理解在這個網絡卡上可以虛擬出多個虛擬網絡卡,這些虛擬網絡卡可以直接透傳到qemu中的客戶機,並且網絡卡內部會有一個小的交換機實現VM客戶機資料包的收發,可以極大的減少時延,這個numvfs便是告訴這個pci裝置目前虛擬出多少個虛擬網絡卡(vf)。重要,主要應用在虛擬化場合。
  • sriov_totalvfs: 只讀,作用與sriov_numfs相同,不過是總數,揭示這個PCI裝置一共可以申請多少個vf。
  • subsystem_device: PCI子系統裝置ID,只讀。
  • subsystem_vendor: PCI子系統生產商ID,只讀。
  • vendor:PCI生產商ID,比如intel便是0x8086.重要。

  上面便是關於PCI裝置目錄下的一些檔案的解釋。

  但是DPDK真的是通過讀取resource檔案來拿到BAR的麼?答案其實是否定的...DPDK獲取PCI BAR並不是這麼獲取的。接下來上程式碼,程式碼位於drivers/bus/pci/linux/pci_uio.c檔案中:

/*
 * 對映resource資源獲取PCI BAR
 * @param DPDK中關於某一個PCI裝置的抽象例項
 * @param res_id下標,說白了就是獲取第幾個BAR
 * @param uio_res用來存放PCI BAR資源的結構
 * @param map_idx uio_res陣列的計數器
*/

int
pci_uio_map_resource_by_index(struct rte_pci_device *dev, int res_idx,
        struct mapped_pci_resource *uio_res, int map_idx)
{
    ..... //省略
    //開啟/dev/bus/pci/devices/[pci_addr]/resource0..N檔案
    if (!wc_activate || fd < 0) {
        snprintf(devname, sizeof(devname),
            "%s/" PCI_PRI_FMT "/resource%d",
            rte_pci_get_sysfs_path(),
            loc->domain, loc->bus, loc->devid,
            loc->function, res_idx);

        /* then try to map resource file */
        fd = open(devname, O_RDWR);
        if (fd < 0) {
            RTE_LOG(ERR, EAL, "Cannot open %s: %s\n",
                devname, strerror(errno));
            goto error;
        }
    }

    /* try mapping somewhere close to the end of hugepages */
    if (pci_map_addr == NULL)
        pci_map_addr = pci_find_max_end_va();
    //進行mmap對映,拿到PCI BAR在程序虛擬空間下的地址
    mapaddr = pci_map_resource(pci_map_addr, fd, 0,
            (size_t)dev->mem_resource[res_idx].len, 0);
    close(fd);
    if (mapaddr == MAP_FAILED)
        goto error;

    pci_map_addr = RTE_PTR_ADD(mapaddr,
            (size_t)dev->mem_resource[res_idx].len);
        //將拿到的PCI BAR對映至程序虛擬空間內的地址存起來
    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;

    return 0;

error:
    rte_free(maps[map_idx].path);
    return -1;
}


/*
 * 對pci/resource0..N進行mmap,將PCI BAR空間通過mmap的方式對映到程序內部的虛擬空間,供使用者態應用來操作裝置
*/
void *
pci_map_resource(void *requested_addr, int fd, off_t offset, size_t size,
         int additional_flags)
{
    void *mapaddr;

    //核心便是這句mmap,其中要注意的是,offset必須為0
    mapaddr = mmap(requested_addr, size, PROT_READ | PROT_WRITE,
            MAP_SHARED | additional_flags, fd, offset);
    if (mapaddr == MAP_FAILED) {
        RTE_LOG(ERR, EAL,
            "%s(): cannot mmap(%d, %p, 0x%zx, 0x%llx): %s (%p)\n",
            __func__, fd, requested_addr, size,
            (unsigned long long)offset,
            strerror(errno), mapaddr);
    } else
        RTE_LOG(DEBUG, EAL, "  PCI memory mapped at %p\n", mapaddr);

    return mapaddr;
}

程式碼2

  關於記憶體對映resource0..N的方法來讓使用者空間得到PCI BAR空間的操作其實在Linux kernel doc中早有說明:https://www.kernel.org/doc/Documentation/filesystems/sysfs-pci.txt,具體可以看圖11.

圖11.Linux Kernel Doc中關於PCI裝置resource0..N的說明

  可以看到,DPDK是怎麼拿到PCI BAR的呢?是igb_uio將pci bar暴露給使用者態的麼?其實完全不是,而是直接mmap resource0..N就做到了,至於resource0..N則是核心自帶的一個供使用者態程式通過mmap的方式訪問PCI BAR。網上很多的文章提到igb_uio的作用,基本都是以下兩點:

  • igb_uio負責將PCI BAR提供給使用者態應用,也就是DPDK;
  • igb_uio負責處理中斷,形成使用者態程式和核心中斷的一個橋樑。

  這兩點中,第二點是正確的,但是第一點則是非常不準確的,第一點很容易誤導人,讓人產生“DPDK之所以能bypass核心空間獲得PCI BAR靠的就是igb_uio”,事實不然,DPDK訪問PCI BAR完全繞過了igb_uio,igb_uio的確提供了方法可以讓使用者態空間應用來訪問PCI BAR,不過DPDK沒有用。關於這個地方,intel 包處理專家、《DPDK深入淺出》一書的作者樑存銘樑大師給出的解釋是:

UIO提供了(PCI BAR)訪問方式,但是DPDK直接mmap了resource,Kernel對resource實現的mmap跟在igb_uio中實現一個mmap是一樣的實現,沒有區別,用kernel自己的方式不是更好麼?

  所以我們可以確定的是:

  1. igb_uio負責建立uio裝置並載入igb_uio驅動,負責將核心驅動接管的網絡卡搶過來,以此來先遮蔽掉核心驅動以及核心協議棧;
  2. igb_uio負責一個橋樑的作用,銜接中斷訊號以及使用者態應用,因為中斷只能在核心態處理,所以igb_uio相當於提供了一個介面,銜接使用者態與核心態的驅動,關於驅動,後續會開文章專門講解DPDK的中斷;

  事實上,igb_uio做的就是上面兩點,接下來會從程式碼以及函式的角度分析igb_uio.ko的實現以及uio如何將PCI BAR暴露給使用者態(雖然DPDK沒有使用這種方式,但是如何將PCI BAR暴露給使用者態,是UIO驅動的一大特色)

【5.igb_uio以及uio的部分程式碼分析】

  想讀懂一個核心模組的作用,首先得確定其工作流程。

  igb_uio.ko初始化流程如圖12所示:

圖12.igb_uio.ko的初始化流程

  igb_uio.ko初始化主要是做了兩件事:

  1. 第一件事是配置中斷模式;
  2. 第二種模式便是註冊驅動,見圖13.;

 

圖13.igbuio_pci_init_module函式註冊igb_uio驅動

  註冊驅動後,剩餘的進入核心處理核心模組的流程,也就是核心遍歷註冊的driver,呼叫driver的probe方法,在igb_uio.c中,也就是igbuio_pci_probe函式,見圖14.。

圖14.核心處理註冊的驅動以及呼叫probe的流程

  接下來便進入igbuio_pci_probe函式,處理主要的註冊uio驅動的邏輯,函式呼叫圖如圖14所示。

 

圖15.igbuio_pci_probe函式的內部呼叫流程

  • pci_enable_device : 使能PCI裝置
  • igbuio_pci_bars : 對PCI BAR進行ioremap的對映,拿到所有的PCI BAR。
  • uio_register_device : 註冊uio裝置
  • pci_set_drvdata : 設定私有變數

  其中在igbuio_pci_bars函式中,會遍歷6個PCI BAR,獲得其PCI BAR的起始地址,並對這些起始地址進行ioremap,見程式碼3。這裡需要注意的是,核心空間若想通過IO記憶體的方式訪問外設在記憶體空間的暫存器,必須利用ioremap對PCI BAR的起始地址進行對映後才能訪問。

static int
igbuio_setup_bars(struct pci_dev *dev, struct uio_info *info)
{
    int i, iom, iop, ret;
    unsigned long flags;
    static const char *bar_names[PCI_STD_RESOURCE_END + 1]  = {
        "BAR0",
        "BAR1",
        "BAR2",
        "BAR3",
        "BAR4",
        "BAR5",
    };

    iom = 0;
    iop = 0;
        //遍歷PCI裝置的6個BAR
    for (i = 0; i < ARRAY_SIZE(bar_names); i++) {
                //PCI BAR空間不等於0且起始地址不等於0,認為為有效BAR
        if (pci_resource_len(dev, i) != 0 &&
                pci_resource_start(dev, i) != 0) {
                        //拿到BAR的標識,如果為0x00000200則為記憶體空間
            flags = pci_resource_flags(dev, i);
            if (flags & IORESOURCE_MEM) {
                                //對記憶體空間的PCI BAR進行對映
                ret = igbuio_pci_setup_iomem(dev, info, iom,
                                 i, bar_names[i]);
                if (ret != 0)
                    return ret;
                iom++;
                        //IO空間不再討論範圍內
            } else if (flags & IORESOURCE_IO) {
                ret = igbuio_pci_setup_ioport(dev, info, iop,
                                  i, bar_names[i]);
                if (ret != 0)
                    return ret;
                iop++;
            }
        }
    }

    return (iom != 0 || iop != 0) ? ret : -ENOENT;
}

//對記憶體BAR進行對映,以及填充資料結構
static int
igbuio_pci_setup_iomem(struct pci_dev *dev, struct uio_info *info,
               int n, int pci_bar, const char *name)
{
    unsigned long addr, len;
    void *internal_addr;

    if (n >= ARRAY_SIZE(info->mem))
        return -EINVAL;
        //拿到PCI BAR的起始地址
    addr = pci_resource_start(dev, pci_bar);
        //拿到PCI BAR的長度
    len = pci_resource_len(dev, pci_bar);
    if (addr == 0 || len == 0)
        return -1;
        //wc_activate為igb_uio.ko的引數,預設為0,會進入if條件
    if (wc_activate == 0) {
                //對PCI BAR進行ioremap,對映到核心空間,得到可以在核心空間對映後的PCI BAR地址,雖然沒什麼用,因為igb_uio完全不需要操作PCI裝置,因此獲得此地址意義不大
        internal_addr = ioremap(addr, len);
        if (internal_addr == NULL)
            return -1;
    } else {
        internal_addr = NULL;
    }
        //填充資料結構
    info->mem[n].name = name; //PCI  BAR名,例如BAR0、BAR1
    info->mem[n].addr = addr; //PCI BAR起始地址,實體地址
    info->mem[n].internal_addr = internal_addr; //經過ioremap對映後的PCI BAR,可以供核心空間訪問
    info->mem[n].size = len; //PCI BAR長度
    info->mem[n].memtype = UIO_MEM_PHYS; //PCI BAR型別,為記憶體BAR
    return 0;
}

程式碼3

  可以看到igbuio_set_bars做的工作也非常簡單,就是填充資料結構加上對PCI BAR的IO記憶體(實體地址)進行ioremap,但是在這裡ioremap其實沒什麼用,進行ioremap對映後會得到一個可以供核心空間訪問的PCI BAR地址(虛擬地址),不過從設計角度上講,igb_uio不需要對PCI裝置得到BAR空間,並對PCI裝置進行配置,因此意義不大。接下來便是呼叫uio_register_devcie註冊uio裝置。

 

 圖16.uio_register_device呼叫流程

  uio_register_device的流程主要是做了4件事:

  • dev_set_name : 給裝置設定名稱,uio0...N,為/dev/uio0..N
  • device_register : 註冊裝置
  • uio_dev_add_attribute : 主要是建立一些裝置屬性,這裡說屬性也有點不太恰當,從表現形式來看是在/sys/class/uio/uio0/目錄中建立maps目錄,裡面包含的主要也是和resource檔案一致,就是pci裝置經過uio驅動接受以後再把resource資源通過檔案系統暴露給使用者態而已,可以看圖17.

 

圖17.uio_dev_add_attribute的作用

  到這裡位置,igb_uio的初始化以及註冊過程都已經完成了,最終表現形式便是在/dev/uio建立了一個uio裝置,這個裝置是用來銜接核心態的中斷訊號與使用者態應用的,關於uio申請中斷這裡的細節以後會專門開一篇文章介紹DPDK的中斷,這裡先不予介紹。介紹到這裡,貼一張資料結構關係圖供大家理解,見圖18.

 

 

圖18.資料結構關係

  • struct resource : 核心將PCI BAR的資訊儲存在這個資料結果中,可以理解為PCI BAR的抽象,可以理解這個resource結構體就對應了/sys/bus/pci/devices/[pci_addr]/resource檔案
    1. start : PCI BAR空間起始地址(這裡不一定是記憶體空間還是IO空間);
    2. end : PCI BAR空間的結束地址;
    3. name : PCI BAR的名字,例如BAR 0、BAR1、BAR2....BAR5;
    4. flags : PCI BAR的標識,如果flags & 0x00000200則為記憶體空間,如果flags & 0x00000100則為IO空間;
    5. desc : IO資源描述符
  • struct pci_dev : pci裝置的抽象,可以理解為一個struct pci_dev就代表一個pci裝置
    1. vendor : 生產商id,intel為0x0806,見/sys/bus/pci/devices/[pci_addr]/vendor檔案;
    2. device : 裝置id;
    3. subsystem_vendor : 子系統生產商id;
    4. subsystem_device : 子系統裝置id;
    5. driver : 當前PCI裝置所用驅動;
    6. resource : 當前pci裝置的pci bar資源;
  • struct rte_uio_pci_dev : igb_uio的抽象,可以理解為igb_uio本身
    1. info : 用於關聯uio資訊;
    2. pdev : 用於關聯pci裝置;
    3. mode : 中斷模式配置
  • struct uio_info : uio 資訊配置的抽象
    1. uio_dev : 用來指向所屬於的uio裝置例項;
    2. name : 這個uio裝置的名字,例如/dev/uio0,/dev/uio1,/dev/uio2;
    3. mem : 同樣是PCI BAR資源,不過這裡是已經做了區分,特指Memory BAR,這裡的值仍然來自於核心的resource結構體,不過這裡往往是將核心resource結構體對映後的值,可以理解為原始資料“加工”後的值;
    4. port : 同樣是PCI BAR資源,不過這裡是已經做了區分,特質Port BAR,這裡的值仍然來自於核心的resource結構體,不過這裡往往是將核心resource結構體對映後的值,可以理解為原始資料“加工”後的值;
    5. irq : 中斷號;
    6. irq_flags : 中斷標識;
    7. priv : 一個回撥指標,指向dpdk的igb_uio驅動例項,其實這個欄位的設計並不是為了專門服務於dpdk的igb_uio;
    8. handler、mmap、open、release、irqcontrol:分別為幾個函式鉤子,例如對/dev/uio進行open操作後,最終就會通過uio的file_operations -> open呼叫到igbuio_pci_open中,可以理解為open操作的內部實現;
  • struct uio_device : uio裝置的抽象,其例項可以代表一個uio裝置
    1. 這裡的內容不多加介紹,因為關於一個uio裝置的主要配置和資訊都在uio_info結構中
  • struct uio_mem : 經過對resource進行處理後的Memory BAR資訊,這裡的資訊主要是指的對PCI BAR進行ioremap
    1. name : PCI Memory BAR的名字,例如BAR 0、BAR1、BAR2....BAR5;
    2. addr : PCI Memory BAR的起始地址,為實體地址,這個地址必須經過ioremap對映後才可以給核心空間使用;
    3. offs : 偏移,一般為0;
    4. size : PCI Memory BAR的大小,通常可以用resource檔案中的第二列(PCI BAR的終止地址)和resource檔案中的第一列(PCI BAR的起始地址) + 1計算得出;
    5. memtype : 這個Memory Bar的記憶體型別,可以選擇為實體地址、邏輯地址、虛擬地址三種類型,在DPDK的igb_uio中賦值為實體地址;
    6. internal_addr : 這個是一個關鍵,這個值即為PCI Memory BAR起始地址經過ioremap對映後得到的可以在核心空間直接訪問的虛擬地址,當然之前也描述過,這個地址對於uio這種設計理念的裝置而言是不需要的;

 以上便是關於igb_uio、uio程式碼中主要的資料結構關係以及資料結構之間的欄位介紹,那麼重新思考那個問題:

假設不侷限於DPDK的igb_uio,也不考慮核心開放出來的resource0..N,uio該怎麼向用戶空間暴露PCI BAR提供給使用者空間使用呢?

經過上述的流程分析和資料結構的分析,我們起碼可以知道一個事實,那就是uio內部其實是拿得到PCI BAR資源的,那麼該怎麼將這個BAR資源給使用者態應用使用呢?答案其實也很簡單,就是對/dev/uio0..N這個裝置呼叫mmap進行記憶體對映,呼叫mmap之後,將會轉到核心態事先註冊好的file_operations.mmap鉤子函式上,也就是呼叫uio_mmap,呼叫流程如圖19所示:

圖19.mmap /dev/uio0..N的核心態函式呼叫流程

  當然之前也說過,igb_uio其實完全沒有做mmap這塊的工作,因此uio_info->mmap這個鉤子函式其實是NULL,所以DPDK完全不靠igb_uio得到PCI BAR,而是直接呼叫核心已經對映過的resource0..N即可。

  現在回到第二章的那三個Question上,現在經過3、4、5這三章的講解,已經完全可以回答第一個Questions

Q:igb_uio/vfio-pci的作用是什麼?為什麼要用這兩個驅動?這裡的“驅動”和dpdk內部對網絡卡的“驅動”(dpdk/driver/)有什麼區別呢?
A:igb_uio主要作用是實現了兩個功能,第一個功能是將PCI裝置進行take-over,以此來遮蔽掉核心驅動和核心協議棧;第二個功能是實現了一個橋樑的作用,銜接核心態的中斷與使用者態(當然中斷的內容會在後續開始講解)。

【6.如何將PCI裝置的驅動重新繫結】

  這個操作其實只需要兩個步驟:

  1. 將當前PCI裝置的現有驅動目錄下的unbind寫入PCI裝置的PCI地址,例如:
    • echo "0000:81:00.0" > /sys/bus/pci/drivers/ixgbe/unbind
  2. 拿到當前PCI裝置的device id和vendor id,並將其寫入新的驅動的new_id中,例如我手頭上的intel 82599網絡卡的device id是10fb,intel的vendor id是8086,那麼繫結例子如下:
    • echo "8086 10fb" > /sys/bus/pci/drivers/igb_uio/new_id

  那這麼做背後的原理是什麼呢?其實也很簡單,在核心原始碼目錄/include/linux/devices.h中有這麼一組巨集:

#define DRIVER_ATTR_RW(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_RW(_name)
#define DRIVER_ATTR_RO(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_RO(_name)
#define DRIVER_ATTR_WO(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_WO(_name)

程式碼5.對於attribute的三種宣告

  利用這三種巨集宣告的attribute,最終在檔案系統中就是這個驅動中的attribute檔案的狀態,Linux中萬物皆檔案,這些attribute實際上就是/sys/bus/pci/drivers/[driver_name]/目錄下的檔案。例如以上述兩個步驟中使用的unbind和new_id為例,程式碼位於/driver/base/bus.c中

/*
 * PCI裝置驅動的unbind屬性實現
*/
static ssize_t unbind_store(struct device_driver *drv, const char *buf,
                size_t count)
{
    struct bus_type *bus = bus_get(drv->bus);
    struct device *dev;
    int err = -ENODEV;
        //先根據寫入的引數找到裝置,根據例子命令,便是根據"0000:08:00.0"這個pci地址找到對應的pci裝置例項
    dev = bus_find_device_by_name(bus, NULL, buf);
    if (dev && dev->driver == drv) {
        if (dev->parent && dev->bus->need_parent_lock)
            device_lock(dev->parent);
                //pci裝置釋放驅動,其中呼叫的就是driver或者bus的remove鉤子函式,然後再將device中的driver指標置空
        device_release_driver(dev);
        if (dev->parent && dev->bus->need_parent_lock)
            device_unlock(dev->parent);
        err = count;
    }
    put_device(dev);
    bus_put(bus);
    return err;
}
static DRIVER_ATTR_WO(unbind); //進行attribute生命,宣告為只寫

程式碼6.unbind attribute的實現

  可以看到對unbind檔案進行寫操作後,最終會轉到核心態的pci裝置的unbind_store函式,這個函式的內容也非常簡單,首先根據輸入的PCI 地址找到對應的PCI裝置例項,然後呼叫device_release_driver函式釋放device相關聯的driver,而new_id的屬性實現則是在/drivers/pci/pci-driver.c中,函式呼叫流程即為圖14中的下半部分,最終會調到驅動的probe鉤子上,在igb_uio驅動中即為igbuio_pci_probe函式。

  以上,便是dpdk-devbinds實現驅動的解綁以及重綁的實現,有興趣的可以自己寫個pyhon或者shell指令碼試一下。

圖20,層級結構

圖20是個人理解:

  1. 核心接管硬體並將PCI BAR通過sysfs暴露給使用者態,供使用者態對其mmap後直接訪問Memory BAR空間;
  2. 應用層程式通過sysfs介面實現pci裝置的驅動的unbind/bind;
  3. UIO為一框架,無法獨立生存,需要在框架的基礎上開發出igb_uio,igb_uio實現了uio裝置的生命週期管理全權交給使用者態應用掌管;
  4. 其中中斷訊號仍然只能在核心態處理,不過uio通過建立/dev/uio來實現了一個"橋樑"來銜接使用者態和核心態的中斷處理,這時已經可以將使用者態應用視為一種"中斷下半部";
  5. Application為最終的業務層,只需要呼叫PMD的對上介面即可;

【7.後話】

1.3-6章的講解,基本解決了第二章的前兩個Questions,最後一個Questions以及DPDK如何實現的中斷,以及vfio的解析會在後續文章中逐一發出。

2.這篇文章花費了較多的精力完成,並且內容較多,涉及到的知識也多為底層知識,因此其中難免會存在錯別字、語法不通順、以及筆誤的情況,當然理解錯誤的地方也可能存在,還望各位朋友能夠點明其中不合理的分析以及疏漏。

3.寫完這篇文章後,不禁再次感慨,畢業如今一年半,遇到令我震撼的專案一共有兩個,第一個是DPDK,第二個便是VPP,經過分析原理才發現,設計者是真的牛逼,根本不是我等菜雞所能企及的存在...