【DPDK】談談DPDK如何實現bypass內核的原理 其一 PCI設備與UIO驅動

【前言】node

  隨着網絡的高速發展,對網絡的性能要求也愈來愈高,DPDK框架是目前的一種加速網絡IO的解決方案之一,也是最爲流行的一套方案。DPDK經過bypass內核協議棧與內核驅動,將驅動的工做從內核態移至用戶態,並利用polling mode的線程工做模式加速網絡I/O使得網絡IO性能出現大幅度的增加。linux

  在使用DPDK的時候,咱們經常會說提到用DPDK來接管網卡以達到bypass內核驅動以及內核協議棧的操做,本篇文章將主要分析DPDK是如何實現的bypass內核驅動來實現所謂的「接管網卡」的功能。shell

注意:數組

  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設備驅動發生的變化架構

 

  那麼通過上述三個操做,至少腦子裏會產生這麼幾個問題:app

  Q:igb_uio/vfio-pci的做用是什麼?爲何要用這兩個驅動?這裏的「驅動」和dpdk內部對網卡的「驅動」(dpdk/driver/)有什麼區別呢?框架

  Q:dpdk-devbinds是如何作到的將內核驅動解綁後綁定新的驅動呢?dom

  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、BAR一、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、BAR一、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上,如今通過三、四、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,通過分析原理才發現,設計者是真的牛逼,根本不是我等菜雞所能企及的存在...

相關文章
相關標籤/搜索