3. IOCTL 命令定义规范
在 VxWorks 字符设备驱动中,IOCTL 命令是一个 32 位的无符号整数,其内部被划分为多个位域,分别编码命令的方向、类型、序号和大小。遵循统一的命令码结构,可以避免设备驱动之间的命令冲突,并让内核能够自动校验用户空间与内核空间之间的数据拷贝方向与缓冲区长度。
3.1 命令码组成结构
VxWorks 沿用了 Linux 内核的 IOCTL 命令码编码方式,将 32 位划分为 4 个位域,从高位到低位依次为:
| 位域 | 位范围 | 宽度(bit) | 宏/标识 | 说明 |
|---|---|---|---|---|
| 方向位(DIR) | 31 – 30 | 2 | _IOC_DIR | 数据传输方向:读、写、读写、无数据 |
| 类型位(TYPE) | 29 – 24 | 6 | _IOC_TYPE | 设备类型幻数,通常使用 ASCII 字符 |
| 序号位(NR) | 23 – 16 | 8 | _IOC_NR | 命令序号,同一类型下唯一 |
| 大小位(SIZE) | 15 – 0 | 16 | _IOC_SIZE | 用户数据缓冲区长度(字节) |
关键点:命令码的编码与解码全部通过预定义宏完成,驱动开发者不应手动拼接 32 位整数,而应使用
_IO()、_IOR()、_IOW()、_IOWR() 等构造宏。
3.2 方向位(DIR)
方向位占用 bit 31 和 bit 30,表示 IOCTL 调用过程中用户空间与内核空间之间的数据流动方向。VxWorks 定义了 4 种方向常量:
| 宏 | 值 | 方向 | 典型场景 |
|---|---|---|---|
_IOC_NONE |
0U | 无数据 | 设备复位、使能/禁止、状态查询(无参数) |
_IOC_READ |
2U | 设备 → 用户 | 读取设备寄存器、获取统计信息 |
_IOC_WRITE |
4U | 用户 → 设备 | 设置参数、下发配置结构体 |
_IOC_READ | _IOC_WRITE |
6U | 双向 | 读写复合操作(如先写配置再读状态) |
注意:方向位不仅用于语义描述,VxWorks 内核在
ioctl() 系统调用入口处会根据方向位自动决定是否执行 copyin() 或 copyout()。方向位设置错误可能导致内核访问非法地址或数据损坏。
3.3 类型位(TYPE)
类型位占用 bit 29 – 24,共 6 位,用于标识命令所属的设备驱动类型。通常使用一个 ASCII 字符(如 'M'、'S'、'U')作为幻数,确保不同驱动之间的命令码不会重叠。
- 唯一性:每个设备驱动应在项目范围内选择一个唯一的 TYPE 幻数,并在头文件中通过
#define声明。 - 取值范围:0 – 63(6 位),常用 0x00 – 0xFF 中的 ASCII 可打印字符,便于调试时识别。
- 示例:
/* 设备类型幻数定义 */
#define MY_DEV_TYPE 'M' /* 0x4D */
#define SENSOR_TYPE 'S' /* 0x53 */
#define UART_TYPE 'U' /* 0x55 */
最佳实践:在大型项目中,建议维护一份
ioctl_type_alloc.h 文件,集中登记所有已使用的 TYPE 幻数,避免冲突。
3.4 序号位(NR)
序号位占用 bit 23 – 16,共 8 位,用于在同一 TYPE 下区分不同的 IOCTL 命令。每个设备驱动可以定义最多 256 个命令(0 – 255)。
- 顺序递增:建议从 0 开始顺序定义,便于维护和代码审查。
- 保留值:0 通常保留为无效命令或测试命令。
- 示例:
/* 同一设备类型下的命令序号 */
#define MY_DEV_GET_STATUS _IOR(MY_DEV_TYPE, 1, struct dev_status)
#define MY_DEV_SET_CONFIG _IOW(MY_DEV_TYPE, 2, struct dev_config)
#define MY_DEV_RESET _IO(MY_DEV_TYPE, 3)
#define MY_DEV_RW_REG _IOWR(MY_DEV_TYPE, 4, struct reg_access)
3.5 大小位(SIZE)
大小位占用 bit 15 – 0,共 16 位,表示与 IOCTL 命令关联的用户数据缓冲区的大小(以字节为单位)。
- 自动校验:VxWorks 内核在
ioctl()入口处会检查用户传入的arg参数大小是否与 SIZE 字段匹配,若不匹配则返回EINVAL。 - 无数据命令:对于
_IO()构造的命令,SIZE 固定为 0。 - 结构体大小:对于
_IOR()、_IOW()、_IOWR(),SIZE 应设置为对应结构体的sizeof值。
/* 示例:定义带数据缓冲区的命令 */
struct my_dev_config {
int baud_rate;
int data_bits;
int stop_bits;
};
/* SIZE 自动设置为 sizeof(struct my_dev_config) */
#define MY_DEV_SET_BAUD _IOW(MY_DEV_TYPE, 5, struct my_dev_config)
提示:如果结构体包含指针字段,SIZE 只统计结构体本身的长度,不包含指针指向的动态数据。内核不会自动跟随指针进行深层拷贝,驱动需要手动处理指针指向的数据。
3.6 命令码构造与分解宏
VxWorks 在 ioLib.h 中提供了一套完整的宏,用于构造和分解 IOCTL 命令码:
| 宏 | 功能 | 示例 |
|---|---|---|
_IO(type, nr) |
构造无数据命令 | _IO('M', 1) |
_IOR(type, nr, size) |
构造读方向命令 | _IOR('M', 2, struct status) |
_IOW(type, nr, size) |
构造写方向命令 | _IOW('M', 3, struct config) |
_IOWR(type, nr, size) |
构造双向命令 | _IOWR('M', 4, struct reg) |
_IOC_DIR(cmd) |
提取方向位 | _IOC_DIR(cmd) |
_IOC_TYPE(cmd) |
提取类型位 | _IOC_TYPE(cmd) |
_IOC_NR(cmd) |
提取序号位 | _IOC_NR(cmd) |
_IOC_SIZE(cmd) |
提取大小位 | _IOC_SIZE(cmd) |
/* 完整示例:构造与分解 */
#include <ioLib.h>
#define MY_DEV_TYPE 'M'
/* 构造命令 */
#define CMD_GET_STATUS _IOR(MY_DEV_TYPE, 1, struct dev_status)
#define CMD_SET_CONFIG _IOW(MY_DEV_TYPE, 2, struct dev_config)
/* 在驱动中分解命令 */
STATUS myDevIoctl
(
MY_DEV *pDev,
int cmd,
void *arg
)
{
int dir = _IOC_DIR(cmd);
int type = _IOC_TYPE(cmd);
int nr = _IOC_NR(cmd);
int size = _IOC_SIZE(cmd);
/* 校验类型 */
if (type != MY_DEV_TYPE)
return ERROR;
switch (nr)
{
case 1: /* CMD_GET_STATUS */
if (dir != _IOC_READ)
return ERROR;
/* ... */
break;
case 2: /* CMD_SET_CONFIG */
if (dir != _IOC_WRITE)
return ERROR;
/* ... */
break;
default:
return ERROR;
}
return OK;
}
总结:IOCTL 命令码的规范定义是驱动安全性和可维护性的基石。通过 DIR、TYPE、NR、SIZE 四个位域的合理划分,VxWorks 实现了命令的唯一标识、数据方向的自动校验以及缓冲区长度的自动检查。驱动开发者应始终使用
_IO() / _IOR() / _IOW() / _IOWR() 宏构造命令,并在命令处理函数中使用 _IOC_DIR() / _IOC_TYPE() / _IOC_NR() / _IOC_SIZE() 进行分解校验。