尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux驱动开发:模块加载与内核编译内建两种形态
板子上的驱动能不能跑起来很多时候不取决于代码写得多漂亮而取决于它是以什么形态进到内核里的。我见过太多次这样的场景代码在主机上编译通过.ko拷到目标机insmod一敲返回一句invalid module format或者反过来把驱动塞进内核树里改成内建make menuconfig里翻了三遍找不到自己的选项。这两个方向——模块加载驱动程序和内核编译时添加驱动——是 Linux 驱动开发绕不开的两条路也是新手最容易混为一谈的地方。这篇就按我自己的实操顺序把两条路各自的完整链路、关键细节、以及踩过的坑一次讲清楚。不管你是刚接触字符设备驱动还是已经能把hello world模块跑起来但搞不懂为什么内建之后行为变了下面的内容应该都能对上号。核心关键词会反复出现模块加载、内核编译、Kconfig、Makefile、insmod、initcall一个都不会绕开。1. 驱动的两种落地形态先做这道选择题1.1 从“文件明明在设备却不存在”说起有个现象特别典型把编译好的xxx.ko拷到目标机的/lib/modules/$(uname -r)/extra/下面modprobe xxx却提示Module xxx not found in directory /lib/modules/...。文件就在那儿路径也没错为什么找不到原因往往不是路径问题而是模块索引没更新——modprobe不直接扫文件系统它读的是modules.dep、modules.alias这些由depmod生成的索引文件。你新拷进去的.ko不在索引里modprobe自然视而不见。反过来还有一种情况驱动明明写了module_init内核也编译进去了启动后ls /dev里却找不到设备节点。这多半是把“驱动注册成功”和“设备节点创建成功”当成了同一件事而实际上中间还隔着总线匹配、probe 调用、class 与 device 的创建好几个环节。这两个现象指向同一个底层事实同一份驱动代码以模块形式加载和以内建形式编入内核走的初始化路径、依赖关系、调试手段都不一样。不先把这层差异想明白后面所有操作都是在碰运气。1.2 可加载模块与内建驱动差异远不止“放哪儿”很多人对这两者的理解停留在“一个是.ko文件一个是编进zImage”这个描述没错但太浅。真正影响开发决策的是下面这些维度维度可加载模块.ko内建驱动built-in编译产物独立的.ko文件编入vmlinux/zImage加载时机用户态执行insmod/modprobe内核启动阶段按 initcall 级别自动执行修改成本重编模块 重新加载秒级重编内核 重启分钟到小时符号可见性只能调用内核EXPORT_SYMBOL导出的符号可直接链接内核内部任意非 static 符号内存占用卸载后可释放常驻内存不可释放版本约束受vermagic严格约束与内核同源编译无此问题调试便利性可反复加载卸载改一行试一行每次改动都要重启迭代慢典型适用场景开发调试期、可选外设、第三方驱动启动必需驱动、根文件系统相关、存储与时钟驱动表格里最容易被低估的是符号可见性这一行。模块编译时链接器面对的是一个“半开放”的内核符号表只有被EXPORT_SYMBOL/EXPORT_SYMBOL_GPL标记过的函数你才调得到。我踩过一次很典型的坑某个内核内部函数在头文件里声明得清清楚楚模块里直接调用编译阶段不报错insmod的时候才炸出Unknown symbol in module。而同样的代码改成内建一点问题没有因为它和内核是一起链接的。这个差异在选型阶段不显眼等到你真去调某个内部接口时才要命。另一个常被忽略的是vermagic。模块的.modinfo段里记录了编译时内核的版本字符串、SMP 标志、抢占配置、模块卸载支持等一串信息加载时会和目标内核做逐项比对任一项不一致就拒绝加载。内建驱动不存在这个问题因为它本来就是跟着内核一起编出来的。1.3 我的三条选型判断线第一条线这个驱动是不是系统启动就必须就绪。如果它承担的是存储控制器、根文件系统所在的总线、时钟源、中断控制器这类角色那基本没得选必须内建。道理很直白——模块本身要从文件系统里读出来如果读文件系统所依赖的驱动还是个模块就成了鸡生蛋问题。第二条线这个驱动的硬件是不是确定存在。产品化的板子上焊死的器件配置固定、参数固定内建省心。但如果是一块调试板今天插 A 模块明天插 B 模块或者要通过 USB、PCIe 热插拔那模块形态提供的动态性就是刚需。第三条线你此刻处于哪个阶段。开发期我几乎百分之百用模块原因只有一个字快。改一个printkmake一下三秒钟rmmod insmod两秒钟五秒看到结果。改成内建一次完整的内核编译加烧录重启顺利的话也得十几分钟。这个迭代效率的差距会直接影响你的调试耐心而调试耐心直接决定你能挖多深。真正的工程做法通常是一份代码两种产出开发期用模块跑通逻辑定版时通过配置切换到内建。这个后面第 6 章会展开讲具体怎么组织。2. 模块路线把一份字符设备驱动从零跑通2.1 驱动骨架里那几个不能省的部件先给一份最小可用的字符设备驱动。它做不了什么正事但把模块生命周期、设备号申请、cdev 注册、设备节点创建这几件事全串起来了后面所有复杂驱动都是在这个骨架上长肉。#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEMO_NAME demo_chr #define DEMO_MINOR 1 static dev_t demo_devno; static struct cdev demo_cdev; static struct class *demo_class; static struct device *demo_device; static int demo_open(struct inode *inode, struct file *filp) { pr_info(demo_chr: open, minor%d\n, iminor(inode)); return 0; } static int demo_release(struct inode *inode, struct file *filp) { pr_info(demo_chr: release\n); return 0; } static ssize_t demo_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { char kbuf[] hello from demo_chr\n; size_t n min(len, sizeof(kbuf)); if (*off sizeof(kbuf)) return 0; if (copy_to_user(buf, kbuf *off, n)) return -EFAULT; *off n; return n; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, }; static int __init demo_init(void) { int ret; ret alloc_chrdev_region(demo_devno, 0, DEMO_MINOR, DEMO_NAME); if (ret 0) { pr_err(demo_chr: alloc_chrdev_region failed: %d\n, ret); return ret; } cdev_init(demo_cdev, demo_fops); demo_cdev.owner THIS_MODULE; ret cdev_add(demo_cdev, demo_devno, DEMO_MINOR); if (ret 0) goto err_cdev; demo_class class_create(DEMO_NAME); if (IS_ERR(demo_class)) { ret PTR_ERR(demo_class); goto err_class; } demo_device device_create(demo_class, NULL, demo_devno, NULL, DEMO_NAME); if (IS_ERR(demo_device)) { ret PTR_ERR(demo_device); goto err_device; } pr_info(demo_chr: loaded, major%d\n, MAJOR(demo_devno)); return 0; err_device: class_destroy(demo_class); err_class: cdev_del(demo_cdev); err_cdev: unregister_chrdev_region(demo_devno, DEMO_MINOR); return ret; } static void __exit demo_exit(void) { device_destroy(demo_class, demo_devno); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(demo_devno, DEMO_MINOR); pr_info(demo_chr: unloaded\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(demo); MODULE_DESCRIPTION(minimal char device demo);几个点值得单独说MODULE_LICENSE(GPL)不是可选项。不写的话内核会标记为tainted很多 GPL-only 导出的符号比如kmalloc的部分变体、某些 trace 接口你根本调不到某些内核配置下甚至直接拒绝加载。错误处理用goto分层回收而不是一路return。字符设备初始化涉及四五种资源申请顺序和释放顺序必须严格对称写成嵌套if很快就会乱。这套err_xxx标签的写法在内核源码里到处都是照着抄不会错。class_create的签名在不同内核版本上不一样。老内核是class_create(THIS_MODULE, name)两个参数较新的内核把它简化成了class_create(name)。用错版本编译会直接报参数个数不匹配这个编译期就能发现不算坑。2.2 Makefile 才是模块编译的真正入口.ko不是用gcc xxx.c -o xxx.ko编出来的这一点必须先定性。模块编译依赖内核构建系统因为要拿到内核头文件路径、编译选项、符号版本信息、以及最终的链接脚本。外部模块的标准 Makefile 长这样obj-m : demo_chr.o KDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean install: $(MAKE) -C $(KDIR) M$(PWD) modules_install逐行拆开看obj-m : demo_chr.o的意思是“把demo_chr.o编译成一个可加载模块”。如果写成obj-y同样会被编译但会被链接进内核而不是单独生成.ko——这一行就是第 4 章内建路线的伏笔同样的语法换字母产出完全不同。KDIR指向内核构建树。主机上编译时它是/lib/modules/$(uname -r)/build这是个软链接通常指向/usr/src/linux-headers-xxx这种头文件包。交叉编译时必须显式覆盖成 SDK 里那份完整内核源码路径因为头文件包通常不含构建所需的全套脚本和 Kconfig 体系。这里的区别很多人搞混编译模块需要的是完整的、至少 configure 过的内核源码树不是简单的头文件目录。某些发行版的 headers 包裁剪过make -C进去会报一堆脚本找不到。-C $(KDIR)的意思是切到内核目录执行M$(PWD)告诉内核构建系统的“外部模块在当前目录”。这两个参数缺一不可-C不带M就是编整个内核M不带-C就没有构建规则。交叉编译的实际写法make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- \ KDIR/path/to/kernel-source \ -C /path/to/kernel-source M$PWD modules注意ARCH必须和KDIR指向的那份源码一致否则会去错误架构的目录下找配置。2.3 加载、验证、卸载的完整闭环编译出来是demo_chr.ko接下来这几步我建议按固定顺序走每一步都能定位到特定的失败原因# 1. 先看模块自身信息包括 vermagic modinfo demo_chr.ko # 2. 装载 sudo insmod demo_chr.ko # 3. 确认模块已在内核中 lsmod | grep demo_chr # 4. 确认字符设备号已注册 cat /proc/devices | grep demo_chr # 5. 确认设备节点已生成 ls -l /dev/demo_chr # 6. 实际读写验证 cat /dev/demo_chr # 7. 查看内核日志 dmesg | tail -20 # 8. 卸载 sudo rmmod demo_chr第 1 步的modinfo经常被跳过但它是排查insmod失败最快的入口。输出里的vermagic行包含内核版本、SMP、preempt、模块卸载支持等信息srcversion用于标识源码版本。如果后面insmod报invalid module format第一件事就是拿这里的vermagic和uname -r对一下。第 6 步实际读设备cat /dev/demo_chr能打出hello from demo_chr说明整条链路都通了。这一步如果报Permission denied不是驱动问题是/dev节点的权限问题如果报No such device or address多半是设备号没注册上或者 cdev 没加进去。第 7 步的dmesg要注意很多发行版默认dmesg_restrict1普通用户看不到内核日志需要sudo或者echo 0 /proc/sys/kernel/dmesg_restrict。3. 模块路线上的高频故障与排查顺序3.1 invalid module formatvermagic 对不上的完整链路这个报错几乎每个做驱动的人都遇到过。insmod之后dmesg里会出现一行类似demo_chr: version magic 5.10.0 SMP mod_unload should be 5.10.0-test SMP preempt mod_unload的信息信息量很足但新手容易只看前半段。排查顺序我是这样走的第一比对内核版本串。uname -r的输出必须和vermagic里冒号前面那截完全一致包括-test、-rt这类后缀。很多板子用的是定制内核版本号带后缀用通用发行版的 headers 编出来的模块必然对不上。第二查 SMP 和 preempt。这两个标志来自内核.config。CONFIG_SMP开没开、CONFIG_PREEMPT是什么级别都会写进vermagic。用单核配置的源码树去编双核内核的模块一样会被拒。第三查模块卸载支持。CONFIG_MODULE_UNLOAD没开的内核编出来的模块vermagic里没有mod_unload字样加载到开了这个选项的内核上同样不匹配。如果真的手里只有二进制模块、没有源码还有个modprobe --force-vermagic的强制加载手段但我强烈不建议在正式环境用。强制加载绕过了版本校验但绕过不了 ABI 变化后面在某个结构体偏移上炸出来的内存错误比一开始就拒绝加载难排查一百倍。3.2 modprobe 说找不到模块而 insmod 能加载这个现象说明模块文件本身没问题问题在索引。modprobe的工作流程是查modules.dep找到模块及其依赖 → 按依赖顺序加载。modules.dep由depmod命令生成。所以正确姿势是sudo cp demo_chr.ko /lib/modules/$(uname -r)/extra/ sudo depmod -a sudo modprobe demo_chrdepmod -a会扫描整个/lib/modules/$(uname -r)/目录重建索引。不开-a的话它只处理命令行指定的模块。还有个更隐蔽的情况modprobe找的是/lib/modules/$(uname -r)/下的模块如果你的内核版本带后缀但/lib/modules/下目录名不一致比如目录是5.10.0而uname -r是5.10.0-test那modprobe连目录都找不对。这种情况要么建软链接要么老老实实按uname -r的输出去命名。3.3 设备号拿到了/dev 下却没有节点cat /proc/devices能看到你的驱动名和主设备号说明register_chrdev_region或alloc_chrdev_region成功了但/dev/demo_chr不存在说明节点没创建出来。节点创建有两条路。一条是我在骨架里用的device_create它会通过 sysfs 的 uevent 通知用户态的udev或者嵌入式里常见的mdev去/dev下建节点。这条路依赖用户态守护进程活着。如果板子上跑的是精简到极致、没有 udev 也没有 mdev 的根文件系统那device_create只是生成了/sys/class/demo_chr/demo_chr/这个目录/dev下还是空的。另一条路是手工mknod# 从 /proc/devices 拿到主设备号比如 240 sudo mknod /dev/demo_chr c 240 0 sudo chmod 666 /dev/demo_chr这个差别在嵌入式调试初期特别容易撞上。我第一次在一块精简根文件系统上跑驱动dmesg显示加载成功/proc/devices也在就是cat /dev/demo_chr报No such file or directory查了半小时才反应过来是没有 udev。3.4 rmmod 报 Device or resource busy卸载失败通常有三个来源按排查成本从低到高排第一有进程还开着设备节点。lsof /dev/demo_chr或者fuser /dev/demo_chr能查出来。这个最常见尤其你在另一个终端cat着设备没退出。第二模块引用计数不为零。lsmod输出的第三列就是引用计数。如果别的模块依赖你得先卸依赖方。modprobe -r会自动处理依赖顺序rmmod不会这是两者最实质的差别之一。第三代码里自己把自己锁死了。比如open里加了引用计数但release忘了减或者file_operations里的.owner没设成THIS_MODULE导致计数管理失效。这种情况lsmod显示计数不为 0 但没有任何进程占用只能靠代码审查。3.5 Unknown symbol 的连锁反应insmod报Unknown symbol xxx (err -2)意思是模块引用了内核没导出的符号。就两种可能符号名拼错了少个下划线、大小写不符或者这个符号确实没被导出。第二种情况只能换 API 或者用一个已导出的封装层绕过去。这里有个经验内核 API 的导出状态会随版本变化同一个函数在 5.4 里导出、在 6.1 里可能就被收回或者改名了。跨版本移植驱动时Unknown symbol往往不是第一个暴露的问题但一定是暴露得最直白的问题。还有一种特殊情况是内核配置裁剪导致的。某些函数在头文件里恒有声明但实现被#ifdef CONFIG_XXX包着配置没开就变成空实现或者根本不存在。这种编译阶段可能过加载阶段炸掉排查时得回头翻make menuconfig。4. 内建路线把同一份驱动编进内核镜像4.1 驱动代码在内核树里的落位规则内建驱动的第一条规矩代码必须放进内核源码树。和模块那种“放哪儿都行-C指过去就行”完全不同。放的位置有约定俗成的规则遵循它能让构建系统少出问题驱动类型推荐目录字符设备drivers/char/杂项设备drivers/misc/平台设备SoC 外设drivers/总线名/如drivers/iio/网络设备drivers/net/输入设备drivers/input/存储控制器drivers/ata/、drivers/mmc/自定义/产品专用drivers/misc/下开子目录自己建新目录也完全可以但必须在父目录的Makefile和Kconfig里登记否则构建系统根本不知道它存在。我的建议是即使驱动最终要内建开发期也先在独立目录里做成模块跑通确认逻辑没问题后再搬进内核树。搬进去的时候只改 Makefile 和 Kconfig代码基本不动。4.2 Kconfig 条目怎么写才不出岔子Kconfig 是配置系统的定义语言它决定了make menuconfig里能不能看到你的选项、能不能选、选了之后对应什么配置宏。一个最小可用的 Kconfig 条目config DEMO_CHR tristate Demo character device depends on OF default n help A minimal character device driver used for demonstration. It registers one char device and provides a read interface. To compile this driver as a module, choose M here. If unsure, say N.逐项解释都是踩过坑的点config DEMO_CHR定义了配置宏名最终在.config里就是CONFIG_DEMO_CHRy或m。这个名字和代码里、Makefile 里必须完全一致大小写敏感。tristate和bool的区别是整件事的关键tristate三选一Y 内建 / M 模块 / N 不编译bool只有两选一Y / N。如果你写bool那这个驱动就永远只能是内建.config里不可能是m。反过来如果你希望同一份 Kconfig 支持两种产出形态就必须用tristate。depends on OF是个例子用来表达依赖。如果你的驱动用了设备树匹配of_match_table那依赖OF是合理的如果还依赖某个子系统比如 GPIO就再加depends on GPIOLIB。依赖不满足时menuconfig里这个选项会直接不显示——这解释了一个经典困惑“我明明在 Kconfig 里写了选项menuconfig 里怎么就找不到”答案就是依赖没满足选项被隐藏了。default n表示默认不选。产品化时有些驱动会设成default y这样make defconfig之后自动进来不用每次手动勾。但要注意default y的驱动如果依赖不满足会出问题得配合depends on一起用。help段落不是可有可无的装饰。它的格式要求是缩进一个 tab 再加两空格缩进错了menuconfig里会显示错乱甚至报语法错误。我第一次写 Kconfig 就是在help里用了空格缩进make menuconfig直接抛Kconfig:xxx: syntax error。4.3 两层 Makefile 的语法与变量传递Kconfig 管“选不选”Makefile 管“怎么编”。内建驱动的 Makefile 和模块的完全不是一回事目录级的Makefile放在驱动所在目录obj-$(CONFIG_DEMO_CHR) demo_chr.o就这一行。解释一下展开逻辑如果.config里是CONFIG_DEMO_CHRy这行变成obj-y demo_chr.odemo_chr.o被链接进vmlinux如果是m变成obj-m demo_chr.o编译成独立的.ko如果是# CONFIG_DEMO_CHR is not set变成obj- demo_chr.o啥也不做。同一行 Makefile三种产出。这就是为什么说内建和模块在构建系统层面是同源的。再说一个多文件驱动的写法obj-$(CONFIG_DEMO_CHR) demo_chr.o demo_chr-objs : demo_main.o demo_ops.o注意demo_chr-objs用的是连字符不是下划线。这是个硬性规定写成下划线构建系统不认链接时会报一堆undefined reference。这个坑我印象很深因为报错信息完全不提命名规范你不会往那想。父目录的Makefile比如drivers/char/Makefile需要加一行把子目录挂上去obj-$(CONFIG_DEMO_CHR) demo/同时父目录的Kconfig里要source drivers/char/demo/Kconfig否则子目录里的 Kconfig 不会被读进来。三处登记缺一不可子目录 Kconfig 有内容、父 Kconfigsource了它、父 Makefileobj-$(CONFIG_XXX) 子目录/。少任何一处表现都是“配置项不出现”或“配置选了但没被编”。4.4 从 menuconfig 到烧录验证的完整链路配置好之后完整流程是这样# 1. 进入配置界面 make ARCHarm64 menuconfig # 在 Device Drivers - Character devices 下找到 # * Demo character device # 选 * 表示内建M 表示模块 表示不编 # 2. 确认配置写入 grep CONFIG_DEMO_CHR .config # 期望输出CONFIG_DEMO_CHRy # 3. 编译内核 make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc) # 4. 编译设备树如果驱动依赖 DT 匹配 make ARCHarm64 dtbs # 5. 安装模块如果还有 m 的部分 make ARCHarm64 modules_install INSTALL_MOD_PATH/path/to/rootfs # 6. 烧录 zImage / Image dtb 到板子第 2 步的grep是个必要的确认动作。menuconfig里显示*但.config里没写进去的情况是存在的通常是 Kconfig 依赖冲突导致选项被静默重置。我习惯每次改完配置都grep一遍关键项比烧完发现驱动没起来再回头找要省事得多。验证驱动确实编进去了有两个办法。一是看编译产物# 找驱动里的函数符号 nm vmlinux | grep demo_init # 或者 aarch64-linux-gnu-nm vmlinux | grep demo # 看 initcall 段里有没有注册 readelf -S vmlinux | grep .initcall二是启动后在板子上确认# 内建驱动不出现在 lsmod 里 lsmod | grep demo_chr # 预期为空 # 但 /proc/devices 里应该有 cat /proc/devices | grep demo_chr # /sys/bus/platform/drivers/ 下也应该有如果是平台驱动 ls /sys/bus/platform/drivers/ | grep demolsmod里查不到内建驱动这一点是新手最容易怀疑“我是不是没编进去”的地方。lsmod读的是/proc/modules只有可加载模块才在里面。内建驱动的存在感只能从/proc/devices、/sys、/proc/kallsyms这些地方确认。5. 内建之后行为变了初始化顺序、参数与调试5.1 module_init 编进内核后变成了什么这是从模块切内建后第一个会让人困惑的地方。module_init(xxx)这个宏在两种编译形态下展开成完全不同的东西/* include/linux/module.h 里的大致逻辑 */ #ifdef MODULE #define module_init(initfn) \ static inline int __init initfn(void); \ int init_module(void) __attribute__((alias(#initfn))); #else #define module_init(initfn) \ static int __init __initcall_##initfn(void) { return initfn(); } #endif编成模块时module_init给函数起了个别名init_moduleinsmod时由init_module系统调用触发。编进内核时它变成一个__initcall函数放进.initcall6.init段由内核启动阶段的do_initcalls()按段顺序统一调用。这个段号 6 对应的是device_initcall级别。完整的初始化级别从低到高是级别宏典型用途0early_initcall极早期几乎不用1core_initcall核心子系统2postcore_initcall总线、设备模型3arch_initcall架构相关4subsys_initcall子系统如 IOMMU、DMA5fs_initcall文件系统6device_initcallmodule_init默认落在这里7late_initcall最后兜底级别决定的是“什么时候执行”但同一级别内部的执行顺序由链接顺序决定而链接顺序取决于 Makefile 里obj-y的排列。这意味着内建驱动的初始化顺序不是你能通过代码直接控制的只能通过调 Makefile 顺序或者换 initcall 级别来间接影响。我遇到过一个问题驱动 A 内建后在init里访问一个由驱动 B 创建的全局结构B 也内建。单独测的时候没问题合进去启动就 oops。原因就是链接后 A 排在了 B 前面A 执行时 B 还没初始化。改成subsys_initcall把 B 提前或者让 A 改成在probe里延迟获取都能解决。模块形态下不存在这个烦恼因为insmod顺序完全由你手动控制想先加哪个先加哪个。5.2 模块参数在内建形态下还能用吗module_param系列宏定义的参数在内建形态下依然有效但传入方式变了。模块形态下参数通过insmod/modprobe传入sudo insmod demo_chr.ko debug_level2 buf_size4096 # 或者写进 /etc/modprobe.d/demo.conf: options demo_chr debug_level2内建形态下参数写在内核启动命令行里格式是模块名.参数名值# 在 bootargs 或 grub 的 kernel 行里追加 demo_chr.debug_level2 demo_chr.buf_size4096这个前缀不能省。写成debug_level2内核不认识当成未知启动参数忽略了你的驱动拿到的还是默认值。而demo_chr.这个前缀来自驱动里.name的定义不是源文件名也不是设备节点名。另外内建形态下用户在启动后没法再改这些参数因为module_param的 0644 权限只在模块形态下有意义——内建的话代码在__init段执行完就被回收了/sys/module/demo_chr/parameters/目录可能还在但写入不会生效到已经初始化的驱动上。所以我的做法是开发期用模块参数做快速调参定版时把关键参数写死成编译期宏或者从设备树读。设备树这条路线其实更干净of_property_read_u32(np, demo,buf-size, val)这种写法参数跟着板级配置走一板一值不用改代码也不用改启动参数。5.3 内建驱动的调试手段少在哪儿模块形态下最舒服的一点是可以反复rmmod insmod。内建形态下唯一的调试手段是重启。这个差距直接改变了调试策略。内建驱动的调试我主要靠这几样printk打点。这是最原始的也是最快的。但内建驱动的一大问题是启动过程日志太多你的输出会被淹没。解决办法有两条一是用pr_info加上明显的驱动名前缀比如demo_chr: ...然后dmesg | grep demo_chr二是把CONFIG_CONSOLE_LOGLEVEL_DEFAULT调高或者开机时加loglevel8让所有级别的日志都打到串口。dynamic_debug。这是内核提供的一套动态调试机制可以在运行时打开某个文件、某一行、某个函数的pr_debug输出不用重新编译# 打开 demo_chr.c 里所有 pr_debug echo file demo_chr.c p /sys/kernel/debug/dynamic_debug/control # 只打开某个函数 echo func demo_probe p /sys/kernel/debug/dynamic_debug/control # 查看当前所有被打开的点 grep p /sys/kernel/debug/dynamic_debug/control前提是驱动里用的是pr_debug/dev_dbg而不是pr_info而且内核开了CONFIG_DYNAMIC_DEBUG。这套机制在内建驱动的开发里价值很高因为编译一次内核太贵了能在运行时控制输出密度等于省了几轮重启。/proc/kallsyms查符号。内建驱动的所有非 static 函数都会出现在这里grep demo /proc/kallsyms这个能快速确认驱动到底有没有被链接进去比看dmesg更早一步。ftrace。想知道某个函数的调用频次、调用栈、耗时function_graph追踪器比printk有力得多代价是学习曲线陡。我一般只在性能问题卡住时才上这套。6. 一份代码两种产出工程组织上的实际做法6.1 目录结构与条件编译的组织真实项目里一份驱动既要能在开发期当模块跑也要能在定版时编进内核。我的做法是把代码放在一个独立目录里同时提供两套构建入口demo_chr/ ├── demo_chr.c # 驱动主体模块和内建共用 ├── demo_chr.h ├── Makefile # 外部模块编译用obj-m ├── Kconfig # 内建路线的配置定义 └── Makefile.kbuild # 内建路线用obj-$(CONFIG_...)开发期直接在demo_chr/目录下make走Makefile产出.ko秒级迭代。定版时把整个目录拷进drivers/char/demo/把Makefile.kbuild改名为Makefile在父目录登记 Kconfig 和 Makefile走obj-$(CONFIG_DEMO_CHR) demo_chr.o这条线。代码本身几乎不用改但有三处需要留意module_init/module_exit保留。编成内建时它们自动展开成__initcall不需要手动改。module_exit在内建形态下会变成__exitcall实际上会被丢弃__exit段在编进vmlinux时被回收所以内建驱动不会有卸载流程这是正常的。MODULE_*宏保留。MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION在内建时也会生成信息只是不出现在modinfo里而是编进.modinfo段/sys/module/name/下能查到。不影响功能。条件编译慎用。有些人喜欢写#ifdef MODULE ... #else ... #endif来区分两条路径这个宏在编模块时确实由构建系统定义。但滥用会让代码变得难以维护我一般只在确实需要区别对待的地方用比如模块形态下允许参数可写、内建形态下改从设备树读这种真正有差异的逻辑。6.2 我在实际项目里踩出来的几条经验第一条开发期永远先用模块别图省事直接内建。驱动开发里 90% 的时间在调试模块的迭代速度是内建的几十倍。我见过有人一开始就把驱动塞进内核树改一行printk编十分钟烧五分钟一天下来改不了几处效率极低。第二条Kconfig 的tristate和 Makefile 的obj-$(CONFIG_XXX)必须配对。只要这两处有一个写成bool或者写成obj-y就永远失去m的能力。这个错误不会报错只会让你某天想把驱动改成模块时突然发现改不动。第三条内建驱动不要依赖“初始化顺序刚好是我要的”。任何跨驱动的依赖都应该通过probe的延迟机制、或-EPROBE_DEFER返回值来处理而不是赌链接顺序。链接顺序会因为你加了一行 Makefile、换了个编译器版本就变。第四条一定要在板子上确认内建驱动真的在跑别只看编译通过。nm vmlinux | grep 你的函数名能确认链接进去了cat /proc/devices能确认注册了dmesg | grep 驱动名能确认init执行了。三步都过才算真的起来了。我吃过一次亏配置项选了*编译也过了但那次烧的是旧的zImage白折腾一小时。第五条给内建驱动留一个运行时可查的状态出口。比如在/sys/class/demo_chr/下加几个attribute文件暴露内部计数器、缓冲区状态、错误计数。内建驱动不能重新加载这些出口就是你在现场唯一能看到内部状态的手段。调试模块时觉得没必要定版出问题时会庆幸当初写了。第六条模块形态下的depmod -a要养成肌肉记忆。每次往/lib/modules/里放了新模块第一件事就是跑depmod -a然后再modprobe。跳过这一步省下的三秒钟换来的是十分钟的“为什么找不到模块”。最后分享一个我自己用的小技巧在demo_init的第一行和最后一行各打一条带时间戳的日志中间的关键节点也打。模块形态下这两条日志看不出什么但切到内建形态后启动日志里这两条的时间差能直接告诉你驱动的初始化耗时以及它在整个启动流程里的相对位置。定位“某个设备初始化太晚导致依赖它的驱动起不来”这类问题时这个信息比翻 initcall 段顺序快得多。
RELATED

相关推荐

Docker部署YOLOv8推理环境与性能测试实践

Docker部署YOLOv8推理环境与性能测试实践

1. 先把结论说在前面:为什么要用 Docker 跑 YOLOv8YOLOv8 测试系列的第四篇,聊的是部署形态。前三篇基本都在折腾模型本身——权重下载、推理效果、导出格式。但只要你想把一个检测模型真正跑起来、跑得久、跑给别人看,环境这件事就会冒出来变…

📅 2026/10/26 3:19:13
宝塔Linux下SVN部署与自动更新实战避坑

宝塔Linux下SVN部署与自动更新实战避坑

SVN 这套东西,说起来有点“老派”,但在外包交付、传统项目源码管理、文档版本归档这些场景里,它依然是绕不开的刚需。尤其是手里只有一台装着宝塔 Linux 面板的机器,既想跑网站又想顺手放个版本库,很多人第一反应是上 …

📅 2026/10/26 3:19:13
应用商店合规下架潮:开发者应对策略与趋势分析

应用商店合规下架潮:开发者应对策略与趋势分析

1. 应用商店生态变局观察最近业内同行们都在讨论一个现象:国内主流应用商店出现了一波明显的应用下架潮。作为从业十余年的移动应用开发者,我深切感受到这次调整带来的震动。不同于以往零星的下架事件,这次波及范围广、持续时间长&#xff0c…

📅 2026/10/26 3:14:13
MORE NEWS

更多资讯

📰

PCB设计规范深度解析:从网络表到布局布线的工程机制与电磁学原理

简介:这份《华为PCB设计规范》面向PCB设计工程师、电子工程师及项目经理,提供一套可参照的企业级设计准则,帮助解决布局布线、评审流程与工艺要求缺乏统一标准的问题。资源为单个PDF文件,压缩包约248KB,内容按章节组织…

📰

数码摄影中深度信息丢失的原因与还原技术

1. 相机丢失深度信息现象解析第一次用单反拍完照片导入电脑时,发现原本在取景器里层次分明的场景变得扁平化,那种立体感完全消失了——这就是典型的深度信息丢失。这种现象在摄影圈被称为"平面化诅咒",几乎所有从光学取景器转向数码…

📰

Java工程师AI应用开发指南:LangChain4j从入门到生产实践

1. 从Java工程师到AI应用开发:为什么LangChain4j值得你花时间如果你是一名写了几年Java的开发者,最近大概率会有一种隐隐的焦虑:身边做Python的同事开始聊RAG、聊Agent、聊大模型应用开发,而自己还在写CRUD、调MyBatis的XML、处理…

📰

操作系统内存管理第十七章答案精讲:地址转换、多级页表与TLB计算题解析

1. 这份“第十七章答案”到底在解决什么问题操作系统导论这类课程,到了第十七章前后,基本都绕不开内存管理这条主线——分页、分段、地址转换、页表结构、TLB、交换空间,这些概念单拎出来都懂,凑到一起做题就开始犯迷糊。我见过太…

📰

工业机器人标定技术:从基础原理到前沿应用

1. 机器人标定技术概述十年前我第一次接触工业机器人标定时,车间老师傅用游标卡尺测量TCP(工具中心点)位置的场景至今难忘。如今激光跟踪仪和视觉标定系统已成为产线标配,这种技术迭代背后是制造业对精度需求的指数级增长。机器人…

📰

手机GUI Agent双通道感知:无障碍树与投屏像素融合方案

1. 为什么手机 GUI Agent 需要双通道感知做过手机端自动化的人都有一个共识:单靠一种感知方式,Agent 迟早会在某个场景里翻车。我最早做 GUI Agent 的时候,只用了无障碍树(AccessibilityService)来获取界面元素&#x…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬