
内核模块基础一、Hello World最小内核模块1.1 完整代码1.2 每个部件的作用1.3 关于 taints kernel二、驱动传参2.1 六种常用写法对照2.2 module_param_array2.3 module_param_string2.4 charp vs string内存模型完全不同三、符号导出3.1 为什么需要 导出3.2 Module.symvers符号的 通讯录3.3 使用方怎么写四、内核启动过程中的 initcall 优先级五、kmod模块管理工具集与自动加载一、Hello World最小内核模块1.1 完整代码#includelinux/module.h/* module_init/exit、MODULE_LICENSE */#includelinux/kernel.h/* pr_info 等打印 */MODULE_LICENSE(GPL);/* ← 必须有 */staticint__inithello_init(void)/* 加载时调用一次 */{pr_info(Hello!\n);return0;/* 返回 0 成功负数 加载失败 */}staticvoid__exithello_exit(void)/* 卸载时调用一次 */{pr_info(Bye!\n);}module_init(hello_init);module_exit(hello_exit);1.2 每个部件的作用部件作用不写会怎样module_init(fn)指定insmod时调用的函数模块加载后什么也不做module_exit(fn)指定rmmod时调用的函数模块无法卸载MODULE_LICENSE(GPL)声明许可证编译警告且用不了EXPORT_SYMBOL_GPL的符号__init把代码放进.init.text段初始化后可释放只是少一点内存优化__exit卸载用代码段模块若编译进内核这段代码会被丢弃pr_info()打印printk(KERN_INFO ...)无输出调试全靠猜关于MODULE_LICENSE内核里大量符号是用EXPORT_SYMBOL_GPL导出的只有声明了 GPL 兼容许可证的模块才能用。关于init返回值返回 0 表示成功返回负数如-ENODEV内核会拒绝加载并自动调用清理。这是驱动里 “硬件不存在就放弃加载” 的标准做法。1.3 关于 taints kernelhello: loading out-of-tree module taints kernel. ← 外部模块提示无害 [hello]: Hello! [hello]: Bye!加载外部模块都会打印这一行意思是 “这个内核被第三方模块碰过了”。内核维护者收到 bug 报告时若发现内核被污染会要求先去掉第三方模块再复现。二、驱动传参staticinttimes1;/* ① 一个普通 C 变量 */module_param(times,int,0644);/* ② 让它在 sysfs 里可见 */MODULE_PARM_DESC(times,说明);/* ③ 一句文档 */它只做两件事定义变量 在/sys/module/模块名/parameters/参数名建一个文件。2.1 六种常用写法对照写法声明命令行用法整型static int n;module_param(n, int, 0644);n5布尔static bool b;module_param(b, bool, 0644);b1/Y/N长整型static long l;module_param(l, long, 0644);l123456数组static int a[8]; int a_n;module_param_array(a, int, a_n, 0444);a1,2,3字符串static char s[64];module_param_string(nm, s, sizeof(s), 0644);nmhi字符串(指针)static char *p;module_param(p, charp, 0644);phi权限位0644 拥有者可读写、其他只读。0444 谁都只能读。上面只是常用的 6 种组合。真实情况是 “宏 × 类型” 的乘积宏常用的就 3 个 ——module_param/module_param_array/module_param_string类型内核include/linux/moduleparam.h里实现了 15 种byte short ushort int uint long ulong ullong hexint charp bool bool_enable_only invbool bint string其中invbool是 “取值取反”写 1 得到 0bint是布尔版 int用得少。2.2 module_param_arraystaticintvalues[8];staticintvalues_n0;/* 内核回填实际传了几个 */module_param_array(values,int,values_n,0444);你不知道用户会传几个值所以内核把实际数量写回values_n。遍历时必须用它不能用sizeof(values)那会得到容量 8读出一堆没初始化的 0。2.3 module_param_stringstaticcharmsg[64]hello;module_param_string(message,msg,sizeof(msg),0644);/* ↑对外名字 ↑你的缓冲区 */第 1 个参数是 sysfs/命令行里用的名字第 2 个才是你的 C 变量。insmod lab_params.komessagehi driver# 用 message# [params] message (string) hi driver2.4 charp vs string内存模型完全不同module_param(p, charp, ...)module_param_string(nm, buf, len, ...)变量形态char *p指针char buf[]数组内存归属内核分配自己的缓冲区长度不限受sizeof(buf)限制超出截断能改内容吗❌ 不能只读✅ 能绝大多数情况用module_param_string有边界、安全、可改。三、符号导出3.1 为什么需要 “导出”内核是单一地址空间所有模块和内核本体共享同一份内存你的模块技术上能 “看到” 内核的任何函数。但内核要求必须显式导出才能用/* 在导出方mod_a */intlab_add(inta,intb){returnab;}intlab_secret(intx){returnx*42;}EXPORT_SYMBOL(lab_add);/* 谁都能用 */EXPORT_SYMBOL_GPL(lab_secret);/* 只有 GPL 模块能用 */没导出的函数其他模块编译时就会报错。这套机制是内核维持内部边界的手段。宏谁能用EXPORT_SYMBOL()任何模块EXPORT_SYMBOL_GPL()只有MODULE_LICENSE为 GPL 兼容的模块3.2 Module.symvers符号的 “通讯录”编译完mod_a后目录里会生成一个文件实测$ cat modules/06-symbols/Module.symvers 0x00000000 lab_add .../mod_a EXPORT_SYMBOL 0x00000000 lab_secret .../mod_a EXPORT_SYMBOL_GPL它记录了 “哪个模块导出了哪些符号”。同时mod_b.ko里也记下了自己的依赖$ modinfo mod_b.ko name: mod_b depends: mod_a ← 自动分析出来的 vermagic: 6.12.111 SMP mod_unload ARMv7 p2v8⚠️ 跨目录编译必须指定符号表如果两个模块在同一个 Makefile 里obj-m : mod_a.o mod_b.o一次make会自动处理它们之间的依赖。但如果分在不同目录编译mod_b时必须告诉它去哪找mod_a的符号表否则实测ERROR: modpost: lab_add [.../mod_b.ko] undefined! ERROR: modpost: lab_secret [.../mod_b.ko] undefined!解决办法 —— 在mod_b的 Makefile 里加一行KBUILD_EXTRA_SYMBOLS : /path/to/mod_a/Module.symvers3.3 使用方怎么写/* mod_b.c不需要 include 任何头文件extern 声明即可 */externintlab_add(inta,intb);externintlab_secret(intx);staticint__initmod_b_init(void){pr_info(lab_add(3,4) %d\n,lab_add(3,4));pr_info(lab_secret(2) %d\n,lab_secret(2));return0;}实测输出[mod_b] lab_add(3, 4) 7 [mod_b] lab_secret(2) 84 (GPL-only 符号)四、内核启动过程中的 initcall 优先级源码调用链init/main.cstart_kernel() └─ rest_init() :创建内核线程 └─ kernel_init() :内核线程入口 └─ kernel_init_freeable() ├─ do_pre_smp_initcalls() ← 只跑 early 级SMP 启动前 └─ do_basic_setup() ├─ driver_init() ← 设备模型bus / class / devtmpfs ├─ init_irq_proc() ├─ do_ctors() └─ do_initcalls() ← 8 个级别依次执行八个级别// init/main.cstaticconstchar*initcall_level_names[]__initdata{pure,core,postcore,arch,subsys,fs,device,late,};执行是两层循环外层按级别 0→7内层跑该级别的所有函数。编号级别宏典型用途—earlyearly_initcall()SMP 启动前跑由do_pre_smp_initcalls()单独执行0purepure_initcall()不依赖任何东西的纯初始化1corecore_initcall()核心子系统2postcorepostcore_initcall()依赖 core 的组件3archarch_initcall()架构相关初始化4subsyssubsys_initcall()各子系统如 I2C、SPI 总线核心5fsfs_initcall()文件系统、网络协议栈6devicedevice_initcall()module_init()设备驱动数量最多7latelate_initcall()收尾如释放 __init 内存前最后做的事带_sync后缀的宏如device_initcall_sync在同一级别内排在不带 sync 的之后用于同一级别内的二次排序。**module_init 到底是哪一级—— 第 6 级 device定义链include/linux/init.h#definemodule_init(x)__initcall(x);#define__initcall(fn)device_initcall(fn)#definedevice_initcall(fn)__define_initcall(fn,6)所以module_init就是device_initcall编号 6排在倒数第二。这也解释了 device 级的 initcall 数量远超其他级别 —— 绝大多数驱动都用它。五、kmod模块管理工具集与自动加载kmod 是 Linux 官方的内核模块管理工具集所有命令都是指向同一个可执行文件的符号链接靠 “被叫成什么名字” 决定行为lsmod/modprobe/insmod/rmmod/depmod/modinfo全部 →/usr/bin/kmod六个命令各做什么命令作用吃什么参数lsmod列出已加载的模块本质是格式化/proc/modules无insmod加载模块不管依赖.ko文件路径rmmod卸载模块被别人引用时会失败模块名modinfo查看.ko的元信息不用加载就能看.ko文件depmod扫描模块目录生成依赖表建索引不加载目录modprobe智能加载/卸载按名字或别名自动处理依赖模块名或别名lsmod—— 看现在装了什么lsmod# Module Size Used by# mod_b 12288 0# mod_a 12288 1 mod_b ← 被 mod_b 引用 1 次三列依次是模块名、占用内存、被引用次数 被谁引用。insmod—— 加载依赖得自己保证insmod /mnt/host/modules/01-hello/hello.ko参数是文件路径后面可以直接跟模块参数❌ 不解析依赖——依赖没加载就报Unknown symbolrmmod—— 卸载rmmod hello参数是模块名被别的模块引用时失败Resource temporarily unavailablemodinfo—— 不开机也能看模块信息modinfo hello.ko# filename: .../hello.ko# license: GPL# depends: ← 依赖哪些模块# vermagic: 6.12.111 SMP mod_unload ARMv7 p2v8 ← 版本指纹# parm: times: (int) 打印次数最常看两项depends依赖和vermagic和内核版本对不对得上。depmod—— 建立依赖表depmod-a# 扫描 /lib/modules/$(uname -r)/生成 modules.dep它本身不加载任何东西只是建索引。生成的modules.dep长这样extra/mod_b.ko: extra/mod_a.ko ← 冒号右边 它依赖谁 extra/mod_a.ko:modprobe就是靠这张表知道 “装 mod_b 之前得先装 mod_a”。modprobe—— 日常最常用modprobe mod_b# 只说要 mod_bmod_a 自动带上modprobe-rmod_b# 卸载并把不再被依赖的一起卸掉参数是模块名模块必须放在/lib/modules/$(uname -r)/下✅ 递归加载依赖、-r反向卸载全自动