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 的完整调用路径:
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 位内核指针。