zoukankan      html  css  js  c++  java
  • dpdk UIO 分析

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

    而且这些驱动进不了主内核源码。于是就出现了用户空间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 框架以及内核内部函数的关系

    /**
     * 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

    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 __init
    igbuio_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
    #else
    static int
    #endif
    igbuio_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 int
    igbuio_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 int
    igbuio_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设备

    uio_register_device的流程主要是做了4件事:

    • dev_set_name : 给设备设置名称,uio0...N,为/dev/uio0..N
    • device_register : 注册设备
    • uio_dev_add_attribute : 主要是创建一些设备属性,从表现形式来看是在/sys/class/uio/uio0/目录中创建maps目录,里面包含的主要也是和resource文件一致,
      •   就是pci设备经过uio驱动接受以后再把resource资源通过文件系统暴露给用户态而已

       igb_uio的初始化以及注册过程都已经完成了,最终表现形式便是在/dev/uio创建了一个uio设备,这个设备是用来衔接内核态的中断信号与用户态应用

    • struct resource : 内核将PCI BAR的信息存储在这个数据结果中,可以理解为PCI BAR的抽象,可以理解这个resource结构体就对应了/sys/bus/pci/devices/[pci_addr]/resource文件
      1. start : PCI BAR空间起始地址(这里不一定是内存空间还是IO空间);
      2. end : PCI BAR空间的结束地址;
      3. name : PCI BAR的名字,例如BAR 0、BAR1、BAR2....BAR5;
      4. flags : PCI BAR的标识,如果flags & 0x00000200则为内存空间,如果flags & 0x00000100则为IO空间;
      5. desc : IO资源描述符
    • struct pci_dev : pci设备的抽象,可以理解为一个struct pci_dev就代表一个pci设备
      1. vendor : 生产商id,intel为0x0806,见/sys/bus/pci/devices/[pci_addr]/vendor文件;
      2. device : 设备id;
      3. subsystem_vendor : 子系统生产商id;
      4. subsystem_device : 子系统设备id;
      5. driver : 当前PCI设备所用驱动;
      6. resource : 当前pci设备的pci bar资源;
    • struct rte_uio_pci_dev : igb_uio的抽象,可以理解为igb_uio本身
      1. info : 用于关联uio信息;
      2. pdev : 用于关联pci设备;
      3. mode : 中断模式配置
    • struct uio_info : uio 信息配置的抽象
      1. uio_dev : 用来指向所属于的uio设备实例;
      2. name : 这个uio设备的名字,例如/dev/uio0,/dev/uio1,/dev/uio2;
      3. mem : 同样是PCI BAR资源,不过这里是已经做了区分,特指Memory BAR,这里的值仍然来自于内核的resource结构体,不过这里往往是将内核resource结构体映射后的值,可以理解为原始数据“加工”后的值;
      4. port : 同样是PCI BAR资源,不过这里是已经做了区分,特质Port BAR,这里的值仍然来自于内核的resource结构体,不过这里往往是将内核resource结构体映射后的值,可以理解为原始数据“加工”后的值;
      5. irq : 中断号;
      6. irq_flags : 中断标识;
      7. priv : 一个回调指针,指向dpdk的igb_uio驱动实例,其实这个字段的设计并不是为了专门服务于dpdk的igb_uio;
      8. handler、mmap、open、release、irqcontrol:分别为几个函数钩子,例如对/dev/uio进行open操作后,最终就会通过uio的file_operations -> open调用到igbuio_pci_open中,可以理解为open操作的内部实现;
    • struct uio_device : uio设备的抽象,其实例可以代表一个uio设备
      1. 这里的内容不多加介绍,因为关于一个uio设备的主要配置和信息都在uio_info结构中
    • struct uio_mem : 经过对resource进行处理后的Memory BAR信息,这里的信息主要是指的对PCI BAR进行ioremap
      1. name : PCI Memory BAR的名字,例如BAR 0、BAR1、BAR2....BAR5;
      2. addr : PCI Memory BAR的起始地址,为物理地址,这个地址必须经过ioremap映射后才可以给内核空间使用;
      3. offs : 偏移,一般为0;
      4. size : PCI Memory BAR的大小,通常可以用resource文件中的第二列(PCI BAR的终止地址)和resource文件中的第一列(PCI BAR的起始地址) + 1计算得出;
      5. memtype : 这个Memory Bar的内存类型,可以选择为物理地址、逻辑地址、虚拟地址三种类型,在DPDK的igb_uio中赋值为物理地址;
      6. internal_addr : 这个是一个关键,这个值即为PCI Memory BAR起始地址经过ioremap映射后得到的可以在内核空间直接访问的虚拟地址,当然之前也描述过,这个地址对于uio这种设计理念的设备而言是不需要的;

      uio内部其实是拿得到PCI BAR资源的,那么该怎么将这个BAR资源给用户态应用使用呢?答案是,就是对/dev/uio0..N这个设备调用mmap进行内存映射,调用mmap之后,将会转到内核态事先注册好的file_operations.mmap钩子函数上,也就是调用uio_mmap

     

      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
    2. 拿到当前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的对上接口即可;
  • 相关阅读:
    angularjs制作的iframe后台管理页切换页面
    javascript读取本地文件
    nginx Engine X静态网页服务器介绍
    关于 bounds 和 frame
    iOS 开发常见函数
    HTTP POST GET 本质区别详解(转)
    从 UIAlertView 到 UIAlertController
    学习 AFNetworking 3.0
    UICollectionView详解
    UITableView整理
  • 原文地址:https://www.cnblogs.com/codestack/p/15673333.html
Copyright © 2011-2022 走看看