dpdk uio驱动实现

一、dpdk uio驱动框架

        uio是运行在用户空间的I/O技术,Linux系统中,一般的设备驱动都是运行在内核空间。而uio则是将驱动的很少一部分运行在内核空间(例如处理网卡硬件中断,因为硬件中断只能在内核处理,如果硬件中断在应用层处理,然而应用层进程有可能被杀掉导致设备硬件中断无法被处理;暴露网卡设备的寄存器、内存、io等空间以便应用层使用),然后在用户空间实现驱动的绝大多数功能。使用uio可以避免设备的驱动程序需要随着内核的更新而更新的问题。

        igb_uio借助uio技术来截获中断,并重设中断回调行为,从而绕过linux内核协议栈后续的处理流程。并且igb_uio会在内核初始化的过程中将 NIC 的硬件寄存器映射到用户空间。igb_uio驱动的作用是让你在用户态就可以操作网卡设备的内存以及寄存器。dpdk同时还提供了pmd用户态驱动,用户态pmd驱动就是通过uio机制,通过操作网卡的寄存器实现在用户态收发报文。

                   

        在系统加载igb_uio驱动后,每当有网卡和igb_uio驱动进行绑定时, 就会在/dev目录下创建一个uio设备,例如/dev/uio1。uio设备是一个接口层,用于将pci网卡的内存空间以及网卡的io空间暴露给应用层。通过这种方式,应用层访问uio设备就相当于访问网卡。具体来说,当有网卡和uio驱动绑定时,被内核加载的igb_uio驱动, 会将pci网卡的内存空间,网卡的io空间保存在uio目录下,例如/sys/class/uio/uio1/maps文件中,同时也会保存到pci设备目录下的uio文件中。这样应用层就可以访问这2个文件中的任意一个文件里面保存的地址空间,然后通过mmap将文件中保存网卡的物理内存映射成虚拟地址, 应用层访问这个虚拟地址空间就相当于访问pci设备。

        从图中可以看出,一共由用户态驱动pmd, 运行在内核态的igb_uio驱动,以及linux的uio框架组成。用户态驱动pmd通过轮询的方式,直接从网卡收发报文,将内核旁路了,绕过了协议栈,避免了内核和应用层之间的拷贝性能;   内核态驱动igb_uio,用于将pci网卡的内存空间,io空间暴露给应用层,供应用层访问,同时会处理在网卡的硬件中断;linux uio框架提供了一些给igb_uio驱动调用的接口,例如uio_open打开uio;  uio_release关闭uio; uio_read从uio读取数据; uio_write往uio写入数据。linux uio框架的代码在内核源码drivers/uio/uio.c文件中实现。linux uio框架也会调用内核提供的其他api接口函数。

        应用层pmd通过read系统调用来访问/dev/uiox设备,进而调用igb_uio驱动中的接口, igb_uio驱动最终会调用linux uio框架提供的接口。可以以下方式操作/dev/uiox设备:

mmap() 接口:用于映射设备的寄存器空间到应用层来。
read() 接口:用于等待一个网卡设备中断。
write() 接口:用于控制硬件中断关闭/打开

二、用户态驱动pmd轮询与uio中断的关系

        pmd用户态驱动是通过轮询的方式,直接从网卡收发报文,将内核旁路了,绕过了协议栈。那为什么还要实现uio呢? 在某些情况下应用层想要知道网卡的状态信息之类的,就需要网卡硬件中断的支持。因为硬件中断只能在内核上完成, 目前dpdk的实现方式是在内核态igb_uio驱动上实现小部分硬件中断,例如统计硬件中断的次数, 然后唤醒应用层注册到epoll中的/dev/uiox中断,进而由应用层来完成大部分的中断处理过程,例如获取网卡状态等。

        有一个疑问,是不是网卡报文到来时,产生的硬件中断也会到/dev/uiox中断来呢? 肯定是不会的, 因为这个/dev/uiox中断只是控制中断,网卡报文收发的数据中断是不会触发到这里来的。为什么数据中断就不能唤醒epoll事件呢,dpdk是如何区分数据中断与控制中断的? 那是因为在pmd驱动中,调用igb_intr_enable接口开启uio中断功能,设置中断的时候,是可以指定中断掩码的, 例如指定E1000_ICR_LSC网卡状态改变中断掩码,E1000_ICR_RXQ0接收网卡报文中断掩码; E1000_ICR_TXQ0发送网卡报文中断掩码等。 如果某些掩码没指定,就不会触发相应的中断。dpdk的用户态pmd驱动中只指定了E1000_ICR_LSC网卡状态改变中断掩码,因此网卡收发报文中断是被禁用掉了的,只有网卡状态改变才会使得epoll事件触发。因此当有来自网卡的报文时,产生的硬件中断是不会唤醒epoll事件的。这些中断源码在e1000_defines.h文件中定义。

        另一个需要注意的是,igb_uio驱动在注册中断处理回调时,会将中断处理函数设置为igbuio_pci_irqhandler,也就是将正常网卡的硬件中断给拦截了, 这也是用户态驱动pmd能够直接访问网卡的原因。得益于拦截了网卡的中断回调,因此在中断发生时,linux uio框架会唤醒epoll事件,进而应用层能够读取网卡中断事件,或者对网卡进行一些控制操作。拦截硬件中断处理回调只是对网卡的控制操作才有效, 对于pmd用户态驱动轮询网卡报文是没有影响的。也就是说igb_uio驱动不管有没拦截硬件中断回调,都不影响pmd的轮询。 劫持硬件中断回调,只是为了应用层能够响应硬件中断,并对网卡做些控制操作。

三、dpdk uio驱动的实现过程

       先来整体看下igb_uio驱动做了哪些操作。

(1)  针对uio设备本身的操作,例如创建uio设备结构,并注册一个uio设备。此时将会在/dev/目录下创建一个uio文件,例如/dev/uiox。同时也会在/sys/class/uio目录下创建一个uio目录,例如/sys/class/uio/uio1;  并将这个uio目录拷贝到网卡目录下,例如/sys/bus/pci/devices/0000:02:06.0/uio。

(2) 为pci网卡预留物理内存与io空间,同时将这些空间保存到uio设备上,相当于将这些物理空间与io空间暴露给uio设备。应用层访问uio设备就相当于访问网卡设备

(3)  在idb_uio驱动注册硬件中断回调, 驱动层的硬件中断代码越少越好,大部分硬件中断由应用层来实现。

1、igb_uio驱动初始化

        在执行insmod命令加载igb_uio驱动时,会进行uio驱动的初始化操作, 注册一个uio驱动到内核。注册uio驱动的时候,会指定一个驱动操作接口igbuio_pci_driver,其中的probe是在网卡绑定uio驱动的时候 ,uio驱动探测到有网卡进行绑定操作,这个时候probe会被调度执行; 同理当网卡卸载uio驱动时,uio驱动检测到有网卡卸载了,则remove会被调度执行。

        可以看出id_table为空,所以当我们在内核中加载igb_uio.ko的时候,并不会调用probe函数。只有在我们运行dpdk提供的dpdk-devbind.py脚本绑定网卡的时候,probe函数才会被调用。

static struct pci_driver igbuio_pci_driver = 
{
	.name = "igb_uio",
	.id_table = NULL,
	.probe = igbuio_pci_probe,	//为pci设备绑定uio驱动时会被调用
	.remove = igbuio_pci_remove,//为pci设备卸载uio驱动时会被调用
};
//igb_uio驱动初始化
static int __init igbuio_pci_init_module(void)
{
	return pci_register_driver(&igbuio_pci_driver);
}

2、驱动探测probe

        上面已经提到过这个接口被调用的时间,也就是在网卡绑定igb_uio驱动的时候会被调度执行,现在来分析下这个接口的执行过程。需要注意的是,这个接口内部调用了linux uio框架的接口以及调用了一堆linux内核的api接口, 读者在分析这部分代码的时候,关注重点流程就好了,不要被内核的这些接口干扰。

        要将网卡设备和igb_uio驱动关联,有两种方式。

配置设备,让其选择驱动:向 sys/bus/pci/devices/{pci id}/driver_override 写入指定驱动的名称。
配置驱动,让其支持新的 PCI 设备:向 sys/bus/pci/drivers/igb_uio/new_id 写入要 bind 的网卡设备的 PCI ID(式为:设备厂商号 设备号)。
        按照内核的文档 https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci 中提到,这两个动作都会促使驱动程序 bind 新的网卡设备,而 DPDK 使用了第 2 种方式。

2.1 激活pci设备

        在igb_uio驱动能够操作pci网卡之前,需要将pci设备给激活, 唤醒pci设备。在驱动程序可以访问PCI设备的任何设备资源之前(I/O区域或者中断),驱动程序必须调用该函数。也就是说只有激活了pci设备, igb_uio驱动以及应用层调用者,才能够访问pci网卡的内存或者io空间。

int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	//激活PCI设备,在驱动程序可以访问PCI设备的任何设备资源之前(I/O区域或者中断),驱动程序必须调用该函数
	err = pci_enable_device(dev);
}
int pci_enable_device(struct pci_dev *dev)
{
	//使得驱动能够访问pci设备的内存与io空间
	return __pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO);
}

2.2 为pci设备预留内存与io空间

        igb_uio驱动会根据网卡目录下的resource文件, 例如/sys/bus/pci/devices/0000:02:06.0文件记录的io空间的大小,开始位置; 内存空间的大小,开始位置。在内存中为pci设备预留这部分空间。分配好后空间后,这个io与内存空间就被该pci网卡独占,应用层可以通过访问/dev/uiox设备,其实也就是访问网卡的这部分空间。这样处于用户态的pmd驱动程序就可以访问这些原本只能被内核访问的bar空间了。

        dpdk一共使用前6个,分为网卡的内存区域和io区域,通过最后一列来区分。第一列为网卡的物理地址开始位置;第二列为网卡物理地址的结束位置;第三列用于区分该区域是内存区域还是io区域。

         lspci -s 0000:03:00.0 -vv -n可以查看网卡的这两部分区域。

int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	//在内存中申请pci设备的内存区域
	err = pci_request_regions(dev, "igb_uio");
}
int _kc_pci_request_regions(struct pci_dev *dev, char *res_name)
{
	int i;
	//根据网卡目录下的/sys/bus/pci/devices/0000:02:06.0文件记录的网卡内存与io空间的大小,在内存中申请这些空间
	for (i = 0; i < 6; i++) 
	{
		if (pci_resource_flags(dev, i) & IORESOURCE_IO) 
		{
			//在内存中为网卡申请io空间
			request_region(pci_resource_start(dev, i), pci_resource_len(dev, i), res_name);
		}
		else if (pci_resource_flags(dev, i) & IORESOURCE_MEM) 
		{
			//在内存中为网卡申请物理内存空间
			request_mem_region(pci_resource_start(dev, i), pci_resource_len(dev, i), res_name);
		}
	}
}

2.3 为pci网卡设置dma模式

        将网卡设置为dma模式, 用户态pmd驱动就可以轮询的从dma直接接收网卡报文,或者将报文交给dma来发送

int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	//设置dma总线模式,使得pmd驱动可以直接从dma收发报文
	pci_set_master(dev);
	//设置可以访问的地址范围0-64地址空间
	err = pci_set_dma_mask(dev,  DMA_BIT_MASK(64));
	err = pci_set_consistent_dma_mask(dev, DMA_BIT_MASK(64));
}

2.4 将pci网卡的物理空间以及io空间暴露给uio设备

        将pci网卡的物理内存空间以及io空间保存在uio设备结构struct uio_info中的mem成员以及port成员中,uio设备就知道了网卡的物理以及io空间。应用层访问这个uio设备的物理空间以及io空间,就相当于访问pci设备的物理以及io空间。本质上就是将pci网卡的空间暴露给uio设备。

int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	//将pci内存,端口映射给uio设备
	struct rte_uio_pci_dev *udev;
	err = igbuio_setup_bars(dev, &udev->info);
}
static int igbuio_setup_bars(struct pci_dev *dev, struct uio_info *info)
{
	//pci内存,端口映射给uio设备
	for (i = 0; i != sizeof(bar_names) / sizeof(bar_names[0]); i++) 
	{
		if (pci_resource_len(dev, i) != 0 && pci_resource_start(dev, i) != 0) 
		{
			flags = pci_resource_flags(dev, i);
			if (flags & IORESOURCE_MEM) 
			{
				//暴露pci的内存空间给uio设备
				ret = igbuio_pci_setup_iomem(dev, info, iom,  i, bar_names[i]);
			} 
			else if (flags & IORESOURCE_IO) 
			{
				//暴露pci的io空间给uio设备
				ret = igbuio_pci_setup_ioport(dev, info, iop,  i, bar_names[i]);
			}
		}
	}
}

         将pci设备的物理内存空间以及io空间保存在uio设备结构struct uio_info中的mem成员以及port成员中,之后在下面调用uio_register_device注册一个uio设备时。内部就将mem以及port成员保存的信息分别保存到/sys/class/uio/uiox目录下的maps以及portio, 这样应用层访问这两个目录里面文件记录的内容,就可以访问的pci设备的物理以及地址空间,真正的暴露给应用层。

        可以简单查看内核的源码,__uio_register_device会调用uio_dev_add_attributes接口来完成将网卡的物理内存空间以及io空间保存到文件中

static int uio_dev_add_attributes(struct uio_device *idev)
{
	for (mi = 0; mi < MAX_UIO_MAPS; mi++) 
	{
		//将pci物理内存保存到/sys/class/uio/uio1/maps目录下
		mem = &idev->info->mem[mi];
		idev->map_dir = kobject_create_and_add("maps", &idev->dev->kobj);
	}
	for (pi = 0; pi < MAX_UIO_PORT_REGIONS; pi++) 
	{
		//将pci设备io空间保存到/sys/class/uio/uio1/portio目录下
		port = &idev->info->port[pi];
		idev->portio_dir = kobject_create_and_add("portio", &idev->dev->kobj);
	}
}

        然而dpdk映射网卡 pci resource 地址时,并不通过mmap映射/sys/class/uio/目录下的uio设备暴露出来的地址空间,而是通过访问每个pci设备在/sys目录树下生成的resource文件获取pci内存资源信息,然后依次mmap每个pci内存资源对应的resourceX文件,这里执行的 mmap 将 resource 文件中的物理地址映射为用户态程序中的虚拟地址。实际上这两种方式都是可以的。

2.5 设置uio设备的中断

  • 应用层开启/关闭网卡硬件中断

        应用层如何开启或者关闭网卡的硬件中断呢?应用层在uio_intr_enable函数中通过write系统调用,往/dev/uio设备写入1来开启硬件中断,写入0来关闭网卡硬件中断的。

static int uio_intr_enable(const struct rte_intr_handle *intr_handle)
{
	const int value = 1;
	write(intr_handle->fd, &value, sizeof(value));
}

        write系统调用后,进而调用的是uio_write;uio_write然后调用igb_uio驱动中注册的igbuio_pci_irqcontrol

static ssize_t uio_write(struct file *filep, const char __user *buf,
			size_t count, loff_t *ppos)
{
	idev->info->irqcontrol(idev->info, irq_on);
}
  • igb_uio驱动中硬件中断的处理  

        注册uio设备的中断回调,也就是上面提到的拦截硬件中断回调。拦截硬件中断后,当硬件中断触发时,就不会一直触发内核去执行中断回调。也就是通过这种方式,才能在应用层实现硬件中断处理过程。再次注意下,这里说的中断仅是控制中断,而不是报文收发的数据中断,数据中断是不会走到这里来的,因为在pmd开启中断时,没有设置收发报文的中断掩码,只注册了网卡状态改变的中断掩码。

int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
	//填充uio信息
	udev->info.name = "igb_uio";
	udev->info.version = "0.1";
	udev->info.handler = igbuio_pci_irqhandler;		//硬件控制中断的入口,劫持原来的硬件中断
	udev->info.irqcontrol = igbuio_pci_irqcontrol;	//应用层开关中断时被调用,用于是否开始中断
}
static irqreturn_t igbuio_pci_irqhandler(int irq, struct uio_info *info)
{
	if (udev->mode == RTE_INTR_MODE_LEGACY && !pci_check_and_mask_intx(udev->pdev))
	{
		return IRQ_NONE;
	}
	//返回IRQ_HANDLED时,linux uio框架会唤醒等待uio中断的进程。注册到epoll的uio中断事件就会被调度
	/* Message signal mode, no share IRQ and automasked */
	return IRQ_HANDLED;
}
static int igbuio_pci_irqcontrol(struct uio_info *info, s32 irq_state)
{
	//调用内核的api来开关中断
	if (udev->mode == RTE_INTR_MODE_LEGACY)
	{
		pci_intx(pdev, !!irq_state);
	}
	else if (udev->mode == RTE_INTR_MODE_MSIX)\
	{
		list_for_each_entry(desc, &pdev->msi_list, list)
			igbuio_msix_mask_irq(desc, irq_state);
	}
}

        在下面调用uio_register_device注册uio设备的时候,会注册一个linux uio框架下的硬件中断入口回调uio_interrupt。这个回调里面会调用igb_uio驱动注册的硬件中断回调igbuio_pci_irqhandler。通常igbuio_pci_irqhandler直接返回IRQ_HANDLED,因此会唤醒阻塞在uio设备中断的进程,应用层注册到epoll的uio中断事件就会被调度,例如e1000用户态pmd驱动在eth_igb_dev_init函数中注册的uio设备中断处理函数eth_igb_interrupt_handler就会被调度执行,来获取网卡的状态信息。

        总结下中断调度流程:linux uio硬件中断回调被触发  ----> igb_uio驱动的硬件中断回调被调度 ----> 唤醒用户态注册的uio中断回调。

int __uio_register_device(struct module *owner,struct device *parent, struct uio_info *info)
{
	//注册uio框架的硬件中断入口
	ret = request_irq(idev->info->irq, uio_interrupt,
				  idev->info->irq_flags, idev->info->name, idev);
}
static irqreturn_t uio_interrupt(int irq, void *dev_id)
{
	struct uio_device *idev = (struct uio_device *)dev_id;
	//调度igb_uio驱动注册的中断回调
	irqreturn_t ret = idev->info->handler(irq, idev->info);
	//唤醒所有阻塞在uio设备中断的进程,注册到epoll的uio中断事件就会被调度
	if (ret == IRQ_HANDLED)
		uio_event_notify(idev->info);
}
  •  dpdk应用层响应中断

        dpdk 单独创建了一个中断线程负责监听并处理硬件中断事件,其主要过程如下:创建 epoll_event;遍历中断源列表,添加每一个需要监听的 uio 设备事件的 uio 文件描述符到 epoll_event 中;调用 epoll_wait 监听事件,监听到事件后调用 eal_intr_process_interrupts 调用相关的中断回调函数。

2.6 uio设备的注册

        最后执行uio设备的注册,在/dev/目录下创建uio文件,例如/dev/uio1; 同时也会在/sys/class/uio目录下创建一个uio目录,例如/sys/class/uio/uio1;  最后将/sys/class/uio/uio1目录下的内容拷贝到网卡目录下,例如/sys/bus/pci/devices/0000:02:06.0/uio

        另外还会执行上面提到过的,将uio设备保存的网卡的内存空间,io空间保存到文件中,以便应用层能够访问这个网卡空间。同时也会注册一个linux uio框架下的网卡硬件中断。

int __uio_register_device(struct module *owner,struct device *parent, struct uio_info *info)
{
	//创建uio设备/dev/uiox
	idev->dev = device_create(uio_class->class, parent,
				  MKDEV(uio_major, idev->minor), idev, "uio%d", idev->minor);
	//将uio设备保存的网卡的内存空间,io空间保存到文件中
	ret = uio_dev_add_attributes(idev);
	//注册uio框架的硬件中断入口
	ret = request_irq(idev->info->irq, uio_interrupt,
				  idev->info->irq_flags, idev->info->name, idev);
}

        到此为止uio设备驱动与pmd的关系也就分析完成了。接下来的文章将详细分析用户态驱动pmd是如何收发网卡报文的。

四、参考文章:

DPDK 系列(6)IGB_UIO 实现解析 - 墨天轮

https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值