title: 内核模块编程 tags: [Linux内核, 模块, insmod, 驱动开发, 嵌入式] created: 2026-09-16 updated: 2026-09-16
💡 关联知识: [[02-内核模块机制]] | [[03-Linux驱动开发核心/01-字符设备驱动框架]]
内核模块(Kernel Module)是可在运行时动态加载到 Linux 内核中的代码片段,无需重新编译整个内核即可扩展功能。模块编译后生成 .ko(Kernel Object)文件,通过 insmod 或 modprobe 命令加载到内核空间执行。
flowchart LR
accTitle: 内核模块加载流程
accDescr: 展示从源码到模块加载到内核的完整流程
src[".c 源码"] -->|编译| ko[".ko 文件"]
ko -->|insmod/modprobe| kernel["内核空间"]
kernel -->|rmmod| unload["卸载模块"]
classDef source fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef kernel fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
classDef action fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
class src source
class kernel kernel
class ko,unload action
| 特性 | 应用程序 | 内核模块 |
|---|---|---|
| 入口点 | main() |
module_init() 指定的初始化函数 |
| 运行空间 | 用户态(Ring 3) | 内核态(Ring 0) |
| 标准库 | libc(printf, malloc 等) | 无 libc,使用内核 API(printk, kmalloc) |
| 内存访问 | 虚拟地址(MMU 保护) | 直接访问物理地址或通过内核映射 |
| 错误处理 | errno |
返回负错误码(-ENOMEM 等) |
| 调试手段 | gdb、printf | printk、ftrace、kdump |
| 编译方式 | gcc 链接 libc | 需要内核源码树,使用 obj-m 编译为 .ko |
| 加载方式 | 直接执行 | insmod / modprobe 动态加载 |
| 崩溃影响 | 进程崩溃,系统继续运行 | 系统崩溃(kernel panic) |
stateDiagram-v2
accTitle: 内核模块生命周期
accDescr: 展示模块从编译到卸载的完整状态转换
[*] --> 编译: make
编译 --> 未加载: 生成 .ko
未加载 --> 初始化: insmod
初始化 --> 运行中: module_init() 返回 0
初始化 --> 失败: module_init() 返回负值
失败 --> [*]: 内核拒绝加载
运行中 --> 清理: rmmod
清理 --> [*]: module_exit() 执行完成
/* 注册模块初始化函数 —— insmod 时执行 */
module_init(xxx_init);
/* 注册模块退出函数 —— rmmod 时执行 */
module_exit(xxx_exit);
关键说明:
module_init() 与 module_exit() 必须成对出现module_init() 变为 early_initcall 级别0 表示成功,负值表示失败(如 -ENOMEM、-ENODEV)MODULE_LICENSE("GPL"); /* 必须,否则内核被标记为 tainted */
MODULE_AUTHOR("ALIENTEK"); /* 模块作者 */
MODULE_DESCRIPTION("A simple example"); /* 模块简短描述 */
MODULE_VERSION("1.0"); /* 模块版本号 */
MODULE_ALIAS("my-alias"); /* 模块别名 */
MODULE_INFO(intree, "Y"); /* 标记为内核源码树内模块 */
许可证选项:
| 许可证 | 说明 |
|---|---|
GPL |
GNU General Public License,推荐使用 |
GPL v2 |
GNU General Public License v2 |
Dual MIT/GPL |
双许可证 |
Proprietary |
专有(会标记内核为 tainted) |
#include <linux/kernel.h>
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 |
1 | 必须立即处理 | 严重安全问题 |
KERN_CRIT |
2 | 严重错误 | 硬件故障 |
KERN_ERR |
3 | 一般错误 | 驱动初始化失败 |
KERN_WARNING |
4 | 警告 | 非致命问题 |
KERN_NOTICE |
5 | 正常但重要 | 状态变化 |
KERN_INFO |
6 | 信息性 | 模块加载成功 |
KERN_DEBUG |
7 | 调试信息 | 开发调试 |
printk 注意事项:
\n 结尾,否则输出会混乱printf,printk 是内核函数,无需链接 libcdmesg 查看/proc/sys/kernel/printk 控制控制台日志级别#include <linux/module.h>
#include <linux/moduleparam.h>
/* 整型参数 */
static int myvalue = 0;
module_param(myvalue, int, 0644);
MODULE_PARM_DESC(myvalue, "An integer parameter");
/* 字符串参数 */
static char *mystring = "hello";
module_param(mystring, charp, 0644);
MODULE_PARM_DESC(mystring, "A string parameter");
/* 数组参数 */
static int myarray[3] = {1, 2, 3};
module_param_array(myarray, int, NULL, 0644);
MODULE_PARM_DESC(myarray, "An integer array");
参数权限值:
| 权限 | 含义 | sysfs 行为 |
|---|---|---|
0444 |
只读 | 用户可读,不可写 |
0644 |
可读写 | 用户可读写 |
0600 |
所有者读写 | 仅 root 可读写 |
加载时传参:
# 加载时传递参数
sudo insmod mymodule.ko myvalue=100 mystring="world"
# 运行时通过 sysfs 修改(需权限 0644)
echo 200 > /sys/module/mymodule/parameters/myvalue
/* __init 标记:初始化函数,执行后释放内存 */
static int __init my_init(void)
{
/* 初始化代码 */
return 0;
}
/* __exit 标记:退出函数,模块编译为 built-in 时被丢弃 */
static void __exit my_exit(void)
{
/* 清理代码 */
}
修饰符说明:
| 修饰符 | 作用 | 使用场景 |
|---|---|---|
__init |
标记函数为初始化代码 | module_init 注册的函数 |
__exit |
标记函数为退出代码 | module_exit 注册的函数 |
__initdata |
标记数据为初始化数据 | 初始化阶段使用的全局变量 |
__exitdata |
标记数据为退出数据 | 退出阶段使用的全局变量 |
/* 导出函数供其他模块使用(任何模块可用) */
EXPORT_SYMBOL(my_function);
/* 导出 GPL-only 符号(仅 GPL 模块可用) */
EXPORT_SYMBOL_GPL(my_function);
使用导出符号:
/* 在另一个模块中使用 */
extern void my_function(void);
static int __init other_init(void)
{
my_function(); /* 调用导出的函数 */
return 0;
}
# 加载指定 .ko 文件
sudo insmod mymodule.ko
# 加载并传递参数
sudo insmod mymodule.ko myvalue=100 mystring="hello"
特点:
.ko 文件# 加载模块(自动处理依赖)
sudo modprobe mymodule
# 加载并传递参数
sudo modprobe mymodule myvalue=100
# 卸载模块(自动处理依赖)
sudo modprobe -r mymodule
特点:
/lib/modules/$(uname -r)/ 目录查找模块# 卸载模块
sudo rmmod mymodule
# 强制卸载(慎用)
sudo rmmod -f mymodule
# 列出已加载模块
lsmod
# 输出示例:
# Module Size Used by
# mymodule 16384 0
# hid_logitech_dj 28672 0
# usbhid 65536 0
字段说明:
Module:模块名称Size:模块大小(字节)Used by:引用计数(0 表示未被使用)# 查看已加载模块(等同于 lsmod)
cat /proc/modules
# 查看模块参数
cat /sys/module/mymodule/parameters/myvalue
# 查看模块信息
modinfo mymodule.ko
# 输出示例:
# filename: /home/user/mymodule.ko
# license: GPL
# description: A simple example module
# author: ALIENTEK
# vermagic: 4.1.15 SMP preempt mod_unload ARMv7
# parm: myvalue:An integer parameter (int)
# parm: mystring:A string parameter (charp)
| 操作 | insmod | modprobe | rmmod | modprobe -r |
|---|---|---|---|---|
| 加载模块 | ✅ | ✅ | ❌ | ❌ |
| 卸载模块 | ❌ | ❌ | ✅ | ✅ |
| 处理依赖 | ❌ | ✅ | ❌ | ✅ |
| 指定路径 | 需要 | 不需要 | 不需要 | 不需要 |
| 推荐场景 | 调试 | 生产 | 生产 | 生产 |
# 内核模块 Makefile(放在与 .c 文件同一目录)
obj-m := mymodule.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
# 清理编译产物
make clean
# 生成的文件
# mymodule.ko - 编译后的内核模块
# mymodule.mod.c - 模块元信息
# mymodule.o - 目标文件
# modules.order - 模块列表
# 交叉编译 Makefile
obj-m := mymodule.o
# 内核源码路径(根据实际路径修改)
KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 交叉编译工具链
CROSS_COMPILE := arm-linux-gnueabihf-
# 目标架构
ARCH := arm
# 当前目录
PWD := $(shell pwd)
all:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules
clean:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean
IMX6ULL 交叉编译示例:
# 设置环境变量
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
# 编译模块
make
# 拷贝到开发板
scp mymodule.ko root@192.168.1.200:/home/root/
| 选项 | 作用 |
|---|---|
obj-m |
编译为可加载模块 |
obj-y |
编译进内核(built-in) |
-C $(KDIR) |
切换到内核源码目录执行 |
M=$(PWD) |
指定模块源码目录 |
ARCH=arm |
指定目标架构 |
CROSS_COMPILE= |
指定交叉编译工具链前缀 |
| 问题 | 原因 | 解决方法 |
|---|---|---|
vermagic 不匹配 |
模块与内核版本不一致 | 使用对应版本的内核源码编译 |
insmod: ERROR |
模块签名验证失败 | 使用 insmod -f(慎用)或禁用签名 |
Module not found |
modprobe 执行 depmod |
运行 depmod -a 更新模块依赖 |
tainted kernel |
缺少 MODULE_LICENSE | 添加 MODULE_LICENSE("GPL") |
#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("ALIENTEK");
MODULE_DESCRIPTION("A simple hello world kernel module");
MODULE_VERSION("1.0");
逐行解释:
| 行号 | 代码 | 说明 |
|---|---|---|
| 1-3 | #include |
包含内核头文件 |
| 5-6 | static int hello_value |
定义模块参数变量 |
| 7 | module_param(...) |
注册模块参数,权限 0644 |
| 8 | MODULE_PARM_DESC(...) |
描述参数用途 |
| 10-13 | hello_init() |
模块初始化函数,加载时执行 |
| 14-17 | hello_exit() |
模块退出函数,卸载时执行 |
| 19-20 | module_init/exit |
注册初始化和退出函数 |
| 22-25 | MODULE_* |
模块元信息(许可证、作者、描述、版本) |
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
/* 定义多种类型的参数 */
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of iterations");
static char *msg = "hello";
module_param(msg, charp, 0644);
MODULE_PARM_DESC(msg, "Message to display");
static int flags[3] = {0, 0, 0};
module_param_array(flags, int, NULL, 0644);
MODULE_PARM_DESC(flags, "An array of flags");
static int __init param_init(void)
{
int i;
printk(KERN_INFO "Module loaded with:\n");
printk(KERN_INFO " count = %d\n", count);
printk(KERN_INFO " msg = %s\n", msg);
for (i = 0; i < 3; i++)
printk(KERN_INFO " flags[%d] = %d\n", i, flags[i]);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_INFO "Module unloaded\n");
}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
使用方法:
# 加载时传递参数
sudo insmod param.ko count=5 msg="world" flags=1,2,3
# 查看输出
dmesg | tail -5
# [xxxxx] Module loaded with:
# [xxxxx] count = 5
# [xxxxx] msg = world
# [xxxxx] flags[0] = 1
# [xxxxx] flags[1] = 2
# [xxxxx] flags[2] = 3
# 运行时修改参数
echo 10 > /sys/module/param/parameters/count
cat /sys/module/param/parameters/msg
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "chrtest"
#define DEVICE_COUNT 1
/* 字符设备结构体 */
static struct cdev chrtest_cdev;
static dev_t devno;
static int major = 0; /* 0 表示动态分配 */
static int minor = 0;
static struct class *chrtest_class;
static struct device *chrtest_device;
/* 设备数据缓冲区 */
static char readbuf[100];
static char writebuf[100];
static int data_len = 0;
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrtest: device opened\n");
return 0;
}
/* 读取设备 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
if (cnt > data_len)
cnt = data_len;
if (copy_to_user(buf, readbuf, cnt)) {
ret = -EFAULT;
goto out;
}
printk(KERN_INFO "chrtest: read %zu bytes\n", cnt);
ret = cnt;
out:
return ret;
}
/* 写入设备 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
if (cnt > sizeof(writebuf))
cnt = sizeof(writebuf);
if (copy_from_user(writebuf, buf, cnt)) {
ret = -EFAULT;
goto out;
}
memcpy(readbuf, writebuf, cnt);
data_len = cnt;
printk(KERN_INFO "chrtest: wrote %zu bytes\n", cnt);
ret = cnt;
out:
return ret;
}
/* 关闭设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrtest: device closed\n");
return 0;
}
/* 文件操作结构体 */
static struct file_operations chrtest_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
/* 模块初始化 */
static int __init chrtest_init(void)
{
int ret = 0;
/* 动态分配设备号 */
if (major) {
devno = MKDEV(major, minor);
ret = register_chrdev_region(devno, DEVICE_COUNT, DEVICE_NAME);
} else {
ret = alloc_chrdev_region(&devno, 0, DEVICE_COUNT, DEVICE_NAME);
major = MAJOR(devno);
minor = MINOR(devno);
}
if (ret < 0) {
printk(KERN_ERR "chrtest: failed to get device number\n");
goto fail_devno;
}
/* 初始化 cdev */
cdev_init(&chrtest_cdev, &chrtest_fops);
ret = cdev_add(&chrtest_cdev, devno, DEVICE_COUNT);
if (ret < 0) {
printk(KERN_ERR "chrtest: failed to add cdev\n");
goto fail_cdev;
}
/* 创建设备类 */
chrtest_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(chrtest_class)) {
ret = PTR_ERR(chrtest_class);
printk(KERN_ERR "chrtest: failed to create class\n");
goto fail_class;
}
/* 创建设备节点 */
chrtest_device = device_create(chrtest_class, NULL, devno,
NULL, DEVICE_NAME);
if (IS_ERR(chrtest_device)) {
ret = PTR_ERR(chrtest_device);
printk(KERN_ERR "chrtest: failed to create device\n");
goto fail_device;
}
printk(KERN_INFO "chrtest: module loaded, major=%d minor=%d\n",
major, minor);
return 0;
fail_device:
class_destroy(chrtest_class);
fail_class:
cdev_del(&chrtest_cdev);
fail_cdev:
unregister_chrdev_region(devno, DEVICE_COUNT);
fail_devno:
return ret;
}
/* 模块退出 */
static void __exit chrtest_exit(void)
{
device_destroy(chrtest_class, devno);
class_destroy(chrtest_class);
cdev_del(&chrtest_cdev);
unregister_chrdev_region(devno, DEVICE_COUNT);
printk(KERN_INFO "chrtest: module unloaded\n");
}
module_init(chrtest_init);
module_exit(chrtest_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
flowchart TB
accTitle: 内核空间与用户空间内存布局
accDescr: 展示 32 位 Linux 系统的内存空间划分
user["用户空间<br/>0x00000000 - 0xBFFFFFFF<br/>应用程序运行区域"]
kernel["内核空间<br/>0xC0000000 - 0xFFFFFFFF<br/>内核代码和数据"]
user --- kernel
classDef user fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef kernel fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
class user user
class kernel kernel
#include <linux/uaccess.h>
/* 将内核数据复制到用户空间 */
unsigned long copy_to_user(void __user *to,
const void *from,
unsigned long n);
/* 将用户数据复制到内核空间 */
unsigned long copy_from_user(void *to,
const void __user *from,
unsigned long n);
返回值说明:
0:成功0:失败,未复制的字节数/* 内核读取用户数据(用户空间 → 内核空间) */
static ssize_t my_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
if (copy_to_user(buf, kernel_buf, cnt))
return -EFAULT;
return cnt;
}
/* 内核写入用户数据(内核空间 → 用户空间) */
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
if (copy_from_user(kernel_buf, buf, cnt))
return -EFAULT;
return cnt;
}
| 特性 | 用户空间 | 内核空间 |
|---|---|---|
| 权限级别 | Ring 3 | Ring 0 |
| 地址空间 | 虚拟地址(进程隔离) | 统一虚拟地址 |
| 内存访问 | 不能直接访问硬件 | 可直接访问硬件 |
| 崩溃影响 | 进程终止 | 系统崩溃 |
| 内存分配 | malloc/free | kmalloc/kfree |
| 打印输出 | printf | printk |
| 调试工具 | gdb, strace | printk, ftrace, kdump |
| API | 方向 | 说明 |
|---|---|---|
copy_to_user() |
内核 → 用户 | 复制数据到用户空间 |
copy_from_user() |
用户 → 内核 | 从用户空间复制数据 |
put_user() |
内核 → 用户 | 写入单个值到用户空间 |
get_user() |
用户 → 内核 | 从用户空间读取单个值 |
| 特性 | IMX6ULL | STM32MP1 | RK3568 |
|---|---|---|---|
| 架构 | ARM Cortex-A7 | ARM Cortex-A7/A9 | ARM Cortex-A55 |
| 内核版本 | 4.1.15 | 5.10 | 5.10 |
| 交叉编译器 | arm-linux-gnueabihf- | arm-linux-gnueabihf- | aarch64-linux-gnu- |
| 内核源码 | NXP 官方 | ST 官方 | Rockchip 官方 |
| 模块加载 | 支持 | 支持 | 支持 |
| 设备树 | 支持 | 支持 | 支持 |
| 开发工具 | VSCode + GCC | STM32CubeIDE | VSCode + GCC |
# IMX6ULL 交叉编译
KDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
# RK3568 交叉编译(64位)
KDIR := /home/user/kernel-5.10
CROSS_COMPILE := aarch64-linux-gnu-
ARCH := arm64
考察点:模块概念
参考答案:
.ko 文件考察点:模块加载机制
参考答案:
| 特性 | insmod | modprobe |
|---|---|---|
| 依赖处理 | 不处理 | 自动处理 |
| 路径要求 | 需要完整路径 | 自动在 /lib/modules/$(uname -r)/ 查找 |
| 推荐场景 | 调试 | 生产环境 |
| 卸载功能 | 仅 rmmod |
modprobe -r 自动递归卸载 |
| 依赖信息 | 无 | 读取 modules.dep |
考察点:内核 API
参考答案:
printf 不可用printk 是内核自带的打印函数,无需额外依赖KERN_INFO、KERN_ERR 等)dmesg 查看/proc/sys/kernel/printk 动态调整日志级别__init 和 __exit 修饰符的作用是什么?考察点:内存优化
参考答案:
__init:标记初始化函数,执行后释放其内存(节省内核内存)__exit:标记退出函数,模块编译为 built-in 时该函数被丢弃__initdata:标记初始化阶段使用的数据,初始化完成后释放考察点:数据传递机制
参考答案:
read 函数)write 函数)0 表示成功,非 0 表示失败(需返回 -EFAULT)mmap 将内核内存映射到用户空间最后更新: 2026-09-16