3. 字符设备驱动基础:设备号、file_operations 与 cdev

字符设备是 Linux 驱动开发中最基础、最常用的设备类型,其核心特征是数据按字节流顺序访问,典型的字符设备包括串口、键盘、鼠标、LED 等。本章将深入剖析字符设备驱动的三大基石:设备号(主/次设备号)、file_operations 结构体以及 cdev 的注册与注销。掌握这些概念是编写任何字符设备驱动的第一步。

核心要点:字符设备驱动的本质是“用户空间通过文件接口(open/read/write/close)操作硬件”,内核通过设备号定位驱动,通过 file_operations 结构体将系统调用映射到驱动函数,通过 cdev 将设备号与操作集绑定到内核。

3.1 设备号:主设备号与次设备号

在 Linux 中,每个设备在内核中都有一个唯一的数字标识,称为设备号(dev_t)。设备号是一个 32 位无符号整数,高 12 位表示主设备号(major number),低 20 位表示次设备号(minor number)。

  • 主设备号:标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。
  • 次设备号:由驱动内部使用,用于区分同一驱动管理的多个物理或逻辑设备。
概念 位数 范围 作用
主设备号 12 位 0 ~ 4095 标识驱动程序
次设备号 20 位 0 ~ 1048575 标识具体设备实例
设备号操作宏:内核提供了一组宏来操作 dev_t 类型,驱动开发者应优先使用这些宏,而非手动位运算。
#include <linux/types.h>   // dev_t
#include <linux/kdev_t.h>  // 宏定义

dev_t dev = MKDEV(major, minor);  // 由主次设备号合成 dev_t
int major = MAJOR(dev);           // 从 dev_t 提取主设备号
int minor = MINOR(dev);           // 从 dev_t 提取次设备号

3.1.1 静态分配与动态分配

设备号的分配有两种方式:

  • 静态分配:驱动开发者手动指定一个主设备号(如 240),使用 register_chrdev_region() 注册。适用于已知且未被占用的设备号,但容易与其它驱动冲突。
  • 动态分配:调用 alloc_chrdev_region() 让内核自动分配一个未使用的主设备号。推荐用于大多数驱动,避免冲突。
避坑指南:静态分配主设备号时,务必先通过 cat /proc/devices 查看当前已占用的设备号列表,避免冲突。动态分配是更安全、更现代的做法。
设备号分配流程对比 静态分配 register_chrdev_region(dev, count, name) 风险:设备号冲突 动态分配 alloc_chrdev_region(&dev, baseminor, count, name) 优点:自动分配,无冲突 推荐使用动态分配 alloc_chrdev_region(),避免设备号冲突

3.2 file_operations 结构体

file_operations 是字符设备驱动的核心数据结构,它定义了用户空间通过文件系统接口操作设备时,内核应调用的驱动函数指针。该结构体定义在 <linux/fs.h> 中。

成员函数 系统调用触发 典型用途
open open() 初始化设备、递增使用计数
release close() 释放资源、递减使用计数
read read() 从设备读取数据到用户空间
write write() 从用户空间写入数据到设备
unlocked_ioctl ioctl() 设备控制命令(无大内核锁)
llseek lseek() 修改文件读写位置
典型 file_operations 定义示例:
#include <linux/fs.h>

static int my_open(struct inode *inode, struct file *filp) {
    pr_info("Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    pr_info("Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t len, loff_t *off) {
    // 实现读取逻辑
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t len, loff_t *off) {
    // 实现写入逻辑
    return len;
}

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};
关键点:file_operations 结构体中的 .owner 必须设置为 THIS_MODULE,用于防止模块正在使用时被卸载。如果某个操作不需要实现,直接设为 NULL,内核会使用默认行为。
用户空间系统调用到驱动函数的映射流程 用户空间 VFS 系统调用层 file_operations 驱动函数实现 映射关系示例 用户调用 open("/dev/mydev", O_RDWR) → VFS → my_fops.open → my_open() 用户调用 read(fd, buf, 1024) → VFS → my_fops.read → my_read() 用户调用 close(fd) → VFS → my_fops.release → my_release()

3.3 cdev 结构体:注册与注销

cdev 是内核用于表示字符设备的核心结构体,定义在 <linux/cdev.h> 中。它负责将设备号与 file_operations 绑定在一起,并注册到内核的设备管理子系统中。

3.3.1 cdev 初始化与注册步骤

  1. 分配设备号:使用 alloc_chrdev_region() 或 register_chrdev_region()。
  2. 初始化 cdev:调用 cdev_init(),将 cdev 与 file_operations 关联。
  3. 添加 cdev 到内核:调用 cdev_add(),将 cdev 与设备号绑定并激活。
完整注册示例:
#include <linux/cdev.h>
#include <linux/device.h>

static dev_t my_dev;
static struct cdev my_cdev;

static int __init my_driver_init(void) {
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&my_dev, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }
    pr_info("Allocated device number: major=%d, minor=%d\n",
            MAJOR(my_dev), MINOR(my_dev));

    // 2. 初始化 cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 3. 添加 cdev 到内核
    ret = cdev_add(&my_cdev, my_dev, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(my_dev, 1);
        return ret;
    }

    pr_info("Character device registered successfully\n");
    return 0;
}

3.3.2 cdev 注销步骤

  1. 删除 cdev:调用 cdev_del(),将 cdev 从内核中移除。
  2. 释放设备号:调用 unregister_chrdev_region()。
完整注销示例:
static void __exit my_driver_exit(void) {
    // 1. 删除 cdev
    cdev_del(&my_cdev);

    // 2. 释放设备号
    unregister_chrdev_region(my_dev, 1);

    pr_info("Character device unregistered\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
重要顺序:注销时必须先调用 cdev_del() 再调用 unregister_chrdev_region()。如果先释放设备号,cdev 仍然指向一个无效的设备号,可能导致内核崩溃。
cdev 生命周期:注册与注销流程 注册流程 ① alloc_chrdev_region() ② cdev_init(&cdev, &fops) ③ cdev_add(&cdev, dev, count) 注销流程 ① cdev_del(&cdev) ② unregister_chrdev_region(dev, count) 关键顺序约束 注册:先分配设备号 → 再初始化 cdev → 最后添加 cdev 注销:先删除 cdev → 再释放设备号(顺序不可颠倒)

3.4 完整驱动框架模板

将上述三个核心要素整合在一起,形成一个可编译的字符设备驱动模板:

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

static dev_t my_dev;
static struct cdev my_cdev;

static int my_open(struct inode *inode, struct file *filp) {
    pr_info("Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    pr_info("Device closed\n");
    return 0;
}

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

static int __init my_init(void) {
    int ret;

    ret = alloc_chrdev_region(&my_dev, 0, 1, "my_device");
    if (ret < 0) return ret;

    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    ret = cdev_add(&my_cdev, my_dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(my_dev, 1);
        return ret;
    }

    pr_info("Module loaded: major=%d minor=%d\n",
            MAJOR(my_dev), MINOR(my_dev));
    return 0;
}

static void __exit my_exit(void) {
    cdev_del(&my_cdev);
    unregister_chrdev_region(my_dev, 1);
    pr_info("Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
编译与测试:将上述代码保存为 my_char_dev.c,编写 Makefile 编译为内核模块。加载后通过 cat /proc/devices 查看分配的主设备号,使用 mknod /dev/mydev c major 0 创建设备节点,即可通过 echo 和 cat 测试。

3.5 本章小结

组件 作用 关键函数/宏
设备号 标识驱动和设备实例 MKDEV, MAJOR, MINOR, alloc_chrdev_region, unregister_chrdev_region
file_operations 定义系统调用到驱动函数的映射 struct file_operations, .owner = THIS_MODULE
cdev 将设备号与操作集绑定并注册到内核 cdev_init, cdev_add, cdev_del