2. Linux字符设备驱动基础:cdev结构体、设备号注册、file_operations结构体、模块加载卸载模板

字符设备驱动是Linux驱动开发中最基础、最常用的驱动类型。本章将系统讲解字符设备驱动的核心数据结构、设备号管理机制、操作函数集以及标准的模块加载/卸载模板,为后续实现ioctl命令集奠定坚实的底层基础。

2.1 字符设备驱动核心数据结构

Linux内核使用 struct cdev 结构体来描述一个字符设备。每个字符设备在内核中对应一个cdev实例,该结构体包含了设备的主次设备号、操作函数集指针以及设备列表链接信息。

成员 类型 说明
kobj struct kobject 内核对象基类,用于设备生命周期管理
owner struct module * 指向所属模块的指针,通常设为 THIS_MODULE
ops const struct file_operations * 设备操作函数集指针,驱动核心接口
list struct list_head 用于将cdev链接到系统设备链表
dev dev_t 设备号(主设备号+次设备号)
count unsigned int 关联的次设备号数量
关键理解: cdev结构体是字符设备在内核中的"身份证",驱动开发者需要先分配并初始化cdev,然后将其注册到内核,用户空间才能通过设备文件访问该设备。

2.2 设备号注册与分配

设备号由主设备号(major)和次设备号(minor)组成,类型为 dev_t(32位无符号整数)。主设备号标识设备类型对应的驱动,次设备号标识同一驱动管理的不同设备实例。

2.2.1 设备号操作宏

宏/函数 功能
MAJOR(dev_t dev) 从dev_t中提取主设备号
MINOR(dev_t dev) 从dev_t中提取次设备号
MKDEV(int major, int minor) 由主次设备号组合成dev_t
register_chrdev_region() 静态注册指定范围的设备号
alloc_chrdev_region() 动态分配设备号(推荐方式)
unregister_chrdev_region() 注销已注册的设备号

2.2.2 设备号分配策略对比

策略 优点 缺点 适用场景
静态注册 设备号固定,便于用户空间预知 可能与其他驱动冲突,需申请官方编号 广泛使用的标准设备
动态分配 避免冲突,自动分配空闲设备号 设备号不固定,需通过/proc/devices查看 大多数自定义驱动
实践建议: 在开发阶段始终使用动态分配(alloc_chrdev_region),避免与系统中已有驱动产生设备号冲突。产品定型后可根据需要切换为静态注册。

2.3 file_operations结构体详解

struct file_operations 是字符设备驱动的核心接口表,定义了用户空间通过系统调用操作设备文件时内核调用的驱动函数。每个成员对应一个系统调用或VFS操作。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*fsync) (struct file *, loff_t, loff_t, int);
    /* ... 其他可选成员 */
};
成员 系统调用 驱动实现职责
open open() 设备初始化、权限检查、私有数据设置
release close() 资源释放、设备关闭清理
read read() 从设备读取数据到用户空间
write write() 从用户空间写入数据到设备
unlocked_ioctl ioctl() 设备控制命令处理(ioctl核心)
compat_ioctl ioctl() (32位兼容) 64位内核处理32位应用程序ioctl
llseek lseek() 修改文件读写位置
mmap mmap() 设备内存映射到用户空间
ioctl相关重点: 在2.6.36之后的内核中,ioctl 成员已被移除,驱动必须实现 unlocked_ioctl。对于需要支持32位应用程序的64位驱动,还必须实现 compat_ioctl。

2.4 字符设备驱动完整框架SVG

字符设备驱动框架图 字符设备驱动完整框架 用户空间 应用程序 → open() / read() / write() / ioctl() / close() VFS 系统调用层 file_operations 结构体 .open = my_open | .release = my_release | .read = my_read .write = my_write | .unlocked_ioctl = my_ioctl | .llseek = my_llseek .owner = THIS_MODULE cdev 结构体 dev = MKDEV(major, minor) ops = &my_fops | owner = THIS_MODULE 设备号注册 alloc_chrdev_region(&dev, 0, 1, "mydev") cdev_add(&my_cdev, dev, 1) 内核空间 驱动核心

2.5 模块加载/卸载标准模板

以下是一个完整的字符设备驱动模板,集成了cdev初始化、设备号动态分配、file_operations实现以及模块加载/卸载函数。

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

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

static int major;
static struct class *my_class = NULL;
static struct device *my_device = NULL;

/* 设备结构体:封装cdev和私有数据 */
struct my_device_data {
    struct cdev cdev;
    char buffer[256];
    int data_len;
};

static struct my_device_data *dev_data;

/* open 实现 */
static int my_open(struct inode *inode, struct file *file)
{
    struct my_device_data *data;
    data = container_of(inode->i_cdev, struct my_device_data, cdev);
    file->private_data = data;
    return 0;
}

/* release 实现 */
static int my_release(struct inode *inode, struct file *file)
{
    return 0;
}

/* read 实现 */
static ssize_t my_read(struct file *file, char __user *buf,
                       size_t count, loff_t *ppos)
{
    struct my_device_data *data = file->private_data;
    size_t available = data->data_len - *ppos;
    size_t to_read = min(count, available);

    if (to_read == 0)
        return 0;

    if (copy_to_user(buf, data->buffer + *ppos, to_read))
        return -EFAULT;

    *ppos += to_read;
    return to_read;
}

/* write 实现 */
static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t count, loff_t *ppos)
{
    struct my_device_data *data = file->private_data;
    size_t to_write = min(count, sizeof(data->buffer));

    if (copy_from_user(data->buffer, buf, to_write))
        return -EFAULT;

    data->data_len = to_write;
    *ppos = 0;
    return to_write;
}

/* unlocked_ioctl 实现(后续章节重点展开) */
static long my_ioctl(struct file *file, unsigned int cmd,
                     unsigned long arg)
{
    /* ioctl命令处理将在后续章节详细实现 */
    return -ENOTTY;
}

/* file_operations 结构体实例化 */
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,
};

/* 模块初始化函数 */
static int __init my_driver_init(void)
{
    dev_t dev_num;
    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;
    }
    major = MAJOR(dev_num);
    pr_info("Allocated major number: %d\n", major);

    /* 2. 分配设备数据结构 */
    dev_data = kzalloc(sizeof(struct my_device_data), GFP_KERNEL);
    if (!dev_data) {
        unregister_chrdev_region(dev_num, 1);
        return -ENOMEM;
    }

    /* 3. 初始化cdev */
    cdev_init(&dev_data->cdev, &my_fops);
    dev_data->cdev.owner = THIS_MODULE;

    /* 4. 添加cdev到内核 */
    ret = cdev_add(&dev_data->cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        kfree(dev_data);
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    /* 5. 创建设备类和设备节点(自动生成/dev/下设备文件) */
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        cdev_del(&dev_data->cdev);
        kfree(dev_data);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }

    my_device = device_create(my_class, NULL, dev_num,
                              NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        cdev_del(&dev_data->cdev);
        kfree(dev_data);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_device);
    }

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

/* 模块退出函数 */
static void __exit my_driver_exit(void)
{
    dev_t dev_num = MKDEV(major, 0);

    /* 按注册逆序释放资源 */
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&dev_data->cdev);
    kfree(dev_data);
    unregister_chrdev_region(dev_num, 1);

    pr_info("Character device driver unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Character device driver template with ioctl support");
MODULE_VERSION("1.0");

2.6 模块加载/卸载流程SVG

模块加载/卸载流程 模块加载与卸载流程 加载流程 (insmod) ① alloc_chrdev_region() ② kzalloc() 分配设备数据 ③ cdev_init() + cdev_add() ④ class_create() + device_create() 卸载流程 (rmmod) ① device_destroy() ② class_destroy() ③ cdev_del() ④ kfree() + unregister_chrdev_region() 关键原则:卸载时严格按照加载的逆序释放资源,避免内存泄漏或内核崩溃 加载顺序:设备号 → 内存 → cdev → 设备类/设备节点 卸载顺序:设备节点 → 设备类 → cdev → 内存 → 设备号

2.7 关键避坑指南

  • cdev_add 失败处理: 必须在cdev_add之后才能认为设备已注册成功,之前的任何失败都需要清理已分配的设备号。
  • container_of 宏: 在open函数中通过 container_of(inode->i_cdev, struct my_device_data, cdev) 获取设备私有数据结构,这是字符设备驱动的标准做法。
  • copy_to_user / copy_from_user: 内核空间与用户空间数据交换必须使用这两个函数,禁止直接解引用用户空间指针。
  • 模块计数: file_operations 的 owner 字段必须设为 THIS_MODULE,防止设备正在使用时模块被卸载。
  • 设备节点自动创建: 使用 class_create + device_create 组合可以在 /dev/ 下自动生成设备文件,无需手动mknod。
模板使用说明: 上述模板是字符设备驱动的"骨架",后续章节将在此基础上填充ioctl命令处理逻辑、添加锁机制、实现阻塞I/O等高级特性。建议读者将此模板作为所有字符设备驱动的起点。

2.8 本章小结

知识点 核心要点
cdev结构体 字符设备内核描述符,包含设备号、操作函数集、所有者
设备号管理 动态分配优先,使用alloc_chrdev_region避免冲突
file_operations 驱动与VFS的接口表,unlocked_ioctl替代了旧版ioctl
模块模板 加载按序分配资源,卸载逆序释放,错误处理必须完整
设备节点 class_create + device_create 实现自动创建设备文件