浅析Linux UIO驱动框架
文章目录
- 概述
- UIO工作原理
- 访问设备的内存
- 响应设备中断
- UIO驱动框架实现
- struct uio_device
- strcut uio_info
- struct uio_mem
- 驱动API接口
- uio_register_device
- uio_event_notify
- UIO设备访问
- 打开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_device
struct 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_info
struct 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_mem
struct 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_device
uio_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/uioX,X表示子设备号 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_notify
uio_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_open:
static 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_physical
uio_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 UIO(Userspace I/O)驱动模型技术详解