当前位置: 首页 > news >正文

浅析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设备的常规流程如下:

  1. 打开/dev/uioX字符设备,获取文件描述符fd;
  2. 执行mmap系统调用,映射指定的设备内存到进程地址空间,关键传入参数为设备内存空间的索引;
  3. 完成mmap映射后,用户态驱动可以访问硬件设备寄存器空间,并对设备进行配置;
  4. 若用户态驱动需要处理中断事件,执行read系统调用监听中断;
  5. 用户态驱动可以执行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)驱动模型技术详解
http://www.jsqmd.com/news/1177913/

相关文章:

  • 【世界杯中的AI】(2026-07-12)狂野逆转与AI神预测:世界杯1/4决赛夜,科技如何“封神”?
  • 页面淘汰算法:从LRU到Linux时钟算法的内存管理实战
  • SD卡无法格式化怎么解决?【图文讲解】全套安全修复实操完整教程
  • 影刀RPA社区版实战指南:个人自动化从入门到部署
  • AI项目本地部署实战:从环境配置到性能优化全指南
  • 2026年7月最新长春宝玑官方售后联系电话与客户服务中心网点地址 - 亨得利钟表维修中心
  • 如何用 Claude 4.8 搞定考研、考公备考计划?超实用的 AI 效率规划指南
  • 工业级C++异常处理:从防御性编程到契约设计的实战指南
  • 非刚性ICP与3D高斯溅射:从视频生成连贯动态3D场景
  • 3步搞定B站缓存转换:免费工具让视频永远属于你
  • 2026年7月最新上海雷达官方售后客户服务热线与维修网点地址汇总 - 亨得利钟表维修中心
  • 重磅变革!DeFi 迈入自治金融第四纪元,TrueDAO AVN 自治价值网络完整方案重磅落地
  • 亲身到店探访杭州亨得利官方名表服务中心|官方地址及联系电话(2026年7月更新) - 亨得利官方
  • n8n 实战工作流模板:GitHub→飞书通知、定时爬虫→数据库、Webhook→多渠道推送
  • 多模态大模型核心技术:从Transformer架构到10-300亿参数实战
  • 做了 3 个月 AI 产品才明白:记忆分三层,最难的那层叫遗忘
  • 终极设备救砖指南:3步快速掌握MTKClient刷机工具
  • 即梦SeedMusic AI音乐生成全流程实操指南:从提示词到爆款歌曲
  • 2026最新广安本地漏水检测公司本地精选权威推荐:正规防水补漏公司优选口碑TOP5:卫生间/厨房/阳台/飘窗/地下室渗漏水维修师傅上门 - 即刻修防水
  • HarmonyOS APP实战-画图APP - 第3篇:矩形与圆形绘制
  • 2026最新宜昌本地漏水检测公司本地精选权威推荐:正规防水补漏公司优选口碑TOP5:卫生间/厨房/阳台/飘窗/地下室渗漏水维修师傅上门 - 即刻修防水
  • 深入理解Linux多线程互斥锁:从临界区到底层原子指令的完整闭环
  • 2026年7月最新绍兴美度官方售后热线及客户服务网点地址 - 亨得利钟表维修中心
  • GitHub开源神器Zhubuntu:浏览器运行真实安卓级桌面系统
  • AirSim 1.8.1 Windows 配置:3个典型CMake与VS版本兼容性错误深度解析
  • PromQL 实战:5分钟构建3个核心监控告警规则(附HTTP API调用)
  • 3分钟解锁AMD Ryzen处理器的终极调试能力:SMUDebugTool完全指南
  • LangChain 1.3 AI Agent开发实战:从入门到企业级应用部署
  • AI投资代理实战:多智能体架构实现146%收益的技术解析
  • 5MB轻量级C盘清理工具:免费解决Windows系统空间不足问题