DPDK — IGB_UIO,與 UIO Framework 進行交互的內核模塊

目錄

前文列表

DPDK — 安裝部署
DPDK — 數據平面開發技術
DPDK — 架構解析

IGB_UIO

PMD 是 DPDK 在用戶態實現的網卡驅動程序,但實際上還是會依賴於內核提供的支持。其中 UIO 內核模塊,是內核提供的用戶態驅動框架,而 IGB_UIO(igb_uio.ko)是 DPDK 用於與 UIO 交互的內核模塊,通過 IGB_UIO 來 bind 指定的 PCI 網卡設備給到用戶態的 PMD 使用。IGB_UIO 藉助 UIO 技術來截獲中斷,並重設中斷回調行爲,從而繞過內核協議棧後續的處理流程。並且 IGB_UIO 會在內核初始化的過程中將網卡硬件寄存器映射到用戶態。

IGB_UIO 內核模塊主要功能之一就是用於註冊一個 PCI 設備。通過 DPDK 提供個 Python 腳本 dpdk-devbind 來完成,當執行 dpdk-devbind 來 bind 網卡時,會通過 sysfs 與內核交互,讓內核使用指定的驅動程序(e.g. igb_uio)來綁定網卡。

IGB_UIO 內核模塊的另一個主要功能就是讓用於態的 PMD 網卡驅動程序得以與 UIO 進行交互

  1. 調用 igbuio_setup_bars,設置 uio_info 的 uio_mem 和 uio_port。
  2. 設置 uio_info 的其他成員。
  3. 調用 uio_register_device,註冊 UIO 設備。
  4. 打開 UIO 設備並註冊中斷。
  5. 調用 uio_event_notify,將註冊的 UIO 設備的 “內存空間” 映射到用戶態的應用空間。其 mmap 的函數爲 uio_mmap。至此,UIO 就可以讓 PMD 驅動程序在用戶態應用層訪問設備的大部分資源了。
  6. 應用層 UIO 初始化。同時,DPDK 還需要把 PCI 設備的 BAR 映射到應用層。在 pci_uio_map_resource 函數中會調用 pci_uio_map_resource_by_index 做資源映射。
  7. 在 PMD 驅動程序中,DPDK 應用程序,會調用 rte_eth_rx_burst 讀取數據報文。如果網卡接收 Buffer 的描述符表示已經完成一個報文的接收(e.g. 有 E1000_RXD_STAT_DD 標誌),則 rte_mbuf_raw_alloc 一個 mbuf 進行處理。
  8. 對應 RTC 模型的 DPDK 應用程序來說,就是不斷的調用 rte_eth_rx_burst 去詢問網卡是否有新的報文。如果有,就取走所有的報文或達到參數 nb_pkts 的上限。然後進行報文處理,處理完畢,再次循環。

IGB_UIO 是如何註冊 PCI 設備的?

Linux 中的 PCI 設備

在這裏插入圖片描述

  • config:PCI 設備的配置空間,二進制,可讀寫。
  • device:PCI Device ID,只讀。PCI 設備的標識,很重要。
  • vendor:PCI vendor ID,比如:Intel 爲 0x8086。
  • driver:爲 PCI 設備採用的驅動目錄的軟連接,真正的目錄位於 /sys/bus/pci/drivers/ 下。上圖可以參數當前這個 PCI 設備使用的是 igb_uio 驅動。
  • enable:設備是否正常使能,可讀寫;
  • irq:被分到的中斷號,只讀;
  • local_cpulist:這個網卡的內存空間位於和同處於一個 NUMA 節點上的 CPUs 有哪些,只讀。協助 NUMA 親和性的實現。
  • local_cpu:和 local_cpulist 的作用一樣,不過是以掩碼的方式給出。
  • numa_node:說明 PCI 設備屬於哪一個 NUMA node,只讀。
  • resource:PCI 設備的 BAR 記錄,只讀。
  • resource0…N:某一個 PCI BAR 空間,二進制,只讀,可進行 mmap() 映射。如果用戶進程希望操作 PCI 設備就必須通過 mmap() 這個resource0…N。
  • sriov_numfs:用於設定 SR-IOV 網卡的 VF 數量。
  • sriov_totalvfs:作用與 sriov_numfs 相同,設定這個 PCI 設備一共可以申請多少個 VF。
  • subsystem_device:PCI 子系統設備 ID,只讀。
  • subsystem_vendor:PCI 子系統生產商 ID,只讀。

PCI 的 BAR(基地址)

程序要操作一個外設,首先需要找到它的寄存器並對其進行配置,而找到寄存器的前提是拿到外設的基地址,即:通過 “基地址+寄存器偏移” 就能找到寄存器所在的地址,然後就可以配置了。

下圖爲一個 PCI 設備的配置空間,其中,基地址(Bare Address Registers,BAR)是最重要的部分,在 0x0010 ~ 0x0028 這 24Byte 中,分佈着 6 個 PCI BAR。PCI 設備配置空間的信息,在系統啓動時,就已經被解析完成了,並以文件系統的方式供用戶態程序讀取。
在這裏插入圖片描述
首先思考一個問題:爲什麼 PCI 設備需要 6 個 BAR 呢?

在這裏插入圖片描述
上圖中藍色部分進行了說明,PCF 設備的 6 個 BAR 允許設備爲不同的目的提供不同的區域(Region)。結合 Intel 82599 型號網卡的 datasheet 可以得知:

在這裏插入圖片描述

其擁有的 6 個 BAR 被分成了三塊區域:

  1. Memory BAR:內存 BAR,標誌着這塊 BAR 空間位於的內存空間。這段內存空間在通過 mmap() 映射後,用戶進程即可以直接訪問。
  2. I/O BAR:IO BAR 空間,標誌着這塊 BAR 空間位於的 IO 空間,用戶進程對其的訪問不能像 Memory BAR 那樣 mmap() 映射之後即可直接訪問。而是必須要通過專門的操作來進行讀寫。
  3. MSI-X BAR:這個 BAR 空間主要是用來配置 MSI-X 中斷向量。

那麼,爲什麼 6 個 BAR 只有 3 個 Region 呢?

這是因爲每個 BAR 爲 32bit,而 Intel 82599 的一個 Region 需要 64bit。

在這裏插入圖片描述

相對的,再看一款低端網卡 I350 的 datasheet:

在這裏插入圖片描述

對於 I350 這種低端的千兆網卡,可以將其配置爲工作在 32bit 或 64bit 模式下,而 Intel 82599 這種萬兆的卡,就只能工作在 64bit 模式下了。

再一個問題:程序訪問 PCI 的時候是以 Memory BAR 還是 I/O BAR 爲基準的呢?

首先我們要知道爲什麼會有 Memory 空間和 I/O 空間的區別:

在這裏插入圖片描述

在 x86 體系架構下,外設是進行獨立編址的,如上圖所示,因此就出現了 Memory 空間 和 IO 空間的區別。另外,也可以看出訪問外設其實也具有兩種方式:

  1. 一種是通過 I/O 空間用專有的指令進行訪問。例如:in、out 指令,而端口號表示了外設的寄存器地址。Intel x86 語法中的 in、out 指令格式如下:
IN 累加器, {端口號 | DX}
OUT {端口號 | DX}, 累加器
  1. 另外一種便是訪問內存空間,而訪問內存空間就相對而言要容易的多。

那麼爲什麼外設需要擁有兩個不同的空間呢?這裏是由於外設通常會自帶一個 “存儲器”,即外設寄存器,對應了上述的 I/O 空間。這樣通過內存映射之後 CPU 就可以通過 I/O 指令來操作外設的寄存器了。

綜上,我們就知道了:想要操作一個 PCI 設備,那麼就需要獲取到 PCI 設備的 Memory BAR 或 IO BAR。其中 Memory BAR 是必須的,而 IO BAR 是可選的。

IGB_UIO 如何獲得 PCI 的 Memory BAR?

這個問題的答案其實在上文中已有提到:當執行 dpdk-devbind 來 bind 網卡時,會通過 sysfs 與內核交互,讓內核使用指定的驅動程序(e.g. igb_uio)來綁定網卡。

打開 dpdk-18.08/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 文件,只讀
    if (f == NULL) {
        RTE_LOG(ERR, EAL, "Cannot open sysfs resource\n");
        return -1;
    }
    // 掃描 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;
}

這段代碼的邏輯很簡單,就是掃描某個 PCI 設備的 resource 文件並獲得 Memory BAR。e.g.

$ cat /sys/bus/pci/devices/0000:00:08.0/resource
0x0000000000001000 0x000000000000103f 0x0000000000040101
0x00000000c0040000 0x00000000c0040fff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000440000000 0x0000000440003fff 0x000000000014220c
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000c0000000 0x00000000c003ffff 0x000000000004e200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

前 6 行爲 PCI 設備的 6 個 BAR,還是以 Intel 82599 爲例,前兩個 BAR 爲 Memory BAR,中間兩個 BAR 爲 IO BAR,最後兩個 BAR 爲 MSI-X BAR。其中,每個 BAR 又分爲 3 列:

  1. 第 1 列爲 PCI BAR 的起始地址
  2. 第 2 列爲 PCI BAR 的終止地址
  3. 第 3 列爲 PCI BAR 的標識

再看一段代碼 dpdk-18.08/drivers/bus/pci/linux/pci_uio.c:

/*
 * 用於映射 resource 資源,並獲取 PCI BAR
 * @param dev: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;
}

其實,關於用戶進程通過內存映射 resource0…N 的方法來得到 PCI BAR 空間的操作在 Linux kernel doc(https://www.kernel.org/doc/Documentation/filesystems/sysfs-pci.txt)中早有說明:

在這裏插入圖片描述

綜上所述,我們知道雖然 UIO 框架提供了 PCI BAR 訪問方式,但 DPDK 實際上沒有使用,而是直接 Kernel 提供的 mmap() 方式來訪問 PCI 設備的 resource,讓用戶進程(e.g. PMD Driver)得以訪問 PCI 設備。

使用 dpdk-devbind 綁定網卡驅動的流程

在 Linux 中,將設備(Device)與驅動(Driver)進行綁定的方法有兩種:

  1. 配置設備,讓其選擇驅動:向 /sys/bus/pci/devices/{pci id}/driver_override 寫入指定驅動的名稱。
  2. 配置驅動,讓其支持新的 PCI 設備:向 /sys/bus/pci/drivers/igb_uio/new_id 寫入要 bind 的網卡設備的 PCI ID(e.g. 8086 10f5,格式爲:設備廠商號 設備號)。

按照內核的文檔 https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci 中提到,這兩個動作都會促使驅動程序 bind 新的網卡設備。

而 dpdk-devbind 具體的步驟如下:

  1. 獲取腳本執行參數指定的網卡(e.g. eth1)設備的 PCI 信息。實際是執行指令 lspci–Dvmmn 查看,主要關注 Slot、Vendor ID 以及 Device ID 信息。
Slot: 0000:06:00.1
Class: 0200
Vendor: 8086
Device: 1521
SVendor: 15d9
SDevice: 1521
Rev: 01
  1. unbind 網卡設備之前的 igb 模塊,將 Step 1 中獲取到的 eth1 對應的 Slot 信息 0000:06:00.1 值寫入 igb 驅動的 unbind 文件。e.g. echo 0000:06:00.1 > /sys/bus/pci/drivers/igb/unbind。從內核代碼分析此動作就是將 igb 模塊信息和該 PCI 設備去關聯。

  2. bind 網卡設備到新的 igb_uio 模塊,將 eth1 的 Vendor 和 Device ID 信息寫入 igb_uio 驅動的 new_id 文件。e.g. echo 0x8086 0x1521 > /sys/bus/pci/drivers/igb_uio/new_id

IGB_UIO 的初始化流程

一個設備的驅動要實現的功能根據實際的需要可能千差萬別,但是究其本質來說無非兩件事情:

  1. 一個是內存的操作
  2. 另外一個就是中斷的處理

igb_uio 驅動和 igb 驅動都是網卡這個 PCI 設備的驅動程序,相同點都是能夠使能 PCI 設備、分配內存等,而不同的就在於對內存和中斷的處理方式的差異。

記錄設備的資源

igb_uio 驅動會遍歷該 PCI 設備的 BAR 空間,對於類型爲存儲器空間 IORESOURCE_MEM 的 BAR(Memory BAR),將其物理地址、大小等信息保存到 uio_info 結構的 mem 數組中;將類型爲寄存器空間 IORESOURCE_IO 的 BAR(IO BAR),將其物理地址、大小等信息保存到 uio_info 結構的 port 數組中。

在這裏插入圖片描述

而 igb 驅動同樣也會遍歷 BAR 空間,但是它不會記錄空間的物理地址,而是調用 ioremap() 將物理地址映射爲虛擬地址,然後驅動就可以在內核態中讀寫映射出來的虛擬地址,而不是像 igb_uio 驅動似的在用戶態中進行讀寫。

註冊一個 uio 設備

Linux 上的驅動設備一般都是運行在內核態的,提供接口函數給用戶態函數調用即可。而 UIO 技術則是將驅動的大部分事情移到了用戶態。之所以能夠實現,正如前面所說,是因爲 igb_uio 將 PCI BAR 空間的物理地址、大小等信息都記錄下來並傳給了用戶態。

除了記錄 BAR 空間資源信息,UIO 框架還會在內核態實現中斷處理相關的初始化工作。如下 igbuio_pci_probe 的代碼片段:

* fill uio infos */  
udev->info.name = "igb_uio"; 
udev->info.version = "0.1"; 
udev->info.handler = igbuio_pci_irqhandler; 
udev->info.irqcontrol = igbuio_pci_irqcontrol;          

註冊的 uio 設備名爲 igb_uio,內核態中斷處理函數爲 igbuio_pci_irqhandler,中斷控制函數 igbuio_pci_irqcontrol。

$ ls -l /dev/uio*
crw------- 1 root root 243, 0 5月   8 00:18 /dev/uio0
switch (igbuio_intr_mode_preferred) { 
	case RTE_INTR_MODE_MSIX:  
		msix_entry.entry =0; 
		if (pci_enable_msix(dev,&msix_entry,1)==0) {                                                                        
			udev->info.irq =msix_entry.vector; 
			udev->mode =RTE_INTR_MODE_MSIX; 
			break; 
		}  

	case RTE_INTR_MODE_LEGACY:  
		if (pci_intx_mask_supported(dev)) { 
			udev->info.irq_flags =IRQF_SHARED; 
			udev->info.irq =dev->irq; 
			udev->mode =RTE_INTR_MODE_LEGACY; 
			break; 
		}

變量 igbuio_intr_mode_preferred 表示中斷的模式,它由 igb_uio 驅動的參數 intr_mode 決定,有 MSI-X 中斷和 Legacy 中斷兩種模式,默認爲 MSI-X 中斷模式。

  • 如果是 MSI-X 中斷模式,則調用 pci_enable_msix 函數向 PCI 子系統申請分配一個 MSI-X 中斷。若分配成功就會初始化 uio_info 的 irq 爲申請到的中斷號。
  • 如果是傳統的 Intx 中斷模式,則調用 pci_intx_mask_supported 函數讀取 PCI 配置空間,檢查是否支持 Intx 中斷。

總結

  1. igb_uio 負責創建 uio 設備(e.g. /dev/uio0)並加載 igb_uio 驅動,負責將原先被內核驅動接管的網卡轉移到 igb_uio 驅動,以此來屏蔽掉原生的內核驅動以及內核協議棧;
  2. igb_uio 負責一個橋樑的作用,銜接中斷信號以及用戶態應用,因爲中斷只能在內核態處理,所以 igb_uio 相當於提供了一個接口,銜接用戶態與內核態的驅動。

在這裏插入圖片描述

參考文章

https://www.cnblogs.com/jungle1996/p/12398915.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章