☰
Linux内核模块管理:从lsmod到modprobe的依赖解析与排障指南
2026/9/26 17:00:52 网站建设 项目流程

前两天有同事抱着笔记本过来问我:模块编译成功了,modprobe 也执行了,退出码是 0,可 lsmod 里就是找不到这个模块。当时我没急着给答案,先让他把 dmesg 最后几十行拉出来看看。结果倒也不意外,加载失败的提示早就躺在那里了,只是被 modprobe 的"一切正常"掩盖掉了。这种场景我遇到太多次了。lsmod 确实是排查 Linux 内核模块问题的第一道入口,但它本身只是一个只读查看工具,真正决定模块能不能加载、能不能卸载的,是它背后的内核模块子系统、依赖解析和配置机制。这篇文章不打算只罗列命令参数,我想把 lsmod 和它上下游的几个命令——insmod、rmmod、modprobe、depmod、modinfo——串成一条完整链路讲清楚,适合刚接触内核模块的开发者、系统运维和嵌入式方向的 Linux 使用者,看完能自己动手排查大部分"模块不加载、卸载不掉"的问题。

1. lsmod 那几列数据是从哪来的:/proc/modules 的映射关系

1.1 lsmod 只是借用了 /proc/modules 的一张表

很多人以为 lsmod 会去遍历内核里某个神秘的数据结构,其实它做的事比想象中简单:读取 /proc/modules 这个虚拟文件,把里面的内容按列对齐,加上一行表头。你可以直接对比一下:

$ cat /proc/modules ext4 757760 3 - Live 0xffffffffc0000000 nls_ascii 16384 1 nls_cp437, Live 0xffffffffc01...
$ lsmod Module Size Used by ext4 757760 3 - nls_ascii 16384 1 nls_cp437

看明白了就清楚,lsmod 自己不做任何加载或卸载动作,它只是把内核模块链表通过 /proc/modules 暴露出来的信息打印得更整齐。字段顺序依次是:模块名、模块占用内存大小(单位是字节)、当前引用计数、是谁在引用它、模块状态。后面的地址和括号里的 taint 标记是给内核调试用的,lsmod 默认不展示。

所以如果你想当然地用 lsmod 来判断"模块到底有没有加载成功",是不够的。正确姿势是 lsmod 配合 dmesg、/proc/modules、/sys/module 三者一起看。这一点在后面几节的排障过程里会反复出现。

1.2 每一列的含义与排障用途

先把 lsmod 五列信息拆开看:

字段含义排障用途
Module模块名,对应 .ko 去掉扩展名后的名字查看哪些驱动在运行
Size模块代码段、数据段在内存中的占用(字节)粗略评估模块内存开销
Used by 里的数字内核模块引用计数卸载前必须确认是否为 0
Used by 里的列表持有引用的内核模块名找到谁在依赖它
StateLive / Loading / Unloading判断模块加载或卸载是否卡住

Size 这一列要留个心眼。内核加载 .ko 时会把代码段、只读数据段、可写数据段分别放进对应的内存区,lsmod 显示的大小是这些区域的总占用。但模块 init 段里的临时数据在初始化函数执行完后会被释放,所以实际常驻内存可能比 .ko 文件小,也比 Size 列看上去小。如果某个模块的 Size 异常大,先怀疑是不是编译时开了太多调试选项,再怀疑是不是把不该放 init 段的全局数据塞进去了。

State 列最常见的是 Live,表示正常加载。如果看到 Unloading,说明模块正卡在卸载流程里,通常是 delete_module 系统调用还在等它把资源清干净;看到 Loading 说明还在初始化。如果模块的 init 函数死循环,模块会一直卡在 Loading,这时候 dmesg 里基本都有调用栈,顺着栈去查模块代码比反复重启有用得多。

1.3 在容器和最小系统里 lsmod 为什么可能是空的

lsmod 依赖 /proc/modules 能否读到内核的模块列表。在完整物理机或虚拟机上没问题,但在某些容器环境里 /proc 是隔离的,/proc/modules 根本不存在或者内容为空,这时 lsmod 输出为空并不代表内核真的没加载模块。别一上来就怀疑驱动没装,先确认自己看的是不是宿主机视角。

最小化裁剪的嵌入式系统则更极端:如果内核编译时没开 CONFIG_MODULES,那 /proc/modules 连文件都没有,模块子系统整个不存在,驱动只能编进内核。判断方法很简单:

$ test -f /proc/modules && echo "modules enabled" || echo "no /proc/modules" $ grep CONFIG_MODULES /boot/config-$(uname -r)

这个坑我在至少两个容器场景里踩过,每次都以为是自己模块编译得不对,折腾半天才发现是视角问题。先确认环境再看数据,能省掉一上午。

2. Used by 背后的引用计数:模块卸载不掉的真正原因

2.1 引用计数什么时候会被加一

Used by 列里第一个看到的数字是内核模块引用计数。内核用这个计数决定模块能不能被安全卸载:计数为 0,可以卸;大于 0,卸载时内核直接拒绝,返回 EBUSY。引用计数主要在两个方向上涨,一是模块符号依赖,二是内核对象的使用句柄。

第一种是模块间依赖。模块 B 引用了模块 A 导出的符号,depmod 发现依赖关系后,modprobe 会先加载 A 再加载 B,A 的引用计数因为 B 的依赖而加一。B 卸载时,A 的引用计数再减一。这就是为什么 lsmod 里经常看到 fat 被 vfat 引用,或者 netfilter 的子表被某个 main table 引用。

第二种是用户态持有句柄。模块通过字符设备、块设备、proc 文件、sysfs 属性对用户态暴露能力,进程 open 设备节点时内核会调用 try_module_get 引用计数加一,直到 close 才减掉。这种引用不会在 Used by 列表里显示成某个模块名,因为持引用的不是一个内核模块,而是一个进程。这就是为什么我会说,lsmod 里能看到的只是"哪些内核模块在用它",看不见"哪些进程、文件描述符、挂载点在用它"。

2.2 Used by 列不等于全部真相:一个真实卸载失败案例

之前测试环境遇到一个现象:lsmod 里显示模块 mydev 的引用计数是 1,但 Used by 列表是空的。这个组合其实已经说明问题了——用它的不是内核模块,而是用户态进程。我猜是有程序一直占着 /dev/mydev 这个设备节点,用 fuser 验证:

$ lsmod | grep mydev mydev 16384 1 - $ fuser -v /dev/mydev USER PID ACCESS COMMAND /dev/mydev: root 1234 rw mydaemon

把 mydaemon 停掉之后,再 rmmod mydev 就干净利落。排这类问题记住一条链路:先看 lsmod 引用计数,如果大于 0 但看不到模块名,去翻 /proc/modules 第三列确认数值,然后用 lsof、fuser 找出打开设备节点的进程。还有一种情况是模块提供文件系统驱动,挂载点没卸载,虽然 lsmod 看不出端倪,umount 之后引用计数自己就归零了。

如果引用计数显示为 0 仍卸不掉,那大概率是模块内部自己拽着一把锁、一个 workqueue 或者中断处理例程不放。这时候在 rmmod 上反复试没意义,上 dmesg 看卸载时打印的调用栈才是正确方向。新版内核基本不允许强制卸载,就算你找到 rmmod -f 这种参数,强拆之后的野指针问题也只是延后爆发,别在生产环境碰。

2.3 卸载顺序与 modprobe -r 的连锁动作

rmmod 一次只能卸一个模块,而且不会去解依赖链。你先加载 vfat,它依赖 fat;rmmod vfat 之后,fat 可能还孤零零挂在列表里,引用计数已经是 0 了。要手动清理就得再执行一次 rmmod fat,很啰嗦,而且容易漏。

modprobe -r 会把卸载这件事做得更聪明一点,它会判断这个模块被移除后,依赖链上那些模块是否还有别的使用者;如果某个依赖模块因此变成无人引用,就顺手一起卸掉。当然它也严格遵循引用计数,任何还有使用者的模块都不会被强卸。所以日常运维里,要"把一个功能整体撤掉",用 modprobe -r 更符合直觉;要"精确控制某个模块去留",用 rmmod 更直接。

3. insmod、rmmod、modprobe:三种工具,两种加载哲学

3.1 insmod:按路径加载,不管依赖

insmod 的用法看起来最简单:

sudo insmod /lib/modules/$(uname -r)/kernel/drivers/net/xxx.ko sudo insmod ./hello.ko

它背后走的是 init_module 或 finit_module 系统调用,把 .ko 文件内容直接交给内核,由内核负责解析 ELF、重定位、执行模块初始化。insmod 不管依赖:如果这个模块引用了别的模块导出的符号,而那个模块还没加载,内核在重定位阶段找不到符号就会报 Unknown symbol in module。它也不去 /lib/modules 下帮你搜索模块文件,路径给错了就直接失败。

所以 insmod 更适合"我就一个 .ko,所有依赖已经齐了"的场景,典型就是内核模块开发调试阶段。我写模块还在改代码的时候,基本只用 insmod,因为路径明确,参数明确,出了问题也容易定位。但一旦进入部署和管理层面,insmod 就不够看了。

3.2 rmmod:按名字卸载,但管不了引用

rmmod 和 insmod 一样直球,把模块名告诉它就行,不需要路径。它调的是 delete_module 系统调用。如果模块引用计数不为 0,内核直接返回错误,rmmod 报 Module is in use。

rmmod 不会去判断依赖。比如你先加载 vfat,再想清理干净,得先卸 vfat 再卸 fat,两个命令分开执行。它也不会自动处理模块配置,比如卸载前要不要先跑一段清理脚本,这些它都不管。好处是行为可预期,适合在脚本里精确控制。

另外提醒一句,rmmod 需要 root 权限。普通用户执行会得到 Operation not permitted,除非你有专门的 capability(CAP_SYS_MODULE),否则别想着在无特权容器里卸载模块,内核不会同意的。

3.3 modprobe:真正能在生产环境直接用的入口

modprobe 的本质是"按模块名加载,并自动处理依赖、配置和别名"。它不需要你写路径,自己会去 /lib/modules/$(uname -r)/ 下找对应 .ko;它也不要求你手动判断依赖,读取 modules.dep 之后会把依赖模块按顺序 insmod 进来。

modprobe 还叠加了配置层:/etc/modprobe.d 和 /lib/modprobe.d 下的文件可以设置模块参数、黑名单、自定义 install/remove 命令,甚至把某个模块名映射成一个 alias。这些能力是 insmod、rmmod 根本不具备的。

日常用得最多的几个组合:

sudo modprobe e1000e sudo modprobe -r e1000e sudo modprobe -v e1000e # 打印实际执行的 insmod 序列 sudo modprobe -n -v e1000e # 只打印不执行,排障演练首选

其中 -n -v 组合是我排障时的第一步。它不会真的碰系统,只会把 modprobe 准备执行的命令原样打出来,一眼就能判断配置有没有问题。

3.4 一张表记住什么时候用哪个

场景命令原因
临时加载单个模块,已知路径insmod简单直接,不引入依赖解析
判断加载逻辑,干跑演练modprobe -n -v只打印命令,不执行
按名加载正式驱动modprobe依赖、别名、配置全自动
卸载单个模块rmmod精确控制
卸载模块及其连带依赖modprobe -r自动清理引用链
查询模块文件信息modinfo查看编译期元数据
新增或替换 .ko 后刷新索引depmod -a依赖表必须更新

这张表是我实际项目的使用习惯,不是教材式分类。记住一条原则:写内核模块代码时用 insmod/rmmod,部署系统时用 depmod/modprobe,两者职责完全不同。

4. depmod 与依赖图:为什么 modprobe 能自动拉起一串模块

4.1 "Unknown symbol" 是怎么产生的

内核模块本质上是内核态的 ELF 目标文件。不同 .ko 之间通过 EXPORT_SYMBOL 导出符号,别的模块引用这些符号,本质上和用户态程序链接 .so 类似,只不过链接时机发生在模块加载那一刻。如果你 insmod 一个引用了未加载模块符号的 .ko,内核无法重定位,就会报 Unknown symbol。

dmesg 里一般会写清楚到底是哪个符号缺了,像这样:

$ dmesg | tail xxx: Unknown symbol my_func_a (err -2)

这种情况不是模块坏了,是依赖没满足。看到这个错误别急着改代码,先想想这个符号属于哪个模块,把那个模块先加载起来。很多新人在这里兜圈子,其实是没理解"模块加载 = 运行时链接"这回事。

4.2 depmod 做的事:把 .ko 之间的依赖关系编成目录

depmod -a 会扫描 /lib/modules/$(uname -r)/ 下所有 .ko,解析每个 ELF 文件里引用而未定义的符号,再在已加载模块导出的符号表里反查谁提供了这些符号,最终生成 modules.dep、modules.dep.bin、modules.alias、modules.symbols 等文件。modules.dep 每一行格式类似:

/lib/modules/6.8.0-45-generic/kernel/fs/fat/fat.ko: /lib/modules/6.8.0-45-generic/kernel/fs/vfat/vfat.ko: /lib/modules/6.8.0-45-generic/kernel/fs/fat/fat.ko

冒号后面就是依赖列表。modprobe 加载 vfat 时读到这一行,会先把 fat 加载进来,再加载 vfat。

关键点在于:如果你手动拷贝了一个新 .ko 到系统里,或者自己交叉编译后 make modules_install,modules.dep 不会自动更新。modprobe 很可能找不到它,或者即使找到也不知道依赖关系。所以每次往系统里加模块,第一件事就是跑:

sudo depmod -a

然后再 modprobe。这个坑我在嵌入式板上踩过太多次,make modules_install 在部分交叉编译场景不会自动调用 depmod,结果 modprobe 一直报 FATAL: Module not found,而 .ko 文件明明就躺在目录里。

4.3 动手复现一次依赖加载过程

最直观的验证方式是写两个小模块:module_a.ko 导出函数 my_a_func,module_b.ko 调用它。把两个 .ko 拷贝到 /lib/modules/$(uname -r)/extra/,先不跑 depmod,直接执行:

$ sudo modprobe module_b modprobe: FATAL: Module module_b not found in directory /lib/modules/$(uname -r)

跑了 depmod -a 之后再看:

$ sudo modprobe -n -v module_b insmod /lib/modules/$(uname -r)/extra/module_a.ko insmod /lib/modules/$(uname -r)/extra/module_b.ko

注意顺序:先 A 后 B。这就是依赖解析的实际效果。真的执行 modprobe module_b,然后 lsmod | grep module_,两个模块都在列表里,module_a 的 Used by 会写着 module_b。这套流程非常适合新手理解"依赖"这个概念,比看任何文档都直观。

5. modinfo 与 alias:插上设备后驱动是怎么"自己"出现的

5.1 modinfo 里最值得看的五个字段

modinfo 和 lsmod 一样是只读命令,但它读的不是运行时信息,而是模块文件本身的编译期元数据,也就是写进 .ko 的那些 MODULE_* 宏。用法很简单:

modinfo e1000e modinfo /path/to/xxx.ko modinfo -F parm e1000e # 只取某个字段

值得关注五个字段。filename 告诉你模块实际躺在哪个文件,排查路径问题不用瞎猜;license 决定这个模块能不能被 GPL 符号引用,很多内核只对 GPL 模块开放特定符号;depends 直接列出依赖,和 lsmod 的 Used by 互为镜像,一个说"我需要谁",一个说"谁再用我";vermagic 是内核版本和配置签名,加载时内核会严格比对,不一致直接 Invalid module format;parm 是模块支持的可传参数列表,名字、类型、默认值全在里面。

调试网卡这类驱动时,很多厂商驱动会用 parm 暴露 ring size、interrupt throttle 等参数,modinfo -F parm 一下全出来,比翻源码快得多。

5.2 alias、modalias 与 udev:自动加载的完整链路

你可能从来没手动 modprobe 过某个驱动,但只要插上一个 USB 设备,lsmod 里就会多出对应的模块。这条链路的入口就是 alias。

内核在设备加入时会生成一个 uevent,里面带一行 MODALIAS,类似 usb:v1D6Bp0102d0006dc00dsc00dp00ic08isc06ip50in00 这样的字符串。udev 收到后,把这串字符串作为参数去调 modprobe,modprobe 拿着它去 modules.alias 里查表,找到对应模块名,再按依赖链加载。modules.alias 是 depmod 生成的,它把所有 .ko 里的 MODULE_ALIAS 声明收集起来做成索引。

所以一个模块要被"自动"加载,关键不在它本身写得多好,而在于它有对应的 MODULE_ALIAS 声明,并且 depmod 已经跑过。modinfo 里看到的 alias 行,就是给这条链路准备的。如果你写的模块希望插上设备就能自动加载,记得在产品驱动里加上 MODULE_ALIAS,否则用户永远得手动 insmod,体验极差。

5.3 modules.builtin:为什么有时候 lsmod 查不到驱动

有些驱动你 lsmod 怎么都找不到,但设备工作得好好的,这时候八成是它根本没做成模块,而是直接编进了内核镜像。内核编译时如果某个驱动选项是 =y,它就成为内核的一部分,不会出现在模块加载列表里。为了保留这些信息,编译时会生成 modules.builtin 文件。

排障套路:lsmod 里没有某个模块,先别急着下结论,执行:

grep 模块名 /lib/modules/$(uname -r)/modules.builtin grep CONFIG_XXX /boot/config-$(uname -r)

如果在这里面找到了,说明驱动已经内置,功能正常,只是 lsmod 这个视角天然看不到它。很多人在这儿卡半天,其实是把"模块化"和"编译进内核"两件事搞混了。lsmod 只反映模块子系统,不反映内核镜像自带的驱动。

6. 开机自动加载与 /etc/modprobe.d:藏在配置里的"隐形开关"

6.1 开机时谁在加载模块

除了 udev 在设备出现时动态加载,开机阶段还会有一份固定模块清单被加载。现代 systemd 发行版主要看 /etc/modules-load.d/ 下的 .conf 文件,每行一个模块名;老一点的方式是通过 /etc/modules。无论哪种,最终都是把这些名字喂给 modprobe 去执行。

如果你希望某个模块在任何设备插入之前就绪,比如虚拟化模块 kvm_intel,或者启动早期就要就位的网卡驱动,就把模块名写进 /etc/modules-load.d/xxx.conf。注意模块名不要带 .ko 后缀,也不要带路径。验证文件有没有生效,重启后可以看 systemd-modules-load 服务的运行状态,dmesg 里也会有对应的一次性加载记录。

6.2 /etc/modprobe.d 里的 options、blacklist 和 install

模块参数除了加载时写在命令行,更常用的做法是写进配置,让 modprobe 每次加载都自动带上:

options e1000e InterruptThrottleRate=3,3

blacklist 用来阻止"自动加载",典型场景是两个驱动争抢同一个硬件:

blacklist nouveau

但要注意 blacklist 的语义:它只是让 udev 和 modprobe 在自动匹配 alias 时不选这个模块,不代表模块被禁用了。你手动执行 modprobe nouveau,它照样加载。想彻底屏蔽某个驱动,用 install 覆盖:

install nouveau /bin/true

这样 modprobe nouveau 会假装成功,什么都不加载,日志也不会报错。一些驱动冲突问题就是靠这一招解决的。install 也可以写成一条真正的命令,比如加载前先做硬件配置,功能非常强大,但也容易把系统搞乱,不建议新手随便玩。

所有 /etc/modprobe.d 里的改动,可以执行 modprobe -c 查看合并后的完整配置,确认你的设置真的生效了。

6.3 一次"模块被悄悄屏蔽"的真实排障

回到开头说的那个故事。同事的 uvcvideo 摄像头驱动在一次系统升级后 lsmod 里消失了,设备完全不工作。我查了 dmesg,没有加载失败记录;lsmod 也确实没有 uvcvideo。正在怀疑是固件问题,无意中跑了:

modprobe -v uvcvideo

输出里没有出现 insmod,反而出现了一行 install uvcvideo /bin/true。这下真相大白:某个软件包在安装时往 /etc/modprobe.d/ 里写了一条屏蔽配置,把整个摄像头驱动摁住了。注释掉那行配置,再执行 modprobe uvcvideo,lsmod 里立刻出现了模块,摄像头也恢复正常。

这次排障里最有价值的一步就是 modprobe -v。它把 modprobe 实际执行的内容打印出来,到底是真 insmod 还是用户自定义的 install 脚本,一眼就能看出来。平时我排查模块相关问题,第一步永远是 modprobe -n -v,先看命令序列,再谈加载,这个习惯帮我挡掉了不少莫名其妙的坑。

最后分享一个固定习惯:无论什么时候往系统里加了 .ko,都按 depmod -a、modprobe -n -v、lsmod 这个顺序走一遍。前两步把依赖和配置层的问题提前暴露出来,最后一步用 lsmod 确认运行时状态。这套流程简单,但确实能挡住绝大多数模块加载的暗坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询