2. Linux 字符设备驱动基础回顾:file_operations 结构体、cdev 注册、设备号分配、简单的 open/read/write 实现

在深入阻塞与非阻塞 I/O 及 poll 机制之前,必须牢固掌握字符设备驱动的核心骨架。本章将系统回顾 file_operations 结构体、cdev 注册流程、设备号分配以及最基本的 open/read/write 实现。这些基础组件是后续所有高级 I/O 模型的基石。

2.1 字符设备驱动核心数据结构

字符设备驱动的灵魂是 struct file_operations,它定义了用户空间系统调用与内核驱动函数之间的映射关系。每个打开的设备文件在内核中都对应一个 struct file 实例,而 struct inode 则代表底层的设备节点。

关键概念: 用户态调用 read() → VFS 层 → 根据 file->f_op 找到驱动注册的 xxx_read() 函数 → 执行硬件或缓冲区操作 → 返回数据到用户空间。

2.1.1 file_operations 结构体成员速查

成员函数 系统调用对应 典型用途
open open() 初始化设备、递增使用计数、分配私有数据
release close() 清理资源、递减使用计数
read read() 从设备或内核缓冲区拷贝数据到用户空间
write write() 从用户空间拷贝数据到内核缓冲区或设备
poll poll() / select() / epoll() 查询设备是否可读/可写,支持非阻塞等待
unlocked_ioctl ioctl() 设备特定的控制命令(无大内核锁)
mmap mmap() 将设备内存映射到用户空间
驱动编写提示: 并非所有成员都必须实现。对于简单的字符设备,通常只需实现 open、release、read、write 四个函数。未实现的成员应设置为 NULL,VFS 会返回 -ENOTTY 或 -EINVAL。

2.2 设备号分配:静态 vs 动态

每个字符设备在内核中由 主设备号(Major Number)和 次设备号(Minor Number)唯一标识。主设备号对应驱动程序,次设备号对应具体的设备实例。

2.2.1 设备号分配方式对比

分配方式 函数/宏 优点 缺点
静态分配 register_chrdev_region() 设备号固定,适合广泛使用的标准设备 需要提前申请,可能与其他驱动冲突
动态分配 alloc_chrdev_region() 自动分配未使用的主设备号,避免冲突 设备号不固定,需要手动 mknod 或依赖 udev

2.2.2 设备号操作宏

#include <linux/fs.h>
#include <linux/kdev_t.h>

// 从主/次设备号合成 dev_t
dev_t dev = MKDEV(major, minor);

// 从 dev_t 提取主设备号
int major = MAJOR(dev);

// 从 dev_t 提取次设备号
int minor = MINOR(dev);

2.3 cdev 结构体与注册流程

struct cdev 是内核中表示字符设备的对象。驱动必须初始化一个 cdev 实例并将其添加到内核的设备表中。

2.3.1 cdev 注册标准步骤

  1. 分配设备号:使用 alloc_chrdev_region() 或 register_chrdev_region()。
  2. 初始化 cdev:调用 cdev_init(),绑定 file_operations 结构体。
  3. 添加 cdev 到内核:调用 cdev_add(),将设备与设备号关联。
  4. 创建设备节点:手动 mknod 或使用 device_create() 自动创建(需配合类)。
常见错误: cdev_add() 必须在设备准备好处理 I/O 之后调用。如果在 cdev_add() 之后才初始化硬件或缓冲区,用户空间可能在设备未就绪时发起操作,导致内核崩溃。

2.3.2 典型注册代码模板

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

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

static int __init my_init(void)
{
    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    
    // 2. 初始化 cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    
    // 3. 添加 cdev 到内核
    cdev_add(&my_cdev, dev_num, 1);
    
    // 4. 创建 device class 和 device(自动生成 /dev/my_device)
    my_class = class_create(THIS_MODULE, "my_class");
    device_create(my_class, NULL, dev_num, NULL, "my_device");
    
    return 0;
}

static void __exit my_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

module_init(my_init);
module_exit(my_exit);

2.4 简单的 open/read/write 实现

以下是一个基于内核缓冲区的虚拟字符设备实现,展示了最基础的读写操作。该实现使用一个简单的环形缓冲区或固定数组来模拟设备数据。

2.4.1 驱动内部数据结构

#define BUF_SIZE 1024

struct my_device_data {
    char buffer[BUF_SIZE];
    int head;
    int tail;
    struct mutex lock;
};

static struct my_device_data *dev_data;

2.4.2 open 与 release 实现

static int my_open(struct inode *inode, struct file *filp)
{
    // 将私有数据指针存入 filp->private_data
    filp->private_data = dev_data;
    return 0;
}

static int my_release(struct inode *inode, struct file *filp)
{
    // 清理工作(本例中无特殊操作)
    return 0;
}

2.4.3 read 实现(阻塞版本)

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    struct my_device_data *data = filp->private_data;
    ssize_t ret;
    
    mutex_lock(&data->lock);
    
    // 计算可读字节数
    size_t available = data->head - data->tail;
    if (available == 0) {
        mutex_unlock(&data->lock);
        return 0;  // 无数据可读
    }
    
    size_t to_read = min(count, available);
    
    // 从内核缓冲区拷贝到用户空间
    if (copy_to_user(buf, data->buffer + data->tail, to_read)) {
        mutex_unlock(&data->lock);
        return -EFAULT;
    }
    
    data->tail += to_read;
    *f_pos += to_read;
    
    mutex_unlock(&data->lock);
    return to_read;
}

2.4.4 write 实现(阻塞版本)

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos)
{
    struct my_device_data *data = filp->private_data;
    ssize_t ret;
    
    mutex_lock(&data->lock);
    
    // 计算剩余空间
    size_t space = BUF_SIZE - (data->head - data->tail);
    if (space == 0) {
        mutex_unlock(&data->lock);
        return -ENOSPC;  // 缓冲区满
    }
    
    size_t to_write = min(count, space);
    
    // 从用户空间拷贝到内核缓冲区
    if (copy_from_user(data->buffer + data->head, buf, to_write)) {
        mutex_unlock(&data->lock);
        return -EFAULT;
    }
    
    data->head += to_write;
    *f_pos += to_write;
    
    mutex_unlock(&data->lock);
    return to_write;
}

2.5 字符设备驱动整体架构图

下图展示了用户空间、VFS 层、字符设备驱动核心以及硬件之间的完整调用链路与数据流关系。

字符设备驱动整体架构图 字符设备驱动整体架构图 用户空间 应用程序调用 open() / read() / write() / close() 虚拟文件系统 (VFS) 层 根据 inode 查找 file_operations,分发系统调用 字符设备驱动核心 file_operations cdev 注册 设备号管理 缓冲区 硬件设备 / 内核缓冲区 物理设备寄存器 / DMA / 内存映射 I/O 说明:驱动通过 cdev_add() 注册到内核,VFS 通过设备号找到对应的 file_operations,完成系统调用分发。

2.6 本章核心要点总结

组件 关键函数/宏 注意事项
设备号分配 alloc_chrdev_region() 动态分配优先,避免主设备号冲突
cdev 初始化 cdev_init() 必须在 cdev_add() 之前完成
cdev 注册 cdev_add() 确保设备已就绪后再注册
用户数据拷贝 copy_to_user() / copy_from_user() 必须检查返回值,失败返回 -EFAULT
并发保护 mutex_lock() / mutex_unlock() 多进程同时访问时必须加锁
设备节点创建 device_create() 配合 class_create() 自动生成 /dev 节点