
本文概述
Linux内核模块(LKM)是可以动态加载和卸载的内核代码,无需重新编译内核即可扩展内核功能。基本结构包括模块初始化函数(module_init)、模块退出函数(module_exit)、模块许可证声明(MODULE_LICENSE)。加载方式有insmod(简单加载)和modprobe(自动解决依赖),编译需要对应内核源码树,生成.ko文件。
核心要点速览
内核模块可以动态加载卸载,无需重新编译内核,驱动开发常用形式
基本结构:module_init(入口)、module_exit(出口)、MODULE_LICENSE(许可证)
insmod直接加载指定.ko文件,modprobe自动加载依赖模块
模块参数用module_param声明,可以在加载时传入参数
编译需要内核源码树和正确的交叉编译工具链,生成.ko文件
一、面试题
请说明Linux内核模块的基本结构,以及你是怎么编写和加载一个内核模块的。
二、面试官考察点
1. 内核模块基础概念
考察候选人对内核模块作用、基本结构、与用户程序区别的理解程度,以及常用操作命令的掌握。
2. 实际开发经验
通过具体项目判断候选人是否有实际的内核模块开发经验,是否了解编译、加载、调试的完整流程。
3. 内核编程思维
是否理解内核态和用户态的区别,是否知道内核编程的注意事项(如不能使用C库、不能随意休眠等)。
三、回答思路
1. 什么是内核模块
Linux内核模块(Loadable Kernel Module,简称LKM)是一种可以在运行时动态加载到内核中的目标代码。加载后成为内核的一部分,可以直接调用内核函数和访问硬件资源。不需要时可以卸载,释放内存。
2. 内核模块的基本结构
一个最简单的内核模块至少包含以下几个部分:
头文件:#include
(模块基本定义)、#include (内核常用函数)、#include (初始化和退出宏定义) 初始化函数:用module_init()宏注册,模块加载时执行一次,返回0表示成功,负数表示失败
退出函数:用module_exit()宏注册,模块卸载时执行一次,负责清理资源
许可证声明:MODULE_LICENSE("GPL"),声明模块遵循的许可证,影响能否使用GPL-only的内核函数
作者/描述/版本(可选):MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION
3. 常用模块操作命令
| 命令 | 功能 | 说明 |
|---|---|---|
| insmod | 加载模块 | 直接加载指定.ko文件,不处理依赖关系 |
| rmmod | 卸载模块 | 卸载指定模块,模块被使用时不能卸载 |
| modprobe | 智能加载/卸载 | 自动处理依赖关系,从模块目录查找 |
| lsmod | 列出已加载模块 | 显示模块名、大小、被引用次数和依赖模块 |
| modinfo | 查看模块信息 | 显示模块的作者、描述、版本、依赖等 |
| depmod | 生成模块依赖 | 扫描模块目录生成modules.dep文件 |
4. 模块参数
内核模块可以接受加载时传入的参数,使用module_param宏声明:
格式:module_param(变量名, 类型, 权限)
常用类型:byte、short、ushort、int、uint、long、ulong、charp(字符串指针)、bool
权限:0表示不在sysfs中显示,S_IRUGO表示所有用户可读,S_IWUSR表示root可写
用法示例:insmod mymodule.ko irq_num=10 debug_enable=1
模块参数数组:module_param_array(name, type, nump, perm),nump指向实际传入的元素数量
5. 模块编译方法
编译环境:需要安装对应内核版本的源码树或头文件,以及交叉编译工具链
Makefile写法:使用内核的Kbuild系统,obj-m += mymodule.o 指定要编译的模块
编译命令:make -C /path/to/kernel M=$PWD modules
生成文件:.ko文件是最终的可加载模块,还有.ko.cmd、.mod.c、.o等中间文件
版本校验:modinfo可以查看模块的vermagic,和uname -r对比确认版本匹配
6. 内核模块编程注意事项
不能使用C标准库
内核有自己的函数库,如printk代替printf,kmalloc代替malloc,copy_to_user/copy_from_user代替memcpy用于用户空间数据拷贝
注意休眠上下文
中断上下文(中断处理函数、tasklet等)中不能休眠,也不能调用可能休眠的函数(如kmalloc with GFP_KERNEL),要用原子操作和GFP_ATOMIC标志
栈空间有限
内核栈只有4KB或8KB(取决于架构和配置),不能在函数中定义大数组,大内存分配要用kmalloc
出错后果严重
内核模块崩溃会导致整个系统panic(内核恐慌),不像用户程序只是段错误。开发中要做好边界检查和错误处理
模块卸载要清理干净
退出函数必须释放所有申请的资源:释放ioremap、注销设备号、销毁设备类、释放中断、停止定时器等,否则卸载后再加载可能出问题
四、追问(4个)
追问1:printk和printf有什么区别?
printk是内核提供的打印函数,printf是C标准库的用户态函数。区别主要有:printk运行在内核态,printf在用户态;printk有日志级别(8个级别,从KERN_EMERG到KERN_DEBUG),可以通过dmesg查看输出,printf输出到stdout;printk对浮点数支持不好(内核一般不用浮点);printk可以在中断上下文调用,printf不能。日志级别低于console_loglevel的消息才会打印到控制台。
追问2:模块加载时提示"disagrees about version of symbol"是什么意思?
这是因为编译模块用的内核源码树和运行的内核版本不一致,或者内核配置不同。每个内核符号(函数、变量)都有版本校验信息(crc值),模块加载时会校验这些值是否匹配。解决方法是用目标板运行的内核对应的源码树重新编译模块。如果只是小版本差异且确定不会有问题,也可以用modprobe --force强制加载,但不推荐。
追问3:内核模块怎么传递参数?有什么限制?
用module_param宏声明参数,加载时通过"参数名=值"的方式传入。限制有:参数类型只能是内核定义的那几种(int、long、charp等),不能传结构体;数组参数长度有限制;charp类型传入的字符串在内核中被拷贝一份,不是用户空间的指针;权限决定了参数在sysfs中是否可见、是否可写。加载后可以通过/sys/module/模块名/parameters/目录查看和修改参数。
追问4:什么是模块的依赖关系?modprobe怎么处理?
一个模块可能调用另一个模块导出的符号(EXPORT_SYMBOL),这就形成了依赖关系。depmod命令会扫描所有模块,分析符号引用关系,生成modules.dep文件。modprobe加载模块时,会先读取modules.dep,找出所有依赖的模块,按依赖顺序依次加载。卸载时则反向卸载。比如A依赖B,加载时先加载B再加载A,卸载时先卸载A再卸载B。
五、失分表达
"内核模块就是用户程序编译成.ko就行"——不理解内核态和用户态的本质区别
"随便找个内核源码编译就能加载"——忽略了内核版本匹配问题(vermagic)
"内核里可以用printf和malloc"——混淆用户态和内核态的函数库
"insmod和modprobe一样,随便用哪个都行"——不理解依赖处理的区别
六、准备动作
能写出一个最简单的Hello World内核模块的完整代码
掌握insmod、rmmod、modprobe、lsmod、modinfo等命令的用法和区别
理解内核态和用户态编程的区别和注意事项
知道内核模块的编译方法和Makefile写法
了解模块参数的声明和使用方法
七、核心关键词
内核模块 LKM module_init insmod modprobe 内核编译 内核态 printk

扫码关注






























