本文共 6056 字,大约阅读时间需要 20 分钟。
关于dpdk的驱动层,一直以来都没有理的很清楚。一是因为本人不是开发驱动的,对驱动知识相当匮乏,二来用dpdk来开发,貌似也不需要过多关注底层驱动逻辑。但是这块不懂的话,总会感觉对dpdk一知半解的,不踏实。所以这篇博客就是通过查阅资料和阅读源码总结出来的,如有理解错误的地方还望各位指正。因为uio是对IO设备而言的,因此本博客中的设备指的是IO设备。
大家都知道,linux操作系统分为两个层级,一个是内核态,一个是用户态。平时编写的软件都是运行在用户态。以一个简单的udp socket通信举例,client1准备向client2发送数据,用户态的程序首先要将数据准备好,然后需要将数据传给网卡设备。但是这个时候问题就来了,我们如何将数据传给网卡设备呢?写过socket的知道,要先创建一个socket句柄,然后bind绑定ip+port,最后再sendto发送数据。这里面的bind和sendto就是内核给我们提供是api,调用这些函数就进行了一次系统调用。系统调用可以简单的理解成一次软中断,内核收到这个中断之后,会执行相应的动作,最终会调用到网卡驱动提供的send等函数。最后将结果返回给用户态的进程。client2如果想接收到client1发送过来的数据,也要进行bind和recvfrom。当网卡收到数据后,产生一次中断,通知内核将网卡的数据转到用户态。这个过程中还包含了内核检查报文头部,判断这些数据是否为client2希望收到的数据。
系统调用是很耗费cpu性能的,这也就是为什么I/O密集型任务并不适合传统linux架构的原因。幸运的是,一些前辈大牛搞出了uio机制,让驱动大部分功能运行在用户态,只有一小部分运行在内核态,比如中断等。值得注意的是,仅仅是uio驱动还不能实现网卡的收发包,因为这个驱动并没有提供网卡的配置函数和收发包函数。uio驱动的作用是让你在用户态就可以操作网卡设备的内存。dpdk同时还提供了pmd用户态驱动,用户态pmd驱动就是通过uio机制,通过操作网卡的寄存器实现在用户态收发报文。关于用户态pmd驱动,下一篇文章再叙述。
下面贴一个几乎每个讲解uio机制的文章都会有的一张图:
注册到uio驱动上的设备,uio驱动会在/sys/class/uio目录下生成相对应的文件夹来记录设备的一些信息,同时会在/dev目录下生成相对应的设备文件。这样用户态就可以通过/sys/class/uio/uio0/maps/map0和/sys/class/uio/uio0/portio/port0来访问这个uio设备。注意/sys/class/uio/uio0/maps/map0下的各个文件,addr文件对应的就是该设备内存的物理地址。后续开发对应网卡的驱动,会读取这个地址,然后通过offset文件的值,获取到设备寄存器的地址。关于这个地址是如何产生的,有兴趣的读者可以参考这篇文章。关于如何写一个简单的uio驱动,可以参考这篇文章。
dpdk自己实现了一个uio驱动,名称叫igb_uio驱动。源码路径在lib\librte_eal\linuxapp\igb_uio\igb_uio.c。
定义一个struct pci_deiver的结构体。
static struct pci_driver igbuio_pci_driver = { .name = "igb_uio", .id_table = NULL, .probe = igbuio_pci_probe, .remove = igbuio_pci_remove,};
简单的说明下struct pci_deiver这个结构体。在linux系统中,每个pci驱动都有一个pci_driver实例,用以描述驱动名称,支持的设备信息,以及对应的操作函数;
/* 描述一个pci设备,每个pci驱动必须创建一个pci_driver实例*/struct pci_driver { struct list_head node; const char *name; /* 驱动程序名,内核中所有pci驱动程序名都是唯一的 */ const struct pci_device_id *id_table; /* must be non-NULL for probe to be called pci设备配置信息数组 */ int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted 设备插入内核时调用 */ void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) 设备从内核移除时调用 */*/ int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */ int (*suspend_late) (struct pci_dev *dev, pm_message_t state); int (*resume_early) (struct pci_dev *dev); int (*resume) (struct pci_dev *dev); /* Device woken up */ void (*shutdown) (struct pci_dev *dev); int (*sriov_configure) (struct pci_dev *dev, int num_vfs); /* PF pdev */ const struct pci_error_handlers *err_handler; struct device_driver driver; struct pci_dynids dynids;};
在内核中注册一个pci驱动,要调用内核提供的api:pci_register_driver,入参就是struct pci_driver。当有设备绑定到这个pci驱动且设备的struct pci_device_id在id_table里的时候,内核就会调用probe函数。 igb_uio驱动中,id_table设置为空,所以当我们在内核中加载igb_uio.ko的时候,并不会调用probe函数。只有在我们运行dpdk提供的dpdk-devbind.py脚本绑定网卡的时候,probe函数才会被调用。
static int __initigbuio_pci_init_module(void){ int ret; ret = igbuio_config_intr_mode(intr_mode); if (ret < 0) return ret; return pci_register_driver(&igbuio_pci_driver);}static void __exitigbuio_pci_exit_module(void){ pci_unregister_driver(&igbuio_pci_driver);}module_init(igbuio_pci_init_module);module_exit(igbuio_pci_exit_module);
module_init是注册模块初始化函数,模块加载时会执行这个函数。pci_register_driver就是向内核注册一个pci驱动,名称是struct pci_driver指定的,这里为igb_uio,probe函数为igbuio_pci_probe。
我们来关注下igbuio_pci_probe这个函数。
static int __devinit#elsestatic int#endifigbuio_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; udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL); if (!udev) return -ENOMEM; mutex_init(&udev->lock); /* * enable device: ask low-level code to enable I/O and * memory */ 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; 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); /* 注册完成之后,可以发现在/dev目录下出现了uioX,在/sys/class/uio目录下出现了uioX文件夹。 * 用户态进程可以通过读取/sys/class/uio/uioX/maps/map0目录下的文件来操作设备*/ 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;}
函数执行完成之后,设备就被绑定到igb_uio驱动上了,后续就是通过pmd驱动来对网卡进行配置、报文读取等。
有关dpdk uio机制其实已经解读完成,心中还有一个疑惑,就是关于如何理解外设的物理地址。这篇文章做了一个比较详细的说明,在此mark一下,学习学习~
转载地址:http://jyali.baihongyu.com/