2. 实现基本的文件操作接口

任务目标

为虚拟视频设备驱动实现基本的文件操作接口(open、release、read、write),并验证用户空间程序能够成功打开设备节点并进行数据读写。

环境与依赖

  • Linux内核源码树(本案例基于内核 5.15+)
  • 内核编译工具链(gcc、make、linux-headers)
  • 开发板或虚拟机(具备root权限)

核心实现

本案例使用虚拟内存缓冲区模拟视频设备,实现完整的字符设备文件操作。驱动注册后创建 /dev/virtvideo0 设备节点,用户空间通过标准文件I/O接口访问。

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

#define DEVICE_NAME "virtvideo"
#define CLASS_NAME "v4l2virt"
#define BUFFER_SIZE 4096

static int major_num;
static struct class *video_class = NULL;
static struct device *video_device = NULL;

struct virtvideo_dev {
    char *buffer;
    size_t buffer_size;
    struct mutex lock;
    struct cdev cdev;
};

static struct virtvideo_dev *video_dev;

static int dev_open(struct inode *inode, struct file *filp)
{
    struct virtvideo_dev *dev = container_of(inode->i_cdev, 
                                            struct virtvideo_dev, cdev);
    filp->private_data = dev;
    mutex_lock(&dev->lock);
    dev->buffer_size = BUFFER_SIZE;
    memset(dev->buffer, 0, BUFFER_SIZE);
    mutex_unlock(&dev->lock);
    pr_info("virtvideo: device opened\n");
    return 0;
}

static int dev_release(struct inode *inode, struct file *filp)
{
    pr_info("virtvideo: device closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filp, char __user *buf, 
                        size_t count, loff_t *offset)
{
    struct virtvideo_dev *dev = filp->private_data;
    ssize_t ret;
    
    mutex_lock(&dev->lock);
    if (*offset >= dev->buffer_size) {
        mutex_unlock(&dev->lock);
        return 0;
    }
    
    if (count > dev->buffer_size - *offset)
        count = dev->buffer_size - *offset;
    
    if (copy_to_user(buf, dev->buffer + *offset, count)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }
    
    *offset += count;
    ret = count;
    mutex_unlock(&dev->lock);
    return ret;
}

static ssize_t dev_write(struct file *filp, const char __user *buf, 
                         size_t count, loff_t *offset)
{
    struct virtvideo_dev *dev = filp->private_data;
    ssize_t ret;
    
    mutex_lock(&dev->lock);
    if (*offset >= dev->buffer_size) {
        mutex_unlock(&dev->lock);
        return -ENOSPC;
    }
    
    if (count > dev->buffer_size - *offset)
        count = dev->buffer_size - *offset;
    
    if (copy_from_user(dev->buffer + *offset, buf, count)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }
    
    *offset += count;
    ret = count;
    mutex_unlock(&dev->lock);
    return ret;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .release = dev_release,
    .read = dev_read,
    .write = dev_write,
};

static int __init virtvideo_init(void)
{
    dev_t dev_num;
    int result;
    
    result = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (result < 0) {
        pr_err("virtvideo: failed to allocate major number\n");
        return result;
    }
    major_num = MAJOR(dev_num);
    
    video_dev = kzalloc(sizeof(struct virtvideo_dev), GFP_KERNEL);
    if (!video_dev) {
        unregister_chrdev_region(dev_num, 1);
        return -ENOMEM;
    }
    
    video_dev->buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!video_dev->buffer) {
        kfree(video_dev);
        unregister_chrdev_region(dev_num, 1);
        return -ENOMEM;
    }
    
    mutex_init(&video_dev->lock);
    cdev_init(&video_dev->cdev, &fops);
    video_dev->cdev.owner = THIS_MODULE;
    
    result = cdev_add(&video_dev->cdev, dev_num, 1);
    if (result) {
        kfree(video_dev->buffer);
        kfree(video_dev);
        unregister_chrdev_region(dev_num, 1);
        return result;
    }
    
    video_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(video_class)) {
        cdev_del(&video_dev->cdev);
        kfree(video_dev->buffer);
        kfree(video_dev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(video_class);
    }
    
    video_device = device_create(video_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(video_device)) {
        class_destroy(video_class);
        cdev_del(&video_dev->cdev);
        kfree(video_dev->buffer);
        kfree(video_dev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(video_device);
    }
    
    pr_info("virtvideo: driver loaded, major=%d\n", major_num);
    return 0;
}

static void __exit virtvideo_exit(void)
{
    dev_t dev_num = MKDEV(major_num, 0);
    
    device_destroy(video_class, dev_num);
    class_destroy(video_class);
    cdev_del(&video_dev->cdev);
    kfree(video_dev->buffer);
    kfree(video_dev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("virtvideo: driver unloaded\n");
}

module_init(virtvideo_init);
module_exit(virtvideo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("V4L2 Course");
MODULE_DESCRIPTION("Virtual video device with basic file operations");

编译与加载

# Makefile
obj-m += virtvideo.o

# 编译命令
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载驱动
sudo insmod virtvideo.ko

# 验证设备节点
ls -l /dev/virtvideo0

用户空间验证

# 编译测试程序
cat > test_virtvideo.c << 'EOF'
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("/dev/virtvideo0", O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return 1;
    }
    
    char write_buf[] = "V4L2 file operation test";
    char read_buf[64] = {0};
    
    ssize_t wret = write(fd, write_buf, strlen(write_buf) + 1);
    if (wret < 0) {
        perror("write failed");
        close(fd);
        return 1;
    }
    
    lseek(fd, 0, SEEK_SET);
    ssize_t rret = read(fd, read_buf, sizeof(read_buf));
    if (rret < 0) {
        perror("read failed");
        close(fd);
        return 1;
    }
    
    printf("Read %zd bytes: %s\n", rret, read_buf);
    close(fd);
    return 0;
}
EOF

gcc test_virtvideo.c -o test_virtvideo
sudo ./test_virtvideo

验证判定标准

  • 加载成功:dmesg | tail 显示 virtvideo: driver loaded,且 /dev/virtvideo0 节点存在
  • 打开成功:测试程序执行后输出 Read 25 bytes: V4L2 file operation test,无错误信息
  • 卸载正常:sudo rmmod virtvideo 后设备节点消失,dmesg 显示 driver unloaded

常见问题

  • 设备节点未创建:确认 udev 规则或手动执行 mknod /dev/virtvideo0 c $(grep virtvideo /proc/devices | awk '{print $1}') 0
  • 权限拒绝:使用 sudo chmod 666 /dev/virtvideo0 或加入 video 组
  • 模块加载失败:检查内核版本与头文件是否匹配,dmesg 查看具体错误