3. 字符设备基础:cdev结构体、设备号分配、file_operations接口注册

字符设备是Linux驱动模型中最基础、最常用的设备类型,其核心在于通过cdev结构体、设备号和file_operations接口三要素完成内核与用户态的桥梁搭建。本节将逐一拆解这三者的定义、关系与注册流程。

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

每个字符设备在内核中由唯一的设备号标识。设备号是一个32位无符号整数(dev_t),高12位为主设备号,低20位为次设备号。

概念 位数 作用 示例
主设备号 12 bit (0~4095) 标识设备对应的驱动程序 1 (内存设备)、8 (SCSI磁盘)
次设备号 20 bit (0~1048575) 区分同一驱动管理的多个设备实例 0、1、2…
提示:使用 MAJOR(dev_t) 和 MINOR(dev_t) 宏可以从设备号中提取主/次编号;使用 MKDEV(major, minor) 宏可以将主/次编号合并为 dev_t 类型。

3.1.1 静态分配 vs 动态分配

  • 静态分配:调用 register_chrdev_region(dev_t first, unsigned int count, const char *name),需手动指定起始设备号。适用于已知且固定的主设备号场景。
  • 动态分配:调用 alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name),由内核自动分配未使用的主设备号。推荐用于模块化驱动,避免冲突。
注意:动态分配后,需通过 /proc/devices 查看分配的主设备号,或使用 printk 输出。卸载模块时必须调用 unregister_chrdev_region(dev_t dev, unsigned int count) 释放设备号。

3.2 cdev结构体:字符设备的内核抽象

struct cdev 是内核用于管理字符设备的核心数据结构,定义在 <linux/cdev.h> 中。每个字符设备在内核中对应一个 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;           // 次设备号数量
};

3.2.1 cdev 初始化与注册流程

  1. 分配 cdev 对象:静态定义或使用 cdev_alloc() 动态分配。
  2. 初始化 cdev:调用 cdev_init(struct cdev *cdev, const struct file_operations *fops),绑定 file_operations。
  3. 设置 owner:手动设置 cdev->owner = THIS_MODULE。
  4. 注册 cdev:调用 cdev_add(struct cdev *cdev, dev_t dev, unsigned int count),将设备加入内核。
关键点:cdev_add() 必须在设备号分配成功之后调用,且一旦调用,设备立即对用户态可见。建议在 cdev_add() 之前完成所有初始化工作,避免设备暴露时状态不一致。

3.3 file_operations接口:用户态与内核的桥梁

struct file_operations 定义了用户态程序通过系统调用操作设备时,内核调用的驱动函数指针。每个成员对应一个系统调用。

成员 系统调用 典型用途
open open() 打开设备,初始化私有数据
release close() 关闭设备,释放资源
read read() 从设备读取数据到用户空间
write write() 从用户空间写入数据到设备
unlocked_ioctl ioctl() 设备特定控制命令(无大内核锁)
mmap mmap() 将设备内存映射到用户空间
poll poll()/select() 查询设备是否可读/写

3.3.1 典型 file_operations 定义示例

static int my_open(struct inode *inode, struct file *filp) {
    // 获取私有数据,初始化硬件
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    // 清理资源
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos) {
    // 从内核缓冲区拷贝数据到用户空间
    // 使用 copy_to_user()
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos) {
    // 从用户空间拷贝数据到内核缓冲区
    // 使用 copy_from_user()
    return count;
}

static long my_ioctl(struct file *filp, unsigned int cmd,
                     unsigned long arg) {
    // 处理设备控制命令
    return 0;
}

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
};
避坑指南:用户空间指针 __user *buf 不能直接解引用,必须使用 copy_to_user() / copy_from_user() 进行安全拷贝。直接访问会导致内核崩溃或安全漏洞。

3.4 完整注册流程与SVG架构图

将设备号分配、cdev初始化、file_operations注册串联起来,形成字符设备驱动的标准初始化流程。

字符设备驱动注册完整流程 字符设备驱动注册完整流程 1. 分配设备号 alloc_chrdev_region() / register_chrdev_region() 2. 初始化cdev结构体 cdev_init() + cdev->owner = THIS_MODULE 3. 注册cdev到内核 cdev_add(cdev, dev, count) 4. 创建设备节点 mknod /dev/mydev c major minor 5. 用户态通过file_operations操作 open / read / write / ioctl / mmap 核心数据结构 • dev_t 设备号 • struct cdev • struct file_operations • struct inode / file

3.5 完整代码模板

以下是一个最小字符设备驱动的初始化与退出代码框架,整合了上述所有要素。

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

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME  "my_class"

static int major;
static struct cdev my_cdev;
static dev_t dev_num;

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 const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
};

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

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

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

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

static void __exit my_exit(void) {
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("Char device unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Minimal char device driver example");
最佳实践:实际项目中,建议在 cdev_add() 之后使用 class_create() 和 device_create() 自动在 /dev 下创建设备节点,避免手动 mknod。这部分内容将在后续章节详细展开。

3.6 常见错误与排查

错误现象 可能原因 解决方案
insmod 失败,提示“Device or resource busy” 设备号冲突或 cdev_add 重复 使用动态分配,检查 /proc/devices
open 设备返回 -ENODEV cdev 未成功注册或设备节点主次号不匹配 检查 cdev_add 返回值,核对 mknod 参数
read/write 返回 -EFAULT 直接解引用用户空间指针 改用 copy_to_user / copy_from_user
rmmod 后设备节点仍存在 未调用 cdev_del 或 device_destroy 确保 exit 函数中正确清理