在之前我们介绍了如何实现一个简单的字符设备驱动,并介绍了简单的open,close,read,write等驱动提供的基本功能。但是一个真正的设备驱动往往提供了比简单读写更高级的功能。这一篇我们就来介绍一些驱动动中使用的一些高级的操作的实现。
大部分驱动除了提供对设备的读写操作外,还需要提供对硬件控制的接口,比如查询一个framebuffer设备能提供多大的分辨率,读取一个RTC设备的时间,设置一个gpio的高低电平等等。而这些对硬件操作能力的实现一般都是通过ioctl方法来实现的
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 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能对此作出警告。
在实现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等宏定义更多的内容可参考头文件中的定义。
IOCTL的实现往往都是一个switch case语句,返回值依赖每个case分支的实现。当遇到没有定义的cmd时改返回什么值呢,我建议使用-EINVAL,表示无用的参数。另外一点,在case分支比较多的时候,有些人大意常常会忘记写break,导致后面的case分支同样执行,导致错误发生。
有些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 校验过的地址。
以上是ioctl操作相关的内容,由于篇幅原因,就写到这里,下一节接着写一些其他的高级操作方法,例如阻塞IO、非阻塞IO等。敬请关注。
之前系列文章如下,欢迎阅读关注:
Linux设备驱动第一篇:设备驱动程序简介
Linux设备驱动第二篇:构造和运行模块
Linux设备驱动第三篇:写一个简单的字符设备驱动
Linux设备驱动第四篇:以Oops信息定位代码行为例谈驱动调试方法
Linux设备驱动第五篇:驱动中的并发与竟态
本文属原创,转载请注明出处,违者必究
关注微信公众平台:程序员互动联盟(coder_online),你可以第一时间获取原创技术文章,和(java/C/C++/Android/Windows/Linux)技术大牛做朋友,在线交流编程经验,获取编程基础知识,解决编程问题。程序员互动联盟,开发人员自己的家。