2. ioctl命令实现

任务目标

在Linux字符设备驱动中实现ioctl接口,定义设备控制命令(设置参数、获取状态),并编写用户空间测试程序完成验证。本案例基于虚拟设备实现,不依赖具体硬件。

环境依赖

  • Linux内核源码树(已编译,版本与运行内核一致)
  • 交叉编译工具链(若目标为ARM板)或本机gcc(若在本机验证)
  • 内核头文件:linux/ioctl.h、linux/cdev.h、linux/uaccess.h

驱动实现

驱动维护一个32位状态寄存器和一个32位参数寄存器。ioctl支持以下命令:

  • DEVICE_IOC_SET_PARAM:写入参数(带方向,从用户空间拷贝)
  • DEVICE_IOC_GET_STATUS:读取状态(带方向,拷贝到用户空间)
  • DEVICE_IOC_TOGGLE_BIT:翻转状态寄存器的指定位(无数据方向)
```c // device_ioctl.c #include #include #include #include #include #include #include #include #define DEVICE_NAME "ioctl_demo" #define CLASS_NAME "ioctl_class" // 设备结构 struct demo_dev { unsigned int status; unsigned int param; struct cdev cdev; }; static struct demo_dev *dev_ptr; static dev_t dev_num; static struct class *dev_class; // 定义ioctl命令 // 类型 'D',编号1-3 #define DEVICE_IOC_MAGIC 'D' #define DEVICE_IOC_SET_PARAM _IOW(DEVICE_IOC_MAGIC, 1, unsigned int) #define DEVICE_IOC_GET_STATUS _IOR(DEVICE_IOC_MAGIC, 2, unsigned int) #define DEVICE_IOC_TOGGLE_BIT _IO(DEVICE_IOC_MAGIC, 3) static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { unsigned int val; unsigned int bit_mask; switch (cmd) { case DEVICE_IOC_SET_PARAM: if (copy_from_user(&val, (void __user *)arg, sizeof(val))) return -EFAULT; dev_ptr->param = val; pr_info("ioctl: param set to 0x%x\n", val); break; case DEVICE_IOC_GET_STATUS: if (copy_to_user((void __user *)arg, &dev_ptr->status, sizeof(dev_ptr->status))) return -EFAULT; pr_info("ioctl: status read as 0x%x\n", dev_ptr->status); break; case DEVICE_IOC_TOGGLE_BIT: // arg为要翻转的位号(0~31) if (arg > 31) return -EINVAL; bit_mask = 1U << arg; dev_ptr->status ^= bit_mask; pr_info("ioctl: toggle bit %lu, status now 0x%x\n", arg, dev_ptr->status); break; default: return -ENOTTY; } return 0; } static int device_open(struct inode *inode, struct file *file) { return 0; } static int device_release(struct inode *inode, struct file *file) { return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = device_open, .release = device_release, .unlocked_ioctl = device_ioctl, }; static int __init demo_init(void) { int ret; // 分配设备号 ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { pr_err("Failed to alloc region\n"); return ret; } // 分配设备结构 dev_ptr = kzalloc(sizeof(*dev_ptr), GFP_KERNEL); if (!dev_ptr) { unregister_chrdev_region(dev_num, 1); return -ENOMEM; } // 初始化cdev cdev_init(&dev_ptr->cdev, &fops); dev_ptr->cdev.owner = THIS_MODULE; ret = cdev_add(&dev_ptr->cdev, dev_num, 1); if (ret) { kfree(dev_ptr); unregister_chrdev_region(dev_num, 1); return ret; } // 创建设备类与设备节点 dev_class = class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(dev_class)) { cdev_del(&dev_ptr->cdev); kfree(dev_ptr); unregister_chrdev_region(dev_num, 1); return PTR_ERR(dev_class); } device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME); dev_ptr->status = 0x0; dev_ptr->param = 0x0; pr_info("ioctl_demo: initialized\n"); return 0; } static void __exit demo_exit(void) { device_destroy(dev_class, dev_num); class_destroy(dev_class); cdev_del(&dev_ptr->cdev); kfree(dev_ptr); unregister_chrdev_region(dev_num, 1); pr_info("ioctl_demo: exited\n"); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Driver Lab"); MODULE_DESCRIPTION("ioctl command demo driver"); ```

用户空间测试程序

```c // test_ioctl.c #include #include #include #include #include #include #include #define DEVICE_PATH "/dev/ioctl_demo" // 与驱动中定义保持一致 #define DEVICE_IOC_MAGIC 'D' #define DEVICE_IOC_SET_PARAM _IOW(DEVICE_IOC_MAGIC, 1, unsigned int) #define DEVICE_IOC_GET_STATUS _IOR(DEVICE_IOC_MAGIC, 2, unsigned int) #define DEVICE_IOC_TOGGLE_BIT _IO(DEVICE_IOC_MAGIC, 3) int main(void) { int fd; unsigned int param = 0xA5A5A5A5; unsigned int status = 0; int ret; fd = open(DEVICE_PATH, O_RDWR); if (fd < 0) { perror("open"); return 1; } // 1. 设置参数 ret = ioctl(fd, DEVICE_IOC_SET_PARAM, ¶m); if (ret < 0) { perror("ioctl SET_PARAM"); close(fd); return 1; } printf("SET_PARAM: sent 0x%x\n", param); // 2. 翻转bit 3 ret = ioctl(fd, DEVICE_IOC_TOGGLE_BIT, 3); if (ret < 0) { perror("ioctl TOGGLE_BIT"); close(fd); return 1; } printf("TOGGLE_BIT: bit 3 toggled\n"); // 3. 翻转bit 3再次(恢复) ret = ioctl(fd, DEVICE_IOC_TOGGLE_BIT, 3); if (ret < 0) { perror("ioctl TOGGLE_BIT 2nd"); close(fd); return 1; } printf("TOGGLE_BIT: bit 3 toggled again\n"); // 4. 翻转bit 7 ret = ioctl(fd, DEVICE_IOC_TOGGLE_BIT, 7); if (ret < 0) { perror("ioctl TOGGLE_BIT bit7"); close(fd); return 1; } printf("TOGGLE_BIT: bit 7 toggled\n"); // 5. 获取状态 ret = ioctl(fd, DEVICE_IOC_GET_STATUS, &status); if (ret < 0) { perror("ioctl GET_STATUS"); close(fd); return 1; } printf("GET_STATUS: read 0x%x\n", status); close(fd); return 0; } ```

编译与加载

驱动编译使用内核Makefile机制。创建Makefile:

```makefile obj-m := device_ioctl.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean ```

执行make生成device_ioctl.ko。加载与测试:

```bash sudo insmod device_ioctl.ko # 确认设备节点存在 ls -l /dev/ioctl_demo # 编译用户测试程序 gcc -o test_ioctl test_ioctl.c # 运行测试 sudo ./test_ioctl # 查看内核日志 dmesg | tail -20 # 卸载驱动 sudo rmmod device_ioctl ```

验证方法与判定标准

测试程序预期输出如下(与代码逻辑严格对应):

``` SET_PARAM: sent 0xa5a5a5a5 TOGGLE_BIT: bit 3 toggled TOGGLE_BIT: bit 3 toggled again TOGGLE_BIT: bit 7 toggled GET_STATUS: read 0x80 ```

判定通过条件:

  • 测试程序输出与上述预期一致,GET_STATUS读回值为0x80(仅bit7被置位,bit3两次翻转后恢复为0)
  • dmesg中能看到驱动打印的对应ioctl操作日志,无EFAULT或ENOTTY错误
  • 卸载模块时无资源泄漏警告

常见问题

  • 设备节点未生成:确认udev规则或手动执行mknod /dev/ioctl_demo c $(grep ioctl_demo /proc/devices | awk '{print $1}') 0
  • ioctl返回-1且errno=ENOTTY:用户空间命令号与内核定义不一致,检查_IOW/_IOR/_IO参数是否完全相同
  • copy_from_user/copy_to_user失败:检查传入的用户空间指针是否有效,测试程序传参需使用变量地址而非值本身
用户空间 test_ioctl open() / ioctl() / close() 系统调用接口 内核空间 VFS (struct file_operations) device_ioctl() switch(cmd) 分发处理 _IOW/_IOR/_IO 宏编码 SET_PARAM | GET_STATUS | TOGGLE_BIT 数据流与命令方向 SET_PARAM (_IOW) 用户 → 内核 (copy_from_user) GET_STATUS (_IOR) 内核 → 用户 (copy_to_user) TOGGLE_BIT (_IO) 无数据拷贝,arg为位号 设备结构体 status: uint32 | param: uint32