2. ioctl命令码编码规范

ioctl 命令码是用户空间与内核驱动之间传递控制指令的唯一标识。Linux 内核定义了一套严格的编码规范,将命令码分解为 幻数 (magic number)、序号 (sequence number)、数据传输方向 和 数据大小 四个字段。遵循这套规范可以避免命令码冲突,并让内核自动校验参数合法性。

2.1 命令码的 32 位结构

Linux 内核将 ioctl 命令码定义为 unsigned int (32 位),其内部布局如下:

命令码位域布局 (从高位到低位)
位 31–30: 数据传输方向 (2 位)
位 29–16: 数据大小 (14 位)
位 15–8:  幻数 (8 位)
位 7–0:   序号 (8 位)

内核通过 include/uapi/asm-generic/ioctl.h 中的宏来构造和解析命令码。下表列出了四个核心构造宏:

宏 方向 数据流向 典型用途
_IO(type, nr) 无数据 — 纯控制命令 (如复位、使能)
_IOR(type, nr, size) 读 内核 → 用户 读取设备状态、寄存器值
_IOW(type, nr, size) 写 用户 → 内核 写入配置参数、控制字
_IOWR(type, nr, size) 读写 双向传输 交换结构体数据 (如配置+状态)
编码提示: size 参数必须使用 sizeof(type) 传入,内核在 copy_from_user/copy_to_user 前会校验缓冲区大小是否匹配。

2.2 幻数 (Magic Number) 设计原则

幻数是一个 8 位值 (0–255),用于标识设备驱动类型。内核通过幻数区分不同驱动,防止命令码交叉冲突。

  • 唯一性: 每个驱动选择一个未被其他驱动占用的幻数。内核文档 Documentation/ioctl/ioctl-number.rst 记录了已分配的幻数。
  • 可读性: 通常使用 ASCII 字符作为幻数,例如 'M' (0x4D)、'p' (0x70),便于在代码中识别。
  • 静态定义: 幻数在驱动头文件中以 #define 常量定义,编译期固定。
常见错误: 使用 0 或 0xFF 作为幻数。这些值已被内核通用 ioctl 命令占用,会导致命令码冲突和不可预测的行为。

2.3 序号 (Sequence Number) 设计规范

序号是一个 8 位值 (0–255),在同一幻数下唯一标识一个 ioctl 命令。设计时需注意:

  • 递增分配: 从 0 开始递增,避免跳跃过大导致管理混乱。
  • 预留保留: 为未来扩展预留部分序号 (如 0xF0–0xFF)。
  • 分组管理: 按功能模块分配序号区间,例如 0–31 用于控制命令,32–63 用于配置命令。

2.4 命令码定义实战示例

以下是一个字符设备驱动 mydev 的命令码定义,幻数选择 'M' (0x4D):

/* mydev_ioctl.h */
#ifndef __MYDEV_IOCTL_H__
#define __MYDEV_IOCTL_H__

#include <linux/ioctl.h>

#define MYDEV_MAGIC  'M'

#define MYDEV_CMD_RESET       _IO(MYDEV_MAGIC, 0)
#define MYDEV_CMD_GET_STATUS  _IOR(MYDEV_MAGIC, 1, int)
#define MYDEV_CMD_SET_CONFIG  _IOW(MYDEV_MAGIC, 2, struct mydev_config)
#define MYDEV_CMD_EXCHANGE    _IOWR(MYDEV_MAGIC, 3, struct mydev_data)

#define MYDEV_CMD_MAX_NR      3

#endif /* __MYDEV_IOCTL_H__ */

内核在 unlocked_ioctl 中通过 _IOC_TYPE(cmd) 和 _IOC_NR(cmd) 宏提取幻数和序号进行匹配:

static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    if (_IOC_TYPE(cmd) != MYDEV_MAGIC) {
        pr_err("ioctl: invalid magic number\n");
        return -ENOTTY;
    }
    if (_IOC_NR(cmd) > MYDEV_CMD_MAX_NR) {
        pr_err("ioctl: invalid sequence number\n");
        return -ENOTTY;
    }

    switch (cmd) {
    case MYDEV_CMD_RESET:
        /* 无数据拷贝 */
        break;
    case MYDEV_CMD_GET_STATUS: {
        int status;
        /* 读取状态 */
        if (copy_to_user((int __user *)arg, &status, sizeof(status)))
            return -EFAULT;
        break;
    }
    case MYDEV_CMD_SET_CONFIG: {
        struct mydev_config cfg;
        if (copy_from_user(&cfg, (struct mydev_config __user *)arg, sizeof(cfg)))
            return -EFAULT;
        break;
    }
    case MYDEV_CMD_EXCHANGE: {
        struct mydev_data data;
        if (copy_from_user(&data, (struct mydev_data __user *)arg, sizeof(data)))
            return -EFAULT;
        /* 处理数据 */
        if (copy_to_user((struct mydev_data __user *)arg, &data, sizeof(data)))
            return -EFAULT;
        break;
    }
    default:
        return -ENOTTY;
    }
    return 0;
}

2.5 命令码编码流程 SVG 图

下图展示了从需求到命令码生成的完整编码流程:

ioctl 命令码编码流程 ioctl 命令码编码流程 1. 需求分析 2. 选择幻数 (Magic) 3. 分配序号 (NR) 4. 确定方向 (_IO/_IOR/_IOW/_IOWR) 5. 生成命令码 6. 内核校验 (方向/大小) 每个步骤都影响命令码的唯一性和安全性,缺一不可

2.6 命令码校验与安全边界

内核在 do_vfs_ioctl 中自动执行以下校验,开发者无需手动实现:

  • 方向校验: 根据 _IOC_DIR(cmd) 判断是否需要读写权限,防止只读命令写入数据。
  • 大小校验: 通过 _IOC_SIZE(cmd) 检查用户缓冲区大小是否与命令码声明一致。
  • 幻数校验: 驱动层通过 _IOC_TYPE(cmd) 匹配,拒绝非法幻数的命令。
安全最佳实践: 在 unlocked_ioctl 入口处始终执行幻数和序号范围校验,即使内核已做部分检查。双重校验可以防御未来内核版本的行为变化。

2.7 常见编码错误与避坑指南

错误类型 错误示例 后果 正确做法
幻数冲突 使用 0x00 或 0xFF 与内核通用命令冲突 查阅 ioctl-number.rst 选择未分配值
方向错误 读操作使用 _IOW 内核拒绝写权限检查 根据数据流向选择正确宏
大小不匹配 _IOR(MAGIC, 1, int) 但传递结构体 数据截断或越界拷贝 确保 sizeof 与实际类型一致
序号重复 两个命令使用相同序号 switch 语句匹配到错误命令 维护序号分配表,避免重复

2.8 命令码解析与调试工具

内核提供了 _IOC_DIR、_IOC_TYPE、_IOC_NR、_IOC_SIZE 四个宏用于从命令码中提取字段。调试时可以使用以下方法打印命令码信息:

static void dump_cmd(unsigned int cmd)
{
    pr_debug("cmd=0x%08x dir=%d type='%c' nr=%d size=%zu\n",
             cmd,
             _IOC_DIR(cmd),
             (char)_IOC_TYPE(cmd),
             _IOC_NR(cmd),
             _IOC_SIZE(cmd));
}

用户空间可以通过 ioctl_list 工具或 strace -e ioctl 捕获命令码,与内核定义对比验证一致性。

调试技巧: 在驱动开发阶段,将 dump_cmd 放在 unlocked_ioctl 入口,配合 dynamic_debug 动态开启/关闭日志,快速定位命令码不匹配问题。