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 开始递增 |
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))
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)分配策略
序号字段在同一类型下唯一标识一个命令。推荐策略:
- 从 0 开始递增:每个新命令分配下一个可用序号。
- 预留范围:将 0-9 留给基础操作(如打开、关闭、复位),10-99 留给功能命令。
- 避免跳过:不要随意跳过序号,除非为了兼容旧版本。
- 文档化:在头文件中用注释标明每个序号的用途。
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自动推导,确保数据结构固定长度。