浅析Linux UIO驱动框架 文章目录概述UIO工作原理访问设备的内存响应设备中断UIO驱动框架实现struct uio_devicestrcut uio_infostruct uio_mem驱动API接口uio_register_deviceuio_event_notifyUIO设备访问打开UIO设备映射设备内存uio_mmap_physical监听中断事件相关参考本文基于Linux 6.6.101版本内核源码进行分析。概述UIO全称为用户空间IO(Userspace I/O)最早于Linux 2.6.32版本引入是Linux提供的一种在用户态空间实现设备驱动的框架。通过UIO框架用户态程序可以直接操控硬件设备从而旁路内核减少调用流程以此获得性能上的提升。UIO工作原理对于驱动程序来说核心职能是与硬件设备进行交互这里包含两个重要的基础功能访问设备内存响应设备中断要在用户态实现设备驱动就必须要解决这两个问题。由于在Linux的设计模型里硬件设备都是由内核驱动进行接管基于这一前提UIO驱动框架只在内核态保留了精简的驱动实现主要负责通过Sysfs和uio字符设备将硬件设备的资源信息暴露给用户态而用户态层获取到硬件资源信息后在用户空间实现完整的设备驱动如下是uio驱动框架的整体框图访问设备的内存Linux通过映射物理设备的内存到用户态来提供访问但是这种方法会引入安全性和可靠性的问题。UIO通过限制不相关的物理设备的映射改善了这个问题。由此基于UIO开发的用户态驱动不需要关心与内存映射相关的安全性和可靠性的问题。响应设备中断中断本身需要在内核处理因此针对这个限制还需要一个小的内核模块通过最基本的中断服务程序来处理。这个中断服务程序可以只是向操作系统确认中断或者关闭中断等最基础的操作剩下的具体操作可以在用户态处理。UIO驱动框架实现UIO驱动框架的实现并不复杂其核心功能是向用户空间暴露硬件设备的内存以及中断事件传递。UIO驱动框架提供了设备注册接口主要用于为每个需要实现用户态驱动的硬件设备生成对应的UIO字符设备后续用户态空间可以通过字符设备访问硬件资源。UIO驱动框架的核心数据结构及与UIO字符设备间的关联示意如下struct uio_devicestruct uio_device唯一对应一个硬件设备每个uio_device都会生成/dev/uioX字符设备。struct uio_device { struct module *owner; struct device dev; int minor; atomic_t event; struct fasync_struct *async_queue; wait_queue_head_t wait; struct uio_info *info; struct mutex info_lock; struct kobject *map_dir; struct kobject *portio_dir; };关键字段描述minor/dev/uioX的次设备号event中断事件计数wait中断事件等待队列进程对/dev/uioX发起read操作时UIO框架会将进程挂入等待队列中断触发时唤醒map_dir用于在sysfs中生成mmap信息。strcut uio_infostruct uio_info用于描述硬件设备的资源信息包括设备内存空间、中断信息以及特定于驱动的处理回调。struct uio_info { struct uio_device *uio_dev; const char *name; const char *version; struct uio_mem mem[MAX_UIO_MAPS]; struct uio_port port[MAX_UIO_PORT_REGIONS]; long irq; unsigned long irq_flags; void *priv; // 驱动的私有数据 irqreturn_t (*handler)(int irq, struct uio_info *dev_info); int (*mmap)(struct uio_info *info, struct vm_area_struct *vma); int (*open)(struct uio_info *info, struct inode *inode); int (*release)(struct uio_info *info, struct inode *inode); int (*irqcontrol)(struct uio_info *info, s32 irq_on); };关键字段功能描述如下mem设备内存信息描述包括物理地址和长度最多可以支持5段空间irq设备中断号UIO框架对中断的支持比较有限只能支持一个中断handler 驱动自定义的中断处理函数mmap驱动自定义的mmap回调通过mmap映射设备内存时会调用非空时irqcontrol驱动提供的中断控制函数用于打开或关闭中断通过/dev/uioX设备写0/1进行操作。struct uio_memstruct uio_mem用于描述设备内存信息。struct uio_mem { const char *name; phys_addr_t addr; unsigned long offs; resource_size_t size; int memtype; void __iomem *internal_addr; struct uio_map *map; };关键字段功能描述如下addr设备内存起始地址需要对齐到物理页面大小offs页面内偏移size设备内存长度memtype内存类型UIO驱动框架定义了相关的宏#define UIO_MEM_NONE 0 #define UIO_MEM_PHYS 1 #define UIO_MEM_LOGICAL 2 #define UIO_MEM_VIRTUAL 3 #define UIO_MEM_IOVA 4驱动API接口Linux提供了操作接口定义在include/linux/uio_driver.h用于设备驱动实现自己的uio驱动模块关键接口如下接口描述uio_register_device注册UIO设备uio_unregister_device去注册UIO设备devm_uio_register_device注册UIO设备uio_register_deviceuio_register_device负责注册新的UIO设备到UIO驱动框架中UIO驱动框架会负责生成/dev/uioX设备和sysfs文件系统内相关内容。int __uio_register_device(struct module *owner, struct device *parent, struct uio_info *info) { struct uio_device *idev; int ret 0; if (!uio_class_registered) return -EPROBE_DEFER; if (!parent || !info || !info-name || !info-version) return -EINVAL; info-uio_dev NULL; // 分配uio_device数据结构 idev kzalloc(sizeof(*idev), GFP_KERNEL); if (!idev) { return -ENOMEM; } // uio_device结构初始化 idev-owner owner; idev-info info; mutex_init(idev-info_lock); init_waitqueue_head(idev-wait); atomic_set(idev-event, 0); // 申请UIO字符设备的次设备号主设备号在UIO框架初始化的时候已经进行了分配 ret uio_get_minor(idev); if (ret) { kfree(idev); return ret; } device_initialize(idev-dev); idev-dev.devt MKDEV(uio_major, idev-minor); idev-dev.class uio_class; idev-dev.parent parent; idev-dev.release uio_device_release; dev_set_drvdata(idev-dev, idev); // 设置字符设备名/dev/uioXX表示子设备号 ret dev_set_name(idev-dev, uio%d, idev-minor); if (ret) goto err_device_create; ret device_add(idev-dev); if (ret) goto err_device_create; // 创建/dev/uioX字符设备 ret uio_dev_add_attributes(idev); if (ret) goto err_uio_dev_add_attributes; info-uio_dev idev; // 设备中断号有效时UIO框架会注册中断处理函数中断统一回调uio_interrupt if (info-irq (info-irq ! UIO_IRQ_CUSTOM)) { ret request_irq(info-irq, uio_interrupt, info-irq_flags, info-name, idev); if (ret) { info-uio_dev NULL; goto err_request_irq; } } return 0; err_request_irq: uio_dev_del_attributes(idev); err_uio_dev_add_attributes: device_del(idev-dev); err_device_create: uio_free_minor(idev-minor); put_device(idev-dev); return ret; }uio_event_notifyuio_event_notify用于中断事件通知其内部实现比较简单主要是增加事件计数然后唤醒挂在uio_device等待队列上的进程。void uio_event_notify(struct uio_info *info) { struct uio_device *idev info-uio_dev; atomic_inc(idev-event); wake_up_interruptible(idev-wait); // 唤醒等待进程 kill_fasync(idev-async_queue, SIGIO, POLL_IN); }UIO设备访问UIO字符设备是Linux内核提供给用户态驱动访问物理设备资源的入口用户态驱动可以使用标准文件的系统调用访问设备信息包括设备内存和设备中断资源。用户空间操作UIO设备的常规流程如下打开/dev/uioX字符设备获取文件描述符fd执行mmap系统调用映射指定的设备内存到进程地址空间关键传入参数为设备内存空间的索引完成mmap映射后用户态驱动可以访问硬件设备寄存器空间并对设备进行配置若用户态驱动需要处理中断事件执行read系统调用监听中断用户态驱动可以执行write调用控制中断的开关。/dev/uioX作为标准的字符设备执行read/write/mmap等系统调用时内核会调用uio_fios中注册的各类回调函数。uio_fios定义如下static const struct file_operations uio_fops { .owner THIS_MODULE, .open uio_open, .release uio_release, .read uio_read, .write uio_write, .mmap uio_mmap, .poll uio_poll, .fasync uio_fasync, .llseek noop_llseek, };打开UIO设备打开UIO设备的主要目的是为了获取文件描述符建立用户态驱动与UIO内核驱动间的信息访问通道。open调用最终会调用uio_openstatic int uio_open(struct inode *inode, struct file *filep) { struct uio_device *idev; struct uio_listener *listener; int ret 0; mutex_lock(minor_lock); // 根据设备号查找匹配的uio_device结构 idev idr_find(uio_idr, iminor(inode)); if (!idev) { ret -ENODEV; mutex_unlock(minor_lock); goto out; } get_device(idev-dev); mutex_unlock(minor_lock); // 获取模块引用 if (!try_module_get(idev-owner)) { ret -ENODEV; goto err_module_get; } listener kmalloc(sizeof(*listener), GFP_KERNEL); if (!listener) { ret -ENOMEM; goto err_alloc_listener; } listener-dev idev; listener-event_count atomic_read(idev-event); filep-private_data listener; mutex_lock(idev-info_lock); if (!idev-info) { mutex_unlock(idev-info_lock); ret -EINVAL; goto err_infoopen; } // 调用UIO设备驱动自定义open函数 if (idev-info-open) ret idev-info-open(idev-info, inode); mutex_unlock(idev-info_lock); if (ret) goto err_infoopen; return 0; err_infoopen: kfree(listener); err_alloc_listener: module_put(idev-owner); err_module_get: put_device(idev-dev); out: return ret; }映射设备内存用户态驱动通过mmap系统调用完成设备物理内存空间到进程虚拟地址空间的映射UIO设备最大可以支持5段MMIO空间上层可以指定空间的索引。mmap系统调用最终会调用uio驱动框架的uio_mmap函数进行实际的映射操作。static int uio_mmap(struct file *filep, struct vm_area_struct *vma) { struct uio_listener *listener filep-private_data; struct uio_device *idev listener-dev; int mi; unsigned long requested_pages, actual_pages; int ret 0; if (vma-vm_end vma-vm_start) return -EINVAL; vma-vm_private_data idev; mutex_lock(idev-info_lock); if (!idev-info) { ret -EINVAL; goto out; } mi uio_find_mem_index(vma); if (mi 0) { ret -EINVAL; goto out; } requested_pages vma_pages(vma); actual_pages ((idev-info-mem[mi].addr ~PAGE_MASK) idev-info-mem[mi].size PAGE_SIZE -1) PAGE_SHIFT; if (requested_pages actual_pages) { ret -EINVAL; goto out; } // 若UIO设备驱动自定义mmap函数uio_info-mmap则调用驱动自定义mmap进行内存映射 if (idev-info-mmap) { ret idev-info-mmap(idev-info, vma); goto out; } switch (idev-info-mem[mi].memtype) { case UIO_MEM_IOVA: case UIO_MEM_PHYS: // 映射设备物理内存主要是用这条路径 ret uio_mmap_physical(vma); break; case UIO_MEM_LOGICAL: case UIO_MEM_VIRTUAL: ret uio_mmap_logical(vma); break; default: ret -EINVAL; } out: mutex_unlock(idev-info_lock); return ret; }uio_mmap_physicaluio_mmap_physical完成设备内存空间到进程虚拟地址空间的映射。static int uio_mmap_physical(struct vm_area_struct *vma) { struct uio_device *idev vma-vm_private_data; int mi uio_find_mem_index(vma); struct uio_mem *mem; if (mi 0) return -EINVAL; mem idev-info-mem mi; if (mem-addr ~PAGE_MASK) return -ENODEV; if (vma-vm_end - vma-vm_start mem-size) return -EINVAL; vma-vm_ops uio_physical_vm_ops; // 这里要注意映射的属性由于是访问的设备内存不能使用Cache if (idev-info-mem[mi].memtype UIO_MEM_PHYS) vma-vm_page_prot pgprot_noncached(vma-vm_page_prot); return remap_pfn_range(vma, vma-vm_start, mem-addr PAGE_SHIFT, vma-vm_end - vma-vm_start, vma-vm_page_prot); }监听中断事件当用户空间需要监听硬件设备的中断事件时可以执行read系统调用在设备无中断时read默认会阻塞当硬件设备产生中断事件时文件描述符会切换到可读状态触发read系统调用返回用户空间由此感知到设备有中断进行中断响应。read系统调用到内核后会执行uio_read函数static ssize_t uio_read(struct file *filep, char __user *buf, size_t count, loff_t *ppos) { struct uio_listener *listener filep-private_data; struct uio_device *idev listener-dev; DECLARE_WAITQUEUE(wait, current); ssize_t retval 0; s32 event_count; if (count ! sizeof(s32)) return -EINVAL; // 将当前进程添加到中断事件等待队列上 add_wait_queue(idev-wait, wait); do { mutex_lock(idev-info_lock); if (!idev-info || !idev-info-irq) { retval -EIO; mutex_unlock(idev-info_lock); break; } mutex_unlock(idev-info_lock); // 设置当前进程状态为TASK_INTERRUPTIBLE set_current_state(TASK_INTERRUPTIBLE); event_count atomic_read(idev-event); if (event_count ! listener-event_count) { __set_current_state(TASK_RUNNING); if (copy_to_user(buf, event_count, count)) retval -EFAULT; else { listener-event_count event_count; retval count; } break; } // 若文件描述符是非阻塞式则直接返回 if (filep-f_flags O_NONBLOCK) { retval -EAGAIN; break; } if (signal_pending(current)) { retval -ERESTARTSYS; break; } // 触发调度此时当前进程会睡眠直到有中断事件或者进程信号 schedule(); } while (1); __set_current_state(TASK_RUNNING); remove_wait_queue(idev-wait, wait); return retval; }相关参考Linux UIOUserspace I/O驱动模型技术详解