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失败:检查传入的用户空间指针是否有效,测试程序传参需使用变量地址而非值本身