2. 字符设备驱动框架:file_operations结构体、主次设备号、cdev结构体、设备节点的创建

字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕file_operations结构体、设备号、cdev结构体以及设备节点四个要素展开。面试中,面试官通常要求你从零搭建一个完整的字符设备驱动骨架,并解释每个要素的作用与关联。

面试高频提示: 字符设备驱动的“四步法”是必考内容——分配设备号、初始化cdev、注册cdev、创建设备节点。任何一步缺失或顺序错误都会导致驱动无法正常工作。

2.1 file_operations结构体:驱动与VFS的契约

file_operations 是字符设备驱动的核心数据结构,它定义了用户空间通过系统调用(open、read、write、ioctl等)访问设备时,内核VFS层调用的驱动回调函数集合。每个字符设备驱动必须实现该结构体中的相关成员。

成员 函数原型 说明
open int (*open)(struct inode *, struct file *) 打开设备时调用,用于初始化设备或递增引用计数
release int (*release)(struct inode *, struct file *) 关闭设备时调用,用于释放资源或递减引用计数
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据,需使用copy_to_user安全拷贝
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据,需使用copy_from_user安全拷贝
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令处理(现代内核使用,替代ioctl)
llseek loff_t (*llseek)(struct file *, loff_t, int) 修改文件读写位置
// 典型的file_operations定义示例
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
    .llseek  = my_llseek,
};
避坑指南: 面试中常问“为什么read/write回调中不能直接使用用户空间指针?”——因为用户空间指针可能无效或导致内核崩溃,必须通过copy_to_user/copy_from_user进行安全拷贝,并检查返回值。
file_operations 与 VFS 调用关系 用户空间应用 系统调用 (VFS) file_operations open() read() / write() unlocked_ioctl() release() 调用流程说明: 1. 应用调用 open("/dev/mydev", O_RDWR) 2. VFS 根据 inode 找到 cdev,获取 file_operations 3. VFS 调用 fops->open() 回调 4. 驱动在回调中完成硬件初始化或状态设置

2.2 主次设备号:设备的身份证

设备号是内核标识字符设备的唯一编号,由主设备号和次设备号组成(32位dev_t类型,高12位为主设备号,低20位为次设备号)。主设备号对应驱动类型,次设备号对应同类型下的不同设备实例。

概念 位数 作用 示例
主设备号 (major) 12 bit (0~4095) 标识设备驱动类型,内核通过主设备号找到对应的cdev 1: 内存设备, 4: tty设备
次设备号 (minor) 20 bit (0~1048575) 标识同类型下的具体设备实例 /dev/tty0 (4,0), /dev/tty1 (4,1)
// 设备号操作宏
#include <linux/types.h>   // dev_t
#include <linux/kdev_t.h>  // MAJOR/MINOR/MKDEV

dev_t dev_num = MKDEV(major, minor);  // 合成设备号
int major = MAJOR(dev_num);           // 提取主设备号
int minor = MINOR(dev_num);           // 提取次设备号
面试要点: 设备号分配有两种方式——静态分配(手动指定主设备号,使用register_chrdev_region)和动态分配(内核自动分配,使用alloc_chrdev_region)。面试中推荐回答“优先使用动态分配,避免主设备号冲突”。
设备号 (dev_t) 结构 32-bit dev_t 主设备号 (12 bit) 次设备号 (20 bit) 范围: 0 ~ 4095 范围: 0 ~ 1048575 MKDEV(major, minor) → 合成 MAJOR(dev) / MINOR(dev) → 提取

2.3 cdev结构体:字符设备的内核表示

cdev 是内核中用于描述字符设备的核心结构体,它建立了设备号与file_operations之间的关联。每个注册的字符设备在内核中都有一个对应的cdev实例。

// cdev结构体定义(简化)
struct cdev {
    struct kobject kobj;          // 内核对象,用于设备模型
    struct module *owner;         // 所属模块,通常设为 THIS_MODULE
    const struct file_operations *ops;  // 操作函数集
    struct list_head list;        // 链表节点
    dev_t dev;                    // 设备号
    unsigned int count;           // 次设备号数量
};

cdev的初始化与注册流程是驱动加载的核心步骤:

  1. 分配cdev:使用 cdev_alloc() 或静态定义
  2. 初始化cdev:调用 cdev_init(&cdev, &fops) 绑定file_operations
  3. 设置owner:cdev.owner = THIS_MODULE
  4. 注册cdev:调用 cdev_add(&cdev, dev_num, count) 将设备加入内核
// 标准cdev注册示例
static struct cdev my_cdev;

static int __init my_driver_init(void)
{
    dev_t dev_num;
    int ret;

    // 1. 分配设备号(动态)
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

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

    // 3. 注册cdev
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to add cdev\n");
        return ret;
    }

    pr_info("My driver loaded, major=%d minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    return 0;
}

static void __exit my_driver_exit(void)
{
    dev_t dev_num = my_cdev.dev;

    // 卸载顺序与加载相反
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("My driver unloaded\n");
}
关键注意: cdev_add 之后设备立即可用,因此必须在cdev_add成功之后才暴露设备节点。卸载时先cdev_del再释放设备号,顺序不可颠倒。
cdev 注册与卸载流程 加载流程 (init) ① alloc_chrdev_region() ② cdev_init(&cdev, &fops) ③ cdev_add(&cdev, dev, count) ④ 创建设备节点 (udev/mdev) 卸载流程 (exit) ① 删除设备节点 ② cdev_del(&cdev) ③ unregister_chrdev_region() 注意:cdev_add 成功后设备立即可用,卸载时必须先 cdev_del 再释放设备号,顺序不可颠倒。 设备节点可通过 mknod 手动创建,或由 udev/mdev 自动创建。

2.4 设备节点的创建:用户空间的访问入口

设备节点(device node)是用户空间访问字符设备的文件接口,位于/dev目录下。设备节点记录了设备类型(字符/块)和设备号,VFS通过设备节点中的设备号找到对应的cdev驱动。

创建方式 命令/接口 适用场景
手动创建 mknod /dev/mydev c major minor 嵌入式系统、调试阶段
udev自动创建 基于uevent机制,用户空间udev监听内核事件 桌面Linux、服务器
mdev自动创建 BusyBox中的轻量级udev替代 嵌入式Linux(Buildroot/Yocto)
devtmpfs 内核自动创建,无需用户空间工具 现代Linux内核(推荐方式)
// 手动创建设备节点示例
# mknod /dev/mydevice c 240 0
# ls -l /dev/mydevice
crw------- 1 root root 240, 0 Jan 1 00:00 /dev/mydevice

// 使用class_create自动创建设备节点(驱动中实现)
static struct class *my_class;

static int __init my_driver_init(void)
{
    // ... 分配设备号、注册cdev ...

    // 创建设备类
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        // 错误处理
    }

    // 创建设备节点(自动在/dev下生成)
    device_create(my_class, NULL, dev_num, NULL, "mydevice%d", 0);
    return 0;
}

static void __exit my_driver_exit(void)
{
    // 卸载时销毁设备节点和类
    device_destroy(my_class, dev_num);
    class_destroy(my_class);

    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}
面试加分点: 现代驱动开发推荐使用class_create + device_create组合,内核会自动在/dev下创建设备节点,无需手动mknod。面试时提及“devtmpfs + class_create”方案,表明你了解最新的内核机制。
设备节点 → VFS → cdev → 驱动 关联链路 用户空间应用 设备节点 /dev/mydevice VFS 层 cdev 结构体 file_operations 硬件操作函数 通过设备号查找 关键路径:应用 open("/dev/mydevice") → VFS 解析设备节点中的设备号 → 通过 cdev 找到 file_operations → 调用驱动回调 设备节点中存储了设备类型(c=字符)和设备号(major, minor)

2.5 完整字符设备驱动框架总结

将上述四个要素整合,一个完整的字符设备驱动框架包含以下核心步骤:

步骤 操作 关键函数/宏 注意事项
1 定义file_operations struct file_operations fops = {...} 必须设置.owner = THIS_MODULE
2 分配设备号 alloc_chrdev_region() 或 register_chrdev_region() 优先动态分配
3 初始化cdev cdev_init() 绑定file_operations
4 注册cdev cdev_add() 成功后设备立即可用
5 创建设备节点 class_create() + device_create() 或使用mknod/udev
6 实现回调函数 open/read/write/ioctl/release 注意用户空间数据拷贝
面试核心问题清单:
  • file_operations中的每个回调在什么场景下被调用?
  • 主次设备号的作用是什么?如何避免主设备号冲突?
  • cdev_add之后设备就可用了吗?为什么?
  • 设备节点中存储了什么信息?VFS如何通过设备节点找到驱动?
  • 驱动卸载时为什么必须先cdev_del再unregister_chrdev_region?

掌握字符设备驱动框架是Linux驱动开发的基石。面试中,面试官通常会让你在白板上画出cdev、file_operations、设备号、设备节点之间的关系图,并手写一个最小化的字符设备驱动骨架。熟练理解并能够清晰阐述上述四个要素的协作关系,是顺利通过面试的关键。