| 特性 | 应用程序 | 内核模块 |
|---|---|---|
| 入口点 | 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():注册模块卸载时执行的清理函数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(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 输出)
#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(放在与 .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
EXPORT_SYMBOL_GPL 导出的符号__init / __exit:告诉编译器这些函数只在初始化/退出时使用,可以释放其内存module_init 函数返回 0 表示成功,负值表示失败(如 -ENOMEM)\n:内核 printk 不会自动换行,不加 \n 会导致 dmesg 输出混乱return 在 module_exit 中:module_exit 函数无返回值(void)insmod 不处理模块依赖,modprobe 会自动加载依赖模块Q1:内核模块和普通应用程序的入口函数有什么区别?
A:应用程序从 main() 开始执行,内核模块使用 module_init() 宏注册初始化函数,模块被加载(insmod/modprobe)时才执行该函数。卸载时执行 module_exit() 注册的清理函数。
Q2:为什么内核模块中推荐使用 printk 而不是 printf?
A:内核没有链接 libc,printf 不可用。printk 是内核自带的打印函数,支持日志级别控制(KERN_INFO、KERN_ERR 等),输出到内核环形缓冲区,可通过 dmesg 查看。
Q3:insmod 和 modprobe 有什么区别?
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_SYMBOL 和 EXPORT_SYMBOL_GPL 有什么区别?
A:EXPORT_SYMBOL 导出的符号可被任何模块使用(包括非 GPL 模块)。EXPORT_SYMBOL_GPL 导出的符号只能被声明为 GPL 兼容许可证的模块使用。内核推荐使用 EXPORT_SYMBOL_GPL。