前两天对DPDK的igb_uio相关的代码做了下分析,先把这期间碰到的一些问题和代码分析记下来,作为一个小总结。
Igb_uio代码相关的可以分为三个部分:igb_uio内核驱动,内核uio框架,uio用户态部分。
Igb_uio内核驱动
Igb_uio驱动主要做的就是注册一个pci设备。但是igbuio_pci_driver对应的保存pci设备信息的id_table指针为空,这样在内核注册此pci设备时,会找不到匹配的设备,就不会调用igb_uio驱动中的探测probe函数,只会在/sys目录下创建Igb_uio相应的目录。
如何probe
在插入igb_uio.ko时是probe不到设备的,那是什么时候侦测到的呢?dpdk提供了一个Python脚本dpdk_nic_bind.py。
python tools/dpdk_nic_bind.py --bind=igb_uio eth1
运行上述命令就是将eth1网卡绑定到igb_uio模块。这时dmesg就会看到igb_uio模块的probe函数执行了,也就是意味着扫描到了匹配的pci设备。
经过分析dpdk_nic_bind.py,此脚本文件在上述情况下主要做了以下几步:
◆获取参数指定的网卡eth1的设备信息。使用lspci–Dvmmn查看。
Slot: 0000:06:00.1 Class: 0200 Vendor: 8086 Device: 1521 SVendor: 15d9 SDevice: 1521 Rev: 01 |
可以查看到slot槽位信息、厂商号vendor ID、设备号device ID等信息。
◆unbind之前的igb模块。
将前面获取到的eth1对应的slot信息0000:06:00.1值写入igb的unbind文件。
echo 0000:06:00.1 > /sys/bus/pci/drivers/igb/unbind
从内核代码分析此unbind的动作就是将igb模块信息和此pci设备Dev去关联。将dev->driver指针置为空,这个很重要。在内核处理pci设备注册的函数中,就算驱动的vendor ID和device ID与设备的都匹配上了,如果此设备的dev->driver指针不为空,也不会调用probe函数的。
◆bind新的igb_uio模块
将eth1设备的vendor和device ID信息写入igb_uio的new_id文件。
echo 0x8086 0x1521 > /sys/bus/pci/drivers/igb_uio/new_id
内核中处理此步的函数为store_new_id,此函数中是将写入的vendor和device存入到此driver,也就是igb_uio的id_table,然后以此与PCI上的设备进行匹配,这个时候肯定会匹配成功,然后调用igb_uio模块的probe函数进行初始化动作。
初始化分析
一个设备驱动要实现的功能根据实际需要可能千差万别,但是究其本质来说无非两件事情:一个是内存的操作,另外一个就是中断的处理。Igb_uio驱动和igb驱动都是网卡这个PCI设备的驱动,相同点就是要使能PCI设备,分配内存等,不同的就在于对内存和中断的处理方式的差异。
下面看下igb_uio驱动与igb驱动相比的probe函数的不同之处:
◆记录设备的资源
igb_uio模块遍历此PCI设备BAR空间,对应于类型为存储器空间IORESOURCE_MEM的BAR,将其物理地址、大小等信息保存到uio_info结构的mem数组中之中;类型为寄存器空间IORESOURCE_IO的BAR,将其物理地址、大小等信息保存到uio_info结构的port数组中。
从上图可知,此网卡PCI设备有三个BAR空间,BAR0类型为存储器空间,物理地址为0xfbd00000,大小为128K;BAR2类型为寄存器空间,物理地址为0xb000,大小为32字节;BAR3类型为存储器空间,物理地址为0xfbd40000,大小为16K。
Igb驱动也会遍历BAR空间,但是它不会记录空间的物理地址,而是调用ioremap函数将物理地址映射为虚拟地址,驱动在内核态读写操作映射出来的虚拟地址。
◆注册一个uio设备
Linux上的驱动设备一般都是运行在内核态的,提供接口函数给用户态函数调用即可。而新引入的UIO技术,则是将驱动的大部分事情移到了用户态。前面讲到probe函数会记录设备的资源,具体而言就是PCI设备BAR空间的物理地址、大小等信息记录下来传给用户态。除了记录BAR空间资源信息,uio框架还会在内核态实现中断处理相关的初始化工作。
igbuio_pci_probe代码片段:
/* fill uio infos */ udev->info.name ="igb_uio"; udev->info.version ="0.1"; udev->info.handler =igbuio_pci_irqhandler; udev->info.irqcontrol =igbuio_pci_irqcontrol; |
注册的uio设备名为igb_uio,内核态中断处理函数为igbuio_pci_irqhandler,中断控制函数igbuio_pci_irqcontrol。这两个函数有什么用呢?下面在分析UIO注册函数时分析。
igbuio_pci_probe代码片段:
switch (igbuio_intr_mode_preferred) { case RTE_INTR_MODE_MSIX: msix_entry.entry =0; if (pci_enable_msix(dev,&msix_entry,1)==0) { udev->info.irq =msix_entry.vector; udev->mode =RTE_INTR_MODE_MSIX; break; }
case RTE_INTR_MODE_LEGACY: if (pci_intx_mask_supported(dev)) { udev->info.irq_flags =IRQF_SHARED; udev->info.irq =dev->irq; udev->mode =RTE_INTR_MODE_LEGACY; break; } |
变量igbuio_intr_mode_preferred表示中断的模式,它由igb_uio驱动模块的参数intr_mode决定,有MSIX中断和Legacy中断两种模式,默认的是MSIX中断模式。
如果是MSIX中断模式,调用pci_enable_msix函数向PCI子系统申请分配一个MSIX中断;如果分配成功就会初始化uio_info的irq为申请到的中断号。如果是传统的Intx中断模式,调用pci_intx_mask_supported函数读取PCI配置空间,检查是否支持Intx中断。
在对uio_info内存和中断相关的成员初始化之后,就是调用uio_register_device函数来注册uio设备了,下面结合内核代码看下此函数做了些什么。
__uio_register_device代码片段:
idev->owner =owner; idev->info =info; init_waitqueue_head(&idev->wait); atomic_set(&idev->event,0); idev->dev =device_create(&uio_class,parent, MKDEV(uio_major,idev->minor),idev, "uio%d",idev->minor);
ret =uio_dev_add_attributes(idev); info->uio_dev =idev;
if (info->irq &&(info->irq !=UIO_IRQ_CUSTOM)) { ret =devm_request_irq(idev->dev,info->irq,uio_interrupt, info->irq_flags,info->name,idev); } |
上面代码是__uio_register_device函数的主要部分
1、 初始化uio_device结构体指针idev,主要包括等待队列wait、中断事件计数event、
次设备号minor等。
2、在/dev目录下创建了一个uio设备,设备名为uio%d,%d对应的就是次设备号minor。
3、接着就是调用uio_dev_add_attributes函数在/sys/class/uio/uioX/目录下创建maps和portio接口。前面讲到会遍历此PCI设备的BAR空间,将存储器空间类型的BAR的物理地址等信息存储在uio_info的mem数组中,这里就会根据此mem数组在maps目录下为每个寄存器类型的BAR创建一个目录,如下图所示:
前面图1可以看到igb网卡有两个类型为IORESOURCE_MEM的BAR,分别为BAR0和BAR3,这里就创建了map0和map1两个子目录分别对应BAR0和BAR3。你可以依次查看对应目录下的name、addr字段,正好对应于图1中看到的信息。
同理会根据port数组在portio目录下为每个寄存器类型的BAR创建一个子目录,这里就不细说了。
4、最后就是注册中断了,中断的中断号、中断标志等在前面有讲到,这里看下注册的中断处理函数uio_interrupt。
static irqreturn_t uio_interrupt(intirq,void *dev_id) { struct uio_device *idev =(struct uio_device *)dev_id; irqreturn_t ret =idev->info->handler(irq,idev->info);
if (ret==IRQ_HANDLED) uio_event_notify(idev->info);
return ret; } |
此函数首先调用igb_uio驱动中设置的中断处理函数igbuio_pci_irqhandler来检查中断是不是此设备的中断,如果是就返回IRQ_HANDLED表示需要处理,接着调用函数uio_event_notify来唤醒等待队列wait上进程来处理中断事宜。