ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

V4L2报错ENOSPC:从USB带宽到缓冲区的排查实战

V4L2报错ENOSPC:从USB带宽到缓冲区的排查实战 先说结论这个报错在V4L2摄像头开发里出现频率极高尤其是在嵌入式平台、树莓派或者同时挂载多个USB摄像头的场景下。很多人第一次遇到时会被英文报错误导以为真的是磁盘满了其实它和存储空间几乎没有任何关系。我最早是在做智能车摄像头循迹项目时踩进这个坑的当时排查了整整一下午最后发现是USB带宽分配的问题。这篇文章就把这个错误的原理、排查思路和解决方案完整讲透保证你看完之后再遇到它能少走弯路。1. 从报错说起这个错误到底卡在哪一步1.1 V4L2的采集流程里STREAMON扮演什么角色理解这个报错的前提是搞清楚V4L2Video for Linux 2的完整采集流程。我习惯把它拆成四步打开设备节点open /dev/videoX设置采集格式VIDIOC_S_FMT指定分辨率、像素格式申请缓冲区并映射VIDIOC_REQBUFS、VIDIOC_QUERYBUF、mmap开始采集VIDIOC_STREAMON然后循环VIDIOC_DQBUF取帧、VIDIOC_QBUF回填VIDIOC_STREAMON这个ioctl的作用是告诉驱动“我要开始真正搬运数据了”。在这之前应用层和驱动之间的buffer已经协商好内存也映射完成但这个调用一旦发出驱动就会真正启动DMA传输或者USB的URB传输把摄像头传感器采到的数据源源不断地搬到内存里。报错发生在这个节点意味着前面所有配置都通过了但在“启动数据传输”这一瞬间系统认为没有足够的资源让驱动开始干活。换句话说资源协商阶段都还正常真正动手的时候发现不够用了。1.2 “No space left on device”的误导性这个报错字符串对应的errno是ENOSPC在文件系统语义里是“磁盘空间不足”。但在V4L2的语境下它压根不是磁盘的事。内核的V4L2核心框架在vb2_streamon返回错误时会直接把这个errno传递给用户空间而很多驱动在底层返回ENOSPC时含义是“无法为数据流分配足够的DMA描述符、URB或者带宽”。我记得有两个典型场景最容易制造这种误导场景A嵌入式设备上/dev/shm空间不足mmap映射buffer时正常但驱动在streamon阶段尝试分配额外的内存或使用共享内存页时失败返回ENOSPC。场景BUSB摄像头在streamon时USB核心无法为所有端点分配足够的等时传输带宽返回ENOSPC。所以排查方向绝对不能只盯着磁盘而是要把视野放宽到内存、共享内存、USB带宽、DMA内存池这些真正和“设备空间”相关的资源上。2. 最常见的元凶USB带宽被占满2.1 为什么USB摄像头会触发ENOSPC如果是USB接口的摄像头这个错误的头号原因就是USB带宽不足。USB 2.0协议中等时传输Isochronous Transfer模式被视频设备广泛使用它的总带宽是固定的。以USB 2.0为例理论带宽480Mbps但实际等时传输可用带宽大约只有384Mbps因为协议本身有开销还要给控制传输、中断传输留位置。一个典型的USB 2.0摄像头在YUYV格式、640x48030fps时需要的带宽大约是640×480×2×30 18,432,000字节/秒约等于147Mbps。听起来离384Mbps还远但如果同时挂两个摄像头带宽占用就接近300Mbps了。再加上USB控制器还要处理鼠标、键盘、无线网卡的数据一来二去就会触顶。我在树莓派上做过实验一个罗技C270用640x48030fps YUYV没问题再加一个免驱USB摄像头同样参数立刻报VIDIOC_STREAMON: No space left on devicedmesg里能看到usb 1-1.2: bandwidth allocation failure的提示。2.2 用lsusb -t定位带宽分配排查USB带宽问题我最先用的命令是lsusb -t。它能把USB拓扑树和每个设备占用的带宽直观列出来lsusb -t输出大概是这样的/: Bus 01.Port 1: Dev 1, Classroot_hub, Driverxhci_hcd/4p, 480M |__ Port 2: Dev 2, If 0, ClassVideo, Driveruvcvideo, 480M |__ Port 3: Dev 3, If 0, ClassVideo, Driveruvcvideo, 480M看到多个ClassVideo的设备挂在同一个root hub下面就要警惕带宽冲突了。USB 2.0的hub下所有设备共享480MbpsUSB 3.0的hub向下兼容2.0时也要共享带宽。如果条件允许最好的办法是让两个摄像头分别插在独立的USB控制器上。Intel主板通常有两个USB控制器一个USB 3.0 xHCI含2.0设备、一个USB 2.0 EHCI把摄像头分散插到不同控制器上带宽立刻翻倍。注意树莓派4B只有一个USB控制器所有USB口共享同一个总线。这种情况下指望换口没用只能从降低单个摄像头的带宽需求入手。2.3 降低带宽占用的三个手段带宽不够那就压缩需求。我在实践中用过三种手段按推荐程度排序第一个手段是切换像素格式。从YUYV换成MJPEG。YUYV是未压缩格式一帧640x480的数据量是614,400字节MJPEG是硬件压缩格式同样分辨率一帧通常只有50KB到100KB取决于画面复杂度带宽直接降到原来的1/8到1/12。改法很简单用v4l2-ctl验证v4l2-ctl --device/dev/video0 --list-formats-ext如果输出里有MJPEG就用它。OpenCV里设置方式如下cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G))实测用MJPEG格式同样一个USB 2.0控制器下带两个720p30fps摄像头都稳定换成YUYV连一个都卡。第二个手段是降低帧率和分辨率。如果应用场景不要求高帧率30fps降到15fps带宽需求直接减半。分辨率从1080p降到720p比降帧率更立竿见影。第三个手段是减少同时打开的摄像头数量。如果业务非要三路视频可以考虑用硬件方案一个支持多通道的采集卡或者干脆换用USB 3.0摄像头并确保插在USB 3.0口蓝色接口上。3. 被忽略的坑buffer与共享内存不足3.1 mmap模式下缓冲区到底存在哪USB带宽排查完之后如果问题还在就要考虑buffer层面的原因了。V4L2的buffer通过mmap映射到用户空间但物理内存页是驱动分配的。对于UVC这类USB摄像头驱动使用urb sg列表来组织数据传输每个urb都对应一组内存页。这一块的内存压力分为两层第一层是“高端内存/DMA内存池”不足。嵌入式平台树莓派、各种开发板上CMAContiguous Memory Allocator区域有限如果系统其他地方占用了大量连续内存驱动在streamon阶段申请DMA buffer就会失败。第二层是系统总内存不足。申请多个buffer时每个buffer可能只是几MB但有些驱动会为每个buffer额外申请描述符表、urb数组这些小对象乘起来也相当可观。问题不一定会出现在REQBUFS阶段反而经常出现在STREAMON阶段因为很多驱动是在streamon时才真正为urb分配内存。这就是为什么报错和buffer申请代码可能隔得很远。3.2 /dev/shm空间不足的典型案例这个坑我在树莓派上踩得比较深值得单独拿出来讲。有时候摄像头是通过某种中间层去访问的比如某些框架在底层会用POSIX共享内存来做零拷贝传输。如果/dev/shm已经被其他服务占满摄像头驱动的buffer映射也会变相受影响。有一次我在树莓派上同时跑了摄像头采集和人脸识别服务人脸识别服务把特征库和临时数据全堆到/dev/shm结果摄像头一开就报错。用下面命令一看df -h /dev/shm发现/dev/shm已经用了98%。清理掉部分临时文件后摄像头立刻恢复正常。处理办法有两种一是定期清理无用的共享内存文件/dev/shm下的临时文件二是通过/etc/fstab调整/dev/shm的大小。在树莓派的/boot/config.txt里增加tmpfs /dev/shm tmpfs defaults,size128M 0 0或在树莓派系统中使用systemd方式修改挂载参数。具体的挂载调整要小心不能影响系统已有共享内存服务。3.3 buffers数量的调整V4L2的buffer数量是通过REQBUFS申请的数量太少也会导致streamon时驱动认为无法调度。我见过某些驱动必须至少申请4个buffer否则在streamon时报错。申请代码类似于这样struct v4l2_requestbuffers req; req.count 4; // 有些板子必须 4 req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { perror(VIDIOC_REQBUFS); return -1; }如果count设为4仍然报错可以尝试增加到6或者8。不过要注意buffer数量越多内存占用越大同时延迟也可能变高。具体的平衡点要结合平台来调。经验法则在内存充足的PC上buffer设6个是甜点值在内存只有512MB的嵌入式板子上先试4个不行再降分辨率而不是降buffer。4. 实战排查一条条命令挖出真凶4.1 dmesg永远是最先看的无论报错怎么提示dmesg是定位底层问题的第一站。STREAMON报ENOSPC时内核往往会在日志里输出更具体的信息。每次遇到这个问题我的习惯是先清空日志再复现sudo dmesg -C # 运行你的摄像头程序触发错误 sudo dmesg | tail -n 50常见的日志有这几种对应的方向也不同内核日志关键字大概率问题处理方向usb 1-1.2: bandwidth allocation failureUSB带宽不足降低格式、帧率或换USB控制器uvch264 encoder: Out of memoryUVC驱动内存不足减少分辨率或buffer数释放系统内存alloc_contig_range failedCMA连续内存不足修改内核CMA大小或减少并发流shmem filesystem: No space left/dev/shm不足清理共享内存文件调整tmpfs大小v4l2: more than 128 buffers allocatedbuffer数量超过限制减少REQBUFS的count值4.2 v4l2-ctl的快速验证v4l2-ctl是排查摄像头问题最趁手的工具可以直接用它复现STREAMON调用省去写代码的步骤。先查看当前格式v4l2-ctl --device/dev/video0 --get-fmt-video然后尝试用指定格式和大小启动流v4l2-ctl --device/dev/video0 --set-fmt-videowidth640,height480,pixelformatMJPG --stream-mmap --stream-count30如果这条命令能正常运行说明底层采集没问题问题大概率出在应用层的格式协商或者资源调用上。如果这条也报No space left on device那基本可以肯定是驱动、内核或设备端的资源问题。v4l2-ctl还能用来检测摄像头支持的所有格式不少设备默认输出YUYV但实际支持MJPEG切换后带宽压力小很多。4.3 从设备树到驱动层面的检查嵌入式平台上的排查逻辑稍有不同。我遇到过的情况里有一半是设备树配置问题另一半是驱动编译时DMA内存配置不合理。先看摄像头在设备树里的节点cat /proc/device-tree/soc/i2c0mux0/i2c0/camera3c/status如果系统里同时启用了太多需要DMA的设备比如摄像头、显示器、硬件编码器CMA区域的竞争会很激烈。查看CMA剩余空间cat /proc/buddyinfo这个命令的输出能反映连续页分配情况。如果高阶内存块很少说明连续内存碎片化严重或CMA不足。在树莓派上可以调整CMA大小增强连续内存可用量。修改/boot/config.txt# 把CMA从默认值加大到256M cma256M改完重启再测试。这个参数在一些其他SoC平台比如全志、瑞芯微上可能是通过内核cmdline传入的格式为cma256M具体位置取决于平台。注意加大CMA会减少普通内存的可用量如果你的系统总内存只有512M建议先从128M试起别一下加太多。5. 代码层面的规避策略5.1 申请buffer时的失败处理即便都排查清楚了代码里也应该做好防守。我见过的很多项目在VIDIOC_REQBUFS失败后直接perror退出这在嵌入式环境里太脆弱了。更好的做法是降级重试先用高分辨率申请失败后自动降一档。这里有一个我比较常用的降级策略int try_request_buffers(int fd, int *buf_count) { struct v4l2_requestbuffers req {0}; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; // 先尝试用户想要的buffer数 req.count *buf_count; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { // 失败后从高到低逐个尝试 for (int cnt *buf_count - 1; cnt 2; cnt--) { req.count cnt; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { *buf_count cnt; return 0; } } return -1; } // 驱动可能给出比请求更少的buffer数以驱动返回为准 *buf_count req.count; return 0; }这样即使资源紧张程序至少能以较低的buffer数量跑起来而不是直接崩溃。5.2 多点数据流的带宽预算如果你的应用像智能车那样要同时处理多个摄像头我强烈建议在代码里写一个“带宽预算”表。把每个摄像头的像素格式、分辨率、帧率、格式类型压缩/非压缩都列出来估算总带宽。带宽估算公式如下YUYV/未压缩格式宽度 × 高度 × 2字节 × 帧率MJPEG/H.264压缩格式平均码率H.264可以通过码率控制MJPEG看画质一般假设每帧30KB到100KB假设两个摄像头一个720p MJPEG30fps约6Mbps一个1080p H.26430fps约8Mbps加起来14MbpsUSB 2.0绰绰有余。但如果两个都是YUYV 1080p30fps每个就要995MbpsUSB 2.0根本扛不住甚至USB 3.0都吃紧。在程序启动时做一个简单的总带宽校验超过阈值直接报警并建议降低参数能省掉一大堆现场排查的时间。我习惯在配置文件的注释里写清楚每个参数对应的带宽占用方便团队其他人上手。5.3 mmap映射失败时的后备方案如果mmap映射失败导致streamon异常还可以考虑切换到USERPTR模式。USERPTR模式下由应用自己分配buffer内存驱动通过用户指针去访问。在一些DMA能力受限的平台上USERPTR能绕过驱动连续内存分配的问题。不过USERPTR引入的问题也不少缓存一致性、内存对齐要求、驱动支持度。更稳妥的方案是尝试read/write模式V4L2_MEMORY_READWRITE驱动内部帮你搬运数据应用不需要管理mmap。虽然性能差一些带宽小的场景下完全够用。struct v4l2_requestbuffers req {0}; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_USERPTR; req.count 4;如果驱动不支持USERPTR这个调用本身就会返回错误到时候再退回到MMAP也是可以的。6. 常见问题速查与经验总结6.1 问题排查表把我在实际项目中遇到的几种情况汇总一下方便大家直接对着排查报错上下文复现规律根因解决手段多路USB摄像头同时打开报错开第二个摄像头必现USB带宽不足改用MJPEG或换USB控制器/降低帧率树莓派长时间运行后报错运行几小时后出现/dev/shm被临时文件占满清理/dev/shm增大tmpfs标注为DO-178的采集卡在128M内存板卡上报错频率不定CMA连续内存不足加大CMA区域降低分辨率OpenCV偶尔报错v4l2-ctl正常随机复现buffer数量不足或应用层格式协商错误检查程序里FOURCC设置增加buffer数系统刚启动时报错运行一段时间后正常启动阶段其他驱动尚未释放USB带宽等待USB设备稳定后再初始化摄像头长时间运行高分辨率流量时报错温度升高后部分芯片存在USB控制器过热降级加散热片或降低负载测试最后一行是我在一个工业相机项目里发现的奇怪现象摄像头在高温下偶发STREAMON失败dmesg完全无记录最后是给USB控制器加了个散热片解决的。这种玄学问题比例很低但也不能完全忽略。6.2 实操心得说几个我在多次调试中得出的经验不一定写在文档里但确实有效。第一个心得先查环境再查代码。任何摄像头采集程序跑不通先跑v4l2-ctl先看dmesg先检查/dev/shm然后再去看代码。代码写错的概率远低于环境资源出问题的概率尤其当你的代码以前还能跑的时候。第二个心得Bayer格式如SBGGR8能省带宽但不能省内存。单反相机模组输出Bayer格式时每像素只有1字节比YUYV省一半带宽但应用层必须做demosaic转换。在嵌入式平台上这个转换的CPU开销远超省下的那点带宽反而是MJPEG让摄像头内部完成压缩更合算。第三个心得学会用strace定位应用层还是驱动层问题。strace能看到ioctl的完整调用序列和返回值能确认报错到底是哪个ioctl返回的strace -e ioctl -o /tmp/v4l2_trace.txt ./your_camera_app然后在trace文件里搜ENOSPC就能看到是VIDIOC_STREAMON、VIDIOC_REQBUFS还是其他调用出了问题。这比瞎猜快得多。第四个心得如果摄像头应用是跑在虚拟化或容器里先检查宿主机USB重定向配置。虚拟机的USB设备重定向和物理机直插完全不同带宽和内存模型都有区别这种环境下的ENOSPC往往要同时看宿主机日志才能定位。6.3 最后的避坑建议我刚入行时遇到这个问题差点把摄像头驱动的源码从头到尾读一遍。后来想通了V4L2报错的思路应该是由外到内、由资源到代码、由应用到驱动。系统资源就那么几样USB带宽、内存、共享内存、文件描述符一个个排查下来绝大多数问题半个小时内都能定位。如果你正在做智能车、树莓派监控或者多路视觉项目把这篇文章里提到的排查表格存一份出问题按顺序过一遍就行。另外摄像头参数和运行环境强相关同一套代码在x86 PC上没问题、在ARM板子上报错是完全正常的不要怀疑是代码逻辑错了先去确认板子的资源余量。我这几年下来最大的体会是摄像头采集的坑八成不在摄像头本身而在它背后的总线、内存和驱动生态。能把这层机制想明白V4L2报错基本就难不倒你了。
返回列表