4. 字符设备驱动基础:cdev结构体、file_operations实现、设备号分配、miscdevice框架在摄像头驱动中的应用
摄像头在 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(操作函数表)。
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");
}
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() |