2. ioctl 系统调用原型:用户空间与内核空间接口

ioctl 是 Linux 设备驱动中实现“非标准控制操作”的核心系统调用。与 read/write 不同,ioctl 允许驱动定义任意数量的自定义命令,每个命令可以携带任意类型的参数。理解 ioctl 必须从用户空间和内核空间两个视角分别掌握其函数原型,并关注 64/32 位兼容场景下的 compat_ioctl 处理。

2.1 用户空间 ioctl 函数原型

用户空间通过 glibc 封装的 ioctl 函数发起系统调用,其原型定义在 <sys/ioctl.h> 中:

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
参数 类型 说明
fd int 通过 open() 获得的设备文件描述符
request unsigned long 驱动定义的命令码,编码了方向、类型、序号、大小
... 可变参数 通常为 void *,指向与命令相关的数据缓冲区
关键点:第三个参数是可变参数,但实际使用时几乎总是传入一个 void * 指针。glibc 内部将其直接传递给内核的 sys_ioctl 入口。

返回值:成功返回 0;失败返回 -1 并设置 errno(常见错误码包括 ENOTTY、EINVAL、EFAULT)。

2.2 内核空间 unlocked_ioctl 原型

在内核 2.6.36 之后,传统的 ioctl 方法被移除了 struct file_operations,取而代之的是 unlocked_ioctl。驱动开发者必须实现此接口:

#include <linux/fs.h>

long (*unlocked_ioctl) (struct file *filp,
                        unsigned int cmd,
                        unsigned long arg);
参数 类型 说明
filp struct file * 打开的设备文件结构,包含 private_data 等上下文
cmd unsigned int 用户空间传入的命令码(注意与用户空间的 unsigned long 区别)
arg unsigned long 用户空间传入的指针参数,需使用 copy_from_user/copy_to_user 访问
重要区别:用户空间 request 是 unsigned long,内核空间 cmd 是 unsigned int。在 64 位系统上,高 32 位会被截断,但标准命令码编码保证低 32 位足够使用。

2.3 compat_ioctl:64/32 位兼容接口

当 64 位内核运行 32 位用户空间应用程序时,struct 的对齐和指针宽度不同。内核提供了 compat_ioctl 来处理这种大小端和位宽转换:

long (*compat_ioctl) (struct file *filp,
                      unsigned int cmd,
                      unsigned long arg);

原型与 unlocked_ioctl 完全一致,但语义不同:

  • unlocked_ioctl:处理原生(native)位宽的用户空间请求。
  • compat_ioctl:处理 32 位兼容模式的用户空间请求,arg 指向的是 32 位兼容结构体。
场景 内核位宽 用户空间位宽 调用的方法
原生 64 位 64 位 64 位 unlocked_ioctl
原生 32 位 32 位 32 位 unlocked_ioctl
兼容模式 64 位 32 位 compat_ioctl
最佳实践:如果驱动只使用不包含指针的固定大小命令(如 int、long long),可以直接让 compat_ioctl = unlocked_ioctl。如果涉及包含指针或对齐敏感的结构体,必须实现 compat_ioctl 并使用 compat_ptr() 转换指针。

2.4 系统调用完整流程 SVG 图

下图展示了从用户空间 ioctl() 到内核空间 unlocked_ioctl/compat_ioctl 的完整调用路径:

ioctl 系统调用完整流程 用户空间应用程序 ioctl(fd, cmd, arg) 内核 sys_ioctl 入口 vfs_ioctl() 分发 用户空间 位宽判断 原生 unlocked_ioctl 32位兼容 compat_ioctl 驱动 ioctl 命令处理 驱动 compat 处理 说明 64 位内核 + 32 位用户空间 → compat_ioctl;其余场景 → unlocked_ioctl 驱动必须同时注册两个方法,或显式设置 compat_ioctl = unlocked_ioctl

2.5 驱动注册示例

在 struct file_operations 中注册两个 ioctl 方法:

static const struct file_operations my_fops = {
    .owner          = THIS_MODULE,
    .open           = my_open,
    .release        = my_release,
    .unlocked_ioctl = my_unlocked_ioctl,
    .compat_ioctl   = my_compat_ioctl,   /* 或直接 = my_unlocked_ioctl */
};
简化技巧:如果驱动所有命令都使用 _IOR/_IOW/_IOWR 宏定义且数据大小固定(不包含指针),可以直接写 .compat_ioctl = my_unlocked_ioctl,内核会自动处理 arg 的位宽转换。

2.6 常见错误与避坑

  • 忘记注册 compat_ioctl:64 位内核上运行 32 位应用时,ioctl 返回 ENOTTY,应用崩溃。
  • cmd 参数位宽不匹配:用户空间 unsigned long 与内核 unsigned int 的截断问题,标准命令码编码已规避此风险。
  • 直接解引用 arg:内核空间不能直接访问用户空间指针,必须使用 copy_from_user/copy_to_user 或 get_user/put_user。
  • compat 中未使用 compat_ptr:当命令码包含指针时,必须用 compat_ptr(arg) 转换 32 位指针为 64 位内核指针。