4. 内核空间与用户空间数据交互

在 Linux 驱动开发中,内核空间与用户空间的数据交互是核心环节。由于内核拥有独立的地址空间和更高的权限等级,用户程序不能直接访问内核内存,必须通过系统调用接口完成数据拷贝。本章详细讲解 copy_from_user / copy_to_user 函数的使用规范,以及 ioctl 命令码的定义与实现。

4.1 数据拷贝函数:copy_from_user 与 copy_to_user

这两个函数是内核提供的安全数据拷贝接口,用于在用户空间缓冲区与内核空间缓冲区之间传输数据。它们会检查用户空间地址的有效性,并在拷贝过程中处理缺页异常,避免内核崩溃。

4.1.1 函数原型与参数

函数 原型 返回值
copy_from_user unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) 返回未拷贝成功的字节数,成功返回 0
copy_to_user unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) 返回未拷贝成功的字节数,成功返回 0
  • to:目标缓冲区指针(内核空间或用户空间)。
  • from:源缓冲区指针(用户空间或内核空间)。
  • n:要拷贝的字节数。
  • __user:标记指针来自用户空间,用于静态检查。

4.1.2 使用规范与注意事项

  • 必须检查返回值:非零返回值表示拷贝未完全完成,应返回 -EFAULT 给用户程序。
  • 禁止直接使用 memcpy:用户空间地址可能无效或未映射,直接访问会导致内核 oops。
  • 睡眠安全:这两个函数在缺页时可能睡眠,不能在原子上下文(如中断处理函数、自旋锁持有期间)中调用。
  • 地址范围验证:内核会自动检查用户空间地址是否在合法范围内,但驱动仍需确保缓冲区大小合理。
典型错误示例: 在中断处理函数中调用 copy_to_user,会导致内核调度异常。正确做法是将数据放入内核缓冲区,通过工作队列或等待队列在进程上下文中完成拷贝。

4.1.3 代码示例:字符设备读写中的拷贝

// 内核驱动中的 read 实现
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    char *kernel_buf = filp->private_data;
    size_t data_len = strlen(kernel_buf);

    if (*off >= data_len)
        return 0;

    if (len > data_len - *off)
        len = data_len - *off;

    if (copy_to_user(buf, kernel_buf + *off, len)) {
        pr_err("copy_to_user failed\n");
        return -EFAULT;
    }

    *off += len;
    return len;
}

// 内核驱动中的 write 实现
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
    char *kernel_buf = filp->private_data;

    if (len > MAX_BUF_SIZE)
        return -ENOMEM;

    memset(kernel_buf, 0, MAX_BUF_SIZE);
    if (copy_from_user(kernel_buf, buf, len)) {
        pr_err("copy_from_user failed\n");
        return -EFAULT;
    }

    *off = len;
    return len;
}

4.2 ioctl 命令码定义与实现

ioctl(Input/Output Control)是用户程序与设备驱动进行控制交互的主要接口。通过 ioctl,用户程序可以发送自定义命令,驱动根据命令执行特定操作,如设置参数、获取状态、启动硬件等。

4.2.1 命令码的组成结构

Linux 内核推荐使用标准化的命令码格式,通过宏定义生成唯一且可自解释的命令码。命令码是一个 32 位整数,由以下字段组成:

位域 宽度 含义
方向(dir) 2 bits 数据传输方向:_IOC_NONE(0)、_IOC_READ(1)、_IOC_WRITE(2)、_IOC_READ|_IOC_WRITE(3)
类型(type) 8 bits 设备类型标识符,通常使用 ASCII 字符,如 'M'、'D'
序号(nr) 8 bits 命令序号,同一类型下唯一
大小(size) 14 bits 传输数据的大小(字节数)

4.2.2 辅助宏定义

内核头文件 <linux/ioctl.h> 提供了以下宏来生成和解析命令码:

// 生成命令码
_IOC(dir, type, nr, size)       // 通用宏
_IO(type, nr)                   // 无数据传输
_IOR(type, nr, size)            // 读数据(从设备到用户)
_IOW(type, nr, size)            // 写数据(从用户到设备)
_IOWR(type, nr, size)           // 双向数据传输

// 解析命令码
_IOC_DIR(cmd)                   // 获取方向
_IOC_TYPE(cmd)                  // 获取类型
_IOC_NR(cmd)                    // 获取序号
_IOC_SIZE(cmd)                  // 获取数据大小

4.2.3 命令码定义示例

// 定义设备类型为 'M'
#define MY_DEVICE_TYPE 'M'

// 定义命令
#define MY_CMD_GET_STATUS   _IOR(MY_DEVICE_TYPE, 0, int)   // 读取状态
#define MY_CMD_SET_VALUE    _IOW(MY_DEVICE_TYPE, 1, int)   // 设置值
#define MY_CMD_RESET        _IO(MY_DEVICE_TYPE, 2)         // 复位设备
#define MY_CMD_READ_BUF     _IOR(MY_DEVICE_TYPE, 3, 256)   // 读取缓冲区(256字节)
#define MY_CMD_WRITE_BUF    _IOW(MY_DEVICE_TYPE, 4, 256)   // 写入缓冲区(256字节)

4.2.4 ioctl 函数实现模板

static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    int value;
    char *kernel_buf = filp->private_data;

    // 检查命令类型和序号是否合法
    if (_IOC_TYPE(cmd) != MY_DEVICE_TYPE) {
        pr_err("Invalid ioctl type\n");
        return -ENOTTY;
    }

    // 检查数据访问权限(可选,内核会自动检查)
    if (_IOC_DIR(cmd) & _IOC_READ)
        ret = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
    else if (_IOC_DIR(cmd) & _IOC_WRITE)
        ret = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
    if (ret)
        return -EFAULT;

    switch (cmd) {
    case MY_CMD_GET_STATUS:
        value = get_device_status();
        if (copy_to_user((int __user *)arg, &value, sizeof(value)))
            return -EFAULT;
        break;

    case MY_CMD_SET_VALUE:
        if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
            return -EFAULT;
        set_device_value(value);
        break;

    case MY_CMD_RESET:
        reset_device();
        break;

    case MY_CMD_READ_BUF:
        if (copy_to_user((char __user *)arg, kernel_buf, 256))
            return -EFAULT;
        break;

    case MY_CMD_WRITE_BUF:
        if (copy_from_user(kernel_buf, (char __user *)arg, 256))
            return -EFAULT;
        break;

    default:
        return -ENOTTY;  // 未知命令
    }

    return 0;
}
最佳实践: 在 ioctl 实现中,始终使用 switch 语句处理命令,并在 default 分支返回 -ENOTTY。对于涉及数据拷贝的命令,务必使用 copy_from_user / copy_to_user,不要直接解引用用户空间指针。

4.3 内核空间与用户空间数据交互流程

下图展示了用户程序通过系统调用与内核驱动进行数据交互的完整路径:

用户空间与内核空间数据交互流程 用户空间 应用程序调用 open() / read() / write() / ioctl() 传递用户空间缓冲区指针(__user 标记) 系统调用层(VFS / 内核入口) 检查文件描述符、分发到驱动 file_operations 内核驱动层 调用 copy_from_user() / copy_to_user() 安全拷贝数据 执行硬件操作、更新内核缓冲区、返回结果 硬件设备层 寄存器读写、中断处理、DMA 传输 返回结果(成功/错误码) 关键点:地址检查 + 异常处理

4.4 常见错误与调试技巧

  • -EFAULT(Bad address):用户空间指针无效或未映射,检查 copy_from_user / copy_to_user 返回值。
  • -ENOTTY(Inappropriate ioctl for device):命令码类型或序号不匹配,检查 _IOC_TYPE 和 _IOC_NR 是否与驱动定义一致。
  • -ENOMEM(Cannot allocate memory):内核内存分配失败,检查缓冲区大小是否合理。
  • 内核 oops / 死机:通常由在原子上下文中调用可能睡眠的函数引起,检查是否在中断或自旋锁中调用了 copy_to_user。
安全警告: 永远不要信任用户空间传入的指针或长度值。即使 copy_from_user 会做地址检查,驱动仍需验证数据内容的合法性,避免缓冲区溢出或越界访问。

4.5 本章小结

  • copy_from_user 和 copy_to_user 是内核空间与用户空间数据交互的唯一安全途径,必须检查返回值。
  • ioctl 命令码使用标准宏定义,确保唯一性和可读性;驱动实现中应使用 switch 分支处理。
  • 数据交互流程遵循:用户空间调用 → 系统调用 → 驱动拷贝 → 硬件操作 → 结果返回。
  • 调试时重点关注地址有效性、命令码匹配和原子上下文限制。