ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

04-Linux 内核模块

04-Linux 内核模块 4.1 内核模块简介内核模块.ko是可以运行时动态加载/卸载的内核代码。它的价值减小内核体积不常用的驱动编译成模块按需加载。加快开发迭代改驱动不用重编内核、不用重启机器。便于发行版适配同一内核支持多种硬件。便于实验本手册几乎所有实验都用模块完成。模块不是什么不是独立程序没有main()没有独立地址空间运行在内核态崩溃即内核崩溃。不是沙箱模块拥有内核的全部权限。不是想调什么就能调什么只能用已导出的符号EXPORT_SYMBOL。模块与内核的边界4.2 内核模块程序结构最小模块hello_module.c#include linux/module.h #include linux/init.h #include linux/kernel.h static int __init hello_init(void) { pr_info(hello_module: loaded, jiffies%lu\n, jiffies); return 0; } static void __exit hello_exit(void) { pr_info(hello_module: unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Kevin); MODULE_DESCRIPTION(Minimal kernel module for Linux 7.2.5 study); MODULE_VERSION(1.0);结构要点元素作用#include linux/module.h必需提供module_init/module_exit/MODULE_*__init/__exit段属性init 代码在初始化后释放exit 代码在编入内核时不保留module_init(fn)注册加载函数返回int0 表示成功module_exit(fn)注册卸载函数返回voidMODULE_LICENSE必须否则内核标记为专有模块taint并影响部分符号使用MODULE_AUTHOR/DESCRIPTION/VERSIONmodinfo可见便于维护7.2.5 的补充include/linux/cleanup.h提供了__free()、guard()、scoped_guard()等自动清理机制写模块时可用于简化错误路径但不要在模块的 init/exit 函数里依赖复杂的清理链保持申请-释放成对可读更安全。4.3 模块加载函数static int __init my_init(void) { int ret; ret register_chrdev_region(my_dev, 1, mymod); if (ret) return ret; /* ... */ return 0; /* 0 成功负 errno 失败模块不会加载 */ } module_init(my_init);规则返回0 成功返回负 errno失败失败时内核会调用你的清理逻辑吗不会——你必须自己回滚已经申请的资源。__init函数在模块中不会被释放模块是动态加载的但加上__init无害且能节省 built-in 情况下的内存。加载顺序insmod会先做 ELF 校验、符号解析、重定位、签名检查再调用 init。若 init 返回-ENODEVinsmod会打印insmod: ERROR: could not insert module ...: No such device。多设备/自动加载的场景现代驱动通常不写加载函数里扫描硬件而是注册 driver由设备模型在设备出现时回调probe()module_platform_driver(my_platform_driver); /* 展开为 module_init/module_exit */ module_i2c_driver(my_i2c_driver); module_spi_driver(my_spi_driver); module_pci_driver(my_pci_driver);4.4 模块卸载函数static void __exit my_exit(void) { unregister_chrdev_region(my_dev, 1); } module_exit(my_exit);规则卸载前必须确保没有任何人在使用你的模块内核通过模块引用计数refcount保证。若rmmod时计数不为 0会报Module mymod is in use。卸载函数不能部分失败返回值是void必须完成全部清理。不要在卸载函数里做可能永久阻塞的操作del_timer_sync()、flush_workqueue()、cancel_work_sync()是常用但必须小心死锁不要在持有对方所需锁时调用。卸载后内核会释放模块内存任何仍然指向模块代码的指针都是悬空的——这正是引用计数存在的意义。4.5 模块参数static int count 1; module_param(count, int, 0444); MODULE_PARM_DESC(count, Number of devices to create (default 1)); static char *name globalmem; module_param(name, charp, 0644); MODULE_PARM_DESC(name, Device name); static int sizes[4]; static int nsizes; module_param_array(sizes, int, nsizes, 0444); MODULE_PARM_DESC(sizes, Per-device sizes in bytes); static bool debug; module_param(debug, bool, 0644); MODULE_PARM_DESC(debug, Enable verbose logging);权限位第三参数值含义0不在 sysfs 中暴露只能用insmod mymod.ko count4或 modprobe 配置0444只读属性0644读写属性必须自己保证并发安全S_IRUGO/S_IWUSR旧式宏仍可用但新代码推荐八进制字面量用户空间访问# 加载时传参 insmod ./mymod.ko count4 debug1 # 查看参数 ls /sys/module/mymod/parameters/ cat /sys/module/mymod/parameters/count # 运行时修改需要 0644 权限 echo 8 /sys/module/mymod/parameters/count modinfo ./mymod.ko # 看 parm/parmtype 行7.2.5 注意事项可写参数必须在你的代码里用锁保护内核不会替你同步。如果参数在模块运行中途被改变必须保证语义合法例如不能把 buffer 大小改小到已有数据之外。复杂配置请用sysfs 属性 / debugfs / configfs / netlink不要滥用模块参数。4.6 导出符号模块 A 想调用模块 B 的函数B 必须导出符号/* 在 B 模块中 */ static int internal_helper(void) { ... } /* 私有 */ int my_exported_func(int x) /* 导出给任何模块 */ { return internal_helper() x; } EXPORT_SYMBOL(my_exported_func); void my_gpl_func(void) { ... } EXPORT_SYMBOL_GPL(my_gpl_func); /* 只给 GPL 兼容模块 */7.2.5 的符号命名空间原书没有的知识点大型子系统必备/* 定义方 */ EXPORT_SYMBOL_NS(my_func, MY_SUBSYSTEM); /* 使用方必须显式声明导入哪个命名空间 */ MODULE_IMPORT_NS(MY_SUBSYSTEM);命名空间的目的是让子系统内部接口半私有化避免被无关模块滥用。使用方忘记MODULE_IMPORT_NS()会出现类似错误ERROR: modpost: module mymod uses symbol my_func from namespace MY_SUBSYSTEM, but does not import it.符号解析与modpostERROR: modpost: foo [./bar.ko] undefined!常见原因foo没有EXPORT_SYMBOL。foo所在的模块没有先加载且不是 built-in。模块与内核版本/配置不匹配vermagic不同。检查已导出符号grep -w foo /proc/kallsyms # guest 中 nm vmlinux | grep T foo # 宿主中 modprobe --dump-modversions foo.ko # 查看模块依赖的符号版本4.7 模块声明与描述MODULE_LICENSE(GPL); /* 必须GPL / GPL v2 / Dual BSD/GPL / Proprietary */ MODULE_AUTHOR(Kevin kevinexample.com); MODULE_DESCRIPTION(Global memory character driver); MODULE_VERSION(1.2.3); MODULE_ALIAS(platform:globalmem); /* 别名配合 udev/modprobe 自动加载 */ MODULE_SOFTDEP(pre: crc32c); /* 软依赖加载前先尝试加载 crc32c */ MODULE_FIRMWARE(mydrv/fw.bin); /* 声明需要的固件便于打包 */ MODULE_DEVICE_TABLE(of, my_of_match); /* 生成 modalias实现设备热插拔自动加载 */ MODULE_DEVICE_TABLE(i2c, my_i2c_id); MODULE_DEVICE_TABLE(platform, my_platform_id);MODULE_DEVICE_TABLE是自动加载的关键它把设备匹配表写进模块的__mod_bus_device_table段depmod处理后写入/lib/modules/$(uname -r)/modules.alias。当内核发现新设备并发出 uevent 时udev 根据 modalias 自动modprobe对应模块。# 观察 modalias 与自动加载 cat /sys/bus/i2c/devices/0-0050/modalias grep i2c /lib/modules/$(uname -r)/modules.alias | head查看模块信息modinfo ./globalmem.ko lsmod cat /proc/modules ls /sys/module/globalmem/ ls /sys/module/globalmem/sections/ # 各段地址需要权限 cat /sys/module/globalmem/refcnt4.8 模块的使用计数内核维护每个模块的引用计数防止正在被使用的代码被卸载。/* 典型场景文件操作期间防止模块被卸载 */ static int my_open(struct inode *inode, struct file *filp) { if (!try_module_get(THIS_MODULE)) return -ENODEV; ... return 0; } static int my_release(struct inode *inode, struct file *filp) { ... module_put(THIS_MODULE); return 0; }7.2.5 的现实情况大多数情况下你不需要手工写try_module_get()设备驱动通过struct device_driver.owner THIS_MODULE注册后设备打开/绑定期间内核自动持有引用。字符设备通过cdev_addfops-owner THIS_MODULEVFS 会在打开时自动try_module_get()。只有自己创建的 kthread、自建 workqueue、定时器回调、以及暴露给其他模块的入口才需要手工管理。观察引用计数cat /sys/module/globalmem/refcnt rmmod globalmem # 若 refcnt ! 0会失败 # 正确的卸载流程先关闭所有使用者再 rmmod4.9 模块的编译4.9.1 树内模块built-in 或模块把驱动放进drivers/下按第 3 章的 Kconfig/Kbuild 规则添加然后make -C $KSRC O$KBUILD modules # 只编译模块 make -C $KSRC O$KBUILD modules_install # 安装到 /lib/modules/...4.9.2 外部模块本手册推荐方式目录结构/home/works/study/modules/04-hello/ ├── Makefile └── hello_module.cMakefileobj-m : hello_module.o # 多文件模块 # obj-m : mymod.o # mymod-y : main.o fileops.o irq.o # 只在本 Makefile 被 Kbuild 直接调用时才展开 KSRC ? /home/works/study/linux-7.2.5 KBUILD ? /home/works/study/build/linux-7.2.5-x86_64 all: $(MAKE) -C $(KSRC) O$(KBUILD) M$(PWD) modules clean: $(MAKE) -C $(KSRC) O$(KBUILD) M$(PWD) clean编译cd /home/works/study/modules/04-hello make ls -l hello_module.ko modinfo hello_module.ko7.2.5 的新写法-f代替-CLinux 6.13 起见Documentation/kbuild/modules.rst。它不会切换工作目录产物直接落在当前目录# 旧写法仍然可用 make -C $KSRC O$KBUILD M$PWD modules # 新写法6.13 make -f $KBUILD/Makefile M$PWD modules把模块产物放到单独目录MOmake -C $KSRC O$KBUILD M$PWD MO$PWD/build modules跨模块依赖KBUILD_EXTRA_SYMBOLS模块 A 依赖模块 B 的导出符号而 B 也在源码树外时make -C $KSRC O$KBUILD M$PWD \ KBUILD_EXTRA_SYMBOLS/path/to/B/Module.symvers modules模块签名发行版内核常见CONFIG_MODULE_SIG# 生成密钥内核树内 scripts/sign-file sha256 private_key.pem public_key.pem mymod.ko # 若内核启用了强制签名CONFIG_MODULE_SIG_FORCE未签名模块无法加载 # insmod: ERROR: could not insert module mymod.ko: Key was rejected by service模块版本CONFIG_MODVERSIONS7.2.5 中如果启用CONFIG_GENDWARFKSYMS配合CONFIG_DEBUG_INFO符号版本改用DWARF 信息计算而不是旧的genksyms源码解析。这对 Rust 模块是必需的详见Documentation/kbuild/gendwarfksyms.rst。对驱动开发者的影响是模块与内核必须同源同配置编译否则报disagrees about version of symbol xxx。想绕过版本检查只能靠force加载危险不要在生产上做。4.9.3 在 QEMU 中加载模块三种方式按推荐顺序把模块放进 initramfs最稳定make -C $KSRC O$KBUILD INSTALL_MOD_PATH$ROOTFS modules_install # 重新打包 initramfs 后启动 QEMU9p 共享目录改代码不用重打包# guest 中 mount -t 9p -o transvirtio,version9p2000.L hostshare /mnt/host insmod /mnt/host/modules/04-hello/hello_module.kovirtiofs / 网络传输进阶。4.10 使用模块绕开GPL这是原书中争议最大的一节必须把技术事实和法律边界讲清楚。技术事实EXPORT_SYMBOL_GPL()导出的符号只允许声明了 GPL 兼容许可证的模块使用。模块的许可证由MODULE_LICENSE()决定。写MODULE_LICENSE(Proprietary)或完全不写MODULE_LICENSE()内核会把该模块标记为tainted污染cat /proc/sys/kernel/tainted # 非 0 表示内核已被污染 dmesg | grep -i taint被污染的内核社区在收到 bug 报告时会要求你先复现于未污染内核。为什么不能绕开从 4.0 到 7.2.5EXPORT_SYMBOL_GPL的范围持续扩大很多关键接口如kallsyms_lookup_name、部分tracepoint相关接口、很多核心子系统的内部 API都是 GPL-only 或干脆不再导出。历史上有人用kallsyms_lookup_name()或直接读取/proc/kallsyms来抓符号地址从而调用未导出函数。内核已采取措施kallsyms_lookup_name()在 5.7 之后不再导出非特权读取/proc/kallsyms只得到 0 地址。更本质的是这不是技术问题而是许可证问题。Linux 采用 GPLv2内核社区以及多数法务认为与内核链接的模块属于衍生作品必须 GPL 兼容。绕过的做法会让你的产品处于法律风险中并且一旦涉及EXPORT_SYMBOL_GPL的符号也违反内核的明确技术约定。正确做法用GPL 兼容许可证发布驱动MODULE_LICENSE(GPL)。如果确实需要闭源走厂商认可的边界把专有逻辑放到用户空间或通过明确的、稳定的 ABIsysfs/netlink/字符设备 ioctl/virtio 等通信。需要内核内部功能但不想直接链接时考虑BPFBPF_PROG_TYPE_*、kprobe/tracepoint 程序这类官方支持的可编程接口。符号命名空间与 GPL 的关系7.2.5EXPORT_SYMBOL_NS_GPL(subsys_helper, MY_NS); /* 命名空间 GPL-only */ MODULE_IMPORT_NS(MY_NS); /* 使用方声明 */命名空间是技术隔离不改变许可证规则。4.11 总结模块 运行时可加载的内核代码有 init/exit 成对、有引用计数、有签名与版本校验。模块参数、导出符号、模块声明是模块的对外接口MODULE_DEVICE_TABLE决定能否自动加载。外部模块编译在 7.2.5 中既可用传统-C也可用 6.13 引入的-fM/MO/KBUILD_EXTRA_SYMBOLS是常用变量。模块版本检查在 7.2.5 中可能由CONFIG_GENDWARFKSYMS通过 DWARF 计算模块必须与内核同源同配置编译。GPL 边界必须遵守EXPORT_SYMBOL_GPL不可绕过污染内核的技术手段也已被大幅封堵。
返回列表