1.1 Linux 驱动开发概述
Linux 驱动是操作系统内核与硬件设备之间的桥梁。它负责将上层应用程序的通用 I/O 请求转换为硬件设备能够理解的具体操作指令。本章作为整个课程的开篇,将系统梳理内核模块的基础结构、字符设备驱动的标准框架,以及驱动与应用程序之间的典型交互方式,为后续实现 ioctl 接口与并发控制打下坚实的理论基础。
1.1.1 内核模块基础
Linux 内核采用模块化设计,允许在运行时动态加载或卸载功能代码,这种可扩展的代码单元称为内核模块。模块机制使得开发者无需重新编译整个内核即可添加驱动支持,极大地提高了开发效率和系统灵活性。
模块的入口与出口
每个内核模块都必须包含两个核心宏:
| 宏定义 | 作用 | 触发时机 |
|---|---|---|
module_init() |
指定模块加载时执行的初始化函数 | insmod 命令执行时 |
module_exit() |
指定模块卸载时执行的清理函数 | rmmod 命令执行时 |
一个最简的内核模块代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module Loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module Unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("A simple hello world kernel module");
__init 和 __exit 标记用于优化内存布局。初始化函数在模块加载完成后会被内核回收,以节省内存空间。
模块的编译与加载
内核模块的编译依赖内核源码树中的 Makefile 框架,通常使用以下模板:
obj-m := hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
编译完成后,通过 insmod hello.ko 加载模块,使用 lsmod 查看已加载模块列表,使用 rmmod hello 卸载模块。
模块信息与许可证
模块描述宏不仅用于文档记录,还影响内核的符号导出策略:
- MODULE_LICENSE:声明许可证类型,非 GPL 许可证可能导致某些内核符号不可用。
- MODULE_AUTHOR:标识模块作者。
- MODULE_DESCRIPTION:简要描述模块功能。
- MODULE_VERSION:版本号,便于维护。
1.1.2 字符设备驱动框架
在 Linux 设备驱动模型中,字符设备是最基础、最常用的设备类型。它以字节流的形式进行数据传输,典型的字符设备包括串口、键盘、鼠标、GPIO 等。字符设备驱动的核心是向内核注册一个 file_operations 结构体,该结构体定义了设备支持的所有操作函数指针。
核心数据结构
| 结构体/类型 | 作用 | 关键字段 |
|---|---|---|
struct cdev |
内核中表示字符设备的对象 | ops、owner、dev |
struct file_operations |
定义设备支持的文件操作函数 | open、release、read、write、unlocked_ioctl |
dev_t |
设备号,包含主设备号和次设备号 | MAJOR()、MINOR() |
字符设备驱动标准实现步骤
- 分配设备号:使用
alloc_chrdev_region()动态分配,或register_chrdev_region()静态指定。 - 初始化 cdev:调用
cdev_init()将file_operations绑定到struct cdev。 - 添加设备到系统:使用
cdev_add()将字符设备注册到内核。 - 创建设备节点:通过
device_create()在/dev目录下生成设备文件。 - 实现操作函数:填充
open、read、write、release等回调。 - 注销与清理:卸载时依次调用
device_destroy()、cdev_del()、unregister_chrdev_region()。
file_operations 中的函数,内核会在应用程序发起系统调用时自动调用对应的回调函数。
1.1.3 驱动与应用程序的交互方式
应用程序与内核驱动之间的交互通过系统调用实现。当用户程序调用 open()、read()、write()、ioctl() 等标准 C 库函数时,库函数会触发相应的系统调用,陷入内核态,最终由驱动中注册的 file_operations 回调函数处理。
主要交互接口对比
| 系统调用 | 驱动回调 | 典型用途 | 数据传输方式 |
|---|---|---|---|
open() |
.open |
打开设备,初始化硬件或分配资源 | 无数据 |
close() |
.release |
关闭设备,释放资源 | 无数据 |
read() |
.read |
从设备读取数据到用户空间 | 内核 → 用户 |
write() |
.write |
从用户空间写入数据到设备 | 用户 → 内核 |
ioctl() |
.unlocked_ioctl |
设备控制命令,配置参数、获取状态 | 双向控制命令 |
数据拷贝机制
由于内核空间与用户空间的内存地址空间相互隔离,驱动不能直接访问用户空间指针。内核提供了专门的 API 用于安全地拷贝数据:
copy_to_user():将数据从内核空间拷贝到用户空间,用于read实现。copy_from_user():将数据从用户空间拷贝到内核空间,用于write实现。access_ok():验证用户空间地址的合法性,防止非法访问。
copy_from_user / copy_to_user 系列函数,否则会导致内核崩溃或安全漏洞。
ioctl 接口的特殊地位
在字符设备驱动中,ioctl 接口具有特殊的重要性。与 read/write 的纯数据流传输不同,ioctl 允许应用程序向设备发送控制命令,实现配置、查询、复位等非数据流操作。在 Linux 2.6.36 之后,内核使用 .unlocked_ioctl 替代了旧的 .ioctl,驱动开发者需要自行管理并发访问的锁保护。
1.1.4 本章小结
本章从内核模块的基础结构出发,逐步深入到字符设备驱动的标准框架,最后梳理了驱动与应用程序之间的交互方式。核心要点总结如下:
- 内核模块是 Linux 驱动的载体,通过
module_init/module_exit管理生命周期。 - 字符设备驱动以
struct cdev和file_operations为核心,遵循“注册-回调”模式。 - 应用与驱动交互依赖系统调用,数据拷贝必须使用
copy_to_user/copy_from_user保证安全。 - ioctl 接口是设备控制的核心通道,后续章节将深入讲解其实现与并发保护。