【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,經過分析原理才發現,設計者是真的牛逼,根本不是我等菜雞所能企及的存在...

 


免責聲明!

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



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