【通俗易懂】详解 ioctl 函数是如何操控底层的

【通俗易懂】详解 ioctl 函数是如何操控底层的

1. ioctl函数的意义

​ 设计出 ioctl 函数的主要原因是:内核设计者希望将用户空间和内核空间的驱动模块的交互分成两部分:数据读写以及状态控制。引入 ioctl 函数之后,数据读写和状态控制将被区分开进行管理,互不干扰。

​ 另外,ioctl函数在使用时,要求用户按照内核特定的方式进行命令码的封装。此后,应用层工程师和内核驱动工程师都可以按照同一套封装标准进行命令码的封装和解析,实现了应用层和内核空间更好的对接。

2. ioctl函数的形象理解

​ 使用 ioctl 函数,本质上就是用户空间向内核空间提交一段具有特定含义的命令码,内核空间根据内核规定好的方式,对命令码进行解析,执行对应的底层操作。即:命令码和底层操作应该是一一对应的,一个具体的命令码就代表了一次底层操作的全部信息

3. ioctl 函数命令码的解析

ARM架构下的Linux内核,每个命令码都由 32bit 组成:

bit 位数31 : 3029 : 1615 : 87 : 0
代表含义数据传输方向数据传递大小设备类型码功能码
占用bit数21488

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),思路如下:

  1. 由于要制定具体是哪一盏LED亮或灭,因此必须要传递数据(比如1表示LED1,2表示LED2等)这边是往内核写数据,因此方向位写 01(只写)
  2. 由于要向内核传递是操作第几盏LED,因此这边可以传递一个无符号整型数据(unsigned int)因此数据传递大小位写 00000000000100(4)
  3. 给 LED 设备类型取一个类型码,此处随意,可取为 ‘L’,它的 ascii 码为 76,因此设备类型码位写 01001100(76)
  4. 给具体功能取一个功能码,此处随意,可以用 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 : 数据传递大小(在这里必须填入一个数据类型)
*/

因此使用内核定义好的宏定义封装我们自己的命令码的过程如下:

  1. 方向为:只写,因此使用 _IOW() 宏定义;
  2. 设备类型码取为 ‘L’;
  3. 功能码:用 10 表示亮灯功能,用 11 表示灭灯功能;
  4. 数据传递大小:这里传递的数据是用来判断操作哪一盏灯的,使用无符号整型(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;
}
  • 10
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
ioctl函数Linux系统中用于设备控制的函数,它可以用来向设备发送控制命令,或者获取设备状态信息等。 ioctl函数的原型如下: ```c int ioctl(int fd, unsigned long request, ...); ``` 其中,fd表示设备文件描述符,request表示设备控制命令,后面的可变参数用于传递控制命令的参数。 request参数一般是一个32位的整数,由四个部分组成: ```c ioctl命令 = (魔数 << 8) | 命令序号 | 方向 | 大小 ``` 其中,魔数是一个16位的数,用于标识该ioctl命令所属的设备类型;命令序号是一个8位的数,用于标识该ioctl命令的具体含义;方向用于表示该ioctl命令是读操作还是写操作;大小用于表示该ioctl命令的参数大小。 例如,下面是一个ioctl命令: ```c #define MY_IOCTL _IOW('k', 1, int) ``` 其中,'k'是魔数,1是命令序号,_IOW表示该ioctl命令是写操作,int表示该ioctl命令参数的大小。 在应用程序中调用ioctl函数时,需要传入一个指向控制命令参数的指针,如下: ```c int val = 10; ioctl(fd, MY_IOCTL, &val); ``` 在设备驱动中,可以通过switch语句来处理ioctl命令,如下: ```c long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int val; switch (cmd) { case MY_IOCTL: if (copy_from_user(&val, (int __user *)arg, sizeof(val))) return -EFAULT; // 处理MY_IOCTL命令 break; // 处理其他命令 default: return -ENOTTY; } return 0; } ``` 其中,copy_from_user函数用于将用户空间中的数据拷贝到内核空间,sizeof(val)表示拷贝的数据大小。处理完命令后,需要返回0代表成功,或者返回一个负数代表错误。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值