4. 字符设备驱动基础

摄像头在 Linux 系统中通常被抽象为字符设备。用户空间通过标准的文件操作接口(open、read、write、ioctl、mmap、poll)与摄像头硬件交互。本章深入剖析字符设备驱动的核心组件:cdev 结构体、file_operations、设备号分配以及miscdevice 框架,并展示它们在摄像头驱动中的具体应用模式。

4.1 字符设备驱动核心模型

Linux 内核使用 struct cdev 表示一个字符设备。每个 cdev 实例绑定一组操作函数(file_operations)和一个设备号(dev_t)。当用户空间打开设备文件时,VFS 通过设备号找到对应的 cdev,进而调用其 file_operations 中的方法。

核心概念:字符设备驱动 = 设备号(标识设备) + cdev(内核对象) + file_operations(操作函数表)。
字符设备驱动核心架构 用户空间应用程序 VFS 虚拟文件系统 设备号 dev_t 查找 struct cdev owner / ops / dev struct file_operations .open = cam_open .ioctl = cam_ioctl .mmap = cam_mmap 摄像头硬件(I2C / CSI / USB)

4.2 cdev 结构体详解

struct cdev 是内核用于管理字符设备的核心数据结构,定义在 <linux/cdev.h> 中。驱动开发者需要分配、初始化并注册 cdev 对象。

成员 类型 说明
kobj struct kobject 内核对象基类,用于设备生命周期管理和 sysfs 导出
owner struct module * 指向所属模块,通常设为 THIS_MODULE
ops const struct file_operations * 指向设备操作函数表
list struct list_head 用于将 cdev 链接到系统设备链表
dev dev_t 设备号(主设备号 + 次设备号)
count unsigned int 关联的次设备号数量
最佳实践:在摄像头驱动中,通常将 cdev 嵌入到一个自定义的摄像头设备结构体中,而不是单独分配。这样可以保持数据关联性,便于在 file_operations 回调中通过 container_of 获取摄像头私有数据。

4.2.1 cdev 初始化与注册流程

// 1. 分配 cdev 对象(静态或动态)
struct cdev cam_cdev;

// 2. 初始化 cdev,绑定 file_operations
cdev_init(&cam_cdev, &cam_fops);
cam_cdev.owner = THIS_MODULE;

// 3. 注册 cdev 到内核
//    在设备号已分配的前提下
int ret = cdev_add(&cam_cdev, dev_num, 1);
if (ret < 0) {
    printk(KERN_ERR "Failed to add cdev\n");
    return ret;
}

// 4. 卸载时删除 cdev
cdev_del(&cam_cdev);
注意:cdev_add() 调用后设备立即生效,用户空间可能立刻打开设备。因此,必须在 cdev_add() 之前完成所有硬件初始化,避免在 file_operations 回调中访问未就绪的硬件资源。

4.3 file_operations 实现

struct file_operations 是字符设备驱动的核心接口表。摄像头驱动通常需要实现以下关键方法:

方法 摄像头驱动中的典型实现 说明
open 上电、初始化传感器、分配缓冲区 每次打开设备时执行,需支持多开计数
release 停止流、释放缓冲区、下电 最后一个关闭时执行硬件复位
ioctl 设置分辨率、帧率、曝光、增益 摄像头控制的核心入口,使用 VIDIOC_* 命令
read 从缓冲区拷贝一帧图像数据 简单实现,但效率较低,推荐 mmap
mmap 将 DMA 缓冲区映射到用户空间 零拷贝传输,高性能摄像头驱动必备
poll 等待新帧就绪 支持 select/poll/epoll 非阻塞 I/O

4.3.1 摄像头驱动 file_operations 示例

static int cam_open(struct inode *inode, struct file *filp)
{
    struct cam_device *cam = container_of(inode->i_cdev,
                                          struct cam_device, cdev);
    filp->private_data = cam;

    // 增加引用计数,防止模块卸载
    if (!try_module_get(THIS_MODULE))
        return -ENODEV;

    // 硬件初始化
    mutex_lock(&cam->lock);
    if (cam->open_count == 0) {
        cam_sensor_power_on(cam);
        cam_sensor_init(cam);
    }
    cam->open_count++;
    mutex_unlock(&cam->lock);

    return 0;
}

static long cam_ioctl(struct file *filp, unsigned int cmd,
                      unsigned long arg)
{
    struct cam_device *cam = filp->private_data;

    switch (cmd) {
    case VIDIOC_S_FMT:
        // 设置图像格式
        return cam_set_format(cam, arg);
    case VIDIOC_STREAMON:
        // 启动视频流
        return cam_start_streaming(cam);
    case VIDIOC_STREAMOFF:
        // 停止视频流
        return cam_stop_streaming(cam);
    default:
        return -ENOTTY;
    }
}

static const struct file_operations cam_fops = {
    .owner          = THIS_MODULE,
    .open           = cam_open,
    .release        = cam_release,
    .unlocked_ioctl = cam_ioctl,
    .mmap           = cam_mmap,
    .poll           = cam_poll,
};

4.4 设备号分配

设备号是字符设备在内核中的唯一标识,由主设备号(major)和次设备号(minor)组成。Linux 提供两种分配方式:

分配方式 函数 适用场景 优点 缺点
静态分配 register_chrdev_region() 已知主设备号(如摄像头常用 81) 设备号固定,便于udev规则 可能冲突,需向Linux维护者申请
动态分配 alloc_chrdev_region() 大多数驱动,包括摄像头 避免冲突,自动分配 设备号不固定,需配合udev

4.4.1 动态分配设备号示例

dev_t dev_num;
int major, minor;

// 动态分配一个设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "camera");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate chrdev region\n");
    return ret;
}

major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk(KERN_INFO "Allocated device number: %d:%d\n", major, minor);

// 卸载时释放
unregister_chrdev_region(dev_num, 1);
摄像头驱动建议:使用动态分配设备号,并在驱动加载时通过 printk 或 sysfs 输出设备号。用户空间通过 udev 规则自动创建 /dev/videoX 节点,无需手动 mknod。

4.5 miscdevice 框架

miscdevice(杂项设备)是内核提供的一种简化字符设备注册的框架。它自动处理 cdev 的分配、设备号的分配(主设备号固定为 10)以及设备文件的创建。对于简单的摄像头控制接口(如仅需 ioctl 进行配置),miscdevice 是理想选择。

4.5.1 miscdevice 结构体

struct miscdevice {
    int minor;              // 次设备号,MISC_DYNAMIC_MINOR 表示动态分配
    const char *name;       // 设备名称,出现在 /dev/ 和 /sys/class/misc/
    const struct file_operations *fops; // 操作函数表
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const struct attribute_group **groups;
    const char *nodename;
    umode_t mode;
};

4.5.2 使用 miscdevice 实现摄像头控制接口

static const struct file_operations cam_misc_fops = {
    .owner          = THIS_MODULE,
    .open           = cam_misc_open,
    .release        = cam_misc_release,
    .unlocked_ioctl = cam_misc_ioctl,
};

static struct miscdevice cam_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "camera_ctrl",
    .fops  = &cam_misc_fops,
};

static int __init cam_init(void)
{
    int ret;

    // 注册 misc 设备
    ret = misc_register(&cam_misc_device);
    if (ret) {
        pr_err("Failed to register misc device\n");
        return ret;
    }

    pr_info("Camera control device registered as /dev/camera_ctrl\n");
    return 0;
}

static void __exit cam_exit(void)
{
    misc_deregister(&cam_misc_device);
    pr_info("Camera control device unregistered\n");
}
miscdevice 框架 vs 标准 cdev 注册流程对比 标准 cdev 注册流程 1. alloc_chrdev_region() 2. cdev_init() 3. cdev_add() 4. class_create() 5. device_create() 6. 手动创建设备节点 需 5~6 步,代码量大 miscdevice 注册流程 1. 填充 miscdevice 结构体 2. misc_register() └─ 自动完成: • 分配设备号 (major=10) • cdev_init + cdev_add • 创建 /dev/name 节点 仅需 2 步,自动完成

4.6 摄像头驱动中的综合应用

在实际摄像头驱动中,通常采用分层设计:

  • 主设备层:使用标准 cdev + file_operations,实现完整的 V4L2 接口(/dev/videoX),提供 mmap、poll、ioctl 等高性能操作。
  • 控制设备层:使用 miscdevice 框架,实现简单的控制接口(/dev/camera_ctrl),用于调试、固件升级或私有协议。

4.6.1 摄像头驱动设备注册完整示例

struct cam_device {
    struct cdev cdev;           // 标准字符设备
    struct miscdevice misc;     // 杂项控制设备
    dev_t dev_num;
    struct class *class;
    struct device *device;
    // ... 摄像头私有数据
};

static int __init cam_probe(struct platform_device *pdev)
{
    struct cam_device *cam;
    int ret;

    cam = kzalloc(sizeof(*cam), GFP_KERNEL);
    if (!cam)
        return -ENOMEM;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&cam->dev_num, 0, 1, "video");
    if (ret)
        goto err_free;

    // 2. 初始化 cdev
    cdev_init(&cam->cdev, &cam_v4l2_fops);
    cam->cdev.owner = THIS_MODULE;

    // 3. 注册 cdev
    ret = cdev_add(&cam->cdev, cam->dev_num, 1);
    if (ret)
        goto err_unregister;

    // 4. 创建 device class 和节点
    cam->class = class_create(THIS_MODULE, "camera");
    cam->device = device_create(cam->class, NULL,
                                cam->dev_num, NULL, "video%d", 0);

    // 5. 注册 misc 控制设备
    cam->misc.minor = MISC_DYNAMIC_MINOR;
    cam->misc.name  = "camera_ctrl";
    cam->misc.fops  = &cam_ctrl_fops;
    ret = misc_register(&cam->misc);
    if (ret)
        goto err_device;

    platform_set_drvdata(pdev, cam);
    return 0;

err_device:
    device_destroy(cam->class, cam->dev_num);
    class_destroy(cam->class);
    cdev_del(&cam->cdev);
err_unregister:
    unregister_chrdev_region(cam->dev_num, 1);
err_free:
    kfree(cam);
    return ret;
}
设计要点:主设备 /dev/video0 使用 mmap 实现零拷贝帧传输,控制设备 /dev/camera_ctrl 使用 ioctl 实现参数配置。两者共享同一个摄像头硬件,通过互斥锁保护寄存器访问。

4.7 本章小结

组件 摄像头驱动中的角色 关键函数/宏
cdev 字符设备内核对象,绑定设备号和操作函数 cdev_init(), cdev_add(), cdev_del()
file_operations 定义用户空间可调用的所有操作 .open, .release, .unlocked_ioctl, .mmap, .poll
设备号 设备唯一标识,用于 VFS 查找 alloc_chrdev_region(), MAJOR(), MINOR()
miscdevice 简化控制接口注册,自动创建设备节点 misc_register(), misc_deregister()