3. ioctl 命令编码规范:32 位编码结构
Linux 内核为 ioctl 命令定义了一套严格的 32 位编码规范,将命令码划分为 type(幻数)、number(序号)、direction(数据传输方向) 和 size(参数大小) 四个字段。这套规范定义在 <linux/ioctl.h> 及 <asm-generic/ioctl.h> 中,是驱动开发者必须遵守的编码契约。
核心原则: 每个 ioctl 命令都是一个唯一的 32 位整数,通过位操作组合而成。内核在
do_vfs_ioctl() 路径中通过 _IOC_TYPE()、_IOC_NR()、_IOC_DIR()、_IOC_SIZE() 宏解构命令码,实现权限校验和自动拷贝。
3.1 32 位编码结构总览
ioctl 命令码的 32 位布局如下(位序从高位到低位):
3.2 字段详解与编码宏
内核提供了一套辅助宏,用于构建和解构 ioctl 命令码。驱动开发者应始终使用这些宏,而非手动拼接位域。
| 宏名称 | 功能 | 位域 | 示例 |
|---|---|---|---|
_IO(type, nr) |
构造无数据传输的命令 | direction = _IOC_NONE | _IO('V', 0x10) |
_IOR(type, nr, size) |
构造只读命令(内核→用户) | direction = _IOC_READ | _IOR('V', 0x20, struct mydata) |
_IOW(type, nr, size) |
构造只写命令(用户→内核) | direction = _IOC_WRITE | _IOW('V', 0x30, struct mydata) |
_IOWR(type, nr, size) |
构造读写双向命令 | direction = _IOC_READ|_IOC_WRITE | _IOWR('V', 0x40, struct mydata) |
编码规范提示: type 字段通常选用一个 ASCII 字符(如 'V'、'S'、'M'),确保在
/proc/devices 或调试时具有可读性。避免使用数字 0,因为 _IOC_TYPE(cmd) == 0 可能被内核视为未初始化命令。
3.3 方向字段详解
direction 字段占用最高 2 个比特位,定义在 <linux/ioctl.h> 中:
/* 方向定义 */
#define _IOC_NONE 0U /* 无数据传输 */
#define _IOC_WRITE 1U /* 用户空间 → 内核空间(写) */
#define _IOC_READ 2U /* 内核空间 → 用户空间(读) */
/* 内核内部位域提取宏 */
#define _IOC_DIRBITS 2
#define _IOC_DIRSHIFT (30)
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS) - 1)
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
方向字段在 do_vfs_ioctl() 中用于权限校验:
- _IOC_READ:内核需要将数据拷贝到用户空间,对应
copy_to_user()。 - _IOC_WRITE:内核需要从用户空间读取数据,对应
copy_from_user()。 - _IOC_READ|_IOC_WRITE:双向传输,内核先读后写或同时进行。
- _IOC_NONE:仅控制命令,无数据拷贝,如设备复位、状态查询。
3.4 size 字段与自动拷贝机制
size 字段占用低 14 位,最大支持 16383 字节的数据结构。内核在 fs/ioctl.c 的 do_vfs_ioctl() 中利用该字段实现 自动数据拷贝:
/* 内核 ioctl 入口中的自动拷贝逻辑(简化) */
long do_vfs_ioctl(struct file *filp, unsigned int fd,
unsigned int cmd, unsigned long arg)
{
unsigned int dir = _IOC_DIR(cmd);
unsigned int size = _IOC_SIZE(cmd);
void __user *argp = (void __user *)arg;
char stack_buf[128]; /* 小数据栈上分配 */
void *buf = NULL;
/* 自动分配缓冲区并拷贝用户数据 */
if (dir & _IOC_WRITE) {
if (size <= sizeof(stack_buf))
buf = stack_buf;
else
buf = kmalloc(size, GFP_KERNEL);
if (copy_from_user(buf, argp, size))
return -EFAULT;
}
/* 调用驱动实际的 unlocked_ioctl */
int ret = filp->f_op->unlocked_ioctl(filp, cmd, (unsigned long)buf);
/* 自动将结果拷贝回用户空间 */
if (dir & _IOC_READ) {
if (copy_to_user(argp, buf, size))
return -EFAULT;
}
if (buf != stack_buf)
kfree(buf);
return ret;
}
安全警告: size 字段必须与驱动实际使用的数据结构大小严格一致。如果 size 字段声明为 256 字节,但驱动内部只处理 64 字节,会导致内核越界访问或信息泄露。始终使用
sizeof(struct your_data) 作为 size 参数。
3.5 编码规范检查清单
在定义新的 ioctl 命令时,请逐项核对以下规范:
| 检查项 | 规范要求 | 违规后果 |
|---|---|---|
| type 唯一性 | 同一子系统内 type 必须唯一,建议在 Documentation/ioctl/ioctl-number.rst 中注册 | 命令冲突,驱动间误匹配 |
| number 递增 | 同一驱动内 number 从 0 开始递增,避免跳跃 | 调试困难,代码可维护性差 |
| direction 正确 | 根据实际数据流向选择 _IOR / _IOW / _IOWR | 权限校验失败,数据拷贝方向错误 |
| size 精确 | 使用 sizeof(struct) 而非硬编码数字 | 缓冲区溢出或截断 |
| 宏使用 | 必须使用 _IO / _IOR / _IOW / _IOWR 宏 | 位域错位,命令码不可移植 |
| 参数对齐 | 数据结构应使用 __attribute__((packed)) 或自然对齐 | 跨架构拷贝时数据错位 |
3.6 完整示例:字符设备驱动中的 ioctl 命令定义
以下是一个完整的 ioctl 命令定义示例,展示了如何遵循 32 位编码规范:
/* mydevice.h - 用户态和内核态共享的头文件 */
#ifndef MYDEVICE_IOCTL_H
#define MYDEVICE_IOCTL_H
#include <linux/ioctl.h> /* 内核态 */
/* #include <sys/ioctl.h> */ /* 用户态 */
/* 1. 定义幻数(type) */
#define MYDEVICE_MAGIC 'M'
/* 2. 定义数据结构 */
struct mydevice_config {
unsigned int baud_rate; /* 波特率 */
unsigned char parity; /* 校验位:0=无, 1=奇, 2=偶 */
unsigned char stop_bits; /* 停止位:1 或 2 */
unsigned short flags; /* 标志位 */
} __attribute__((packed));
/* 3. 定义命令序号(number) */
#define MYDEVICE_GET_CONFIG _IOR(MYDEVICE_MAGIC, 0x10, struct mydevice_config)
#define MYDEVICE_SET_CONFIG _IOW(MYDEVICE_MAGIC, 0x11, struct mydevice_config)
#define MYDEVICE_RESET _IO (MYDEVICE_MAGIC, 0x12)
#define MYDEVICE_GET_STATUS _IOR(MYDEVICE_MAGIC, 0x13, unsigned int)
#define MYDEVICE_SET_DEBUG _IOW(MYDEVICE_MAGIC, 0x14, int)
/* 4. 验证命令码不冲突(编译时检查) */
#define MYDEVICE_CMD_MAX 0x15
#endif /* MYDEVICE_IOCTL_H */
最佳实践: 将 ioctl 命令定义放在一个单独的头文件中,同时被内核驱动和用户态应用程序包含。使用
__attribute__((packed)) 确保数据结构在不同架构下具有相同的内存布局。在驱动初始化时,使用 BUILD_BUG_ON() 宏验证命令码不重叠。
3.7 内核校验流程
当用户态调用 ioctl(fd, cmd, arg) 时,内核按以下流程校验命令码:
整个校验流程确保:只有 type 匹配、direction 正确、size 合法的命令才会被传递到驱动的 unlocked_ioctl 回调函数。任何字段不匹配都会导致内核返回 -ENOTTY(不适当的 ioctl 命令)或 -EFAULT(地址错误)。