DPDK系列第八篇:深入剖析DPDK中 IGB_UIO的使用

 系列文章

DPDK 系列第一篇: DPDK架构阐述-CSDN博客

DPDK 系列第二篇:CPU Cache详解及DPDK在Cache方面的性能应用-CSDN博客

DPDK 系列第三篇:CPU 亲和性及实际应用-CSDN博客

DPDK 系列第四篇:TLB和大页-CSDN博客

DPDK 系列第五篇:基于大页的内存管理 (内附整理的全内存数据结构关联图,全网无第二份)-CSDN博客

DPDK系列第六篇:IOVA前期知识 DMA、IOMMU概念及关联-CSDN博客

DPDK系列第七篇:IOVA介绍及实际使用-CSDN博客

引言

        上一节IOVA的讲解中,我们引入了IGB_UIO和 VFIO两个PCI内核态驱动,这两个驱动是DPDK实现用户态PMD的基石,我们将分两节进行介绍,本节将针对IGB_UIO进行深入剖析。

整体概念

        老规矩,我们还是以先总后分的形式,让大家对IGB_UIO在DPDK中的位置和 作用有一个总体上的认识,然后再逐步拆解每一个技术点。

        如贴图所示,主要涉及UIO Framework、Driver(IGB_UIO)、/dev/uiox及用户态驱动部分。

        UIO Framework是一种通用的用户态I/O框架,本身是一个字符类型驱动,支持将用户态驱动的很少一部分运行在内核空间(例如处理网卡硬件中断),然后将大部分功能在用户空间运行。

        Driver 部分就是本文的主角IGB_UIO,它是UIO的一个具体实现,从形态上是一种 PCI驱动,将网卡绑定到IGB_UIO驱动后,相当于隔离了网卡的内核驱动,同时IGB_UIO还会完成网卡中断内核态初始化,并将中断信号映射到用户态。

        /dev/uiox 则是使用mknod 创建的用于用户态驱动和IGB_UIO/UIO去通信的设备节点,它通过与UIO字符设备驱动代码中创建的主设备号关联起来,来实现用户空间程序与字符设备驱动的交互。用户空间对/dev/uiox文件的操作最终都会对应到对UIO驱动中 uio_fops 的不同方法的调用上,在dpdk中,主要就是用于中断信号的映射。

        Userspace 下的 Driver 在DPDK中就是用户态的PMD了,比如DPDK中 针对intel710系列网卡的 i40e 驱动。

        后边的章节我们会针对这几个部件挨个进行代码层面的讲解,并针对部件间的关联进行详细描述。为了便于后边理解,这里特别强调驱动中的几个概念:

        主设备号:

        驱动程序在初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过/proc/devices文件来查看系统设备的主设备号。

        次设备号:

        驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。

        设备节点(设备文件):

        Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux中称为设备文件。创建设备文件时需要指定主设备号和次设备号。

        设备节点,驱动,硬件设备是如何关联到一起的呢?这是通过设备号实现的,包括主设备号和次设备号。当我们创建一个设备节点时需要指定主设备号和次设备号。应用程序通过名称访问设备,而设备号指定了对应的驱动程序和对应的设备。主设备号标识设备对应的驱动程序,次设备号由内核使用,用于确定设备节点所指设备。

415b71a0a3db45f5971126901318d624.png        

UIO框架

static const struct file_operations uio_fops = {
	.owner		= THIS_MODULE,
	.open		= uio_open,
	.release	= uio_release,
	.read		= uio_read,
	.write		= uio_write,
	.mmap		= uio_mmap,
	.poll		= uio_poll,
	.fasync		= uio_fasync,
	.llseek		= noop_llseek,
};
......
struct cdev {
    struct kobject kobj; /* 内嵌的kobject对象 */
    struct module *owner; /* 所属模块*/
    struct file_operations *ops; /* 文件操作结构体*/
    struct list_head list;
    dev_t dev; /* 设备号*/
    unsigned int count;
};
......
static int uio_major_init(void)
{
	static const char name[] = "uio";
	struct cdev *cdev = NULL;
	dev_t uio_dev = 0;
	int result;

	result = alloc_chrdev_region(&uio_dev, 0, UIO_MAX_DEVICES, name);
	if (result)
		goto out;

	result = -ENOMEM;
	cdev = cdev_alloc();
	if (!cdev)
		goto out_unregister;

	cdev->owner = THIS_MODULE;
	cdev->ops = &uio_fops;
	kobject_set_name(&cdev->kobj, "%s", name);

	result = cdev_add(cdev, uio_dev, UIO_MAX_DEVICES);
	if (result)
		goto out_put;

	uio_major = MAJOR(uio_dev);
	uio_cdev = cdev;
	return 0;
out_put:
	kobject_put(&cdev->kobj);
out_unregister:
	unregister_chrdev_region(uio_dev, UIO_MAX_DEVICES);
out:
	return result;
}
......
static int init_uio_class(void)
{
	int ret;

	/* This is the first time in here, set everything up properly */
	ret = uio_major_init();
	if (ret)
		goto exit;

	ret = class_register(&uio_class);
	if (ret) {
		printk(KERN_ERR "class_register failed for uio\n");
		goto err_class_register;
	}

	uio_class_registered = true;

	return 0;

err_class_register:
	uio_major_cleanup();
exit:
	return ret;
}
......
static int __init uio_init(void)
{
	return init_uio_class();
}
......
module_init(uio_init)
module_exit(uio_exit)
MODULE_LICENSE("GPL v2");
 

我们分析下uio 框架代码。

        了解过内核模块的同学应该知道,module_init是一个宏,用于定义在模块加载时需要执行的初始化函数。当内核加载一个模块时,它会查找模块的module_init宏,并将其指定的函数注册为模块的初始化函数。这个初始化函数将在模块加载时被调用,用于执行一些必要的初始化操作。

        如上UIO的代码,module_init(uio_init) 定义了uio内核模块被加载时的初始化函数uio_init,uio_init 函数调用了init_uio_class 函数,该函数首先调用uio_major_init 申请主设备号、创建一个cdev并向系统添加该字符设备(驱动),然后通过调用class_register 函数来注册uio_class 类别。        当调用 cdev_add 函数时,实际上是将一个字符设备驱动注册到内核中,并与一个主设备号和设备结构(uio_fops)结构体关联起来。

IGB_UIO

int
igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{

......
	udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL);
	if (!udev)
		return -ENOMEM;

	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);

	/* remap IO memory */
	err = igbuio_setup_bars(dev, &udev->info);
	if (err != 0)
		goto fail_release_iomem;

	/* set 64-bit DMA mask */
	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 */
	udev->info.name = "igb_uio";
	udev->info.version = "0.1";
	udev->info.irqcontrol = igbuio_pci_irqcontrol;
	udev->info.open = igbuio_pci_open;
	udev->info.release = igbuio_pci_release;
	udev->info.priv = udev;
	udev->pdev = dev;
	atomic_set(&udev->refcnt, 0);

	err = sysfs_create_group(&dev->dev.kobj, &dev_attr_grp);
	if (err != 0)
		goto fail_release_iomem;

	/* register uio driver */
	err = uio_register_device(&dev->dev, &udev->info);
	if (err != 0)
		goto fail_remove_group;

	pci_set_drvdata(dev, udev);

    ......
}
......
static struct pci_driver igbuio_pci_driver = {
	.name = "igb_uio",
	.id_table = NULL,
	.probe = igbuio_pci_probe,
	.remove = igbuio_pci_remove,
};
......
static int
igbuio_config_intr_mode(char *intr_str)
{
    ......
	if (!strcmp(intr_str, RTE_INTR_MODE_MSIX_NAME)) {
		igbuio_intr_mode_preferred = RTE_INTR_MODE_MSIX;
		pr_info("Use MSIX interrupt\n");
	} else if (!strcmp(intr_str, RTE_INTR_MODE_MSI_NAME)) {
		igbuio_intr_mode_preferred = RTE_INTR_MODE_MSI;
		pr_info("Use MSI interrupt\n");
	} else if (!strcmp(intr_str, RTE_INTR_MODE_LEGACY_NAME)) {
		igbuio_intr_mode_preferred = RTE_INTR_MODE_LEGACY;
		pr_info("Use legacy interrupt\n");
	} else {
		pr_info("Error: bad parameter - %s\n", intr_str);
		return -EINVAL;
	}

	return 0;
}
......
static int __init
igbuio_pci_init_module(void)
{
    ......

	ret = igbuio_config_intr_mode(intr_mode);
	if (ret < 0)
		return ret;

	return pci_register_driver(&igbuio_pci_driver);
}
......
module_init(igbuio_pci_init_module);
module_exit(igbuio_pci_exit_module);
 

我们分析下IGB_UIO的代码。

        这里不得不先提一下“设备-总线-驱动” 这种绑定逻辑,在Linux 2.6以后的设备驱动模型中,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。 在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

        IGB_UIO 驱动也完全能反应这种绑定逻辑关系,在执行insmod命令加载IGB_UIO驱动时,会进行uio实例驱动的初始化操作, 注册一个uio实例驱动到内核。这里会指定一个驱动操作接口igbuio_pci_driver,其中的probe(igbuio_pci_probe)是在总线探测到网卡,并绑定uio实例驱动的时候被调度执行; 同理当网卡卸载uio实例驱动时,uio驱动检测到有网卡卸载了,则remove会被调度执行。

        在igbuio_pci_driver 结构中可以看到id_tbale 字段是置NULL的,这里置NULL会保证通过insmod igb_uio.ko 插入igb_uio驱动模块时,系统不会去查找匹配的设备。设备与igb_uio驱动的关联是发生在向/sys系统中igb_uio驱动目录下的bind文件写入设备id信息时候发生的。

       我们这里分析igbuio_pci_probe 函数,具体看下是如何初始化设备的

        1、激活设备(调用pci_enable_device)

        该函数实际调用了pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO), 使得驱动能够访问pci设备的内存与io空间

        2、为pci设备映射虚拟内存与虚拟io空间(igbuio_setup_bars)

        该函数会读取网卡的resource文件(例如/sys/bus/pci/devices/0000:00:0b.0/resource), 然后根据读取到的BAR中物理内存起始地址,映射虚拟内存的地址,这样实现虚拟内存和物理内存1:1的效果(这一点我们在上一篇IOVA 章节讲过, igb_uio 方式IOVA=PA,这种模式下虚拟内存和物理内存是对应的关系),io空间也是同样的道理。如下截图,可以看到pci 地址为0000:00:0b.0的网卡设备,有两个内存BAR,没有IO空间BAR。

        这里有个注意点,目前分析的19.11 dpdk 代码,并没有使用这种内存映射, 而是在启动时候进行总线扫描时调用了pci_parse_sysfs_resource 去直接扫描 resource文件获取物理地址。

4eb728da8e934878b353ca4e90f07ee2.png

        映射完的地址信息会存储在uio_info结构下。每一个 uio 设备都会实例化一个 uio_info 结构体,uio 驱动自身不会实例化 uio_info 结构体,它只提供一个框架,igb_uio驱动probe的时候调用 uio_register_device 进行实例化,后边会讲到。

        3、为PCI网卡设置DMA模式(pci_set_master)

        pci_set_master函数的作用就是将指定的PCI设备设置为主控制器状态,使其可以发起数据传输请求。设置为主控状态后设备就可以主动与其他设备进行通信或执行DMA操作。

        pci_set_dma_mask 和pci_set_consistent_dma_mask两个函数则用于设置DMA的访问宽度。(DMA访问宽度有时候有限,通过IOMMU可以解决,参考之前IOMMU章节)

        4、设置UIO设备的中断开打及处理函数

       通过 "uio_info->irqcontrol = igbuio_pci_irqcontrol"  和 "info->open = igbuio_pci_open" 两项赋值操作设置对应设备的中断设置。

        在下边的uio设备注册后,用户态通过对设备节点打开即会调用到这里的 igbuio_pci_open,对设备节点的写入最终又会调用到igbuio_pci_irqcontrol 函数中。

       需注意的是,这里的中断只是用于网卡状态等的处理,并不会用于PMD驱动收发包,否则PMD就失去意义了。还有一个是中断打开时会覆写掉原有硬件中断处理函数,这样就可以截获中断到用户态。

static int uio_open(struct inode *inode, struct file *filep)
{
......

	if (idev->info->open)
		ret = idev->info->open(idev->info, inode);
......
	return ret;
}

static ssize_t uio_write(struct file *filep, const char __user *buf,
			size_t count, loff_t *ppos)
{
......
	if (!idev->info->irqcontrol) {
		retval = -ENOSYS;
		goto out;
	}

	retval = idev->info->irqcontrol(idev->info, irq_on);
......
}

        5、注册uio设备(uio_register_device)

        该函数内部会调用到device_create函数,该函数中会将设备添加到设备树、添加一些列设备文件系统相关文件、创建设备节点/dev/uiox 等等

         0aed663e88ee49c6ab31d1c6a4ce1298.png

5211363d526c4bc4ad6e09b55ee54a04.png

        以上我们分析该驱动probe流程是基于内核pci系统探测到设备,并将设备与igb_uio驱动进行绑定为前提叙述的,特别又强调这点是想说明,到这里的绑定和dpdk是没有半毛钱关系的,这个绑定只是把设备使能了,设置了Dma模式,通过uio的机制,使得内核态的网卡驱动不可用了,仅此而已。后期是使用dpdk还是其他不管谁开发的用户态驱动,都可以直接在用户态去操作对应的硬件了。

DPDK实际网卡设备上线流程

         根据上边对uio 及 igb_uio 的描述,我们可以推断dpdk 网卡的上线流程

        1、modprobe uio  // 加载uio内核模块

        2、insmod igb_uio // 基于uio,加载igb_uio 驱动

        3、我们在上边分析中说到 在igbuio_pci_driver 结构中id_tbale 字段是置NULL的,保证第2步中通过insmod igb_uio.ko 插入igb_uio驱动模块时,系统不会去查找匹配的设备。那想让设备上线,第三步应该 对  /sys/bus/pci/drivers/igb_uio/new_id 文件写入要绑定的设备id,并向同目录下的bind文件写入设备的pci地址,使得内核pci系统可以探测设备和驱动关系,进行原始设备驱动的剥离。

        4、下来就是dpdk的启动,代码递进关系如下

rte_eal_init
     rte_bus_scan
         rte_pci_scan
             pci_scan_one
                 pci_get_kernel_driver_by_path (通过查看pci目录下的driver 软连接,获取网卡驱动,这里获取到的是igb_uio, 这个很重要,这里把 获取的igb_uio赋值给了dev->kdrv,后边的一些内存初始化什么的都会用到这个变量)
                 rte_pci_add_device (Add a device to PCI bus)
     rte_bus_probe 
         pci_probe
             pci_probe_all_drivers
                 rte_pci_probe_one_driver
                     rte_pci_map_device(这里会用到dev->kdrv)
                     xx_pci_probe (执行实际网卡用户态PMD驱动的probe)

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值