005字符设备驱动(下):高级特性实战笔记

005字符设备驱动(下):高级特性实战笔记 上周调试一个车载摄像头模块遇到个头疼的问题应用层想动态调整曝光参数同时还要实时获取图像数据。用read/write不是不行但得定义一堆协议格式效率还低。这时候就该把ioctl、mmap、poll这几个高级特性用上了。从ioctl开始说起ioctl这玩意儿在驱动里就是个“万能接口”专门处理那些不适合用标准读写操作的控制命令。先看个典型场景用户空间想设置摄像头的曝光值。// 驱动端定义命令码#defineCAMERA_MAGICC#defineCAMERA_SET_EXPOSURE_IOW(CAMERA_MAGIC,1,int)#defineCAMERA_GET_EXPOSURE_IOR(CAMERA_MAGIC,2,int)staticlongcamera_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){structcamera_dev*devfilp-private_data;intret0;switch(cmd){caseCAMERA_SET_EXPOSURE:// 这里要检查权限很多安全漏洞都出在这儿if(!capable(CAP_SYS_ADMIN)){return-EPERM;}if(copy_from_user(dev-exposure,(int__user*)arg,sizeof(int))){return-EFAULT;// 用户空间指针一定要校验}// 实际设置硬件的代码write_reg(dev,EXPOSURE_REG,dev-exposure);break;caseCAMERA_GET_EXPOSURE:if(copy_to_user((int__user*)arg,dev-exposure,sizeof(int))){return-EFAULT;}break;default:// 一定要返回这个别自己瞎编错误码return-ENOTTY;}returnret;}用户空间调用长这样intexposure1000;ioctl(fd,CAMERA_SET_EXPOSURE,exposure);注意_IOW宏的第三个参数是数据类型不是大小这里踩过坑有人写sizeof(int)编译能过运行时大概率出错。mmap让用户空间直接操作设备内存图像传输用read/write太慢了内存映射才是正道。但mmap的实现比ioctl复杂得搞清楚几个关键点。staticintcamera_mmap(structfile*filp,structvm_area_struct*vma){structcamera_dev*devfilp-private_data;unsignedlongoffsetvma-vm_pgoffPAGE_SHIFT;unsignedlongsizevma-vm_end-vma-vm_start;// 第一道防线检查映射范围if(offsetsizedev-buffer_size){dev_err(dev-device,mmap超出范围\n);return-EINVAL;}// 第二道防线检查权限if(vma-vm_flagsVM_WRITE){if(!(dev-buffer_flagsBUFFER_WRITABLE)){return-EACCES;}}// 关键步骤建立页表映射// remap_pfn_range适用于物理连续内存// 如果是DMA缓冲区用dma_mmap_coherentintretremap_pfn_range(vma,vma-vm_start,(dev-phys_addroffset)PAGE_SHIFT,size,vma-vm_page_prot);if(ret){dev_err(dev-device,remap_pfn_range失败: %d\n,ret);returnret;}// 可选增加引用计数get_device(dev-device);return0;}这里有个大坑vm_page_prot可能包含缓存属性。对DMA缓冲区通常要用pgprot_noncached()关掉缓存否则CPU和DMA看到的数据可能不一致。我在某个车载项目里调试了两天才发现是这个原因。poll/select多路复用等待设备有数据时通知应用层而不是让应用层傻等。poll的实现相对简单但细节要注意。staticunsignedintcamera_poll(structfile*filp,poll_table*wait){structcamera_dev*devfilp-private_data;unsignedintmask0;// 必须调用poll_wait否则唤醒机制不工作poll_wait(filp,dev-read_queue,wait);poll_wait(filp,dev-write_queue,wait);// 检查状态if(dev-data_available){mask|POLLIN|POLLRDNORM;// 可读}if(dev-buffer_available){mask|POLLOUT|POLLWRNORM;// 可写}returnmask;}驱动里触发通知的典型代码// 数据到达时dev-data_available1;wake_up_interruptible(dev-read_queue);// 应用层调用poll后内核会调用驱动注册的poll函数注意wake_up_interruptible和wake_up的区别前者用于可中断等待后者用于不可中断等待。用错了可能导致进程无法被信号唤醒。实战中的坑与经验ioctl命令码管理别手动编命令码一定用_IOC宏。不同驱动用不同的幻数magic number防止冲突。建议在头文件里统一管理内核和用户空间共用同一个头文件。mmap的内存类型物理连续内存kmalloc用remap_pfn_rangeDMA缓冲区dma_alloc_coherent用dma_mmap_coherentvmalloc分配的内存需要先映射到页表比较麻烦硬件寄存器用ioremap后映射poll的唤醒时机数据准备好再唤醒别提前。唤醒太频繁浪费CPU太迟影响实时性。中断处理函数里唤醒最常见。并发控制这三个接口都可能被多个进程同时调用一定要加锁。但mmap里不能睡眠所以用自旋锁ioctl和poll可以用互斥锁。用户空间指针验证ioctl的arg指针、mmap的偏移量必须验证有效性。特别是64位系统32位应用传上来的地址要小心。个人建议驱动开发到最后拼的不是语法而是细节。ioctl的命令码规划好比设计协议前期乱定义后面改起来要命。mmap用对了性能提升明显用错了就是灾难现场。poll看似简单但和中断、定时器的配合要测试充分。实际项目中我习惯先设计ioctl命令集写个文档说明每个命令的用途、参数、返回值。然后实现mmap因为涉及内存布局。最后加poll支持完善通知机制。调试时多用printk_ratelimited别刷爆日志。嵌入式Linux驱动说到底是个桥梁既要理解硬件时序又要懂内核机制还得考虑应用层的方便性。这三个高级特性用好了驱动才算得上“专业级”。下次聊聊怎么把这些驱动放到设备树里配置那又是另一个故事了。