摘要
现场设备升级或重启后,自定义内核模块突然加载失败,这种事并不少见。
这次报错是:
Module sio_gpio not found in directory /lib/modules/6.8.0-111-generic
第一眼看,很容易以为 sio_gpio.ko 被删了。实际查下来,文件还在,只是放在旧内核目录里。
这次机器上旧模块放在:
/lib/modules/6.8.0-107-generic/extra/sio_gpio.ko
但系统当前已经启动到:
6.8.0-111-generic
而 modprobe 不会满机器乱找,它默认只查当前运行内核的模块目录:
/lib/modules/$(uname -r)
所以这次要确认的不是“机器上到底有没有这个 .ko 文件”,而是:
这个模块是不是在当前运行内核对应的
/lib/modules/$(uname -r)目录下。
下面按现场排查顺序记录一下:先怎么恢复,再怎么避免下次重启又踩一次。
一、问题现象
现场执行:
sudo modprobe sio_gpio
系统提示:
Module sio_gpio not found in directory /lib/modules/6.8.0-111-generic
看 /lib/modules,机器上其实有好几个内核版本:
6.5.0-18-generic
6.8.0-90-generic
6.8.0-94-generic
6.8.0-107-generic
6.8.0-110-generic
6.8.0-111-generic
再去旧内核目录看:
ls /lib/modules/6.8.0-107-generic/extra
能看到:
sio_gpio.ko
到这里就能先排除一件事:模块文件不是完全丢了。
问题卡在另一边:系统现在跑的已经不是 6.8.0-107-generic,而是 6.8.0-111-generic。
二、先看当前内核
排这种问题,先别急着复制文件,第一条命令看当前内核:
uname -r
如果输出是:
6.8.0-111-generic
那 modprobe sio_gpio 默认只会去这里找:
/lib/modules/6.8.0-111-generic/
它不会因为旧目录里存在:
/lib/modules/6.8.0-107-generic/extra/sio_gpio.ko
就自动跨版本加载。
所以这句 Module not found 不能直接理解成“整个系统里没有这个文件”。放到这次现场,它的意思更接近:
当前运行内核对应的模块目录里没有这个模块。
三、为什么换个内核就不行
Linux 内核模块不是普通动态库,不能只看文件名一样不一样。
.ko 模块通常跟内核版本、内核配置、符号表和编译参数绑在一起。哪怕都是 6.8.0 系列,6.8.0-107-generic 和 6.8.0-111-generic 对模块来说也不是同一个目标。
可以看一下这个模块当时是按哪个内核编出来的:
modinfo /lib/modules/6.8.0-107-generic/extra/sio_gpio.ko | grep vermagic
如果看到类似:
vermagic: 6.8.0-107-generic SMP preempt mod_unload modversions
而当前:
uname -r
输出是:
6.8.0-111-generic
那就很明确了:这个模块原本是给 6.8.0-107-generic 编译的。
这时有两种情况:
- 模块刚好还能被当前内核接受,复制到当前目录后可以临时加载
- 模块和当前内核 ABI 不匹配,复制过去也会报
Invalid module format
所以复制 .ko 只能当现场尝试,别把它当成正式修复。
四、先恢复现场:复制到当前内核目录
如果当前目标是先让设备恢复起来,可以先把旧目录里的模块放到当前内核目录。
sudo mkdir -p /lib/modules/$(uname -r)/extra
sudo cp /lib/modules/6.8.0-107-generic/extra/sio_gpio.ko \
/lib/modules/$(uname -r)/extra/
sudo depmod -a "$(uname -r)"
sudo modprobe sio_gpio
这里最容易漏的是这一步:
sudo depmod -a "$(uname -r)"
depmod 会重新生成模块依赖索引。否则文件虽然已经复制过去,modprobe 仍然可能查不到。
加载后再确认:
lsmod | grep sio_gpio
如果还想看内核侧日志:
sudo dmesg | tail -50
这套操作只能算临时恢复。它解决的是“当前内核目录里没有模块”的问题,不保证模块一定适配当前内核。
五、如果提示 Invalid module format
如果复制后执行:
sudo modprobe sio_gpio
报:
Invalid module format
或者 dmesg 里出现类似:
version magic '6.8.0-107-generic ...' should be '6.8.0-111-generic ...'
这就说明模块和当前内核对不上。
这时继续复制就没意义了,要在当前内核下重新编译。
先装当前内核头文件:
sudo apt update
sudo apt install build-essential "linux-headers-$(uname -r)"
然后进入 sio_gpio 源码目录:
make clean
make
安装到当前内核目录:
sudo mkdir -p /lib/modules/$(uname -r)/extra
sudo cp sio_gpio.ko /lib/modules/$(uname -r)/extra/
sudo depmod -a "$(uname -r)"
sudo modprobe sio_gpio
最后再确认:
lsmod | grep sio_gpio
这一步才算是针对当前内核处理。
六、排查顺序
这类问题按下面顺序走就行,没必要一上来就重装系统,也别先怀疑驱动源码。
这张图里别跳过前两步:
uname -r
和:
ls /lib/modules/$(uname -r)/extra
先把“当前内核”和“模块所在目录”对上,后面的判断才不会跑偏。
七、现场设备别让内核随便变
这次问题说到底,是系统升级后启动到了新内核。
如果这台机器是机器人主机、工控机、采集设备,或者任何依赖自定义驱动的现场设备,内核版本就别让它随便变。
可以把 GRUB 默认启动项固定到已验证过的内核,比如:
6.8.0-107-generic
先查看 GRUB 菜单项:
grep -E "^[[:space:]]*(submenu|menuentry) '" /boot/grub/grub.cfg
找到类似:
submenu 'Advanced options for Ubuntu' ...
Ubuntu, with Linux 6.8.0-107-generic
编辑:
sudo vim /etc/default/grub
把:
GRUB_DEFAULT=0
改成类似:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.8.0-107-generic"
这里的名称必须和 /boot/grub/grub.cfg 里的 submenu、menuentry 名称一致,中间用 > 连接。
更新 GRUB:
sudo update-grub
重启后确认:
uname -r
如果输出:
6.8.0-107-generic
说明启动内核已经回到指定版本。
八、还要控制 apt 升级内核
只固定 GRUB 还不够,这个坑下次还可能被 apt upgrade 带回来。
只要后面继续执行 apt upgrade,系统仍然可能安装新的内核包。现场设备如果没有驱动重编译流程,下次重启还是可能回到同一个问题。
先看本机实际安装的内核相关包:
dpkg -l 'linux-generic*' 'linux-image*' 'linux-headers*' 'linux-modules*' | grep '^ii'
如果确认用的是 Ubuntu 普通 generic 内核元包,可以锁定:
sudo apt-mark hold linux-generic linux-image-generic linux-headers-generic
如果用的是 HWE 内核,元包名称可能是 linux-generic-hwe-*、linux-image-generic-hwe-* 这一类,不要照抄,要按上一步查到的实际包名替换。
如果要锁定具体内核版本,也可以执行:
sudo apt-mark hold linux-image-6.8.0-107-generic
sudo apt-mark hold linux-headers-6.8.0-107-generic
sudo apt-mark hold linux-modules-6.8.0-107-generic
sudo apt-mark hold linux-modules-extra-6.8.0-107-generic
查看已经锁定的包:
apt-mark showhold | grep linux
这个做法能稳住现场。代价也很直接:内核安全更新不会再自动跟着走,后面要有人安排升级窗口和验证流程。
九、长期还是上 DKMS
如果 sio_gpio 要长期用,别一直靠手动复制 .ko,也别指望永远不升级内核。把它纳入 DKMS 更省心。
DKMS 干的就是这件事:
安装新内核时,自动为新内核重新编译并安装模块。
也就是说,系统从:
6.8.0-107-generic
升级到:
6.8.0-111-generic
DKMS 可以把 sio_gpio 重新编译到新内核对应的目录里。
我一般按这个顺序处理:
DKMS 自动重编译 > 固定内核版本 > 手动复制 .ko
手动复制是救急,固定内核是稳住现场,DKMS 才是长期维护自定义驱动该走的路。
小结
这次 sio_gpio 加载失败,不是模块文件被删了,而是模块还在旧内核目录,系统却已经启动到了新内核。
这几个对应关系别看错:
旧模块位置:
/lib/modules/6.8.0-107-generic/extra/sio_gpio.ko
当前运行内核:
6.8.0-111-generic
modprobe 查找目录:
/lib/modules/6.8.0-111-generic
现场先恢复,可以复制到当前内核目录并执行 depmod -a "$(uname -r)":
sudo mkdir -p /lib/modules/$(uname -r)/extra
sudo cp /lib/modules/6.8.0-107-generic/extra/sio_gpio.ko \
/lib/modules/$(uname -r)/extra/
sudo depmod -a "$(uname -r)"
sudo modprobe sio_gpio
如果提示 Invalid module format,就别继续绕了,直接按当前内核重新编译。
以后遇到类似问题,先记住这个判断:
modprobe找的是当前运行内核的模块目录,不是以前放过模块的目录。
对工控机、机器人主机、采集卡、GPIO、串口、CAN 这类依赖内核模块的设备来说,内核升级一定要和驱动验证放在一起做。普通升级看着没什么,落到现场设备上,可能就是驱动直接起不来。
评论区