【专业技术】Linux设备驱动第六篇:高级字符驱动操作之iotcl

在之前我们介绍了如何实现一个简单的字符设备驱动,并介绍了简单的open,close,read,write等驱动提供的基本功能。但是一个真正的设备驱动往往提供了比简单读写更高级的功能。这一篇我们就来介绍一些驱动动中使用的一些高级的操作的实现。

大部分驱动除了提供对设备的读写操作外,还需要提供对硬件控制的接口,比如查询一个framebuffer设备能提供多大的分辨率,读取一个RTC设备的时间,设置一个gpio的高低电平等等。而这些对硬件操作能力的实现一般都是通过ioctl方法来实现的

1.原型介绍

Ioctl在用户空间的原型为:

int ioctl(int fd, unsigned long cmd, ...);

原型中的点不表示一个变数目的参数, 而是一个单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查. 第 3个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 ). 一些命令不用参数, 一些用一个整数值, 以及一些使用指向其他数据的指针. 使用一个指针是传递任意数据到 ioctl 调用的方法; 设备接着可与用户空间交换任何数量的数据.

ioctl在内核空间的原型为:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能对此作出警告。

2.iotcl cmd的选择

在实现ioctl之前,我们应该来定义一组ioctl命令,一种简单的方法是使用一组简单的数字来标示,比如从0到9。这种情况一般也没有问题,但是最好不要这样做,ioctl的cmd应该是在系统内是唯一的,这样可以防止向错误的设备发出正确的命令。而如果ioctl命令在系统内是唯一的,那么就不会发生这种情况。

Linux中把ioctl cmd划分成几个位段来帮助创建唯一的cmd。这几个位段一般是:type(模数),序号,传输方向和参数大小。在定义的时候可以参考include/asm/ioctl.h 和 Documentation/ioctl-number.txt两个文件,头文件定义了构建cmd命令的宏,而ioctl-number.txt列举了内核中已经使用的tpye,为了唯一性,尽量不要和这里的type重叠。

以下是这几个位段的简单介绍:

type

魔数. 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中. 这个成员是 8 位宽(_IOC_TYPEBITS).

number

序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.

direction

数据传送的方向,如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.

size

涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值. 你使用这个 size 成员不是强制的 - 内核不检查它 -- 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构, 但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员.

下面是一个定义ioctl命令的展示:

/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */

#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
 * S means "Set" through a ptr,
 * T means "Tell" directly with the argument value
 * G means "Get": reply by setting through a pointer
 * Q means "Query": response is on the return value
 * X means "eXchange": switch G and S atomically
 * H means "sHift": switch T and Q atomically
 */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
#define SCULL_IOC_MAXNR 14

关于_IOWR等宏定义更多的内容可参考头文件中的定义。

3. IOCTL的返回值

IOCTL的实现往往都是一个switch case语句,返回值依赖每个case分支的实现。当遇到没有定义的cmd时改返回什么值呢,我建议使用-EINVAL,表示无用的参数。另外一点,在case分支比较多的时候,有些人大意常常会忘记写break,导致后面的case分支同样执行,导致错误发生。

4. IOCTL的arg参数

有些ioctl命令并不需要arg这个参数,而大部分ioctl需要在应用层和内核层传递数据,就需要用到这个参数。当arg参数是一个整形的时候,非常简单,我们直接拿来使用就可以了。如果是一个指针,就需要小心一些。

应用层和内核层的数据交换我们常用的是copy_from_user 和 copy_to_user 函数,它们可用来安全地用来移动数据。 这些函数也可用在 ioctl 方法中。但是ioctl中的数据项常常是很小的数据,用这两个函数有点笨重了,我们可以尝试使用其他的方式来实现数据的传递。

int access_ok(int type, const void *addr, unsigned long size);

这个函数用来检查给定的地址是否满足特定的访问需求,这个函数只检查而没有数据copy。使用access_ok之后就可以安全地传输数据。可使用下面的接口来做数据的传输:

put_user(datum, ptr)

__put_user(datum, ptr)

这些宏定义写 datum 到用户空间;它们相对快,且应当被调用来代替 copy_to_user 无论何时要传送单个值时。这些宏已被编写来允许传递任何类型的指针到 put_user, 只要它是一个用户空间地址。传送的数据大小依赖 prt 参数的类型,并且在编译时使用 sizeof 和 typeof 等编译器内建宏确定。结果是,如果 prt 是一个 char 指针,传送一个字节,以及对于 2, 4, 和 可能的 8 字节。

put_user 检查来确保这个进程能够写入给定的内存地址,它在成功时返回 0,并且在错误时返回 -EFAULT。 __put_user 进行更少的检查(它不调用 access_ok),但是仍然能够失败如果被指向的内存对用户是不可写的。因此, __put_user 应当只用在内存区已经用 access_ok 检查过的时候。

作为一个通用的规则,当你实现一个 read 方法时,调用 __put_user 来节省几个周期,或者当你拷贝几个项时,因此, 在第一次数据传送之前调用 access_ok 一次, 如同上面 ioctl 所示。

get_user(local, ptr) __get_user(local, ptr)

这些宏定义用来从用户空间接收单个数据。它们象 put_user 和 __put_user,但是在相反方向传递数据。获取的值存储于本地变量 local; 返回值指出这个操作是否成功。再次, __get_user 应当只用在已经使用 access_ok 校验过的地址。

原文发布于微信公众号 - 程序员互动联盟(coder_online)

原文发表时间:2015-07-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java成神之路

java分层架构概念

1.JAVA中Action层, Service层 ,modle层 和 Dao层的功能区分?(下面所描述的service层就是biz)       首先这是现在...

804
来自专栏青枫的专栏

day56_BOS项目_08

  注意1:权限数据属于比较特殊的数据,系统在上线之后,必须先把权限数据给它初始化到数据库中去,然后这个系统才可以跑起来。如果不初始化权限数据的话,那么登录上系...

702
来自专栏你不就像风一样

[原创]一款小巧、灵活的Java多线程爬虫框架(AiPa)

AiPa 只需要使用者提供网址集合,即可在多线程下自动爬取,并对一些异常进行处理。

473
来自专栏IT杂记

Java Socket Timeout总结

1. Socket timeout     Java socket有如下两种timeout: 建立连接timeout,暂时就叫 connect timeout;...

3569
来自专栏Java 技术分享

Struts2 学习之小白开始

2966
来自专栏Play & Scala 技术分享

PlayScala 2.5.x - 关于Content-Type的注意事项

3164
来自专栏web编程技术分享

JavaScript : 浅讲ajax1.ajax入门案例

1815
来自专栏Java帮帮-微信公众号-技术文章全总结

JavaWeb08-XML,tomcat,HTTP轻松入门

XML&tomcat&HTTP 一.XML基础知识 1. xml介绍 XML 指可扩展标记语言(EXtensible Markup Language),也是一种...

3569
来自专栏yukong的小专栏

【SpringBoot系列04】SpringBoot之使用JPA完成简单的rest api

【SpringBoot2.0系列02】SpringBoot之使用Thymeleaf视图模板

1516
来自专栏大内老A

[WCF REST] Web消息主体风格(Message Body Style)

对于Web HTTP编程模型来说,服务契约中作为操作的方法无须应用OperationContractAttribute特性,只需要根据需要应用WebGetAtt...

1769

扫码关注云+社区