驱动开发_1.内核

1. 基础概念

1.1 内核模块是什么?Linux为什么要引入内核模块这一机制?

内核,是一个操作系统的核心,是基于硬件的第一层软件封装,提供操作系统的最基本的功能,是操作系统运行的基础,决定着整个操作系统的性能和稳定性。内核按照体系结构分为两类:微内核和宏内核。
微内核:
在微内核架构中,内核只提供操作系统的核心功能,如进程管理,存储器管理,进程间通信,I/O设备管理等。而其它功能如文件系统、设备驱动等不被包含到内核中,将这些功能模块置于微内核之外,所以针对这些功能模块的修改并不会影响到微内核的核心性能。微内核具有动态扩展性强的优点。如Windows 操作系统、华为的鸿蒙操作系统就属于这类微内核架构。
宏内核:
宏内核架构是将上述包括微内核以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核Linux 操作系统正是采用了宏内核结构。为了解决这一缺点,linux 中引入了内核模块这一机制。

1.2 内核模块引入原因

Linux 是一个跨平台的操作系统,支持众多的设备,在Linux 内核源码中有超过50% 的代码都与设备驱动相关。Linux 为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。因此就需要解决内核庞大臃肿的问题,Linux就引入了内核模块机制,内核模块是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地扩展了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。

1.3 内核模块涉及的操作命令

  1. lsmod
    功能:打印出当前内核中已经安装的模块列表
    英文:list module,将模块列表显示
  2. insmod
    功能:向当前内核中去安装一个模块,用法是insmod xxx.ko
    英文:install module,安装模块
  3. modinfo
    功能:打印出一个内核模块的自带信息
    英文:module information,模块信息
    用法:modinfo xxx.ko,注意要加.ko,也就是说是一个静态的文件形式。
  4. rmmod
    功能:从当前内核中卸载一个已经安装了的模块
    英文:remove module,卸载模块
    用法:rmmod xxx.ko,rmmod xxx都可以
  5. modprobe
    功能:和insmod一样都是载入内核,不过modprobe能够处理module载入的相依问题。
    英文:module probe, 模块探测
    用法:rmmod xxx,注意这里无需输入.ko后缀
  6. depmod
    功能:用于生成内核模块的依赖关系列表。它通过分析/lib/modules/kernel-release目录中的内核模块,创建一个类似“Makefile”的依赖文件,名为modules.dep。这些模块通常来自配置文件中指定的目录或在命令行上提到的目录。
    英文:dependency modules

2. 内核模块加载的过程

2.1 提要

内核模块加载是一个将模块代码(.ko 文件)动态插入运行中的内核,使其成为内核功能一部分的过程。
这个过程涉及用户空间工具和内核空间协作。以下是详细步骤。

2.2 用户空间:触发加载

工具: 用户通常使用insmod(直接加载) 或modprobe(智能加载,处理依赖) 命令。
命令:

sudo insmod /path/to/module.ko   # 直接加载指定模块文件
sudo modprobe module_name        # 按模块名加载,自动处理依赖

2.3 内核空间:系统调用

系统调用入口:insmod/modprobe最终会调用 init_module() 或 finit_module() 系统调用

init_module(void *module_image, unsigned long len, const char *param_values): 将整个模块镜像(.ko 文件内容)复制到内核空间。
finit_module(int fd, const char *param_values, int flags): 更高效,通过文件描述符 fd 让内核直接读取 .ko 文件内容。

2.4 内核空间:核心加载流程

内核收到加载请求后,执行一系列关键操作:

2.4.1 权限检查:
  • 检查调用进程是否具有 CAP_SYS_MODULE 能力(通常需要 root 权限)。
  • 检查内核是否启用了模块加载 (CONFIG_MODULES=y)。
  • (现代内核重要安全机制) 模块签名验证:
    如果内核配置要求模块签名 (CONFIG_MODULE_SIG=y),内核会使用内置的公钥验证模块的加密签名。确保模块来源可信且未被篡改,防止加载恶意模块。
2.4.2 分配内存:

在内核空间分配内存,用于存放即将加载的模块代码、数据和元数据。

2.4.3 解析模块文件 (ELF 格式):

1.内核将 .ko 文件视为一个 ELF (Executable and Linkable Format) 对象。
2.解析 ELF 头部、程序头、节头等信息,识别出:

  • .text节(代码段): 模块的可执行指令。
  • .data/.bss节 (数据段): 模块的已初始化/未初始化全局变量。
  • .rodata节: 只读数据。
  • .modinfo节: 包含模块的元信息(作者、描述、许可证、依赖、参数等)。
  • __versions节: 包含模块所依赖的内核符号及其预期 CRC 校验值(用于版本检查)。
  • .symtab/.strtab节: 符号表和字符串表(用于符号解析)。
  • .rel.*节: 重定位信息(用于地址修正)。
2.4.4 版本和许可证检查:

版本检查 (CONFIG_MODVERSIONS)

  • 内核计算模块引用的每个内核符号的实际 CRC 值。
  • 与模块 __versions 节中存储的预期 CRC 值进行比较。
  • 如果不匹配,说明内核接口可能已改变,加载会失败(防止模块使用不兼容的接口导致崩溃)。

许可证检查 (MODULE_LICENSE):

  • 检查模块声明的许可证(如 GPL, GPL v2, BSD, Proprietary 等)。
  • 如果模块声明为 GPL 或兼容许可证,它可以访问内核中使用 EXPORT_SYMBOL_GPL()导出的专有 GPL 接口。
  • 非 GPL 模块只能访问使用 EXPORT_SYMBOL() 导出的接口。
2.4.5 符号解析与重定位:
  • 解析未定义符号: 遍历模块的符号表,找出所有未定义的符号(即模块需要但自身未定义的函数或变量)。
  • 查找内核符号表: 在内核导出的全局符号表 (/proc/kallsyms) 中查找这些符号的地址。
    内核使用 EXPORT_SYMBOL()EXPORT_SYMBOL_GPL() 导出的符号才能被模块使用。
  • 处理依赖模块的符号: 如果符号是由另一个模块导出的:
    内核会确保该依赖模块已被加载(modprobe在用户空间已处理)。
    从依赖模块的导出符号表中查找地址。
  • 重定位: 根据找到的符号地址和模块内的重定位信息(.rel.*节),修改模块代码和数据段中的引用地址,使其指向正确的内核或依赖模块符号。
2.4.6 执行模块初始化函数:
  • 找到模块源代码中通过 module_init() 宏指定的初始化函数入口点。
  • 调用初始化函数: 内核执行这个函数。
  • 初始化函数的任务
    向内核注册模块提供的功能(如:注册字符设备驱动 cdev_add()、注册文件系统 register_filesystem()、注册网络协议 proto_register()等)。
    申请资源(内存、I/O 端口、IRQ 中断线等)。
    初始化模块内部数据结构。
    执行其他必要的设置工作。
  • 返回值: 初始化函数返回 0 表示成功。返回非零值表示失败,加载过程会终止,模块会被卸载。
2.4.7 模块状态更新:
  • 将新模块添加到内核维护的模块链表 (struct module) 中。
  • 更新 /proc/modulessysfs(/sys/module/) 中的信息,使模块可见。
  • 模块状态标记为 LIVE

2.5 加载完成

  • 用户空间工具 (insmod/modprobe) 收到内核返回的成功信号后退出。
  • 模块的功能(如驱动、文件系统、网络协议等)立即生效,无需重启系统。
  • 可以使用 lsmod命令查看已加载的模块列表及其依赖关系。

2.6 总结

用户触发: insmod 或 modprobe 命令发起请求。
系统调用: 通过 init_module/finit_module 进入内核。
安全检查: 权限、签名验证。
内存分配: 为模块分配内核内存。
ELF 解析: 拆解 .ko 文件结构。
兼容性检查: 版本 (CRC)、许可证。
符号解析与重定位: 链接模块到内核和其他模块(核心链接过程)。
初始化执行: 调用 module_init 函数注册功能、申请资源。
加入系统: 将模块加入内核链表,更新状态信息。

3. 内核模块简单示例

3.1 编写内核模块代码

创建一个名为 hello.c 的文件,并在其中编写以下代码:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello Kernel Module\n");
    return 0;
}

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

/*声明 hello_init是模块的加载函数,即内核加载此模块时调用此函数*/
module_init(hello_init);
/*声明 hello_init是模块的卸载函数,即内核卸载此模块时调用此函数*/
module_exit(hello_exit);
/*模块所遵循的版本约束*/
MODULE_LICENSE("GPL");

这个简单的内核模块在初始化时打印"Hello, Kernel Module!“,在退出时打印"Goodbye, Kernel Module!”。


…未完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值