4. ioctl命令实现

任务目标

在Linux内核驱动中实现ioctl接口,提供设备状态参数的设置与获取能力。用户态程序通过标准文件操作打开设备后,使用ioctl命令控制设备状态,并验证读写结果一致。

环境与依赖

  • Linux内核源码树(本案例基于5.15版本验证接口兼容性)
  • 内核编译工具链(gcc、make、linux-headers)
  • root权限(加载模块、创建设备节点)

核心实现

驱动定义两个ioctl命令:SET_STATE写入设备状态,GET_STATE读取当前状态。设备状态用int变量保存,通过copy_from_user/copy_to_user与用户空间交换数据。完整代码如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "ioctl_demo"
#define CLASS_NAME "demo_class"
#define IOCTL_MAGIC 'D'
#define IOCTL_SET_STATE _IOW(IOCTL_MAGIC, 1, int)
#define IOCTL_GET_STATE _IOR(IOCTL_MAGIC, 2, int)

static int major_num;
static struct class *demo_class;
static struct device *demo_device;
static dev_t dev_num;
static int device_state = 0;  // 设备状态变量

static long device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    int user_val;

    switch (cmd) {
    case IOCTL_SET_STATE:
        if (copy_from_user(&user_val, (int __user *)arg, sizeof(int))) {
            ret = -EFAULT;
            break;
        }
        device_state = user_val;
        pr_info("ioctl_demo: state set to %d\n", device_state);
        break;

    case IOCTL_GET_STATE:
        if (copy_to_user((int __user *)arg, &device_state, sizeof(int))) {
            ret = -EFAULT;
            break;
        }
        pr_info("ioctl_demo: state read as %d\n", device_state);
        break;

    default:
        ret = -ENOTTY;
        break;
    }
    return ret;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = device_ioctl,
};

static int __init demo_init(void)
{
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
        pr_err("ioctl_demo: failed to alloc region\n");
        return -1;
    }
    major_num = MAJOR(dev_num);

    cdev_init(&demo_cdev, &fops);
    if (cdev_add(&demo_cdev, dev_num, 1) < 0) {
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    demo_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(demo_class)) {
        cdev_del(&demo_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(demo_class);
    }

    demo_device = device_create(demo_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(demo_device)) {
        class_destroy(demo_class);
        cdev_del(&demo_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(demo_device);
    }

    pr_info("ioctl_demo: initialized, major=%d\n", major_num);
    return 0;
}

static void __exit demo_exit(void)
{
    device_destroy(demo_class, dev_num);
    class_destroy(demo_class);
    cdev_del(&demo_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("ioctl_demo: exited\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("ioctl command demo driver");

注意:上述代码中demo_cdev需在文件顶部声明为static struct cdev demo_cdev;,此处为聚焦ioctl逻辑未重复列出。

用户态测试程序

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define IOCTL_MAGIC 'D'
#define IOCTL_SET_STATE _IOW(IOCTL_MAGIC, 1, int)
#define IOCTL_GET_STATE _IOR(IOCTL_MAGIC, 2, int)

int main(void)
{
    int fd = open("/dev/ioctl_demo", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    int set_val = 42;
    if (ioctl(fd, IOCTL_SET_STATE, &set_val) < 0) {
        perror("ioctl set");
        close(fd);
        return 1;
    }

    int get_val = 0;
    if (ioctl(fd, IOCTL_GET_STATE, &get_val) < 0) {
        perror("ioctl get");
        close(fd);
        return 1;
    }

    printf("set=%d, get=%d\n", set_val, get_val);
    close(fd);
    return (set_val == get_val) ? 0 : 1;
}

编译与运行

  1. 将驱动代码保存为ioctl_demo.c,编写Makefile:
obj-m += ioctl_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	make -C $(KDIR) M=$(PWD) clean
  1. 编译并加载模块:
make
sudo insmod ioctl_demo.ko
dmesg | tail  # 确认 "initialized" 日志
  1. 编译并运行用户态测试:
gcc test_ioctl.c -o test_ioctl
./test_ioctl
echo $?  # 输出0表示验证通过

验证方法

程序执行后,终端打印set=42, get=42,且echo $?返回0,说明设置与读取的状态一致,ioctl双向数据通路正常。若返回非0,检查dmesg中是否有EFAULT或ENOTTY错误日志,并确认设备节点权限(当前用户需在dialout组或使用sudo运行测试程序)。

常见问题

  • 设备节点未创建:确认udev规则或手动执行mknod /dev/ioctl_demo c $(grep ioctl_demo /proc/devices | awk '{print $1}') 0。
  • ioctl返回-1且errno=ENOTTY:检查命令号编码是否一致,确保用户态与内核态的_IOW/_IOR参数(magic、序号、类型)完全相同。
  • copy_from_user失败:确认传入的用户空间指针有效且可读,不要传递未初始化的野指针。