linux系统调用之 ioctl 函数介绍

1. ioctl为什么会出现

  1. 虽然在文件操作结构体 “struct file_operations” 中有很多对应的设备操作函数,但是有些命令是实在找不到对应的操作函数。如CD-ROM的驱动,想要一个弹出光驱的操作,这种操作并不是所有的字符设备都需要的,所以文件操作结构体也不会有对应的函数操作。

  2. 大部分驱动除了需要具备读写设备的能力之外,还需要具备对硬件控制的能力,用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。

  3. 编写应用的程序猿要做的事只需要通过驱动开发者在驱动相关的 “.h” 文件中提供的命令码进行发送相应指令,命令码用文字进行了说明,因为有些驱动是加密的,里面包含了ioctl的操作,不会给你看源码,但是会提供 “.h” 文件,里面包含了该驱动所有命令码的解释与操作结果说明,这样一来你只要理解命令码即可,应用程序员可快速开发上手。

总结:出于这样的原因,ioctl就有它的用处了————一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。所以,ioctl函数里面一般都实现了多个对硬件的操作步骤,将他们综合起来完成一个任务,而不是单一的某个write/read操作,通过应用层传入的命令来调用相应的操作。

2. ioctl 函数原型

  1. 应用程序中,使用 ioctl 系统调用来控制设备,原型如下
/*
* fd: 文件描述符
* cmd: 控制命令
* ...: 可选参数:插入*argp,具体内容依赖于cmd
*/
intioctl(int fd, unsigned long cmd, ...);
  1. 驱动程序中,使用 ioctl 用来操作硬件,原型如下
/*
* inode 与 filp 两个指针对应于应用程序传递的文件描述符 fd,这和传递 open 方法的参数一样。
* cmd 由用户空间 直接不经修改的传递给驱动程序
* arg 可选参数
*/
int(*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

总结:当我们把驱动加载到内核的时候,驱动会调用内核的注册函数,将驱动的 file_operations 注册,这个时候,我们通过应用层打开该驱动的设备文件 fd,调用 ioctl 时,会自动挂接到指定驱动中的 ioctl 函数,完成指定的硬件操作。

3. ioctl 函数几个重要参数

  1. cmd 为什么会变得如此复杂
    a> 所谓的复杂命令无非也是一个32bit的整数,比如用一个最简单的方式定义一个命令也是可以的。
     #define TEST_CLEAR 155

如上但内核开发人员发现这样有点不对劲。如果有两个不同的设备,但它们的 ioctlcmd 却一样的。

    #define TEST_OPEN 155

当程序代码量,功能,模块太大时,哪天应用开发者没有注意,在操作某一个功能时,本来要打开A设备,操作A硬件,不小心写错了代码打开了B设备,并且调用 ioctl,这样就完蛋了。因为这个文件里面同样有 cmd 对应实现,你就无故的影响了B驱动的正常使用。为了防止这样的事情发生,内核对 cmd 又有了新的定义,规定了cmd都应该不一样。

使用复杂 cmd 后代码:

#define TEST_MAGIC 'x' //定义幻数

#define TEST_MAX_NR 3 //定义命令的最大序数

#define TEST_CLEAR _IO(TEST_MAGIC, 1)

#define TEST_OFFSET _IO(TEST_MAGIC, 2)

#define TEST_KBUF _IO(TEST_MAGIC, 3)
  1. cmd 的组成

在驱动程序中实现的 ioctl 函数体内,实际上是有一个 switch {case} 结构,每一个 case 对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情,因为设备都是特定的。关键在于怎么样组织命令码,因为在 ioctl 中命令码是唯一联系用户程序命令和驱动程序支持的途径。

在Linux核心中是这样定义一个命令码的:

设备类型序 列 号方 向数据 尺寸
8 bit8 bit2 bit8~14 bit

a> 幻数:说得再好听的名字也只不过是个0~0xff的数,占8bit(_IOC_TYPEBITS)。这个数是用来区分不同的驱动的,像设备号申请的时候一样,内核有一个文档给出一些推荐的或者已经被使用的幻数。

b> 序数:用这个数来给自己的命令编号,占8bit(_IOC_NRBITS),我的程序从1开始排序。

c> 数据传输方向:占2bit(_IOC_DIRBITS)。如果涉及到要传参,内核要求描述一下传输的方向,传输的方向是以应用层的角度来描述的。

d> 数据大小:与体系结构相关,ARM下占14bit(_IOC_SIZEBITS),如果数据是int,内核给这个赋的值就是sizeof(int)

总结:这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以Kernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。

如下:

//nr为序号,命令最大个数,datatype为数据类型,如 int

_IO(type, nr)//没有参数的命令

_IOR(type, nr, datatype)//从驱动中读数据

_IOW(type, nr, datatype)//写数据到驱动

_IOWR(type, nr, datatype)//双向传送

定义命令例子:

#define MEM_IOC_MAGIC'm'//定义类型

#define MEM_IOCSET_IOW(MEM_IOC_MAGIC, 0, int)

#define MEM_IOCGQSET_IOR(MEM_IOC_MAGIC, 1, int)

cmd 最后达到的目的:既然这么费劲定义了命令,当然要检验命令是否有效,所有我们在驱动中就可以添加验证代码,判断命令是否为该驱动的命令

if(_IOC_TYPE(cmd) != TEST_MAGIC) return - EINVAL;

if(_IOC_NR(cmd) > TEST_MAX_NR) return - EINVAL;
  1. ioctl 中的 arg 之传参

应用层的 ioctl 的第三个参数是"…",这个跟 printf 的 “…” 可不一样,printf 中是意味这你可以传任意个数的参数,而 ioctl 最多也只能传一个,"…" 的意思是让内核不要检查这个参数的类型。也就是说,从用户层可以传入任何参数,只要你传入的个数是 1

一般会有两种的传参方法:

a> 整数,那可是省力又省心,直接使用就可以了。

如下代码:作为运算参数

应用层:

ioctl(fd, TEST_OFFSET, 10);

驱动层:

switch(cmd)
{
	case TEST_OFFSET:
	filp->f_pos += (int)arg;
}

b> 指针,通过指针的就传什么类型都可以了,当然用起来就比较烦。

(1) 考虑到参数不可能永远只是一个正数这么简单,如果要传多一点的东西,譬如是结构体,那就得用上指针了。

(2) 一讲到从应用程序传来的指针,就得想起我邪恶的传入了非法指针的例子。所以,驱动程序中任何与应用层打交道的指针,都得先检验指针的安全性。

代码如下:因为指针是从用户程序传来,所以必须检查安全性

驱动代码 :

if(copy_from_user(&val, (struct ioctl_data *)arg, sizeof(struct ioctl_data))
{
	ret = - EFAULT;
	goto RET;
}

应用代码

struct ioctl_data my_data = 
{
	.size = 10,
	.buf = "123456789"
};
ioctl(fd, TEST_KBUF, &my_data);
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值