CPU 不是系统中唯一的智能设备,每一个物理设备都由它自己的硬件控制器。键盘、鼠标和串行口由 SuperIO 芯片控制, IDE 磁盘由 IDE 控制器控制, SCSI 磁盘由 SCSI 控制器控制,等等。每一个硬件控制器都由自己的控制和状态控制器( CSR ),不同的设备之间是不同的。一个 Adaptec 2940 SCSI 控制器的 CSR 和 NCR 810 SCSI 控制器的完全不同。 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 支持三类的硬件设备:字符、块和网络。字符设备直接读写,没有缓冲区,例如系统的串行端口 /dev/cua0 和 /dev/cua1 。块设备只能按照一个块(一般是 512 字节或者 1024 字节)的倍数进行读写。块设备通过 buffer cache 访问,可以随机存取,就是说,任何块都可以读写而不必考虑它在设备的什么地方。块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。只有一个块设备可以支持一个安装的文件系统。网络设备通过 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 通道的地址和计数寄存器,加上数据传输的方向,读或写。当传输结束的时候,设备中断 PC 。这样,传输发生的时候, CPU 可以作其他事情。
使用 DMA 的时候设备驱动程序必须小心。首先,所有的 DMA 控制器都不了解虚拟内存,它只能访问系统中的物理内存。因此,需要进行 DMA 传输的内存必须是物理内存中连续的块。这意味着你不能对于一个进程的虚拟地址空间进行 DMA 访问。但是你可以在执行 DMA 操作的时候把进程的物理也锁定到内存中。第二: DMA 控制器无法访问全部的物理内存。 DMA 通道的地址寄存器表示 DMA 地址的首 16 位,跟着的 8 位来自于页寄存器( page register )。这意味着 DMA 请求限制在底部的 16M 内存中。
DMA 通道是稀少的资源,只有 7 个,又不能在设备驱动程序之间共享。象中断一样,设备驱动程序必须有能力发现它可以使用哪一个 DMA 通道。象中断一样,一些设备有固定的 DMA 通道。比如软驱设备,总是用 DMA 通道 2 。有时,设备的 DMA 通道可以用跳线设置:一些以太网设备用这种技术。一些更灵活的设备可以告诉它(通过它们的 CSR )使用哪一个 DMA 通道,这时,设备驱动程序可以简单地找出一个可用的 DMA 通道。
Linux 使用 dma_chan 数据结构向量表(每一个 DMA 通道一个)跟踪 DMA 通道的使用。 Dma_chan 数据结构只有两个玉:一个字符指针,描述这个 DMA 通道的属主,一个标志显示这个 DMA 通道是否被分配。当你 cat /proc/dma 的时候显示的就是 dma_chan 向量表。
8.3 Memory (内存)
设备驱动程序必须小心使用内存。因为它们是 Linux 核心的一部分,它们不能使用虚拟内存。每一次设备驱动程序运行的时候,可能是接收到了中断或者调度了一个 buttom half handler 或任务队列,当前的进程都可能改变。设备驱动程序不能依赖于一个正在运行的特殊进程。象核心中其他部分一样,设备驱动程序使用数据结构跟踪它控制的设备。这些数据结构可以在设备驱动程序的代码部分静态分配,但是这会让核心不必要地增大而浪费。多数设备驱动程序分配核心的、不分页的内存存放它们的数据。
Linux 核心提供了核心的内存分配和释放例程,设备驱动程序正是使用了这些例程。核心内存按照 2 的幂数的块进行分配。例如 128 或 512 字节,即使设备驱动程序请求的数量没有这么多。设备驱动程序请求的字节数按照下一个块的大小取整。这使得核心的内存回收更容易,因为较小的空闲块可以组合成更大的块。
请求核心内存的时候 Linux 还需要做更多的附加工作。如果空闲内存的总数太少,物理页需要废弃或者写到交换设备。通常, Linux 会挂起请求者,把这个进程放到一个等待队列,直到有了足够的物理内存。不是所有的设备驱动程序(或者实际是 Linux 的核心代码)希望发生这样的事情,核心内存分配例程可以请求如果不能立刻分配内存就失败。如果设备驱动程序希望为 DMA 访问分配内存,它也需要指出这块内存是可以进行 DMA 的。因为需要让 Linux 核心明白系统中哪些是连续的可以进行 DMA 的内存,而不是让设备驱动程序决定。
8.4 Interfacing Device Drivers with the Kernel (设备驱动程序和核心接口)
Linux 核心必须能够用标准的方式和它们作用。每一类的设备驱动程序:字符、块和网络,都提供了通用的接口供核心在需要请求它们的服务的时候使用。这些通用的接口意味着核心可以完全相同地看待通常是非常不同的设备和它们的设备驱动程序。例如, SCSI 和 IDE 磁盘的行为非常不同,但是 Linux 核心对它们使用相同的接口。
Linux 非常地动态,每一次 Linux 核心启动,它都可能遇到不同的物理设备从而需要不同的设备驱动程序。 Linux 允许你在核心建立的时间通过配置脚本包含设备驱动程序。当启动的时候这些设备驱动程序初始化,它们可能没发现它们可以控制的任何硬件。其他驱动程序可以在需要的时候作为核心模块加载。为了处理设备驱动程序的这种动态的特质,设备驱动程序在它们初始化的时候向核心登记。 Linux 维护已经登记的设备驱动程序列表,作为和它们接口的一部分。这些列表包括了例程的指针和支持这一类设备的接口的信息。
8.4.1 Character Devices (字符设备)
字符设备, Linux 最简单的设备,象文件一样访问。应用程序使用标准系统调用打开、读取、写和关闭,完全好像这个设备是一个普通文件一样。甚至连接一个 Linux 系统上网的 PPP 守护进程使用的 modem ,也是这样的。当字符设备初始化的时候,它的设备驱动程序向 Linux 核心登记,在 chrdevs 向量表增加一个 device_struct 数据结构条目。这个设备的主设备标识符(例如对于 tty 设备是 4 ),用作这个向量表的索引。一个设备的主设备标识符是固定的。 Chrdevs 向量表中的每一个条目,一个 device_struct 数据结构,包括两个元素:一个登记的设备驱动程序的名称的指针和一个指向一组文件操作的指针。这块文件操作本身位于这个设备的字符设备驱动程序中,每一个都处理特定的文件操作比如打开、读、写和关闭。 /proc/devices 中字符设备的内容来自 chrdevs 向量表
参见 include/linux/major.h
当代表一个字符设备(例如 /dev/cua0 )的字符特殊文件打开,核心必须做一些事情,从而去掉用正确的字符设备驱动程序的文件操作例程。和普通文件或目录一样,每一个设备特殊文件都用 VFS I 节点表达。这个字符特殊文件的 VFS inode (实际上所有的设备特殊文件)都包括设备的 major 和 minor 标识符。这个 VFS I 节点由底层的文件系统(例如 EXT2 ),在查找这个设备特殊文件的时候根据实际的文件系统创建。
参见 fs/ext2/inode.c ext2_read_inode()
每一个 VFS I 节点都联系着一组文件操作,依赖于 I 节点所代表的文件系统对象不同而不同。不管代表一个字符特殊文件的 VFS I 节点什么时候创建,它的文件操作被设置成字符设备的缺省操作。这只有一种文件操作: open 操作。当一个应用程序打开这个字符特殊文件的时候,通用的 open 文件操作使用设备的主设备标识符作为 chrdevs 向量表中的索引,取出这种特殊设备的文件操作块。它也建立描述这个字符特殊文件的 file 数据结构,让它的文件操作指向设备驱动程序中的操作。然后应用程序所有的文件系统操作都被映射到字符设备的文件操作。
参见 fs/devices.c chrdev_open() def_chr_fops
8.4.2 Block Devices (块设备)
块设备也支持象文件一样被访问。这种为打开的块特殊文件提供正确的文件操作组的机制和字符设备的十分相似。 Linux 用 blkdevs 向量表维护已经登记的块设备文件。它象 chrdevs 向量表一样,使用设备的主设备号作为索引。它的条目也是 device_struct 数据结构。和字符设备不同,块设备进行分类。 SCSI 是其中一类,而 IDE 是另一类。类向 Linux 核心登记并向核心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。例如, SCSI 设备驱动程序必须向 SCSI 子系统提供接口,让 SCSI 子系统用来对核心提供这种设备的文件操作
参见 fs/devices.c
每一个块设备驱动程序必须提供普通的文件操作接口和对于 buffer cache 的接口。每一个块设备驱动程序填充 blk_dev 向量表中它的 blk_dev_struct 数据结构。这个向量表的索引还是设备的主设备号。这个 blk_dev_struct 数据结构包括一个请求例程的地址和一个指针,指向一个 request 数据结构的列表,每一个都表达 buffer cache 向设备读写一块数据的一个请求。
参见 drivers/block/ll_rw_blk.c include/linux/blkdev.h
每一次 buffer cache 希望读写一块数据到或从一个登记的设备的时候它就在它的 blk_dev_struc 中增加一个 request 数据结构。图 8.2 显示了每一个 request 都有一个指针指向一个或多个 buffer_head 数据结构,每一个都是一个读写一块数据的请求。这个 buffer_head 数据结构被锁定( buffer cache ),可能会有一个进程在等待这个缓冲区的阻塞进程完成。每一个 request 结构都是从一个静态表, all_request 表中分配的。如果这个 request 增加到一个空的 request 列表,就调用驱动程序的 request 函数处理这个 request 队列。否则,驱动程序只是简单地处理 request 队列中的每一个请求。
一旦设备驱动程序完成了一个请求,它必须把每一个 buffer_head 结构从 request 结构中删除,标记它们为最新的,然后解锁。对于 buffer_head 的解锁会唤醒任何正在等待这个阻塞操作完成的进程。这样的例子包括文件解析的时候:必须等待 EXT2 文件系统从包括这个文件系统的块设备上读取包括下一个 EXT2 目录条目的数据块,这个进程将会在将要包括目录条目的 buff_head 队列中睡眠,直到设备驱动程序唤醒它。这个 request 数据结构会被标记为空闲,可以被另一个块请求使用。
8.5 Hard Disks (硬盘)
硬盘把数据存放在转动的磁碟上,提供了一个更永久存储数据的方式。为了写入数据,微小的磁头把磁碟表面的一个微小的点磁化。通过磁头可以探测指定的微粒是否被磁化,从而可以读出数据。
一个磁盘驱动器由一个或多个磁碟组成,每一个都用相当光滑的玻璃或者陶瓷制成,并覆盖上一层精细的金属氧化物。磁碟放在一个中心轴上面,并按照稳定的速度转动。转动速度根据型号不同从 3000 到 1000RPM (转 / 每分钟)。磁盘的读 / 写磁头负责读写数据,每一个磁碟有一对,每一面一个。读 / 写磁头和磁碟表面并没有物理的接触,而是在一个很薄的空气垫(十万分之一英寸)上面漂浮。读写磁头通过一个驱动器在磁碟表面移动。所有的磁头都粘在一起,一起在磁碟表面移动。
每一个磁碟的表面都分成多个狭窄的同心环,叫做磁道( track )。磁道 0 是最外面的磁道,最高编号的磁道是最接近中心轴的磁道。一个柱面( cylinder )是相同编号磁道的组合。所以每一个磁碟的每一面的所有的第 5 磁道就是第 5 柱面。因为柱面数和磁道数相同,所以磁盘的尺寸常用柱面来描述。每一个磁道分成扇区。一个扇区是可以从硬盘读写的最小数据单元,也就是磁盘的块大小。通常扇区大小是 512 字节,扇区大小通常是在制造磁盘的时候进行格式化的时候设定的。
磁盘通常用它的尺寸( geometry )描述:柱面数、磁头数和扇区数。例如,启动的时候 Linux 这样描述我的 IDE 磁盘:
hdb: Conner Peripherals 540MB - CFS540A, 516MB w/64kB Cache, CHS=1050/16/63
这意味着它由 1050 柱面(磁道), 16 头( 8 个磁碟)和 63 个扇区 / 磁道。对于 512 字节的扇区或块大小,磁盘的容量是 529200K 字节。这和磁盘声明的 516M 的存储能力不符合,因为一些扇区用作存储磁盘的分区信息。一些磁盘可以自动找出坏的扇区,对其进行重新索引。
硬盘可以再分为分区。一个分区是分配用于特定目的的一大组扇区。对磁盘分区允许磁盘用于几个操作系统或多个目的。大多数单个磁盘的 Linux 系统都由 3 个分区:一个包含 DOS 文件系统,另一个是 EXT2 文件系统,第三个是交换分区。硬盘的分区用分区表描述,每一个条目用磁头、扇区和柱面号描述分区的起止位置。对于用 fdisk 格式化的 DOS 磁盘,可以有 4 个主磁盘分区。不是分区表所有的 4 个条目都必须用到。 Fdisk 支持三种类型的分区:主分区、扩展分区和逻辑分区。扩展分区不是真正的分区,它可以包括任意数目的逻辑分区。发明扩展分区和逻辑分区是为了突破 4 个主分区的限制。下面是一个包括 2 个主分区的磁盘的 fdisk 的输出:
Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Units = cylinders of 2048 * 512 bytes
Device Boot Begin Start End Blocks Id System
/dev/sda1 1 1 478 489456 83 Linux native
/dev/sda2 479 479 510 32768 82 Linux swap
Expert command (m for help): p
Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Nr AF Hd Sec Cyl Hd Sec Cyl Start Size ID
1 00 1 1 0 63 32 477 32 978912 83
2 00 0 1 478 63 32 509 978944 65536 82
3 00 0 0 0 0 0 0 0 0 00
4 00 0 0 0 0 0 0 0 0 00
它显示了第一个分区开始于柱面或磁道 0 ,磁头 1 和扇区 1 ,直到柱面 477 ,扇区 32 和磁头 63 。因为一个磁道由 32 个扇区和 64 个读写磁头,这个分区的柱面都是完全包括的。 Fdisk 缺省把分区对齐在柱面的边界。它从最外面的柱面( 0 )开始向内,朝向中心轴,扩展 478 个柱面。第 2 个分区,交换分区,开始于下一个柱面( 478 )并扩展到磁盘最里面的柱面。
在初始化的时候 Linux 映射系统中的硬盘的拓扑结构。它找出系统中有多少个硬盘以及硬盘的类型。 Linux 还找出每一个磁盘如何分区。这些都是由 gendisk_head 指针列表指向的一组 gendisk 数据结构的列表表达。对于每一个磁盘子系统,例如 IDE ,初始化的时候生成 gendisk 数据结构表示它找到的磁盘。这个过程和它登记它的文件操作和在 blk_dev 数据结构中增加它的条目发生在同一时间。每一个 gendisk 数据结构都由一个唯一的主设备号,和块特殊设备的相同。例如, SCSI 磁盘子系统会创建一个独立的 gendisk 条目(“ sd ”),主设备号是 8 (所有 SCSI 磁盘设备的主设备号)。图 8.3 显示了两个 gendisk 条目,第一个是 SCSI 磁盘子系统,第二个是 IDE 磁盘控制器。这里是 ide0 ,主 IDE 控制器。
虽然磁盘子系统在初始化的时候会建立相应的 gendisk 条目, Linux 只是在进行分区检查的时候才用到。每一个磁盘子系统必须维护自己的数据结构,让它自己可以把设备的主设备号和次设备号映射到物理磁盘的分区上。不管什么时候读写块设备,不管是通过 buffer cache 或者文件操作,核心都根据它在块特殊设备文件(例如 /dev/sda2 )中找到的主设备号和次设备号把操作定向到合适的设备。是每一个设备驱动程序或子系统把次设备号映射到真正的物理设备上。
8.5.1 IDE Disks ( IDE 磁盘)
今天 Linux 系统中最常用的磁盘是 IDE 磁盘( Integrated Disk Electronic )。 IDE 和 SCSI 一样是一个磁盘接口而不是一个 I/O 总线。每一个 IDE 控制器可以支持最多 2 个磁盘,一个是 master ,另一个是 slave 。 Master 和 slave 通常用磁盘上的跳线设置。系统中的第一个 IDE 控制器叫做主 IDE 控制器,下一个叫从属控制器等等。 IDE 可以从 / 向磁盘进行 3.3M/ 秒的传输, IDE 磁盘的最大尺寸是 538M 字节。扩展 IDE 或 EIDE 把最大磁盘尺寸增加到 8.6G 字节,数据传输速率高达 16.6M/ 秒。 IDE 和 EIDE 磁盘比 SCSI 磁盘便宜,大多数现代 PC 都有一个或更多的主板上的 IDE 控制器。
Linux 按照它发现的控制器的顺序命名 IDE 磁盘。主控制器上的主磁盘是 /dev/had , slave 磁盘是 /dev/hdb 。 /dev/hdc 是次 IDE 控制器上的 master 磁盘。 IDE 子系统向 Linux 登记 IDE 控制器而不是磁盘。主 IDE 控制器的主标识符是 3 ,次 IDE 控制器的标识符是 22 。这意味着如果一个系统有两个 IDE 控制器,那么在 blk_dev 和 blkdevs 向量表中在索引 3 和 22 会有 IDE 子系统的条目。 IDE 磁盘的块特殊文件反映了这种编号:磁盘 /dev/had 和 /dev/hdb ,都连接在主 IDE 控制器上,主设备号都是 3 。核心使用主设备标识符作为索引,对于这些块特殊文件的 IDE 子系统进行的所有的文件或者 buffer cache 操作都被定向到相应的 IDE 子系统。当执行一个请求的时候, IDE 子系统负责判断这个请求是针对哪一个 IDE 磁盘。为此, IDE 子系统使用设备特殊文件中的次设备号,这些信息允许它把请求定向到正确的磁盘的正确的分区。 /dev/hdb ,主 IDE 控制器上的 slave IDE 磁盘的设备标识符是( 3 , 64 )。它的第一个分区( /dev/hdb1 )的设备标识符是( 3 , 65 )。
8.5.2 Initializing the IDE Subsystem (初始化 IDE 子系统)
IBM PC 的大部分历史中都有 IDE 磁盘。这期间这些设备的接口发生了变化。这让 IDE 子系统的初始化过程比它第一次出现的时候更加复杂。
Linux 可以支持的最大 IDE 控制器数目是 4 。每一个控制器都用一个 ide_hwifs 向量表中的一个 ide_hwif_t 数据结构表示。每一个 ide_hwif_t 数据结构包含两个 ide_drive_t 数据结构,分别表示可能支持的 master 和 slave IDE 驱动器。在 IDE 子系统初始化期间, Linux 首先查看在系统的 CMOS 内存中记录的磁盘的信息。这种用电池做后备的内存在 PC 关机的时候不会丢失它的内容。这个 CMOS 内存实际上在系统的实时时钟设备里面,不管你的 PC 开或者关,它都在运行。 CMOS 内存的位置由系统的 BIOS 设置,同时告诉 Linux 系统中找到了什么 IDE 控制器和驱动器。 Linux 从 BIOS 中获取找到的磁盘的尺寸( geometry ),用这些信息设置这个驱动器的 ide_hwif_t 的数据结构。大多数现代 PC 使用 PCI 芯片组例如 Intel 的 82430 VX 芯片组,包括了一个 PCI EIDE 控制器。 IDE 子系统使用 PCI BIOS 回调( callback )定位系统中的 PCI ( E ) IDE 控制器。然后调用这些芯片组的询问例程。
一旦发现一个 IDE 接口或者控制器,就设置它的 ide_hwif_t 来反映这个控制器和上面的磁盘。操作过程中 IDE 驱动程序向 I/O 内存空间的 IDE 命令寄存器写命令。主 IDE 控制器的控制和状态寄存器的缺省的 I/O 地址是 0x1F0-0x1F7 。这些地址是早期的 IBM PC 约定下来的。 IDE 驱动程序向 Linux 的 buffer cache 和 VFS 登记每一个控制器,分别把它加到 blk_dev 和 blkdevs 向量表中。 IDE 驱动程序也请求控制适当的中断。同样,这些中断也有约定,主 IDE 控制器是 14 ,次 IDE 控制器是 15 。但是,象所有的 IDE 细节一样,这些都可以用核心的命令行选项改变。 IDE 驱动程序在启动的时候也为每一个找到的 IDE 控制器在 gendisk 列表中增加一个 gendisk 条目。这个列表稍后用于查看启动时找到的所有的硬盘的分区表。分区检查代码明白每一个 IDE 控制器可以控制两个 IDE 磁盘。
8.5.3 SCSI Disks ( SCSI 磁盘)
SCSI ( Small Computer System Interface 小型计算机系统接口)总线是一种有效的点对点的数据总线,每个总线支持多达 8 个设备,每个主机可以有一或者多个。每一个设备都必须由一个唯一的标识符,通常用磁盘上的跳线设置。数据可以在总线上的任意两个设备之间同步或者异步传输,可以用 32 位宽的数据传输,速度可能高达 40M/ 秒。 SCSI 总线可以在设备之间传输数据和状态信息,发起者( initiator )和目标( target )之间的事务会涉及多达 8 个不同的阶段。你可以通过 SCSI 总线上的 5 种信号判断出当前的阶段。这 8 个阶段是:
BUS FREE 没有设备有总线的控制权,当前没有发生任何事务。
ARBITRATION (仲裁)一个 SCSI 设备试图得到 SCSI 总线的控制权,它在地址管脚上声明( assert )它的 SCSI 标识符。最高编号的 SCSI 标识符成功。
SELECTION 一个设备通过仲裁成功地得到了 SCSI 总线的控制权,现在它必须向它要发送命令的 SCSI 目标发送信号。它在地址管脚上声明目标的 SCSI 标识符。
RESELECTION SCSI 设备在处理请求的过程中可能断线,目标会重新选择发起者。并非所有的 SCSI 设备都支持这一阶段。
COMMAND 6 、 10 或者 12 字节的命令可以从发起者发送到目标。
DATA IN , DATA OUT 在这一阶段,数据在发起者和目标之间传输。
STATUS 在完成了所有的命令,进入这一阶段。允许目标向发起者发送一个状态字节,表示成功或失败。
MESSAGE IN , MESSAGE OUT 在发起者和目标之间传递的附加信息。
Linux SCSI 子系统由两个基本元素组成,每一个都用数据结构表示:
Host 一个 SCSI host 是一个物理的硬件,一个 SCSI 控制器。 NCR810 PCI SCSI 控制器是一个 SCSI host 的例子。如果一个 Linux 系统有多于一个同类型的 SCSI 控制器,每一个实例都分别用一个 SCSI host 表示。这意味着一个 SCSI 设备驱动程序可能控制多于一个控制器的实例。 SCSI host 通常总是 SCSI 命令的发起者( initiator )。
Device SCSI 设备通常是磁盘,但是 SCSI 标准支持多种类型:磁带、 CD-ROM 和通用( generic ) SCSI 设备。 SCSI 设备通常都是 SCSI 命令的目标。这些设备必须不同地对待。例如可移动介质如 CD-ROM 或磁带, Linux 需要探测介质是否取出。不同的磁盘类型有不同的主设备编号,允许 Linux 把块设备请求定向到合适的 SCSI 类型。
Initializing the SCSI Subsystem (初始化 SCSI 子系统)
初始化 SCSI 子系统相当复杂,反映出 SCSI 总线和设备的动态的实质。 Linux 在启动的时候初始化 SCSI 子系统:它查找系统中的 SCSI 控制器( SCSI host ),并探测每一个 SCSI 总线,查找每一个设备。然后初始化这些设备,让 Linux 核心的其余部分可以通过普通的文件和 buffer cache 块设备操作访问它们。这个初始化过程有四个阶段:
首先, Linux 找出核心建立的时候建立到核心的哪一个 SCSI host 适配器或控制器有可以控制的硬件。每一个内建的 SCSI host 在 buildin_scsi_hosts 向量表中都有一个 Scsi_Host_Template 的条目。这个 Scsi_Host_Template 数据结构包括例程的指针,这些例程可以执行和 SCSI host 相关的动作例如探测这个 SCSI host 上粘附了什么 SCSI 设备。这些例程在 SCSI 子系统配置期间被调用,是支持这种 host 类型的 SCSI 设备驱动程序的一部分。每一个查到的 SCSI 控制器(有真实的 SCSI 设备粘附),它的 Scsi_Host_Template 数据结构都加到 scsi_hosts 列表中,表示有效的 SCSI host 。每一个探测到的 host 类型的每一个实例都用 scsi_hostlist 列表中的一个 Scsi_Host 数据结构表示。例如一个系统有两个 NCR810 PCI SCSI 控制器,在这个列表中会有两个 Scsi_Host 条目,每一个控制器一个。每一个 Scsi_Host 指向的 Scsi_Host_Template 表示它的设备驱动程序。
现在每一个 SCSI host 都找到了, SCSI 子系统必须找到每一个 host 总线上的所有的 SCSI 设备。 SCSI 设备编号从 0 到 7 ,每一个设备编号或者 SCSI 标识符在它所粘附的 SCSI 总线上都是唯一的。 SCSI 标识符通常用设备上的跳线设置。 SCSI 初始化代码通过向每一个设备发送 TEST_UNIT_READY 命令来查找一个 SCSI 总线上的每一个 SCSI 设备。当一个设备回应,再向它发送一个 ENQUIRY 命令来完成它的判别。这向 Linux 给出 Vendor 的名称和设备的型号和修订号。 SCSI 命令用一个 Scsi_Cmnd 数据结构来表示,这些命令通过调用这个 SCSI host 的 Scsi_Host_Template 数据结构中的设备驱动程序例程传递给设备驱动程序。每一个找到的 SCSI 设备用一个 Scsi_Device 数据结构表示,每一个都指向它的父 Scsi_Host 。所有的 Scsi_Device 数据结构都加到 scsi_devices 列表中。图 8.4 显示了主要的数据结构和其他数据结构的关系。
有四种 SCSI 设备类型:磁盘、磁带、 CD 和通用( generic )。每一种 SCSI 类型都分别向核心登记,有不同的主块设备类型。但是,它们只有在一个或多个给定的 SCSI 设备类型的设备找到的时候才登记自己。每一个 SCSI 类型,例如 SCSI 磁盘,维护它自己的设备表。它用这些表把核心的块操作(文件或 buffer cache )定向到正确的设备驱动程序或 SCSI host 。每一个 SCSI 类型都用一个 Scsi_Type_Template 数据结构表示。它包括这种类型的 SCSI 设备的信息和执行多种任务的例程的地址。 SCSI 子系统使用这些模板调用每一种 SCSI 设备类型的 SCSI 类型例程。换句话说,如果 SCSI 子系统希望粘附一个 SCSI 磁盘设备,它会调用 SCSI 磁盘类型的例程。如果探测到某类型的一个或多个 SCSI 设备,它的 Scsi_Type_Templates 的数据结构就加到了 scsi_devicelist 列表中。
SCSI 子系统初始化的最后阶段是调用每一个登记的 Scsi_Device_Template 的完成函数。对于 SCSI 磁盘类型让所有的 SCSI 磁盘转动起来并记录它们的磁盘尺寸。它也把表示所有 SCSI 磁盘的 gendisk 数据结构增脚的磁盘的链接列表中,如图 8.3 。
Delivering Block Device Requests (传递块设备请求)
一旦 Linux 初始化了 SCSI 子系统,就可以使用 SCSI 设备了。每一个有效的 SCSI 设备类型都在核心中登记自己,所以 Linux 可以把块设备请求定向到它那里。这些请求可能是通过 blk_dev 的 buffer cache 请求或者是通过 blkdevs 的文件操作。拿一个由一个或多个 EXT2 文件系统分区的 SCSI 磁盘驱动器为例,当它的 EXT2 分区安装上的时候核心的缓冲区请求是如何定向到正确的 SCSI 磁盘呢?
每一个向 / 从一个 SCSI 磁盘分区读 / 写一块数据的请求都会在 blk_dev 向量表中这个 SCSI 磁盘的 current_request 列表中加入一个新的 request 数据结构。如果这个 request 列表正在处理,那么 buffer cache 不需要做什么。否则它必须让 SCSI 磁盘子系统处理它的请求队列。系统中的每一个 SCSI 磁盘用一个 Scsi_Disk 数据结构表示。它们保存在 rscsi_disks 向量表中,用 SCSI 磁盘分区的次设备号的一部分作为索引。例如, /dev/sdb1 主设备号 8 ,次设备号 17 ,它的所以是 1 。每一个 Scsi_Disk 的数据结构包括一个指向表示这个设备的 Scsi_Device 数据结构的指针。 Scsi_Device 又指向一个“拥有它”的 Scsi_Host 数据结构。 Buffer cache 中的 request 数据结构转换成为描述需要发送到 SCSI 设备的 SCSI 命令的 Scsi_Cmd 数据结构中,并在表示这个设备的 Scsi_Host 数据结构中排队。一旦适当的数据块读 / 写之后,会由各自的 SCSI 设备驱动程序处理。
8.6 Network Devices (网络设备)
一个网络设备,只要关系到 Linux 的网络子系统,是一个发送和接收数据包的实体。通常是一个物理的设备,例如一个以太网卡。但是一些网络设备是纯软件的,例如 loopback 设备,用于向自己发送数据。每一个网络设备用一个 device 数据结构表示。网络设备驱动程序在核心启动网络初始化的时候向 Linux 登记它控制的设备。 Device 数据结构包括这个设备的信息和允许大量支持的网络协议使用这个设备的服务的函数的地址。这些函数多数和使用这个网络设备传输数据有关。设备使用标准的网络支持机制,向适当的协议层传输接收的数据。传输和接收的所有的网络数据(包 packets )都用 sk_buff 数据结构表示,这是灵活的数据结构,允许网络协议头很容易地增加和删除。网络协议层如何使用网络设备,它们如何使用 sk_buff 数据结构来回传递数据,在网络章(第 10 章)有详细的描述。本章集中在 device 数据结构以及网络设备如何被发现和初始化。
参见 include/linux/netdevice.h
device 数据结构包括网络设备的信息:
Name 不象块和字符设备,它们的设备特殊文件用 mknod 命令创建,网络设备特殊文件在系统的网络设备发现并初始化的时候自然出现。它们的名字是标准的,每一个名字都表示了它的设备类型。同种类型的多个设备从 0 向上依次编号。因此以太网设备编号为 /dev/eth0 、 /dev/eth1 、 /dev/eth2 等等。一些常见的网络设备是:
/dev/ethN 以太网设备
/dev/slN SLIP 设备
/dev/pppN PPP 设备
/dev/lo loopback 设备
Bus Information 这是设备驱动程序控制设备需要的信息。 Irq 是设备使用的中断。 Base address 是设备的控制和状态寄存器在 I/O 内存种的地址。 DMA 通道是这个网络设备使用的 DMA 通道号。所有这些信息在启动时设备初始化的时候设置。
Interface Flags 这些描述了这个网络设备的特性和能力。
IFF_UP 接口 up ,正在运行
IFF_BROADCAST 设备的广播地址有效
IFF_DEBUG 设备的 debug 选项打开
IFF_LOOPBACK 这是一个 loopback 设备
IFF_POINTTOPOINT 这是点对点的连接( SLIP and PPP )
IFF_NOTRAILERS No network trailers
IFF_RUNNING 分配了资源
IFF_NOARP 不支持 ARP 协议
IF_PROMISC 设备在混合( promiscuous )接收模式,它会接收所有的包,不管它们的地址是谁。
IFF_ALLMULTI 接收所有的 IP Multicast 帧
IFF_MULTICAST 可以接收 IP multicast 帧
Protocal Information 每一个设备都描述它可以被网络协议层如何使用:
Mtu 不包括需要增加的链路层的头这个网络能够传输的最大尺寸的包。这个最大值用于协议层例如 IP ,来选择一个合适的包大小进行发送。
Family family 显示了设备可以支持的协议族。所有 Linux 网络设备都支持的 family 是 AF_INET , Internet 地址 family 。
Type 硬件接口类型描述了这个网络设备连接的介质。 Linux 网络设备支持多种介质类型。包括 Ethernet 、 X.25 , Token Ring 、 Slip 、 PPP 和 Apple Localtalk 。
Addresses device 数据结构保存一些和这个网络设备相关的地址,包括 IP 地址
Packet Queue 这是一个 sk_buff 的包队列,等待网络设备进行传输
Support Functions 每一个设备都提供了一组标准的例程,让协议层调用,作为对于设备链路层的接口的一部分。包括设置和帧传输例程,以及增加标准帧头和收集统计信息的例程。这些统计信息可以用 ifcnfig 看到
8.6.1 Initializing Network Devices (初始化网络设备)
网络设备驱动程序象其他 Linux 设备驱动程序一样,可以建立到 Linux 核心中。每一个可能的网络设备都用 dev_base 列表指针指向的网络设备列表中的一个 device 数据结构表示。如果需要设备相关的操作,网络层调用网络设备服务例程(放在 device 数据结构中)其中的一个。但是,初始的时候,每一个 device 数据结构只是放了初始化或者探测例程的地址。
网络驱动程序必须解决两个问题。首先,不是所有建立在 Linux 核心的网络设备驱动程序都会有控制的设备;第二,系统中的以太网设备总是叫做 /dev/eth0 、 /dev/eth1 等等,而不管底层的设备驱动程序是什么。“丢失“网络设备的问题容易解决。在调用每一个网络设备的初始化例程的时候,它返回一个状态,显示它是否定位到了它驱动的控制器的一个实例。如果驱动程序没有找到任何设备,它由 dev_base 指向的 device 列表中的条目就被删除。如果驱动程序可以找到一个设备,它就用这个设备的信息和网络设备驱动程序中的支持函数的地址填充 device 数据结构其余的部分。
第二个问题,就是动态地分配以太网设备到标准的 /dev/ethN 设备特殊文件上,用更优雅的方式解决。 Device 列表中有 8 个标准的条目: eth0 、 eth1 到 eth7 。所有条目的初始化例程都一样。它顺序尝试建立在核心的每一个以太网设备驱动程序,直到找到一个设备。当驱动程序找到它的以太网设备,它就填充它现在拥有的 ethN 的 device 数据结构。这时网络驱动程序也要初始化它控制的物理硬件,并找出它使用的 IRQ 、 DMA 等等。驱动程序可能找到它控制的网络设备的几个实例,在这种情况下,它就占用几个 /dev/ethN 的 device 数据结构。一旦所有的 8 个标准的 /dev/ethN 都分配了,就不会再探测更多的以太网设备。