Linux内核模块卸载命令rmmod完全指南:用法、原理与排坑
搞内核驱动和系统调试的人几乎都绕不开内核模块的加载与卸载而rmmod就是这个体系里最直接、最基础的卸载命令。我之前有段时间天天和驱动打交道卸载模块时动不动就报 Module is in use一开始也懵后来把rmmod的机制、参数、依赖关系彻底捋了一遍才算真正玩明白。这篇不扯虚的直接把rmmod的用法、原理、实操和踩坑经验一次说清。1. 内核模块的加载机制与rmmod的职责1.1 为什么需要动态卸载模块Linux内核为了兼顾功能完整性和系统精简采用了模块化设计。设备驱动、文件系统、网络协议等组件并不全编译进内核核心而是做成独立的内核模块(.ko文件)按需加载。这样做的好处很明显系统启动速度快内存占用低硬件变更时不用重新编译整个内核。有加载就必然有卸载。比如你调试一块USB网卡驱动改完代码重新编译出新的ko文件就得先把旧的模块卸载掉再加载新模块。再比如某些硬件支持热插拔设备拔掉之后对应的驱动模块如果继续留在内存里白占内存不说还可能干扰后续设备识别。rmmod就是用来完成卸载这个动作的工具。我在实际调试中感受最深的一点是动态卸载让内核开发迭代效率大幅提升。没有它每改一次驱动代码就得重启整台机器那调试周期要以小时计。有了rmmod几十秒就能完成一次卸载-编译-加载-测试循环。1.2 rmmod、modprobe与insmod的分工差异新手最容易混淆的三个命令就是insmod、rmmod和modprobe。先给它们做一个简单分工insmod加载指定的模块文件参数写死不处理依赖。rmmod卸载已加载的模块只负责卸载同样不处理依赖。modprobe智能加载/卸载会自动分析模块依赖关系加载时先装依赖模块卸载时逆向卸载。三者本质上是不同层级的工具。rmmod和insmod是原始操作把你怎么说的就怎么做不帮任何额外的事modprobe则是基于模块依赖数据库的封装更符合日常使用习惯。但这里有个关键点等会我会细讲当你需要精确控制、排查模块卸载问题或者模块依赖关系不完整时rmmod反而比modprobe更可预期。因为modprobe的依赖解析机制在某些手动编译安装模块的场景下会失效——它读的是系统的modules.dep文件而这个文件不会自动感知你手动拷贝到/lib/modules下的新ko文件。1.3 rmmod执行时发生了什么rmmod表面上看只是一个命令但后端的执行链路很有代表性。当你敲下rmmod xxx它通过delete_module这个系统调用通知内核内核会做三件事检查模块的引用计数(refcnt)。如果计数不为0说明有别的模块或进程正在使用它拒绝卸载并返回EBUSY。检查模块是否注册了退出函数(cleanup函数)。如果有调用它执行清理动作比如释放内存、注销设备号、删除proc条目等。确认清理流程全部走完后把模块的代码段和数据段从内核内存中移除模块标记为未加载状态。这三个步骤听起来简单但每一步都有不少坑。比如引用计数检查失败、退出函数里没把资源释放干净、模块在卸载时阻塞等我在下文会结合实例展开。2. 掌握rmmod命令的核心选项2.1 常规卸载与显示信息rmmod最基本的用法就是直接加模块名rmmod 模块名注意这里模块名不需要带.ko后缀也不需要写完整路径。比如模块文件是hello.ko加载后模块名是hello卸载时直接写rmmod hello即可。如果不知道模块名先敲lsmod查看已加载模块列表的第一列那列就是模块名。常用选项里-v(--verbose)用于显示详细执行过程建议每次都用能看清楚内核到底执行了什么rmmod -v hello正常输出类似Deleting module: hello如果模块没加载会报错Module hello is not loaded。用-v能看到这个明确的信息不加-v时部分发行版上也提示存在但有些脚本环境下不打印任何内容容易产生歧义。还有一个容易被忽略的选项是-s(--syslog)。它把卸载过程中的错误或提示写到内核日志(通过printk)里而不是标准输出。在脚本环境中标准输出可能被吞掉这时候用-s配合dmesg查看日志是排查模块卸载问题的利器rmmod -s hello dmesg | tail2.2 无视强制的风险-f选项rmmod的-f(--force)选项官方解释是强制卸载模块即使引用计数不为0或模块正在被使用。有些老手调侃这个选项是自己的模块自己清零因为实际使用中效果很依赖模块是否设置了允许强制卸载的标志。内核模块分两类一类是GPL许可且显式支持强制卸载的可以通过MODULE_FORCE_UNLOAD宏声明另一类没有这个声明即使加了-f也会被内核拒绝。这算是一个安全保护——并非所有模块都能强制卸载。强行卸载一个正在使用的模块后果非常严重内存被访问后飞掉直接内核崩溃(panic)。模块占用的设备节点、中断号没有释放后续加载同名模块时冲突不断。依赖它的其他模块长时间处于半挂状态系统行为诡异。我的建议只在开发调试的虚拟机里且明确知道模块不会被访问时使用-f。生产环境绝不能用-f强卸这是我踩过坑后的真实感受——曾经在测试机上强卸一个网卡驱动结果整个网络栈直接锁死只能硬重启。2.3 等待与自动清理-w选项-w(--wait)选项在部分新版本内核中被启用行为是如果模块当前正在被使用rmmod不会直接报错退出而是阻塞等待直到引用计数降为0后再卸载。场景举例某个内核线程正在使用模块A计数为1但它在300毫秒后就会释放。此时rmmod -w A会等这300毫秒然后自动完成卸载。而普通rmmod A会立刻报错Module A is in use。刚上手时我习惯性地认为-w什么都能等但内核线程如果一直不释放模块引用-w就会一直卡住看起来像死锁。实际使用时建议配合timeout命令一起用timeout 5 rmmod -w hello超过5秒没卸掉就自动退出避免终端被卡死。2.4 命令参数速查表参数功能说明适用场景-v显示详细操作信息确认卸载是否成功-s记录到系统日志脚本环境排查错误-w等待引用计数归零后卸载模块稍后被释放的场景-f强制卸载(需模块支持)开发测试环境生产禁用3. 实操从加载到卸载的一次完整演练3.1 先构造一个测试模块理论讲再多都不如实际跑一遍。我第一次完整跑通这套流程时用的是自写的一个hello驱动模块代码里注册了字符设备加载时打印初始化信息卸载时打印清理信息。构造它的目的就是看rmmod执行后内核日志里输出的清理流程。先建个目录写模块源码mkdir -p ~/modtest cd ~/modtest模块代码很简单但五脏俱全#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO hello module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL);然后写个Makefileobj-m : hello.o KERNELDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean执行编译make目录下会生成hello.ko这就是我们要加载和卸载的模块文件。3.2 逐层执行加载、查询、卸载先加载模块sudo insmod hello.ko确认模块加载状态lsmod | grep hello此时应该看到一行记录第一列是hello第二列是模块占用内存大小第三列是引用计数。刚加载时计数为0。再看实时内核日志确认init函数被执行dmesg | tail -5能看到hello module loaded。到这里动态加载就体验完了。然后执行卸载操作用我最推荐的组合参数sudo rmmod -v hello再查一次lsmod | grep hello应该没有任何输出说明模块已经成功卸掉。dmesg里也会多出一行hello module unloaded证明cleanup函数正确触发。这个过程中真正重要的是每执行一步都回头看一次内核日志和lsmod状态形成命令执行状态验证的闭环习惯。很多内核模块出问题都是因为加载/卸载后没有及时验证状态导致错误一步步累积。3.3 脚本化的加载卸载流程手工敲命令适合单次调试批次测试就要脚本化。这里分享一个我调试驱动时常用的小脚本模板#!/bin/bash MOD_NAMEhello MOD_FILE./hello.ko # 加载前先清理残留 if lsmod | grep -q ^${MOD_NAME}; then echo Module already loaded, removing first sudo rmmod ${MOD_NAME} fi # 加载模块 sudo insmod ${MOD_FILE} || { echo insmod failed; exit 1; } echo Module loaded, refcnt$(lsmod | grep ^${MOD_NAME} | awk {print $3}) # 检查初始化日志 dmesg | grep -E hello | tail -3 # 业务操作...这里省略 # 卸载模块 sudo rmmod -v ${MOD_NAME}脚本的核心思路是先清理残留再加载避免Module already loaded错误。4. 依赖关系与模块引用计数rmmod的隐形门槛4.1 模块之间的依赖是怎么形成的很多模块不是孤立的。举个例子USB存储驱动依赖usb-core声卡驱动依赖snd-pcm等基础模块。被依赖的基础模块会记录依赖方的引用这就是它的引用计数来源之一。当你尝试卸载一个有依赖方的模块时内核会检查引用计数发现非0直接拒绝。这很好理解上游还在用你你突然撤了上游马上崩给你看。我看过有人在论坛抱怨rmmod怎么不灵贴出来的截图里明明模块没啥特殊就是卸不掉。结果一查lsmod模块下面挂了好几个use by依赖子模块没卸完父模块根本不可能卸掉。4.2 用lsmod解读依赖关系lsmod的输出格式Module Used By usb_storage 2 usbcore 25Used By这一列如果是0表示没有模块在用它。如果不是0后面的数字是被引用的次数如果显示模块名则明确列出了是哪些模块引用了它。正确卸载有依赖的模块步骤是由子到父逐层卸载。比如usb_storage依赖usbcore那就先卸usb_storage再卸usbcoresudo rmmod usb_storage sudo rmmod usbcore顺序一旦反了内核会报Module usbcore is in use的错。判断依赖方向其实有规律驱动代码里调用了其他模块导出的符号就会产生依赖被调用的那一方是父亲调用方是孩子。4.3 modprobe -r如何简化依赖卸载modprobe -r模块名会自动分析依赖链从最上层的子模块开始往前卸直到把目标模块卸完。这个功能的底层依据是modules.dep文件里面记录了模块依赖的完整拓扑。手工安装模块场景下modules.dep文件往往不更新modprobe不识别你新编译的ko。此时用rmmod反而更可靠——它不解析依赖只按模块名直接从当前内核的已加载模块列表里找找到了就卸。所以当我编译开发模块时卸载一律用rmmod管理发行版自带的模块时才用modprobe -r。5. 典型问题与排查记录实战中的坑与解法5.1 模块卸载失败提示Module X is in use这是最常见的错误也是新手第一个坎。原因不外乎两种一是其他内核模块引用计数非0二是有进程打开了与此模块关联的设备文件。排查步骤lsmod确认模块名和引用计数。检查 /proc/modules 中的第二列和第三列第二列是内存大小第三列就是引用计数。如果是别的模块引用按依赖关系先卸掉子模块。如果是进程占用用fuser命令找到设备的占用进程fuser -v /dev/你的设备节点找到之后杀掉占用进程或正常关闭程序释放设备文件句柄再执行rmmod。这里特别提醒一个隐蔽场景应用程序把设备文件打开后没关即使进程已经退出只要句柄没释放引用计数依然不为0rmmod就卡着。有些老进程异常退出时会留下这种僵局需要重启相关服务或重启系统才能释放。5.2 卸载时系统直接死机或报Oops模块的exit函数里有资源没释放或者释放顺序不对是卸载时崩溃的头号原因。我调试一个串口驱动时遇到过exit函数里释放了中断却在之后又尝试往下写寄存器内核直接报Oops。这个问题本质上是模块代码有问题不是rmmod本身的问题。排查经验如下先在调试机上复现用dmesg看崩溃前的最后几条内核日志定位崩溃点在哪个函数。检查exit函数里的资源释放逻辑特别是中断、定时器、workqueue、proc节点这些内核对象必须按注册顺序逆序释放。用kernel debug options比如打开KMEMLEAK和FAULT_INJECTION能更快暴露资源泄漏。处理卸载崩溃我个人的建议是不要追求unload得快代码写完exit函数多review几遍把release路径当成主业务流程对待。5.3 强制卸载后模块残留或状态混乱有的模块内部维护了全局链表或缓存强制卸载时exit函数没有正确清理即使模块从内核模块列表消失了实际资源还挂在系统里。最典型的表现是再次insmod同一个模块时报设备号冲突、proc目录已存在、中断号已被占用等乱七八糟的错误。这种情况的标准处理流程是重启系统恢复干净环境。真要定位泄漏点得借助kprobes或者ftrace跟踪模块的分配和释放路径。实际工程里重启往往是最快的选择毕竟强卸本身已经制造了不可信状态。5.4 模块卸载后内存没释放干净rmmod正常卸载后理论上模块的内存会被释放。但模块运行期间动态分配的内存如果不依靠exit函数里的对应释放逻辑就会变成内核内存泄漏。卸载模块后free命令看到的内存并不会立刻变少很多因为内核页缓存等机制会掩盖一部分。检测方式是在卸载前后分别用cat /proc/meminfo查看Slab和KernelStack等指标的变化。如果模块反复加载卸载后内存持续增长说明exit函数里的释放逻辑有缺失。我曾经遇到过某个网卡驱动模块每次加载卸载后Slab内存增加约几十KB循环20次后系统可用内存明显下降最后定位到是驱动里的一个固定大小的kmalloc没有释放。5.5 常见错误速查表错误信息直接原因解决方向Module X is in use有引用计数检查子模块和进程占用Module X is not loaded模块未加载或名称错误lsmod确认模块名Permission denied当前用户无权限sudo或root执行ERROR: Module X does not exist参数写成了ko文件名去掉.ko后缀rmmod: ERROR: could not remove module X内核拒绝卸载看dmesg定位具体原因6. 从rmmod延伸出的系统管理经验6.1 生产环境卸载模块前必须做的安全检查生产环境卸载内核模块前期准备比执行命令本身重要得多。我之前在生产环境卸载一个存储驱动前会先做以下检查确认模块确实没有业务流量在走看设备的读写进程列表。确认依赖该模块的软件栈会不会受影响包括用户态的工具和内核态的子模块。准备好回滚方案包括模块文件备份和自动加载配置备份。在执行窗口前先做一次虚拟机的演练确认卸载后系统功能符合预期。这些看起来繁琐但真到生产环境出问题时每一步都是救命的。我见过为了省事直接rmmod卸载后业务起不来的情况而回滚方案没准备的话只能在业务停机中找解决方案。6.2 避免自动加载模块的陷阱有些模块被配置为开机自动加载rmmod虽然能卸掉但一旦系统内部触发模块的请求比如访问对应硬件设备内核会自动再次加载。这是因为Kernel Module Auto Loading的机制用户态程序访问设备节点或发特定请求时内核会触发modprobe自动加载对应模块。如果在测试环境想保持模块卸载状态需要暂时把自动加载配置移掉。以某模块为例sudo mv /lib/modules/$(uname -r)/modules.alias /tmp/或按发行版规范修改/etc/modprobe.d下的配置加入blacklistblacklist 模块名否则你卸了又自动加载排查问题时会怀疑人生的。6.3 rmmod与内核日志的协同排查法我调试模块问题时习惯同时开两个终端一个执行rmmod操作另一个实时跟踪dmesg输出watch -n 0.5 dmesg | tail -20这样卸载命令一执行内核反馈日志立刻显示出来。很多rmmod的问题都不是rmmod本身报错而是卸载后内核日志连续刷出多个异常通过日志分析比反复执行rmmod更高效。这个习惯帮过我大忙。有一次某个驱动模块卸载后系统出现周期性网络中断现场没人能解释。我通过观察卸载前后dmesg的日志流发现模块卸载时注册的netlink回调没有注销导致内核持续向用户空间发消息消耗大量软中断资源。后来在退出函数里补上注销逻辑问题才消失。每次卸载模块我都把dmesg从crash之前的几百行翻一遍已成固定动作。6.4 从rmmod到驱动热插拔机制的思考rmmod解决的模块生命周期管理问题也是设备热插拔管理的基础。当USB设备拔出时内核调用驱动remove回调驱动内部再触发模块引用计数下降为模块自动卸载创造条件。理解rmmod后再去看驱动的probe/remove、module_init/module_exit这个生命周期脉络整个内核驱动框架会清晰很多。每次我在驱动代码里编写remove函数时都会下意识地想这段清理逻辑够不够让rmmod安全执行这个思维模式对我来说受益良多。很多驱动模块的设计缺陷从rmmod能不能干净卸载这个角度一照直接原形毕露。回到实操层面还是那句话rmmod不复杂真正复杂的是模块自身的资源管理。把模块的加载和卸载路径都纳入测试覆盖你会发现系统稳定性的提升比你想象的大得多。