【通俗易懂】详解 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) // 不传输数据