《Linux设备驱动程序》学习2—高级字符设备驱动ioctl

时间:2021-03-24 17:55:00
今天进入《Linux设备驱动程序》第六章高级字符设备驱动程序操作的学习,学习的过程和简单字符设备驱动程序的学习是一样的,看书,看程序,然后就是看Tek的博客笔记。依然tek的博客中对于这一部分的知识点概括的很详细了,所以我依旧谈谈对这一部分自己的理解体会。     总的来说这一块虽然叫做高级字符设备驱动程序操作,涉及的知识点特别是函数比简单字符设备驱动少多了,但是仅仅是考虑这一点的话那就错了。前面的简单字符设备驱动函数的种类虽然比较多,但是它是有限的,可以说基本上就是那几个系统调用在驱动上的实现。相对于简单字符设备驱动,高级字符设备驱动操作的定义简单,也就意味着留给我们操作的空间也就很大了。就如书中讲的 除了读取和写入设备以外,我们还希望通过设备驱动程序执行各种类型的硬件控制,这些操作就通过ioctl来实现。可以这么理解,把简单驱动程序看成是C语言中的基本类型,如int, char, double,这些基本类型各不同,但是是有限的,而ioctl就相当于结构体类型,虽然基本的结构差不多,但是每个结构体却是各不相同的,而且需要我们自己去定义。
在用户空间下,ioctl系统条用具有如下原型:
  1. int ioctl(int fd, unsigned long cmd, ...);
而驱动程序的ioctl方法原型为:
  1. int (*ioctl) (struct innode *innode,struct file *filp, unsigned int cmd, unsigned long arg);
    虽然两者版本不同,但是其中的参数还是有很大关系的,用户空间下的fd指的是文件描述符,和驱动程序下的innode,filp相对应,而cmd参数则一致。不管可选的参数arg是否由用户给定为一个整数或一个指针,它都以一个unsigned long的形式传递。如果调用程序不传递arg参数, 被驱动收到的 arg 值是未定义的。因为在arg参数上的类型检查被关闭了,所以若一个非法参数传递给 ioctl,编译器是无法报警的,且任何关联的错误难以查找。参数可以是整数参数,也可以是指针,如果用指针的话,就可以向ioctl调用传递任意的数据了,这样设备就可以跟用户空间交换任意的数据了。当然这个指针所指向的用户空间数据必须是有效的才行,如何判断它的有效性后面会讲到。
一、ioctl步骤      实现ioctl方法的步骤一般包括两步,定义命令和实现命令
1、定义命令
    前面说过,ioctl给予我们很大的操作空间,而定义命令就是我们定义自己对设备操作的第一步了。要按 Linux 内核的约定方法为驱动选择 ioctl 的命令号, 应该首先看看 include/asm/ioctl.h 和 Documentation/ioctl-number.txt这两个文件。 

要使用的位字段符号定义在<linux/ioctl.h> :

type(幻数):8 位宽(_IOC_TYPEBITS),参考ioctl-number.txt选择一个数,并在整个驱动中使用它。

number(序数):顺序编号,8 位宽(_IOC_NRBITS)。

direction(数据传送的方向):可能的值是 _IOC_NONE(没有数据传输)、_IOC_READ、 _IOC_WRITE和 _IOC_READ|_IOC_WRITE (双向传输数据)。该字段是一个位掩码(两位), 因此可使用 AND 操作来抽取_IOC_READ 和 _IOC_WRITE。

size(数据的大小):宽度与体系结构有关,ARM为14位.可在宏 _IOC_SIZEBITS 中找到特定体系的值. 

    一般来说一个驱动程序肯定是针对某一种设备的,而命令中的幻数位段就是为了区分这个驱动程序是否是针对这个设备的,也就是说幻数和设备驱动是在一个层次上的,每个设备驱动只有一个幻数,两个不同设备驱动之间的幻数不同。一个设备驱动上可能会有好几个命令,我们就用number(序数)来区分,也就是同一设备驱动下的不同命令的序数是不一样的。

    在编程过程中, <asm/ioctl.h>定义了一些构造命令编号的宏:

  1. _IO(type,nr)/*没有参数的命令*/
  2. _IOR(type, nr, datatype)/*从驱动中读数据*/
  3. _IOW(type,nr,datatype)/*写数据*/
  4. _IOWR(type,nr,datatype)/*双向传送*/

这个头文件还定义了用来解开这个字段的宏:

  1. _IOC_DIR(nr)
  2. _IOC_TYPE(nr)
  3. _IOC_NR(nr)
  4. _IOC_SIZE(nr)

2、实现命令

    实现命令从用户空间来看就是实现ioctl这个系统调用,实现ioctl系统调用可分为实现三个技术环节:返回值、参数使用、命令操作

返回值

    ioctl的实现通常就是一个机遇命令号的switch语句,就是前面命令定义的在这个设备驱动中几个命令。但是当命令号不能匹配合法操作时,比如你用户空间下传入的命令号在设备驱动中根本就没有定义。此时,有些内核会返回 -ENVAL(Invalid argument 非法参数),这是合理的。而POSIX 标准规定:如果使用了不合适的 ioctl 命令号,应当返回-ENOTTY 。这个错误码被 C 库解释为"不合适的设备 ioctl。然而,它返回-EINVAL仍是相当普遍的。


参数使用

    前面讲用户空间ioctl系统调用中的“...”代表着可选的arg参数,不管用户程序使用的是指针还是整数值,驱动程序的ioctl方法都识别成unsigned long的形式。但是驱动在使用这个传过来的参数时,就会有区别了,如果传递的是一个整数,它可以直接使用,这是没有问题的。如果是一个指针,驱动程序也就要以指针来对待它,就要小心了。比如当一个指针指向用户空间时,就必须确保指向的用户空间时合法的,此时驱动程序就要负责对用到的用户空间地址做适当的检查了。

    用户空间地址的检测有课分为两种,一种是调用的内核函数是带有检测的,这样就不需要检测了,如

  1. copy_from_user
  2. copy_to_user
  3. get_user
  4. put_user

另外一种是调用的内核函数是不带有检测的,如

  1. __get_user
  2. __put_user

我们在调用这两个函数传输数据前,首先要通过函数access_ok验证地址。

  1. int access_ok(int type, const void *addr, unsigned long size); 
  2. //从addr指的地址下进行size个字节的type(read或者write)检测

    关于access_ok,有两点有趣之处需要注意。第一,它并没有完成验证内存的全部工作,而只检查了所引用的内存是否位于进程有对应访问权限的区域内,特别是要确保访问地址没有指向内核空间的内存区。第二,大多数驱动常年供需代码中都不需要真正调用access_ok,因为会用到更好的内存管理程序。

命令操作

    命令操作就是switch语句选中的命令下要完成的事,这是用户程序要求内核对设备做的真正的事,相当于这个函数被调用了。


二、ioctl在scull中的实现分析

1、定义命令

    下面是scull中的一些ioctl命令定义(scull.h)。这些命令是用来设置和获取驱动程序的配置参数。

  1. /* 使用“k”作为幻数 */
  2. #define SCULL_IOC_MAGIC 'k'
  3. /* 在你自己的代码中,请使用不同的8位数字*/

  4. #define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)

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

  25. /*
  26.  * The other entities only have "Tell" and "Query", because they're
  27.  * not printed in the book, and there's no need to have all six.
  28.  * (The previous stuff was only there to show different ways to do it.
  29.  */
  30. #define SCULL_P_IOCTSIZE _IO(SCULL_IOC_MAGIC, 13)
  31. #define SCULL_P_IOCQSIZE _IO(SCULL_IOC_MAGIC, 14)
  32. /* ... more to come */
 #define SCULL_IOC_MAXNR 14

    程序先定义了一个幻数,用字符“k”表示,这个是随机的,只要所有的幻数一致就行。接着定义SCULL_IOC_MAX个不同的cmd。


2、实现中的ioctl参数使用

  1. int scull_ioctl(struct inode *inode, struct file *filp,
  2.                  unsigned int cmd, unsigned long arg)
  3. {

  4.     int err = 0, tmp;
  5.     int retval = 0;
  6.     
  7.     /*
  8.      * 抽取类型和编号位字段,并拒绝错误的命令号:
  9.      * 在调用access_ok()之前返回ENOTTY(不恰当的ioctl)
  10.      */
  11.     if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
  12.     if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;

  13.     /*
  14.      * 方向是一个位掩码,而VERIFY_WRITE用于R/W传输
  15.      */
  16.     if (_IOC_DIR(cmd) & _IOC_READ)
  17.         err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
  18.     else if (_IOC_DIR(cmd) & _IOC_WRITE)
  19.         err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
  20.     if (err) return -EFAULT;

  21.     switch(cmd) {
  22. case ...
  23. ...
  24. }
  25. ...
}

    首先用解开为字段的宏来判断类型和编号是不是这个驱动程序的,也就是前面说的一个驱动中只有一个固定的幻数,这是区别去其他驱动的关键。

    特别要注意的是,定义命令的时候direction位段是从应用程序的角度看的,也就是说,IOC_READ意味着从设备中读取数据,IOC_WRITE意味着向设备中写数据。而access_ok中的第一个参数type(VERITY_READ或VERIFY_WRITE),取决于要执行的动作是要读取还是写入用户空间的内存区。所以他们之间的概念刚好相反。

3、scull中ioctl命令的实现

    正如前面说过的,scull中ioctl命令实现也就是用了一个switch语句,在每个cmd下都有不用的操作函数入口。


三、测试

    测试程序我还是运用了简单字符设备测试程序里思想,其中运用了ioctl给设备传送了参数quantum,观察默认quantum和改变quantum下的写读操作。

模块程序: ioctl.zip  

测试程序: ioctl-test.zip   


实验板测试输出:

  1. ./ioctl_test
  2. write code=20 
  3. ever out 
  4. read code=20 
  5. quantum has been changed      
  6. [0]=[1]=[2]=[3]=[4]=4
  7. [5]=[6]=[7]=[8]=[9]=9
  8. [10]=10 [11]=11 [12]=12 [13]=13 [14]=14
  9. [15]=15 [16]=16 [17]=17 [18]=18 [19]=19
  10. Now the quantum is changed 
  11. write code=
  12. write code=
  13. write code=
  14. writever out 
  15. ever out       
  16. ever out 
  17. ever out 
  18. e code=
  19. read code=
  20. read code=
  21. read code=
  22. read code=
  23. [0]=[1]=[2]=[3]=[4]=4
  24. [5]=[6]=[7]=[8]=[9]=9
  25. [10]=10 [11]=11 [12]=12 [13]=13 [14]=14
  26. [15]=15 [16]=16 [17]=17 [18]=18 [19]=19