3. 字符设备驱动框架:设备号注册与注销、file_operations结构体、cdev结构体、设备节点的创建(手动与自动)

字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕设备号、cdev结构体和file_operations操作集展开。本章将系统讲解字符设备驱动的完整骨架:从设备号的申请与释放,到内核对象的注册与注销,再到用户空间设备节点的创建方式。

3.1 字符设备驱动整体架构

字符设备驱动的用户态与内核态交互遵循“文件操作”抽象模型。应用程序通过open()、read()、write()等系统调用进入内核,VFS层根据设备号找到对应的cdev,再回调驱动注册的file_operations函数指针。

字符设备驱动整体架构图 用户空间 应用程序:open / read / write / ioctl 虚拟文件系统 (VFS) 根据设备号查找 inode → cdev 内核驱动层 cdev 结构体 设备号 + ops file_operations open/read/write/... 硬件操作层 ioremap / 寄存器 物理硬件设备

3.2 设备号:主设备号与次设备号

设备号是内核标识字符/块设备的唯一键值,由主设备号和次设备号两部分组成,类型为dev_t(32位无符号整数)。

组成部分 位宽 作用 示例
主设备号 (major) 高12位 标识设备驱动程序 8 (SD/MMC块设备)
次设备号 (minor) 低20位 标识同驱动下的不同设备实例 0, 1, 2 (分区)
内核宏操作:使用 MAJOR(dev_t) 提取主设备号,MINOR(dev_t) 提取次设备号,MKDEV(major, minor) 合成 dev_t。

3.2.1 静态申请 vs 动态分配

设备号注册有两种策略,推荐使用动态分配以避免冲突:

方式 函数 优点 缺点
静态申请 register_chrdev_region() 设备号固定,适合已知标准设备 容易与已有驱动冲突
动态分配 alloc_chrdev_region() 内核自动分配未使用的主设备号 设备号不固定,需通过 /proc/devices 查看
动态分配代码模板:
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));

3.2.2 设备号注销

在驱动卸载时,必须释放已注册的设备号,否则下次加载会失败:

void unregister_chrdev_region(dev_t from, unsigned count);
常见错误:卸载驱动时忘记调用 unregister_chrdev_region(),导致 /proc/devices 残留僵尸设备号,下次 insmod 返回 -EBUSY。

3.3 file_operations 结构体

file_operations 是字符设备驱动的“函数跳转表”,每个成员对应一个系统调用。驱动只需实现需要的回调,未实现的成员置为 NULL,内核会返回 -EINVAL。

成员 系统调用 典型实现
open open() 递增使用计数、初始化硬件
release close() 递减使用计数、释放资源
read read() 从硬件读取数据到用户缓冲区
write write() 从用户缓冲区写入硬件
unlocked_ioctl ioctl() 设备控制命令处理
llseek lseek() 修改文件读写位置
标准初始化方式(推荐使用宏):
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,
};

3.4 cdev 结构体:内核设备对象

cdev 是内核中表示字符设备的内部对象,必须在使用前分配并初始化,然后添加到内核设备链表中。

3.4.1 cdev 生命周期

cdev 生命周期流程图 1. 分配 cdev cdev_alloc() 2. 初始化 cdev cdev_init() 3. 注册 cdev cdev_add() 4. 注销 cdev cdev_del() 关键说明 • cdev_alloc() 与 cdev_init() 二选一:前者动态分配,后者初始化栈/静态变量 • cdev_add() 必须在设备号注册之后调用 • cdev_del() 必须在设备号注销之前调用

3.4.2 标准初始化代码

struct cdev my_cdev;
dev_t dev_num;

/* 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 */
int ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

3.4.3 卸载时清理

cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
顺序不可颠倒:必须先 cdev_del() 再 unregister_chrdev_region()。如果先注销设备号,cdev 仍在内核链表中,后续访问会导致内核崩溃。

3.5 设备节点的创建

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

3.5.1 手动创建:mknod

适用于开发调试阶段,需要已知主次设备号:

# 查看已注册的设备号
cat /proc/devices

# 手动创建设备节点
sudo mknod /dev/my_device c 240 0
sudo chmod 666 /dev/my_device
参数 含义 示例值
c 字符设备类型 c
240 主设备号 从 /proc/devices 获取
0 次设备号 0 ~ 255

3.5.2 自动创建:udev + class

生产环境必须使用自动创建,依赖内核的 device class 机制和用户空间的 udev 守护进程。

自动创建设备节点流程 内核驱动 class_create() → device_create() 发送 uevent 到用户空间 用户空间 udev 接收 uevent 在 /dev 下创建节点 + 设置权限 结果:/dev/my_device 自动出现 权限 0666,属主 root:root

3.5.3 自动创建代码实现

static struct class *my_class;

/* 在 probe 或 init 中 */
my_class = class_create(THIS_MODULE, "my_device_class");
if (IS_ERR(my_class)) {
    unregister_chrdev_region(dev_num, 1);
    return PTR_ERR(my_class);
}

device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);

/* 在 remove 或 exit 中 */
device_destroy(my_class, dev_num);
class_destroy(my_class);
调试技巧:使用 udevadm monitor --kernel --property 可以实时查看内核发送的 uevent,确认自动创建流程是否正常。

3.6 完整驱动框架模板

将以上所有要素整合,形成字符设备驱动的标准骨架:

#include <linux/module.h>
#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 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) {
    pr_info("Read %zu bytes\n", len);
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t len, loff_t *off) {
    pr_info("Write %zu bytes\n", len);
    return len;
}

static 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) {
    int ret;

    /* 1. 分配设备号 */
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0) return ret;

    /* 2. 初始化并注册 cdev */
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    /* 3. 自动创建设备节点 */
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }
    device_create(my_class, NULL, dev_num, NULL, "my_device");

    pr_info("my_device loaded: major=%d minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    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);
    pr_info("my_device unloaded\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Character device driver template");

3.7 常见错误与排查

错误现象 可能原因 解决方案
insmod 返回 -EBUSY 设备号已被占用 使用动态分配或卸载冲突驱动
open /dev/my_device 返回 -ENOENT 设备节点未创建或主设备号不匹配 检查 /proc/devices,重新 mknod
open 返回 -ENODEV cdev_add 未调用或设备号错误 检查 cdev_add 返回值,确认 dev_t 正确
rmmod 后 /dev 节点仍存在 未调用 device_destroy 手动 rm /dev/my_device,修复 exit 函数
核心原则总结:
  • 设备号注册 ↔ 注销:alloc_chrdev_region ↔ unregister_chrdev_region
  • cdev 注册 ↔ 注销:cdev_add ↔ cdev_del
  • 设备节点创建 ↔ 销毁:device_create ↔ device_destroy
  • 卸载顺序:device_destroy → class_destroy → cdev_del → unregister_chrdev_region