【通俗易懂】详解 ioctl 函数是如何操控底层的
1. ioctl函数的意义
设计出 ioctl 函数的主要原因是:内核设计者希望将用户空间和内核空间的驱动模块的交互分成两部分:数据读写以及状态控制。引入 ioctl 函数之后,数据读写和状态控制将被区分开进行管理,互不干扰。
另外,ioctl函数在使用时,要求用户按照内核特定的方式进行命令码的封装。此后,应用层工程师和内核驱动工程师都可以按照同一套封装标准进行命令码的封装和解析,实现了应用层和内核空间更好的对接。
2. ioctl函数的形象理解
使用 ioctl 函数,本质上就是用户空间向内核空间提交一段具有特定含义的命令码,内核空间根据内核规定好的方式,对命令码进行解析,执行对应的底层操作。即:命令码和底层操作应该是一一对应的,一个具体的命令码就代表了一次底层操作的全部信息。
3. ioctl 函数命令码的解析
ARM架构下的Linux内核,每个命令码都由 32bit 组成:
bit 位数 | 31 : 30 | 29 : 16 | 15 : 8 | 7 : 0 |
---|---|---|---|---|
代表含义 | 数据传输方向 | 数据传递大小 | 设备类型码 | 功能码 |
占用bit数 | 2 | 14 | 8 | 8 |
3.1 数据传输方向:
用户从内核读取数据 or 用户向内核传递数据
[00] 表示不传递数据,内核宏定义为: _IO
[10] 表示只读,内核定义为: _IOR
[01] 表示只写,内核宏定义为: _IOW
[11] 表示可读可写,内核宏定义为: _IOWR
3.2 数据传递大小:
用户空间和内核传递的数据的大小
当使用宏定义进行命令码封装的时候,这边就必须填写数据类型:
例如无符号四字节就要填 int,一个长度为20的字符数组就要写 char [20]
原因是:可以从底层封装中得知:这个地方的数据大小,底层是通过 sizeof() 实现的,且这边必须填一个常量,因此要填入数据类型
3.3 设备类型码:
每一个驱动通过一个唯一的字符来代表,只是为了区分开不同的设备
例如:LED驱动用 ‘L’ 代表,串口驱动用 ‘U’ 代表
3.4 功能码:
不同的功能通过功能码进行区分,这个自己指定即可
例如:开启LED的功能码为10,关闭LED的功能码为20
4. 使用内核定义好的宏进行命令码封装
例如要封装一个:让LED亮或灭的命令码(可指定哪一盏LED),思路如下:
- 由于要制定具体是哪一盏LED亮或灭,因此必须要传递数据(比如1表示LED1,2表示LED2等)这边是往内核写数据,因此方向位写 01(只写)
- 由于要向内核传递是操作第几盏LED,因此这边可以传递一个无符号整型数据(unsigned int)因此数据传递大小位写 00000000000100(4)
- 给 LED 设备类型取一个类型码,此处随意,可取为 ‘L’,它的 ascii 码为 76,因此设备类型码位写 01001100(76)
- 给具体功能取一个功能码,此处随意,可以用 10 表示亮灯功能,用 11 表示灭灯功能,因此功能码位写 00001010(10 亮灯)或者 00001011(11 灭灯)
综上所述,手动封装一个命令码的结果是:
亮灯:(0b) 01 00000000000100 01001100 00001010
灭灯:(0b) 01 00000000000100 01001100 00001011
用宏定义封装即为:
#define LED_ON_FUNC 0b01000000000001000100110000001010
#define LED_OFF_FUNC 0b01000000000001000100110000001011
使用内核定义好的宏定义可以大大简化命令码封装过程,内核的宏定义如下:
#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))) // 可读可写
/*
@ type : 设备类型码
@ nr : 功能码
@ size : 数据传递大小(在这里必须填入一个数据类型)
*/
因此使用内核定义好的宏定义封装我们自己的命令码的过程如下:
- 方向为:只写,因此使用 _IOW() 宏定义;
- 设备类型码取为 ‘L’;
- 功能码:用 10 表示亮灯功能,用 11 表示灭灯功能;
- 数据传递大小:这里传递的数据是用来判断操作哪一盏灯的,使用无符号整型(4字节)也就是 int 类型
综上,使用内核定义好的宏定义封装命令码的过程如下:
#define LED_ON_FUNC _IOW('L', 10, int)
#define LED_OFF_FUNC _IOW('L', 11, int)
可以看到使用内核定义好的宏定义封装命令码,可以大大简化封装过程。
5. ioctl函数的分析
5.1 用户空间应用层的 ioctl函数
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
/*
功能:控制驱动设备工作状态
参数:
@ fd : 文件描述符
@ request : 命令码
@ ...... : 传递的数据,必须传递数据的地址(可以理解为在用户空间和内核空间的传递只能用地址传递了,不能值传递了)
写成省略号是因为:可以不传递数据
返回值:
成功 返回0
失败 返回 -1 并置位错误码
*/
5.2 内核空间的 ioctl 函数
这个函数位于上文提到过的 fops 操作方法结构体中,这个结构体中都是函数指针,把这个结构体中的成员赋值为自己封装的函数的函数名,就相当于告诉内核,用户在应用层进行系统调用操作的时候,应该匹配到哪个函数进行执行。
这个函数指针的返回值类型为:long (*unlocked_ioctl) 类型
形参列表为:struct file *file, unsigned int cmd, unsigned long arg
其中 cmd 就相当于等着接用户传递进来的命令码,arg 就相当于等着接用户传递进来的数据的地址
6. ioctl函数使用的实例
6.1 应用层
#include <sys/ioctl.h>
/* 使用宏定义封装命令码 表示只写 设备类型码为 'L' 点灯功能码为 10 or 11 传输数据大小为 int 的大小*/
#define LED_ON_FUNC _IOW('L', 10, int)
#define LED_OFF_FUNC _IOW('L', 11, int)
.........
int main(int argc, const char *argv[])
{
int fd; // 文件描述符
int ledSwitch = 0; // 用来表示操作哪盏 LED
if((fd = open("/dev/ledDev", O_RDWR)) == -1)
{
perror("Open dev failed");
return -1;
}
ledSwitch = 2; // 表示要操作 LED2
/* 使用 ioctl 函数 注意传入的是 ledSwitch 的地址*/
if((ioctl(fd, LED_ON_FUNC, &ledSwitch)) == -1)
{
perror("ioctl failed");
return -1;
}
}
6.2 内核驱动层
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/uaccess.h>
const struct file_operations fops = {
/* 操作方法结构体中的 unlocked_ioctl 成员 赋值为 ledDev_ioctl
之后用户在应用层调用 ioctl 函数就会关联到 ledDev_ioctl 执行*/
.unlocked_ioctl = ledDev_ioctl
};
..........
/* 自己封装的 准备被应用层 ioctl 函数关联的函数 */
long ledDev_ioctl(struct file* file, unsigned int cmd, unsigned long arg)
{
int ret; // 记录数据搬运函数的返回值
int ledSwitch_kernal; // 用来接应用层传进来的 ledSwitch 的值
switch(cmd)
{
case LED_ON_FUNC:
/* arg 是应用层传上来的代表 LED 编号的地址 这里强转成 (void *) 类型 保证编译器识别
对 ledSwitch_kernal 取地址 表示把数据存到这个地址
4 表示搬运数据的大小为 4字节(int)
总结来说 就是从应用层传递的数据的地址上取地址 放在要接数据的变量的地址上
从而实现值的传递
*/
if((ret = copy_from_user(&ledSwitch_kernal, (void *)arg, 4)) != 0)
{
printk("Copy from user failed\n");
return -EIO;
}
if(ledSwitch_kernal == 2)
{
......... // 让 LED2 点亮的寄存器操作
}
case LED_OFF_FUNC:
if((ret = copy_from_user(&ledSwitch_kernal, (void *)arg, 4)) != 0)
{
printk("Copy from user failed\n");
return -EIO;
}
if(ledSwitch_kernal == 2)
{
......... // 让 LED2 熄灭的寄存器操作
}
break;
}
return 0;
}