3. 内核空间 ioctl 实现:unlocked_ioctl 与 compat_ioctl 函数指针
在 Linux 内核 2.6.36 之后,传统的 ioctl 方法被拆分为两个独立的函数指针:unlocked_ioctl 和 compat_ioctl。这一拆分解决了大内核锁(BKL)带来的性能瓶颈,并提供了对 32 位用户空间程序在 64 位内核上运行时的兼容支持。本章将深入剖析这两个函数指针的定义、注册流程以及在 file_operations 结构体中的集成方式。
3.1 unlocked_ioctl:无大内核锁的 ioctl 入口
unlocked_ioctl 是当前 Linux 内核中实现自定义命令的标准接口。与旧版 ioctl 不同,它在调用时不会持有大内核锁,驱动开发者必须自行确保并发安全。
函数原型:
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
- filp:指向打开的设备文件结构体,包含文件描述符、当前偏移量等信息。
- cmd:用户空间传入的命令码,通常通过
_IO/_IOR/_IOW/_IOWR宏生成。 - arg:命令参数,可以是整数、指针(需使用
copy_from_user/copy_to_user访问)或 NULL。
并发安全提示:由于
unlocked_ioctl 不持有 BKL,驱动必须使用互斥锁(mutex)、自旋锁(spinlock)或 RCU 机制保护共享数据结构。推荐使用 struct mutex 配合 mutex_lock_interruptible 实现可睡眠的互斥访问。
3.2 compat_ioctl:32/64 位兼容层
当 64 位内核运行 32 位用户空间程序时,struct 的对齐方式和成员大小可能不同(例如 long 在 32 位下为 4 字节,64 位下为 8 字节)。compat_ioctl 负责将 32 位用户空间的命令和参数转换为内核期望的 64 位格式。
函数原型:
long (*compat_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
参数含义与 unlocked_ioctl 完全相同,但 arg 指向的是 32 位用户空间的地址。驱动需要根据 cmd 判断是否需要转换数据结构。
常见陷阱:如果驱动不实现
compat_ioctl,32 位用户程序调用 ioctl 时会返回 -ENOTTY(不适当的 ioctl 操作)。对于只传递整数或简单枚举值的命令,可以直接将 compat_ioctl 指向 unlocked_ioctl 函数。
3.3 file_operations 结构体注册
驱动通过填充 struct file_operations 结构体,将 unlocked_ioctl 和 compat_ioctl 函数指针注册到内核。该结构体在驱动初始化时赋值,并在 cdev_init 或 register_chrdev 时与字符设备关联。
| 字段 | 类型 | 说明 |
|---|---|---|
owner |
struct module * |
通常设为 THIS_MODULE,防止模块被卸载时仍有打开的文件 |
open |
int (*open)(...) |
设备打开时的回调,用于初始化资源 |
release |
int (*release)(...) |
设备关闭时的回调,用于清理资源 |
unlocked_ioctl |
long (*unlocked_ioctl)(...) |
无锁 ioctl 入口(必须实现) |
compat_ioctl |
long (*compat_ioctl)(...) |
32 位兼容 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,
};
static int __init my_init(void)
{
// 分配设备号、初始化 cdev、添加设备到系统
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, dev_num, 1);
return 0;
}
3.4 内核 ioctl 调用流程与函数指针选择
当用户空间调用 ioctl(fd, cmd, arg) 时,内核 VFS 层根据当前进程的架构选择对应的函数指针:
关键判断逻辑:内核通过
is_compat_task() 宏检测当前进程是否为 32 位兼容模式。如果是,则调用 compat_ioctl;否则调用 unlocked_ioctl。如果 compat_ioctl 为 NULL,内核会尝试调用 unlocked_ioctl,但可能导致数据结构解析错误。
3.5 实战:实现一个带互斥锁的 ioctl 驱动
以下代码展示了一个完整的字符设备驱动,包含 unlocked_ioctl 和 compat_ioctl 的实现,并使用互斥锁保护共享数据:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define MY_IOC_MAGIC 'k'
#define MY_IOC_GET _IOR(MY_IOC_MAGIC, 1, int)
#define MY_IOC_SET _IOW(MY_IOC_MAGIC, 2, int)
struct my_device {
int data;
struct mutex lock;
struct cdev cdev;
};
static struct my_device *my_dev;
static long my_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct my_device *dev = filp->private_data;
int ret = 0, val;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
switch (cmd) {
case MY_IOC_GET:
val = dev->data;
if (copy_to_user((int __user *)arg, &val, sizeof(val)))
ret = -EFAULT;
break;
case MY_IOC_SET:
if (copy_from_user(&val, (int __user *)arg, sizeof(val))) {
ret = -EFAULT;
break;
}
dev->data = val;
break;
default:
ret = -ENOTTY;
break;
}
mutex_unlock(&dev->lock);
return ret;
}
static long my_compat_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
// 对于只传递 int 的命令,直接复用 unlocked_ioctl
return my_unlocked_ioctl(filp, cmd, (unsigned long)compat_ptr(arg));
}
static int my_open(struct inode *inode, struct file *filp)
{
struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev;
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
return 0;
}
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,
};
static int __init my_init(void)
{
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_ioctl_dev");
if (ret < 0)
return ret;
my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
if (!my_dev) {
unregister_chrdev_region(dev_num, 1);
return -ENOMEM;
}
mutex_init(&my_dev->lock);
cdev_init(&my_dev->cdev, &my_fops);
my_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&my_dev->cdev, dev_num, 1);
if (ret) {
kfree(my_dev);
unregister_chrdev_region(dev_num, 1);
return ret;
}
return 0;
}
static void __exit my_exit(void)
{
dev_t dev_num = my_dev->cdev.dev;
cdev_del(&my_dev->cdev);
kfree(my_dev);
unregister_chrdev_region(dev_num, 1);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
compat_ptr 宏:在
compat_ioctl 中,必须使用 compat_ptr(arg) 将 32 位用户空间地址转换为内核可用的指针。直接强制转换 (void __user *)arg 在 64 位内核上会导致高位地址截断,引发内核崩溃。
3.6 函数指针注册的常见错误与调试
| 错误场景 | 现象 | 解决方案 |
|---|---|---|
未实现 unlocked_ioctl |
用户空间 ioctl 返回 -ENOTTY | 在 file_operations 中显式赋值 unlocked_ioctl |
未实现 compat_ioctl |
32 位程序调用 ioctl 返回 -ENOTTY | 实现 compat_ioctl,或直接指向 unlocked_ioctl(仅限无结构体参数的命令) |
compat_ioctl 中未使用 compat_ptr |
内核崩溃或数据错误 | 使用 compat_ptr(arg) 转换地址 |
| 并发访问未加锁 | 数据竞争、内核 oops | 使用 mutex 或 spinlock 保护临界区 |
调试技巧:使用
strace -e ioctl ./test_app 跟踪用户空间的 ioctl 调用,观察返回值。内核侧可通过 printk 或 dev_dbg 打印 cmd 和 arg 值,验证命令码是否匹配。
3.7 本章小结
- unlocked_ioctl 是当前内核标准的 ioctl 入口,不持有大内核锁,驱动需自行保证并发安全。
- compat_ioctl 提供 32/64 位兼容支持,必须使用
compat_ptr转换用户空间地址。 - 两个函数指针通过
file_operations结构体注册,内核根据进程架构自动选择调用。 - 实战中推荐使用互斥锁保护共享数据,并始终实现
compat_ioctl以支持跨架构用户空间程序。