DPDK网卡驱动流程总结
本文基于DPDK-16.07.2、Linux 4.4.2分析总结1 简介1.1 什么是UIO技术UIO(Userspace I/O)是运行在用户空间的I/O技术,Linux系统中一般的驱动设备都是运行在内核空间,而在用户空间用应用程序调用即可,而UIO则是将驱动的很少一部分运行在内核空间,而在用户空间实现驱动的绝大多数功能,使用UIO可以避免设备的驱动程序需要随着内核的更新而更新的问题。1.2 为
本文基于DPDK-16.07.2、Linux 4.4.2分析总结
1 简介
1.1 什么是UIO技术
UIO(Userspace I/O)是运行在用户空间的I/O技术,Linux系统中一般的驱动设备都是运行在内核空间,而在用户空间用应用程序调用即可,而UIO则是将驱动的很少一部分运行在内核空间,而在用户空间实现驱动的绝大多数功能,使用UIO可以避免设备的驱动程序需要随着内核的更新而更新的问题。
1.2 为什么出现了UIO
硬件设备可以根据功能分为网络设备,块设备,字符设备,或者根据与CPU相连的方式分为PCI设备,USB设备等。它们被不同的内核子系统支持。这些标准的设备的驱动编写较为容易而且容易维护。很容易加入主内核源码树。但是,又有很多设备难以划分到这些子系统中,比如I/O卡,现场总线接口或者定制的FPGA。通常这些非标准设备的驱动被实现为字符驱动。这些驱动使用了很多内核内部函数和宏。而这些内部函数和宏是变化的。这样驱动的编写者必须编写一个完全的内核驱动,而且一直维护这些代码。而且这些驱动进不了主内核源码。于是就出现了用户空间I/O框架(Userspace I/O framework)。
1.3 UIO是怎么工作的
一个设备驱动的主要任务有两个:
- 存取设备的内存
- 处理设备产生的中断
对于第一个任务,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,对该文件的读写就是对设备内存的读写。
1.4 UIO框架分层
uio代码相关的可以分为三个部分:内核uio框架及内核内部函数,uio内核驱动部分,uio用户驱动部分。
1)内核uio框架通过配置内核选项CONFIG_UIO=y使能Userspace I/O drivers,在内核初始化时会调用uio_init创建uio_class;
2)igb_uio内核驱动通过编译运行igb_uio.ko加载并注册一个pci设备,但是igbuio_pci_driver对应的保存pci设备信息的id_table指针为空,这样在内核注册此pci设备时,会找不到匹配的设备,就不会调用igb_uio驱动中的探测probe函数uio用户态驱动,在运行dpdk提供的Python脚本dpdk-devbind.py绑定网卡设备后才会执行其probe函数;
3)uio用户态驱动则是在dpdk实例程序(如l2fwd)初始化EAL环境抽象层时才会进行驱动与设备匹配加载。
从图中可以看出,用户空间下的驱动程序比运行在内核空间的驱动要多得多,UIO框架下运行在内核空间的驱动程序所做的工作很简单,常做的只有两个:分配和记录设备需要的资源和注册uio设备,和必须在内核空间实现的小部分中断应答函数。
2 内核UIO框架总结
2.1 UIO的内核使能及初始化
配置内核UIO相关选项使能,内核编译相关代码并在内核启动时初始化UIO内核核心框架;
执行流程:申请字符设备号(alloc_chrdev_region),设备(cdev_alloc),并注册到系统中(cdev_add),注册uio_class到系统中(class_register),创建“/sys/class/uio”,此时该目录为空,在insmod igb_uio.ko后且运行python脚本绑定网卡后此目录下才有内容,见3.2.2。
2.2 UIO重要数据结构
uio核心部分是一个名为"uio"的字符设备(下文称为“uio核心字符设备“)。用户驱动的内核部分(igb_uio.ko)使用uio_register_device向uio核心部分注册uio设备。uio核心的任务就是管理好这些注册的uio设备。这些uio设备使用的数据结构是uio_device。而这些设备属性,比如name, open(), release()等操作都放在了uio_info结构中,用户使用 uio_register_device注册这些驱动之前要设置好uio_info。
uio核心字符设备注册duio_open ,uio_fasync, uio_release, uio_poll, uio_read ,uio_write中除了完成相关的维护工作外,还调用了注册在uio_info中的相关方法。比如,在 uio_open中调用了uio_info中注册的open方法。
在uio_init()—init_uio_class()—uio_major_init()中完成uio核心字符设备操作函数挂载(cdev->ops = &uio_fops;)
uio核心和uio设备之间的关系图如下:
2.3 UIO设备注册接口
在用户驱动的内核部分igbuio_pci_probe()调用uio_register_device()(内部调用了__uio_register_device)注册uio设备时,在__uio_register_device中调用了uio_get_minor函数,在uio_get_minor函数中,利用idr机制(idr_get_new)建立了次设备号(整数ID)和uio_device类型指针之间的联系。而uio_device指针指向了代表注册的uio设备的内核结构。device_create()调用完毕后在 /sys/class/uio/下就会出现代表uio设备的uioX文件夹,其中X为uio设备的次设备号。
执行流程:申请uio_device指针(devm_kzalloc),初始化设备等待队列(init_waitqueue_head),清空中断事件计数器(&idev->event, 0),映射次设备号(uio_get_minor),创建设备并关联uio_class(device_create),初始化设备中断(uio_interrupt);
2.4 UIO设备开启接口
在uio_open中先取得了设备的次设备号(iminor(inode)),再次利用idr机制提供的方法(idr_find)取得了对应的uio_device类型的指针。并且把该指针保存在了uio_listener结构中,以方便以后使用。
执行流程:根据次设备号获取设备指针(idr_find),分配并初始化uio_listener结构,调用内部open函数。
2.4 UIO设备中断注册
在__uio_register_device中,为uio设备注册了统一的中断处理函数uio_interrupt,在该函数中调用了uio设备自己提供的中断处理函数handler(uio_info结构中)。并调用了uio_event_notify函数对uio设备的中断事件计数器增一, 通知各个读进程“有数据可读”。每个uio设备的硬件中断处理函数都是单独注册的。
对于每一个注册的uio设备(uio_device)都关联一个这样的结构。它的作用就是跟踪每个uio设备(uio_device)的中断事件计数器值。在用户空间进行文件打开操作(open)时,与uio设备关联的uio_listener结构就被分配(uio_open中),指向它的指针被保存在filep指针的private_data字段以供其他操作使用。
uio_poll 操作判断是否有数据可读的依据就是 listener中的中断事件计数值(event_count)和uio设备中的中断事件计数器值不一致(前者小于后者)。因为listener的值除了在执行文件打开操作时被置为被赋值外,只在uio_read操作中被更新为uio设备的中断事件计数器值。
如果用户空间要等待一个设备中断,它只需要简单的阻塞在对 /dev/uioX的read()操作上(对应内核uio_read)。当设备产生中断时,read()操作立即返回。UIO也实现了poll()系统调用(对应内核uio_poll),可以使用 select()来等待中断的发生。select()有一个超时参数可以用来实现有限时间内等待中断。
3 UIO内核驱动部分总结
3.1 UIO内核驱动编译
DPDK在编译生成igb_uio.ko时需要依赖于内核开发包kernel-devel,主要是调用内核接口函数(pci_enable_device、uio_register_device等)和头文件等,如此更近一步说明此部分驱动属于内核态驱动,实际上就是调用UIO内核核心框架的接口函数;编译时确认下ls /usr/src/kernels/是否有软件包,有软链接/lib/modules/如下:
编译和运行平台不一致时insmod igb_uio.ko时会出现报错,可将运行平台内核源码拷贝到/usr/src/kernels/2.6.32-573.el6.x86_64/重新编译运行。
3.2 UIO内核驱动加载
3.2.1 Insmod igb_uio.ko执行
Igb_uio驱动主要做的就是注册一个pci设备。但是igbuio_pci_driver对应的保存pci设备信息的id_table指针为空,这样在内核注册此pci设备时,会找不到匹配的设备,就不会调用igb_uio驱动中的探测probe函数(对应igb_uio的igbuio_pci_probe()不会被调用到),只会在/sys/bus/pci/drivers/目录下创建Igb_uio相应的目录。
dpdk的uio实现也是按模块加载先赋值intr_mode中断模式,然后初始化一个pci_driver结构体,在igbuio_pci_init_module函数中直接调用linux提供的pci注册API接口函数pci_register_driver(&igbuio_pci_driver);
3.2.2 Python脚本dpdk-devbind.py执行
dpdk提供了一个Python脚本dpdk-devbind.py,可以查看、绑定网卡到igb_uio模块,由于当前防火墙没有python解释器,所以需要单独交叉编译,方法参见《防火墙交叉编译python解释器的方法.txt》;
1)使用方法如下:
2)使用Python脚本dpdk-devbind.py -s查看当前设备状态,当前设备有四块网卡,有一块网卡已被内核驱动,另外三块网卡未驱动,如下:
3)使用Python脚本dpdk-devbind.py --bind=igb_uio eth0/0000:04:00.1
将eth0网卡绑定到igb_uio模块,已驱动的网卡会先卸载驱动(ifconfig eth0消失了),然后重新bind到igb_uio模块。
这时dmesg就会看到igb_uio模块的probe函数执行了(id_table不为空了),也就是意味着扫描到了匹配的pci设备,同时生成/dev/uioX设备(X为次设备号),此时/sys/class/uio/目录下已产生于/dev/uioX设备对应的内容。
3.2.3 Python脚本dpdk-devbind.py解析
经过分析dpdk_nic_bind.py,此脚本文件主要做了以下几步:
1)获取参数指定的网卡eth0的设备信息。使用lspci –Dvmmn查看。
Slot: 0000:06:00.1
Class: 0200
Vendor: 8086
Device: 1521
SVendor: 15d9
SDevice: 1521
Rev: 01
可以查看到slot槽位信息、厂商号vendor ID、设备号device ID等信息。
脚本相关内容如下:
2)unbind之前的igb模块。
将前面获取到的eth0对应的slot信息0000:04:00.0值写入igb的unbind文件,卸载内核驱动。
从内核代码分析此unbind的动作就是将igb模块信息和此pci设备Dev去关联。将dev->driver指针置为空,这个很重要。在内核处理pci设备注册的函数中,就算驱动的vendor ID和device ID与设备的都匹配上了,如果此设备的dev->driver指针不为空,也不会调用probe函数的。
3)bind新的igb_uio模块
将eth0设备的vendor和device ID信息写入igb_uio的new_id文件。
内核中处理此步的函数为store_new_id,此函数中是将写入的vendor和device存入到此driver,也就是igb_uio的id_table,然后以此与PCI上的设备进行匹配,这个时候肯定会匹配成功,然后调用igb_uio模块的probe函数(igbuio_pci_probe)进行初始化动作。
3.2.4 igbuio_pci_probe执行
UIO的内核驱动注册与其他驱动类似,通过调用linux提供的uio API接口进行注册,在注册之前所做的主要工作如下:
1)分配一个DPDK封装的UIO设备数据结构,包括了uio_info:
2)使能PCI设备及设置掩码:
3)填充uio_info结构体的信息,主要包括内存大小、类型等信息的填充:
4)映射UIO设备PCI资源空间(PCI设备的物理地址及大小),调用函数为igbuio_setup_bars()—igbuio_setup_bars()—igbuio_pci_setup_iomem(),并且填充uio_info结构体的内存信息:
5)根据填充uio_info结构体的信息注册UIO设备:
填充uio_info结构体的信息,主要包括内存大小、类型、私有结构和中断处理函数等信息。调用UIO内核核心框架的接口函数__uio_register_device()(见2.3 UIO设备注册接口),将uio_info注册到内核中。注册后在/sys/class/uio/uioX,其中X是我们注册的第几个uio设备,比如uio0,在该文件夹下的map/map0会有我们刚才填充的一些信息,包括addr、name、size、offset,其中addr保存的是设备的物理地址,size保存的是地址的大小,这些在用户态会将其读出,并mmap至用户态进程空间,这样用户态便可直接操作设备的内存空间。
6)最后就是注册中断了,初始化中断的中断号、中断模式、中断标志等,并初始化uio_info的handler字段,那么在产生中断时,注册的中断处理函数将会被调用。如果有实际的硬件设备,那么irq应该是硬件设备实际使用的中断号。
这里再看下UIO内核核心框架中注册的中断处理函数uio_interrupt(对应2.4 UIO设备中断注册)。
此函数首先调用igb_uio驱动中设置的中断处理函数igbuio_pci_irqhandler来检查中断是不是此设备的中断,如果是就返回IRQ_HANDLED表示需要处理,接着调用函数uio_event_notify来唤醒等待队列wait上进程来处理中断事宜。
4 UIO用户驱动加载
网卡驱动模型一般包含三层,即PCI总线设备、网卡设备以及网卡设备的私有数据结构,即将设备的共性一层层的抽象,PCI总线设备包含网卡设备,网卡设备又包含其私有数据结构。在DPDK中,首先会注册设备驱动,然后查找当前系统有哪些PCI设备,并通过PCI_ID为PCI设备找到对应的驱动,最后调用驱动初始化设备。
4.1网卡驱动注册
以e1000网卡驱动为例说明。
网卡驱动的注册使用了一种奇技淫巧的方法,使用GCC attribute扩展属性的constructor属性,使得网卡驱动的注册在程序MAIN函数之前就执行了。
其中PMD_REGISTER_DRIVER()宏的定义如下:
使用attribute的constructor属性,在MAIN函数执行前(4.3 初始化PCI驱动时需要遍历使用),就执行rte_eal_driver_register()函数,将pmd_igb_drv驱动挂到全局dev_driver_list链表上,其他的网卡驱动也是使用相同的方式挂到该链表上;
4.2扫描当前系统PCI设备
以为l2fwd的main.c为例,main()调用rte_eal_init()—>rte_eal_pci_init()函数,查找当前系统中有哪些网卡,分别是什么类型,并将它们挂到全局链表pci_device_list上。
1)首先初始化全局链表pci_driver_list、pci_device_list。用于挂载PCI驱动及PCI设备。
2)pci_scan()通过读取/sys/bus/pci/devices/目录下的信息,扫描当前系统的PCI设备,并初始化struct rte_pci_device数据结构,并按照PCI地址从大到小的顺序挂在到pci_device_list链表上,调用关系如下:
Main()----rte_eal_init()----rte_eal_pci_init()----rte_eal_pci_scan()----
pci_scan_one()
----parse_pci_addr_format()----获取并赋值bus、dev、func等信息
eal_parse_sysfs_value()----获取并赋值vendor id、device id 等信息
pci_parse_sysfs_resource()----获取phys_addr、end_addr、len等信息
在/sys/bus/pci/devices/下显示的PCI设备信息和lspci相同;
vendor文件:获取PCI_ID.vendor_id。
device文件:获取PCI_ID.device_id。
subsystem_vendor文件:获取PCI_ID.subsystem_vendor_id。
subsystem_device文件:获取PCI_ID.subsystem_device_id。
numa_node文件:获取PCI设备属于哪个CPU socket。
resource文件:获取PCI设备的在地址总线上的物理地址,以及物理地址空间的大小,记录在struct rte_pci_resouce数据结构中。
4.3 PCI驱动注册
调用rte_eal_init()—>rte_eal_dev_init()函数,遍历dev_driver_list链表,执行网卡驱动对应的init的回调函数,注册PCI驱动。
这里就用到了之前初始化的dev_driver_list链表:使用attribute的constructor属性,在MAIN函数执行前,就执行rte_eal_driver_register()函数,将pmd_igb_drv驱动挂到全局dev_driver_list链表上。其他的网卡驱动也是使用相同的方式挂到该链表上,此处遍历链表回调各自的init的函数;
以e1000网卡为例,执行的init回调函数就是rte_igb_pmd_init()函数。
rte_eth_driver_register()主要是指定PCI设备的初始化钩子函数devinit,以及注册PCI驱动,将PCI驱动挂到pci_driver_list全局链表上。
其中,rte_igb_pmd数据结构如下,主要是初始化name、id_table等,指定e1000网卡的初始化钩子函数是eth_igb_dev_init()。
其中的pci_id_igb_map保存的是设备厂商的id和设备id;会在后面的驱动和设备匹配时用到;这是e1000网卡的驱动,所以它的私有数据.dev_private_size = sizeof(struct e1000_adapter),保存的是千兆的私有结构体信息;
4.4网卡初始化
调用rte_eal_init()—>rte_eal_pci_probe()函数,遍历pci_device_list和pci_driver_list链表,根据PCI_ID,将pci_device与pci_driver绑定,并调用pci_driver的init回调函数rte_eth_dev_init(),初始化PCI设备。
遍历pci_device_list链表,其中再嵌套遍历pci_driver_list链表;
在rte_eal_pci_probe_one_driver()函数中,判断driver与device是否匹配,若匹配则回调驱动的devinit钩子函数初始化网卡设备;
4.4.1 driver与device的match
在rte_eal_pci_probe_one_driver()函数中,判断driver与device是否匹配:
主要是通过比对PCI_ID的vendor_id、device_id、subsystem_vendor_id、subsystem_device_id四个字段判断pci设备和pci驱动是否匹配。
4.4.2 map resource的资源映射
PCI设备和PCI驱动匹配后,调用rte_eal_pci_map_device()函数为该PCI设备创建map resource。具体如下:
a、首先pci_get_uio_dev读取/sys/bus/pci/devices/PCI设备目录下的uio目录,获取uio设备的ID,该ID就是uio目录名最后几位的数字。当igb_uio模块与网卡设备绑定的时候,会在/sys/bus/pci/devices/对应的PCI设备目录下创建uio目录。如果启动参数中指定了OPT_CREATE_UIO_DEV_NUM,会在/dev目录下创建对应uio设备的设备文件。
b、初始化PCI设备的中断句柄,/* ID为0或1,即uio0或uio1*/。
c、读取/sys/bus/pci/devices/0000:04:00.0/uio/uio0/maps/map0/目录下的文件和/sys/bus/pci/devices/0000:04:00.0/resource文件,获取UIO设备的map resource。并将其记录在struct pci_map数据结构中;
d、将所有UIO设备的resource信息都记录在struct mapped_pci_resource数据结构中,并挂到全局链表pci_res_list上。
4.4.3初始化PCI设备
在driver与device的match成功,并且做完PCI资源空间地址映射后,调用rte_eth_dev_init()初始化PCI设备;
a、首先,组装网口名称,然后调用rte_eth_dev_allocate()在全局数组rte_eth_devices[]中分配一个网卡设备。并在全局数组rte_eth_dev_data[]中为网卡设备的数据域分配内存空间。
网口名字组装:
初始化网口数据结构:
为网卡设备的私有数据结构分配空间:
b、调用eth_igb_dev_init()初始化网卡设备。首先设置网卡设备的操作函数集,以及收包、发包函数。
初始化网卡设备的硬件相关数据结构struct e1000_hw,包括设备ID、硬件操作函数集、在内存地址总线上映射的地址、MAC地址等等。
c、注册中断处理函数。
中断的具体实现在另一篇总结文档再详细介绍;
更多推荐
所有评论(0)