收藏 分享(赏)

linux设备驱动程序示范.doc

上传人:yjrm16270 文档编号:6211543 上传时间:2019-04-02 格式:DOC 页数:36 大小:241KB
下载 相关 举报
linux设备驱动程序示范.doc_第1页
第1页 / 共36页
linux设备驱动程序示范.doc_第2页
第2页 / 共36页
linux设备驱动程序示范.doc_第3页
第3页 / 共36页
linux设备驱动程序示范.doc_第4页
第4页 / 共36页
linux设备驱动程序示范.doc_第5页
第5页 / 共36页
点击查看更多>>
资源描述

1、第十一章 设备驱动程序11.1 概述在 Linux 中输入/输出设备被分为三类:块设备,字符设备和网络设备。这种分类的使用方法,可以将控制不同输入/输出设备的驱动程序和其它操作系统软件成分分离开来。例如文件系统仅仅控制抽象的块设备,而将与设备有关的部分留给低层软件,即驱动程序。字符设备指那些无需缓冲区可以直接读写的设备,如系统的串口设备/dev/cua0 和/dev/cua1。块设备则仅能以块为单位进行读写的设备,如软盘,硬盘,光盘等,典型块的大小为 512 或 1024 字节。从名称使人想到,字符设备在单个字符的基础上接收和发送数据。为了改进传送数据的速度和效率,块设备在整个数据缓冲区填满时

2、才一起传送数据。网络设备可以通过 BSD 套接口访问数据,关于这方面的内容我们将在第十二章中进行讨论。在 Linux 中,对每一个设备的描述是通过主设备号和从设备号,其中主设备号描述控制这个设备的驱动程序,也就是说驱动程序和主设备号是一一对应的,从设备号是用来区分同一个驱动程序控制的不同设备。例如主 IDE 硬盘的每个分区的从设备号都不相同,/dev/hda2 表示主 IDE 硬盘的主设备号为 3 而从设备号为 2。Linux 通过使用主、从设备号将包含在系统调用中的设备特殊文件映射到设备的管理程序,以及大量系统表格中,如字符设备表chrdevs。 块(磁盘)设备和字符设备的设备特殊文件可以通

3、过 mknod 命令来创建,并使用主从设备号来描述此设备。网络设备也用设备相关文件来表示,但 Linux 寻找和初始化网络设备时才建立这种文件11.1.1 I/O 软件I/O 软件的总体目标就是将软件组织成一种层次结构,低层软件用来屏蔽具体设备细节,高层软件则为用户提供一个简洁规范的界面。这种层次结构很好的体现了 I/O 设计的一个关键的概念:设备无关性,其含义就是程序员写的软件无须修改就能读出软盘,硬盘以及 CD-ROM 等不同设备上的文件。输入/ 输出系统的层次结构及各层次的功能如图 11.1 所示:从图可以看出,用户进程的下层是设备无关的软件,在 Linux 中,设备无关软件的功能大部分

4、由文件系统去完成,其基本功能就是执行适用于所有设备的常用的输入/输出功能,向用户软件提供一个一致的接口。其结构如图 11.2 所示:图 11.1 输入/输出系统的层次结构及各层次的功能图 11.2 设备无关软件的功能设备无关的软件具有以下特点: 文件和设备采用统一命名。设备无关软件负责将设备名映射到相应的驱动程序,一个设备名唯一地确定一个索引节点,索引节点中包含了主设备号和次设备号,通过主设备号可以找到相应的设备驱动程序,通过次设备号确定具体的物理设备。对设备提供的保护机制同文件系统一样都采用 rwx 权限。数据块的大小可能对于不同的设备其大小不一样,但操作系统屏蔽这一事实,向高层软件提供了统

5、一的逻辑块的大小。为了解决数据交换速度的匹配问题,采用了缓冲技术,对于缓冲区的管理由文件系统去完成。块设备的存储分配也是由文件系统去处理。对于独占设备的分配和释放属于对临界资源的管理。11.1.2 设备驱动程序CPU 并不是系统中唯一的智能设备,每个物理设备都拥有自己的控制器。键盘、鼠标和串行口由一个高级 I/O 芯片统一管理,IDE 控制器控制 IDE 硬盘而 SCSI 控制器控制 SCSI硬盘等等。每个硬件控制器都有各自的控制状态寄存器(CSR)并且各不相同。例如Adaptec 2940 SCSI 控制器的 CSR 与 NCR 810 SCSI 控制器完全不一样。这些寄存器用来启动、停止、

6、初始化设备以及对设备进行诊断。在 Linux 中管理硬件设备控制器的代码并没有放置在每个应用程序中而是由内核统一管理,这些处理和管理硬件控制器的软件就是设备驱动程序。Linux 内核的设备管理是由一组运行在特权级上,驻留在内存以及对底层硬件进行处理的共享库的驱动程序来完成的。设备管理的一个基本特征是设备处理的抽象性,即所有硬件设备都被看成普通文件,可以通过用操纵普通文件相同的系统调用来打开、关闭、读取和写入设备。系统中每个设备都用一种设备特殊文件来表示,例如系统中第一个 IDE 硬盘被表示成/dev/hda。那么,系统是如何将设备在用户视野中屏蔽起来的呢?图 11.3 说明了用户进程请求设备进

7、行输入输出的简单流程。图 11.3 用户进程请求设备服务的流程首先当用户进程发出输入输出时,系统把请求处理的权限放在文件系统,文件系统通过驱动程序提供的接口将任务下放到驱动程序,驱动程序根据需要对设备控制器进行操作,设备控制器再去控制设备本身。这样通过层层隔离,对用户进程基本上屏蔽了设备的各种特性,使用户的操作简便易行,不必去考虑具体设备的运作,就象对待文件操作一样去操作设备,因为实际上在驱动程序向文件系统提供的接口已经屏蔽掉了设备的电器特性。设备控制器对设备本身的控制是电器工程师所关心的事情,操作系统对输入/输出设备的管理只是通过文件系统和驱动程序来完成。也就是说在操作系统中,输入/输出系统

8、所关心的只是驱动程序。 Linux 设备驱动程序的主要功能有: 对设备进行初始化;使设备投入运行和退出服务;从设备接收数据并将它们送回内核; 将数据从内核送到设备;检测和处理设备出现的错误; 在 Linux 中,设备驱动程序是一组相关函数的集合。它包含设备服务子程序和中断处理程序。设备服务子程序包含了所有与设备相关的代码,每个设备服务子程序只处理一种设备或者紧密相关的设备。其功能就是从与设备无关的软件中接受抽象的命令并执行之。当执行一条请求时,具体操作是根据控制器对驱动程序提供的接口(指的是控制器中的各种寄存器) ,并利用中断机制去调用中断服务子程序配合设备来完成这个请求。设备驱动程序利用结构

9、 file_operations 与文件系统联系起来,即设备的各种操作的入口函数存在file_operation 中。对于特定的设备来说有一些操作是不必要的,其入口置为 NULL。Linux 内核中虽存在许多不同的设备驱动程序但它们具有一些共同的特性: 1. 驱动程序属于内核代码 设备驱动程序是内核的一部分,它象内核中其它代码一样运行在内核模式,驱动程序如果出错将会使操作系统受到严重破坏,甚至能使系统崩溃并导致文件系统的破坏和数据丢失。 2. 为内核提供统一的接口设备驱动程序必须为 Linux 内核或其它子系统提供一个标准的接口。例如终端驱动程序为 Linux 内核提供了一个文件 I/O 接口

10、。3. 驱动程序的执行是属于内核机制并且使用内核服务 。设备驱动可以使用标准的内核服务如内存分配、中断发送和等待队列等等。 4. 动态可加载 多数 Linux 设备驱动程序可以在内核模块发出加载请求时加载,而不再使用时将其卸载。这样内核能有效地利用系统资源。 5. 可配置 Linux 设备驱动程序可以连接到内核中。当内核被编译时,被连入内核的设备驱动程序是可配置的。 11.2 设备驱动基础11.2.1 I/O 端口每个连接到 I/O 总线上的设备都有自己的 I/O 地址集,即所谓的 I/O 端口(I/O port) 。在 IBM PC 体系结构中,I/O 地址空间一共提供了 65,536 个

11、8 位的 I/O 端口。可以把两个连续的 8 位端口看成一个 16 位端口,但是这必须是从偶数地址开始。同理,也可以把两个连续的 16 位端口看成一个 32 位端口,但是这必须是从 4 的整数倍地址开始。有四条专用的汇编语言指令可以允许 CPU 对 I/O 端口进行读写:它们分别是 in、ins、out 和 outs。在执行其中的一条指令时,CPU 使用地址总线选择所请求的 I/O 端口,使用数据总线在 CPU寄存器和端口之间传送数据。I/O 端口还可以被映射到物理地址空间:因此,处理器和 I/O 设备之间的通信就可以直接使用对内存进行操作的汇编语言指令(例如,mov、and、or 等等) 。

12、现代的硬件设备更倾向于映射 I/O,因为这样处理的速度较快,并可以和 DMA 结合起来使用。系统设计者的主要目的是提供对 I/O 编程的统一方法,但又不牺牲性能。为了达到这个目的,每个设备的 I/O 端口都被组织成如图 11.4 所示的一组专用寄存器。CPU 把要发给设备的命令写入控制寄存器(control register) ,并从状态寄存器(status register)中读出表示设备内部状态的值。CPU 还可以通过读取输入寄存器(input register)的内容从设备取得数据,也可以通过向输出寄存器(output register)中写入字节而把数据输出到设备。图 11.4 专用

13、I/O 端口为了降低成本,通常把同一 I/O 端口用于不同目的。例如,某些位描述设备的状态,而其他位指定发布给设备的命令。同理,也可以把同一 I/O 端口用作输入寄存器或输出寄存器。那么如何访问 I/O 端口? in、out、ins 和 outs 汇编语言指令都可以访问 I/O 端口。Linux 内核中定义了以下辅助函数来简化这种访问: inb( )、inw( )、inl( )函数分别从 I/O 端口读取 1、2 或 4 个连续字节。后缀“b” 、 “w”、 “l”分别代表一个字 节(8 位) 、一个字(16 位)以及一个长整型(32 位) 。 inb_p( )、inw_p( )、inl_p(

14、 )分别从 I/O 端口读取 1、2 或 4 个连续字节,然后执行一条“哑元(dummy,即空指令) ”指令使 CPU 暂停。 outb( )、outw( )、outl( )分别向一个 I/O 端口写入 1、2 或 4 个连续字节。 outb_p( )、outw_p( )、outl_p( )控制寄存器控制寄存器控制寄存器控制寄存器CPU设备I/O接口分别向一个 I/O 端口写入 1、2 或 4 个连续字节,然后执行一条“哑元”指令使CPU 暂停。 insb( )、insw( )、insl( )分别从 I/O 端口读入以 1、2 或 4 个字节为一组的连续字节序列。字节序列的长度由该函数的参数给

15、出。 outsb( )、outsw( )、outsl( )分别向 I/O 端口写入以 1、2 或 4 个字节为一组的连续字节序列。虽然访问 I/O 端口非常简单,但是检测哪些 I/O 端口已经分配给 I/O 设备可能就不这么简单,特别是对基于 ISA 总线的系统来说更是如此。通常,I/O 设备驱动程序为了侦探硬件设备,需要盲目地向某一 I/O 端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用 iotable 表来记录分配给每个硬件设备的 I/O 端口。任何设备驱动程序都可以使用下面三个函数:request_region( ) 把一个给

16、定区间的 I/O 端口分配给一个 I/O 设备。check_region( ) 检查一个给定区间的 I/O 端口是否空闲,或者其中一些是否已经分配给某个 I/O设备。release_region( ) 释放以前分配给一个 I/O 设备的给定区间的 I/O 端口。当前分配给 I/O 设备的 I/O 地址可以从/proc/ioports 文件中获得。11.2.2 I/O 接口及设备控制器I/O 接口是处于一组 I/O 端口和对应的设备控制器之间的一种硬件电路。它起翻译器的作用,即把 I/O 端口中的值转换成设备所需要的命令和数据。从另一个角度来看,它检测设备状态的变化,并对起状态寄存器作用的 I/

17、O 端口进行相应地更新。还可以通过一条IRQ 线把这种电路连接到可编程中断控制器上,以使它代表相应的设备发出中断请求。有两类类型的接口:专用 I/O 接口专门用于一个特定的硬件设备。在一些情况下,设备控制器与这种 I/O 接口处于同一块卡中,连接到专用 I/O 接口上的设备可以是内部设备(位于 PC 机箱内部的设备) ,也可以是外部设备(位于 PC 机箱外部的设备) 。例如键盘接口、图形接口、磁盘接口、总线鼠标接口及网络接口都属于专用 I/O 接口。通用 I/O 接口用来连接多个不同的硬件设备。连接到通用 I/O 接口上的设备通常都是外部设备。例如并口、串口、通用串行总线(USB) 、PCMC

18、IA 接口及 SCSI 接口都属于通用 I/O接口。复杂的设备可能需要一个设备控制器来驱动。控制器具有两方面的作用,一是对从 I/O接口接收到的高级命令进行解释,并通过向设备发送适当的电信号序列强制设备执行特定的操作;二是对从设备接收到的电信号进行转换和解释,并通过 I/O 接口修改状态寄存器的值。磁盘控制器是一种比较典型的设备控制器,它通过 I/O 接口从微处理器接收诸如“写这个数据块”之类的高级命令,并将其转换成诸如“把磁头定位在正确位置的磁道”上和“把数据写入这个磁道”之类的低级磁盘操作。现在的磁盘控制器相当复杂,因为它们可以把磁盘数据快速保存到内存的缓存区中,还可以根据实际磁盘的几何结

19、构重新安排 CPU的高级请求,使其优化。11.2.3 设备文件设备文件是用来表示 Linux 所支持的大多数设备的,每个设备文件除了设备名,还有三个属性:即类型、主设备号、次设备号。设备文件是通过 mknod 系统调用创建的。其原型为:mknod(const char * filename, int mode, dev_t dev)其参数有设备文件名、操作模式、主设备号及次设备号。最后两个参数合并成一个 16位的 dev_t 无符号短整数,高 8 位用于主设备号,低 8 位用于次设备号。内核中定义了三个宏来处理主、次设备号:MAJOR 和 MINOR 宏可以从 16 位数中提取出主、次设备号,

20、而MKDEV 宏可以把主、此号合并为一个 16 位数。实际上,dev_t 是专用于应用程序的一个数据类型;在内核中使用 kdev_t 数据类型。在 Linux 2.4 及以前的版本中,这两个类型都会是一个无符号短整型,但是在以后的 Linux 版本中,kdev_t 会成为一个完整的设备文件描述符,也就说,也许会扩成 32 位的长整数。分配给设备号的正式注册信息及/dev 目录索引节点存放在Documentation/devices.txt 文件中。也可以在 include/linux/major.h 文件中找到所支持的主设备号。设备文件通常位于/dev 目录下。表 11.1 显示了一些设备文件

21、的属性。注意同一主设备号既可以标识字符设备,也可以标识块设备。表 11.1 设备文件的例子设备名 类型 主设备号次号 说明/dev/fd0 块设备 2 0 软盘/dev/hda 块设备 3 0 第一个 IDE 磁盘/dev/hda2 块设备 3 2 第一个 IDE 磁盘上的第二个主分区/dev/hdb 块设备 3 64 第二个 IDE 磁盘/dev/hdb3 块设备 3 67 第二个 IDE 磁盘上的第三个主分区/dev/ttyp0 字符设备 3 0 终端/dev/console 字符设备 5 1 控制台/dev/lp1 字符设备 6 1 并口打印机/dev/ttyS0 字符设备 4 64 第

22、一个串口/dev/rtc 字符设备 10 135 实时时钟/dev/null 字符设备 1 3 空设备(黑洞)一个设备文件通常与一个硬件设备(如硬盘,/dev/hda)相关连, 或硬件设备的某一物理或逻辑分区(如磁盘分区,/dev/hda2)相关联。但在某些情况下,设备文件不会和任何实际的硬件关联,而是表示一个虚拟的逻辑设备。例如,/dev/null 就是对应于一个“黑洞”的设备文件:所有写入这个文件的数据都被简单地丢弃,因此,该文件看起来总为空。就内核所关心的内容而言,设备文件名是无关紧要的。如果你建立了一个名为/tmp/disk 的设备文件,类型为“块” ,主设备号是 3,次设备号为 0,

23、那么这个设备文件就和表中的/dev/hda 等价。另一方面,对某些应用程序来说,设备文件名可能就很有意义。例如,通信程序可以假设第一个串口和/dev/ttyS0 设备文件关联。1块设备和字符设备的比较块设备具有以下特点: 可以在一次 I/O 操作中传送固定大小的数据块。 可以随机访问设备中所存放的块:传送数据块所需要的时间独立于块在设备中的位置,也独立于当前设备的状态。块设备典型的例子是硬盘、软盘及 CD-ROM。也可以把 RAM 磁盘当作块设备来对待,这是通过把部分 RAM 配置成快速硬盘而获得的,因此,可以把这部分 RAM 作为应用程序高效存取数据的临时存储器。字符设备具有以下特点: 可以

24、在一次 I/O 操作中传送任意大小的数据。实际上,诸如打印机之类的字符设备可以一次传送一个字节,而诸如磁带之类的设备可以一次传送可变大小的数据块。 通常访问连续的字符。2. 网卡有些 I/O 设备没有对应的设备文件。最明显的一个例子是网卡。实际上,网卡把向外发送的数据放入通往远程计算机系统的一条线上,把从远程系统中接收到的报文装入内核内存。从 BSD 开始,所有的 Unix 类系统为计算机中的每个网卡都分配一个不同的符号名;例如,第一个以太网卡名为 eth0。然而,这个名字并没有对应的设备文件,也没有对应的索引节点。由于没有使用文件系统,所以系统管理员必须建立设备名和网络地址之间的联系。因此,

25、应用程序和网络接口之间的数据通信不是基于标准的有关文件的系统调用的;而是基于 socket( )、 bind( )、listen( )、accept()和 connect( )系统调用的,这些系统调用对网络地址进行操作。这组系统调用是在 Unix BSD 中首先引入的,现在已经成为网络设备的标准编程模型。11.2.4 VFS 对设备文件的处理虽然设备文件也在系统的目录树中,但是它们和普通文件以及目录有根本的不同。当进程访问普通文件(即磁盘文件)时,它会通过文件系统访问磁盘分区中的一些数据块;而在进程访问设备文件时,它只要驱动硬件设备就可以了。例如,进程可以访问一个设备文件以从连接到计算机的温度

26、计读取房间的温度。VFS 的责任是为应用程序隐藏设备文件与普通文件之间的差异。为了做到这点,VFS 改变打开的设备文件的缺省文件操作;因此,可以把对设备文件的任一系统调用转换成对设备相关的函数的调用,而不是对主文件系统相应函数的调用。设备相关的函数对硬件设备进行操作以完成进程所请求的操作。控制 I/O 设备的一组设备相关的函数称为设备驱动程序。由于每个设备都有一个唯一的 I/O 控制器,因此也就有唯一的命令和唯一的状态信息,所以大部分 I/O 设备类型都有自己的驱动程序。11.2.5 中断处理设备一般都比 CPU 慢得多。因此一般情况下,当一个进程通过设备驱动程序向设备发出读写请求后,CPU

27、并不等待 I/O 操作的完成,而是让正在执行的进程去睡眠,CPU 自己做别的事情,例如唤醒另一个进程执行。当设备完成 I/O 操作需要通知 CPU 时,会向 CPU 发出一个中断请求;然后 CPU 根据中断请求来决定调用相应的设备驱动程序。当设备执行某个命令时,如“将读取磁头移动到软盘的第 42 扇区上” ,设备驱动程序可以从查询方式和中断方式中选择一种来判断设备是否已经完成此命令。 查询方式意味着需要经常读取设备的状态,一直到设备状态表明请求已经完成为止。如果设备驱动程序被连接进内核,这时使用查询方式将会带来灾难性后果:内核将在此过程中无所事事,直到设备完成目前的请求。有一种方法可以有效的改

28、善这一弊端,就是通过使用系统定时器,使内核周期性调用设备驱动程序中的某个例程来检查设备状态。使用定时器是查询方式中最好的一种,但更有效的方法是使用中断。 基于中断的设备驱动程序,指的是在硬件设备需要服务时向 CPU 发一个中断信号,引发中断服务子程序执行 。这样就大大地提高了系统资源的利用率,使内核不必一直等到设备执行完任务后才开始有事可干,而是在设备工作期间内核就可以转去处理其它的事务,收到中断请求信号时再回头响应设备。1. Linux 对中断的管理Linux 内核为了将来自硬件设备的中断传递到相应的设备驱动程序,在驱动程序初始化的时候就将其对应的中断程序进行了登记,即通过调用函数 requ

29、est_irq ( ) 将其中断信息添加到结构为 irqaction 的数组中,从而使中断号和中断服务程序联系起来。请参见第四章。request_irq ( )函数原形如下:int request_irq(unsigned int irq, /* 中断请求号 */void (*handler)(int, void *, struct pt_regs *), /* 指向中断服务子程序 */unsigned long irqflags, /* 中断类型 */const char * devname, /* 设备的名字 */void *dev_id);另外,irqaction 的数据结构如下,其图示

30、如图 11.5。 struct irqaction void (*handler)(int, void *, struct pt_regs *); unsigned long flags;unsigned long mask;const char *name;void *dev_id;struct irqaction *next;static struct irqaction *irq_actionNR_IRQS+1 图 11.5 irqaction 的数据结构根据设备的中断号可以在数组 irq_action 检索到设备的中断信息。对中断资源的请求在驱动程序初始化时就已经完成。在传统的 PC

31、体系结构中 ,有些中断已经被固定下来。软盘设备正是这种情况,它的中断号总为 6。有时设备驱动程序可能不知道设备使用的中断号,对 PCI 设备来说这不是什么大问题,它们总是可以通过设备配置头知道其中断号。但对于 ISA 设备则没有取得中断号的方便方式,Linux 通过让设备驱动程序检测它们的中断号来解决这个问题。 让我们来看一下对 ISA 设备中断号的检测过程。设备驱动程序首先迫使 ISA 设备引起一个中断,系统中所有未被分配的中断都被打开。此时设备引发的中断可以通过可编程中断控制器来发送出去,在它接受到 CPU 的响应信号以后将中断号放置在数据线上,Linux 读取此数据并将其内容返回给设备驱

32、动程序。非 0 结果则表示在此次检测中有中断发生,设备驱动程序然后将关闭检测并将所有未分配中断屏蔽掉, 这样 ISA 设备驱动程序就成功的找到了设备的 IRQ 号。 基于 PCI 系统比基于 ISA 系统有更多的动态性。ISA 设备使用的中断引脚通常是通过硬件设备上的跳线来设置的。而每个 PCI 设备都对应一个配置头, PCI 设备在系统启动与初始化 PCI 时由 PCI BIOS 或 PCI 子系统来分配中断,将其放入配置头中,故而驱动程序可以方便的获得 PCI 设备使用的中断号。系统中可能存在许多 PCI 中断源,比如在使用 PCI-PCI 桥接器时。这些中断源的个数可能将超出系统可编程中

33、断控制器的引脚数。此时 PCI 设备必须共享中断号,中断控制器上的一个引脚可能被多个 PCI 设备同时使用。Linux 让中断的第一个请求者申明此中断是否可以共享,中断的共享将导致 irq_action 数组中的一个入口同时指向几个 irqaction数据结构,如图 11.5 所示。当共享中断发生时 Linux 将调用对应此中断源的所有中断处理过程。2. Linux 对中断的处理Linux 中断处理子系统的一个基本任务是将中断正确联系到中断处理代码中的正确位置。这些代码必须了解系统的中断拓扑结构。例如在中断控制器上引脚 6 上发生的软盘控制器中断必须被辨认出的确来自软盘并同系统的软盘设备驱动的

34、中断服务子程序联系起来。中断发生时, Linux 首先读取系统可编程中断控制器中中断状态寄存器,判断出中断源,将其转换成 irq_action 数组中偏移值(例如来自软盘控制器引脚 6 的中断将被转换成对应于 irq_action 数组中的第 7 个指针) ,然后调用其相应的中断处理程序。当 Linux 内核调用设备驱动程序的中断服务子程序时,必须找出中断产生的原因以及相应的解决办法,这是通过读取设备上的状态寄存器的内容来完成的。下面我们结合输入/输出系统的层次结构来看一下中断在驱动程序工作的过程中的作用:(1)用户发出某种输入/输出请求;(2)调用驱动程序的 read() 函数或 reque

35、st() 函数,将完成的输入/输出的指令送给设备控制器,现在设备驱动程序等待操作的发生。(3)一小段时间以后,硬设备准备好完成指令的操作,并产生中断信号标志事件的发生。(4)中断信号导致调用驱动程序的中断服务子程序,它将所要的数据从硬设备复制到设备驱动程序的缓冲区中,并通知正在等待的 read() 函数和 request()函数,现在数据可供使用。(5)在数据可供使用时,read()或 request()函数现在可将数据提供给用户进程。上述过程是经过了简化了的,但却反映了中断的主要过程的主要方面。11.2.6 驱动 DMA 工作所有的 PC 都包含一个称为直接内存访问控制器或 DMAC 的辅助

36、处理器,它可以用来控制在 RAM 和 I/O 设备之间数据的传送。DMAC 一旦被 CPU 激活,就可以自行传送数据;当数据传送完成之后,DMAC 发出一个中断请求。当 CPU 和 DMAC 同时访问同一内存单元时,所产生的冲突由一个称为内存仲裁器的硬件电路来解决。使用 DMAC 最多的是磁盘驱动器和其他需要一次传送大量字节的慢速设备。因为 DMAC的设置时间相当长,所以在传送数量很少的数据时直接使用 CPU 效率更高。原来的 ISA 总线所使用的第一个 DMAC 非常复杂,难于对其进行编程。PCI 和 SCSI 总线所使用的最新 DMAC 依靠总线中的专用硬件电路,这就使设备驱动程序开发人员

37、的开发工作变得简单。到现在为止,我们已区分了三类内存地址:逻辑地址、线性地址以及物理地址,前两个在 CPU 内部使用,最后一个是 CPU 从物理上驱动数据总线所用的内存地址。但是,还有第四种内存地址,称为总线地址:它是除 CPU 之外的硬件设备驱动数据总线所用的内存地址。在 PC 体系结构中,总线地址和物理地址是一致的;但是在其他体系结构中,例如 Sun的 SPARC 和 Compaq 的 Alpha 体系结构中,这两种地址是不同的。从根本上说,内核为什么应该关心总线地址呢?这是因为在 DMA 操作中数据传送不用CPU 的参与:I/O 设备和 DMAC 直接驱动数据总线。因此,在内核开始 DM

38、A 操作时,必须把所涉及的内存缓冲区总线地址或写入 DMAC 适当的 I/O 端口、或写入 I/O 设备适当的 I/O 端口。很多 I/O 驱动程序都使用直接内存访问控制器(DMAC)来加快操作的速度。DMAC 与设备的 I/O 控制器相互作用共同实现数据传送;后文中我们还会看到,内核中包含一组易用的例程来对 DMAC 进行编程。当数据传送完成时,I/O 控制器通过 IRQ 向 CPU 发出信号。当设备驱动程序为某个 I/O 设备建立 DMA 操作时,必须使用总线地址指定所用的内存缓冲区。内核提供两个宏 virt_to_bus 和 bus_to_virt,分别把虚拟地址转换成总线地址或把总线地

39、址转换成虚拟地址。与 IRQ 一样,DMAC 也是一种资源,必须把这种资源动态地分配给需要它的设备驱动程序。驱动程序开始和结束 DMA 操作的方法依赖于总线的类型。1.ISA 总线的 DMA每个 ISA DMAC 只能控制有限个通道。每个通道都包括一组独立的内部寄存器,所以,DMAC 就可以同时控制几个数据的传送。设备驱动程序通常使用下面的方式来申请和释放 ISA DMAC。设备驱动程序照样要靠一个引用计数器来检测什么时候任何进程都不再访问设备文件。驱动程序执行以下操作:(1) 在设备文件的 open( )方法中把设备的引用计数器加 1。如果原来的值是 0,那么,驱动程序执行以下操作: 调用

40、request_irq( )来分配 ISA DMAC 所使用的 IRQ 中断号 调用 request_dma( )来分配 DMA 通道 通知硬件设备应该使用 DMA 并产生中断 如果需要,为 DMA 缓冲区分配一个存储区域(2)当必须启动 DMA 操作时,在设备文件的 read()和 write()方法执行以下操作: 调用 set_dma_mode( )把通道设置成读/写模式。 调用 set_dma_addr( )来设置 DMA 缓冲区的总线地址。 (因为只有最低的 24 位地址会发给 DMAC,所以缓冲区必须在 RAM 的前 16MB 中。 ) 调用 set_dma_count( )来设置要

41、发送的字节数。 调用 set_dma_dma( )来启用 DMA 通道。 把当前进程加入该设备的等待队列,并把它挂起。当 DMAC 完成数据传送操作时,设备的 I/O 控制器就发出一个中断,相应的中断处理程序会唤醒正在睡眠的进程。 进程一旦被唤醒,就立即调用 disable_dma( )来禁用这个 DMA 通道。 调用 get_dma_residue( )来检查是否所有的数据都已被传送。(3) 在设备文件的 release 方法中,减少设备的引用计数器。如果该值变成 0,就执行以下操作: 禁用 DMA 和对这个硬件设备上的相应中断 调用 free_dma( )来释放 DMA 通道 调用 fre

42、e_irq( )来释放 DMA 所使用的 IRQ 线2PCI 总线的 DMAPCI 总线对于 DMA 的使用要简单得多,因为 DMAC 是集成到 I/O 接口内部的。在 open方法中,设备驱动程序照样必须分配一条 IRQ 线来通知 DMA 操作的完成。但是,并没有必要分配一个 DMA 通道,因为每个硬件设备都直接控制 PCI 总线的电信号。要启动 DMA 操作,设备驱动程序在硬件设备的某个 I/O 端口中简单地写入 DMA 缓冲区的总线地址、传送方向以及数据大小;然后驱动程序就挂起当前进程。在最后一个进程关闭这个文件对象时,release 方法负责释放这条 IRQ 线。11.2.7 I/O

43、空间的映射很多硬件设备都有自己的内存,通常称之为 I/O 空间。例如,所有比较新的图形卡都有几 MB 的 RAM,称为显存,用它来存放要在屏幕上显示的屏幕影像。1地址映射根据设备和总线类型的不同,PC 体系结构中的 I/O 空间可以在三个不同的物理地址范围之间进行映射:(1)对于连接到 ISA 总线上的大多数设备I/O 空间通常被映射到从 0xa0000 到 0xfffff 的物理地址范围,这就在 640K 和 1MB 之间留出了一段空间,这就是所谓的“洞” 。(2)对于使用 VESA 本地总线(VLB)的一些老设备这是主要由图形卡使用的一条专用总线:I/O 空间被映射到从 0xe00000

44、到 0xffffff的地址范围中,也就是 14MB 到 16MB 之间。因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。(3)对于连接到 PCI 总线的设备I/O 空间被映射到很大的物理地址区间,位于 RAM 物理地址的顶端。这种设备的处理比较简单。2访问 I/O 空间内核如何访问一个 I/O 空间单元?让我们从 PC 体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。不要忘了内核程序作用于虚拟地址,因此 I/O 空间单元必须表示成大于 PAGE_OFFSET的地址。在后面的讨论中,我们假设 PAGE_OFFSET 等于 0xc0000000,也就是说,

45、内核虚拟地址是在第 4G。内核驱动程序必须把 I/O 空间单元的物理地址转换成内核空间的虚拟地址。在 PC 体系结构中,这可以简单地把 32 位的物理地址和 0xc0000000 常量进行或运算得到。例如,假设内核需要把物理地址为 0x000b0fe4 的 I/O 单元的值存放在 t1 中,把物理地址为0xfc000000 的 I/O 单元的值存放在 t2 中,就可以使用下面的表达式来完成这项功能:t1 = *(unsigned char *)(0xc00b0fe4); t2 = *(unsigned char *)(0xfc000000); 在第六章我们已经介绍过,在初始化阶段,内核已经把可

46、用的 RAM 物理地址映射到虚拟地址空间第 4G 的最初部分。因此,分页机制把出现在第一个语句中的虚拟地址0xc00b0fe4 映射回到原来的 I/O 物理地址 0x000b0fe4,这正好落在从 640K 到 1MB 的这段“ISA 洞”中。这正是我们所期望的。但是,对于第二个语句来说,这里有一个问题,因为其 I/O 物理地址超过了系统 RAM的最大物理地址。因此,虚拟地址 0xfc000000 就不需要与物理地址 0xfc000000 相对应。在这种情况下,为了在内核页表中包括对这个 I/O 物理地址进行映射的虚拟地址,必须对页表进行修改:这可以通过调用 ioremap( )函数来实现。

47、ioremap( )和 vmalloc( )函数类似,都调用 get_vm_area( ) 建立一个新的 vm_struct 描述符,其描述的虚拟地址区间为所请求 I/O 空间区的大小。然后,ioremap( )函数适当地更新所有进程的对应页表项。因此,第二个语句的正确形式应该为:io_mem = ioremap(0xfb000000, 0x200000); t2 = *(unsigned char *)(io_mem + 0x100000); 第一条语句建立一个 2MB 的虚拟地址区间,从 0xfb000000 开始;第二条语句读取地址0xfc000000 的内存单元。驱动程序以后要取消这种

48、映射,就必须使用 iounmap( )函数。现在让我们考虑一下除 PC 之外的体系结构。在这种情况下,把 I/O 物理地址加上0xc0000000 常量所得到的相应虚拟地址并不总是正确的。为了提高内核的可移植性,Linux 特意包含了下面这些宏来访问 I/O 空间:readb, readw, readl 分别从一个 I/O 空间单元读取 1、2 或者 4 个字节writeb, writew, writel 分别向一个 I/O 空间单元写入 1、2 或者 4 个字节memcpy_fromio, memcpy_toio 把一个数据块从一个 I/O 空间单元拷贝到动态内存中,另一个函数正好相反,把一

49、个数据块从动态内存中拷贝到一个 I/O 空间单元memset_io 用一个固定的值填充一个 I/O 空间区域对于 0xfc000000 I/O 单元的访问推荐使用这样的方法:io_mem = ioremap(0xfb000000, 0x200000); t2 = readb(io_mem + 0x100000); 使用这些宏,就可以隐藏不同平台访问 I/O 空间所用方法的差异。11.2.8 设备驱动程序框架由于设备种类繁多,相应的设备驱动程序也非常之多。尽管设备驱动程序是内核的一部分,但设备驱动程序的开发往往由很多人来完成,如业余编程高手、设备厂商等。为了让设备驱动程序的开发建立在规范的基础上,就必须在驱动程序和内核之间有一个严格定义和管理的接口,例如 SVR4 提出了 DDI/DDK 规范,其含义就是设备与驱动程序接口设备驱动程序与内核接口(Device-Driver InterfaceDriver-Kernel Interface) 。通过这个规范,可以规范设备驱动程序与内核之间的接口。Linux 的设备驱动程序与外接的接口与 DDI/DKI 规范相似,可以分为三部分:(

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 网络科技 > linux/Unix相关

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报