☰
Linux内核模块管理实战:lsmod与modprobe等命令全解析
2026/10/19 3:44:06 网站建设 项目流程

1. 先搞清楚lsmod到底在看什么

如果你管理过几台Linux服务器,或者自己折腾过内核驱动,那你一定对lsmod不陌生。它就是用来显示当前内核中已经加载的模块列表的。说白了,内核模块就是Linux内核的"插件",驱动、文件系统支持、网络协议栈的某些部分,都以模块的形式存在,按需加载。而lsmod,就是查看这些插件当前状态的最直接入口。

我刚接触Linux那会儿,其实并没太把lsmod当回事,觉得不就是列个清单吗?直到后来遇到一次网卡驱动加载失败的问题,排查来排查去,最后靠lsmod的输出和modinfo、modprobe配合,才定位到是模块依赖关系断裂导致的。从那之后我就懂了,lsmod的输出不只是"看有没有加载",它记录的依赖关系、被引用次数,都是排查内核模块问题的关键线索。

这篇文章,我就把lsmod以及它身边那一圈命令——modprobe、insmod、rmmod、modinfo、depmod、dmesg——一起串起来讲。内容定位是"内核模块管理的实用手册",适合刚入门Linux的运维新手,也适合那些需要自己编译驱动、调内核参数的成手。

2. lsmod的输出信息怎么读

2.1 三列数据的含义

运行lsmod,输出大概长这样:

$ lsmod Module Size Used by xt_conntrack 16384 1 nf_conntrack 172032 1 xt_conntrack iptable_filter 16384 1 ip_tables 28672 1 iptable_filter x_tables 45056 8 iptable_filter, ip_tables, xt_conntrack

三列信息分别是:

  • Module:模块名称。注意这里显示的是模块名,不是文件名。比如磁盘驱动对应的模块名叫sd_mod,但它的文件是/lib/modules/$(uname -r)/kernel/drivers/scsi/sd_mod.ko。
  • Size:模块在内存中占用的字节数。是十进制,不是十六进制。严格说这是模块代码段、数据段等占用的内存总和,可以用来快速评估模块对内存的影响。比如有些老显卡驱动模块能占到几MB,在内存紧张的嵌入式设备上就得留意。
  • Used by:这个字段最容易被人忽略,但它恰恰最有价值。它表示当前有哪些其他模块或内核功能正在引用这个模块。引用计数为0,说明当前没有人用它,理论上可以安全卸载;计数不为0,卸载时就会报Module is in use。

看到这里你可能会问:这个Used by是从哪来的?答案是/proc/modules。lsmod本质上只是把/proc/modules里的内容格式化了一下然后打印出来。你可以直接对比:

$ cat /proc/modules xt_conntrack 16384 1 - Live 0x0000000000000000 nf_conntrack 172032 3 nf_nat, xt_conntrack, nf_conntrack_ipv4 Live 0x0000000000000000

格式是:模块名、内存大小、引用计数、依赖列表、模块状态(Live/Loading/Unloading)、模块在内核中的内存地址。lsmod把它重新排版,去掉了内核地址和状态字段(状态基本都是Live),给你看更清爽的三列。

注意:lsmod显示的Used by列,数值为0并不代表模块绝对安全,因为有些模块会被内核子系统间接引用,但引用关系没有通过模块依赖机制体现。这种"假安全"状态在调试时容易误导人,后面我会专门讲。

2.2 依赖关系的隐藏逻辑

内核模块之间是有依赖的。一个模块可能依赖另一个模块提供的符号(函数、变量)。比如上面例子里的x_tables被8个模块引用,意味着那8个模块都用了x_tables导出的接口。这种依赖关系是编译时写进模块元数据里的,也被记录在modules.dep文件中。

真正读依赖关系,应该用modinfo和depmod。modinfo能看单个模块的详细信息:

$ modinfo x_tables filename: /lib/modules/6.1.0/kernel/net/netfilter/x_tables.ko license: GPL description: x_tables - default packet classifier for netfilter depends: retpoline: Y intree: Y name: x_tables vermagic: 6.1.0 SMP preempt mod_unload

depends字段为空,说明x_tables不依赖别的模块,但它可以被其他模块依赖。反过来,看nf_conntrack:

$ modinfo nf_conntrack depends: nf_defrag_ipv6, nf_defrag_ipv4

这就说明,要加载nf_conntrack,必须先加载nf_defrag_ipv6和nf_defrag_ipv4。lsmod的Used by列,实际上就是模块依赖关系的反向视图——它告诉你"谁依赖了我"。

依赖机制的根源在于内核符号表。模块A要使用模块B导出的函数,加载A之前B必须先存在。这和动态链接库的思路几乎一样,只不过这颗"动态链接库"跑在内核态,一旦出错就是panic级别的问题。

3. 管理模块的完整命令家族

3.1 模块加载:modprobe与insmod的区别

加载模块有两个命令:insmod和modprobe。它们的区别,我一句话总结:insmod是死脑筋,只会按你给的路径硬加载单个模块文件;modprobe是聪明人,会先去查依赖,把依赖的依赖全都准备好,再加载目标模块。

举个例子。你要加载一个nf_conntrack模块:

$ insmod /lib/modules/6.1.0/kernel/net/netfilter/nf_conntrack.ko

如果系统里还没有nf_defrag_ipv4,这条命令大概率直接报错,告诉你有一堆未知符号(unknown symbols)。因为nf_conntrack里用到的那些符号,内核里根本没有。

但如果你:

$ modprobe nf_conntrack

modprobe会去查modules.dep,发现依赖nf_defrag_ipv4和nf_defrag_ipv6,先自动加载这两个,然后才加载nf_conntrack。整个过程一条命令搞定。

所以日常操作,用modprobe加载模块;只有调试单个模块、且明确知道依赖都已加载时,才用insmod。我自己写驱动测试的时候才偶尔用insmod,因为它不会去解析依赖,加载失败时的报错信息对定位编译问题更直接。

3.2 模块卸载:rmmod的边界条件

卸载用rmmod。最基本的用法:

$ rmmod x_tables

如果模块正在被其他模块引用,系统会拒绝卸载,并提示Module x_tables is in use。这和安全删除文件系统时的Device or resource busy是一个道理,内核必须防止你卸载一个正在被调用的模块,否则内核态的悬空指针分分钟让你死机。

rmmod有个参数-f(force),可以强制卸载。但根据我个人的血泪教训,千万不要在生产环境用rmmod -f。强制卸载意味着内核不会检查模块间的引用关系,被卸载模块的代码段可能还在别的模块的执行路径上。一旦执行路径走到那段已经被释放的代码,内核直接panic,重启都救不了,只能去看crash dump。

另外提一句,内核有modunload配置选项,某些内核编译时关闭了这个功能,那所有模块都无法卸载。检查方法就是看modinfo输出里的vermagic字段,我上面贴的那个就带mod_unload字样,说明支持模块卸载。有些嵌入式设备的内核不带这个参数,折腾时要先确认。

3.3 模块参数与modprobe配置文件

内核模块不是只能"加载/卸载"这么简单的。很多模块支持参数,加载时可以传参改变行为。比如某些网卡驱动模块可以设置中断阈值,某些存储模块可以设置队列深度。传参有几种方式:

  • 直接命令行传参:modprobe mydriver param=1或insmod mydriver.ko param=1
  • 写进配置文件:/etc/modprobe.d/*.conf

配置文件的方式最常用,因为重启后不会丢。比如我给某块网卡驱动设置参数,就在/etc/modprobe.d/my-eth.conf里写:

options mydriver ring_size=512

这样无论什么时候加载mydriver模块,都会带上ring_size=512。

配置文件的真正威力体现在blacklist和alias上。blacklist用于屏蔽模块自动加载:

blacklist psmouse

这在笔记本上关掉触摸板驱动很常用。alias用于给模块指定别名,让系统知道某个设备该用哪个模块:

alias eth0 e1000e

这是老派写法,现在udev处理设备节点更精细,但理解这种机制对排障仍有帮助。

3.4 模块信息查询:modinfo与dmesg

modinfo已经展示过了,它能从.ko文件的元数据段读出license、author、description、依赖、参数等关键信息。调试驱动时,我第一个动作几乎永远是modinfo 模块名,先看depends和parm,心里就有数了。

加载模块的日志,要用dmesg看。内核模块的打印,走的是printk,日志默认输出到内核环形缓冲区。modprobe某个模块失败时,dmesg里往往有具体原因,比命令行的报错信息丰富得多。比如:

$ modprobe mydriver modprobe: ERROR: could not insert 'mydriver': Exec format error $ dmesg | tail -n 10 [ 1234.567890] mydriver: version magic '6.1.0 SMP preempt mod_unload' should be '6.1.0-rc2 SMP preempt mod_unload'

看到version magic不匹配,就知道是内核版本编译环境的差异导致的。这类信息命令行列不出来,只有dmesg里有。

4. 实战:从模块加载到依赖关系排查

4.1 一次完整的模块操作流程

我拿一个真实场景走一遍完整流程。假设我在一台x86服务器上,要启用一个测试用的字符设备驱动,模块文件是testdrv.ko,放在当前目录。

第一步,确认内核版本和模块目录存在:

$ uname -r 6.1.0-8-amd64 $ ls /lib/modules/6.1.0-8-amd64/ build kernel modules.dep modules.alias modules.symbols ...

注意modules.dep、modules.alias、modules.symbols这些文件,它们是depmod生成的元数据索引。手工把.ko拷进系统后,必须重新运行depmod,这些索引才会更新。

第二步,我把testdrv.ko拷贝到对应内核的模块目录下(通常是/lib/modules/$(uname -r)/extra/,没有就建一个),然后:

$ depmod -a

这条命令会扫描所有.ko文件,重新生成modules.dep体系。如果不跑depmod,modprobe在默认路径下找不到testdrv,就算文件已经在目录里也一样报Could not find module。

第三步,用modinfo看模块元数据:

$ modinfo testdrv filename: /lib/modules/6.1.0-8-amd64/extra/testdrv.ko license: GPL depends: parm: debug (bool)

depends为空,说明没有依赖;parm里有个debug参数。于是加载并传参:

$ modprobe testdrv debug=1 $ lsmod | grep testdrv testdrv 16384 0

模块加载成功,引用计数为0(因为还没有应用打开这个设备)。

第四步,创建设备节点并验证:

$ cat /proc/devices | grep testdrv 240 testdrv $ mknod /dev/testdrv c 240 0

这样应用层就能通过/dev/testdrv访问这个驱动了。

第五步,测试完卸载:

$ rmmod testdrv $ lsmod | grep testdrv $ dmesg | tail -n 5 [ 1300.123456] testdrv: exit

这个过程虽然简单,但把depmod、modinfo、modprobe、lsmod、rmmod、dmesg全都串起来了。日常管理第三方驱动,流程基本就是这样。

4.2 依赖断裂排查实录

有一次我在一台设备上装某品牌网卡驱动,按厂家文档做完,modprobe却报错。当时的报错信息:

modprobe: ERROR: could not insert 'vendor_net': Unknown symbol in module, or unknown parameter

这个报错常见,但原因可以有很多。手法是先看dmesg:

[ 4567.890123] vendor_net: Unknown symbol xxx_register_netdev (err -2)

err -2(-ENOENT)说明内核里没有xxx_register_netdev这个符号。有两种可能:

  • 提供这个符号的模块没有加载。用grep xxx_register_netdev /proc/kallsyms查一下,如果查不到,说明提供方确实没加载。
  • 模块间符号导出版本不匹配。提供方的模块版本和依赖方编译时使用的头文件接口不一致,符号存在但签名不匹配。

查了一遍,发现是依赖模块没有加载。加载依赖模块后,vendor_net顺利加载。这类问题最大的坑在于:报错信息只告诉你未知符号,却不告诉你谁该提供这个符号。这时就要靠modinfo的depends字段顺藤摸瓜,或者用nm查看.ko文件的未定义符号列表:

$ nm vendor_net.ko | grep ' U ' U xxx_register_netdev U printk

再看其他模块有没有导出这些符号:

$ grep xxx_register_netdev /lib/modules/6.1.0-8-amd64/modules.symbols

这个文件记录了符号-模块对应关系。如果一个符号在modules.symbols里查询不到对应的模块,那说明要么内核本身没有这个符号,要么模块编译时用的接口版本不对。我踩过的坑是:某供应商的驱动,在旧内核上编译后,直接拷到新内核环境用insmod加载,结果一堆Unknown symbol,重新在新内核环境下编译才解决。

4.3 模块自动加载机制

系统启动时,模块是怎么被自动加载的?理解这个对排查"为什么驱动不生效"很有用。

核心是udev和kmod的配合。硬件设备被内核枚举出来后,内核通过uevent把设备信息发给udev,udev根据/etc/udev/rules.d/里的规则和modules.alias匹配对应的模块名,然后调用modprobe去加载。

modules.alias这个文件也是depmod生成的,它把模块的"设备别名"和模块文件名关联起来。比如某个USB网卡的模块,alias可能是:

alias usb:v1D6Bp0104d*dc*dsc*dp*icFFiscFFipFFin* rt2800usb

含义是:符合条件的USB设备(vendor ID 1D6B,product ID 0104等),内核会尝试加载rt2800usb。

所以,如果你的驱动没有生成alias信息,或者udev规则没有正确匹配,那设备插上后驱动就不会自动加载。排查自动加载问题时,可以手动触发一次:

$ echo 1 > /sys/bus/pci/rescan

然后看dmesg和lsmod。如果手动都加载不了,那问题就回到前面的依赖排查路线了。

手动写udev规则也可以强制指定模块。在/etc/udev/rules.d/70-my-net.rules里:

ACTION=="add", SUBSYSTEM=="net", KERNEL=="eth*", RUN+="/sbin/modprobe mydriver"

这种写法不够优雅,但在驱动开发调试阶段挺实用。

5. 模块参数、黑名单与性能排查

5.1 常用参数排查思路

很多模块的参数可以通过/sys/module/<模块名>/parameters/在运行时查看。比如:

$ ls /sys/module/testdrv/parameters/ debug $ cat /sys/module/testdrv/parameters/debug N

这里显示debug参数当前为N(关闭)。如果在/etc/modprobe.d/里配置了options testdrv debug=1,重启加载后这个文件里就会显示Y。

利用这个sysfs接口,可以不改代码、不重新编译,就调整模块的运行时行为。前提是模块代码支持对应参数。有些模块把参数限定为只读(read-only),这种情况下文件内容是不可写的,只能通过加载时传递。

排查性能问题时,我经常先看模块参数的当前值,再对照出厂默认值。比如某存储控制器的模块有一个queue_depth参数,默认是32,但实际工作负载可能需要更大队列深度。通过/sys/module/xxx/parameters/queue_depth一看,原来只有默认值,调大后IOPS有明显改善。

5.2 模块黑名单的深坑

blacklist功能的实现方式,其实很多人理解有偏差。它并不是让内核永远不加载该模块,而是阻止modprobe根据alias自动加载它。如果你在命令行里显式执行modprobe 模块名,blacklist是拦不住的。这是个重要细节:

$ cat /etc/modprobe.d/blacklist.conf blacklist bad_driver $ modprobe bad_driver # 这仍然会尝试加载

所以如果你的目的是彻底禁用某个驱动,光写blacklist还不够。正确组合拳是blacklist加上udev规则的屏蔽,或者直接把模块文件挪走。有些发行版提供/etc/modprobe.d/blacklist-<包名>.conf,机制相同。

另一个典型场景是:系统里同时存在多个驱动,它们匹配同一类设备。比如两个帧缓冲驱动,一个内建一个模块化。为了防止模块化的那个被自动加载抢占设备,就需要blacklist它。诊断这类问题的工具,除了lsmod看当前加载了谁,还可以用/sys/bus/下的设备信息确认实际绑定的驱动:

$ ls -l /sys/bus/pci/devices/0000:03:00.0/driver

这个符号链接指向实际绑定到该PCI设备的驱动。如果绑定的不是你想用的那个,而你认为它不该被绑定,那就查模块加载顺序和blacklist配置。

5.3 lsmod用于内存占用审计

搭载大量第三方驱动的环境,模块占用内存不容小觑。lsmod的Size列,可以快速算出来:

$ lsmod | awk 'NR>1 {sum += $2} END {print sum}'

这个值表示所有模块静态占用内存的总和,单位字节。注意这不包括模块运行时动态分配的内存,所以只是一个下界。但即使如此,你也能一眼看出哪些模块体积异常。

遇到某个模块数值特别大,可以用modinfo看看它加载了哪些固件(firmware)。有些驱动把固件加载到内存里,module size虽然不大,但fw相关的内存占用才是大头。这时lsmod就不够用了,得看/proc/slabinfo之类更细的内存分配信息。

另外,如果你在做内核裁剪,lsmod还能告诉你系统实际用到了哪些模块,哪些模块从未被加载。统计方法:

$ find /lib/modules/$(uname -r)/kernel/ -name '*.ko' | wc -l $ lsmod | wc -l

第一个数字是可用的模块总数,第二个是当前加载的模块数。这两者的差距,就是可以精简的空间,对嵌入式系统的rootfs瘦身非常有参考价值。

6. 常见问题与排查技巧实录

6.1 问题速查表

现象可能原因排查命令/方法
modprobe提示Could not find module模块路径不对,或depmod未更新modprobe -v 模块名;确认.ko在正确目录并执行depmod -a
modprobe提示Unknown symbol依赖模块未加载,或接口版本不匹配dmesg看具体符号;nm查看未定义符号;grep 符号 modules.symbols
rmmod提示Module is in use被其他模块或内核引用lsmod查看Used by列;先卸载引用模块
模块加载后设备不工作参数配置不当,或模块加载顺序问题查看/sys/module/模块名/parameters/;调整modprobe.d配置
系统启动后驱动未自动加载udev alias不匹配,或模块被blacklistmodinfo查看alias字段;检查/etc/modprobe.d/
模块加载导致内核panic模块与内核版本不兼容,或强制卸载后访问重启;加载前用modinfo核对vermagic;永远不要在生产环境rmmod -f
lsmod看不到某个模块但设备正常工作驱动被编译进内核而不是模块grep 模块 /lib/modules/$(uname -r)/modules.builtin

6.2 一个有趣的调试案例

有一次,某台服务器升级内核后,重启发现某个硬件设备不识别了。升级前还一切正常。

查看dmesg,发现设备确实被枚举到了,但内核尝试绑定驱动时失败。再用lsmod一看,对应的驱动模块根本没加载。为什么?

我检查了modules.alias,发现新内核的模块alias生成方式变了,老驱动模块的alias信息没有正确生成。重新执行depmod -a后,alias信息更新,但modprobe加载后还是报Invalid module format。用modinfo对比后发现,该驱动是在旧内核头文件环境下编译的,新内核的module_layout符号版本不一致,内核拒绝加载这种"跨版本"的模块。

解决方法是重新编译驱动。这个案例再次说明了:每次升级内核后,第三方驱动模块都必须重新编译,这是内核模块机制的一个固有约束,也是很多刚接触内核模块的人经常忽略的点。这里的module_layout符号,是内核特意用来标记模块编译环境的防御机制。所有使用内核导出符号的模块,都会链接到module_layout这个符号,通过它的版本指纹判断模块与内核是否兼容。

6.3 我的几条实操心得

第一,修改模块参数后,先查/sys/module/里的实际值,不要想当然认为配置生效了。有时候配置写错路径,或者模块已经被内建,modprobe.d里的配置根本不生效,sysfs里的参数值会告诉你真相。

第二,排查模块问题,dmesg永远比命令行报错信息更值得信任。modprobe的报错是给"人类看的摘要",而dmesg里是内核的真实反馈。我见过太多人卡在Unknown symbol上,却很少有人第一时间去翻dmesg。只要养成这个习惯,一半以上的模块问题都能快速定位。

第三,不轻易用rmmod -f。我承认某些特殊场景(比如开发调试自己的模块代码)强制卸载能省事,但前提是你清楚知道模块代码当前不被任何路径执行。在共享服务器或生产环境,这个前提很难保证,所以宁可等引用计数归零。

第四,善用modprobe -v。它会打印modprobe内部的动作:先加载了哪些依赖模块,最后加载目标模块。这个输出对理解模块依赖关系非常有帮助。比如:

$ modprobe -v nf_conntrack insmod /lib/modules/6.1.0/kernel/net/netfilter/nf_defrag_ipv4.ko insmod /lib/modules/6.1.0/kernel/net/netfilter/nf_defrag_ipv6.ko insmod /lib/modules/6.1.0/kernel/net/netfilter/nf_conntrack.ko

看到这几行,你就明白modprobe比insmod聪明在哪了。

7. 从lsmod出发,建立内核模块的整体认知

lsmod虽然只是一个小命令,但它摆在了内核模块管理入口的位置。通过它,你能看到模块被加载了多少、占用多少内存、被谁引用。顺着这个入口往深处走,会接触到底层的符号依赖机制、内核编译的版本约束、设备与驱动的匹配逻辑。

如果你只是在用Linux桌面或跑几个容器,可能很长时间都不需要碰lsmod。但一旦涉及硬件设备驱动、内核调优、嵌入式开发,lsmod和它的伙伴们就会成为你日常工具箱里不可或缺的部分。我个人的经验是,把这几个命令的用法和原理彻底吃透,远比背一堆外部的"运维脚本"更能解决实际生产问题。

最后再分享一个小习惯:我会在排查完一个模块问题后,把lsmod、dmesg、modprobe -v三者的输出存到同一个日志文件里。以后遇到类似问题,直接对比这三个时间点的数据,能省掉很多重复排查的功夫。内核模块管理就是这样,用的命令不多,但每个命令都能提供不同维度的信息,组合起来才有价值。

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

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

立即咨询