4. 字符设备驱动实战:实现 open/read/write/ioctl/release 接口

本章将带领你从零实现一个完整的字符设备驱动,重点掌握 file_operations 结构体中核心接口的注册与实现,并深入理解用户态与内核态之间的数据拷贝机制。通过本章实战,你将能够独立编写具备基本交互能力的字符设备驱动。

4.1 字符设备驱动核心框架回顾

字符设备驱动的核心是围绕 struct file_operations 展开的。该结构体定义了驱动能够响应的系统调用接口。本章将实现以下五个最常用的接口:

接口 系统调用 触发时机 典型用途
open open() 用户程序打开设备文件时 初始化设备、递增引用计数、分配资源
release close() 用户程序关闭设备文件时 释放资源、递减引用计数、关闭硬件
read read() 用户程序读取设备数据时 从内核缓冲区拷贝数据到用户空间
write write() 用户程序写入设备数据时 从用户空间拷贝数据到内核缓冲区
unlocked_ioctl ioctl() 用户程序发送控制命令时 设备参数配置、状态查询、特殊操作
关键点: 在 Linux 5.x 及以上内核中,ioctl 接口推荐使用 unlocked_ioctl,它不需要调用大内核锁(BKL),性能更好且是未来的标准方向。

4.2 用户态与内核态数据拷贝机制

用户空间和内核空间运行在不同的虚拟地址空间中,驱动程序不能直接解引用用户空间指针。必须使用内核提供的专用拷贝函数,这些函数会进行地址合法性检查并处理缺页异常。

4.2.1 核心拷贝函数

函数 方向 原型 返回值
copy_from_user 用户 → 内核 unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) 0 成功;非0 未拷贝成功的字节数
copy_to_user 内核 → 用户 unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) 0 成功;非0 未拷贝成功的字节数
安全红线: 永远不要使用 memcpy、sprintf 等标准库函数直接操作用户空间指针。这会导致内核崩溃或严重的安全漏洞。必须使用 copy_from_user / copy_to_user 系列函数。

4.2.2 数据拷贝流程示意图

下面的 SVG 图展示了用户态调用 read() 和 write() 时,数据在内核态与用户态之间的流动路径:

用户态与内核态数据拷贝流程 用户态与内核态数据拷贝流程 用户空间 (User Space) 应用程序 用户缓冲区 (buf) 虚拟地址,不可直接访问 内核空间 (Kernel Space) 字符设备驱动 内核缓冲区 (kern_buf) 物理内存映射,安全可访问 write() copy_from_user read() copy_to_user ⚠ 内核驱动必须使用 copy_from_user / copy_to_user 进行数据搬运 这些函数会检查地址合法性、处理缺页异常,保证系统安全稳定 系统调用层 (VFS):open / read / write / ioctl / release

4.3 驱动代码实战实现

下面是一个完整的字符设备驱动模板,实现了 open、release、read、write 和 unlocked_ioctl 接口。代码中包含了详细的注释和错误处理。

4.3.1 驱动头文件与全局变量

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>   // copy_from_user, copy_to_user
#include <linux/slab.h>       // kmalloc, kfree
#include <linux/ioctl.h>

#define DEVICE_NAME "mychardev"
#define CLASS_NAME  "mychar"
#define BUFFER_SIZE 1024

static int major_num;
static struct class *mychar_class = NULL;
static struct device *mychar_device = NULL;
static struct cdev my_cdev;

// 设备结构体
struct mychar_dev {
    char *buffer;
    size_t size;
    struct mutex lock;
};

static struct mychar_dev *dev_ptr;

4.3.2 open 与 release 接口

static int mychar_open(struct inode *inodep, struct file *filep)
{
    struct mychar_dev *dev = container_of(inodep->i_cdev,
                                          struct mychar_dev, cdev);
    filep->private_data = dev;
    mutex_lock(&dev->lock);
    dev->size = 0;  // 每次打开重置缓冲区
    mutex_unlock(&dev->lock);
    printk(KERN_INFO "mychardev: device opened\n");
    return 0;
}

static int mychar_release(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "mychardev: device closed\n");
    return 0;
}

4.3.3 read 与 write 接口(核心数据拷贝)

static ssize_t mychar_read(struct file *filep, char __user *buffer,
                           size_t len, loff_t *offset)
{
    struct mychar_dev *dev = filep->private_data;
    ssize_t retval;

    mutex_lock(&dev->lock);

    // 检查读取位置是否超出当前数据长度
    if (*offset >= dev->size) {
        mutex_unlock(&dev->lock);
        return 0;  // EOF
    }

    // 计算实际可读取的字节数
    if (*offset + len > dev->size)
        len = dev->size - *offset;

    // 从内核缓冲区拷贝到用户空间
    if (copy_to_user(buffer, dev->buffer + *offset, len)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }

    *offset += len;
    retval = len;

    mutex_unlock(&dev->lock);
    printk(KERN_INFO "mychardev: read %zu bytes\n", len);
    return retval;
}

static ssize_t mychar_write(struct file *filep, const char __user *buffer,
                            size_t len, loff_t *offset)
{
    struct mychar_dev *dev = filep->private_data;
    ssize_t retval;

    mutex_lock(&dev->lock);

    // 检查缓冲区剩余空间
    if (*offset + len > BUFFER_SIZE) {
        len = BUFFER_SIZE - *offset;
        if (len == 0) {
            mutex_unlock(&dev->lock);
            return -ENOSPC;  // 设备已满
        }
    }

    // 从用户空间拷贝到内核缓冲区
    if (copy_from_user(dev->buffer + *offset, buffer, len)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }

    *offset += len;
    dev->size = *offset;
    retval = len;

    mutex_unlock(&dev->lock);
    printk(KERN_INFO "mychardev: written %zu bytes\n", len);
    return retval;
}

4.3.4 unlocked_ioctl 接口

// 定义 ioctl 命令
#define MYCHAR_IOCTL_MAGIC 'k'
#define MYCHAR_IOCTL_RESET   _IO(MYCHAR_IOCTL_MAGIC, 0)
#define MYCHAR_IOCTL_GETSIZE _IOR(MYCHAR_IOCTL_MAGIC, 1, int)
#define MYCHAR_IOCTL_SETSIZE _IOW(MYCHAR_IOCTL_MAGIC, 2, int)

static long mychar_ioctl(struct file *filep, unsigned int cmd,
                         unsigned long arg)
{
    struct mychar_dev *dev = filep->private_data;
    int tmp;

    switch (cmd) {
    case MYCHAR_IOCTL_RESET:
        mutex_lock(&dev->lock);
        memset(dev->buffer, 0, BUFFER_SIZE);
        dev->size = 0;
        mutex_unlock(&dev->lock);
        printk(KERN_INFO "mychardev: buffer reset via ioctl\n");
        break;

    case MYCHAR_IOCTL_GETSIZE:
        tmp = dev->size;
        if (copy_to_user((int __user *)arg, &tmp, sizeof(tmp)))
            return -EFAULT;
        break;

    case MYCHAR_IOCTL_SETSIZE:
        if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
            return -EFAULT;
        if (tmp < 0 || tmp > BUFFER_SIZE)
            return -EINVAL;
        mutex_lock(&dev->lock);
        dev->size = tmp;
        mutex_unlock(&dev->lock);
        break;

    default:
        return -ENOTTY;  // 不支持的 ioctl 命令
    }
    return 0;
}

4.3.5 file_operations 结构体与模块初始化

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = mychar_open,
    .release = mychar_release,
    .read    = mychar_read,
    .write   = mychar_write,
    .unlocked_ioctl = mychar_ioctl,
};

static int __init mychar_init(void)
{
    // 分配设备结构体内存
    dev_ptr = kmalloc(sizeof(struct mychar_dev), GFP_KERNEL);
    if (!dev_ptr)
        return -ENOMEM;

    // 分配内核缓冲区
    dev_ptr->buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!dev_ptr->buffer) {
        kfree(dev_ptr);
        return -ENOMEM;
    }
    memset(dev_ptr->buffer, 0, BUFFER_SIZE);
    dev_ptr->size = 0;
    mutex_init(&dev_ptr->lock);

    // 注册字符设备
    // ... (cdev_init, cdev_add, class_create, device_create 等标准流程)
    printk(KERN_INFO "mychardev: driver loaded\n");
    return 0;
}

static void __exit mychar_exit(void)
{
    // 注销设备、释放资源
    if (dev_ptr) {
        kfree(dev_ptr->buffer);
        kfree(dev_ptr);
    }
    printk(KERN_INFO "mychardev: driver unloaded\n");
}

module_init(mychar_init);
module_exit(mychar_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A complete char device driver example");

4.4 用户态测试程序

编写一个简单的用户态程序来验证驱动的所有接口:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define DEVICE_PATH "/dev/mychardev"
#define MYCHAR_IOCTL_MAGIC 'k'
#define MYCHAR_IOCTL_RESET   _IO(MYCHAR_IOCTL_MAGIC, 0)
#define MYCHAR_IOCTL_GETSIZE _IOR(MYCHAR_IOCTL_MAGIC, 1, int)
#define MYCHAR_IOCTL_SETSIZE _IOW(MYCHAR_IOCTL_MAGIC, 2, int)

int main() {
    int fd;
    char write_buf[] = "Hello from userspace!";
    char read_buf[128] = {0};
    int size;

    // 1. 打开设备
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    // 2. 写入数据
    write(fd, write_buf, strlen(write_buf));

    // 3. 读取数据
    lseek(fd, 0, SEEK_SET);
    read(fd, read_buf, sizeof(read_buf));
    printf("Read from device: %s\n", read_buf);

    // 4. 通过 ioctl 获取当前数据大小
    ioctl(fd, MYCHAR_IOCTL_GETSIZE, &size);
    printf("Current buffer size: %d\n", size);

    // 5. 通过 ioctl 重置缓冲区
    ioctl(fd, MYCHAR_IOCTL_RESET);

    // 6. 关闭设备
    close(fd);
    return 0;
}

4.5 常见错误与调试技巧

错误现象 可能原因 解决方案
copy_from_user 返回非0 用户空间地址无效或不可读 检查传入的 __user 指针是否合法;使用 access_ok() 预检查
read 返回 -EFAULT copy_to_user 失败 确认用户缓冲区可写;检查 len 参数是否过大
ioctl 返回 -ENOTTY 命令码不匹配或未注册 检查 _IO/_IOR/_IOW 宏生成的命令码是否与用户态一致
内核崩溃(Oops) 直接解引用用户空间指针 确保所有用户数据访问都通过 copy_from_user / copy_to_user
数据读取不全 未正确处理 loff_t *offset 在 read 和 write 中正确更新 *offset 值
调试建议: 在开发阶段,可以在 read 和 write 函数中加入 printk 打印 len、offset 和拷贝返回值,帮助定位数据流问题。使用 dmesg 查看内核日志。

4.6 本章小结

  • 字符设备驱动的核心是填充 struct file_operations 结构体,实现 open、release、read、write 和 unlocked_ioctl 接口。
  • 用户态与内核态之间的数据拷贝必须使用 copy_from_user 和 copy_to_user,禁止直接解引用用户空间指针。
  • ioctl 命令码使用 _IO/_IOR/_IOW/_IOWR 宏生成,确保用户态和内核态的命令码一致。
  • 驱动中必须使用互斥锁(mutex)保护共享数据,防止并发访问导致的数据竞争。
  • 错误处理是驱动开发的关键,每个系统调用接口都应返回适当的错误码(如 -EFAULT、-ENOMEM、-ENOTTY)。