关于dpdk的驱动层,一直以来都没有理的很清楚。一是因为本人不是开发驱动的,对驱动知识相当匮乏,二来用dpdk来开发,貌似也不需要过多关注底层驱动逻辑。但是这块不懂的话,总会感觉对dpdk一知半解的,不踏实。所以这篇博客就是通过查阅资料和阅读源码总结出来的,如有理解错误的地方还望各位指正。因为uio是对IO设备而言的,因此本博客中的设备指的是IO设备。
1.linux设备驱动uio机制
大家都知道,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文件的值,获取到设备寄存器的地址。关于这个地址是如何产生的,有兴趣的读者可以参考https://blog.csdn.net/weixin_44936219/article/details/102715837这篇文章。关于如何写一个简单的uio驱动,可以参考https://www.cnblogs.com/allcloud/p/7808776.html这篇文章。
2.dpdk igb_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 __init
igbuio_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 __exit
igbuio_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
#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;
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驱动来对网卡进行配置、报文读取等。
3.关于外设的物理地址
有关dpdk uio机制其实已经解读完成,心中还有一个疑惑,就是关于如何理解外设的物理地址。https://blog.csdn.net/baidu_37973494/article/details/82389577这篇文章做了一个比较详细的说明,在此mark一下,学习学习~