5. 内核模块编程.md 6.7 KB

5 内核模块编程

5.1 完整概念讲解

内核模块 vs 应用程序

特性 应用程序 内核模块
入口点 main() module_init() 指定的初始化函数
运行空间 用户态 内核态
标准库 libc(printf, malloc 等) 无 libc,使用内核 API(printk, kmalloc 等)
内存访问 虚拟地址(MMU 保护) 直接访问物理地址或通过内核映射
错误处理 errno 返回负错误码(-ENOMEM 等)
调试手段 gdb、printf printk、ftrace、kdump
编译方式 gcc 链接 libc 需要内核源码树,使用 obj-m 编译为 .ko
加载方式 直接执行 insmod / modprobe 动态加载

模块生命周期

insmod → module_init() → 模块运行中 → rmmod → module_exit() → 模块卸载
  • module_init():注册模块加载时执行的初始化函数
  • module_exit():注册模块卸载时执行的清理函数
  • 如果模块编译进内核(built-in),则没有动态加载/卸载过程

5.2 核心API/语法

模块元信息宏

MODULE_LICENSE("GPL");           // 必须,否则内核被标记为 "tainted"
MODULE_AUTHOR("Author Name");
MODULE_DESCRIPTION("Short description");
MODULE_VERSION("1.0");

模块参数

static int my_value = 0;
module_param(my_value, int, 0644);  // 参数名, 类型, 权限
MODULE_PARM_DESC(my_value, "An example integer parameter");

权限值含义:

  • 0444:只读,用户可通过 /sys/module/<mod>/parameters/<param> 查看
  • 0644:可读写

printk 日志级别

printk(KERN_INFO "info message\n");
printk(KERN_WARNING "warning message\n");
printk(KERN_ERR "error message\n");
printk(KERN_DEBUG "debug message\n");

常用级别(从高到低):

数值 含义
KERN_EMERG 0 系统崩溃
KERN_ALERT 0 必须立即处理
KERN_CRIT 2 严重错误
KERN_ERR 3 一般错误
KERN_WARNING 4 警告
KERN_NOTICE 5 正常但重要
KERN_INFO 6 信息性
KERN_DEBUG 7 调试信息

符号导出

// 导出函数供其他模块使用
EXPORT_SYMBOL(my_function);
// 导出 GPL-only 符号
EXPORT_SYMBOL_GPL(my_function);

模块加载/卸载命令

insmod hello.ko          # 加载模块
rmmod hello              # 卸载模块(不检查依赖)
lsmod                    # 列出已加载模块
modprobe hello           # 加载模块(自动处理依赖)
modprobe -r hello        # 卸载模块(自动处理依赖)
dmesg                    # 查看内核日志(printk 输出)

5.3 代码示例(完整可编译,附gcc命令)

hello.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int hello_value = 0;
module_param(hello_value, int, 0644);
MODULE_PARM_DESC(hello_value, "A demonstration parameter");

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel world! value=%d\n", hello_value);
    return 0;  // 返回0表示成功
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("A simple hello world kernel module");
MODULE_VERSION("1.0");

Makefile

# 内核模块 Makefile(放在与 .c 文件同一目录)

obj-m := hello.o

# 指定内核源码路径(根据实际路径修改)
KDIR := /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

编译与运行

# 编译
make

# 加载模块
sudo insmod hello.ko value=42

# 查看输出
dmesg | tail
# [xxxxx] Hello, kernel world! value=42

# 查看模块参数
cat /sys/module/hello/parameters/hello_value
# 42

# 卸载模块
sudo rmmod hello

# 查看卸载输出
dmesg | tail
# [xxxxx] Goodbye, kernel world!

# 清理编译产物
make clean

5.4 注意事项与易错点

  1. MODULE_LICENSE 必须设置:否则内核标记为 tainted,无法使用 EXPORT_SYMBOL_GPL 导出的符号
  2. init/exit 函数加 __init / __exit:告诉编译器这些函数只在初始化/退出时使用,可以释放其内存
  3. 模块返回值module_init 函数返回 0 表示成功,负值表示失败(如 -ENOMEM
  4. printk 必须加 \n:内核 printk 不会自动换行,不加 \n 会导致 dmesg 输出混乱
  5. 内核代码不能用 returnmodule_exitmodule_exit 函数无返回值(void
  6. insmod vs modprobeinsmod 不处理模块依赖,modprobe 会自动加载依赖模块
  7. 模块参数在加载后修改:通过 sysfs 修改的参数不会影响已运行逻辑(需在代码中读取)

5.5 面试要点(3-5个Q&A)

Q1:内核模块和普通应用程序的入口函数有什么区别? A:应用程序从 main() 开始执行,内核模块使用 module_init() 宏注册初始化函数,模块被加载(insmod/modprobe)时才执行该函数。卸载时执行 module_exit() 注册的清理函数。

Q2:为什么内核模块中推荐使用 printk 而不是 printf A:内核没有链接 libc,printf 不可用。printk 是内核自带的打印函数,支持日志级别控制(KERN_INFO、KERN_ERR 等),输出到内核环形缓冲区,可通过 dmesg 查看。

Q3:insmodmodprobe 有什么区别? A:insmod 直接加载指定的 .ko 文件,不处理模块间的依赖关系。modprobe 会根据 /lib/modules/$(uname -r)/modules.dep 自动加载被依赖的模块,卸载时也会递归卸载未被其他模块使用的依赖。

Q4:模块参数的权限值 0644 是什么意思? A:0644 是八进制权限,表示文件所有者可读写(6),组用户和其他用户只读(4,4)。对应到 sysfs 中,用户可通过 /sys/module/<mod>/parameters/<param> 读取和写入该参数。

Q5:EXPORT_SYMBOLEXPORT_SYMBOL_GPL 有什么区别? A:EXPORT_SYMBOL 导出的符号可被任何模块使用(包括非 GPL 模块)。EXPORT_SYMBOL_GPL 导出的符号只能被声明为 GPL 兼容许可证的模块使用。内核推荐使用 EXPORT_SYMBOL_GPL