2. Linux 驱动基础回顾:模块加载/卸载、file_operations 结构体、字符设备驱动框架

在深入 Platform 驱动与设备树匹配机制之前,必须牢固掌握 Linux 驱动开发的基础。本章将系统回顾三个核心支柱:模块的加载与卸载、file_operations 结构体以及字符设备驱动框架。这些概念是所有复杂驱动(包括 Platform 驱动)的基石。

2.1 内核模块:加载与卸载

Linux 驱动通常以内核模块(Kernel Module)的形式存在,这意味着它们可以在系统运行时动态加载到内核或从内核卸载,无需重新编译整个内核或重启系统。

2.1.1 模块的入口与出口

每个内核模块都必须定义两个关键函数:

  • 入口函数:模块被加载时执行,通常用于初始化资源、注册驱动。
  • 出口函数:模块被卸载时执行,用于释放入口函数申请的资源、注销驱动。

这两个函数通过特定的宏向内核声明:

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

// 模块入口函数
static int __init my_driver_init(void)
{
    printk(KERN_INFO "My driver loaded successfully.\n");
    // 在此处进行硬件初始化、注册设备等操作
    return 0;
}

// 模块出口函数
static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "My driver unloaded.\n");
    // 在此处释放资源、注销设备
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example driver module");
提示: __init 和 __exit 标记告诉内核,这些函数在初始化或卸载完成后,其占用的内存可以被回收,以节省系统资源。

2.1.2 模块操作命令

用户空间通过以下工具与内核模块交互:

命令 功能 内核行为
insmod my_driver.ko 加载模块 调用模块的 module_init() 指定的函数
rmmod my_driver 卸载模块 调用模块的 module_exit() 指定的函数
modprobe my_driver 智能加载模块 自动处理模块依赖关系,并调用 module_init()
lsmod 列出已加载模块 读取 /proc/modules 信息
注意: 使用 insmod 加载模块时,如果模块依赖其他模块,需要先手动加载依赖。而 modprobe 会自动处理这些依赖关系,是更推荐的方式。

2.2 file_operations 结构体:驱动与用户空间的桥梁

在 Linux 中,“一切皆文件”。用户空间程序通过标准的文件操作(如 open, read, write, close)与硬件设备通信。驱动开发者通过填充 struct file_operations 结构体,将这些系统调用与具体的硬件操作函数绑定起来。

2.2.1 核心成员解析

file_operations 结构体定义在 <linux/fs.h> 中,包含大量函数指针。以下是驱动开发中最常用的几个:

成员 系统调用 驱动实现职责
.owner — 通常设置为 THIS_MODULE,防止模块在使用中被卸载
.open open() 打开设备,进行权限检查、初始化硬件状态
.release close() 关闭设备,释放打开时申请的资源
.read read() 从设备读取数据到用户空间缓冲区
.write write() 从用户空间缓冲区写入数据到设备
.unlocked_ioctl ioctl() 执行设备特定的控制命令(无大内核锁)

2.2.2 典型定义示例

// 假设的设备操作函数声明
static int my_dev_open(struct inode *inode, struct file *filp);
static int my_dev_release(struct inode *inode, struct file *filp);
static ssize_t my_dev_read(struct file *filp, char __user *buf, size_t len, loff_t *off);
static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t len, loff_t *off);
static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

// 定义 file_operations 结构体
static const struct file_operations my_fops = {
    .owner          = THIS_MODULE,
    .open           = my_dev_open,
    .release        = my_dev_release,
    .read           = my_dev_read,
    .write          = my_dev_write,
    .unlocked_ioctl = my_dev_ioctl,
};
关键点: 驱动不直接实现系统调用,而是实现 file_operations 中的回调函数。当用户程序调用 read() 时,VFS(虚拟文件系统)根据设备文件关联的 file_operations,自动调用驱动中的 my_dev_read 函数。

2.3 字符设备驱动框架:从注册到注销

字符设备是 Linux 中最基本的设备类型,以字节流的形式进行数据传输。实现一个完整的字符设备驱动,需要遵循一套标准的框架流程。

2.3.1 核心流程与数据结构

字符设备驱动的生命周期涉及三个核心步骤:分配设备号、初始化并注册 cdev、创建设备文件。

字符设备驱动框架流程图 字符设备驱动框架流程图 模块入口 (module_init) 分配设备号 初始化 cdev 注册 cdev 创建设备文件 关键 API • alloc_chrdev_region() • cdev_init() + cdev_add() • class_create() + device_create() • 卸载时反向调用 *_destroy / del / unregister

2.3.2 完整代码框架示例

以下代码片段展示了字符设备驱动框架的核心骨架:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME  "my_class"

static int major;
static struct class *my_class;
static struct device *my_device;
static struct cdev my_cdev;

// 设备操作函数(简化)
static int my_open(struct inode *inode, struct file *filp) { return 0; }
static int my_release(struct inode *inode, struct file *filp) { return 0; }

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
};

static int __init my_init(void)
{
    dev_t dev_num;

    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    major = MAJOR(dev_num);

    // 2. 初始化并注册 cdev
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);

    // 3. 在 /sys/class 下创建类,并在 /dev 下创建设备节点
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);

    printk(KERN_INFO "Char device registered with major %d\n", major);
    return 0;
}

static void __exit my_exit(void)
{
    dev_t dev_num = MKDEV(major, 0);

    // 反向操作:销毁设备、类、注销 cdev、释放设备号
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);

    printk(KERN_INFO "Char device unregistered\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

2.3.3 框架步骤总结

  1. 分配设备号:使用 alloc_chrdev_region() 动态分配,避免与现有设备号冲突。
  2. 初始化 cdev:使用 cdev_init() 将 cdev 结构与 file_operations 绑定。
  3. 注册 cdev:使用 cdev_add() 将字符设备通知给 VFS。
  4. 创建设备文件:通过 class_create() 和 device_create() 在 /dev 目录下自动生成设备节点,方便用户空间访问。
  5. 卸载时逆序释放:严格按照注册的相反顺序销毁设备、类、注销 cdev 并释放设备号。
核心思想: 字符设备驱动框架的本质是“注册回调”。驱动将 file_operations 注册到内核,内核在用户程序操作设备文件时,回调驱动中对应的函数。这种机制将硬件操作与系统调用解耦,是 Linux 驱动设计的精髓。

掌握本章的模块、file_operations 和字符设备框架,是理解后续 Platform 驱动的基础。Platform 驱动本质上是对字符设备框架的进一步封装和抽象,用于管理那些不挂载在传统总线(如 PCI、USB)上的设备。