写点什么

DPDK uio 分析 丨 DPDK 的优势及学习总结

  • 2022 年 3 月 08 日
  • 本文字数:7599 字

    阅读完需:约 25 分钟

通常这些非标准设备的驱动被实现为字符驱动。这些驱动使用了很多内核内部函数和宏。而这些内部函数和宏是变化的。这样驱动的编写者必须编写一个完全的内核驱动,而且一直维护这些代码。

而且这些驱动进不了主内核源码。于是就出现了用户空间 I/O 框架(Userspace I/O framework)。

UIO 怎样实现一个设备驱动的基本任务

一个设备驱动的主要任务有两个: 1. 存取设备的内存 2. 处理设备产生的中断

  • 对于第一个任务,UIO 核心实现了 mmap()可以处理物理内存(physical memory),逻辑内存(logical memory), 虚拟内存(virtual memory)。UIO 驱动的编写是就不需要再考虑这些繁琐的细节。

  • 第二个任务,对于设备中断的应答必须在内核空间进行。所以在内核空间有一小部分代码用来应答中断和禁止中断,但是其余的工作全部留给用户空间处理。

如果用户空间要等待一个设备中断,它只需要简单的阻塞在对 /dev/uioX 的 read()操作上。 当设备产生中断时,read()操作立即返回。UIO 也实现了 poll()系统调用,你可以使用 select()来等待中断的发生。

select()有一个超时参数可以用来实现有限时间内等待中断。对设备的控制还可以通过/sys/class/uio 下的各个文件的读写来完成。你注册的 uio 设备将会出现在该目录下。

假如你的 uio 设备是 uio0 那么映射的设备内存文件出现在 /sys/class/uio/uio0/maps/mapX,对该文件的读写就是 对设备内存的读写。

  如下的图描述了 uio 驱动的内核部分,用户空间部分,和 uio 框架以及内核内部函数的关系

编辑切换为居中

添加图片注释,不超过 140 字(可选)

编辑切换为居中

添加图片注释,不超过 140 字(可选)

/** * A structure describing the private information for a uio device. */struct rte_uio_pci_dev {    struct uio_info info;//uio 通用结构    struct pci_dev *pdev;//pci设备描述结构    enum rte_intr_mode mode;//中断模式    atomic_t refcnt;};
复制代码


 /**来自内核 uio_driver.h 文件 * struct uio_info - UIO device capabilities * @uio_dev:        the UIO device this info belongs to * @name:        device name * @version:        device driver version * @mem:        list of mappable memory regions, size==0 for end of list * @port:        list of port regions, size==0 for end of list * @irq:        interrupt number or UIO_IRQ_CUSTOM * @irq_flags:        flags for request_irq() * @priv:        optional private data * @handler:        the device's irq handler * @mmap:        mmap operation for this uio device * @open:        open operation for this uio device * @release:        release operation for this uio device * @irqcontrol:        disable/enable irqs when 0/1 is written to /dev/uioX */struct uio_info {    struct uio_device    *uio_dev;//uio设备 在 uio_register_device中初始化    const char        *name;//名称 调用__uio_register_device之前必须初始化    const char        *version;//版本号 //调用__uio_register_device之前必须初始化    struct uio_mem        mem[MAX_UIO_MAPS];//可映射的内存区域列表,size == 0表示列表结束    struct uio_port        port[MAX_UIO_PORT_REGIONS];//网口区域列表    long            irq;//UIO_IRQ_CUSTOM 中断号 //分配给uio设备的中断号,调用__uio_register_device之前必须初始化    unsigned long        irq_flags;//请求中断号的标志    void            *priv; //可选的私有数据    irqreturn_t (*handler)(int irq, struct uio_info *dev_info);//中断信息处理    int (*mmap)(struct uio_info *info, struct vm_area_struct *vma);//内存映射操作    int (*open)(struct uio_info *info, struct inode *inode);//打开    int (*release)(struct uio_info *info, struct inode *inode);//释放    int (*irqcontrol)(struct uio_info *info, s32 irq_on);//中断控制操作 关闭/打开 当向/dev/uioX中写入值时};
复制代码

  uio 核心部分是一个名为"uio"的字符设备(下文称为“uio 核心字符设备“)。用户驱动的内核部分使用 uio_register_device 向 uio 核心部分 注册 uio 设备。uio 核心的任务就是管理好这些注册的 uio 设备。这些 uio 设备使用的数据结构是 uio_device。而这些设备属性,比如 name, open(), release()等操作都放在了 uio_info 结构中,用户使用 uio_register_device 注册这些驱动之前 要设置好 uio_info


文章福利:DPDK 的学习资料及学习路线总结图点击:DPDK学习资料 获取及学习路线总结图




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

  1. 第一件事是配置中断模式;

  2. 第二种模式便是注册驱动

static struct pci_driver igbuio_pci_driver = {    .name = "igb_uio",    .id_table = NULL,    .probe = igbuio_pci_probe,    .remove = igbuio_pci_remove,};
static int __initigbuio_pci_init_module(void){ int ret;
if (igbuio_kernel_is_locked_down()) { pr_err("Not able to use module, kernel lock down is enabled\n"); return -EINVAL; }
if (wc_activate != 0) pr_info("wc_activate is set\n");
ret = igbuio_config_intr_mode(intr_mode);//内核insmod时带的参数,中断模式 if (ret < 0) return ret;
return pci_register_driver(&igbuio_pci_driver);//注册PCI设备,如果匹配 id 成功 会调用igbuio_pci_probe 探测。}
复制代码

可以看到 注册时候 id_table 是空的, id 以及 name match 不上, 不会执行起回调 probe 函数;​



/*PCI总线设备、网卡设备以及网卡设备的私有数据结构,即将设备的共性一层层的抽象,PCI总线设备包含网卡设备,网卡设备又包含其私有数据结构。在DPDK中,首先会注册设备驱动,然后查找当前系统有哪些PCI设备,并通过PCI_ID为PCI设备找到对应的驱动,最后调用驱动初始化设备。*/
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 8, 0)static int __devinit#elsestatic int#endifigbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id){ struct rte_uio_pci_dev *udev; dma_addr_t map_dma_addr; void *map_addr; int err;
#ifdef HAVE_PCI_IS_BRIDGE_API if (pci_is_bridge(dev)) { dev_warn(&dev->dev, "Ignoring PCI bridge device\n"); return -ENODEV; }#endif
udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL); if (!udev) return -ENOMEM;
/* * enable device: ask low-level code to enable I/O and * memory 使能设备: 调用更底层的PCI代码使能设备的内存和I/O区域 */ err = pci_enable_device(dev); if (err != 0) { dev_err(&dev->dev, "Cannot enable PCI device\n"); goto fail_free; }
/* enable bus mastering on the device */ pci_set_master(dev); /* 将设备设置层DMA总线主模式 */
/* remap IO memory */ /* 重新映射I/O内存,设置uio_info的uio_mem和uio_port 其中在igbuio_pci_bars函数中,会遍历6个PCI BAR,获得其PCI BAR的起始地址, 并对这些起始地址进行ioremap*/ err = igbuio_setup_bars(dev, &udev->info); if (err != 0) goto fail_release_iomem;
/* set 64-bit DMA mask 若函数返回成功,可以在位于该函数所带参数范围内的任意地址进行DMA操作 */ err = pci_set_dma_mask(dev, DMA_BIT_MASK(64)); if (err != 0) { dev_err(&dev->dev, "Cannot set DMA mask\n"); goto fail_release_iomem; } //内存范围一致性的处理 err = pci_set_consistent_dma_mask(dev, DMA_BIT_MASK(64)); if (err != 0) { dev_err(&dev->dev, "Cannot set consistent DMA mask\n"); goto fail_release_iomem; }
/* fill uio infos */ /* 填充uio信息 注意 uio 没有实现 mmap 函数 去对内存映射 */ udev->info.name = "igb_uio"; udev->info.version = "0.1"; udev->info.irqcontrol = igbuio_pci_irqcontrol; udev->info.open = igbuio_pci_open;// open 打开这个设备的时候会注册驱动终端函数 以及设置相关需要初始化内容 udev->info.release = igbuio_pci_release; udev->info.priv = udev; udev->pdev = dev; atomic_set(&udev->refcnt, 0); //用特定属性创建sysfs节点组 err = sysfs_create_group(&dev->dev.kobj, &dev_attr_grp); if (err != 0) goto fail_release_iomem;
/* register uio driver *//* 注册uio设备 */ err = uio_register_device(&dev->dev, &udev->info); if (err != 0) goto fail_remove_group;
pci_set_drvdata(dev, udev);
/* * Doing a harmless dma mapping for attaching the device to * the iommu identity mapping if kernel boots with iommu=pt. * Note this is not a problem if no IOMMU at all. */ map_addr = dma_alloc_coherent(&dev->dev, 1024, &map_dma_addr, GFP_KERNEL); if (map_addr) memset(map_addr, 0, 1024);
if (!map_addr) dev_info(&dev->dev, "dma mapping failed\n"); else { dev_info(&dev->dev, "mapping 1K dma=%#llx host=%p\n", (unsigned long long)map_dma_addr, map_addr);
dma_free_coherent(&dev->dev, 1024, map_addr, map_dma_addr); dev_info(&dev->dev, "unmapping 1K dma=%#llx host=%p\n", (unsigned long long)map_dma_addr, map_addr); }
return 0;
fail_remove_group: sysfs_remove_group(&dev->dev.kobj, &dev_attr_grp);fail_release_iomem: igbuio_pci_release_iomem(&udev->info); pci_disable_device(dev);fail_free: kfree(udev);
return err;}
复制代码

igbuio_pci_bars 函数中,会遍历 6 个 PCI BAR,获得其 PCI BAR 的起始地址,并对这些起始地址进行 ioremap

static intigbuio_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++) { if (pci_resource_len(dev, i) != 0 &&//PCI BAR空间不等于0且起始地址不等于0,认为为有效BAR pci_resource_start(dev, i) != 0) { flags = pci_resource_flags(dev, i); if (flags & IORESOURCE_MEM) {//拿到BAR的标识,如果为0x00000200则为内存空间 ret = igbuio_pci_setup_iomem(dev, info, iom, i, bar_names[i]);//对内存空间的PCI BAR进行映射 if (ret != 0) return ret; iom++; } 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;}
复制代码


 /* Remap pci resources described by bar #pci_bar in uio resource n. */static intigbuio_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; if (wc_activate == 0) { //wc_activate为igb_uio.ko的参数,默认为0,会进入if条件 internal_addr = ioremap(addr, len); //对PCI BAR进行ioremap,映射到内核空间,得到可以在内核空间映射后的PCI BAR地址 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;}
复制代码

igbuio_set_bars 做的工作很清晰:-->填充数据结构加上对 PCI BAR 的 IO 内存(物理地址)进行 ioremap,进行 ioremap 映射后会得到一个可以供内核空间访问的 PCI BAR 地址(虚拟地址),不过从设计角度上讲,igb_uio 不需要对 PCI 设备得到 BAR 空间,并对 PCI 设备进行配置,因此意义不大。接下来便是调用 uio_register_devcie 注册 uio 设备



igb_uio 其实完全没有做 mmap 这块的工作,因此 uio_info->mmap 这个钩子函数其实是 NULL,所以 DPDK 完全不靠 igb_uio 得到 PCI BAR,而是直接调用内核已经映射过的 resource0..N 即可

如何将 PCI 设备的驱动重新绑定

操作步骤如下:

  1. 将当前 PCI 设备的现有驱动目录下的 unbind 写入 PCI 设备的 PCI 地址,例如:

  • echo "0000:81:00.0" > /sys/bus/pci/drivers/ixgbe/unbind

  1. 拿到当前 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

/* Manually detach a device from its associated driver. */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) {        //pci设备释放驱动,其中调用的就是driver或者bus的remove钩子函数,然后再将device中的driver指针置空        device_driver_detach(dev);        err = count;    }    put_device(dev);    bus_put(bus);    return err;}
/*创建 attribute“文件” 设置属性以及读写函数Linux中万物皆文件,这些attribute实际上就是/sys/bus/pci/drivers/[driver_name]/目录下的文件*/static DRIVER_ATTR_IGNORE_LOCKDEP(unbind, S_IWUSR, NULL, unbind_store);//
复制代码

可以看到对 unbind 文件进行写操作后,最终会转到内核态的 pci 设备的 unbind_store 函数

new_id 的属性实现则是在/drivers/pci/pci-driver.c 中;最终会调到驱动的 probe 钩子上,在 igb_uio 驱动中即为 igbuio_pci_probe 函数

/** * store_new_id - sysfs frontend to pci_add_dynid() * @driver: target device driver * @buf: buffer for scanning device ID data * @count: input size * * Allow PCI IDs to be added to an existing driver via sysfs. */static ssize_t new_id_store(struct device_driver *driver, const char *buf,                size_t count){    struct pci_driver *pdrv = to_pci_driver(driver);    const struct pci_device_id *ids = pdrv->id_table;    u32 vendor, device, subvendor = PCI_ANY_ID,        subdevice = PCI_ANY_ID, class = 0, class_mask = 0;    unsigned long driver_data = 0;    int fields = 0;    int retval = 0;
fields = sscanf(buf, "%x %x %x %x %x %x %lx", &vendor, &device, &subvendor, &subdevice, &class, &class_mask, &driver_data); if (fields < 2) return -EINVAL;
if (fields != 7) { struct pci_dev *pdev = kzalloc(sizeof(*pdev), GFP_KERNEL); if (!pdev) return -ENOMEM;
pdev->vendor = vendor; pdev->device = device; pdev->subsystem_vendor = subvendor; pdev->subsystem_device = subdevice; pdev->class = class;
if (pci_match_id(pdrv->id_table, pdev)) retval = -EEXIST;
kfree(pdev);
if (retval) return retval; }
/* Only accept driver_data values that match an existing id_table entry */ if (ids) { retval = -EINVAL; while (ids->vendor || ids->subvendor || ids->class_mask) { if (driver_data == ids->driver_data) { retval = 0; break; } ids++; } if (retval) /* No match */ return retval; }
retval = pci_add_dynid(pdrv, vendor, device, subvendor, subdevice, class, class_mask, driver_data); if (retval) return retval; return count;}static DRIVER_ATTR_WO(new_id);
复制代码


  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 的对上接口即可;

在这里推荐全网唯一 DPDK 系统学习视频,点击链接:

dpdk/网络协议栈/vpp/OvS/DDos/SDN/NFV/虚拟化/高性能专家之路-学习视频

用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
DPDK uio 分析 丨DPDK的优势及学习总结_Linux服务器开发_Linux服务器开发_InfoQ写作平台