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;
}
编译与运行
- 将驱动代码保存为
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
- 编译并加载模块:
make
sudo insmod ioctl_demo.ko
dmesg | tail # 确认 "initialized" 日志
- 编译并运行用户态测试:
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失败:确认传入的用户空间指针有效且可读,不要传递未初始化的野指针。