在之前我们介绍了怎样实现一个简单的字符设备驱动。并介绍了简单的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參数是一个整形的时候,很easy。我们直接拿来使用就能够了。假设是一个指针,就须要小心一些。
应用层和内核层的数据交换我们经常使用的是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 校验过的地址。
以上是ioctl操作相关的内容,因为篇幅原因。就写到这里。下一节接着写一些其它的高级操作方法,比如堵塞IO、非堵塞IO等。敬请关注。
之前系列文章例如以下,欢迎阅读关注:
Linux设备驱动第四篇:以Oops信息定位代码行为例谈驱动调试方法
本文属原创,转载请注明出处。违者必究
关注微信公众平台:程序猿互动联盟(coder_online),你能够第一时间获取原创技术文章,和(java/C/C++/Android/Windows/Linux)技术大牛做朋友,在线交流编程经验。获取编程基础知识,解决编程问题。程序猿互动联盟,开发者自己的家。