一、dpdk 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框架提供的接口。
二、用户态驱动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会被调度执行。
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接口, 读者在分析这部分代码的时候,关注重点流程就好了,不要被内核的这些接口干扰。
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设备,其实也就是访问网卡的这部分空间。
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);
}
}
2.5 设置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);
}
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是如何收发网卡报文的。