操作系统的一个目的就是对用户隐藏系统硬件设备的特性。例如,虚拟文件系统提供了一个统一的已挂载文件系统视图,而不管底层的物理设备是什么。本章介绍 Linux 内核如何管理系统中的物理设备。
CPU 不是系统中唯一的智能设备,每个物理设备都有自己的硬件控制器。键盘、鼠标和串行端口由 SuperIO 芯片控制,IDE 磁盘由 IDE 控制器控制,SCSI 磁盘由 SCSI 控制器控制,等等。每个硬件控制器都有自己的控制和状态寄存器(CSR),这些寄存器在不同设备之间有所不同。Adaptec 2940 SCSI 控制器的 CSR 与 NCR 810 SCSI 控制器的 CSR 完全不同。CSR 用于启动和停止设备、初始化设备以及诊断设备的任何问题。没有将管理系统中硬件控制器的代码放入每个应用程序中,而是将代码保存在 Linux 内核中。处理或管理硬件控制器的软件称为设备驱动程序。Linux 内核设备驱动程序本质上是一个特权、内存驻留的底层硬件处理例程的共享库。Linux 的设备驱动程序处理它们所管理的设备的特性。
的一个基本特性是它抽象了设备的处理。所有硬件设备看起来都像常规文件;它们可以使用相同的标准系统调用(用于操作文件)打开、关闭、读取和写入。系统中的每个设备都由一个*设备特殊文件*表示,例如,系统中的第一个 IDE 磁盘由/dev/hda表示。对于块(磁盘)和字符设备,这些设备特殊文件由mknod命令创建,它们使用主设备号和次设备号描述设备。网络设备也由设备特殊文件表示,但它们由 Linux 在查找和初始化系统中的网络控制器时创建。由同一设备驱动程序控制的所有设备都有一个公共的主设备号。次设备号用于区分不同的设备及其控制器,例如,主 IDE 磁盘上的每个分区都有不同的次设备号。所以,/dev/hda2主 IDE 磁盘的第二个分区的主设备号为 3,次设备号为 2。Linux 使用主设备号和许多系统表(例如字符设备表)将系统调用(例如,在块设备上挂载文件系统)中传递的设备特殊文件映射到设备的设备驱动程序,chrdevs .
Linux 支持三种类型的硬件设备:字符设备、块设备和网络设备。字符设备直接读取和写入,无需缓冲,例如系统的串行端口/dev/cua0和/dev/cua1。块设备只能以块大小的倍数写入和读取,通常为 512 或 1024 字节。块设备通过缓冲区缓存访问,并且可以随机访问,也就是说,无论块位于设备上的哪个位置,都可以读取或写入任何块。可以通过其设备特殊文件访问块设备,但更常见的是通过文件系统访问块设备。只有块设备才能支持已挂载的文件系统。网络设备通过 BSD 套接字接口和网络章节(第 网络章节)中描述的网络子系统访问。
Linux 内核中有许多不同的设备驱动程序(这是 Linux 的优势之一),但它们都具有一些共同的属性
轮询设备通常意味着经常读取其状态寄存器,直到设备的状态发生变化,表明它已完成请求。由于设备驱动程序是内核的一部分,因此如果驱动程序进行轮询,那将是灾难性的,因为内核中的任何其他内容都不会运行,直到设备完成请求为止。相反,轮询设备驱动程序使用系统计时器让内核稍后调用设备驱动程序中的例程。此计时器例程将检查命令的状态,这正是 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 设备的中断,是在启动时动态分配的。在这种情况下,设备驱动程序必须首先发现它正在控制的设备的中断号(IRQ),然后才能请求拥有该中断。对于 PCI 中断,Linux 支持标准的 PCI BIOS 回调来确定有关系统中的设备的信息,包括它们的 IRQ 号。
如何将中断传递到 CPU 本身取决于体系结构,但在大多数体系结构上,中断以一种特殊模式传递,该模式会阻止系统中发生其他中断。设备驱动程序应在其中断处理例程中尽可能少地执行操作,以便 Linux 内核可以忽略中断并返回到中断前正在执行的操作。由于接收到中断而需要执行大量工作的设备驱动程序可以使用内核的下半部处理程序或任务队列来将例程排队以便稍后调用。
当数据量相当低时,使用中断驱动的设备驱动程序将数据传输到硬件设备或从硬件设备传输数据效果很好。例如,9600 波特的调制解调器大约每毫秒(1/1000 秒)可以传输一个字符。如果中断延迟(硬件设备引发中断和调用设备驱动程序的中断处理例程之间的时间量)较低(例如 2 毫秒),则数据传输的总体系统影响非常低。9600 波特调制解调器数据传输只会占用 CPU 处理时间的 0.002%。对于高速设备(如硬盘控制器或以太网设备),数据传输速率要高得多。SCSI 设备每秒最多可以传输 40 M 字节的信息。
直接内存访问(DMA)的目的是解决这个问题。DMA 控制器允许设备在没有处理器干预的情况下将数据传输到系统内存或从系统内存传输数据。PC 的 ISA DMA 控制器有 8 个 DMA 通道,其中 7 个可供设备驱动程序使用。每个 DMA 通道都与一个 16 位地址寄存器和一个 16 位计数寄存器相关联。为了启动数据传输,设备驱动程序设置 DMA 通道的地址和计数寄存器以及数据传输的方向(读取或写入)。然后它告诉设备,如果它愿意,可以开始 DMA。传输完成后,设备会中断 PC。在传输发生时,CPU 可以自由地执行其他操作。
设备驱动程序在使用 DMA 时必须小心。首先,DMA 控制器对虚拟内存一无所知,它只能访问系统中的物理内存。因此,正在 DMA 到或从 DMA 传输的内存必须是连续的物理内存块。这意味着您不能直接 DMA 到进程的虚拟地址空间中。但是,您可以将进程的物理页面锁定到内存中,防止它们在 DMA 操作期间交换到交换设备。其次,DMA 控制器无法访问整个物理内存。DMA 通道的地址寄存器表示 DMA 地址的前 16 位,接下来的 8 位来自页面寄存器。这意味着 DMA 请求仅限于内存的底部 16 M 字节。
DMA 通道是稀缺资源,只有 7 个,并且它们不能在设备驱动程序之间共享。就像中断一样,设备驱动程序必须能够确定它应该使用哪个 DMA 通道。与中断一样,某些设备具有固定的 DMA 通道。例如,软盘设备始终使用 DMA 通道 2。有时可以通过跳线设置设备的 DMA 通道;许多以太网设备使用这种技术。可以告诉更灵活的设备(通过它们的 CSR)使用哪个 DMA 通道,在这种情况下,设备驱动程序可以简单地选择一个空闲的 DMA 通道来使用。
Linux 使用一个dma_chan数据结构(每个 DMA 通道一个)的向量来跟踪 DMA 通道的使用情况。dma_chan数据结构只包含两个字段,一个是指向描述 DMA 通道所有者的字符串的指针,另一个是指示 DMA 通道是否已分配的标志。正是这个dma_chan数据结构的向量在您cat /proc/dma.
Linux 提供了内核内存分配和释放例程,设备驱动程序就是使用这些例程。内核内存以 2 的幂的块大小进行分配。例如,128 或 512 字节,即使设备驱动程序请求的更少。设备驱动程序请求的字节数会向上舍入到下一个块大小边界。这使得内核内存释放更容易,因为较小的空闲块可以重新组合成更大的块。
当请求内核内存时,Linux 可能需要做很多额外的工作。如果空闲内存量较低,则可能需要丢弃物理页面或将其写入交换设备。通常,Linux 会暂停请求者,将进程放入等待队列,直到有足够的物理内存为止。并非所有设备驱动程序(或实际上是 Linux 内核代码)都希望发生这种情况,因此如果内核内存分配例程无法立即分配内存,则可以请求它们失败。如果设备驱动程序希望使用 DMA 从已分配的内存进行读取或写入,它还可以指定该内存是可 DMA 的。这样,需要了解什么是此系统可 DMA 内存的是 Linux 内核,而不是设备驱动程序。
Linux 非常动态,每次 Linux 内核启动时,它都可能遇到不同的物理设备,因此需要不同的设备驱动程序。Linux 允许您通过其配置脚本在内核构建时包含设备驱动程序。当这些驱动程序在启动时初始化时,它们可能无法发现任何要控制的硬件。其他驱动程序可以在需要时作为内核模块加载。为了应对设备驱动程序的这种动态特性,设备驱动程序会在初始化时向内核注册自己。Linux 维护注册的设备驱动程序表,作为其与它们的接口的一部分。这些表包括指向例程和信息的指针,这些例程和信息支持与该类设备的接口。
字符设备是 Linux 最简单的设备,可以像文件一样访问,应用程序使用标准系统调用来打开它们、从中读取数据、向其中写入数据并关闭它们,就像该设备是一个文件一样。即使该设备是 PPP 守护程序用来将 Linux 系统连接到网络的调制解调器,也是如此。当字符设备初始化时,它的设备驱动程序通过将一个条目添加到chrdevsvector ofdevice_struct数据结构中,向 Linux 内核注册自己。设备的major设备标识符(例如,对于tty设备,是 4)用作此 vector 的索引。设备的major设备标识符是固定的。
vector 中的每个条目是一个chrdevsvector, adevice_struct数据结构包含两个元素;指向已注册设备驱动程序名称的指针和指向文件操作块的指针。此文件操作块本身是设备字符设备驱动程序中例程的地址,每个例程都处理特定的文件操作,例如打开、读取、写入和关闭。/proc/devices对于字符设备的内容是从chrdevsvector
当打开表示字符设备的字符特殊文件(例如/dev/cua0)时,内核必须进行设置,以便调用正确的字符设备驱动程序的文件操作例程。就像一个普通文件或目录一样,每个设备特殊文件都由一个 VFS inode 表示。字符特殊文件(实际上是所有设备特殊文件)的 VFS inode 包含设备的主要和次要标识符。此 VFS inode 由底层文件系统创建,例如EXT2,来自在查找设备特殊文件名时实际文件系统中的信息。
每个 VFS inode 都有一组与之关联的文件操作,这些操作因 inode 表示的文件系统对象而异。每当创建一个表示字符特殊文件的 VFS inode 时,它的文件操作都会设置为默认的字符设备操作
。这只有一个文件操作,即打开文件操作。当应用程序打开字符特殊文件时,通用打开文件操作使用设备的主要标识符作为索引到chrdevsvector 中来检索此特定设备的文件操作块。它还设置file数据结构,描述这个字符特殊文件,使其文件操作指针指向设备驱动程序的文件操作。此后,所有应用程序的文件操作都将映射到对字符设备文件操作集的调用。
每个块设备驱动程序都必须提供一个与缓冲区缓存的接口,以及正常的文件操作接口。每个块设备驱动程序都会在blk_devvector
的blk_dev_struct数据结构中填充其条目。这个 vector 的索引同样是设备的主要编号。blk_dev_struct数据结构由一个请求例程的地址和一个指向request数据结构列表的指针组成,每个数据结构代表缓冲区缓存发出的驱动程序读取或写入数据块的请求。
每当缓冲区缓存希望读取数据块或将数据块写入已注册的设备时,它都会将一个request数据结构添加到它的blk_dev_struct中。图 8.2 显示每个请求都有一个或多个buffer_head数据结构的指针,每个数据结构都是读取或写入数据块的请求。buffer_head结构体被锁定(被缓冲区缓存锁定),并且可能有一个进程在等待完成此缓冲区上的块操作。每个request结构体都是从一个静态列表分配的,也就是all_requests列表。如果请求被添加到一个空的请求列表中,那么驱动程序的请求函数会被调用来启动处理请求队列。否则驱动程序会简单的处理每一个request在请求列表中的请求。
一旦设备驱动程序完成了一个请求,它必须从buffer_head结构体中移除每一个request结构体,把它们标记为最新的并且解锁它们。解锁buffer_head将会唤醒任何正在休眠等待块操作完成的进程。一个例子是当一个文件名正在被解析并且EXT2文件系统必须从持有文件系统的块设备中读取包含下一个EXT2目录项的数据块时。这个进程休眠在buffer_head中,这个request数据结构会被标记为可用,所以它可以被用在另一个块请求中。
磁盘驱动器提供了一种更永久的数据存储方法,将其保存在旋转的磁盘盘片上。为了写入数据,一个微小的磁头将盘片表面上的微小粒子磁化。数据由磁头读取,磁头可以检测到特定微小粒子是否被磁化。
磁盘驱动器由一个或多个盘片组成,每个盘片由精细抛光的玻璃或陶瓷复合材料制成,并涂有精细的氧化铁层。盘片连接到中心轴,并以恒定速度旋转,该速度可能在 3000 到 10,000 RPM 之间变化,具体取决于型号。将其与仅以 360 RPM 旋转的软盘进行比较。磁盘的读/写头负责读取和写入数据,并且每个盘片有一对,每个表面一个头。读/写头不会物理接触盘片的表面,而是漂浮在非常薄(1000 万分之一英寸)的气垫上。读/写头由执行器在盘片表面上移动。所有的读/写头都连接在一起,它们一起在盘片的表面上移动。
盘片的每个表面都分为称为磁道的狭窄的同心圆。磁道 0 是最外面的磁道,编号最高的磁道是最靠近中心轴的磁道。柱面是所有具有相同编号的磁道的集合。因此,来自磁盘中每个盘片的每一侧的所有第 5 个磁道都称为柱面 5。由于柱面的数量与磁道的数量相同,因此您经常看到以柱面描述的磁盘几何形状。每个磁道被划分为扇区。扇区是可以写入或读取到硬盘的最小数据单元,它也是磁盘的块大小。常见的扇区大小为 512 字节,扇区大小是在磁盘格式化时设置的,通常是在制造磁盘时设置的。
磁盘通常由其几何形状描述,即柱面数、磁头数和扇区数。例如,在启动时,Linux 将我的一个 IDE 磁盘描述为
hdb: Conner Peripherals 540MB - CFS540A, 516MB w/64kB Cache, CHS=1050/16/63
这意味着它有 1050 个柱面(磁道),16 个磁头(8 个盘片)和每个磁道 63 个扇区。如果一个扇区(或块)的大小为 512 字节,那么磁盘的存储容量为 529200 字节。这与磁盘标称的 516 Mbytes 容量不符,因为一些扇区用于磁盘分区信息。一些磁盘会自动找到坏扇区并重新索引磁盘以绕过它们。
硬盘可以进一步细分为分区。分区是分配用于特定目的的一大组扇区。对磁盘进行分区允许磁盘被多个操作系统使用,或用于多个目的。许多 Linux 系统都有一个包含三个分区的磁盘;一个包含 DOS 文件系统,另一个包含 EXT2 文件系统,第三个用于交换分区。硬盘的分区由分区表描述;每个条目都描述了分区在磁头、扇区和柱面编号方面的起始和结束位置。对于 DOS 格式化的磁盘,那些由fdisk格式化的磁盘,有四个主磁盘分区。分区表中的并非所有四个条目都必须使用。支持以下三种类型的分区fdisk:主分区、扩展分区和逻辑分区。扩展分区根本不是真正的分区,它们包含任意数量的逻辑分区。发明扩展分区和逻辑分区是为了绕过四个主分区的限制。以下是来自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 个柱面。第二个分区(交换分区)从下一个柱面 (478) 开始,并延伸到磁盘的最内层柱面。
在初始化期间,Linux 会映射系统中硬盘的拓扑结构。它会找出有多少个硬盘以及它们的类型。此外,Linux 还会发现各个磁盘是如何分区的。这一切都由一个gendisk数据结构的列表表示,该列表由gendisk_head列表指针指向。当每个磁盘子系统(例如 IDE)初始化时,它会生成gendisk表示它找到的磁盘的数据结构。它在注册其文件操作并将条目添加到blk_dev数据结构的同时执行此操作。每个gendisk数据结构都有一个唯一的主设备号,这些主设备号与块特殊设备的主设备号匹配。例如,SCSI 磁盘子系统创建一个gendisk条目 (``sd''),其主设备号为 8,这是所有 SCSI 磁盘设备的主设备号。图 8.3 显示了两个gendisk条目,第一个用于 SCSI 磁盘子系统,第二个用于 IDE 磁盘控制器。这是ide0,主 IDE 控制器。
尽管磁盘子系统在初始化期间构建了gendisk条目,但 Linux 仅在分区检查期间使用它们。相反,每个磁盘子系统都维护自己的数据结构,这允许它将设备特殊主设备号和次设备号映射到物理磁盘内的分区。每当通过缓冲区缓存或文件操作读取或写入块设备时,内核都会使用其块特殊设备文件(例如/dev/sda2)中找到的主设备号,将操作定向到相应的设备。由各个设备驱动程序或子系统将次设备号映射到实际的物理设备。
当今 Linux 系统中最常用的磁盘是集成磁盘电子 (IDE) 磁盘。IDE 是一种磁盘接口,而不是像 SCSI 这样的 I/O 总线。每个 IDE 控制器最多可以支持两个磁盘,一个是主盘,另一个是从盘。主盘和从盘功能通常由磁盘上的跳线设置。系统中的第一个 IDE 控制器称为主 IDE 控制器,下一个是辅助控制器,依此类推。IDE 可以管理大约 3.3 Mbytes/秒 的数据传输速率,用于从磁盘或向磁盘传输数据,并且最大的 IDE 磁盘大小为 538Mbytes。扩展 IDE (EIDE) 将磁盘大小提高到最大 8.6 Gbytes,并将数据传输速率提高到 16.6 Mbytes/秒。IDE 和 EIDE 磁盘比 SCSI 磁盘便宜,并且大多数现代 PC 包含一个或多个板载 IDE 控制器。
Linux 按照发现其控制器的顺序命名 IDE 磁盘。主 IDE 控制器上的主盘是/dev/hda,从盘是/dev/hdb. /dev/hdc是辅助 IDE 控制器上的主盘。IDE 子系统向 Linux 内核注册 IDE 控制器,而不是磁盘。主 IDE 控制器的主要标识符是 3,辅助 IDE 控制器的主要标识符是 22。这意味着如果系统有两个 IDE 控制器,则 IDE 子系统将在blk_dev和blkdevs向量中的索引 3 和 22 处有条目。IDE 磁盘的块特殊文件反映了这种编号,磁盘/dev/hda和/dev/hdb,都连接到主 IDE 控制器,主标识符为 3。对这些块特殊文件执行的 IDE 子系统操作的任何文件或缓冲区缓存操作都将被定向到 IDE 子系统,因为内核使用主标识符作为索引。发出请求时,由 IDE 子系统来确定请求用于哪个 IDE 磁盘。为此,IDE 子系统使用来自设备特殊标识符的次设备号,其中包含允许它将请求定向到正确磁盘的正确分区的信息。 的设备标识符/dev/hdb,主 IDE 控制器上的从 IDE 驱动器是 (3,64)。该磁盘的第一个分区 (/dev/hdb1)的设备标识符是 (3,65)。
IDE 磁盘在 IBM PC 的历史中已经存在了很长时间。在这段时间里,这些设备的接口发生了变化。这使得 IDE 子系统的初始化比乍一看要复杂得多。
Linux 可以支持的最大 IDE 控制器数量为 4。每个控制器都由ide_hwif_t数据结构在ide_hwifs向量中表示。每个ide_hwif_t数据结构包含两个ide_drive_t数据结构,每个可能的受支持的主 IDE 驱动器和从 IDE 驱动器一个。在 IDE 子系统的初始化期间,Linux 首先查看系统的 CMOS 存储器中是否存在有关磁盘的信息。这是电池支持的存储器,当 PC 关闭电源时不会丢失其内容。此 CMOS 存储器实际上位于系统的实时时钟设备中,无论您的 PC 是否开启,它始终运行。CMOS 存储器位置由系统的 BIOS 设置,并告诉 Linux 找到了哪些 IDE 控制器和驱动器。Linux 从 BIOS 中检索找到的磁盘的几何形状,并使用该信息来设置ide_hwif_t该驱动器的数据结构。更现代的 PC 使用 PCI 芯片组,例如 Intel 的 82430 VX 芯片组,其中包括 PCI EIDE 控制器。IDE 子系统使用 PCI BIOS 回调来定位系统中的 PCI (E)IDE 控制器。然后,它为存在的那些芯片组调用 PCI 特定的查询例程。
一旦发现每个 IDE 接口或控制器,它的ide_hwif_t会被设置为反映控制器和连接的磁盘。在操作期间,IDE 驱动程序将命令写入 I/O 内存空间中存在的 IDE 命令寄存器。主 IDE 控制器的控制和状态寄存器的默认 I/O 地址是 0x1F0 - 0x1F7。这些地址是在 IBM PC 的早期通过约定设置的。IDE 驱动程序向 Linux 块缓冲区缓存和 VFS 注册每个控制器,分别将其添加到blk_dev和blkdevs向量。IDE 驱动器还将请求控制适当的中断。同样,这些中断是按照约定设置为 14(对于主 IDE 控制器)和 15(对于辅助 IDE 控制器)。但是,与所有 IDE 详细信息一样,可以通过内核的命令行选项覆盖它们。IDE 驱动程序还将gendisk条目添加到启动期间发现的gendisk列表中,每个找到的 IDE 控制器一个。稍后,该列表将用于发现所有在启动时找到的硬盘的分区表。分区检查代码理解 IDE 控制器可以控制两个 IDE 磁盘。
SCSI(小型计算机系统接口)总线是一种高效的对等数据总线,每条总线最多支持八个设备,包括一个或多个主机。每个设备都必须具有唯一的标识符,这通常由磁盘上的跳线设置。数据可以在总线上的任何两个设备之间同步或异步传输,并且通过 32 位宽的数据传输,最高可以达到 40 Mbytes/秒。SCSI 总线在设备之间传输数据和状态信息,发起者和目标之间的单个事务最多可能涉及八个不同的阶段。您可以从总线的五个信号中得知 SCSI 总线的当前阶段。这八个阶段是
Linux SCSI 子系统由两个基本元素组成,每个元素都由数据结构表示
初始化 SCSI 子系统非常复杂,反映了 SCSI 总线及其设备的动态特性。 Linux 在启动时初始化 SCSI 子系统; 它找到系统中的 SCSI 控制器(称为 SCSI 主机),然后探测每个 SCSI 总线,找到其所有设备。 然后,它初始化这些设备,并通过正常的 文件和缓冲区缓存块设备操作使它们可供 Linux 内核的其余部分使用。 此初始化分四个阶段完成
首先,Linux 找出在内核构建时构建到内核中的哪些 SCSI 主机适配器或控制器具有硬件来控制。 每个内置 SCSI 主机都有一个Scsi_Host_Template中的条目builtin_scsi_hosts向量。Scsi_Host_Template数据结构包含指向例程的指针,这些例程执行 SCSI 主机特定的操作,例如检测哪些 SCSI 设备连接到此 SCSI 主机。 当 SCSI 子系统配置自身时,这些例程会被调用,它们是支持此主机类型的 SCSI 设备驱动程序的一部分。 每个检测到的 SCSI 主机(连接了真实的 SCSI 设备的主机)都将其Scsi_Host_Template数据结构添加到scsi_hosts活动 SCSI 主机列表。 检测到的主机类型的每个实例都由一个Scsi_Host数据结构保存在scsi_hostlist列表中。 例如,具有两个 NCR810 PCI SCSI 控制器的系统将在列表中有两个Scsi_Host条目,每个控制器一个。 每个Scsi_Host指向Scsi_Host_Template表示其设备驱动程序。
现在每个 SCSI 主机都已被发现,SCSI 子系统必须找出哪些 SCSI 设备连接到每个主机的总线。 SCSI 设备的编号介于 0 到 7 之间(含 0 和 7),每个设备的编号或 SCSI 标识符在连接到的 SCSI 总线上是唯一的。 SCSI 标识符通常由设备上的跳线设置。 SCSI 初始化代码通过向每个 SCSI 设备发送 TEST_UNIT_READY 命令来查找 SCSI 总线上的每个 SCSI 设备。 当设备响应时,通过向其发送 ENQUIRY 命令来读取其标识。 这为 Linux 提供了供应商名称以及设备的型号和修订名称。 SCSI 命令由一个Scsi_Cmnd数据结构表示,并且通过调用设备驱动程序中的例程,将这些数据结构传递给此 SCSI 主机的设备驱动程序Scsi_Host_Template数据结构。 找到的每个 SCSI 设备都由一个Scsi_Device数据结构表示,每个数据结构都指向其父Scsi_Host。 所有Scsi_Device数据结构都添加到scsi_devices列表中。 图 8.4 显示了主要数据结构如何相互关联。
有四种 SCSI 设备类型:磁盘、磁带、CD 和通用。 这些 SCSI 类型中的每一种都作为不同的主块设备类型单独在内核中注册。 但是,只有在找到给定 SCSI 设备类型的一个或多个设备时,它们才会注册自身。 每种 SCSI 类型(例如 SCSI 磁盘)都维护自己的设备表。 它使用这些表将内核块操作(文件或缓冲区缓存)定向到正确的设备驱动程序或 SCSI 主机。 每种 SCSI 类型都由一个Scsi_Device_Template数据结构表示。 它包含有关这种 SCSI 设备类型的信息以及执行各种任务的例程的地址。 SCSI 子系统使用这些模板为每种类型的 SCSI 设备调用 SCSI 类型例程。 换句话说,如果 SCSI 子系统希望附加一个 SCSI 磁盘设备,它将调用 SCSI 磁盘类型附加例程。Scsi_Type_Template数据结构都添加到scsi_devicelist列表,如果已检测到该类型的一个或多个 SCSI 设备。
SCSI 子系统初始化的最后阶段是为每个已注册的Scsi_Device_Template调用完成函数。 对于 SCSI 磁盘类型,这将启动所有找到的 SCSI 磁盘,然后记录它们的磁盘几何形状。 它还将表示所有 SCSI 磁盘的gendisk数据结构添加到图 8.3 中所示的磁盘链接列表。
每次读取或写入数据块到 SCSI 磁盘分区或从中写入数据块的请求都会导致一个新的request结构被添加到 SCSI 磁盘的current_request中列表blk_dev向量。 如果正在处理request列表,则缓冲区缓存不需要执行任何其他操作; 否则,它必须轻推 SCSI 磁盘子系统以处理其请求队列。 系统中的每个 SCSI 磁盘都由一个Scsi_Disk数据结构表示。 这些保存在rscsi_disks向量中,该向量使用 SCSI 磁盘分区的次设备号的一部分进行索引。 例如,/dev/sdb1的主设备号为 8,次设备号为 17; 这会生成索引 1。 每个Scsi_Disk数据结构包含指向Scsi_Device数据结构的指针,该数据结构表示此设备。 反过来,它指向Scsi_Host数据结构,该数据结构“拥有”它。request来自缓冲区缓存的数据结构被转换为Scsi_Cmd描述需要发送到 SCSI 设备的 SCSI 命令的结构,并将其排队到Scsi_Host表示此设备的结构上。 一旦读取或写入了适当的数据块,这些将由各个 SCSI 设备驱动程序处理。
就 Linux 的网络子系统而言,网络设备是发送和接收数据包的实体。 这通常是物理设备,例如以太网卡。 但是,某些网络设备仅是软件,例如用于将数据发送给自己的环回设备。 每个网络设备由一个device数据结构表示。 网络设备驱动程序在内核启动时在网络初始化期间向 Linux 注册它们控制的设备。device数据结构包含有关设备的信息以及允许各种受支持的网络协议使用设备服务的功能的地址。 这些功能主要与使用网络设备传输数据有关。 该设备使用标准网络支持机制将接收到的数据传递到适当的协议层。 所有发送和接收的网络数据(数据包)都由sk_buff数据结构表示,这些数据结构是灵活的,允许轻松添加和删除网络协议标头。 网络协议层如何使用网络设备,如何使用sk_buff数据结构来回传递数据,将在“网络”章节(第 networks-chapter 章)中详细描述。 本章重点介绍device数据结构以及如何发现和初始化网络设备。
结构体device数据结构包含有关网络设备的信息
/dev/ethN | 以太网设备 |
/dev/slN | SLIP 设备 |
/dev/pppN | PPP 设备 |
/dev/lo | 环回设备 |
IFF_UP | 接口已启动并正在运行, |
IFF_BROADCAST | 广播地址在device有效 |
IFF_DEBUG | 设备调试已启用 |
IFF_LOOPBACK | 这是一个环回设备 |
IFF_POINTTOPOINT | 这是点对点链路 (SLIP 和 PPP) |
IFF_NOTRAILERS | 无网络拖尾 (network trailers) |
IFF_RUNNING | 资源已分配 |
IFF_NOARP | 不支持 ARP 协议 |
IFF_PROMISC | 设备处于混杂接收模式,它将接收 |
所有数据包,无论它们的地址指向谁 | |
IFF_ALLMULTI | 接收所有 IP 组播帧 |
IFF_MULTICAST | 可以接收 IP 组播帧 |
像其他 Linux 设备驱动程序一样,网络设备驱动程序可以构建到 Linux 内核中。 每个潜在的网络设备都由device数据结构表示,该结构位于由dev_base列表指针指向的网络设备列表中。 网络层调用许多网络设备服务例程之一,这些例程的地址保存在device数据结构中,如果它们需要执行特定于设备的工作。 但最初,每个device数据结构仅保存初始化或探测例程的地址。
网络设备驱动程序需要解决两个问题。 首先,并非所有构建到 Linux 内核中的网络设备驱动程序都有设备可以控制。 其次,系统中的以太网设备始终称为/dev/eth0, /dev/eth1等等,无论它们的基础设备驱动程序是什么。 “丢失”网络设备的问题很容易解决。 当调用每个网络设备的初始化例程时,它会返回一个状态,指示它是否找到了它正在驱动的控制器的实例。 如果驱动程序找不到任何设备,则从device由dev_base指向的列表中删除其条目。 如果驱动程序找到一个设备,它会填写device数据结构的其余部分,其中包含有关设备的信息以及网络设备驱动程序中支持函数的地址。
第二个问题,即将以太网设备动态分配给标准/dev/ethN设备特殊文件,则以更优雅的方式解决。 设备列表中有八个标准条目; 一个用于eth0, eth1等等到eth7. 所有这些的初始化例程都是相同的,它依次尝试构建到内核中的每个以太网设备驱动程序,直到找到一个设备。 当驱动程序找到其以太网设备时,它会填写ethN device数据结构,现在它拥有该结构。 也是在这个时候,网络设备驱动程序初始化它正在控制的物理硬件,并确定它正在使用的 IRQ、DMA 通道(如果有)等等。 一个驱动程序可能会找到它正在控制的网络设备的多个实例,在这种情况下,它将接管几个/dev/ethN device数据结构。 一旦分配了所有八个标准/dev/ethN,将不再探测更多的以太网设备。