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:版本号,便于维护。
内核模块生命周期 insmod 加载 rmmod 卸载 lsmod 查看 module_init() 初始化:注册设备、分配资源 module_exit() 清理:注销设备、释放资源 模块运行中(驻留内核)

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()

字符设备驱动标准实现步骤

  1. 分配设备号:使用 alloc_chrdev_region() 动态分配,或 register_chrdev_region() 静态指定。
  2. 初始化 cdev:调用 cdev_init() 将 file_operations 绑定到 struct cdev。
  3. 添加设备到系统:使用 cdev_add() 将字符设备注册到内核。
  4. 创建设备节点:通过 device_create() 在 /dev 目录下生成设备文件。
  5. 实现操作函数:填充 open、read、write、release 等回调。
  6. 注销与清理:卸载时依次调用 device_destroy()、cdev_del()、unregister_chrdev_region()。
框架要点: 字符设备驱动的本质是“注册 + 回调”。驱动开发者只需实现 file_operations 中的函数,内核会在应用程序发起系统调用时自动调用对应的回调函数。
字符设备驱动框架结构 用户空间应用程序 open / read / write / ioctl VFS 虚拟文件系统 file_operations .open = my_open .read = my_read struct cdev 设备号 + ops 指针 硬件设备寄存器 ioremap / readl / writel 驱动核心 注册 cdev 实现 ops 操作硬件

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,驱动开发者需要自行管理并发访问的锁保护。

应用与驱动交互流程 应用程序 fd = open("/dev/mydev") 系统调用陷入内核 驱动 file_operations .open → my_open() .ioctl → my_ioctl() 硬件设备 寄存器操作 / DMA 数据流向 read: 设备 → 用户 write: 用户 → 设备

1.1.4 本章小结

本章从内核模块的基础结构出发,逐步深入到字符设备驱动的标准框架,最后梳理了驱动与应用程序之间的交互方式。核心要点总结如下:

  • 内核模块是 Linux 驱动的载体,通过 module_init / module_exit 管理生命周期。
  • 字符设备驱动以 struct cdev 和 file_operations 为核心,遵循“注册-回调”模式。
  • 应用与驱动交互依赖系统调用,数据拷贝必须使用 copy_to_user / copy_from_user 保证安全。
  • ioctl 接口是设备控制的核心通道,后续章节将深入讲解其实现与并发保护。