1. Ubuntu内核模块编译全流程解析
在Ubuntu系统上修改并重新编译内核模块是Linux开发者常遇到的需求场景。最近我在调试USB串口驱动时,就完整走了一遍从源码获取到模块编译安装的全过程。这个过程中踩了不少坑,也积累了一些实用经验,今天就把完整操作流程和避坑指南分享给大家。
2. 环境准备与源码获取
2.1 启用源码仓库
Ubuntu默认不启用源码仓库,需要先修改/etc/apt/sources.list文件:
bash复制sudo sed -i '/^#\sdeb-src/s/^#//' /etc/apt/sources.list
sudo apt update
这个操作实际上是将所有被注释的deb-src行取消注释。我建议在执行前先备份原文件:
bash复制sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
2.2 解决文件描述符限制问题
获取内核源码时最常见的错误是"Too many open files",这是因为系统默认的文件描述符限制太低。解决方法如下:
- 查看当前限制:
bash复制ulimit -n
- 修改系统限制(需要root权限):
bash复制sudo bash -c 'echo "* soft nofile 1048576" >> /etc/security/limits.conf'
sudo bash -c 'echo "* hard nofile 1048576" >> /etc/security/limits.conf'
sudo bash -c 'echo "session required pam_limits.so" >> /etc/pam.d/common-session'
- 重新登录使配置生效
注意:1048576这个值对大多数场景已经足够,如果仍遇到问题可以适当增大。修改后需要完全退出当前会话重新登录才能生效。
3. 内核源码获取与准备
3.1 下载当前运行内核的源码
使用apt source命令获取与当前运行内核完全匹配的源码:
bash复制apt source linux-image-unsigned-$(uname -r)
这个命令会自动下载三个文件:
- .dsc (描述文件)
- .diff.gz (补丁文件)
- .tar.gz (源码压缩包)
并自动解压到linux-版本号目录下。
3.2 解决权限问题
进入源码目录后,经常会遇到脚本没有执行权限的问题:
bash复制cd linux-$(uname -r | cut -d'-' -f1)
find . -type f -name "*.sh" -exec chmod +x {} \;
find . -type f -name "Kconfig" -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
这个步骤很关键,否则后续的make oldconfig等命令会因为权限问题失败。
4. 内核配置与编译
4.1 准备内核配置
从/boot目录复制当前内核的配置:
bash复制cp /boot/config-$(uname -r) .config
然后更新配置:
bash复制make oldconfig
这个命令会基于当前.config文件,对新版本内核新增的配置项进行交互式询问。如果不想手动确认每个新增选项,可以使用:
bash复制yes "" | make oldconfig
4.2 编译特定模块
假设我们要编译USB串口驱动模块:
bash复制make M=drivers/usb/serial modules -j$(nproc)
这里有几个关键点:
- M= 指定模块路径
- -j$(nproc) 使用所有CPU核心并行编译
- 必须先执行make prepare和make modules_prepare
4.3 解决模块依赖问题
编译时可能会遇到符号未定义的警告,这是因为缺少Module.symvers文件:
bash复制cp /usr/src/linux-headers-$(uname -r)/Module.symvers .
如果提示缺少scripts/module.lds,需要先编译整个内核的基础部分:
bash复制make scripts/module.lds
make prepare
make modules_prepare
5. 模块安装与测试
5.1 安装编译好的模块
编译完成后,可以单独安装修改过的模块:
bash复制sudo make M=drivers/usb/serial modules_install
这会将模块安装到/lib/modules/$(uname -r)/kernel/drivers/usb/serial/目录下。
5.2 加载测试新模块
先卸载旧模块(以usbserial为例):
bash复制sudo modprobe -r usbserial
然后加载新编译的模块:
bash复制sudo modprobe usbserial
可以通过dmesg查看模块加载日志:
bash复制dmesg | tail -20
6. 常见问题与解决方案
6.1 编译错误排查指南
-
"No rule to make target"错误:
- 确保执行了make prepare和make modules_prepare
- 检查M=指定的路径是否正确
-
符号未定义警告:
- 确认Module.symvers文件已正确复制
- 可能需要先编译整个内核:make vmlinux
-
权限相关问题:
- 确保所有.sh文件有执行权限
- 确保Kconfig文件有读取权限
6.2 性能优化建议
- 使用ccache加速重复编译:
bash复制sudo apt install ccache
export CC="ccache gcc"
- 只编译所需模块:
bash复制make M=path/to/module modules
- 清理中间文件节省空间:
bash复制make M=path/to/module clean
7. 高级技巧与经验分享
7.1 增量编译技巧
修改单个源文件后,可以只重新编译该文件:
bash复制make M=drivers/usb/serial drivers/usb/serial/usb-serial.o
然后重新生成模块:
bash复制make M=drivers/usb/serial modules
7.2 调试符号保留
如果需要调试模块,编译时加上调试信息:
bash复制make EXTRA_CFLAGS="-g" M=drivers/usb/serial modules
7.3 版本控制集成
建议将修改后的内核代码纳入git管理:
bash复制git init
git add .
git commit -m "Initial source"
这样方便后续追踪修改和回滚。
8. 完整编译流程总结
经过多次实践,我总结出最可靠的内核模块修改编译流程:
- 准备环境:设置文件描述符限制,安装依赖
- 获取源码:apt source获取匹配版本
- 修复权限:确保所有脚本可执行
- 配置内核:复制现有配置并更新
- 准备编译:make prepare和modules_prepare
- 编译模块:make M=path modules
- 安装测试:modules_install和modprobe
这个流程在Ubuntu 20.04/22.04 LTS上测试通过,适用于大多数内核模块的修改和重新编译需求。