ioctl 函数的使用

Last edited
Last updated July 5, 2023
Pages
Tags

1. 概述

在内核与用户的通信中,Linux 内核有意将正常的数据以及功能的选择用不同的函数来实现,让 read() / write() 函数专注于数据的读写。让功能的选择和设置通过 ioctl 函数来实现,比如串口通信时让 read() / write() 进行正常的数据收发,而串口波特率、数据格式的设置通过 ioctl 函数来实现。

2. API函数

2.1 函数原型

#include <sys/ioctl.h> int ioctl(int fd, unsigned long request, ...);
功能:进行 io 控制
参数
  • fd:文件描述符
  • request:功能码(看第 3 节)
  • :可以有第三个参数,也可以没有,如果添加第三个参数,尽量传递用户空间的指针
返回值:成功返回 0,失败返回错误码

2.2 内核中的操作方法

long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg){}
参数
  • cmd:用来接收用户层 ioctl 函数的第二个参数
  • arg:用来接收 ioctl 函数第三个参数

3. ioctl 函数的功能码

为了追求功能码的唯一性,我们需要对功能码进行编码,在编码时要添加实现的功能是读还是写,也要包含 ioctl 第三个参数的大小灯信息。可以在 Linux 内核源码的该目录下找到功能码的编码规则:
功能码的类别:
_IO
an
ioctl with no parameters
_IOW
an
ioctl with write parameters (copy_from_user)
_IOR
an
ioctl with read parameters (copy_to_user)
_IOWR
an
ioctl with both write and read parameters.
一个功能码是 32 位的数字,它的组成通常如下(也有特殊的,比如 powerpc 架构):
bits
meaning
31-30
00 - no parameters: uses _IO macro 10 - read: _IOR 01 - write: _IOW 11 - read/write: _IOWR
29-16
size of arguments
15-8
ascii character supposedly unique to each driver
7-0
function #
我们不需要手动去编写这个数字,Linux 内核中提供了宏函数去创建它:
#define _IOC(dir,type,nr,size) \ ((unsigned int) \ (((dir) << _IOC_DIRSHIFT) | \ ((type) << _IOC_TYPESHIFT) | \ ((nr) << _IOC_NRSHIFT) | \ ((size) << _IOC_SIZESHIFT))) // nr<<0|type<<8|size<<16<<dir<<30 /* used to create numbers */ #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size)) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size)) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
用法如下(使用不带参数的 ioctl):
#define LED_ON _IO('l', 1) //开灯 #define LED_OFF _IO('l', 0) //关灯
  • 第一个参数对应 15-8 位驱动唯一 ascii 编号
  • 第二个参数用于对应 7-0 调用函数
  • 如果有第三个参数,则写入 29-16 表示数据的大小

4. 实例一:不带参数

完整的目录查看:
这里节选重要的部分代码:
/* head.h 文件 */ #define LED_ON _IO('l', 1) //开灯 #define LED_OFF _IO('l', 0) //关灯 //---------------------------------------------- /* mychrdev.c 文件 */ struct class *cls; struct device *dev; // ...... long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case LED_ON: //开灯 vir_led1->ODR |= (0x1 << 10); break; case LED_OFF: //关灯 vir_led1->ODR &= (~(0X1 << 10)); break; } return 0; } // ...... struct file_operations fops = { .open = mycdev_open, .unlocked_ioctl = mycdev_ioctl, .release = mycdev_close, }; //---------------------------------------------- /* test.c 文件 */ if (a == 1) { // 开灯 ioctl(fd, LED_ON); } else if (a == 0) { //关灯 ioctl(fd, LED_OFF); }
可以看到和之前
字符设备驱动控制 LED 灯实验
中的代码的主要区别是,去除了 openwrite 的操作方法,改用 unlocked_ioctl 替代。在应用层的测试代码中,需要包含同样的头文件,然后使用 ioctl 来控制。

5. 实例二:带整型参数

完整的目录查看:
这里节选重要的部分代码:
/* head.h 文件 */ #define LED_ON _IOW('l', 1, int) //开灯 #define LED_OFF _IOW('l', 0, int) //关灯 //---------------------------------------------- /* mychrdev.c 文件 */ long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case LED_ON: // 开灯 switch (arg) { case 1: // LED1 vir_led1->ODR |= (0x1 << 10); break; case 2: // LED2 vir_led2->ODR |= (0x1 << 10); break; case 3: // LED3 vir_led3->ODR |= (0x1 << 8); break; } break; case LED_OFF: // 关灯 switch (arg) { case 1: vir_led1->ODR &= (~(0X1 << 10)); break; case 2: vir_led2->ODR &= (~(0X1 << 10)); break; case 3: vir_led3->ODR &= (~(0X1 << 8)); break; } break; } return 0; } //---------------------------------------------- /* test.c 文件 */ int a, b; // ...... if (a == 1) { ioctl(fd, LED_ON, b); } else if (a == 0) { ioctl(fd, LED_OFF, b); }
扩展了上面实例一的代码,通过参数来指定点亮的 LED 的编号,其中 mycdev_ioctl 方法的第三个参数 arg 直接当作整型使用。且使用的宏函数从 _IO 换到了 _IOW

6. 实例三:带指针参数

完整的目录查看:
这里节选重要的部分代码:
/* head.h 文件 */ #define LED_ON _IOW('l', 1, int) //开灯 #define LED_OFF _IOW('l', 0, int) //关灯 //---------------------------------------------- /* mychrdev.c 文件 */ long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int which; //通过拷贝函数获取ioctl第三个参数对应空间的数值 int ret = copy_from_user(&which, (void *)arg, 4); if (ret) { printk("拷贝用户空间数据失败\n"); return -EIO; } //进行功能控制 switch (cmd) { case LED_ON: // 开灯 switch (which) { case 1: // LED1 vir_led1->ODR |= (0x1 << 10); break; case 2: // LED2 vir_led2->ODR |= (0x1 << 10); break; case 3: // LED3 vir_led3->ODR |= (0x1 << 8); break; } break; case LED_OFF: // 关灯 switch (which) { case 1: vir_led1->ODR &= (~(0X1 << 10)); break; case 2: vir_led2->ODR &= (~(0X1 << 10)); break; case 3: vir_led3->ODR &= (~(0X1 << 8)); break; } break; } return 0; } //---------------------------------------------- /* test.c 文件 */ int a, b; // ...... if (a == 1) { ioctl(fd, LED_ON, &b); } else if (a == 0) { ioctl(fd, LED_OFF, &b); }
虽然头文件中 __IOW 的第三个参数还是 int ,但它在这里指代指针的大小。在驱动代码中,使用了 copy_from_user 来读取用户空间的对应地址的数据,读取数据的大小是我们预先设计好的,不能从用户传递进驱动。我们在测试文件中传入 b 的地址,驱动会拷贝用户空间中对应地址的数据。