Chinaunix首页 | 论坛 | 博客
  • 博客访问: 872224
  • 博文数量: 366
  • 博客积分: 10267
  • 博客等级: 上将
  • 技术积分: 4290
  • 用 户 组: 普通用户
  • 注册时间: 2012-02-24 14:04
文章分类

全部博文(366)

文章存档

2012年(366)

分类: 系统运维

2012-04-21 22:43:10

一个实际可用的设备除了提供同步写入和读取之外,还会提供更多的功能,本章阐述了编写全功能字符设备驱动程序的几个概念。首先,在本节的学习,将会实现ioctlllseek系统调用,其中,ioctl是用于设备控制的公共接口。

内核版本:linux-2.6.32.2

 

ioctl

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

 

在用户空间,ioctl同名的系统调用原型如下:

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

返回值: 根据命令cmd不同而不同

原型中的点代表可选参数,习惯上用char *argp定义。第三个参数的具体形式依赖于要完成的控制命令,也就是第二个参数。某些控制命令不需要参数,有些需要一个整数参数,而某些则需要一个指针参数,使用指针可以向ioctl调用传递任意数据,这样设备可以与用户空间交换任意数量的数据。

从本质上讲,每个ioctl命令就是一个独立的系统调用,而且是非公开的,因此没有任何方法可以以一种容易理解的方式来审核这些调用。

  

驱动程序的ioctl方法原型:

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

inodefilp两个指针的值对应于应用程序传递的文件描述符fd

参数cmd由用户空间不经修改地传递给驱动程序。

可选的arg参数则无论用户程序使用的是指针还是整数值,它都以unsigned long的形式传递给驱动程序,如果调用程序没有传递第三个参数,那么驱动程序所接收的arg参数就处在未定义状态,所以在用户空间即使不使用arg,也需设置为NULL。由于对这个附加参数的类型检查被关闭了,所以如果为ioctl传递一个非法的参数,编译程序是无法报警的,这样,相关联的程序错误就会很难被发现。

 

大多数的ioctl实现中都包括一个switch语句来根据cmd参数选择对应的操作。不同的命令被赋予不同的数值,为了简化代码,通常会在代码中使用符号名代替数值,这些符号名由C语言的预定义语句定义。定制的设备驱动程序通常会在它们的头文件中声明这些符号,如scull.h中声明了scull所使用的符号。为了访问这些符号,用户程序自然也要包含这些头文件。

 

 

选择ioctl命令

为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一。

为方便程序员创建唯一的ioctl命令号,每一个命令号被分为多个位字段。要按Linux内核的约定方法为驱动程序选择ioctl编号,应该首先看看Documentation/ioctl/ ioctl-number.txtinclude/asm-generic/ioctl.h这两个头文件。

定义新号码的新方法使用了4个位字段,分析ioctl.h可得命令号码各位段意义如下(X86ARM平台):

 

 

 

 

 

type 

    幻数。其位宽为_IOC_TYPEBITS

number

    序数,用以区分不同的命令。其位宽为_IOC_NRBITS

direction

    定义数据传输的方向。可以使用的值包括:

    _IOC_NONE   没有数据传输

    _IOC_READ  

    _IOC_WRITE 

    _IOC_READ | _IOC_WRITE  可读、可写

    该字段是位掩码,可以用逻辑AND操作从中分离出_IOC_READ_IOC_WRITE

size

所涉及用户数据大小。这个字段的宽度与体系结构有关。ARMX8614位。系统并不强制使用这个字段,也就是说,内核不会检查这个位字段。对该位字段的正确使用可以帮助我们检测用户空间程序的错误。如果需要很大的数据传输则可以忽略这个位字段。

 

头文件定义了一些构造命令编号的宏: 

复制代码
1 /* used to create numbers */ 2 // 构造无参数的命令号 3 #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) 4 // 构造从驱动程序中读取数据的命令 5 #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) 6 // 用于写入数据的命令 7 #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) 8 // 用于双向传输 9 #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
复制代码

 其中,typenumber位字段通过参数传入,而size位字段则通过对datatype参数取sizeof获得。

 

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

1 /* used to decode ioctl numbers.. */ 2 #define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK) 3 #define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK) 4 #define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK) 5 #define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

相关的用法,在实验中说明。

 

 

返回值

当命令号不能匹配任何合法的操作时,默认的选择是什么呢?有些内核函数会返回-EINVAL(“Invalid argument,非法参数”),这是合理的。然而,POSIX标准规定,如果使用了不合适的ioctl命令参数,应该返回-ENOTTYC库将这个错误码解释为“Inappropriate ioctl for devices,不合适的设备ioctl”,这看起来更贴切些。尽管如此,对非法的ioctl命令返回-EINVAL仍然是很普遍的做法。

 

 

预定义命令

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

 

预定义命令分为三组: 

1、可用于任何文件的命令(普通、设备、FIFO和套接字)

2、只用于普通文件

3、特定于文件系统的命令

设备驱动程序开发人员只对第一组感兴趣,他们的幻数都是“T”。

 

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

1 FIOCLEX :设置 close-on-exec 标志(File IOctl Close on EXec)。 2 FIONCLEX :清除 close-no-exec 标志(File IOctl Not CLose on EXec)。 3 FIOQSIZE :这个命令返回一个文件或者目录的大小; 当用作一个设备文件, 但是, 它返回一个 ENOTTY 错误。 4 FIONBIO:"File IOctl Non-Blocking I/O"("阻塞和非阻塞操作"一节中描述)。

 

 

使用ioctl参数

在使用ioctl那个附加参数时,如果它是一个整数,那么很简单,直接使用即可。如果它是一个指针,就需要注意一些问题了。

当用一个指针指向用户空间时,必须确保指向的用户空间是合法的。对未验证用户空间指针的访问,可能导致oops、系统崩溃或者安全问题。

copy_from_usercopy_to_user函数可安全的与用户空间交换数据,这两个函数也可在ioctl中使用,但是因为ioctl调用通常涉及到小的数据项,因此可通过其他方法更有效的操作。为此,我们首先通过函数access_ok验证地址(而不传输数据),该函数在中声明:

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

type  应该是VERIFY_READVERIFY_WRITE,取决于要执行的动作是读取还是写入用户空间内存区。如果在指定的地址既要读取又要写入,则应该使用VERIFY_WRITE,因为它是VERIFY_READ的超集。

addr:   一个用户空间地址

size:   字节数,例如要从用户空间读取一个整数,size就是sizeof(int)

注意:access_ok是面向内核的。

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

 

关于access_ok,有两点需要注意:

第一、  它并没有完成验证内存的全部工作,而只检查了所引用的内存是否位于进程有对应访问权限的区域内,特别是要确保访问地址并没有指向内核空间的内存区。

第二、  大多数驱动程序代码中都不需要真正调用access_ok,因为后面要讲到的内存管理程序会处理它。

 

在调用access_ok之后,驱动程序就可以安全地进行实际的数据传送了。除了copy_from_usercopy_to_user函数外,程序员还可以使用已经为最常用的数据大小(1248字节)优化过的一组函数。这些函数定义在中:

1 /* 把datum写到用户空间地址ptr */ 2 put_user(datum, ptr) 3 __put_user(datum, ptr) 4 /* 从用户空间接收一个数据,接收的数值保存在局部变量local中 */ 5 get_user(local, ptr) 6 __get_user(local, ptr)

 它们相对较快,当要传递单个数据时,应该用这些宏而不是copy_(from/to)_user,由于这些宏在展开时不做类型检查,所以可以传递给其任意类型的指针,只要是个用户空间地址即可。传递的数据大小依赖于ptr参数的类型,在编译时由内建指令sizeoftypeof确定。

put_userget_user进行检查以确保进程可以写入/读取指定的内存地址,并在成功时返回0,出错返回-EFAULT__put_user__get_user应该在已经使用access_ok检验过内存区后再使用。

如果试图使用上面列出的函数传递大小不符合任意一个特定值的数值,结果通常是编译器会给出一条奇怪的消息,比如“conversion to non-scalar type requested(需要转换为非标量类型)”。在这种情况下,必须使用copy_(from/to)_user

 

 

权能与受限操作

对设备的访问由设备文件的权限控制,驱动程序通常不进行权限检查。内核专为许可管理使用权限并导出了两个系统调用capgetcapset,这样用户就可以从用户空间来管理权能。

全部权能操作都可以在< linux/capability.h>中找到。对驱动开发者来说有意义的权能如下:

1 CAP_DAC_OVERRIDE /*越过在文件和目录上的访问限制(数据访问控制或 DAC)的能力。*/ 2 CAP_NET_ADMIN /*进行网络管理任务的能力, 包括那些能够影响网络接口的任务*/ 3 CAP_SYS_MODULE /*加载或去除内核模块的能力*/ 4 CAP_SYS_RAWIO /*进行 "raw"(裸)I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯*/ 5 CAP_SYS_ADMIN /*截获的能力, 提供对许多系统管理操作的途径*/ 6 CAP_SYS_TTY_CONFIG /*执行 tty 配置任务的能力*/

 

在执行一项特权操作之前,设备驱动程序应该检查调用进程是否有合适的权能。权能检查通过capable函数实现(定义在中):

范例如下:

1 if (! capable (CAP_SYS_ADMIN)) 2 return -EPERM;

如果有对应权限,则返回真。

 

 

 

定位设备(llseek

llseek是修改文件中的当前读写位置的系统调用。内核中的缺省的实现进行移位通过修改 filp->f_pos, 这是文件中的当前读写位置。对于 lseek 系统调用要正确工作,读和写方法必须通过更新它们收到的偏移量来配合。

如果设备是不允许移位的,你不能只制止声明 llseek 操作,因为缺省的方法允许移位。应当在你的 open 方法中,通过调用 nonseekable_open 通知内核你的设备不支持 llseek :

1 int nonseekable_open(struct inode *inode; struct file *filp);

上述调用会把你的filp标记为不可定位,这样,内核就不会让这种文件上的lseek调用成功。通过这种方式标记文件,我们还可以确保通过preadpwrite系统调用也不能定位文件。

为了完整起见,我们还应该将file_operations结构中的llseek方法设置为特殊的辅助函数no_llseek,该函数定义在中。

 

 

 

开发板实验:

开发板:mini2440(256M)

模块程序链接:http://files.cnblogs.com/ycz9999/ioctl_llseek.zip

模块测试程序链接:http://files.cnblogs.com/ycz9999/ioctl_llseek_test.zip

实验现象:

复制代码
1 open scull ! 2 SCULL_IOCSQUANTUM-SCULL_IOCQQUANTUM : scull_quantum=10 3 SCULL_IOCTQUANTUM-SCULL_IOCGQUANTUM : scull_quantum=6 4 SCULL_IOCXQUANTUM : scull_quantum=6 --> 10 5 SCULL_IOCHQUANTUM : scull_quantum=10 --> 6 6 7 SCULL_IOCSQSET-SCULL_IOCQQSET : scull_qset=2 8 SCULL_IOCTQSET-SCULL_IOCGQSET : scull_qset=4 9 SCULL_IOCXQSET : scull_qset=4 --> 2 10 SCULL_IOCHQSET : scull_qset=2 --> 4 11 12 before reset : scull_quantum=6 scull_qset=4 13 close scull ! 14 reopen scull ! 15 reopen : scull_quantum=6 scull_qset=4 16 write error ! code=6 i=20 17 write error ! code=6 i=14 18 write error ! code=6 i=8 19 write ok ! code=2 20 lseek scull SEEK_SET-->0 ! 21 read error ! code=6 i=20 22 read error ! code=6 i=14 23 read error ! code=6 i=8 24 read ok ! code=2 25 [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 26 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 27 [10]=10 [11]=11 [12]=12 [13]=13 [14]=14 28 [15]=15 [16]=16 [17]=17 [18]=18 [19]=19 29 30 SCULL_IOCRESET 31 after reset : scull_quantum=4000 scull_qset=1000 32 close scull ! 33 reopen scull ! 34 write ok ! code=20 35 lseek scull SEEK_CUR-10-->10 ! 36 read ok ! code=10 37 [0]=10 [1]=11 [2]=12 [3]=13 [4]=14 38 [5]=15 [6]=16 [7]=17 [8]=18 [9]=19 39 lseek scull SEEK_END-20-->0 ! 40 read ok ! code=20
复制代码

 

 

 

 

 

 

 

参考:

Linux设备驱动程序(第三版)

Tekkaman Ninja:http://blog.chinaunix.net/uid/20543672.html

阅读(1207) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~