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 层根据当前进程的架构选择对应的函数指针:

内核 ioctl 调用流程与函数指针选择 用户空间 ioctl() 调用 VFS 层:sys_ioctl() 系统调用 进程是 32 位? 是 compat_ioctl 函数指针 否 unlocked_ioctl 函数指针 驱动 ioctl 处理函数执行
关键判断逻辑:内核通过 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 以支持跨架构用户空间程序。