Device Drivers(设备驱动程序)
操作系统的目标之一是向用户掩盖系统硬件设备的特殊性。例如,虚拟文件系统呈现给用户一个统一的文件系统视图,而和底层的物理设备无关。本章描述Linux内核是如何管理系统中的物理设备的。
CPU不是系统中唯一的智能设备,每一个物理设备都由它自己的硬件控制器。键盘、鼠标和串行口由SuperIO芯片控制,IDE磁盘由IDE控制器控制,SCSI磁盘由SCSI控制器控制,等等。每一个硬件控制器都由自己的控制和状态寄存器(CSR),而且不同的设备有不同的寄存器。一个Adaptec 2940 SCSI控制器的CSR和NCR 810 SCSI控制器的CSR完全不同。CSR用于启动和停止设备,初始化设备和诊断它的问题。管理这些硬件控制器的代码不是放在每一个应用程序里边,而是放在Linux内核。这些处理或者管理硬件控制器的软件叫做设备驱动程序。本质上,Linux内核的设备驱动程序是特权的、驻留在内存的、低级硬件控制例程的共享库。正是Linux的设备驱动程序处理它们所管理的设备的特性。
UNIX的一个基本特点是它抽象了对设备的处理。所有的硬件设备都象常规文件一样看待:它们可以使用和操作文件相同的、标准的系统调用来打开、关闭和读写。系统中的每一个设备都用一个设备特殊文件代表。例如系统中第一个IDE硬盘用/dev/had表示。对于块(磁盘)和字符设备,这些设备特殊文件用mknod命令创建,并使用主(major)和次(minor)设备编号来描述设备。网络设备也用设备特殊文件表达,但是它们由Linux在找到并初始化系统中的网络控制器的时候创建。同一个设备驱动程序控制的所有设备都有一个共同的major设备编号。次设备编号用于区分不同的设备以及它们的控制器。例如,主IDE磁盘的不同分区都有一个不同的次设备编号。所以,/dev/hda2,主IDE磁盘的第2个分区,其主设备号是3,而次设备号是2。Linux使用主设备号表和一些系统表(例如字符设备表chrdevs)把系统调用中传递的设备特殊文件(比如在一个块设备上安装一个文件系统)映射到这个设备的设备驱动程序中。
参见fs/devices.c
Linux支持三种类型的硬件设备:字符、块和网络。
l 字符设备是没有缓冲直接读写的设备,例如系统的串行端口/dev/cua0和/dev/cua1。
l 块设备是只能按照一个块(一般是512字节或者1024字节)的倍数进行读写的设备。块设备通过buffer cache访问,可以随机存取,就是说,任何块都可以读写,不必考虑它在设备的什么地方。块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。只有一个块设备可以支持一个安装的文件系统。
l 网络设备是通过BSD socket接口访问的设备,网络子系统在网络章(第10章)描述。
Linux有许多不同的设备驱动程序(这也是Linux的力量之一),它们都具有一些一般的属性:
Kernel code
设备驱动程序和内核中的其它代码相似,是kenel的一部分,如果发生错误,可能严重损害系统。一个粗劣的驱动程序甚至可能摧毁系统,可能破坏文件系统,丢失数据。
Kenel interfaces
设备驱动程序必须向Linux内核或者它所在的子系统提供一个标准的接口。例如,终端驱动程序向Linux内核提供了一个文件I/O接口,而SCSI设备驱动程序向SCSI子系统提供了SCSI设备接口,接着,向内核提供了文件I/O和buffer cache的接口。
Kernel mechanisms and services
设备驱动程序使用标准的内核服务,例如内存分配、中断转发和等待队列来完成工作。
Loadable
大多数的Linux设备驱动程序,可以在需要的时候作为内核模块加载,在不再需要的时候卸载。这使得内核对于系统资源非常具有适应性和效率。
Configurable
Linux设备驱动程序可以建立在内核。至于哪些设备建立到内核,可以在内核编译的时候配置。
Dynamic
在系统启动,每一个设备启动程序初始化的时候,它会查找它管理的硬件设备。如果一个设备驱动程序所控制的设备不存在并没有关系。这时这个设备驱动程序只是多余的,占用很少的系统内存,而不会产生危害。
8.1 Poling and Interrupts(轮询和中断)
每一次给设备命令(例如“把读磁头移到软盘的第42扇区“)的时候,设备驱动程序都可以选择采用什么手段来判断命令是否已经执行结束。设备驱动程序可以轮询设备或者使用中断。
轮询设备通常意味着不断读取它的状态寄存器,直到设备的状态改变指示它已经完成了请求。因为设备驱动程序是内核的一部分,如果驱动程序一直在轮询,内核在设备完成请求之前不能运行其它任何东西,会是惨重的损失。所以轮询的设备驱动程序使用一个系统计时器,让内核在晚些时候调用设备驱动程序中的一个例程。这个定时器例程会检查命令的状态,Linux的软盘驱动程序就是这样工作的。使用计时器进行轮询是一种最好的近似,而更加有效的方法是使用中断。
中断驱动的设备驱动程序在它控制的硬件设备需要服务时,会发出一个硬件中断。例如:当在网络上接收到一个以太网报文时,以太网设备驱动程序会被设备中断。Linux内核要有能力把从硬件设备来的中断转发到正确的设备驱动程序。这通过设备驱动程序向内核登记它所使用的中断来实现。它登记中断处理程序例程的地址和它希望拥有的中断编号。你通过/proc/interrupts可以看到设备驱动使用了哪些中断和每一类型的中断使用了多少次:
0: 727432 timer
1: 20534 keyboard
2: 0 cascade
3: 79691 + serial
4: 28258 + serial
5: 1 sound blaster
11: 20868 + aic7xxx
13: 1 math error
14: 247 + ide0
15: 170 + ide1
对于中断资源的请求发生在驱动程序初始化时。系统中有些中断是固定的,这是IBM PC体系结构的遗留物。例如,软驱控制器总是用中断6。其它中断,例如PCI设备的中断,在启动的时候动态分配。这时设备驱动程序必须首先找出它所控制的设备的中断号,然后才能请求拥有这个中断的处理权。对于PCI中断,Linux支持标准的PCI BIOS回调(callback)来确定系统中设备的信息,包括它们的IRQ。
一个中断本身如何被转发到CPU,依赖于体系结构。但是在大多数的体系结构上,中断都用一种特殊的模式传递,在这种模式下,系统中其它的中断将被停止。设备驱动程序在它的中断处理例程中应该做尽可能少的工作,以便Linux内核可以结束中断,返回到它被中断以前的地方。收到中断后需要做大量工作的设备驱动程序可以使用内核的bottom half handler或者任务队列把例程排在后面,以便在以后调用。
8.2 Direct Memory Access (DMA)
当数据量比较少的时候,用中断驱动的设备驱动程序向设备传输数据或者从设备接收,会工作得相当好。例如,一个9600波特率的modem每一毫秒(1/1000秒)大约可以传输一个字符。如果中断延迟,就是从硬件设备发出中断到设备驱动程序中的中断处理程序被调用之间所花的时间比较少(比如2毫秒),那么数据传输对系统整体性能的影响就非常小。9600波特率的modem的数据传输只会占用0.002%的CPU处理时间。但是对于高速设备,比如硬盘控制器或者以太网设备,数据传输速率就相当高。一个SCSI设备每秒可以传输高达40M字节的信息。
直接内存存取,或者说DMA,就是发明来解决这个问题的。一个DMA控制器,在不需要处理器干预的情况下,允许在设备和系统内存之间传输数据。PC的ISA DMA控制器有8个DMA通道,设备驱动程序可使用其中的7个。每一个DMA通道都关联一个16位的地址寄存器和一个16位的计数寄存器(count register)。为了初始化一次数据传输,设备驱动程序需要设置DMA通道的地址和计数寄存器,加上数据传输的方向:读或写。然后,设备驱动程序就可以告诉设备:它可以在需要的时候启动DMA。当传输结束时,设备中断PC。这样,在CPU作其它事情的时候(不需要CPU的参与),可以发生传输。
使用DMA时,设备驱动程序要非常小心。首先,所有的DMA控制器都不了解虚拟内存,它只能访问系统中的物理内存。因此,需要进行DMA传输的内存必须是连续的物理内存块。这意味着你不能直接对一个进程的虚拟地址空间进行DMA访问。但是