尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux驱动入门实战:从内核模块到字符设备排障

Linux驱动入门实战:从内核模块到字符设备排障 如果你第一次接触Linux驱动八成是被这几个问题拦住的usb转串口的小板子插上去没反应STLink连不上开发板或者自己写的第一个hello模块一加载就报错。这篇东西就是要把Linux驱动这层窗户纸捅破从一个实用主义者的角度讲清楚驱动的原理、框架和实战排障。不管你是嵌入式开发新人、运维工程师还是单纯想搞懂内核模块是怎么回事都可以照着往下折腾。我早年也被“驱动”这两个字吓得不轻总觉得这是内核大牛才能碰的东西。后来真正上手才发现Linux驱动的入门门槛并没有想象中那么高反而因为内核把很多脏活累活包掉了写一个最小可用的驱动可能比写一个像样的业务系统还简单。关键是你要理解它运行的上下文——驱动不是独立跑的程序它寄生在内核里为一类硬件的操作提供统一入口。1. Linux驱动到底在解决什么问题1.1 用户态与内核态的边界先说点基础的。CPU把运行空间分成用户态和内核态普通应用程序跑在用户态像浏览器、VSCode、你写的Python脚本都在这个圈子里。而内核态是操作系统自己待的地方拥有最高权限可以直接操作物理硬件、内存、中断。这两者之间是隔离的用户程序想读写硬件不能直接操作物理地址必须通过系统调用请求内核帮忙。驱动就是内核里那层专门跟硬件打交道的代码。它的核心作用是把千奇百怪的硬件行为抽象成统一的接口让上层程序可以像读文件、写文件一样操作设备。比如你在Linux里用open、read、write去操作一个串口设备底层就是驱动在帮你把文件系统层面的操作翻译成寄存器的读写时序。这也是为什么驱动不能随便写在普通程序里。你想想普通程序连内存错误都可能导致段错误退出如果硬件操作也这么随性一个野指针直接踩到物理地址上轻则驱动崩溃重则整个系统panic。所以内核设了一条清晰的红线硬件访问必须在内核态完成用户态只能提交请求。1.2 驱动家族的三大类字符、块、网络Linux驱动的分类从文件系统接口的角度看最常见的就三类字符设备按字节流方式读写就像一个水管数据一个字节一个字节流过去。串口、GPIO、I2C、SPI、LED、按键、USB转串口芯片基本都是字符设备。这也是大部分入门者最早接触的类别。块设备按固定大小的块读写有缓冲区、有寻址典型的就是硬盘、SSD、U盘。读写路径比字符设备复杂得多有page cache、IO调度、块层一堆东西。网络设备不走文件系统接口走的是协议栈的收包发包路径比如网卡。它连设备节点都没有所有交互都通过内核网络子系统完成。日常听到的Linux驱动热词绝大多数都落在字符设备这个篮子里。CH340串口驱动、STLink、JLink、LCD显示驱动、触摸屏驱动本质上都是字符设备或字符设备配合其他子系统的产物。理解字符设备驱动框架等于拿到了驱动开发的主钥匙。1.3 驱动以什么形态存在内置模块与可加载模块驱动在Linux里有两种存在方式。一种是直接编译进内核开机就常驻另一种是编译成内核模块后缀是.ko系统启动后随时可以用insmod或modprobe加载不用了再rmmod卸载。模块化是Linux驱动最常见的形态也是开发调试最方便的方式。因为内核编译一次时间很长如果你每次改驱动都要重新编译整个内核再重启那效率太低了。编译成模块之后驱动代码改完只需要编译单个模块加载进当前运行的内核就能测试出问题顶多把模块卸载不用重启机器。模块文件放在哪里也有讲究。你可以在/lib/modules/$(uname -r)/下看到当前内核配套的模块库内核自带的驱动都在这里。编译外部模块的时候如果希望被modprobe自动依赖管理通常会配合depmod使用如果只是临时调试insmod指定.ko路径就够了。2. 字符设备驱动框架从零手写一个最小驱动2.1 设备号与设备节点驱动与文件系统的桥梁字符设备驱动有一个绕不开的概念设备号由主设备号和次设备号组成。主设备号用来区分驱动类型比如同一种驱动芯片的设备共享一个主设备号次设备号用来区分同类型下的不同实例。驱动注册时分配设备号系统再通过mknod或udev创建设备节点把设备号和/dev/下的文件关联起来。设备节点是一个很巧妙的设计。从用户态看/dev/ttyUSB0就是一个文件open它、read它、write它逻辑上跟操作普通文件没什么区别。实际上文件系统层会通过设备的inode找到主设备号和次设备号再通过这两个数字找到对应的驱动文件操作接口。这样一来硬件设备被成功伪装成文件这就是Linux“一切皆文件”哲学的硬件版本。2.2 最小驱动代码解读光说不练假把式直接看一个最小的字符设备驱动。我用的是miscdevice框架它是字符设备的轻量封装适合驱动数量不多、只需要一个主设备号的场景比如各种小传感器、小外设。完整代码如下#include linux/module.h #include linux/miscdevice.h #include linux/fs.h static int demo_open(struct inode *inode, struct file *file) { return 0; } static ssize_t demo_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { return 0; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .read demo_read, }; static struct miscdevice demo_misc { .minor MISC_DYNAMIC_MINOR, .name demo_dev, .fops demo_fops, }; static int __init demo_init(void) { return misc_register(demo_misc); } module_init(demo_init); static void __exit demo_exit(void) { misc_deregister(demo_misc); } module_exit(demo_exit); MODULE_LICENSE(GPL);代码逻辑按顺序看就三条线init入口调用misc_register注册一个misc设备注册时带上设备名字demo_dev和file_operations结构体file_operations结构体里填了open和read两个回调函数exit入口调用misc_deregister注销。加载之后/dev/demo_dev这个节点就有了open它不会报错read它返回0字节。这里有个容易忽略的细节read回调里的buf参数带__user修饰。这个修饰不是装饰用的它提醒你buf是用户态地址内核代码绝不能直接解引用。正确做法是通过copy_to_user、copy_from_user这类专门的函数拷贝数据内核会处理地址合法性检查和缺页异常。我见过不少新手在这里直接memcpy然后系统悄悄崩溃或者数据错乱这就是犯了内核编程的大忌。2.3 编译与加载Makefile与insmod实操写驱动和写应用编译方式完全不同。应用靠gcc直接编驱动必须用内核的Kbuild构建系统。下面这个Makefile是驱动开发最常用的模板obj-m : demo_dev.o KERNELDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean关键在obj-m这一行它告诉Kbuilddemo_dev.o要编译成独立可加载模块。KERNELDIR指向当前内核的构建目录在Ubuntu这类发行版上没有的话先装linux-headers包否则make会直接报错找不到目录。M$(PWD)表示去当前目录找模块源码。编译完成会生成demo_dev.ko加载顺序是sudo insmod demo_dev.ko ls /dev/demo_dev sudo rmmod demo_dev加载后立刻看dmesg有没有报错再看/dev/demo_dev是否出现。我建议这里用dmesg -w开一个滚动窗口一边加载一边盯日志驱动的问题几乎都会在这里现原形。2.4 设备节点自动创建udev与mdev手动mknod创建节点在服务器上偶尔用但在实际项目里太不现实了因为主次设备号你记不住设备插拔顺序一变节点就对不上。Linux下设备节点的动态创建靠的是udev桌面/服务器或mdev嵌入式busybox环境。udev的玩法是内核检测到某个设备会通过uevent上报信息udev根据规则匹配自动在/dev/下建立节点。比如你要让USB转串口芯片插入后直接变成0666权限、归plugdev组可以在/etc/udev/rules.d/下写一条规则KERNELttyUSB*, MODE0666, GROUPplugdev写完规则执行udevadm control --reload重新插拔设备就生效了。这个技巧在嵌入式调试线上非常常用尤其是STLink、JLink这类设备默认权限不够就会报“无法打开设备”加一条udev规则就清净了。3. 热词背后那些真实驱动场景一步步落地3.1 USB转串口CH340、CP2102、FT232到底要不要装驱动这个可能是被问得最多的问题。很多人从Windows换到Linux插上一个CH340模块发现系统没有任何反应第一反应就是找驱动装驱动。这里先说结论绝大多数情况下Linux内核已经自带这些芯片的驱动你不需要从厂商网站下任何东西。CH340对应内核模块ch341CP2102对应cp210xFT232对应ftdi_sio。插上设备后执行lsusb看USB设备列表会出现相应芯片的vendor/product信息再执行dmesg | tail能看到usb 1-1: ch341-uart converter now attached to ttyUSB0之类的行。看到这些设备节点就有了直接用minicom、pyserial、或者你的业务程序去操作/dev/ttyUSB0就行。真正难的是两件事。第一权限问题默认ttyUSB0只有root能读写普通用户总是报Permission denied。解决办法就是前面那条udev规则把MODE改成0666。第二芯片识别不出来lsusb里压根没有设备这时候九成不是驱动问题而是板子本身的USB转串口电路没供电或者Type-C线只支持充电不支持数据传输。我踩过一次特别典型的坑模块指示灯亮但lsusb就是没东西换了根带数据线的线立马就好了。3.2 STLink与JLink调试器在Linux下的驱动与权限STLink是ST芯片常见的调试器JLink是SEGGER出品的通用调试器。它们走的是USB协议Linux下驱动分两层USB底层的识别由内核usb子系统完成上层调试协议由安装的软件包提供。STLink在OpenOCD项目下很常用。装好openocd之后把STLink插上如果提示找不到设备或权限不足基本是udev规则没配置。OpenOCD官方仓库提供了contrib/60-openocd.rules拷贝到/etc/udev/rules.d/下然后重新加载规则并重插设备。规则内容大致是匹配STLink的USB Vendor ID 0483设置MODE0666SUBSYSTEMSusb, ATTRS{idVendor}0483, ATTRS{idProduct}374b, MODE0666, GROUPplugdevJLink更省事SEGGER官方提供了Linux安装包装上之后自带的99-jlink.rules会注册到udev设备节点是/dev/jlink用JLinkExe就能连上目标板。我的经验是JLink在Linux下的稳定性其实比Windows还好命令行工具足够强大配合gdb做调试体验相当流畅。3.3 从NT35310看显示类驱动的玩法热词里有NT35310这是一颗LCD控制芯片常见于各种小尺寸屏。这类显示驱动跟串口驱动完全不同难点不在字符设备接口而在显示初始化和帧缓冲。Linux下显示设备驱动通常分两个层面底层是DRM/KMS框架负责控制扫描时序、显示控制器、显示接口用户在应用层访问的是/dev/fb0这类帧缓冲设备直接往里面写像素数据屏幕就会显示内容。对工程师来说最痛苦的部分是初始化序列你要对着芯片手册把一堆寄存器初始化命令按顺序灌进去错一个寄存器屏幕颜色就可能是花的。这类驱动的调试经验就一句话先把硬件点亮再谈画图。如果屏幕一直是白屏或黑屏优先查背光、复位时序、SPI/DBI接口的通信如果屏幕能显示但颜色不对查RGB的位宽和字节序如果显示出现偏移或撕裂查扫描时序的porch参数。手写初始化数组时建议每写一步就看一次示波器波形不要一口气灌完全部才验证不然排查起来真的要命。3.4 电机驱动与PMSM另一种“驱动”的语义陷阱热词里还有PMSM驱动板、L293D电机驱动这类词这里有一个很容易混淆的概念很多人口中的“电机驱动”指的是电机驱动电路和功率控制算法比如PMSM的FOC控制、L293D这种H桥芯片核心是硬件方案和单片机的PWM控制逻辑跟Linux内核驱动没有直接关系。但Linux下面确实可以做电机控制只是路径不一样。最常用的是通过GPIO子系统控制方向引脚通过PWM子系统控制速度让内核帮我们做最底层的寄存器操作业务控制逻辑放在用户态或嵌入式系统里。比如树莓派上控制直流电机先通过设备树把GPIO和PWM控制器配置好然后在用户态用sysfs或libgpiod写电平就能转动电机。这算是一种“不写驱动用驱动”的思路。如果你做的是一个Linux主控的机器人项目建议优先用这种内核子系统提供的现成接口不要自己撸一个自定义驱动程序。内核已经帮你把最杂乱的寄存器、时钟、引脚复用都封装好了你再去重复造轮子等于给自己找罪受。4. 驱动排障日志、工具与经验4.1 dmesg是排障第一入口驱动问题跟应用问题最大的不同是应用可以断点调试可以看堆栈驱动一出问题系统可能直接崩或者悄无声息地没有任何表面症状。这时候dmesg就是你的第一情报源内核的绝大部分驱动提示、错误、警告都会往这里打。dmesg -w加载/卸载模块、插入USB设备、访问设备节点都会在滚动日志里留下踪迹。比如insmod报Unknown symbol那八成是模块依赖的内核符号没导出或版本不匹配比如访问设备节点报No such device可能是设备号分配失败或硬件根本没探测到比如打开设备卡住可能是驱动的open回调无限等待资源。我习惯的做法是在驱动代码里多放几条printk内核版printf加载前dmesg -c清空历史加载后立刻dmesg看结果。每一条关键路径都打印日志可以快速缩小问题范围。等调试完成再根据实际情况删掉或改成dev_dbg这类可动态开关的日志宏。内核动态调试也是一个利器echo file demo_dev.c p /sys/kernel/debug/dynamic_debug/control就可以打开文件里的dev_dbg不用重新编译非常实用。4.2 驱动常见问题速查这些年折腾驱动我把高频问题整理成了一个速查表分享出来现象可能原因排查方向insmod报Unknown symbol依赖的符号未导出、内核版本不一致、依赖模块未加载先modprobe依赖模块检查内核版本头文件是否匹配设备节点出现但open失败设备号冲突、注册回调有误、设备未就绪dmesg看注册信息检查misc_register返回值open成功但read/ioctl没反应回调没实现、回调返回错误、上层传参有问题在回调入口加printk确认是否被调用USB转串口不出现ttyUSB0芯片无供电、线材问题、内核无对应驱动lsusb确认设备枚举、dmesg看usb子系统日志设备节点权限拒绝默认权限过严添加udev规则设置MODE0666加载模块后系统卡死中断没释放、自旋锁死锁、错误操作了硬件检查中断请求、锁的获取顺序、硬件访问时序虚拟机里USB设备不工作USB passthrough未开启或HUB端口没挂载确认虚拟机管理器里的USB重定向设置建议把这几个场景记在心里遇到问题直接对着表格先排除一圈。尤其是Unknown symbol和权限拒绝占了我遇到的驱动问题的一大半。4.3 几个容易误判的场景有些问题表面看是驱动问题实际根本不是。举三个例子第一wsl里删除文件后空间没释放。这个问题在热词里出现了它其实跟驱动没半点关系是WSL2的虚拟磁盘默认不会自动缩容。你删了文件文件系统层面空间释放了但vhd文件的大小没有变化所以宿主机看磁盘占用还是老样子。解决办法是在Windows侧用diskpart或wsl --manage命令去压缩虚拟磁盘而不是折腾Linux内部的驱动。第二应用层报读写超时但驱动日志完全正常。这种大概率是硬件线缆问题或者对方设备没工作。驱动只是把数据搬到线上线上的电气状态、协议解析、时序驱动管不了那么多。遇到这类问题先回去检查硬件接线和对方设备的日志别老盯着内核代码怀疑。第三模块加载成功dmesg也正常但应用就是收不到数据。这种往往不是驱动问题是应用打开错了设备或者打开了同一主设备号下的错误次设备。多路串口转出来的ttyUSB0、ttyUSB1、ttyUSB2顺序可能因为USB拓扑变化而改变你以为是第一个实际上可能是第二个。这时候要按USB设备路径创建符号链接固定设备名比如/dev/serial/by-id/下的软链接。收个尾分享一点个人体会驱动这东西入门难在概念多、环境复杂但真正动手写起来反而容易让人上头。我个人的建议是别一上来就啃内核源码大部头先把手头常用的硬件驱动用起来、调通比如让CH340转出的串口能被你的Python脚本读写让STLink能通过OpenOCD连上单片机让一块小屏幕能通过/dev/fb0显示一张图片。等你对这些“现成驱动”的使用路径烂熟于心再回头写自己的最小驱动会顺畅非常多。驱动开发里有一个很朴素的规律内核已经把百分之八十的通用细节处理掉了真正需要你操心的是你那块特定硬件的初始化、数据通路和错误处理。就像你不需要重新发明USB协议才能用USB鼠标一样你也不需要从头写一个操作系统才能写驱动。把系统提供给你的接口研究透彻剩下的就是对着芯片手册一点点补全属于你的那部分逻辑。
返回列表