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 位布局如下(位序从高位到低位):

ioctl 32位命令编码结构 ioctl 命令 32 位编码结构 direction [31:30] type (幻数) [29:22] number (序号) [21:14] size (参数大小) [13:0] bit 31 bit 30 bit 29 … bit 22 bit 21 … bit 14 bit 13 … bit 0 字段说明: • direction (2 bits):数据传输方向,取值 _IOC_NONE / _IOC_READ / _IOC_WRITE / _IOC_READ|_IOC_WRITE • type (8 bits):幻数/魔数,用于区分不同驱动或子系统,通常选用 ASCII 字符 • number (8 bits):功能序号,同一驱动内每个命令唯一 • size (14 bits):用户态与内核态之间传输的数据结构大小(字节),用于自动拷贝

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) 时,内核按以下流程校验命令码:

内核 ioctl 命令码校验流程 内核 ioctl 命令码校验流程 用户态: ioctl(fd, cmd, arg) 内核: 提取 dir/type/nr/size 校验 type 是否匹配驱动幻数 校验 direction 权限 根据 size 自动拷贝数据 (copy_from/to_user) 驱动 unlocked_ioctl 处理

整个校验流程确保:只有 type 匹配、direction 正确、size 合法的命令才会被传递到驱动的 unlocked_ioctl 回调函数。任何字段不匹配都会导致内核返回 -ENOTTY(不适当的 ioctl 命令)或 -EFAULT(地址错误)。