高级字符驱动程序操作——ioctl

来源:互联网 发布:手机淘宝营销中心 编辑:程序博客网 时间:2024/05/21 17:49

ioctl

除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。如:用户空间经常会请求设备锁门,弹出介质,改变波特率或者执行自破坏等。这些操作通常可通过ioctl方法支持,该方法实现了同名的系统调用。

在用户空间,ioctl系统调用具有如下原型:

int ioctl(int fd, unsigned long cmd, ...);

原型中的这些点并不是数目不定的一串参数,而只是一个可选参数,习惯用char *argp定义。这里使用点的原因是为了防止在编译时进行类型检查。

第三个参数的具体形式依赖于要完成的控制命令(第二个参数),某些命令不需要参数,某些需要一个整数参数,而某些需要一个指针参数。使用指针可以向ioctl调用传递任意数据,这样设备可以与用户空间交换任意数量的数据。

由于ioctl调用的非结构化本质导致众多内核开发者倾向于放弃它。结果有许多需求要求我们通过其他途径来实现繁杂的控制操作,但现实情况中,对真正的设备操作来说,ioctl仍然是最简单最直接的选择。

驱动程序的ioctl方法原型和用户空间版本存在一些不同:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

前两个参数对应于应用程序传递的文件描述符fd。参数cmd由用户空间不经修改地传递给驱动程序,可选参数arg则无论用户程序使用的是指针还是整数值,都以unsigned long的形式传递给驱动程序。如果调用程序没有传递弟三个参数,那么驱动程序所接收的arg参数就处于未定义状态。由于对这个附加参数的类型检查被关闭了,所以如果为ioctl传递一个非法参数,编译器是无法报警的,这样相关联的程序错误就很难发现。

大多数ioctl的实现中都包括一个switch语句来根据cmd参数选择对应的操作。代码中通常用符号名代替数值,这些符号通常在它们的头文件中声明。为了访问这些符号,用户程序自然也要包含这些头文件。

选择ioctl命令

在编写ioctl代码前,需要选择对应不同的命令编号。多数程序员的第一反应是从0或者1开始选择一 组小的编号。然而有很多理由要求我们不能这样选择编号。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一

为方便程序员创建唯一的ioctl命令号,每一个命令号被分为多个位字段。

linux的第一个版本使用了一个16位整数:高8位是与设备相关的“幻数”,低8位是一个序列号。

要按Linux内核约定方法为驱动程序选择ioctl编号,应该首先看看include/asm/ioctl.h和Documentation/ioctl-number.txt这两个文件。头文件中定义了要使用的位字段:类型(幻数),序数,传送方向和参数大小等等。ioctl-number.txt罗列了内核所使用的幻数,在选择自己的幻数时避免和内核冲突。

定义编号的新方法使用了4个位字段,下面所介绍的定义在<linux/ioctl.h>

#define _IOC_NRBITS 8        //序数,8位宽
#define _IOC_TYPEBITS 8    //幻数,8位宽
# define _IOC_SIZEBITS 14  //所涉及的用户数据大小,这个字段与体系结构有关,通常13或14
# define _IOC_DIRBITS 2      //如果涉及数据传输,该位字段定义数据传输方向

#define _IOC_NRSHIFT 0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)

#define _IOC(dir,type,nr,size) \
 (((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),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

下面是scull中的一些ioctl命令定义。

#define SCULL_IOC_MAGIC 'k'     //使用"k"作为幻数

#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)

根据约定,ioctl应该使用指针完成数据交换,但我们选择使用两种方法实现整数参数传递——通过指针和显式的数值。

返回值

ioctl的实现通常就是一个基于命令号的switch语句。但是当命令号不能匹配任何合法的操作时,默认的选择是什么?对于这个问题颇有争议。有些内核函数返回-EINVAL("Invalid argument, 非法参数"),这是合理的,然而POSIX标准规定,应该返回-ENOTTY。尽管如此返回-EINVAL仍然是很普遍的做法。

预定义命令

尽管ioctl系统调用绝大部分用于操作设备,但还是有一些命令是可以由内核识别的。要注意,当这些命令用于我们的设备时,它们会在我们自己的文件操作被调用之前被解码。所以如果你为自己的ioctl命令选用了与这些预定义命令相同的编号,你的设备就永远不会收到该命令的请求,而且由于ioctl编号冲突,应用程序的行为将无法预测。

预定义命令分为三组:

  • 可用于任何文件(普通、设备、FIFO和套接字)的命令
  • 只用于普通文件的命令
  • 特定于文件系统类型的命令 //只能在宿主文件系统上执行(见chattr命令)

设备驱动程序开发人员只对第一组感兴趣,它们的幻数是"T”。其它组的读者自己分析。ext2_ioctl是其中最有意思的函数,它实现了只追加和不可变的标志。

下列ioctl命令对任何文件(包括设备特定文件)都是预定义的:

  1. FIOCLEX, #define FIOCLEX  _IO('f', 1),设置执行时关闭标志(File IOctl Close on Exed),设置了这个标志之后,当调用进程执行一个新程序时,文件描述符将被关闭。
  2. FIONCLEX,清除执行时关闭标志,该命令将恢复通常的文件行为,并撤销上述FIOCLEX命令所做的工作。
  3. FIOASYNC,设置或恢复文件异步通知。这两个动作都可以通过fcnt完成,所以实际上没人会使用FIOASYNC命令,列在这里是为了保持完整。
  4. FIOQSIZE,该命令返回文件或目录的大小。不过当用于设备文件时会导致ENOTTY错误的返回。
  5. FIONBIO,意指"File ioctl Non-Blocking I/O“,即"文件ioctl非阻塞型I/O"。该调用修改filp->f_flags中的O_NONBLOCK标志。传递给系统调用的第三个参数指明了是设置还是清除该标志。注意,修改这个标志的常用方法是由fcntl系统调用使用F_SETFL命令来完成的。

最后一项中我们引入了一个新的系统调用,即fcntl,该调用也要传递一个命令参数和一个附加的可选参数,这点类似于ioctl。它和ioctl的不同是历史原因造成的:当unix的开发人员面对控制I/O操作的问题时,他们认为文件和设备是不一样的。那时,与ioctl实现相关的唯一设备就是终端,这也是为什么非法的ioctl命令的标准返回值是-ENOTTY。现在情况不同了,但是fcntl还是为了向后兼容而留了下来。

使用ioctl参数

 在分析scull驱动程序的ioctl代码之前,还有一点要解释,就是怎样使用那个附加参数。如果它是个整数,那很很简单直接用就行了。如果它是个指针,就需要注意一些问题了。

当用一个指针指向用户空间时,必须确保指向的用户空间是合法的。否则可以导致系统OOPS、系统崩溃或者安全问题。驱动程序应该负责对每个用户空间地址做适当的检查,如果是非法地址则应该返回一个错误。

之前看到的copy_from_user和copy_to_user函数可安全地与用户空间交换数据。这两个函数也可以在ioctl方法中使用,但是ioctl调用通常涉及到的数据都比较小,因此可通过其它方法更有效地操作。为此,我们首先要通过函数access_ok验证地址(不是传输数据),该函数在<asm/uaccess.h>中声明:

int access_ok(int type, const void *addr, unsigned long size);

第一个参数是VERIFY_READ或是VERIFY_WRITE,取决于要执行的动作是读取还是写入用户空间内存区。

addr是一个用户空间地址,size是字节数。如要从用户空间读取一个整数,size就是sizeof(int)。

access_ok返回一个布尔值:1表示成功,0表示失败,如失败,驱动程序通常要返回-EFAULT给调用者。

access_ok并没有完成验证内存的全部工作,而只检查了所引用的内存是否位于进程有对应访问权限的区域内。大多数驱动程序代码中都不需要真正调用access_ok,因为后要讲到内存管理程序会处理它,尽管如此,我们还是示范一下它的使用。

int err = 0, tmp;int retval = 0;/*  抽取类型和编号位字段,并拒绝错误的命令号:  在调用access_ok()之前返回ENOTTY*/if(_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;if(_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;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;

在调用access_ok后,驱动程序就可以安全地进行实际数据传送了。

除了copy_from_user和copy_to_user函数外,程序员还可以使用已经为最常用的数据大小(1、2、4、8字节)优化过的一组函数。这些函数定义在<asm/uaccess.h>中,列下:

put_user(datum, ptr)

__put_user(datum, ptr) //做的检查少些(不调用access_ok),因而它应该在已经使用access_ok检验过的内存区使用

在成功时返回0,出错时返回-EFAULT。

这些宏把datum写到用户空间。它们相对比较快,当要传递单个数据时应该使用这些宏。传递的数据大小依赖于ptr参数的类型,总之,如果ptr是一个字符指针,则传递一个字节,2,4,8字节的情况类似。
 get_user(local, ptr)

__get_user(local, ptr)  //同样在操作地址已被access_ok检验后使用

以上两宏用于从用户空间接收数据。接收的值被保存在局部变量local中,返回值指明操作是否成功。

权能与受限操作

对设备的访问由设备文件的权限控制,驱动程序通常不进行权限检查。不过也有这种情况,允许用户对设备读/写,而其他的操作被禁止。如不是所有的磁盘驱动器使用者都可以设备它的默认块大小。在类似情况下,驱动程序必须进行附加的检查以确认用户是否有权进行请求的操作。

linux提供了一种更为灵活的系统,称为权能(capability)。基于权能的系统抛弃了那种要么全有要么全无的特权分配方式,而是把特权操作划分为独立的组。这样某个特定的用户或程序就可以被授权执行某一指定的特权操作,同时又没有执行其他不相关操作的能力。内核专为许可管理使用权能并导出两个系统调用capget和capset,这样就可以从用户空间来管理权能。

全部的权能操作都可以在<linux/capability>中找到,其中包含系统能够理解的所有权能,不修改内核源代码,驱动程序作者或管理员就无法定义新的权能。对驱动开发者来讲,有意义的权能如下所示:

CAP_DAC_OVERRIDE 越过文件或目录的访问限制(数据访问控制或DAC)的能力。

CAP_NET_ADMIN 执行网络管理任务的能力,包括那些影响网络接口的任务

CAP_SYS_MODULE 载入或卸除内核模块的能力

CAP_SYS_RAWIO 执行“裸”I/O操作的能力。如,访问设备端口或直接与USB设备通信

CAP_SYS_ADMIN 截获的能力,它提供了访问许多系统管理操作的途径。

CAP_SYS_TTY_CONFIG 执行tty配置任务的能力。

在执行一项特权操作之前,设备驱动程序应该检查调用进程是否有合适的权能;如果不执行这类检查,将导致用户进程执行非授权操作,从而影响系统稳定性和安全性。

权能的检查通过capable函数实现(定义在<sys/sched.h>中):

int capable(int capability);

在scull示例程序中,任何用户者被允许查询quantum和qset的大小。但是只有授权用户才可以更改这些值,因为不恰当的值会降低系统性能。scull的ioctl实现了在必要时检查用户的特权级别:

if(! capable (CAP_SYS_ADMIN))

    return -EPERM;

非ioctl的设备控制

 有时通过向设备写入控制序列可以更好地控制设备,用这种方法实现设备控制的好处是,用户仅通过写数据就可以控制设备,无需使用(有时还得编写)配置设备的程序。如果我们有这种方式控制设备,发出命令的程序甚至无需运行在设备所在的同一系统上。

当编写这种“面向命令的”驱动程序时,没什么必要实现ioctl方法。在解释器中新增一条指令,其实现和使用都更简单。

尽管如此,有可能需要做相反的事情:不是使用write解释器来避免使用ioctl,而是只使用ioctl,完全不使用write。同时驱动程序还附带了一个特定的命令行工具,专门负责把命令送给驱动程序。这种方法把内核空间的复杂转移到了用户空间,这样处理起来可能会容易些,并且有助于缩小驱动程序的规模,然而用户却无法再使用简单的命令(如cat或echo)来操作驱动程序了。

原创粉丝点击