Linux 驱动研究 —— V4L2 (12)

📅 2026/8/10 12:15:48
Linux 驱动研究 —— V4L2 (12)
1. MMAP1.1 MMAP 介绍1.2 MMAP 与 IOMMU2. mmap () 调用2.1 过程总览2.2 vb2_mmap ()1. MMAP1.1 MMAP 介绍在深入探讨 Linux 媒体子系统的全貌时我们已经完成了缓冲区队列管理、状态机流转以及底层 DMA 内存分配机制的研究。为了实现内核空间到用户空间的高效数据交互必须研究 mmap内存映射机制。在 V4L2 驱动架构中mmapMemory Map内存映射是一种将内核空间的视频缓冲区内存直接映射到用户空间虚拟地址的技术。其核心作用和技术细节包括消除内核与用户空间之间的数据拷贝在传统的文件读写方式如read或write中底层采集到的图像数据首先存储在内核缓冲区系统必须通过 CPU 将其显式拷贝copy_to_user到用户空间应用程序的内存中。采用 mmap 后内核通过修改进程的页表项PTE直接将内核已申请好的物理内存页映射到用户进程的虚拟地址空间。应用程序可以直接访问这片内存从而实现“零拷贝”Zero-Copy大幅降低 CPU 占用率并提升图像传输的实时性。支持大尺寸高分辨率视频的高效传输现代嵌入式设备如 4K 摄像头产生的数据量极大。如果依赖内存拷贝会严重挤占系统带宽和 CPU 资源。mmap 使得应用层可以直接对硬件 DMA 写入的目标内存进行读取、处理或渲染。统一的缓冲区生命周期管理mmap 建立的映射关系与 VB2 框架的缓冲区队列Buffer Queue紧密结合。应用程序通过VIDIOC_REQBUFS申请缓冲区后通过 mmap 获取每个缓冲区的偏移量offset并完成映射随后配合QBUF入队和DQBUF出队完成整个循环采集生命周期。1.2 MMAP 与 IOMMU在 Linux 内核与驱动开发中mmap和IOMMU都涉及“地址映射”但它们处于完全不同的维度、服务于不同的硬件和对象。一、 核心定义与异同对比比较维度mmap (内存映射)IOMMU (输入输出内存管理单元)服务对象CPU与用户态进程外设硬件如 DMA 设备、网卡、摄像头 CIF与物理内存本质作用将内核空间的物理内存或文件映射到用户空间虚拟地址供应用程序直接读写。将外设发出的IO虚拟地址 (IOVA)翻译成系统物理地址 (PA)。底层硬件依赖依赖 CPU 的MMU内存管理单元及页表。依赖独立于 CPU 的IOMMU硬件芯片如 Intel VT-d, AMD-Vi。典型应用场景V4L2 视频采集中的零拷贝、大文件高效读写、进程间内存共享。PCIe 设备直通VFIO/虚拟机、离散物理内存拼凑连续 IO 空间、防 DMA 攻击。二、 它们的相同点核心目标一致地址转换与隔离两者的底层核心逻辑都是建立一张页表Page Table通过硬件级别的地址转换Virtual Address - Physical Address实现对内存访问权限的控制与管理。解决物理内存碎片化mmap可以将不连续的物理内存页在用户态拼成一段连续的虚拟地址区间。IOMMU可以将分散的物理内存页在设备端DMA拼成一段连续的 IO 虚拟地址 (IOVA)。三、 它们的不同点深度剖析1. 映射的方向与视角不同mmap 的视角CPU 端面向的是应用程序进程。它解决的是“用户态的虚拟地址 (vma) 应该如何对应到内核的物理页或内核虚拟地址”。IOMMU 的视角外设端面向的是硬件外设如摄像头控制器、GPU、网卡。它解决的是“外设在发起 DMA 传输时填写的地址IOVA硬件是如何通过 IOMMU 页表把它翻译成真正的 RAM 物理地址的”。2. 工作阶段与生命周期不同mmap发生在用户空间调用mmap()系统调用时由内核的 MMU 建立进程页表。IOMMU的映射通常在驱动初始化、申请 DMA 缓冲区如dma_alloc_coherent或VFIO_IOMMU_MAP_DMA时由内核或虚拟机管理程序Hypervisor动态配置。四、 在 V4L2 / 驱动开发中的关联在高级嵌入式驱动或虚拟化场景如使用 VFIO 或带有 IOMMU 的 SoC中这两者会协同工作第一步IOMMU 阶段内核驱动通过 DMA 分配器结合 IOMMU为摄像头硬件申请一片缓冲区硬件通过 IOMMU 将离散的物理内存映射为一段连续的IOVA设备可见地址供 DMA 高效写入。这里的DMA 写入是指摄像头传感器 / 摄像头接口控制器如 SoC 内部的 CSI 接收器或 CIF 桥接模块将采集到的图像像素数据直接写入系统的物理内存RAM 中分配好的视频缓冲区中。具体的数据流向和写入路径如下数据源头起点摄像头传感器Camera Sensor不断输出数字图像流通过 MIPI CSI-2 或并行 DVP 接口传输给主控芯片。硬件搬运过程主控芯片内部的摄像头接收模块如 Rockchip CIF / CSI Controller内置了 DMA 控制器。它不需要 CPU 的参与直接通过硬件总线将接收到的图像数据“写”入内存。目的地终点系统内存RAM中为 V4L2 申请好的缓冲区Buffer。而 IOMMU 在其中起到的作用就是把这些可能在物理上东一块、西一块的离散内存页在 DMA 眼中“拼”成一段连续的IOVA设备虚拟地址让摄像头硬件可以像写连续内存一样顺畅地把图像写进去。总结IOMMU保证了外设硬件能安全、顺畅地把数据写进物理内存而mmap保证了用户态程序能高效、无拷贝地从内存中读出这些数据。第二步mmap 阶段当数据采集完成后为了让用户层的 OpenCV 或应用程序能够零拷贝读取这片内存驱动会通过mmap如调用dma_mmap_coherent将这片物理内存映射到用户空间。2. mmap () 调用2.1 过程总览从用户空间发起mmap()请求到最终执行到底层内存管理回调函数如连续内存的vb2_dc_mmap整个 Linux 内核的完整调用链路如下第一阶段用户空间发起系统调用应用层调用用户态程序如 OpenCV 或自定义采集程序通过open(/dev/video0, O_RDWR)打开摄像头设备文件拿到文件描述符fd。程序调用标准 POSIX 接口void*addrmmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);其中offset是由内核通过前置的VIDIOC_QUERYBUF告知应用层的缓冲区偏移量。第二阶段虚拟文件系统VFS与驱动层拦截VFS 分发应用程序陷入内核态Linux 内核的虚拟文件系统VFS根据fd找到对应的file结构体进而读取驱动注册的file_operations操作合集通过struct file_operations结构体中的.mmap指针决定执行哪个 mmap 回调函数。staticconststructv4l2_file_operationsrkcif_fops{.openrkcif_fh_open,.releaserkcif_fh_release,.unlocked_ioctlvideo_ioctl2,.pollvb2_fop_poll,.mmapvb2_fop_mmap,};比如在rkcif_fops中就是执行vb2_fop_mmap进入驱动专属的映射入口通过struct file_operations或 V4L2 专属的v4l2_file_operations结构体中的.mmap指针来决定执行哪个回调函数。直接复用通用框架例如在 Rockchip 的 CSI 驱动中注册的rkcif_fops此时直接调用由通用框架提供的vb2_fop_mmap。厂商定制驱动的中转如 Rockchip VPU 等复杂多媒体编解码节点会进入驱动自己实现的中转函数如rockchip_vpu_mmap。共同任务无论是直接复用还是通过中转函数它们的最终目的都是从设备上下文中提取出当前视频流对应的 VB2 队列指针struct vb2_queue *q然后无条件把q和vma传给 VB2 核心层的vb2_mmap()。最终所有的mmap请求都会汇聚到核心层的vb2_mmap()在那里完成偏移量计算、缓冲区匹配、边界检查并最终分发到底层的内存算子中。第三阶段VB2 核心框架层统一处理 (vb2_mmap)合法性与参数校验进入核心函数vb2_mmap(struct vb2_queue *q, struct vm_area_struct *vma)。类型检查确认当前队列模式为VB2_MEMORY_MMAP。权限检查检查vma-vm_flags是否包含VM_SHARED共享映射并根据输入/输出流属性校验读写权限。忙状态检查确保当前没有在进行旧式的file io操作。还原偏移量并定位缓冲区通过unsigned long off vma-vm_pgoff PAGE_SHIFT;将用户层传进来的页偏移还原为实际字节偏移量。调用__find_plane_by_offset(q, off, buffer, plane)遍历队列中的所有缓冲区精准定位到用户想映射的是哪一个 Buffer 的哪一个 Plane平面。边界溢出检查对比用户申请映射的长度vma-vm_end - vma-vm_start是否超过了缓冲区实际对齐后的长度。第四阶段内存算子分发与底层页表挂载多态算子调用call_memop校验全部通过后核心层通过宏展开调用当前内存模型对应的底层算子retcall_memop(vb,mmap,vb-planes[plane].mem_priv,vma);根据驱动初始化时选定的内存类型分流到对应的底层文件物理连续内存进入drivers/media/common/videobuf2/videobuf2-dma-contig.c的vb2_dc_mmap()。SG 离散内存进入videobuf2-dma-sg.c的vb2_dma_sg_mmap()。底层硬件级映射修改进程页表以连续内存为例vb2_dc_mmap最终会调用内核的内存管理函数如dma_mmap_attrs或通用的remap_pfn_range()。内核通过修改当前用户进程的页表项PTE将底层硬件 DMA 已经写好或准备写入的物理页帧号PFN直接挂载到用户进程的虚拟地址空间vma中。当该函数返回0成功后用户态程序拿到的内存指针addr便直接指向了内核与硬件共享的物理内存至此完成“零拷贝”数据通道的打通。2.2 vb2_mmap ()接下来我们就以vb2_mmap()函数为线索深入分析intvb2_mmap(structvb2_queue*q,structvm_area_struct*vma){/* * 1. 获取偏移量将用户层传入的页偏移vma-vm_pgoff左移 PAGE_SHIFT 位 * 还原为实际的字节偏移量off。该偏移量在应用层调用 mmap 时由 offset 参数决定。 */unsignedlongoffvma-vm_pgoffPAGE_SHIFT;structvb2_buffer*vb;unsignedintbuffer0,plane0;intret;unsignedlonglength;/* * 2. 队列模式检查 * 确保当前 VB2 队列配置的内存管理模式是 VB2_MEMORY_MMAP即内存映射模式 * 如果是 USERPTR 或 DMABUF 模式则不支持此操作直接返回 -EINVAL。 */if(q-memory!VB2_MEMORY_MMAP){dprintk(1,queue is not currently set up for mmap\n);return-EINVAL;}/* * 3. 内存访问模式与权限检查 * 检查 VMA虚拟内存区域的标志位。 * - 必须包含 VM_SHARED共享映射标志因为多媒体内存需要在内核与用户态之间共享。 */if(!(vma-vm_flagsVM_SHARED)){dprintk(1,invalid vma flags, VM_SHARED needed\n);return-EINVAL;}/* * 根据队列方向输入/输出校验读写权限 * - 如果是输出队列is_output如向硬件送显或编码用户态需要有写权限VM_WRITE。 * - 如果是输入队列如摄像头采集用户态需要有读权限VM_READ。 */if(q-is_output){if(!(vma-vm_flagsVM_WRITE)){dprintk(1,invalid vma flags, VM_WRITE needed\n);return-EINVAL;}}else{if(!(vma-vm_flagsVM_READ)){dprintk(1,invalid vma flags, VM_READ needed\n);return-EINVAL;}}/* 加锁保护队列的 mmap 操作防止并发冲突 */mutex_lock(q-mmap_lock);/* * 4. 忙状态检查 * 如果当前队列正在通过传统的文件读写fileio方式工作 * 则不允许同时进行 mmap 操作返回 -EBUSY。 */if(vb2_fileio_is_active(q)){dprintk(1,mmap: file io in progress\n);ret-EBUSY;gotounlock;}/* * 5. 定位缓冲区与平面Plane * 根据前面计算出的字节偏移量 off调用内部函数在队列中查找 * 精准找出用户想映射的是哪一个 Buffer 的哪一个 Plane并返回对应的索引。 */ret__find_plane_by_offset(q,off,buffer,plane);if(ret)gotounlock;/* 通过找到的 buffer 索引获取对应的 vb2_buffer 结构体指针 */vbq-bufs[buffer];/* * 6. 边界与对齐检查 * MMAP 要求缓冲区按内存页Page对齐。 * 获取该 plane 实际分配的长度并进行 PAGE_ALIGN 对齐 * 对比用户请求映射的虚拟内存区间大小vma-vm_end - vma-vm_start是否越界溢出。 */lengthPAGE_ALIGN(vb-planes[plane].length);if(length(vma-vm_end-vma-vm_start)){dprintk(1,MMAP invalid, as it would overflow buffer length\n);ret-EINVAL;gotounlock;}/* * 7. 调用底层内存算子执行真正的映射 * 通过宏展开调用当前内存模型如 dma-contig 或 dma-sg专属的 mem_ops-mmap 回调 * 最终通过 remap_pfn_range 等函数修改进程页表完成物理页到用户空间虚拟地址的挂载。 */retcall_memop(vb,mmap,vb-planes[plane].mem_priv,vma);unlock:/* 释放互斥锁 */mutex_unlock(q-mmap_lock);if(ret)returnret;dprintk(3,buffer %d, plane %d successfully mapped\n,buffer,plane);return0;}call_memop#definecall_memop(vb,op,args...)\({\structvb2_queue*_q(vb)-vb2_queue;\interr;\\log_memop(vb,op);\err_q-mem_ops-op?_q-mem_ops-op(args):0;\if(!err)\(vb)-cnt_mem_##op;\err;\})#definecall_memop(vb,op,args...)\((vb)-vb2_queue-mem_ops-op?\(vb)-vb2_queue-mem_ops-op(args):0)这两个不同版本的call_memop宏定义是通过内核编译配置选项CONFIG_VIDEO_ADV_DEBUG来进行条件编译切换的。它们的区别和具体含义如下1. 当CONFIG_VIDEO_ADV_DEBUG开启Enabled时对应贴出的第一段代码带日志和计数器统计核心特点开启了高级调试与统计功能Debug Profiling。执行细节log_memop(vb, op)会记录或打印当前执行了哪一个内存算子op方便开发者在内核日志dmesg中追踪底层内存操作。安全调用判断_q-mem_ops-op是否存在若存在则执行并返回结果。成功计数cnt_mem_ ## op如果算子执行成功err 0会自动将该缓冲区对应这个内存操作的成功执行次数进行累加例如统计mmap、alloc等操作被成功调用了多少次用于后续的性能分析或状态排查。2. 当CONFIG_VIDEO_ADV_DEBUG关闭Disabled通常是默认的 Release 状态时对应贴出的第二段代码精简版核心特点极致的精简与性能优先Production / Lightweight。执行细节它去掉了所有的日志打印log_memop和计数器累加cnt_mem_。仅仅保留了最核心的函数指针有效性检查与直接调用只要对应的底层内存算子存在就直接把参数传进去执行并返回结果否则返回0。这种方式可以避免在正式发布的嵌入式产品内核中产生多余的调试开销节省 CPU 指令周期。参考 call_ptr_memop无论是call_memop还是call_ptr_memop它们在内核里采用的设计模式和宏展开逻辑是完全同构的宏设计意图相同它们都是为了安全地通过函数指针表mem_ops去调用底层具体的内存操作算子避免直接调用时遇到空指针崩溃。两套实现逻辑相同同样受CONFIG_VIDEO_ADV_DEBUG控制——开启时带日志与计数统计关闭时精简为极致高效的三目运算符。唯一的区别call_memop调用的算子如mmap通常返回的是整型状态码成功返回0失败返回负数错误码。call_ptr_memop调用的算子如alloc通常返回的是一个指针成功返回分配好的内存私有结构体指针失败或未实现则返回NULL。它们在内核里都是典型的多态分发宏核心本质都是在做“先查指针是否存在存在则传参调用不存在则返回安全默认值0 或 NULL”。mem_privcall_ptr_memop(vb,alloc,q-alloc_devs[plane]?:q-dev,q-dma_attrs,size,q-dma_dir,q-gfp_flags);retcall_memop(vb,mmap,vb-planes[plane].mem_priv,vma);这两段代码展示了 VB2 框架在生命周期的两个不同阶段通过高度统一的宏call_ptr_memop与call_memop去调度底层内存算子的典型应用。一、 核心对比表维度第一段 (alloc阶段)第二段 (mmap阶段)宏调用的操作alloc分配mmap内存映射调用的宏名称call_ptr_memopcall_memop返回值类型指针void *或结构体指针整型状态码int0 表示成功发生的时间点应用程序执行VIDIOC_REQBUFS申请缓冲区时应用程序对设备节点执行mmap()系统调用时操作的核心目标“向系统申请物理内存”“将物理内存挂载到用户空间”二、 它们的最后目的这两者的最终目的串联起来正是现代 Linux 多媒体架构实现“零拷贝Zero-Copy高效图像采集”的全过程1. 第一段代码的目的向底层申请物理内存alloc动作通过call_ptr_memop调用底层分配器如vb2_dc_alloc。目的在内核空间为摄像头硬件开辟一块用于存放视频帧的物理缓冲区RAM并将底层分配器返回的内存私有句柄mem_priv保存到vb-planes[plane].mem_priv中。结果此时硬件的 DMA 已经有了可以写入的目标物理内存地址。2. 第二段代码的目的打通用户态与内核态的桥梁mmap动作通过call_memop调用底层映射算子如vb2_dc_mmap。目的把第一步申请好的、属于内核和硬件的物理内存通过修改进程页表remap_pfn_range直接“映射”到用户态应用程序的虚拟地址空间vma中。结果应用程序拿到指针后可以直接在用户态读取这块内存。当摄像头硬件通过 DMA 把图像写进这块内存时用户态程序无需进行任何额外的内存拷贝就能立刻看到最新的图像数据。