2. Linux字符设备驱动回顾:file_operations结构体、cdev注册、设备号分配、设备节点创建

在深入UART中断与工作队列优化之前,必须牢固掌握Linux字符设备驱动的基础骨架。UART驱动本质上是字符设备驱动的一种特殊实现,其注册、文件操作接口、设备号管理以及节点创建机制与标准字符设备完全一致。本节将系统回顾这四个核心要素,为后续的中断处理与延迟工作奠定基础。

2.1 file_operations结构体:驱动与用户空间的契约

file_operations 是字符设备驱动的核心数据结构,它定义了用户空间程序通过系统调用(open、read、write、ioctl等)与内核驱动交互的接口函数指针表。每个字符设备驱动都必须实例化并填充此结构体。

关键理解: file_operations 是驱动开发者向内核注册的“回调函数表”,当用户程序对设备文件执行系统调用时,VFS(虚拟文件系统)通过该表找到对应的驱动函数执行。

以下是一个典型的UART驱动file_operations定义示例:

// uart_drv_fops.c — 典型UART字符设备文件操作接口
static int uart_open(struct inode *inode, struct file *filp) {
    // 获取私有数据、初始化硬件、申请中断等
    return 0;
}

static ssize_t uart_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *f_pos) {
    // 从环形缓冲区拷贝数据到用户空间
    return copied;
}

static ssize_t uart_write(struct file *filp, const char __user *buf,
                          size_t count, loff_t *f_pos) {
    // 将用户数据写入发送FIFO或DMA缓冲区
    return written;
}

static long uart_ioctl(struct file *filp, unsigned int cmd,
                       unsigned long arg) {
    // 处理波特率设置、流控、中断使能等控制命令
    return 0;
}

static int uart_release(struct inode *inode, struct file *filp) {
    // 释放资源、关闭硬件
    return 0;
}

static struct file_operations uart_fops = {
    .owner   = THIS_MODULE,
    .open    = uart_open,
    .read    = uart_read,
    .write   = uart_write,
    .unlocked_ioctl = uart_ioctl,
    .release = uart_release,
};
file_operations 核心成员与UART驱动典型用途
成员 系统调用 UART驱动典型实现
.open open() 初始化UART硬件、申请中断号、分配环形缓冲区
.read read() 从接收FIFO或环形缓冲区拷贝数据到用户空间
.write write() 将用户数据写入发送FIFO或启动DMA传输
.unlocked_ioctl ioctl() 设置波特率、数据位、校验位、流控模式
.release close() 关闭中断、释放DMA通道、释放缓冲区
避坑提示: 在UART驱动中,.read 和 .write 通常需要支持非阻塞操作(O_NONBLOCK),否则用户态程序在无数据时会阻塞在系统调用中。务必检查 filp->f_flags 并返回 -EAGAIN。

2.2 cdev注册:将驱动与内核设备模型绑定

cdev 是内核用于管理字符设备的核心对象。驱动必须初始化一个 cdev 结构体,将其与 file_operations 关联,然后通过 cdev_add() 将其注册到内核的字符设备驱动模型中。只有完成这一步,内核才能根据设备号找到对应的驱动函数。

标准注册流程包含三个步骤:

  1. 分配并初始化cdev: 使用 cdev_alloc() 或静态定义后调用 cdev_init()。
  2. 关联file_operations: 将 cdev->ops 指向已填充的 file_operations 结构体。
  3. 添加到内核: 调用 cdev_add() 传入设备号范围和数量。
// cdev_registration.c — 字符设备注册核心代码片段
#include <linux/cdev.h>
#include <linux/fs.h>

static dev_t uart_dev_num;          // 设备号(主+次)
static struct cdev uart_cdev;       // cdev对象

static int __init uart_init(void) {
    int ret;

    // 步骤1:分配设备号(假设已通过 alloc_chrdev_region 获取)
    // uart_dev_num = MKDEV(major, minor);

    // 步骤2:初始化cdev并关联fops
    cdev_init(&uart_cdev, &uart_fops);
    uart_cdev.owner = THIS_MODULE;

    // 步骤3:注册到内核(1个次设备号)
    ret = cdev_add(&uart_cdev, uart_dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        return ret;
    }

    pr_info("UART driver registered with major=%d minor=%d\n",
            MAJOR(uart_dev_num), MINOR(uart_dev_num));
    return 0;
}

static void __exit uart_exit(void) {
    cdev_del(&uart_cdev);           // 从内核移除cdev
    unregister_chrdev_region(uart_dev_num, 1); // 释放设备号
}
关键时序: cdev_add() 一旦成功,内核立即可以访问该设备。因此,必须在cdev_add()之前完成所有硬件初始化,否则在设备打开瞬间可能因硬件未就绪而导致系统崩溃。
cdev注册流程与内核设备模型关系 cdev注册流程与内核设备模型关系 驱动模块 file_operations uart_fops (open/read/write/ioctl) cdev 对象 ops → uart_fops dev → 设备号 VFS / 设备节点 /dev/ttyUART0 inode → cdev 用户态通过此节点访问 cdev_init() cdev_add() 注册时序要点 先 alloc_chrdev_region → 再 cdev_init → 最后 cdev_add(硬件就绪后) ① 分配设备号 ② cdev_init ③ cdev_add 设备就绪

2.3 设备号分配:主设备号与次设备号的策略

设备号是内核识别字符设备的唯一标识,由主设备号(major)和次设备号(minor)组成。主设备号对应驱动,次设备号对应同一驱动管理的不同设备实例(如多个UART端口)。

内核提供两种分配策略:

设备号分配策略对比
策略 函数 适用场景 优点 缺点
静态分配 register_chrdev_region() 已知主设备号(如调试、固定平台) 设备号固定,易于预测 可能与其他驱动冲突
动态分配 alloc_chrdev_region() 通用驱动、模块化设计 避免冲突,自动分配 设备号不固定,需查看/proc/devices
// device_number_allocation.c — 两种分配方式示例
#include <linux/fs.h>

// 方式一:静态分配(已知主设备号 240)
#define UART_MAJOR  240
#define UART_MINOR  0
#define UART_COUNT  4   // 4个UART端口

static dev_t uart_dev_static = MKDEV(UART_MAJOR, UART_MINOR);
ret = register_chrdev_region(uart_dev_static, UART_COUNT, "uart_drv");
if (ret < 0) {
    pr_err("Static region registration failed\n");
    return ret;
}

// 方式二:动态分配(推荐)
static dev_t uart_dev_dynamic;
ret = alloc_chrdev_region(&uart_dev_dynamic, 0, UART_COUNT, "uart_drv");
if (ret < 0) {
    pr_err("Dynamic region allocation failed\n");
    return ret;
}
pr_info("Allocated major=%d minor=%d\n",
        MAJOR(uart_dev_dynamic), MINOR(uart_dev_dynamic));
最佳实践: 在UART驱动中,通常使用动态分配 alloc_chrdev_region(),因为嵌入式平台的主设备号分配可能因内核版本或板级配置而异。动态分配后,通过 cat /proc/devices 查看分配结果,并使用 mknod 或 udev 创建设备节点。

2.4 设备节点创建:从内核到用户空间的桥梁

设备节点是用户空间访问驱动的入口,位于 /dev 目录下。节点创建有两种方式:手动 mknod 和自动 udev/mdev。在嵌入式系统中,mdev(BusyBox)或 udev(桌面系统)通过内核发送的 uevent 自动创建设备节点。

现代内核驱动推荐使用 device_create() 配合类(class)来自动管理节点:

// device_node_creation.c — 自动创建设备节点
#include <linux/device.h>

static struct class *uart_class;
static struct device *uart_device;

static int __init uart_init(void) {
    // ... 之前的设备号分配和cdev_add ...

    // 创建设备类
    uart_class = class_create(THIS_MODULE, "uart_class");
    if (IS_ERR(uart_class)) {
        pr_err("Failed to create class\n");
        return PTR_ERR(uart_class);
    }

    // 创建设备节点 /dev/uart0
    uart_device = device_create(uart_class, NULL, uart_dev_num,
                                NULL, "uart%d", 0);
    if (IS_ERR(uart_device)) {
        pr_err("Failed to create device\n");
        class_destroy(uart_class);
        return PTR_ERR(uart_device);
    }

    pr_info("Device node /dev/uart0 created automatically\n");
    return 0;
}

static void __exit uart_exit(void) {
    device_destroy(uart_class, uart_dev_num);
    class_destroy(uart_class);
    cdev_del(&uart_cdev);
    unregister_chrdev_region(uart_dev_num, 1);
}
设备节点创建流程与用户空间交互 设备节点创建流程与用户空间交互 内核空间 UART驱动模块 device_create() class_create() 内核设备模型 sysfs / uevent 设备号 → 节点 注册类/设备 用户空间 udev / mdev 监听uevent 自动mknod /dev/uart0 主:次 设备号 权限 666 用户应用程序 open("/dev/uart0") read/write/ioctl uevent (添加设备) 创建节点 系统调用
完整注册流程总结:
① alloc_chrdev_region() → 获取动态设备号
② cdev_init() + cdev_add() → 注册字符设备
③ class_create() + device_create() → 自动生成 /dev/uartX 节点
④ 用户程序通过 open("/dev/uart0") 触发 uart_open() → 驱动开始工作