ZBLOG

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设备的物理以及地址空间,真正的暴露给应用层。

可以简单查看内核的源码,_uioregister_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

转载自: 作者ApeLife

本文链接:https://blog.csdn.net/ApeLife/article/details/100751359

本站部分文章来源于网络,版权归原作者所有,如有侵权请联系站长删除。
转载请注明出处:https://sdn.0voice.com/?id=897

分享:
扫描分享到社交APP
上一篇
下一篇
发表列表
游客 游客
此处应有掌声~
评论列表

还没有评论,快来说点什么吧~

联系我们

在线咨询: 点击这里给我发消息

微信号:3007537140

上班时间: 10:30-22:30

关注我们
x

注册

已经有帐号?