ioctl
ioctl用来干什么的?
除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。ioctl就是用来进行硬件控制的。虽然这个接口存在争议,但是它仍然是操作设备最简单、最直接的方法。
用户空间原型:
int ioctl(int fd, unsigned long request, ...);
函数最后的三个点本意是表示可变数目的参数表,但是在实际系统中,系统调用不会真正使用可变数目的参数,而是使用精确定义的函数类型。因此,原型中的三个点并不是表示数目不定的一串参数,而只是一个可选参数,习惯上用char *argp定义。这里用点只是为了在编译时防止编译器进行类型检查。而第三个参数的类型以及意义是取决于第二个参数的。例如,有些命令不需要参数,那么第三个参数就不存在,有些命令需要一个整型数,那么第三个数就是一个整型,有些命令需要一个指针类型,那么第三个参数就是一个指针类型。这也就意味着可以通过ioctl在用户空间和内核空间交换任意数量的数据。
内核空间原型:
long (*ioctl) (struct file *, unsigned int, unsigned long);
file:对应于应用程序传递的文件描述符fd。
cmd:由用户空间不经修改地传递给驱动程序
可选的arg:无论用户程序使用的是指针还是整数值,它都以unsigned long的形式传递给驱动程序。如果调用程序没有传递第三个参数,那么驱动程序所接收的arg参数就处在未定义状态。由于对这个附加参数的类型检查被关闭了,所以如果给ioctl传递一个非法参数,编译器是无法报警的,这样产生的错误就很难被发现。
ioctl的命令
为什么要规定这些命令?
为了防止我们使用的命令和系统已经定义好的命令冲突,以及对于错误的设备使用正确的命令,我们应当保证我们使用的命令参数在全局内是唯一的,同时为了不让正确的命令作用于不匹配的设备进而产生意想不到的结果,我们需要在对应的驱动程序中加以验证,滤除掉不属于当前设备的命令。
怎么选择命令?
参考include/asm/ioctl.h和Documentation/ioctl-number.txt两个文件,头文件中定义了要使用的位字段:类型、序号、传输方向以及参数的大小,还有需要宏定义来帮助我们构建命令以及解析命令。ioctl-number.txt文件中罗列了内核所使用的类型,这样我们就知道怎么选择不会和内核冲突了。
命令的组成:
一个cmd分为4个位段部分。
// 数据流方向(在从应用程序的角度看的)
#define _IOC_NONE 1U(无数据传输请求)
#define _IOC_READ 2U(用户程序读数据请求,驱动程序验证对应的buf是否可写)
#define _IOC_WRITE 3U(用户程序写数据请求,驱动程序验证对应的buf是否可读)
//所占位宽
#define _IOC_NRBITS 8
#define _IOC_TYPEBITS 8
#define _IOC_SIZEBITS 13
#define _IOC_DIRBITS 3
// 操作掩码
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)//0xff
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1) //0xff
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)//0x1fff
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)//0x7
// 构建命令时需要移动的bit位。
#define _IOC_NRSHIFT 0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS) //8
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS) //16
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS) //29
// 用于构建基础命令的宏
#define _IOC(dir,type,nr,size) \
((unsigned int) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT)))
// 用于构建命令的宏
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(siz e))
// 用于解析命令的宏
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) //将方向数据从高3位
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
尽管根据已有的约定,Ioctl应该使用指针完成数据交换,但我们仍然可以选择用两种方法实现整数参数传递—通过指针或者是显式的数值。同样,这两种方法还可以用于返回整数。从任何一个系统调用返回时,正的返回值是受保护的,而负值则被认为是一个错误,并被用来设置用户空间中的errno变量。
ioctl的返回值
ioctl的实现通常就是一个基于命令号的switch语句,但是当命令号不能匹配合法的操作时,默认的选择是什么?对于这个问题颇有争议。有些内核函数会返回-ENVAL(invalid argument,非法参数),这是合理的,因为命令参数的确不是合法的参数。然而,POSIX标准规定,如果使用了不合适的ioctl命令参数,应该返回-ENOTTY,C库将这个错误码解释为“Inappropriate ioctl for device,不合适的设备ioctl”,这看起来更加贴切。尽管如此,对非法的ioctl命令返回-EINVAL仍然是很普遍的做法。
系统预定义的ioctl命令
尽管ioctl系统调用绝大部分用于操作设备,但还有一些命令时可以由内核识别的。当这些命令作用域我们的设备时,他们会在我们自己的文件操作被调用之前被解码。所以,如果你为自己的ioctl命令选用了与这些预定义命令相同的编号,就永远不会收到该命令的请求,而且由于ioctl编号冲突,应用程序的行为将无法预测。
预定义命令分为三组:
- 可用于任何文件的命令(幻数为“T”)
- 只用于普通文件的命令
- 用于特定文件系统类型的命令
fcntl
函数fcntl看起来很像ioctl,实际上,fcntl调用也要传递一个命令参数和一个附加的可选参数,在这点上类似于ioctl,他和ioctl的不同是由于历史原因造成的:当UNIX的开发人员面对控制IO操作的问题时,他们认为文件和设备是不同的,那时,与IOCTL实现相关联的唯一设备就是终端,这也解释了为什么非法的IOCTL命令的标准返回值是-ENOTTY。虽然现在情况不同了,但是fcntl还是为了向后兼容保留了下来。
使用ioctl参数
如果该参数是一个整数,那么我们直接使用即可,但是如果是一个指针的话,那么我们就要去验证将要写入或者读出的内存地址是否是合法的,对一块非法的内存进行读写可能会导致系统崩溃或者是安全问题。驱动程序应该对使用到的用户空间 进行检查,如果是非法地址应该返回一个错误。
在这里我们不打算使用copy_from_user和copy_to_user函数,虽然这两个函数可以在ioctl中用于内核空间和用户空间之间的数据交换,但是一般而言在ioctl中传输的数据量都比较小,因此存在其他更加有效的操作。
首先我们需要通过<asm/uaccess.h>中声明的access_ok函数来验证地址的合法性。原型如下:
int access_ok(int type, const void *addr, unsigned long size);
参数解释
type:应该是VERIFY_READ或者是VERIFY_WRITE,取决于要执行的动作是读取还是写入用户空间内存区。如果在指定地址既要读取又要写入应该使用VERIFY_WRITE,它是VERIFY_READ的超集。
addr:用户空间地址
size:操作字节数
返回值:一个bool类型,1表示成功,0表示失败。如果失败通常返回-EFAULT给调用者。
ioctl中常用的数据传输函数
#include <asm/uaccess.h>
put_user(datum, ptr);
__put_user(datum, ptr);
这些宏定义把datum写到用户空间,他们相对比较快,当要传输单个数据时,应该用这些宏而不是用copy_to_user。由于这些宏在展开时不进行类型检查,所以可以传递给put_user任意类型的指针,只要是个用户空间地址就行。传递的数据的大小依赖于ptr参数的类型,在编译时由编译器内建的sizeof和typeof确定。
put_user进行检查以确保进程可以写入指定的内存地址,并在成功时返回0,出错时返回-EFAULT。__put_user做的检查少一些(它不调用access_ok,节省了几个时钟周期),但是如果地址指向用户不能写入的内存,也会出现操作失败,因此,__put_user应该使用在已经使用access_ok验证过的内存区后再使用。
get_user(local, ptr);
__get_user(local, ptr);
这些宏用于从用户空间接收一个数据。出了传输方向相反之外,和以上两个函数没有什么差别。将接受的到数值存放在局不能变量local中,返回值说明了操作是否成功。同样__get_user应该在使用access_ok检查之后使用。
注:以上宏函数只能用于传递1、2、4、8个字节的数据,这些大小的数据之外要使用copy_to_user或者copy_from_user。