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

字符设备是 Linux 驱动开发中最基础的设备类型,其核心在于通过 file_operations 结构体将用户空间的系统调用与内核驱动函数绑定。本章将深入剖析字符设备驱动的三大支柱:操作接口、设备编号与节点创建。

3.1 字符设备驱动核心模型

字符设备驱动的本质是“文件抽象”——用户通过 open/read/write/close 等文件操作与硬件交互。内核使用 struct cdev 表示字符设备,并通过 file_operations 提供标准接口。

字符设备驱动核心架构 用户空间应用程序 系统调用 (sys_open/read/write) 虚拟文件系统 (VFS) file_operations .open / .read / .write / .release 驱动实现函数 硬件设备 核心流程:用户调用 → 系统调用 → VFS 查找 → file_operations 分发 → 驱动函数 → 硬件操作

3.2 file_operations 结构体详解

file_operations 是字符设备驱动的“函数跳转表”,定义在 <linux/fs.h> 中。每个成员对应一个系统调用,驱动开发者只需实现需要的接口。

3.2.1 核心成员与函数原型

成员名称 系统调用触发 函数原型 典型用途
.open open() int (*open)(struct inode *, struct file *) 设备初始化、权限检查、分配私有数据
.release close() int (*release)(struct inode *, struct file *) 资源释放、关闭设备
.read read() ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据到用户空间
.write write() ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 从用户空间写入数据到设备
.unlocked_ioctl ioctl() long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令(无大内核锁)
.mmap mmap() int (*mmap)(struct file *, struct vm_area_struct *) 设备内存映射到用户空间
.poll poll()/select() __poll_t (*poll)(struct file *, struct poll_table_struct *) 设备可读/可写状态查询

3.2.2 标准初始化模板

// 定义驱动函数
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 count, loff_t *f_pos) {
    // 实现数据读取逻辑
    return 0;
}

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

// 填充 file_operations 结构体
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};
关键提示: .owner 必须设置为 THIS_MODULE,防止模块正在使用时被卸载。未实现的接口应设为 NULL,内核会返回 -EINVAL。

3.3 主次设备号管理

设备号是内核识别设备的唯一标识,由 主设备号 (major) 和 次设备号 (minor) 组成,类型为 dev_t(32位无符号整数)。

3.3.1 设备号结构

设备号 (dev_t) 位布局 主设备号 (12 bits) 次设备号 (20 bits) bit 31 ~ bit 20 (12位,范围 0~4095) bit 19 ~ bit 0 (20位,范围 0~1048575) 主设备号标识驱动类型,次设备号标识同一驱动下的不同设备实例

3.3.2 设备号操作宏

宏/函数 功能 示例
MKDEV(major, minor) 将主次设备号合并为 dev_t dev_t dev = MKDEV(240, 0);
MAJOR(dev) 从 dev_t 提取主设备号 int major = MAJOR(dev);
MINOR(dev) 从 dev_t 提取次设备号 int minor = MINOR(dev);

3.3.3 设备号分配策略

  • 静态分配: 使用 register_chrdev_region(dev_t first, unsigned int count, const char *name),需提前知道主设备号(常用于已知设备号或教学场景)。
  • 动态分配: 使用 alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name),由内核自动分配主设备号(推荐生产环境使用)。
// 动态分配设备号示例
dev_t dev_num;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
    pr_err("Failed to allocate device number\n");
    return ret;
}
pr_info("Allocated major=%d minor=%d\n",
        MAJOR(dev_num), MINOR(dev_num));
避坑指南: 动态分配时不要假设主设备号固定。用户空间通过 /proc/devices 或 udev 获取设备号,切勿硬编码主设备号到应用程序中。

3.4 设备节点创建

设备节点是用户空间访问驱动的“文件入口”,位于 /dev 目录下。创建方式分为手动和自动两种。

3.4.1 手动创建(mknod)

适用于开发调试阶段,使用 mknod 命令:

# mknod /dev/mydevice c 240 0
# 格式:mknod 路径 类型(c/b) 主设备号 次设备号
# c 表示字符设备,b 表示块设备

3.4.2 自动创建(udev/mdev + class)

生产环境推荐使用设备模型自动创建节点。驱动中只需创建 device class 和 device,用户空间的 udev(桌面)或 mdev(嵌入式 busybox)会自动在 /dev 下生成节点。

static struct class *my_class;
static struct device *my_device;

// 在模块初始化函数中
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
    unregister_chrdev_region(dev_num, 1);
    return PTR_ERR(my_class);
}

my_device = device_create(my_class, NULL, dev_num,
                          NULL, "mydevice");
if (IS_ERR(my_device)) {
    class_destroy(my_class);
    unregister_chrdev_region(dev_num, 1);
    return PTR_ERR(my_device);
}

// 在模块退出函数中
device_destroy(my_class, dev_num);
class_destroy(my_class);
unregister_chrdev_region(dev_num, 1);

3.4.3 设备节点创建流程对比

方式 适用场景 优点 缺点
mknod 手动 开发调试、嵌入式无 udev 简单直接,不依赖用户空间工具 需手动管理,设备号变化时需重新创建
udev 自动 桌面 Linux、服务器 自动创建/删除,支持热插拔 依赖 udev 服务,系统启动稍慢
mdev 自动 嵌入式 busybox 环境 轻量级,适合资源受限设备 功能较 udev 弱,需手动触发扫描

3.5 完整字符设备注册流程

字符设备驱动注册与注销完整流程 模块初始化 (module_init) 1. alloc_chrdev_region 2. cdev_init + cdev_add 3. class_create 4. device_create 模块注销 (module_exit) 1. device_destroy 2. class_destroy 3. cdev_del 4. unregister_chrdev_region 注册与注销顺序必须严格对称,否则导致资源泄漏

3.6 综合代码示例:最小字符设备驱动

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

#define DEVICE_NAME "demo_device"
#define CLASS_NAME  "demo_class"

static dev_t dev_num;
static struct cdev demo_cdev;
static struct class *demo_class;
static struct device *demo_device;

static int demo_open(struct inode *inode, struct file *filp) {
    pr_info("demo: device opened\n");
    return 0;
}

static int demo_release(struct inode *inode, struct file *filp) {
    pr_info("demo: device closed\n");
    return 0;
}

static ssize_t demo_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *f_pos) {
    char kernel_buf[] = "Hello from kernel!\n";
    size_t len = strlen(kernel_buf);

    if (*f_pos >= len)
        return 0;

    if (count > len - *f_pos)
        count = len - *f_pos;

    if (copy_to_user(buf, kernel_buf + *f_pos, count))
        return -EFAULT;

    *f_pos += count;
    return count;
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .release = demo_release,
    .read    = demo_read,
};

static int __init demo_init(void) {
    int ret;

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

    // 2. 初始化并添加 cdev
    cdev_init(&demo_cdev, &fops);
    demo_cdev.owner = THIS_MODULE;
    ret = cdev_add(&demo_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to add cdev\n");
        return ret;
    }

    // 3. 创建设备类
    demo_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(demo_class)) {
        cdev_del(&demo_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(demo_class);
    }

    // 4. 创建设备节点
    demo_device = device_create(demo_class, NULL, dev_num,
                                NULL, DEVICE_NAME);
    if (IS_ERR(demo_device)) {
        class_destroy(demo_class);
        cdev_del(&demo_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(demo_device);
    }

    pr_info("demo: driver loaded successfully\n");
    return 0;
}

static void __exit demo_exit(void) {
    device_destroy(demo_class, dev_num);
    class_destroy(demo_class);
    cdev_del(&demo_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("demo: driver unloaded\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Minimal character device driver demo");
测试步骤:
  1. 编译驱动:make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
  2. 加载驱动:sudo insmod demo.ko
  3. 查看设备节点:ls -l /dev/demo_device
  4. 读取数据:cat /dev/demo_device
  5. 卸载驱动:sudo rmmod demo

3.7 本章小结

  • file_operations 是字符设备驱动的核心接口,每个成员对应一个用户空间系统调用。
  • 设备号 由主设备号(12位)和次设备号(20位)组成,推荐使用 alloc_chrdev_region 动态分配。
  • 设备节点 可通过 mknod 手动创建,或通过 class_create + device_create 配合 udev/mdev 自动创建。
  • 驱动注册与注销必须严格对称,顺序为:alloc → cdev_init/add → class_create → device_create,注销时反向操作。