ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux内核模块全攻略:从原理到实战的驱动加载指南

2026/9/26 17:07:50 拓冰建站 浏览量
Linux内核模块全攻略:从原理到实战的驱动加载指南 我最早被 Linux 内核模块折腾是因为一台老笔记本装完 Linux 系统之后无线网卡毫无反应。当时连lsmod、modprobe都不会用只知道网卡驱动是个.ko文件加载进去就能联网。后来做运维、写驱动相关工具的时间长了才真正明白内核模块这套机制远不止“驱动”这么简单——文件系统、网络过滤、安全审计、虚拟化、容器全都靠着它动态插拔。这篇内容就围绕 Linux 内核模块展开从基本概念、代码骨架、编译加载到卸载排查、常见坑位再到 Docker、虚拟机、Kali 这些高频场景里模块的实际应用。适合三类人看一类是刚接触内核、想动手写第一个模块的开发者一类是做运维、经常被modprobe和dmesg折磨的系统管理员还有一类是纯粹对 Linux 底层好奇想把“内核加载机制”这件事弄明白的爱好者。1. 为什么 Linux 要把内核做成“乐高积木”想理解内核模块先得理解一个核心矛盾Linux 是宏内核monolithic kernel所有核心子系统都放在内核空间里调度、内存管理、文件系统、设备驱动直接互相调用性能路径短、效率高。但这种设计有一个致命问题——不灵活。早期想增加一个驱动或者文件系统支持只能重新编译整个内核编一次二三十分钟配置文件写错还有可能起不来系统哪怕只是加一行打印都要经受这种折腾。模块机制的出现就是为了解决这个矛盾。它允许驱动、文件系统、网络协议等代码以独立文件的形式存放在/lib/modules/$(uname -r)/目录下运行系统时按需加载、不用时可以卸载。原本烧死在内核里的功能现在变成了可以随时拼装的积木块内核核心是底座各种.ko文件是积木insmod和modprobe就是负责搭积木的那双手。这套机制给日常使用带来的好处是非常直观的节省内存。服务器上不需要加载显卡、无线网卡驱动内存就不会被无谓代码占用笔记本上的蓝牙模块不影响桌面机桌面机也就不用背着蓝牙驱动的包袱。开发效率高。写内核驱动的大部分时间不需要重启机器单独编译一个模块加载测试发现问题改代码再编译循环非常快。硬件热插拔友好。插入 USB 设备、PCIe 设备时内核可以根据设备 ID 自动加载对应模块不需要手动重编内核。第三方驱动可以独立发布。显示驱动、虚拟化插件这类没法快速进主线内核的代码能以模块形式单独分发用户装完再用modprobe挂上就能用。但模块机制也不是没有代价。模块说到底还是内核代码和内核共享同一地址空间不享受用户态进程的隔离保护。一个模块里出现野指针或者内存越界后果不是段错误而是直接内核崩溃panic。而且恶意模块本身就是 rootkit 最常见的载体一个隐藏进程、篡改系统调用的模块加载进去用户态工具基本看不出来。这也是为什么现代内核加入了模块签名校验机制后面我会单独说。2. 看懂一个内核模块的三样东西拿到任何一个内核模块或者想从零写一个模块本质上只需要看明白三件事代码入口、编译规则、模块元信息。把这三样吃透大部分内核模块对你来说就已经不神秘了。2.1 代码骨架入口、出口和许可证一个最精简的内核模块代码长下面这样#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO Hello, Linux module!\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, Linux module!\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple demo module); MODULE_VERSION(0.1);这里面有几个关键点新手很容易一知半解。module_init(hello_init)和module_exit(hello_exit)是宏作用是把初始化函数和退出函数告诉内核。insmod加载模块时调用hello_initrmmod卸载模块时调用hello_exit。注意函数名可以随便取重要的是通过这两个宏“注册”进去。如果模块没有module_exit卸载时内核虽然能卸载但清理逻辑没地方执行设备资源、内核定时器、内存池就会漏光。__init和__exit这两个标记有特殊的工程作用。__init表示该函数只在加载阶段使用加载完成后内核会把这部分代码所在的内存页释放掉给其他用途腾地方。__exit同理在模块卸载后相关代码段也会被回收。这是一种非常抠内存的设计哲学用户态程序根本不会这么干因为用户态永远不愁物理内存接触不到。printk不是printf它不依赖标准 C 库内核里也没有浮点运算环境。它的输出会进内核日志缓冲区而不是直接打到终端。KERN_INFO是日志级别完整级别从KERN_EMERG到KERN_DEBUG默认控制台只显示级别不超过 4KERN_WARNING的信息所以很多时候内核日志能查到屏幕上就是看不到。我实际开发时习惯在加载和退出函数里各留一行KERN_INFO日志出问题先dmesg | tail比瞎猜强太多。MODULE_LICENSE(GPL)这行也值得认真对待。很多内核符号只对声明 GPL 的模块导出如果声明其他许可证或者不声明调用某个EXPORT_SYMBOL_GPL导出的函数时会直接失败。另外加载非 GPL 模块时内核会把系统标记为 tainted意思是“内核已经被污染”后续 panic 时堆栈记录也会带上这个标记。生产环境排查问题看到一个Tainted: P就要多留一个心眼。2.2 Makefile把源码变成 .ko 的魔法内核模块的编译不能直接用gcc hello.c -o hello.ko因为内核模块必须和当前运行内核的符号表、头文件、编译选项严格匹配。正确的姿势是使用内核构建系统Kbuild靠一个极简 Makefile 把编译流程交给内核的kbuild系统处理。obj-m hello.o all: make -C /lib/modules/$(shell uname -r)/build M$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M$(PWD) clean逐行拆解。obj-m hello.o是 Kbuild 的规则变量obj-m表示把hello.o及其同名的.c文件编译成内核模块最终生成hello.ko。如果改成obj-y则会把这段代码编译进内核镜像而不是独立模块。这个区别在嵌入式开发裁剪内核时很常见自己写实验模块用obj-m就够了。make -C /lib/modules/$(shell uname -r)/build M$(PWD) modules的意思是先切到/lib/modules/$(uname -r)/build这个目录也就是当前内核的构建目录执行modules目标同时指定M$(PWD)告诉 kbuild 我的模块源码在当前目录。这个目录必须存在它指向内核头文件和部分构建脚本Ubuntu 上由linux-headers-$(uname -r)包提供CentOS 对应kernel-devel。如果这个目录不存在编译第一步就会报错。clean目标用来清理编译产物。注意 Makefile 里的缩进必须是真正的 Tab不能是空格。我见过太多人在make -C前面用四个空格替代 Tab然后疑惑为什么missing separator。2.3 modinfo一眼看穿模块底细编译成功后先用modinfo hello.ko看一下信息filename: /home/user/hello/hello.ko license: GPL author: Your Name description: A simple demo module version: 0.1 vermagic: 5.15.0-91-generic SMP mod_unload depends: name: hello这里面最关键的是vermagic一行。它记录了编译这个模块时所用的内核版本、SMP对称多处理配置、抢占模型、编译器版本等一组校验信息。加载模块时内核会把模块的 vermagic 和当前运行内核比较任何一项不一致insmod都会以“Invalid module format”拒绝加载。很多人从网上找了个.ko文件直接往自己机器上塞失败后一脸懵99% 就是这个原因。depends字段也很重要表示这个模块依赖哪些其他模块。比如一个网卡驱动依赖mdio总线模块这里就会列出依赖名。手动insmod时不会自动加载依赖直接用modprobe则会根据依赖信息自动处理。3. 动手实操从零写一个 hello_linux 模块理论说了这么多不实际操作等于零。这一节我带你完整走一遍“环境准备 - 写代码 - 编译 - 加载 - 卸载”的过程。建议你在虚拟机里做别拿生产环境开玩笑。3.1 环境准备先确认系统里有没有安装当前内核对应的头文件。最直接的检测方法ls -l /lib/modules/$(uname -r)/build如果显示目录存在说明内核构建环境已经就绪如果报错说明缺包。Ubuntu / Debian 执行sudo apt update sudo apt install build-essential linux-headers-$(uname -r)CentOS / RHEL / Rocky 执行sudo yum install gcc make kernel-devel kernel-headers安装完成后再次执行ls -l /lib/modules/$(uname -r)/build确认目录存在。我在这一步踩过最大的坑是“头文件版本和当前内核版本不一致”。比如系统启动到了5.15.0-89-generic但你手滑安装的是linux-headers-5.15.0-91-generic那么/lib/modules/5.15.0-89-generic/build依然不存在编译照样失败。遇到这种情况最稳妥的办法是重装一次头文件或者重启到和头文件匹配的内核版本。3.2 编写源码和 Makefile创建工作目录mkdir -p ~/hello_linux_module cd ~/hello_linux_module创建hello.c内容就是上一节的精简版。为了后面演示参数传递我再加两个参数。完整内容如下#include linux/init.h #include linux/module.h #include linux/kernel.h #include linux/moduleparam.h static int count 1; static char *name world; module_param(count, int, 0644); module_param(name, charp, 0644); MODULE_PARM_DESC(count, How many times to print); MODULE_PARM_DESC(name, Who to greet); static int __init hello_init(void) { int i; for (i 0; i count; i) { printk(KERN_INFO Hello, %s!\n, name); } return 0; } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, %s!\n, name); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple demo module with parameters); MODULE_VERSION(0.1);创建Makefile内容和上一节一致。注意缩进。然后编译make如果一切正常目录下会多出hello.ko、hello.mod.o、hello.o、modules.order等一堆文件。hello.ko就是内核模块本体。3.3 加载、查看、卸载一条龙加载模块sudo insmod hello.ko注意insmod后面跟的是模块文件路径它不解析依赖你给什么就加载什么。如果加载成功没有任何输出因为它默认静默成功。看内核日志dmesg | tail你会在末尾看到Hello, world!确认模块是否存在于内存中lsmod | grep hello输出类似hello 16384 0第三列0表示引用计数也就是当前有几个进程或模块正在使用它。如果为0可以安全卸载。卸载模块sudo rmmod hello再dmesg | tail能看到退出日志。平时我们更常用modprobe而不是insmod。二者的核心区别是modprobe会去/lib/modules/$(uname -r)/目录里按模块名查找.ko文件并自动解析依赖关系。如果你把hello.ko安装到系统模块目录并执行depmod更新依赖索引之后就可以直接sudo modprobe hello和sudo modprobe -r hello。自己写实验模块时图快可以直接用insmod想模拟生产环境建议走一遍install流程。安装模块到系统目录sudo cp hello.ko /lib/modules/$(uname -r)/extra/ sudo depmod sudo modprobe hellodepmod这个命令平时很多人忽略它会把/lib/modules/$(uname -r)/下所有模块的依赖关系扫一遍生成modules.dep。没有它modprobe根本找不到你要的模块。3.4 给模块传参数让它听你的话上一节代码里已经写了两个参数count和name。加载时可以通过insmod传值sudo insmod hello.ko count5 namelinux内核日志里会连续打印五行Hello, linux!。module_param(count, int, 0644)中三个参数的含义分别是参数名count、类型int、权限0644。权限值很讲究它决定了模块加载后对应参数在 sysfs 节点/sys/module/hello/parameters/count下是否可读、可写。0644表示所有用户可读root 可写。加载完成后你可以cat /sys/module/hello/parameters/count echo 10 /sys/module/hello/parameters/count看起来能改运行时参数但这里有个误导改 sysfs 里的值只是改了内存中的变量如果你的模块没有实时读取这个变量来改变行为改了等于没改。像我上面这种count只在初始化函数里用一次运行后改 10 不会有任何效果。真正的运行时动态参数需要模块里有对应的通知机制或者周期性读取这个就不展开说了但你要明确概念。4. 内核模块的日常管理与场景套路写模块只是第一步真正在日常 Linux 使用中大量时间花在“看模块、找模块、加载模块、卸载模块”上。这套命令你用熟了比装一堆调参工具都管用。4.1 六条命令搭配使用我把与内核模块最相关的命令整理成一张速查表方便你做常用命令记录命令作用典型使用场景lsmod列出当前已加载的所有模块读的是/proc/modules确认驱动是否加载、看引用计数modinfo查看模块元信息、参数、依赖安装模块前先摸清底细insmod按路径加载一个.ko不解析依赖调试本地模块modprobe按模块名加载自动处理依赖正常安装/加载驱动rmmod按模块名卸载不处理依赖卸载指定模块modprobe -r按模块名卸载同时处理依赖卸载带依赖的模块depmod重建模块依赖索引文件新增模块到系统目录后必须执行dmesg查看内核环形缓冲区日志排查加载失败原因实际排查问题时的顺序一般是lsmod看现状dmesg看报错modinfo看包信息最后才决定modprobe加载还是rmmod卸载。modprobe从模块名找到实际文件的过程依赖/lib/modules/$(uname -r)/modules.dep索引。所以新放一个.ko进去不跑depmod再敲modprobe 你的模块名就会提示找不到这是新手最容易掉进去的坑。4.2 自动加载与开机生效很多模块需要在开机时就加载比如某些光纤卡驱动、磁盘阵卡驱动。自己编译的模块可以这样配置。在/etc/modules-load.d/目录下建一个.conf文件每行写一个模块名# /etc/modules-load.d/custom.conf hello这样系统启动时就会尝试加载hello。如果模块加载时需要参数可以在/etc/modprobe.d/目录下写配置# /etc/modprobe.d/hello.conf options hello count3 namelinux还有一种常见配置是禁用某个模块。系统自动探测到不该用的硬件时会主动加载驱动想屏蔽它只能用 blacklist# /etc/modprobe.d/blacklist-local.conf blacklist nouveau以nouveau为例很多使用专有显卡驱动的用户都会把它加入黑名单否则两个显卡驱动模块抢设备容易起冲突。4.3 模块卸载失败的排查卸载是很多新手最头疼的一步。最常见的报错是rmmod: ERROR: Module hello is in use by:这个时候lsmod | grep hello看到的第三列引用计数不是 0第四列可能会显示谁在使用比如某个网络接口绑定了这个驱动。你可以用下面的命令定位使用者lsmod | grep hello cat /proc/modules | grep hello ls -l /sys/module/hello/holders 2/dev/null找到引用者后先把对应的设备停掉或者先把上层模块卸载掉。比如网卡驱动被eth0占用就先ip link set eth0 down或者干脆停掉网络服务如果是依赖它的其他模块就用modprobe -r按依赖顺序递归卸载。排除这些外部因素后依旧报 in use那就有可能是驱动自身的引用计数 bug。模块内部调用了try_module_get却没对称调用module_put引用计数只增不减这种问题只能修代码靠命令行是救不了的。这类泄漏在生产事故里非常常见尤其在热插拔设备反复插拔的服务器上。4.4 高频场景里的内核模块内核模块不只是给驱动用的很多上层运维问题归根到底都能扯到内核模块上。Linux 安装 Docker 之后网络起不来是最典型的一类。Docker 的overlay2存储驱动依赖overlayfs模块容器网络依赖bridge、veth、nf_tables、br_netfilter等模块。如果宿主内核裁剪得太狠这些模块没加载docker run会报operation not permitted或者网络栈初始化失败。解决办法很直白sudo modprobe overlay sudo modprobe br_netfilter然后检查lsmod | grep overlay lsmod | grep br_netfilter虚拟机安装 Linux 蓝屏或直接黑屏也要从模块层面找原因。在 Linux 宿主机上跑 KVM 虚拟机时需要加载kvm_intel或kvm_amd模块如果 CPU 虚拟化没在 BIOS 里打开加载会直接失败虚拟机自然起不来。有些人在 VMware Workstation 里装 Linux 碰到蓝屏排查时会发现 3D 加速和虚拟显卡驱动模块不匹配临时解决办法是关闭 3D 加速或者改用默认的虚拟机显卡模型。Kali Linux 安装后无线网卡不识别几乎是新人必遇问题。Kali 的内核虽然带了大量驱动但很多无线网卡芯片需要单独固件文件或额外模块像 RTL88x2 系列芯片系统默认未必加载得自己编译模块再modprobe挂上。安装 Kali 之前最好先确认目标网卡芯片型号把对应模块和固件准备好。Linux 系统安装时黑屏、花屏很多人知道加nomodeset启动参数但很少人知道这个参数的底层逻辑其实是“不让内核提前加载显卡 DRM 模块并初始化显示”。等于绕开问题先让系统用最基本的 VESA 显示模式把安装界面跑起来装好系统再慢慢装厂商驱动。从内核模块的角度理解这个参数遇到相关图形问题就不会再瞎试。现在很多本地化的 Linux 发行版也遵循同样一套机制日常办公软件像 WorkBuddy、希沃白板这类 Linux 版明明只是应用层软件某些触摸功能、手写板支持异常查到最后往往也和底层的 USB HID 模块、图形适配层相关。内核模块在整个 Linux 生态里就像地基里的承重墙应用层表现出的很多疑难杂症源头都在这一层。5. 高频报错与避坑手册模块开发看着简单但踩坑的时候一个比一个隐蔽。我把这几年遇到最多的五个问题整理出来附上排查路径。5.1 version magic 不匹配报错一般长这样insmod: ERROR: could not insert module hello.ko: Invalid module formatdmesg里会更详细version magic 5.15.0-91-generic SMP mod_unload should be 5.15.0-89-generic SMP mod_unload 意思是模块编译用的内核头文件版本和当前运行内核不一致。解决办法有两个方向一是重装对应版本的头文件后重新编译模块二是重启到与头文件匹配的内核版本。我遇到最头疼的版本是 Secure Boot 开启时即使版本魔法匹配也可能加载失败因为未签名模块直接被拒。5.2 rmmod 提示 Module is in use这个前面已经聊过排查逻辑。补充一个容易忽略的点lsmod第三列引用计数确实是 0但模块依然无法卸载可能是有设备的struct file还没释放块设备、字符设备的 open 计数没归零。可以用fuser -v /dev/你的设备看有没有进程占用设备节点。先杀掉进程再卸载通常就顺了。5.3 加载模块后系统直接卡死或 panic内核模块没有用户态进程的保护边界崩溃就是整个系统崩溃。哪怕只是在初始化函数里执行了一个非法指针访问后果也是挂机。我做模块测试有一条铁律先上虚拟机跑通了再碰物理机。虚拟机里出问题可以快照回滚物理机上出问题只能抱着显示器看堆栈。如果已经在物理机上出问题通过串口或者netconsole抓内核日志是最可靠的排障方式。此外可以加内核启动参数nmi_watchdog1之类辅助定位死锁但对新手来说最快的方式还是重启用旧内核。5.4 Secure Boot 和模块签名现在很多电脑默认开启 UEFI Secure Boot内核只允许加载带有效签名的模块。未签名的第三方模块会出现这样的错误Module verification failed: signature not found解决办法有三种关闭 Secure Boot、给模块签名、使用 DKMS 自动处理签名。DKMS 这个机制值得单独说一下。它解决的是“内核升级后模块自动重建”的问题。像 VirtualBox 的vboxdrv、部分显卡驱动都推荐用 DKMS 安装。系统更新内核后DKMS 会自动把模块源码拿过来重新编译免去手动一个个适配的麻烦。对于要长期维护 Linux 机器的人来说了解 DKMS 能省下一大半精力。5.5 专有驱动与新硬件的兼容问题从 Blackwell 架构那代显卡发布开始很多人应该体会过“新硬件发布初期官方只提供私有内核模块”是什么意思。这类专有模块对内核版本的敏感度极高内核从 6.x 升到 6.y不重新编译就是加载失败甚至编译也会失败。新内核本身默认装载的开源驱动模块又会和专有驱动模块抢设备。我的建议是如果不追求新内核特性生产环境不要盲目升级内核如果确实需要新内核在升级前先把专有模块的兼容性确认好最好用 DKMS 托管起来。等官方发布适配版本后再统一升级。老话说“能用就不要动”在内核和驱动这一层尤其成立。6. 最后几条实战经验内核模块这块我踩过的坑不少写几条保命经验作为整个内容的收尾可能更实用。第一永远先看dmesg。不管是insmod失败还是rmmod失败内核都会把真实原因写在日志里。命令行提示只告诉你“不行”日志告诉你“为什么不行”。不会看内核日志搞内核模块等于盲人摸象。第二写模块时一定要做好退出逻辑的对称处理。初始化里kmalloc了退出函数里必须kfree申请了request_irq退出函数里就得free_irq。老话讲“谁申请谁释放一一对应”在驱动开发里是铁律。第三不要在重要机器上直接实验新模块。虚拟机、容器、闲置开发板哪个都比公司服务器适合当小白鼠。内核模块导致的崩溃不像用户态程序容不得你从容地保存文件。第四多留意vermagic和内核头文件版本。这两个东西不匹配能把人折腾到怀疑人生。编译前先uname -r然后顺着这个版本号去安装头文件比什么技巧都管用。内核模块说难不难说简单也不简单。它最迷人的地方在于你写的代码直接跑在内核空间上面是千军万马的各种系统调用下面是一块一块的硬件寄存器。理解这套机制不光是对驱动开发有帮助更能让你对 Linux 系统整体的运行逻辑有一个质的认识。