1.1为什么出现了UIO?
硬件设备可以根据功能分为网络设备,块设备,字符设备,或者根据与CPU相连的方式分为PCI设备,USB设备等。它们被不同的内核子系统支持。这些标准的设备的驱动编写较为容易而且容易维护。很容易加入主内核源码树。但是,又有很多设备难以划分到这些子系统中,比如I/O卡,现场总线接口或者定制的FPGA。通常这些非标准设备的驱动被实现为字符驱动。这些驱动使用了很多内核内部函数和宏。而这些内部函数和宏是变化的。这样驱动的编写者必须编写一个完全的内核驱动,而且一直维护这些代码。而且这些驱动进不了主内核源码。于是就出现了用户空间I/O框架(Userspace I/O framework)。
1.2 dpdk的uio使用
在基于kernel的IO模型中,所有的设备IO都要经过内核处理,在高并发的网络数据包收发的情况下,大量硬件中断会降低内核数据包处理能力,内核和用户空间的数据拷贝也会造成大量的计算资源浪费。所以,作为高并发大流量网络开发框架的DPDK,必须要找到一个能够避免内核中断爆炸和大量数据拷贝的方法,在用户空间能够直接和硬件进行交互。Linux的UIO就是这样一个将硬件操作映射到用户空间的kernel bypass方案。
下图展示了UIO驱动的内核部分,用户空间部分,和UIO框架以及内核的关系:
1.3 dpdk的网口绑定干了啥?
绑定操作:
./dpdk-devbind.py -b igb_uio 0000:00:07.0
既然是设备驱动程序一定会创建设备,/dev下查看:多出了uio0设备;
vm65:/dev # ll |grep uio crw------- 1 root root 246, 0 Feb 18 07:41 uio0 vm65:/dev #
查看/sys/class/uio/uio:
vm65:/sys/class/uio/uio0 # ll total 0 -r--r--r-- 1 root root 4096 Feb 18 06:42 dev lrwxrwxrwx 1 root root 0 Feb 18 06:42 device -> ../../../0000:00:07.0 -r--r--r-- 1 root root 4096 Feb 18 06:42 event drwxr-xr-x 4 root root 0 Feb 18 06:42 maps -r--r--r-- 1 root root 4096 Feb 18 06:42 name drwxr-xr-x 2 root root 0 Feb 18 06:42 power lrwxrwxrwx 1 root root 0 Feb 18 06:42 subsystem -> ../../../../../class/uio -rw-r--r-- 1 root root 4096 Feb 18 06:41 uevent -r--r--r-- 1 root root 4096 Feb 18 06:42 version
同时,查看dmesg,多出了以下几行信息:
<6>[171282.211083] igb_uio 0000:00:07.0: mapping 1K dma=0x123fb9b000 host=ffff88123fb9b000 <6>[171282.211088] igb_uio 0000:00:07.0: unmapping 1K dma=0x123fb9b000 host=ffff88123fb9b000
可以看到。绑定过程中的kernel part会通过uio_register_device 注册对应的uio设备,获取pcie的配置空间BAR信息包括addr、name、size、offset填充到map/map0中,这些在用户态会将其读出,并mmap至用户态进程空间,这样用户态便可直接操作设备的内存空间。
除了内存管理,第二个任务,对于设备中断的应答必须在内核空间进行。所以在内核空间有一小部分代码用来应答和禁止中断,中断的其他任务由用户态完成;
假设用户空间要等待一个设备中断,它仅仅须要简单的堵塞在对 /dev/uioX的read()操作上。 当设备产生中断时,read()操作马上返回。
UIO 也实现了poll()系统调用。你能够使用 select()来等待中断的发生。select()有一个超时參数能够用来实现有限时间内等待中断。
1.4 用户态驱动
常用的用户态驱动开发有两种方式可选:
1、打开/dev/mem 后,使用MMAP映射出芯片物理地址对应的虚拟地址,在用户态访问虚拟地址加偏移就能访问到芯片的寄存器;
2、采用UIO方式,在内核态映射地址后,在用户态通过打开/dev/uioxxx 方式,再使用MMAP映射一次,就可采用跟方式1 相同的方式访问芯片reg或是memory;
这两种方式各有优缺点:
方式1:优点是实现简单,如果不需要用到中断,可以不用管内核态的内容,关注用户态代码就好。缺点是代码运行的进程要有root权限,不然无法访问/dev/mem,如果要使用中断还是会涉及到内核态的驱动;
方式2:优点是无需要root权限,中断可以直接在用户态获取。缺点是必须要在内核态做初始化;即需要在内核态通过uio_register_device注册对应的uio设备,这样user space就可以通过/sys/class/uio/uioX/mapsX来访问这个uio设备;
关键结构体&函数:
注册uio设备前需要初始化对应的uio_info结构体:
struct uio_info { struct uio_device *uio_dev; // 在__uio_register_device中初始化 const char *name; // 调用__uio_register_device之前必须初始化 const char *version; //调用__uio_register_device之前必须初始化 struct uio_mem mem[MAX_UIO_MAPS]; struct uio_port port[MAX_UIO_PORT_REGIONS]; long irq; //分配给uio设备的中断号,调用__uio_register_device之前必须初始化 unsigned long irq_flags;// 调用__uio_register_device之前必须初始化 void *priv; // irqreturn_t (*handler)(int irq, struct uio_info *dev_info); //uio_interrupt中调用,用于中断处理 // 调用__uio_register_device之前必须初始化 int (*mmap)(struct uio_info *info, struct vm_area_struct *vma); //在uio_mmap中被调用, // 执行设备打开特定操作 int (*open)(struct uio_info *info, struct inode *inode);//在uio_open中被调用,执行设备打开特定操作 int (*release)(struct uio_info *info, struct inode *inode);//在uio_device中被调用,执行设备打开特定操作 int (*irqcontrol)(struct uio_info *info, s32 irq_on);//在uio_write方法中被调用,执行用户驱动的 //特定操作。 };
设备注册函数:
#define uio_register_device(parent, info) __uio_register_device(THIS_MODULE, parent, info) int __uio_register_device(struct module *owner, struct device *parent, struct uio_info *info)
用户态驱动示例: