动手写 Linux 内核模块与驱动:从 hello 到 platform 总线(srvD)
本篇基于 srvD(phase6)真实实操。我们写了三个内核模块:第一个模块
hellomod、字符设备globalmem、以及一套 platform 总线设备/驱动,亲眼看到insmod/rmmod时的内核日志、/dev下的字符设备节点,以及 platform 总线"撮合"设备与驱动时probe的调用时机。
1. 环境准备
驱动编译依赖当前运行内核的头文件:
$uname-aLinux ecs-bee4-c6af-00046.8.0-106-generic#106-Ubuntu SMP PREEMPT_DYNAMIC x86_64$apt-getinstall-ybuild-essential kmod"linux-headers-$(uname-r)"$ls/lib/modules/$(uname-r)/build/|head-8每个模块用标准Makefile(make -C $(KDIR) M=$(PWD) modules)调用内核构建系统。
2. 第一个内核模块 hellomod
模块源码里用module_param导出参数who和times,init里打印加载信息,exit里打印卸载信息。
$cd/root/lab/phase6/hellomod&&makemake-C/lib/modules/6.8.0-106-generic/buildM=/root/lab/phase6/hellomod modules CC[M]/root/lab/phase6/hellomod/hellomod.o LD[M]/root/lab/phase6/hellomod/hellomod.ko $ insmod hellomod.kowho='嵌入式学员'times=2$dmesg|tail-4[9496.886426]hellomod: 模块被加载了!(init)[9496.886431]hellomod: Hello, 嵌入式学员!(1/2)[9496.886433]hellomod: Hello, 嵌入式学员!(2/2)通过sysfs还能读出模块参数当前值:
$cat/sys/module/hellomod/parameters/who 嵌入式学员 $cat/sys/module/hellomod/parameters/times2卸载时内核日志显示干净的退出:
$ rmmod hellomod&&dmesg|tail-2[9497.397667]hellomod: 模块被卸载了!(exit)小提示:编译时有一条 “the compiler differs from the one used to build the kernel” 只是信息性提示(本机 gcc 与构建内核的 gcc 小版本一致),不影响加载。
3. 字符设备驱动 globalmem
globalmem是一个经典字符设备:申请设备号、实现read/write、cdev_add后在/dev下生成节点。
$ insmod globalmem.ko&&dmesg|tail-3[9498.481937]globalmem: 申请到设备号major=239minor=0[9498.482029]globalmem: 驱动加载完成, /dev/globalmem 就绪 $ls-l/dev/globalmem crw-------1root root239,0Aug1418:36 /dev/globalmem $grepglobalmem /proc/devices239globalmem用户态直接当文件读写,验证字符设备的"顺序读写"语义:
$echo'Hello 字符设备驱动! 这是用户态写给内核驱动的数据'>/dev/globalmem $cat/dev/globalmem Hello 字符设备驱动!这是用户态写给内核驱动的数据 $printf'第二次写入覆盖前面的内容'>/dev/globalmem&&cat/dev/globalmem 第二次写入覆盖前面的内容globalmem内部用一块内核缓冲区,后一次写覆盖前一次——这就是最简字符设备的样子。卸载后/dev/globalmem节点随之消失。
4. platform 总线:设备与驱动如何"撮合"
platform 总线是嵌入式 SoC 上挂设备(GPIO、I2C、SPI 控制器等)的标准模型。我们的platdemo包含platdev.ko(注册一个名叫demo-led的平台设备)和platdrv.ko(注册匹配该设备的驱动)。
场景一:先注册设备,后注册驱动——驱动注册时总线立即撮合,调用probe:
$ insmod platdev.ko&&insmod platdrv.ko&&dmesg|tail-5[9500.643310]platdrv:>>>probe 成功!匹配到设备 demo-led[9500.643313]platdrv: 平台数据:name=demo-led-1gpio=66[9500.643315]platdrv:(真实驱动会在这里完成 GPIO 申请/时钟使能等硬件初始化)场景二:反过来,先注册驱动,后注册设备——设备注册时才触发probe:
$ insmod platdrv.ko&&insmod platdev.ko&&dmesg|tail-4[9501.188115]platdrv:>>>probe 成功!匹配到设备 demo-led[9501.188120]platdrv: 平台数据:name=demo-led-1gpio=66两种顺序都能probe成功,且gpio=66平台数据正确传递——这正是 platform 总线"设备树/板文件描述资源,驱动匹配后取用"的核心机制。卸载时remove被调用,资源释放:
$ rmmod platdrv;rmmod platdev platdrv:<<<remove: 释放设备 demo-led 的资源 platdev: 平台设备 demo-led 已注销5. 小结
| 对象 | 关键 API | 观察点 |
|---|---|---|
| 模块 | module_init/exit、module_param | dmesg看加载/卸载日志,/sys/module/<name>/parameters看参数 |
| 字符设备 | register_chrdev/cdev_add | /dev下生成节点,/proc/devices列主设备号 |
| platform | platform_device/platform_driver、probe | 设备与驱动谁后到,probe就在谁注册时触发 |
写驱动的乐趣在于:你写的代码跑在内核态,一个insmod就能让dmesg吐出你自己的日志,/dev下多出一个你定义的设备。
系列收官
四台服务器的实验到此全部跑通并验证:
- srvAShell/文件IO:多文件 Makefile、静态/动态库、
fork+execvp迷你 Shell ✅ - srvB进程/线程/IPC:管道/FIFO/消息队列、共享内存竞态、互斥锁/条件变量、
select/epoll✅ - srvC交叉编译/QEMU:ARM 与 x86 双启动,踩平软链接、BINFMT、内核架构三道坑 ✅
- srvD内核模块/驱动:hellomod、globalmem 字符设备、platform 总线 probe ✅
所有命令输出均来自真实远程实操记录(transcripts/*.log),可复现、可审计。