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)来绑定网卡。

在 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 显然是使用了第 2 种方式。

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
");
        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
", __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
", __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
", __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
",
                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)
",
            __func__, fd, requested_addr, size,
            (unsigned long long)offset,
            strerror(errno), mapaddr);
    } else
        RTE_LOG(DEBUG, EAL, "  PCI memory mapped at %p
", 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 设备。

IGB_UIO 的注册 PCI 设备的流程

igb_uio 内核模块主要做的事情就是注册一个 PCI 设备。但是 igbuio_pci_driver 对应保存的 PCI 设备信息的 id_table 指针初始是为空的,这样在内核注册此 PCI 设备时,就会找不到匹配的设备,也就不会调用 igb_uio 驱动中的 probe() 来探测 PCI 设备。只是单存的在 /sys 目录下创建了 igb_uio 相应的目录。

所以,实际上在加载 igb_uio.ko 模块到内核时,igb_uio 是 probe(探测)不到 PCI 设备的,而是在执行 Python 脚本 dpdk_nic_bind.py 的时候完成。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

在这里插入图片描述
一个设备的驱动要实现的功能根据实际的需要可能千差万别,但是究其本质来说无非两件事情:

  1. 一个是内存的操作
  2. 另外一个就是中断的处理

igb_uio 驱动和 igb 驱动都是网卡这个 PCI 设备的驱动程序,相同点都是能够使能 PCI 设备、分配内存等,而不同的就在于对内存和中断的处理方式的差异。

PCI 设备探测

sysfs 文件系统是一种内存文件系统,它提供了一种方法用于导出内核的数据结构、属性以及两者之间的联系到用户空间。用户空间可以通过修改 sysfs 中的文件来修改设备的属性值。上诉 new_id、driver_override、bind、unbind 文件对应 igb_uio 驱动的各属性值。new_id 主要就是将 PCI 设备的 ID 加入至驱动动态设备 ID 列表,供后面驱动设备 probe 时使用。

在这里插入图片描述

  1. 在操作任何 PCI 设备的寄存器之前,需要调用 pci_enable_device 函数启动设备,此函数主要做了以下工作:
    1. wake up the device if it was in suspended state
    2. allocate I/O and memory regions of the device (if BIOS did not)
    3. allocate an IRQ (if BIOS did not)
  2. pci_set_master 设置 PCI COMMAND 寄存器 bus master 位,启动 DMA。
  3. igb_setup_bars 将 BAR 空间导入 uio_info 结构,供后面用户层使用(PMD)。
  4. pci_set_dma_mask 和 pci_set_consistent_dma_mask 设置 DMA 地址掩码位。因为 PCI 设备是有寻址限制的,所以需要标识那些位的地址可用。
  5. uio_register_device 注册 uioX 设备。此处会创建 /dev/uiox 设备文件供后面应用层使用。
  6. pci_set_drvdata 设置私有数据。

在完成上述步骤之后,这时 dmesg 就会看到 igb_uio 内核模块的 probe 函数执行了,也就是意味着扫描到了相应的 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 中断。

在对 uio_info 内存和中断相关的成员初始化之后,就开始调用 uio_register_device 函数来注册 uio 设备了。

idev->owner = owner; 
idev->info = info; 

init_waitqueue_head(&idev->wait); 
atomic_set(&idev->event, 0);

idev->dev =device_create(&uio_class,parent,
						 MKDEV(uio_major, idev->minor),
						 idev, 
						 "uio%d",
						 idev->minor); 

ret =uio_dev_add_attributes(idev); 
info->uio_dev =idev; 

if (info->irq &&(info->irq !=UIO_IRQ_CUSTOM)) { 
	ret =devm_request_irq(idev->dev,info->irq,uio_interrupt,                                                      
	info->irq_flags,info->name,idev); 
}  
  1. 初始化 uio_device 结构体指针 idev,主要包括等待队列 wait、中断事件计数 event、次设备号 minor 等。
  2. 在 /dev 目录下创建了一个 uio 设备,设备名为 uio%d,%d 为次设备号 minor。
$ ls -l /dev/uio*
crw------- 1 root root 243, 0 5月   8 00:18 /dev/uio0
  1. 接着就是调用 uio_dev_add_attributes 函数在 /sys/class/uio/uioX/ 目录下创建 maps 和 portio 接口。前面讲到会遍历此 PCI 设备的 BAR 空间,将存储器空间类型的 BAR 的物理地址等信息存储在 uio_info 的 mem 数组中,这里就会根据此 mem 数组在 maps 目录下为每个寄存器类型的 BAR 创建一个目录。
[root@c-dev ~]# ls -l /sys/class/uio/uio0/maps/map0/
总用量 0
-r--r--r-- 1 root root 4096 5月   8 00:19 addr
-r--r--r-- 1 root root 4096 5月   8 00:19 name
-r--r--r-- 1 root root 4096 5月   8 00:19 offset
-r--r--r-- 1 root root 4096 5月   8 00:19 size
[root@c-dev ~]# ls -l /sys/class/uio/uio0/maps/map1/
总用量 0
-r--r--r-- 1 root root 4096 5月   8 00:19 addr
-r--r--r-- 1 root root 4096 5月   8 00:19 name
-r--r--r-- 1 root root 4096 5月   8 00:19 offset
-r--r--r-- 1 root root 4096 5月   8 00:19 size

可以看出,igb_uio 网卡有两个类型为 IORESOURCE_MEM 的 BAR,分别为 BAR1 和 BAR4,这里就创建了 map0 和 map1 两个子目录分别对应 BAR1 和 BAR1。

[root@c-dev ~]# cat /sys/class/uio/uio0/maps/map1/name
BAR4
[root@c-dev ~]# cat /sys/class/uio/uio0/maps/map1/addr
0x0000000440000000
  1. 最后就是注册中断了,中断的中断号、中断标志等在前面有讲到,这里看下注册的中断处理函数 uio_interrupt。
static irqreturn_t uio_interrupt(intirq,void *dev_id) 
{ 
    struct uio_device *idev =(struct uio_device *)dev_id;                                                           
    irqreturn_t ret =idev->info->handler(irq,idev->info); 
    
    if (ret==IRQ_HANDLED) 
        uio_event_notify(idev->info); 
    return ret; 
} 

此函数首先调用 igb_uio 驱动中设置的中断处理函数 igbuio_pci_irqhandler 来检查中断是不是此设备的中断,如果是就返回 IRQ_HANDLED 表示需要处理,接着调用函数 uio_event_notify 来唤醒等待队列 wait 上进程来处理中断事宜。

总结

  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
https://blog.csdn.net/weijitao/article/details/52949454

原文地址:https://www.cnblogs.com/hzcya1995/p/13309267.html