本文概述:Linux内核模块是可以动态加载和卸载的内核代码,是Linux驱动开发的基本形式。开发流程包括:编写模块源码(包含module_init/module_exit宏、许可声明等)、编写Makefile(指定内核源码路径和模块目标)、交叉编译生成.ko文件、用insmod加载模块、用rmmod卸载模块、通过dmesg和printk调试。字符设备驱动是最基础的驱动类型,需要注册设备号、创建cdev、实现file_operations操作集、创建设备节点。
Linux终端显示内核模块编译加载命令行

面试题

说一下你写Linux内核模块的完整流程。从编写代码到加载运行,中间有哪些步骤?遇到过什么问题,怎么解决的?

面试官考察点

  • 是否实际做过内核模块开发,还是只背过概念

  • 是否掌握模块的基本结构和编译方法

  • 是否了解模块加载卸载的机制和工具

  • 是否有调试驱动的实际经验

  • 对字符设备驱动框架的理解程度

核心要点速览

  • 内核模块是动态加载的,不需要重新编译内核

  • 模块源码必须包含module_init、module_exit和MODULE_LICENSE

  • 编译需要对应版本的内核源码树,用Kbuild/Makefile体系

  • insmod加载模块,rmmod卸载,lsmod查看已加载模块

  • dmesg和printk是最基础的调试手段

  • 字符设备驱动:注册设备号→初始化cdev→添加到内核→创建设备节点

内核模块开发完整流程

第 1 步
编写模块源码

最简单的内核模块需要包含几个基本要素:

  • 头文件:linux/module.h、linux/init.h、linux/kernel.h等

  • 初始化函数:用__init标记,模块加载时执行

  • 退出函数:用__exit标记,模块卸载时执行

  • module_init和module_exit宏:注册初始化和退出函数

  • MODULE_LICENSE:声明许可证,不写的话内核会"被污染"(tainted)

/* hello.c - 最简单的内核模块 */ #include #include #include static int __init hello_init(void) { printk(KERN_INFO "Hello, kernel module! "); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, kernel module! "); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Fanyi");
第 2 步
编写Makefile

内核模块的编译不是普通的gcc编译,而是利用内核的Kbuild编译体系。Makefile写法比较固定:

# Makefile obj-m += hello.o KDIR := /path/to/kernel/source PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

要点:obj-m指定要编译的模块目标;-C指定内核源码路径;M=指定模块源码所在目录。嵌入式开发中KDIR要指向目标平台的内核源码,还要指定交叉编译工具链。

第 3 步
交叉编译生成.ko文件

对于嵌入式开发,需要用交叉编译工具链编译,不能用主机的gcc。通常设置环境变量:

  • ARCH:目标架构(如arm、arm64)

  • CROSS_COMPILE:交叉编译工具前缀(如arm-linux-gnueabihf-)

编译成功后会生成hello.ko文件,这就是可以加载到内核的模块文件。

注意:编译模块所用的内核源码版本必须与目标板上运行的内核版本一致,否则会出现版本魔术(vermagic)不匹配的问题,加载失败。

第 4 步
传输到目标板并加载

把.ko文件传到目标板上(通过scp、tftp、NFS等方式),然后用insmod加载:

# 加载模块 insmod hello.ko # 查看是否加载成功 lsmod | grep hello # 查看内核日志 dmesg | tail

也可以用modprobe加载,但modprobe要求模块已经放在/lib/modules/目录下,并且有modules.dep依赖文件。insmod更直接,适合开发调试阶段。

第 5 步
测试与调试

模块加载后,通过各种方式测试功能是否正常:

  • 字符设备驱动:创建设备节点,用open/read/write/ioctl测试

  • 平台驱动:看probe函数是否执行,设备是否正确初始化

  • 用dmesg看printk输出,排查执行流程

  • 用/proc和/sys查看设备状态

调试手段:printk是最基础也是最常用的;复杂问题可以用kgdb远程调试;还可以用ftrace跟踪内核函数调用。

第 6 步
卸载模块
# 卸载模块 rmmod hello # 或者用modprobe modprobe -r hello # 确认已卸载 lsmod | grep hello

卸载时要确保模块没有被使用(设备没有被打开、没有其他模块依赖等),否则rmmod会失败。

模块退出函数要做好清理工作:释放申请的内存、注销设备、释放中断、取消注册等。如果遗漏清理,再次加载时可能出问题。

回答思路

内核模块开发的完整流程,我一般分六步来讲:写源码、写Makefile、交叉编译、传到板子加载、测试调试、卸载清理。每一步都有需要注意的地方,比如编译必须用对应版本的内核源码树、加载后要用dmesg看日志、卸载前要做好资源清理。

字符设备驱动是最基础的驱动类型,开发步骤是:申请设备号、初始化cdev并关联file_operations、添加cdev到内核、创建设备类和设备节点、实现操作函数。特别要注意错误处理的回退顺序,按注册的逆序清理,哪一步失败都要回退前面已成功的操作。

字符设备驱动开发要点

字符设备驱动是最常见的内核模块类型,也是面试最常考的。基本开发步骤:

  1. 申请设备号:用alloc_chrdev_region动态分配或register_chrdev_region静态注册主设备号和次设备号

  2. 初始化cdev:用cdev_init初始化字符设备结构体,关联file_operations操作集

  3. 添加cdev:用cdev_add把字符设备添加到内核,使其可用

  4. 创建设备类和设备节点:用class_create创建设备类,device_create创建设备节点,让udev/mdev自动在/dev下生成设备文件

  5. 实现操作函数:open、release、read、write、ioctl等file_operations中的函数

面试加分点:说出错误处理和退出清理的顺序

很多人只说注册流程,不提错误处理。实际上驱动开发中"如果中间某一步失败了怎么回退"很重要。正确的做法是:按注册顺序的逆序做清理,每一步失败都要回退之前已成功的操作。比如cdev_add失败了,要先unregister_chrdev_region再返回错误。

常见问题与排查思路

问题一:insmod提示"Invalid module format"

这是最常见的错误,几乎都是版本不匹配导致的。

原因:编译模块的内核版本和目标板上的内核版本不一致,vermagic不匹配。内核会检查模块的版本魔术字符串(包含内核版本、架构、编译器版本等信息),不匹配就拒绝加载。

解决方法:用跟目标板完全相同的内核源码和编译配置来编译模块。可以用modinfo命令查看模块的vermagic,用uname -r查看目标板的内核版本,对比是否一致。

问题二:模块加载后没有任何反应

加载成功了但是printk没输出,可能的原因:

  • printk级别不够:printk有8个日志级别,控制台只显示级别高于console_loglevel的消息。KERN_DEBUG级别太低可能不会显示。用dmesg看完整日志

  • 模块真的加载失败了:insmod没有报错但可能加载过程中出错了。检查dmesg有没有错误信息

  • 初始化函数返回了非0值:module_init的函数返回非0的话,加载会失败,模块不会出现在lsmod中

问题三:rmmod提示"Device or resource busy"


模块正在被使用,不能卸载。常见原因:

  • 设备文件还被某个进程打开着

  • 有其他模块依赖这个模块(用lsmod可以看Used by列)

  • 模块中有未完成的操作或未释放的资源

解决方法:先关闭所有打开的设备,再卸载依赖的模块,最后卸载目标模块。如果是自己写的驱动有bug导致引用计数不对,那就要检查代码。

问题四:加载后内核panic或死机

这是最严重的情况,通常是空指针访问、越界访问、竞争条件等严重bug导致的。

排查方法:

  • 看panic信息,找到出错的函数和地址

  • 用addr2line或gdb根据地址定位代码行

  • 逐步注释代码,缩小问题范围

  • 检查指针是否为NULL、数组是否越界、自旋锁是否死锁

内核态的bug比用户态严重得多,用户态程序崩溃只是进程死掉,内核态崩溃整个系统就挂了。所以内核开发要更加谨慎,做好边界检查和错误处理。

面试官可能的追问

追问一:insmod和modprobe有什么区别?

主要区别有几个方面:

  • 依赖处理:insmod只加载指定的.ko文件,不处理依赖关系,如果模块依赖其他模块而那些模块没加载,加载就会失败。modprobe会自动检查并加载所有依赖的模块,使用更智能

  • 模块位置:insmod需要指定.ko文件的完整路径。modprobe不需要路径,它会在/lib/modules/$(uname -r)/目录下查找模块

  • 配置文件:modprobe会读取modprobe.conf等配置文件,可以设置别名、黑名单、参数等

  • 适用场景:开发调试阶段用insmod更方便,可以直接加载编译出来的.ko;正式系统中用modprobe更稳妥

追问二:内核模块和应用程序有什么不同?

这是一个很基础但容易答不好的问题,可以从几个维度对比:

  • 运行空间:应用程序运行在用户空间,模块运行在内核空间

  • 权限:模块可以直接访问所有硬件和内核资源,权限最高。应用程序只能通过系统调用访问内核

  • 出错后果:应用程序崩溃只是进程退出,影响有限。模块出问题(如空指针)可能导致整个内核panic,系统死机

  • 编译方式:应用程序用普通gcc编译,模块要用内核的Kbuild体系编译

  • 内存管理:应用程序有自己独立的虚拟地址空间。模块在内核地址空间运行,共享内核资源

  • API:应用程序用glibc等标准库,模块用内核提供的函数(如kmalloc、printk等)

追问三:什么是内核污染(Tainted Kernel)?

内核污染是Linux内核的一个机制,用来标记内核的状态是否"纯净"。如果加载了非GPL许可的模块,内核就会被标记为"tainted"。

污染的原因有很多种,常见的:

  • 加载了非GPL许可的专有模块(最常见)

  • 强制加载了版本不匹配的模块

  • 内核出现过oops或panic

  • SMP系统上CPU不支持SMP

污染本身不会影响功能,但社区开发者一般不会为被污染的内核提供技术支持,因为你不知道专有模块里做了什么。所以写驱动时最好加上MODULE_LICENSE("GPL"),这样不仅不会污染内核,还能使用更多GPL-only的内核函数。

失分表达

扣分回答 1

"内核模块就是编译一下然后insmod加载嘛,跟应用程序差不多。"

问题:对内核模块和应用程序的区别理解不深,缺乏内核空间开发的安全意识。

扣分回答 2

问题:只知道版本不匹配这一个原因,对其他可能的问题和排查方法不了解。

扣分回答 3

问题:对驱动的完整生命周期理解不够,忽略了退出清理的重要性。实际项目中exit没写好会导致资源泄漏。

准备动作

  • 在开发板上实际写一个最简单的字符设备驱动,走一遍完整的编译加载卸载流程

  • 故意制造几个常见错误(版本不匹配、init返回错误、忘记释放资源),观察现象并排查

  • 熟悉file_operations结构体中的主要成员函数

  • 了解常用的内核调试手段:dmesg、proc文件系统、sysfs、kgdb、ftrace

核心关键词

内核模块、insmod、rmmod、modprobe、字符设备驱动、cdev、file_operations、module_init、module_exit、Makefile、交叉编译、dmesg、printk、内核污染