4. ioctl 命令编码规范:_IO / _IOR / _IOW / _IOWR 宏定义

ioctl 命令码是用户空间与内核驱动之间传递“操作意图”的唯一标识。Linux 内核定义了一套严格的编码规范,将命令码拆分为 类型(Type)、序号(Number)、方向(Direction) 和 大小(Size) 四个字段,并通过 _IO、_IOR、_IOW、_IOWR 四个宏来生成标准命令码。

4.1 命令码的 32 位组成结构

一个标准的 ioctl 命令码是一个 32 位无符号整数,其位域划分如下:

命令码位域布局(共 32 bits)
位域 位范围 宽度 说明
方向(Direction) bit 31 – bit 30 2 bits 表示数据传输方向:读、写、读写、无数据
大小(Size) bit 29 – bit 16 14 bits 用户态与内核交换的数据结构字节大小
类型(Type) bit 15 – bit 8 8 bits 魔数(Magic Number),通常用 ASCII 字符表示
序号(Number) bit 7 – bit 0 8 bits 同一类型下的功能序号,从 0 开始递增
ioctl 命令码 32 位结构 方向 bit 31-30 大小 bit 29-16 (14 bits) 类型 bit 15-8 (8 bits) 序号 bit 7-0 (8 bits) 总宽度:32 bits 每个字段由内核预定义的宏自动填充,驱动开发者只需提供类型、序号和数据结构

4.2 方向(Direction)定义

方向字段占用最高 2 个 bit,决定数据在用户空间与内核空间之间的流动方式。内核头文件 <linux/ioctl.h> 中定义了以下方向常量:

宏名称 值 含义 典型场景
_IOC_NONE 0U 无数据传输 仅触发动作,如设备复位、启动采集
_IOC_READ 2U 从设备读取数据 获取状态、读取传感器值
_IOC_WRITE 1U 向设备写入数据 设置参数、下发配置
_IOC_READ | _IOC_WRITE 3U 双向数据传输 交换复杂结构体,如同时读写寄存器
方向选择原则: 如果命令只读取设备状态而不修改,使用 _IOR;只写入配置而不读取,使用 _IOW;既读又写使用 _IOWR;纯触发使用 _IO。不要为了省事而滥用 _IOWR,内核会根据方向做权限检查。

4.3 四个核心构造宏

内核提供四个宏来生成标准命令码,开发者无需手动拼接位域:

宏 方向 参数 示例
_IO(type, nr) 无数据 类型魔数、序号 _IO('M', 0)
_IOR(type, nr, datatype) 读 类型魔数、序号、数据类型 _IOR('M', 1, int)
_IOW(type, nr, datatype) 写 类型魔数、序号、数据类型 _IOW('M', 2, struct my_cfg)
_IOWR(type, nr, datatype) 读写 类型魔数、序号、数据类型 _IOWR('M', 3, struct my_buf)

这些宏在 <asm-generic/ioctl.h> 中的实现逻辑如下:

/* 内核源码:include/uapi/asm-generic/ioctl.h */
#define _IO(type, nr)        _IOC(_IOC_NONE,  (type), (nr), 0)
#define _IOR(type, nr, size) _IOC(_IOC_READ,  (type), (nr), sizeof(size))
#define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), sizeof(size))
#define _IOWR(type, nr, size)_IOC(_IOC_READ|_IOC_WRITE, (type), (nr), sizeof(size))

其中 _IOC 是底层位拼接宏:

#define _IOC(dir, type, nr, size) \
    (((dir)  << _IOC_DIRSHIFT)  | \
     ((type) << _IOC_TYPESHIFT) | \
     ((nr)   << _IOC_NRSHIFT)   | \
     ((size) << _IOC_SIZESHIFT))
宏展开流程:从 _IOR 到 32 位命令码 _IOR('M', 1, int) _IOC(READ, 'M', 1, 4) 0x80004D01 命令码 0x80004D01 位域分解 方向 = 2 (READ) → bit 31-30 = 10 大小 = 4 (sizeof(int)) → bit 29-16 = 0x004 类型 = 'M' (0x4D) → bit 15-8 = 0x4D 序号 = 1 → bit 7-0 = 0x01

4.4 类型(Type)选择规范

类型字段(Magic Number)用于区分不同的设备驱动,防止命令码冲突。内核社区推荐以下规则:

  • 使用 ASCII 字符:通常选择一个字母,如 'M'、'K'、'S',便于阅读和调试。
  • 避免与已有驱动冲突:查阅 Documentation/ioctl/ioctl-number.rst 确认所选字符未被占用。
  • 自定义驱动建议:使用大写字母或数字组合,如 'a' 到 'z' 已被大量通用驱动使用,新驱动优先选择 'A' 到 'Z' 中未被分配的字符。
常见错误: 不要使用 0 或 0xFF 作为类型值,这会导致与某些内核默认命令码重叠。也不要使用 _IO('X', ...) 中的 'X',因为 X 系列已被多个子系统占用。

4.5 序号(Number)分配策略

序号字段在同一类型下唯一标识一个命令。推荐策略:

  1. 从 0 开始递增:每个新命令分配下一个可用序号。
  2. 预留范围:将 0-9 留给基础操作(如打开、关闭、复位),10-99 留给功能命令。
  3. 避免跳过:不要随意跳过序号,除非为了兼容旧版本。
  4. 文档化:在头文件中用注释标明每个序号的用途。

4.6 大小(Size)自动推导

大小字段由宏自动从 sizeof(datatype) 推导,开发者只需传入数据类型名称。注意:

  • 数据类型必须是 固定大小 的结构体或基本类型,不能是变长数组或指针。
  • 如果命令不需要传输数据(_IO),大小字段固定为 0。
  • 内核在 copy_from_user / copy_to_user 时会校验大小是否匹配,不匹配返回 -EINVAL。

4.7 完整示例:自定义命令码定义

以下是一个字符设备驱动的命令码定义头文件:

/* mydevice.h */
#ifndef MYDEVICE_IOCTL_H
#define MYDEVICE_IOCTL_H

#include <linux/ioctl.h>

/* 类型魔数:使用 'M' */
#define MYDEVICE_MAGIC 'M'

/* 数据结构 */
struct mydevice_status {
    int  temperature;
    int  humidity;
    char firmware_version[16];
};

struct mydevice_config {
    int  sample_rate;
    int  threshold;
};

/* 命令码定义 */
#define MYDEVICE_GET_STATUS   _IOR(MYDEVICE_MAGIC, 0, struct mydevice_status)
#define MYDEVICE_SET_CONFIG   _IOW(MYDEVICE_MAGIC, 1, struct mydevice_config)
#define MYDEVICE_RESET        _IO (MYDEVICE_MAGIC, 2)
#define MYDEVICE_EXCHANGE     _IOWR(MYDEVICE_MAGIC, 3, struct mydevice_status)

/* 检查命令码是否有效 */
#define MYDEVICE_IOCTL_MAXNR 3

#endif /* MYDEVICE_IOCTL_H */

4.8 命令码解码与调试

在调试时,可以使用内核提供的辅助宏反向解析命令码:

宏 功能 示例输出
_IOC_DIR(cmd) 提取方向 _IOC_READ
_IOC_TYPE(cmd) 提取类型 'M'
_IOC_NR(cmd) 提取序号 1
_IOC_SIZE(cmd) 提取大小 4

用户空间可以通过 ioctl_list 工具或 strace 查看实际传递的命令码值,辅助验证编码是否正确。

核心要点回顾:
  • 命令码 = 方向(2) + 大小(14) + 类型(8) + 序号(8),共 32 位。
  • 使用 _IO / _IOR / _IOW / _IOWR 宏生成命令码,不要手动拼接。
  • 类型选择 ASCII 字符,避免与已有驱动冲突。
  • 序号从 0 开始递增,保持文档化。
  • 大小由 sizeof 自动推导,确保数据结构固定长度。