在用户空间下,ioctl系统条用具有如下原型:
- int ioctl(int fd, unsigned long cmd, ...);
- int (*ioctl) (struct innode *innode,struct file *filp, unsigned int cmd, unsigned long arg);
一、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>定义了一些构造命令编号的宏:
- _IO(type,nr)/*没有参数的命令*/
- _IOR(type, nr, datatype)/*从驱动中读数据*/
- _IOW(type,nr,datatype)/*写数据*/
- _IOWR(type,nr,datatype)/*双向传送*/
这个头文件还定义了用来解开这个字段的宏:
- _IOC_DIR(nr)
- _IOC_TYPE(nr)
- _IOC_NR(nr)
- _IOC_SIZE(nr)
2、实现命令
实现命令从用户空间来看就是实现ioctl这个系统调用,实现ioctl系统调用可分为实现三个技术环节:返回值、参数使用、命令操作。
返回值
ioctl的实现通常就是一个机遇命令号的switch语句,就是前面命令定义的在这个设备驱动中几个命令。但是当命令号不能匹配合法操作时,比如你用户空间下传入的命令号在设备驱动中根本就没有定义。此时,有些内核会返回 -ENVAL(Invalid argument 非法参数),这是合理的。而POSIX 标准规定:如果使用了不合适的 ioctl 命令号,应当返回-ENOTTY 。这个错误码被 C 库解释为"不合适的设备 ioctl。然而,它返回-EINVAL仍是相当普遍的。
参数使用
前面讲用户空间ioctl系统调用中的“...”代表着可选的arg参数,不管用户程序使用的是指针还是整数值,驱动程序的ioctl方法都识别成unsigned long的形式。但是驱动在使用这个传过来的参数时,就会有区别了,如果传递的是一个整数,它可以直接使用,这是没有问题的。如果是一个指针,驱动程序也就要以指针来对待它,就要小心了。比如当一个指针指向用户空间时,就必须确保指向的用户空间时合法的,此时驱动程序就要负责对用到的用户空间地址做适当的检查了。
用户空间地址的检测有课分为两种,一种是调用的内核函数是带有检测的,这样就不需要检测了,如
- copy_from_user
- copy_to_user
- get_user
- put_user
另外一种是调用的内核函数是不带有检测的,如
- __get_user
- __put_user
我们在调用这两个函数传输数据前,首先要通过函数access_ok验证地址。
- int access_ok(int type, const void *addr, unsigned long size);
- //从addr指的地址下进行size个字节的type(read或者write)检测
关于access_ok,有两点有趣之处需要注意。第一,它并没有完成验证内存的全部工作,而只检查了所引用的内存是否位于进程有对应访问权限的区域内,特别是要确保访问地址没有指向内核空间的内存区。第二,大多数驱动常年供需代码中都不需要真正调用access_ok,因为会用到更好的内存管理程序。
命令操作
命令操作就是switch语句选中的命令下要完成的事,这是用户程序要求内核对设备做的真正的事,相当于这个函数被调用了。
二、ioctl在scull中的实现分析
1、定义命令
下面是scull中的一些ioctl命令定义(scull.h)。这些命令是用来设置和获取驱动程序的配置参数。
- /* 使用“k”作为幻数 */
- #define SCULL_IOC_MAGIC 'k'
-
/* 在你自己的代码中,请使用不同的8位数字*/
- #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)
-
/*
- * The other entities only have "Tell" and "Query", because they're
- * not printed in the book, and there's no need to have all six.
- * (The previous stuff was only there to show different ways to do it.
- */
- #define SCULL_P_IOCTSIZE _IO(SCULL_IOC_MAGIC, 13)
- #define SCULL_P_IOCQSIZE _IO(SCULL_IOC_MAGIC, 14)
- /* ... more to come */
程序先定义了一个幻数,用字符“k”表示,这个是随机的,只要所有的幻数一致就行。接着定义SCULL_IOC_MAX个不同的cmd。
2、实现中的ioctl参数使用
- int scull_ioctl(struct inode *inode, struct file *filp,
- unsigned int cmd, unsigned long arg)
-
{
- int err = 0, tmp;
- int retval = 0;
-
- /*
- * 抽取类型和编号位字段,并拒绝错误的命令号:
- * 在调用access_ok()之前返回ENOTTY(不恰当的ioctl)
- */
- if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
- if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;
- /*
- * 方向是一个位掩码,而VERIFY_WRITE用于R/W传输
- */
- if (_IOC_DIR(cmd) & _IOC_READ)
- err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
- else if (_IOC_DIR(cmd) & _IOC_WRITE)
- err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
- if (err) return -EFAULT;
- switch(cmd) {
- case ...
- ...
- }
- ...
首先用解开为字段的宏来判断类型和编号是不是这个驱动程序的,也就是前面说的一个驱动中只有一个固定的幻数,这是区别去其他驱动的关键。
特别要注意的是,定义命令的时候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
实验板测试输出:
- # ./ioctl_test
- write code=20
- ever out
- read code=20
- quantum has been changed
-
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
-
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
-
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
-
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
-
Now the quantum is changed
- write code=6
- write code=6
- write code=6
- writever out
- ever out
- ever out
- ever out
- e code=2
- read code=6
- read code=6
- read code=6
- read code=2
-
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
-
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
-
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
- [15]=15 [16]=16 [17]=17 [18]=18 [19]=19