--- title: 内核模块编程 tags: [Linux内核, 模块, insmod, 驱动开发, 嵌入式] created: 2026-09-16 updated: 2026-09-16 pdf_ref: "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第三章 Linux C编程入门 / 第四十章 字符设备驱动开发" --- # 内核模块编程 > 💡 **关联知识**: [[02-内核模块机制]] | [[03-Linux驱动开发核心/01-字符设备驱动框架]] ## 一、内核模块概述 ### 1.1 什么是内核模块 内核模块(Kernel Module)是可在运行时动态加载到 Linux 内核中的代码片段,无需重新编译整个内核即可扩展功能。模块编译后生成 `.ko`(Kernel Object)文件,通过 `insmod` 或 `modprobe` 命令加载到内核空间执行。 ```mermaid 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 ``` ### 1.2 内核模块 vs 应用程序 | 特性 | 应用程序 | 内核模块 | | -------- | ------------------------- | ----------------------------------------- | | 入口点 | `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) | ### 1.3 内核模块应用场景 - **设备驱动**:字符设备、块设备、网络设备驱动 - **文件系统**:自定义文件系统实现(如 ext4、NFS 模块) - **网络协议栈**:自定义网络过滤规则、VPN 隧道 - **系统调用拦截**:安全审计、行为监控 - **硬件抽象层**:为特定硬件提供统一接口 ### 1.4 模块生命周期 ```mermaid stateDiagram-v2 accTitle: 内核模块生命周期 accDescr: 展示模块从编译到卸载的完整状态转换 [*] --> 编译: make 编译 --> 未加载: 生成 .ko 未加载 --> 初始化: insmod 初始化 --> 运行中: module_init() 返回 0 初始化 --> 失败: module_init() 返回负值 失败 --> [*]: 内核拒绝加载 运行中 --> 清理: rmmod 清理 --> [*]: module_exit() 执行完成 ``` ## 二、内核模块 API ### 2.1 模块注册宏 ```c /* 注册模块初始化函数 —— insmod 时执行 */ module_init(xxx_init); /* 注册模块退出函数 —— rmmod 时执行 */ module_exit(xxx_exit); ``` **关键说明**: - `module_init()` 与 `module_exit()` 必须成对出现 - 若模块编译为 built-in(内建),则 `module_init()` 变为 `early_initcall` 级别 - 初始化函数返回 `0` 表示成功,负值表示失败(如 `-ENOMEM`、`-ENODEV`) ### 2.2 模块信息宏 ```c MODULE_LICENSE("GPL"); /* 必须,否则内核被标记为 tainted */ MODULE_AUTHOR("ALIENTEK"); /* 模块作者 */ MODULE_DESCRIPTION("A simple example"); /* 模块简短描述 */ MODULE_VERSION("1.0"); /* 模块版本号 */ MODULE_ALIAS("my-alias"); /* 模块别名 */ MODULE_INFO(intree, "Y"); /* 标记为内核源码树内模块 */ ``` > ⚠️ **来源说明**:教材第四十章 40.2.4 只讲解了 `MODULE_LICENSE()` 与 `MODULE_AUTHOR()`(LICENSE 必须添加,否则编译报错;作者信息可选)。`MODULE_DESCRIPTION`、`MODULE_VERSION`、`MODULE_ALIAS`、`MODULE_INFO` 为扩展知识。 **许可证选项**: | 许可证 | 说明 | | -------------- | ------------------------------------ | | `GPL` | GNU General Public License,推荐使用 | | `GPL v2` | GNU General Public License v2 | | `Dual MIT/GPL` | 双许可证 | | `Proprietary` | 专有(会标记内核为 tainted) | ### 2.3 printk 内核打印 ```c #include 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 | 调试信息 | 开发调试 | 这 8 个级别定义在 `include/linux/kern_levels.h` 中(教材第四十章),数值越小优先级越高(0 最高、7 最低),每个级别由 `KERN_SOH "\001"` 加数字组成: ```c #define KERN_SOH "\001" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */ #define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */ #define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */ #define KERN_ERR KERN_SOH "3" /* 错误状态,设备驱动中用 KERN_ERR 报告硬件错误 */ #define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */ #define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */ #define KERN_INFO KERN_SOH "6" /* 提示性的信息 */ #define KERN_DEBUG KERN_SOH "7" /* 调试信息 */ ``` **两个默认级别(教材第四十章)**: - 不显式设置级别时,`printk` 使用默认级别 `MESSAGE_LOGLEVEL_DEFAULT`,其值为 **4**; - `include/linux/printk.h` 中的 `CONSOLE_LOGLEVEL_DEFAULT` 默认为 **7**,控制哪些级别的消息能显示在控制台上——只有优先级高于 7 的消息才会显示。默认级别 4 比 7 高,所以直接使用 `printk` 输出的信息可以显示在控制台上。 **printk 注意事项**: - 必须以 `\n` 结尾,否则输出会混乱 - 不同于 `printf`,`printk` 是内核函数,无需链接 libc - 输出到内核环形缓冲区,通过 `dmesg` 查看 - 可通过 `/proc/sys/kernel/printk` 控制控制台日志级别 ### 2.4 模块参数 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第三章、第四十章均未涉及 `module_param` 模块参数机制。 ```c #include #include /* 整型参数 */ 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 可读写 | **加载时传参**: ```bash # 加载时传递参数 sudo insmod mymodule.ko myvalue=100 mystring="world" # 运行时通过 sysfs 修改(需权限 0644) echo 200 > /sys/module/mymodule/parameters/myvalue ``` ### 2.5 init/exit 函数修饰符 ```c /* __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` | 标记数据为退出数据 | 退出阶段使用的全局变量 | > ⚠️ **来源说明**:教材第四十章仅使用 `__init` / `__exit` 修饰驱动入口、出口函数(`__init` 标记的函数执行后释放内存,`__exit` 标记的函数在模块编译为 built-in 时被丢弃);`__initdata` / `__exitdata` 为扩展知识。 ### 2.6 符号导出 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第三章、第四十章均未涉及 `EXPORT_SYMBOL` 符号导出机制。 ```c /* 导出函数供其他模块使用(任何模块可用) */ EXPORT_SYMBOL(my_function); /* 导出 GPL-only 符号(仅 GPL 模块可用) */ EXPORT_SYMBOL_GPL(my_function); ``` **使用导出符号**: ```c /* 在另一个模块中使用 */ extern void my_function(void); static int __init other_init(void) { my_function(); /* 调用导出的函数 */ return 0; } ``` ## 三、内核模块加载与卸载 ### 3.1 insmod 命令 ```bash # 加载指定 .ko 文件 sudo insmod mymodule.ko # 加载并传递参数 sudo insmod mymodule.ko myvalue=100 mystring="hello" ``` **特点**: - 直接加载指定的 `.ko` 文件 - 不处理模块间的依赖关系 - 不检查模块版本兼容性 - 适合调试和开发阶段 ### 3.2 modprobe 命令 ```bash # 加载模块(自动处理依赖) sudo modprobe mymodule # 加载并传递参数 sudo modprobe mymodule myvalue=100 # 卸载模块(自动处理依赖) sudo modprobe -r mymodule ``` **特点**: - 自动处理模块依赖关系(分析 `modules.dep`) - 默认在 `/lib/modules/$(uname -r)/` 目录查找模块,例如内核 4.1.15 就是 `/lib/modules/4.1.15` - 卸载时自动递归卸载未被使用的依赖模块 - 提供依赖性分析、错误检查、错误报告等功能,生产环境推荐使用 > 教材提示:一般自己制作的根文件系统中不会有 `/lib/modules/4.1.15` 这个目录,需要**手动创建**,否则 `modprobe` 无法加载驱动模块。`/lib/modules` 是通用的,只有后面的版本号(如 `4.1.15`、`5.14.31`)需按实际内核版本设置。 ### 3.3 rmmod 命令 ```bash # 卸载模块 sudo rmmod mymodule # 强制卸载(慎用) sudo rmmod -f mymodule ``` ### 3.4 lsmod 命令 ```bash # 列出已加载模块 lsmod # 输出示例: # Module Size Used by # mymodule 16384 0 # hid_logitech_dj 28672 0 # usbhid 65536 0 ``` **字段说明**: - `Module`:模块名称 - `Size`:模块大小(字节) - `Used by`:引用计数(0 表示未被使用) ### 3.5 /proc/modules ```bash # 查看已加载模块(等同于 lsmod) cat /proc/modules # 查看模块参数 cat /sys/module/mymodule/parameters/myvalue ``` ### 3.6 modinfo 命令 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ```bash # 查看模块信息 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) ``` ### 3.7 加载与卸载流程对比 | 操作 | insmod | modprobe | rmmod | modprobe -r | | -------- | ------ | -------- | ------ | ----------- | | 加载模块 | ✅ | ✅ | ❌ | ❌ | | 卸载模块 | ❌ | ❌ | ✅ | ✅ | | 处理依赖 | ❌ | ✅ | ❌ | ✅ | | 指定路径 | 需要 | 不需要 | 不需要 | 不需要 | | 推荐场景 | 调试 | 生产 | 生产 | 生产 | ## 四、内核模块编译 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第三章内容,为扩展知识。教材第四十章 40.4.3 给出了交叉编译驱动模块的 Makefile 模板(见 4.2 节)。 ### 4.1 本机编译 Makefile > ⚠️ **来源说明**:本机编译属于扩展知识,教材只讲解交叉编译。 ```makefile # 内核模块 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 ``` **编译命令**: ```bash # 编译模块 make # 清理编译产物 make clean # 生成的文件 # mymodule.ko - 编译后的内核模块 # mymodule.mod.c - 模块元信息 # mymodule.o - 目标文件 # modules.order - 模块列表 ``` ### 4.2 交叉编译 Makefile 教材第四十章 40.4.3 给出的驱动模块 Makefile 模板(示例代码 40.4.3.1,原文使用 `chrdevbase`): ```makefile KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH := $(shell pwd) obj-m := chrdevbase.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` - `KERNELDIR`:开发板所使用的 Linux 内核源码目录,使用绝对路径,按实际情况填写; - `CURRENT_PATH`:当前路径,通过 `pwd` 命令获取; - `obj-m`:将 `chrdevbase.c` 编译为 `chrdevbase.ko` 模块; - `-C $(KERNELDIR)`:把当前工作目录切换到内核源码目录; - `M=$(CURRENT_PATH)`:指定模块源码目录,`make modules` 会到该目录读取源码并编译为 `.ko`。 若内核尚未在当前 shell 中配置好,可显式传入架构与工具链(扩展写法): ```makefile KERNELDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga CROSS_COMPILE := arm-linux-gnueabihf- ARCH := arm CURRENT_PATH := $(shell pwd) obj-m := mymodule.o all: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` **IMX6ULL 交叉编译示例**: ```bash # 设置环境变量 export CROSS_COMPILE=arm-linux-gnueabihf- export ARCH=arm # 编译模块,成功后生成 chrdevbase.ko make # 拷贝到开发板(NFS 根文件系统可直接拷贝到 rootfs 对应目录) scp mymodule.ko root@192.168.1.200:/home/root/ ``` ### 4.3 编译选项说明 | 选项 | 作用 | | ---------------- | ---------------------- | | `obj-m` | 编译为可加载模块 | | `obj-y` | 编译进内核(built-in) | | `-C $(KERNELDIR)` | 切换到内核源码目录执行 | | `M=$(CURRENT_PATH)` | 指定模块源码目录 | | `ARCH=arm` | 指定目标架构 | | `CROSS_COMPILE=` | 指定交叉编译工具链前缀 | ### 4.4 编译常见问题 | 问题 | 原因 | 解决方法 | | ------------------ | ---------------------- | ---------------------------------- | | `vermagic` 不匹配 | 模块与内核版本不一致 | 使用对应版本的内核源码编译 | | `insmod: ERROR` | 模块签名验证失败 | 使用 `insmod -f`(慎用)或禁用签名 | | `Module not found` | modprobe 执行 `depmod` | 运行 `depmod -a` 更新模块依赖 | | `tainted kernel` | 缺少 MODULE_LICENSE | 添加 `MODULE_LICENSE("GPL")` | ### 4.5 模块加载与卸载实验步骤(教材第四十章 40.4.4) 1. **准备目录**:检查开发板根文件系统中是否存在 `/lib/modules/4.1.15` 目录,没有就自行创建(`modprobe` 会在此目录查找模块)。 2. **拷贝模块**:把 `.ko` 与测试 APP 复制到该目录,例如 ```bash sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ -f ``` 3. **加载模块**: ```bash insmod chrdevbase.ko # 最简单,不处理依赖 # 或 modprobe chrdevbase.ko # 自动处理依赖 ``` 4. **生成依赖文件**:若 `modprobe` 提示无法打开 `modules.dep`,执行 `depmod` 自动生成 `modules.alias`、`modules.symbols` 和 `modules.dep`(有些根文件系统没有 `depmod`,需重新配置 busybox 使能该命令后重新编译)。 ```bash depmod ``` 5. **查看模块与设备**: ```bash lsmod # 查看已加载模块(等价读取 /proc/modules) cat /proc/devices # 查看系统中的字符/块设备及主设备号 ``` 6. **创建设备节点**(教材第四十章使用旧接口 `register_chrdev(200, ...)`,需手动创建设备节点): ```bash mknod /dev/chrdevbase c 200 0 # c=字符设备,200=主设备号,0=次设备号 ls /dev/chrdevbase -l ``` 7. **测试与卸载**: ```bash ./chrdevbaseApp /dev/chrdevbase 1 # 读数据 ./chrdevbaseApp /dev/chrdevbase 2 # 写数据 rmmod chrdevbase.ko # 卸载模块 lsmod # 确认模块已不存在 ``` ## 五、完整源码分析 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。5.1、5.2 的 hello/param 模块教材未给出;5.3 使用了 `cdev` / `class_create` / `device_create` 等新字符设备接口,而教材第四十章 40.4 的 `chrdevbase` 示例使用的是旧接口 `register_chrdev(200, ...)`。 ### 5.1 hello 模块示例 ```c #include #include #include 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_*` | 模块元信息(许可证、作者、描述、版本) | ### 5.2 带参数模块示例 ```c #include #include #include #include /* 定义多种类型的参数 */ 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"); ``` **使用方法**: ```bash # 加载时传递参数 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 ``` ### 5.3 字符设备模块示例 ```c #include #include #include #include #include #include #include #include #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"); ``` ## 六、内核空间与用户空间 ### 6.1 内存映射 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。教材第四十章仅提到“应用程序运行在用户空间,驱动运行于内核空间”,未给出 32 位地址空间划分图。 ```mermaid flowchart TB accTitle: 内核空间与用户空间内存布局 accDescr: 展示 32 位 Linux 系统的内存空间划分 user["用户空间
0x00000000 - 0xBFFFFFFF
应用程序运行区域"] kernel["内核空间
0xC0000000 - 0xFFFFFFFF
内核代码和数据"] 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 ``` ### 6.2 数据传递函数 > 教材第四十章 40.4.1 在 `chrdevbase_read` / `chrdevbase_write` 中讲解了 `copy_to_user` 与 `copy_from_user`:内核空间不能直接操作用户空间的内存,必须借助这两个函数完成数据复制。 ```c #include /* 将内核数据复制到用户空间(教材原型) */ static inline long copy_to_user(void __user *to, const void *from, unsigned long n); /* 将用户数据复制到内核空间 */ static inline long copy_from_user(void *to, const void __user *from, unsigned long n); ``` **返回值说明**: - 返回 `0`:成功 - 返回非 `0`:失败,未复制的字节数 ### 6.3 数据传递示例 ```c /* 内核读取用户数据(用户空间 → 内核空间) */ 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; } ``` ### 6.4 用户空间与内核空间对比 | 特性 | 用户空间 | 内核空间 | | -------- | -------------------- | --------------------- | | 权限级别 | Ring 3 | Ring 0 | | 地址空间 | 虚拟地址(进程隔离) | 统一虚拟地址 | | 内存访问 | 不能直接访问硬件 | 可直接访问硬件 | | 崩溃影响 | 进程终止 | 系统崩溃 | | 内存分配 | malloc/free | kmalloc/kfree | | 打印输出 | printf | printk | | 调试工具 | gdb, strace | printk, ftrace, kdump | ### 6.5 常见数据传递 API | API | 方向 | 说明 | | ------------------ | ----------- | -------------------- | | `copy_to_user()` | 内核 → 用户 | 复制数据到用户空间 | | `copy_from_user()` | 用户 → 内核 | 从用户空间复制数据 | | `put_user()` | 内核 → 用户 | 写入单个值到用户空间 | | `get_user()` | 用户 → 内核 | 从用户空间读取单个值 | > ⚠️ **来源说明**:`put_user()` / `get_user()` 为扩展知识,教材第四十章只讲解了 `copy_to_user()` / `copy_from_user()`。 ## 七、跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 7.1 IMX6ULL vs STM32 vs RK3568 模块差异 | 特性 | 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 | ### 7.2 模块编译差异 ```makefile # IMX6ULL 交叉编译 KERNELDIR := /home/user/linux-imx-rel_imx_4.1.15_2.1.0_ga CROSS_COMPILE := arm-linux-gnueabihf- ARCH := arm # RK3568 交叉编译(64位) KERNELDIR := /home/user/kernel-5.10 CROSS_COMPILE := aarch64-linux-gnu- ARCH := arm64 ``` ## 八、面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 题目 1:什么是内核模块?它的优势是什么? **考察点**:模块概念 **参考答案**: - **定义**:可在运行时加载/卸载的内核代码片段,编译为 `.ko` 文件 - **优势**: - 不需要重新编译整个内核 - 可以动态添加/移除功能 - 便于调试和开发(加载失败不影响系统启动) - 节省内核镜像大小 - **应用**:设备驱动、文件系统、网络协议、安全模块 --- ### 题目 2:insmod 和 modprobe 有什么区别? **考察点**:模块加载机制 **参考答案**: | 特性 | insmod | modprobe | | -------- | ------------ | --------------------------------------- | | 依赖处理 | 不处理 | 自动处理 | | 路径要求 | 需要完整路径 | 自动在 `/lib/modules/$(uname -r)/` 查找 | | 推荐场景 | 调试 | 生产环境 | | 卸载功能 | 仅 `rmmod` | `modprobe -r` 自动递归卸载 | | 依赖信息 | 无 | 读取 `modules.dep` | --- ### 题目 3:为什么内核模块中推荐使用 printk 而不是 printf? **考察点**:内核 API **参考答案**: - 内核没有链接 libc,`printf` 不可用 - `printk` 是内核自带的打印函数,无需额外依赖 - 支持日志级别控制(`KERN_INFO`、`KERN_ERR` 等) - 输出到内核环形缓冲区,可通过 `dmesg` 查看 - 可通过 `/proc/sys/kernel/printk` 动态调整日志级别 --- ### 题目 4:`__init` 和 `__exit` 修饰符的作用是什么? **考察点**:内存优化 **参考答案**: - `__init`:标记初始化函数,执行后释放其内存(节省内核内存) - `__exit`:标记退出函数,模块编译为 built-in 时该函数被丢弃 - `__initdata`:标记初始化阶段使用的数据,初始化完成后释放 - 这些修饰符帮助内核优化内存使用,仅在动态加载模块时保留相关代码 --- ### 题目 5:如何实现内核空间与用户空间的数据传递? **考察点**:数据传递机制 **参考答案**: - **copy_to_user()**:内核数据 → 用户空间(如 `read` 函数) - **copy_from_user()**:用户空间 → 内核数据(如 `write` 函数) - 返回值为 `0` 表示成功,非 `0` 表示失败(需返回 `-EFAULT`) - 不能直接使用指针传递,因为两个空间的虚拟地址不同 - 大数据量场景可使用 `mmap` 将内核内存映射到用户空间 --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第三章 Linux C编程入门、第四十章 字符设备驱动开发 **最后更新**: 2026-09-16