外围组件互连 (PCI),顾名思义,是一种描述如何以结构化和受控方式将系统外围组件连接在一起的标准。该标准描述了系统组件的电气连接方式以及它们应有的行为方式。本章着眼于 Linux 内核如何初始化系统的 PCI 总线和设备。
图 6.1 是一个基于 PCI 的系统示例的逻辑图。PCI 总线和 PCI-PCI 桥接器是将系统组件连接在一起的粘合剂;CPU 连接到 PCI 总线 0,即主 PCI 总线,视频设备也是如此。一个特殊的 PCI 设备,即 PCI-PCI 桥接器,将主总线连接到辅助 PCI 总线,即 PCI 总线 1。在 PCI 规范的术语中,PCI 总线 1 被描述为 PCI-PCI 桥接器的下游,而 PCI 总线 0 是桥接器的上游。连接到辅助 PCI 总线的是系统的 SCSI 和以太网设备。在物理上,桥接器、辅助 PCI 总线和两个设备都将包含在同一组合 PCI 卡上。系统中的 PCI-ISA 桥接器支持较旧的传统 ISA 设备,该图显示了一个超级 I/O 控制器芯片,该芯片控制键盘、鼠标和软盘。1
CPU 和 PCI 设备需要访问它们之间共享的内存。设备驱动程序使用此内存来控制 PCI 设备并在它们之间传递信息。通常,共享内存包含设备的控制和状态寄存器。这些寄存器用于控制设备和读取其状态。例如,PCI SCSI 设备驱动程序会读取其状态寄存器,以了解 SCSI 设备是否已准备好将信息块写入 SCSI 磁盘。或者,它可能会写入控制寄存器以在设备开启后启动设备运行。
CPU 的系统内存可以用于此共享内存,但如果是这样,那么每次 PCI 设备访问内存时,CPU 都必须停顿,等待 PCI 设备完成。对内存的访问通常一次仅限于一个系统组件。这将减慢系统速度。允许系统的外围设备以不受控制的方式访问主内存也不是一个好主意。这将非常危险;一个流氓设备可能会使系统非常不稳定。
外围设备有自己的内存空间。CPU 可以访问这些空间,但设备对系统内存的访问受到 DMA(直接内存访问)通道的严格控制。ISA 设备可以访问两个地址空间:ISA I/O(输入/输出)和 ISA 内存。PCI 有三个:PCI I/O、PCI 内存和 PCI 配置空间。所有这些地址空间也都可以被 CPU 访问,其中 PCI I/O 和 PCI 内存地址空间由设备驱动程序使用,而 PCI 配置空间由 Linux 内核中的 PCI 初始化代码使用。
Alpha AXP 处理器不自然地访问系统地址空间以外的地址空间。它使用支持芯片组来访问其他地址空间,例如 PCI 配置空间。它使用稀疏地址映射方案,该方案窃取大型虚拟地址空间的一部分并将其映射到 PCI 地址空间。
系统中的每个 PCI 设备,包括 PCI-PCI 桥接器,在 PCI 配置地址空间中的某个位置都有一个配置数据结构。PCI 配置标头允许系统识别和控制设备。标头在 PCI 配置地址空间中的确切位置取决于设备在 PCI 拓扑结构中的位置。例如,插入 PC 主板上的一个 PCI 插槽的 PCI 视频卡,其配置标头将位于一个位置,如果将其插入另一个 PCI 插槽,则其标头将出现在 PCI 配置内存中的另一个位置。这无关紧要,因为无论 PCI 设备和桥接器位于何处,系统都将使用其配置标头中的状态和配置寄存器来查找和配置它们。
通常,系统的设计使得每个 PCI 插槽的 PCI 配置标头都位于与其在板上的插槽相关的偏移量处。因此,例如,板上的第一个插槽的 PCI 配置可能位于偏移量 0 处,第二个插槽的 PCI 配置可能位于偏移量 256 处(所有标头的长度都相同,为 256 字节),依此类推。定义了一个特定于系统的硬件机制,以便 PCI 配置代码可以尝试检查给定 PCI 总线的所有可能的 PCI 配置标头,并通过尝试读取标头中的一个字段(通常是供应商标识字段)并获得某种错误来了解哪些设备存在,哪些设备不存在。该描述将一种可能的错误消息描述为在尝试读取空 PCI 插槽的供应商标识和设备标识字段时返回 0xFFFFFFFF。
图 6.2 显示了 256 字节 PCI 配置标头的布局。它包含以下字段
设备使用这两个地址空间与其在 CPU 上运行的 Linux 内核中的设备驱动程序进行通信。例如,DECchip 21141 快速以太网设备将其内部寄存器映射到 PCI I/O 空间。然后,其 Linux 设备驱动程序读取和写入这些寄存器以控制设备。视频驱动程序通常使用大量的 PCI 内存空间来包含视频信息。
在 PCI 系统设置完成并且使用 PCI 配置标头中的命令字段打开设备对这些地址空间的访问之前,任何人都无法访问它们。应该注意的是,只有 PCI 配置代码读取和写入 PCI 配置地址;Linux 设备驱动程序仅读取和写入 PCI I/O 和 PCI 内存地址。
PCI-PCI 桥接器是特殊的 PCI 设备,可将系统的 PCI 总线粘合在一起。简单系统只有一个 PCI 总线,但单个 PCI 总线可以支持的 PCI 设备数量存在电气限制。使用 PCI-PCI 桥接器添加更多 PCI 总线允许系统支持更多 PCI 设备。这对于高性能服务器尤其重要。当然,Linux 完全支持 PCI-PCI 桥接器的使用。
为了使 CPU 的 PCI 初始化代码能够寻址不在主 PCI 总线上的设备,必须有一种机制,允许桥接器决定是否将配置周期从其主接口传递到其辅助接口。周期只是 PCI 总线上出现的地址。PCI 规范定义了 PCI 配置地址的两种格式;类型 0 和类型 1;这些分别在图 6.3 和图 6.4 中显示。类型 0 PCI 配置周期不包含总线号,所有设备都将其解释为用于此 PCI 总线上的 PCI 配置地址。类型 0 配置周期的位 31:11 被视为设备选择字段。设计系统的一种方法是让每个位选择不同的设备。在这种情况下,位 11 将选择插槽 0 中的 PCI 设备,位 12 将选择插槽 1 中的 PCI 设备,依此类推。另一种方法是将设备的插槽号直接写入位 31:11。系统中使用的机制取决于系统的 PCI 内存控制器。
类型 1 PCI 配置周期包含 PCI 总线号,除了 PCI-PCI 桥接器之外,所有 PCI 设备都忽略这种类型的配置周期。所有看到类型 1 配置周期的 PCI-PCI 桥接器都可以选择将其传递到其下游的 PCI 总线。PCI-PCI 桥接器是忽略类型 1 配置周期还是将其传递到下游 PCI 总线取决于 PCI-PCI 桥接器的配置方式。每个 PCI-PCI 桥接器都有一个主总线接口号和一个辅助总线接口号。主总线接口是最靠近 CPU 的接口,辅助总线接口是最远的接口。每个 PCI-PCI 桥接器还有一个从属总线号,这是桥接器辅助总线接口之外所有桥接 PCI 总线的最大总线号。换句话说,从属总线号是 PCI-PCI 桥接器下游编号最高的 PCI 总线。当 PCI-PCI 桥接器看到类型 1 PCI 配置周期时,它会执行以下操作之一
因此,如果我们想寻址拓扑图 pci-pci-config-eg-4 第 页上的总线 3 上的设备 1,我们必须从 CPU 生成类型 1 配置命令。桥接器1 将此命令不变地传递到总线 1。桥接器2 忽略它,但桥接器3 将其转换为类型 0 配置命令,并将其发送到总线 3,设备 1 在总线 3 上响应它。
在 PCI 配置期间分配总线号是每个操作系统的责任,但无论使用何种编号方案,对于系统中的所有 PCI-PCI 桥接器,以下陈述都必须为真
``位于 PCI-PCI 桥接器后面的所有 PCI 总线都必须位于辅助总线号和从属总线号之间(包括端点值)。''
如果违反此规则,则 PCI-PCI 桥接器将无法正确传递和转换类型 1 PCI 配置周期,并且系统将无法找到和初始化系统中的 PCI 设备。为了实现此编号方案,Linux 以特定顺序配置这些特殊设备。第 pci-pci-bus-numbering 节,第 页,详细描述了 Linux 的 PCI 桥接器和总线编号方案,以及一个工作示例。
Linux 中的 PCI 初始化代码分为三个逻辑部分
当 Linux 内核初始化 PCI 系统时,它会构建数据结构,以镜像系统的真实 PCI 拓扑结构。图 6.5 显示了它将为图 6.1 第 pageref 页中的示例 PCI 系统构建的数据结构的关系。
每个 PCI 设备(包括 PCI-PCI 桥接器)都由一个pci_dev数据结构描述。每个 PCI 总线都由一个pci_bus数据结构描述。结果是一个 PCI 总线的树状结构,每个总线都连接了许多子 PCI 设备。由于 PCI 总线只能通过 PCI-PCI 桥接器访问(主 PCI 总线,总线 0 除外),因此每个pci_bus都包含一个指向 PCI 设备(PCI-PCI 桥接器)的指针,它通过该桥接器访问。该 PCI 设备是 PCI 总线的父 PCI 总线的子设备。
图 6.5 中未显示的是指向系统中所有 PCI 设备的指针,pci_devices。系统中所有 PCI 设备都将其pci_dev数据结构排队到此队列中。Linux 内核使用此队列来快速查找系统中的所有 PCI 设备。
PCI 设备驱动程序实际上根本不是设备驱动程序,而是在系统初始化时调用的操作系统功能。PCI 初始化代码必须扫描系统中的所有 PCI 总线,以查找系统中的所有 PCI 设备(包括 PCI-PCI 桥接器设备)。
它使用 PCI BIOS 代码来了解它正在扫描的当前 PCI 总线中的每个可能的插槽是否被占用。如果 PCI 插槽被占用,它会构建一个pci_dev数据结构,描述设备并链接到已知 PCI 设备列表(由pci_devices).
指向)。PCI 初始化代码首先扫描 PCI 总线 0。它尝试读取每个可能的 PCI 插槽中每个可能的 PCI 设备的供应商标识和设备标识字段。当它找到一个被占用的插槽时,它会构建一个pci_dev数据结构,描述设备。PCI 初始化代码构建的所有pci_dev数据结构(包括所有 PCI-PCI 桥接器)都链接到一个单链表中;pci_devices.
如果找到的 PCI 设备是 PCI-PCI 桥接器,则会构建一个pci_bus数据结构,并链接到pci_bus和pci_dev的树中,由pci_root指向。PCI 初始化代码可以判断 PCI 设备是否为 PCI-PCI 桥接器,因为它具有 0x060400 的类代码。然后,Linux 内核配置刚刚找到的 PCI-PCI 桥接器另一侧(下游)的 PCI 总线。如果找到更多 PCI-PCI 桥接器,也会配置这些桥接器。此过程称为深度优先算法;系统的 PCI 拓扑结构在广度优先搜索之前完全按深度映射。查看图 6.1,第 pageref 页,Linux 将配置 PCI 总线 1 及其以太网和 SCSI 设备,然后再配置 PCI 总线 0 上的视频设备。
当 Linux 搜索下游 PCI 总线时,它还必须配置中间 PCI-PCI 桥接器的辅助总线号和从属总线号。这在下面的第 pci-pci-bus-numbering 节中详细描述。
为了使 PCI-PCI 桥接器能够在其上传递 PCI I/O、PCI 内存或 PCI 配置地址空间读取和写入,它们需要知道以下内容
问题在于,当您希望配置任何给定的 PCI-PCI 桥接器时,您不知道该桥接器的从属总线号。您不知道下游是否还有其他 PCI-PCI 桥接器,即使知道,您也不知道将分配给它们的编号。答案是使用深度优先递归算法,并扫描每个总线以查找任何 PCI-PCI 桥接器,并在找到它们时为其分配编号。当找到每个 PCI-PCI 桥接器并为其辅助总线编号时,为其分配一个临时从属编号 0xFF,并扫描并为其下游的所有 PCI-PCI 桥接器分配编号。这一切看起来都很复杂,但下面的工作示例使此过程更加清晰。
PCI BIOS 功能是一系列跨所有平台的标准例程。例如,它们对于基于 Intel 和 Alpha AXP 的系统都是相同的。它们允许 CPU 控制访问所有 PCI 地址空间。
只有 Linux 内核代码和设备驱动程序可以使用它们。
Alpha AXP 的 PCI 修复代码比 Intel 的(基本上什么都不做)做得更多。
对于基于 Intel 的系统,在启动时运行的系统 BIOS 已经完全配置了 PCI 系统。这使得 Linux 除了映射该配置之外几乎无事可做。对于非基于 Intel 的系统,还需要进行进一步的配置,以
接下来的小节描述了该代码的工作原理。
基地址寄存器有两种基本类型,第一种类型指示设备寄存器必须驻留在哪个地址空间中;PCI I/O 或 PCI 内存空间。这由寄存器的位 0 指示。图 6.10 显示了 PCI 内存和 PCI I/O 的基地址寄存器的两种形式。
为了了解给定的基地址寄存器正在请求多少地址空间,您需要将所有 1 写入寄存器,然后将其读回。设备将在无关地址位中指定零,从而有效地指定所需的地址空间。这种设计意味着所有使用的地址空间都是 2 的幂,并且自然对齐。
例如,当您初始化 DECChip 21142 PCI 快速以太网设备时,它会告诉您它需要 0x100 字节的 PCI I/O 或 PCI 内存空间。初始化代码为其分配空间。分配空间后,可以在这些地址看到 21142 的控制和状态寄存器。
Linux 使用的算法依赖于由 PCI 设备驱动程序构建的总线/设备树描述的每个设备都以升序 PCI I/O 内存顺序分配地址空间。再次使用递归算法来遍历 PCI 初始化代码构建的pci_bus和pci_dev数据结构。从根 PCI 总线(由pci_root指向)开始,BIOS 修复代码
以图 6.1 第 pageref 页中的 PCI 系统为例,PCI 修复代码将按以下方式设置系统
1 例如?