4. IOCTL 命令宏定义:_IO、_IOR、_IOW、_IOWR 宏的使用方法与参数含义

在 VxWorks 字符设备驱动中,IOCTL(Input/Output Control)命令是用户态应用程序与内核驱动进行控制交互的核心通道。为了规范命令码的生成,Linux 内核定义了一套标准的宏,VxWorks 也沿用了这一设计。本章将深入解析 _IO、_IOR、_IOW、_IOWR 四个宏的定义、参数含义及使用场景。

4.1 命令码的编码结构

一个标准的 IOCTL 命令码是一个 32 位整数,由四个字段组成:幻数(Type/Magic)、序号(Number)、数据传输方向(Direction) 和 数据大小(Size)。宏的作用就是将这些字段组合成一个唯一的命令标识。

命令码 32 位布局:
| 31 ... 30 | 29 ... 16 | 15 ... 8 | 7 ... 0 |
| 方向(2bit)| 大小(14bit)| 幻数(8bit)| 序号(8bit)|
IOCTL 命令码 32 位位域结构 方向 [31:30] 数据大小 [29:16] 幻数 [15:8] 序号 [7:0] _IOC_DIR _IOC_SIZE _IOC_TYPE _IOC_NR

4.2 四个核心宏的定义与参数

这四个宏定义在 <ioLib.h> 或 <sys/ioctl.h> 中,其原型如下:

宏名称 方向含义 参数列表 典型用途
_IO(type, nr) 无数据传输 type, nr 查询状态、复位、使能/禁止
_IOR(type, nr, size) 从设备读取数据 type, nr, size 读取寄存器、获取统计信息
_IOW(type, nr, size) 向设备写入数据 type, nr, size 设置参数、下发配置
_IOWR(type, nr, size) 双向数据传输 type, nr, size 读写配置块、原子操作

4.3 参数详解

  • type(幻数/Magic Number):一个 8 位字符(通常用 ASCII 字符表示),用于区分不同的设备驱动。例如,串口驱动可能用 'S',GPIO 驱动用 'G'。内核通过幻数验证命令是否属于该设备。
  • nr(序号/Number):一个 8 位无符号整数,范围 0~255,用于在同一个驱动内区分不同的 IOCTL 命令。例如,CMD_START 定义为 0,CMD_STOP 定义为 1。
  • size(数据大小):对于 _IOR、_IOW、_IOWR,第三个参数是数据类型(如 int、struct my_data),宏内部通过 sizeof() 自动计算字节数。对于 _IO,没有数据大小参数。
最佳实践:幻数应使用大写字母,并在驱动头文件中通过 #define 定义常量,避免魔数(magic number)散落在代码中。例如:#define MY_DEV_MAGIC 'M'。

4.4 宏展开与位运算原理

这些宏最终调用 _IOC() 底层宏来组装命令码。以下是典型的宏展开逻辑:

/* 底层宏定义 */
#define _IOC(dir, type, nr, size) \
    (((dir)  << _IOC_DIRSHIFT) | \
     ((type) << _IOC_TYPESHIFT) | \
     ((nr)   << _IOC_NRSHIFT)   | \
     ((size) << _IOC_SIZESHIFT))

/* 方向常量 */
#define _IOC_NONE   0U
#define _IOC_READ   2U
#define _IOC_WRITE  1U

/* 四个对外宏 */
#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))
宏展开流程:从 _IOW 到 32 位命令码 _IOW('M', 1, int) _IOC(1, 'M', 1, 4) (WRITE | 'M' << 8 | 1 << 0 | 4 << 16) 0x4004'4D01 (32 位命令码) 位域分解 方向(bit31-30): 01 = _IOC_WRITE 大小(bit29-16): 4 (sizeof(int)) 幻数(bit15-8): 0x4D ('M') | 序号(bit7-0): 1

4.5 实际代码示例

以下是一个 VxWorks 字符设备驱动中定义 IOCTL 命令的完整示例:

/* myDevice.h - 设备 IOCTL 命令定义 */
#define MY_DEV_MAGIC    'M'

#define CMD_GET_STATUS  _IO(MY_DEV_MAGIC, 0)       /* 无数据,查询状态 */
#define CMD_GET_CONFIG  _IOR(MY_DEV_MAGIC, 1, int)  /* 读取配置值 */
#define CMD_SET_CONFIG  _IOW(MY_DEV_MAGIC, 2, int)  /* 写入配置值 */
#define CMD_EXCHANGE    _IOWR(MY_DEV_MAGIC, 3, struct my_data) /* 双向交换 */

struct my_data {
    int  input;
    int  output;
    int  flags;
};
常见错误:
  • 幻数冲突:不同驱动使用相同的幻数,导致命令码冲突。建议在项目文档中统一登记幻数。
  • 序号重复:同一个驱动内两个命令使用相同的序号,导致驱动无法区分。
  • 大小不匹配:_IOR/_IOW 的 size 参数传入指针而非数据类型,导致 sizeof(指针) 为 4 或 8,而非预期结构体大小。

4.6 方向位与内核权限检查

方向位不仅用于编码,还在内核中用于权限验证。VxWorks 的 ioctl() 系统调用入口会根据方向位决定是否需要检查用户缓冲区的读写权限:

  • _IOC_NONE:不访问用户空间,无需权限检查。
  • _IOC_READ:内核需要从用户空间读取数据,检查用户缓冲区是否可读。
  • _IOC_WRITE:内核需要向用户空间写入数据,检查用户缓冲区是否可写。
  • _IOC_READ | _IOC_WRITE:双向检查。
关键点:方向位的正确设置直接关系到系统安全性。如果将一个实际需要写入数据的命令错误地定义为 _IO(无数据方向),内核将不会执行用户空间缓冲区的拷贝与校验,可能导致内核崩溃或信息泄露。

4.7 宏选择速查表

场景 推荐宏 说明
仅触发动作,无数据交换 _IO 如设备复位、启动、停止
从设备获取数据 _IOR 如读取传感器值、版本号
向设备发送数据 _IOW 如设置阈值、配置模式
先发送后接收(或反之) _IOWR 如执行带参数查询、原子配置