大家好,我是蟹老板~
PCI 的全称是外围组件互连。
它不只是主板上几个插槽的名字,而是一套完整规范,规定设备如何连接、如何寻址、如何报告身份,以及如何与处理器和其他设备交换数据。
这整套过程,就是 PCI 初始化。
CPU 直接连接 PCI 总线 0,视频设备也挂在这条总线上。另一个特殊设备 ——PCI-PCI 桥—— 把总线 0 延伸成了 PCI 总线 1。
站在桥接器的位置看,靠近 CPU 的一侧称为上游,远离 CPU 的一侧称为下游。总线 0 位于桥的上游,总线 1 位于桥的下游。
SCSI 控制器和以太网控制器都连接在总线 1 上。现实中,这座桥、下游总线和两个控制器甚至可能被集成在同一张 PCI 扩展卡中。
图中还有一座 PCI-ISA 桥。它负责照顾那些更老的 ISA 设备,例如键盘、鼠标和软盘控制器。换句话说,一个系统里可能同时生活着几代硬件,而 Linux 必须让它们和平相处。
一、Linux 面对的不是一条总线,而是一棵树
最简单的 PCI 系统确实可能只有一条总线,但实际机器通常没有这么简单。
一条 PCI 总线能够承载的设备数量受到电气特性的限制。当设备越来越多时,就需要通过 PCI-PCI 桥扩展出新的总线。新的总线上还可以继续连接桥,于是整个系统逐渐形成 树状结构:
- • PCI-PCI 桥既是当前总线上的设备,也是通往下一层总线的入口。
这也是理解 Linux PCI 初始化的关键:内核不是简单扫描一排插槽,而是在遍历一棵硬件拓扑树。
但是桥接器的存在给系统带来了两个问题。
CPU 如何访问桥后面的设备?
桥应该转发哪些地址,而哪些地址应该被挡住?
为了回答这两个问题,需要先看看 PCI 世界里到底有哪些地址空间。
二、PCI 设备为什么要拥有自己的地址空间
设备驱动程序想控制一块硬件,必须有地方可以读写。
例如,网卡会提供控制寄存器和状态寄存器。驱动程序通过写控制寄存器启动收发功能,通过读状态寄存器判断数据是否已经处理完成。SCSI 控制器也是类似的:驱动程序可能先检查设备是否就绪,然后再命令它把数据写入磁盘。
一种看似直接的做法,是把这些寄存器放进普通系统内存,让 CPU 和设备共享。
但这样做并不理想。
如果设备每次访问共享内存时都要与 CPU 争用总线,处理器可能不得不停下来等待。更麻烦的是,若允许外围设备毫无限制地读写主内存,一个故障设备甚至恶意设备就可能破坏内核数据,让系统瞬间失控。
于是,PCI 把问题拆开处理。
设备拥有自己的地址区域,CPU 可以访问这些区域;而设备若要访问系统主内存,则通常通过受控制的 DMA,也就是直接内存访问机制完成。
在经典 PCI 模型中,主要涉及三类地址空间:
2.1 PCI I/O 地址空间
这块空间常用于放置设备寄存器。
驱动程序通过 I/O 读写操作访问它。例如,一些以太网控制器会把自己的控制寄存器映射到 PCI I/O 空间中。
2.2 PCI 内存地址空间
这也是设备与 CPU 通信的重要区域,只不过它按照内存访问方式映射。
显卡往往需要较大的地址范围来放置帧缓冲区,许多高性能设备也更倾向于使用内存映射 I/O。
2.3 PCI 配置地址空间
这块空间不负责传输正常业务数据,而是负责回答一组更基础的问题:
因此,PCI 配置空间主要由内核初始化代码访问。普通设备驱动程序通常不需要像初始化代码那样遍历配置空间,而是在设备已经被识别和分配资源后,使用 PCI I/O 或 PCI 内存空间控制设备。
ISA 设备的世界稍微简单一些,它们通常只有 ISA I/O 和 ISA 内存两类地址空间。
某些处理器架构不能像访问普通内存那样直接访问 PCI 配置空间。例如 Alpha AXP 需要借助系统支持芯片组完成转换。常见办法是在处理器巨大的虚拟地址空间中划出一些特殊区域,通过稀疏映射把它们对应到 PCI I/O、PCI 内存或配置空间。
对驱动程序来说,这些架构差异最终会被内核封装起来;但在底层,处理器实际上可能通过完全不同的硬件路径抵达同一个 PCI 寄存器。
三、配置空间就是设备的“身份证档案”
系统中的每个 PCI 设备都拥有一块配置数据,PCI-PCI 桥也不例外。
这块配置数据通常以配置头开始。经典 PCI 配置头占用 256 字节,其中保存了设备的身份、能力、资源需求和当前状态。
配置头在配置空间中的位置,与设备所在的总线、插槽和功能号有关。
一块显卡插在主板第一个 PCI 插槽时,会出现在一个配置地址;把它换到另一个插槽,它的配置地址也会跟着变化。不过,Linux 并不需要预先知道每张卡插在哪里。它只要按照系统提供的配置访问机制,逐个尝试可能存在的设备位置即可。
经典实现中,每个设备功能通常占用 256 字节配置空间。系统可以依次探测插槽和功能,并读取厂商标识与设备标识。
如果某个位置没有设备,读取结果常常是 0xFFFFFFFF。这就像敲门后没有任何回应,内核可以继续检查下一个位置。
配置头中最重要的内容包括以下几类。
厂商标识和设备标识
厂商标识用来说明设备由谁制造,设备标识则说明它具体是哪一款产品。
例如:
- • Digital 21141 快速以太网控制器的设备标识是
0x0009。
厂商标识和设备标识组合起来,能够相当精确地确定设备型号。驱动程序也可以据此判断自己是否支持这块硬件。
类代码
类代码不强调厂商,而是描述设备用途。
显卡、网络控制器、SCSI 控制器和桥接器都有各自的标准类别。例如,SCSI 控制器的类代码可以是 0x0100,PCI-PCI 桥常通过类代码 0x060400 识别。
类代码非常重要,因为即使内核不认识某个具体型号,也能先判断它大概属于哪种设备。
状态寄存器
状态字段记录设备当前报告的各种状态和能力。每一位的含义由 PCI 标准规定。
命令寄存器
命令字段像一组总开关。
只有在其中打开相应位后,设备才可以响应 PCI I/O 地址、PCI 内存地址或执行总线主控操作。也就是说,内核即使已经为设备安排好地址,如果没有打开命令寄存器中的权限,设备仍然不会正式响应这些访问。
基地址寄存器
基地址寄存器通常简称 BAR。
它们既用于告诉系统设备需要哪种地址空间,也用于保存系统最终分配给设备的地址。
BAR 可以表达:
初始化阶段,Linux 会通过一套特殊的写入和读取操作,询问每个 BAR 到底需要多大空间。
中断引脚
经典 PCI 卡最多可以使用四个中断引脚,分别标记为 INTA、INTB、INTC 和 INTD。
配置头中的中断引脚字段说明设备使用哪一个物理引脚。这个选择通常由硬件连线决定,每次启动时不会随意变化。
中断线
中断线字段记录的是系统分配后的中断路由信息。
这个数字本身未必直接代表设备驱动程序眼中的硬件 IRQ,但它可以作为初始化代码、中断子系统和驱动程序之间传递路由结果的媒介。最终目标只有一个:设备发出中断时,Linux 必须把它送到正确的驱动程序处理函数。
四、设备还没分配地址前,不能急着“开门营业”
PCI 设备不会在系统一上电后就自动响应所有 I/O 和内存请求。
内核需要先完成以下工作:
在 PCI 初始化完成之前,驱动程序不能假设设备的寄存器已经出现在某个可用地址。
配置空间和普通设备空间的职责也要分清:
- • 设备驱动程序主要访问已经分配好的 PCI I/O 和 PCI 内存区域。
驱动程序看到的是一块可以正常使用的设备资源,至于它位于主总线上还是穿过了两三座桥,通常不需要驱动程序亲自处理。
这正是桥接机制努力实现的效果——把复杂拓扑藏在内核和硬件后面。
五、PCI-ISA 桥:给老设备留一扇门
早期 PC 上大量设备按照 ISA 规范工作,而且许多寄存器地址早在 IBM PC 时代就固定下来了。
例如,即使是一台昂贵的 Alpha AXP 工作站,它的软盘控制器仍可能出现在与早期 PC 相同的 ISA I/O 地址位置。
硬件更新换代可以很快,为了兼容性。许多系统会同时提供 PCI 和 ISA 设备。
PCI-ISA 桥的工作,就是把针对某些低地址区域的 PCI 访问转换成 ISA 访问。
通常,PCI I/O 和 PCI 内存空间中的低地址部分会保留给传统 ISA 外设。当处理器访问这些地址时,PCI-ISA 桥识别出请求属于 ISA 范围,于是把它转换后送往 ISA 总线。
这样一来,处理器和操作系统仍然可以通过统一的系统路径访问旧设备,而不必把整个系统退回到 ISA 时代。
随着纯 PCI 系统逐渐普及,这类兼容桥的重要性会下降。但在经典 PC 架构中,它是新旧设备能够共存的关键角色。
六、PCI-PCI 桥不只是延长线,它还会过滤流量
PCI-PCI 桥连接两条 PCI 总线,但它并不会把所有请求无脑复制到下游。
假设总线 0 上有显卡,总线 1 上有网卡和 SCSI 控制器。桥接器只应该把属于网卡和 SCSI 控制器的访问转发到总线 1。
显卡的地址明明位于桥上游,如果也被广播到下游,不但浪费带宽,还可能造成地址冲突或设备误响应。
为此,每座 PCI-PCI 桥都包含若干地址窗口:
窗口由基地址和限制地址定义。
只有落在窗口范围内的请求,桥才会向下游转发;范围之外的访问直接被忽略。
例如,某座桥的 PCI 内存窗口被设置为 0x400000 到 0x402000,那么访问这段区域时,请求可以穿过桥;访问显卡所在的 0x200000 区域时,桥就不会理会。
完成配置后,普通驱动程序几乎感觉不到桥的存在。它只需访问设备资源,硬件会根据各座桥的窗口自动把请求送到正确总线。
不过,要达到这种“看起来什么都没有发生”的效果,Linux 初始化阶段必须非常准确地算出每座桥后面有哪些设备,以及它们总共需要多少地址空间。
七、桥后面的设备,怎样才能被配置访问找到
普通 I/O 和内存访问可以通过桥接窗口转发,但系统在刚启动时还有一个更早的问题:桥后面的设备尚未被识别,内核如何读取它们的配置空间?
PCI 规范定义了两种经典配置周期:
Type 0:访问当前总线上的设备
Type 0 配置周期不携带总线编号。
它的意思近似于:“我找的设备就在当前这条总线上。”
配置地址中的一部分用于选择设备。具体如何选择,取决于主机桥或 PCI 控制器的设计。
有些系统采用独热方式:某一位对应某个插槽。例如第 11 位选择插槽 0,第 12 位选择插槽 1。
另一些系统则直接把设备编号编码进相应字段。
无论硬件采用哪种方案,Type 0 的共同特点都是:它只面向当前总线上的设备,不负责跨越桥接器寻找更远的目标。
Type 1:访问其他总线上的设备
Type 1 配置周期中包含总线编号。
普通 PCI 设备通常忽略 Type 1 周期,PCI-PCI 桥则会检查其中的总线号,决定是否应该把请求送往下游。
为了做出这个决定,每座桥至少需要知道三个编号:
- • 从属总线号:从这座桥继续向下,能够抵达的最大总线号。
收到 Type 1 配置周期后,桥大致按照以下规则处理:
- 1. 如果目标总线号不在次级总线号与从属总线号之间,忽略请求;
- 2. 如果目标总线号正好等于次级总线号,把 Type 1 转换成 Type 0,并发送到下游总线;
- 3. 如果目标总线号大于次级总线号、但不超过从属总线号,保持 Type 1 格式继续向下转发。
假设内核要访问总线 3 上的设备 1。
CPU 首先发出目标总线为 3 的 Type 1 配置周期。沿途某座桥发现总线 3 不在自己的管理范围内,就会忽略它;另一座桥发现总线 3 位于自己的下游范围内,于是继续转发;当请求抵达次级总线号恰好为 3 的桥时,它会被转换成 Type 0 周期,最终由总线 3 上的设备 1 响应。
整条链路成立的前提,是所有桥的总线编号必须满足一条核心规则:
一座 PCI-PCI 桥后面的所有总线编号,都必须落在该桥的次级总线号和从属总线号之间。
一旦编号不满足这条规则,桥就无法正确判断配置请求该不该转发。结果往往不是“慢一点”,而是内核彻底看不到下游设备。
总线编号不是为了让输出看起来整齐,而是 PCI 配置路由能够工作的基础。
八、Linux 会先画地图,再让驱动程序上场
经典 Linux PCI 初始化过程可以分成三个逻辑层次。
第一层:扫描设备并建立拓扑
内核从根总线开始,探测每个可能的设备位置。
发现设备后,内核读取它的配置头,记录厂商、型号、类代码、BAR、中断等信息。如果发现的是 PCI-PCI 桥,则继续扫描桥后的总线。
这一阶段负责回答:“系统里到底有什么?”
第二层:PCI BIOS 或平台访问层
不同架构访问 PCI 配置空间的硬件方式并不相同。
Intel PC 可能依赖 BIOS 已经提供的配置结果和标准服务;Alpha AXP 没有完全相同的 PC BIOS 环境,但内核会提供等价的平台代码。
上层 PCI 扫描逻辑不希望关心每种芯片组的具体寄存器,只需要一层统一接口来完成配置空间读写。
这一层回答:“怎样在当前机器上读到那些配置数据?”
第三层:平台修正与资源安排
某些平台的固件已经把设备资源安排妥当,Linux 主要负责读取和复现现有配置。
另一些平台则需要内核亲自完成更多工作,例如:
这一阶段回答:“发现设备之后,怎样让它真正可用?”
在早期实现中,这些平台相关操作常被称为 PCI fixup,也就是 PCI 修正或补救代码。
九、内核用什么数据结构保存整张 PCI 地图
Linux 扫描 PCI 系统时,会在内存中建立一份与硬件拓扑相对应的数据结构。
每个 PCI 设备由一个 pci_dev 结构描述。
这个“设备”不仅包括网卡、显卡和存储控制器,也包括 PCI-PCI 桥。因为从当前总线看,桥本身同样占用一个设备位置,也拥有配置头和 BAR 等信息。
每条 PCI 总线则由一个 pci_bus 结构描述。
pci_bus 会记录:
除根总线外,每条 PCI 总线都是通过某个 PCI-PCI 桥抵达的。一个子 pci_bus 会指向对应的桥设备,而这个桥设备又属于上一级总线。
这样一来,pci_bus 和 pci_dev 自然组成一棵树。
早期 Linux 还会维护一个指向所有 PCI 设备的全局链表,例如 pci_devices。树状结构适合表达父子关系,全局设备链表则适合快速遍历所有设备。
两种视图各有用途:
- • 想让某段代码依次查看所有 PCI 设备,遍历全局链表更方便。
十、扫描从总线 0 开始,但不会只停留在表面
PCI 初始化代码并不是传统意义上服务某个硬件的设备驱动程序。它更像一个开机阶段的硬件调查员。
扫描通常从 PCI 总线 0 开始。
内核依次尝试读取每个可能设备位置上的厂商标识和设备标识。如果返回值表明没有设备,就继续下一个位置;如果读取成功,就创建一个 pci_dev,并把配置头中的关键信息保存下来。
随后,内核检查类代码。
如果这是普通设备,先记录下来,后续再分配资源和匹配驱动。
如果类代码是 0x060400,说明它是一座 PCI-PCI 桥。内核便创建对应的子 pci_bus,给下游总线安排编号,并立即进入下游继续扫描。
这种顺着一条分支走到底,再回来处理其他分支的方法,就是 深度优先扫描。
假设总线 0 上同时连接着一块显卡和一座桥,而桥后的总线 1 上连接着网卡和 SCSI 控制器,那么扫描过程可能是:
- 3. 识别总线 1 上的网卡和 SCSI 控制器;
- 5. 当前分支结束后,再回到总线 0 处理后面的显卡。
这并不是说显卡不重要,而是深度优先遍历更适合同时解决桥接器的总线编号问题。
十一、最麻烦的一步:给桥后面的总线编号
为了让桥转发 Type 1 配置周期,每座桥都要提前知道主总线号、次级总线号和从属总线号。
问题是,第一次发现一座桥时,Linux 只知道它的上游总线,却不知道它后面到底有多少层。
举例来说,内核刚在总线 0 上发现桥 1,此时可以把桥后的第一条总线命名为总线 1。但桥 1 后面是否还有桥?最远会延伸到总线 2、总线 4,还是总线 20?在扫描完成前,内核无法知道。
经典解决办法是:
- 3. 允许大量目标总线号的 Type 1 周期先通过;
- 5. 确定实际最大总线号后,再把
0xFF 改成准确值。
临时使用 0xFF,相当于先告诉桥:“暂时认为你能通往所有更远的总线,等我调查结束再缩小范围。”
下面用四步展示这个过程。
第一步:发现桥 1
桥 1 位于总线 0 上。
Linux 为它设置:
此时,目标总线号为 1 或更大的 Type 1 配置周期都可以通过桥 1。
目标为总线 1 的周期会被转换成 Type 0,直接用于扫描总线 1;目标为更大总线号的周期则保持 Type 1,继续为后续桥接器扫描做准备。
第二步:在总线 1 上发现桥 2
Linux 进入总线 1 后发现桥 2,并把它后面的总线编号设为 2。
继续扫描后,发现桥 2 的下游没有其他 PCI-PCI 桥。所以呢 桥 2 能够到达的最大总线号就是 2。
它最终得到:
这个分支就此结束。
第三步:回到总线 1,发现桥 3
扫描代码回到总线 1,继续检查剩余设备,并发现桥 3。
桥 3 后面的新总线被编号为 3,暂时设置:
此时,总线 1、2、3 都已经能够通过适当的桥接路径接收配置访问。
第四步:在总线 3 上发现桥 4
Linux 进入总线 3,发现桥 4,并把桥后的总线编号设为 4。
桥 4 后面没有更多桥,所以它的次级和从属总线号都是 4。
扫描返回桥 3。现在内核知道,桥 3 的下游最远可以抵达总线 4,于是把桥 3 的从属总线号更新为 4。
随后继续返回桥 1。桥 1 能够抵达总线 1、2、3 和 4,它的最终从属总线号也被设置为 4。
最终结果如下:
这样,任何指向总线 1~4 的配置访问,都能被沿途桥接器正确判断、转发和转换。
十二、发现设备不代表资源已经够用
拓扑建立完成后,Linux 还要解决另一个现实问题:每个设备到底需要多少 PCI I/O 或 PCI 内存空间?
设备不会通过一句人类可读的话告诉内核:“请给我 64KB。”
它通过 BAR 回答这个问题。
用全 1 询问 BAR 的大小
经典 PCI 设备的 BAR 支持一种很巧妙的探测方法。
内核先保存 BAR 原值,然后向 BAR 写入全 1,再把它读回来。
设备不会简单返回刚刚写入的全部 1,而是会把自己不参与地址译码的低位清零。内核根据返回值中的位掩码,就能推导出该 BAR 所需的空间大小。
BAR 最低位还可以帮助判断资源类型:
- • 最低位表示这是 I/O BAR 时,设备请求 PCI I/O 空间;
- • 最低位表示这是内存 BAR 时,设备请求 PCI 内存空间。
假设某个 BAR 探测后表明需要 0x100 字节,那么它不仅需要连续的 0x100 字节,起始地址通常也要按照 0x100 的边界自然对齐。
这套设计使单个 BAR 的大小通常表现为 2 的幂,并且能够通过掩码快速计算。
例如,DECChip 21142 快速以太网控制器可能报告自己需要 0x100 字节的 I/O 或内存区域。Linux 找到合适位置后,把起始地址写回 BAR。此后,该地址范围就对应设备的控制寄存器和状态寄存器。
探测 BAR 时必须格外小心。因为写入全 1 只是询问大小,完成后还要恢复或重新写入正式地址,不能让设备一直保留探测值。
十三、资源分配像俄罗斯套娃:设备地址必须装进桥窗口
PCI I/O 和 PCI 内存空间并不是无限的。
Linux 不但要给每个设备分配地址,还要保证 桥后设备的地址全部落在上游桥的窗口中。
假设一座桥后面有三块设备,它们分别需要三段内存。Linux 不能把这三段地址随意撒在整个 PCI 地址空间里,否则桥就需要建立多个零散窗口,而经典桥接器通常只提供有限的连续范围。
更理想的安排是:
此外,桥窗口还有自己的对齐要求。
通常情况下:
- • PCI I/O 窗口通常按 4KB 边界对齐;
资源分配不仅是“找空位”,还要同时满足设备 BAR 对齐、桥窗口对齐和父子包含关系。
这就像整理行李:
- • 每座桥要求下游所有物品都装进这只箱子的连续空间;
Linux 通常借助已经建立好的 pci_bus 和 pci_dev 树递归完成这项工作。
十四、经典资源分配算法怎样运行
从根总线开始,平台修正代码维护当前可用的 PCI I/O 和 PCI 内存基地址。
处理一条总线时,大致会执行以下步骤。
1. 先满足桥窗口的对齐要求
进入某条总线前,先把当前 I/O 基地址对齐到 4KB 边界,把当前内存基地址对齐到 1MB 边界。
这两个位置可以被看作当前子树资源区间的起点。
2. 为当前总线上的普通设备分配资源
内核检查设备的 BAR 需求,为每个 BAR 寻找满足大小和自然对齐要求的地址。
分配完成后,更新全局 I/O 或内存基地址,使其指向下一段空闲位置。
3. 打开设备访问能力
地址写入 BAR 后,内核可以设置配置头中的命令寄存器,允许设备响应 PCI I/O 或 PCI 内存访问。
设备这时才算真正拥有了可使用的“门牌号”。
4. 递归处理下游总线
如果当前总线上存在 PCI-PCI 桥,内核进入桥后的总线,继续为下游设备分配资源。
递归返回时,全局基地址已经移动到了整个下游子树资源之后。
5. 计算桥窗口终点
下游设备全部处理完后,再次按照 4KB 或 1MB 对齐当前基地址。
进入下游前记录的起点,加上递归结束后的终点,就构成这座桥需要覆盖的 I/O 和内存窗口。
6. 配置并启用桥接
Linux 把窗口起止地址写入 PCI-PCI 桥的配置寄存器,然后打开桥接器的 I/O 和内存转发功能。
此后,落在窗口中的上游请求会被送往下游,窗口之外的请求则不会通过。
这种算法的关键优点,是先完整分配一个下游分支,再确定覆盖整个分支的桥窗口。它与前面的深度优先总线扫描思路十分一致。
十五、用一个具体例子看懂地址是怎样分下去的
继续使用开头的系统:
- • 总线 0 通过一座 PCI-PCI 桥连接总线 1;
- • 系统还需要为 ISA 兼容访问保留低地址区域。
下面是一种经典分配过程。
起点:避开 ISA 保留区域
系统把可分配的 PCI I/O 基地址设为:
0x4000
把可分配的 PCI 内存基地址设为:
0x100000
低于这些位置的地址可以继续留给 PCI-ISA 桥转换成传统 ISA 访问。
不过,设备资源还必须按照自身大小自然对齐,真正使用的起始地址可能继续向上调整。
视频设备:先占用较大的内存区域
假设视频设备请求 0x200000 字节 PCI 内存。
当前内存基地址是 0x100000,但 0x200000 大小的资源通常应按照 0x200000 对齐,起始位置调整到:
0x200000
视频设备获得:
0x200000` ~ `0x3FFFFF
分配后,下一段可用 PCI 内存基地址变为:
0x400000
PCI I/O 基地址仍然是:
0x4000
穿过桥,处理下游设备
接下来进入 PCI-PCI 桥后的总线 1。
因为当前 I/O 基地址 0x4000 已经位于 4KB 边界,内存基地址 0x400000 也位于 1MB 边界,所以可以直接把它们作为下游窗口的起点。
假设以太网设备的若干资源合计占用约 0xB0 字节 I/O 空间和 0xB0 字节内存空间。
它的 I/O 资源从:
0x4000
开始分配,分配后 I/O 游标移动到:
0x40B0
它的内存资源从:
0x400000
开始分配,分配后内存游标移动到:
0x4000B0
实际处理单个 BAR 时,仍然需要分别满足每个 BAR 的大小和对齐要求。
SCSI 设备:先对齐,再分配
假设 SCSI 控制器请求 0x1000 字节 PCI 内存。
当前内存游标是 0x4000B0,并未位于 0x1000 边界,需要向上对齐到:
0x401000
SCSI 设备获得:
0x401000` ~ `0x401FFF
分配完成后,内存游标移动到:
0x402000
I/O 游标仍是:
0x40B0
回到桥上,生成转发窗口
总线 1 上的设备已经分配完成。
桥下游实际使用的 I/O 范围从 0x4000 开始,到 0x40B0 附近结束;内存范围从 0x400000 开始,到 0x402000 附近结束。
Linux 根据桥寄存器的粒度和对齐要求,对窗口终点进行适当上调,然后把结果写入桥。
概念上,这座桥的窗口覆盖:
- • PCI I/O:从
0x4000 开始,包含下游网卡所需区域; - • PCI 内存:从
0x400000 开始,覆盖以太网设备和 SCSI 设备。
这样,访问视频设备的 0x200000 地址区间不会通过桥,而访问 0x400000 或 0x401000 一带的请求则会被转发到总线 1。
桥不需要知道访问最终属于网卡还是 SCSI 控制器。它只负责把属于下游地址窗口的请求送过去,具体由下游总线上的设备继续完成地址匹配。
十六、为什么不同平台上的工作量不一样
在经典 Intel PC 上,系统 BIOS 往往已经在 Linux 启动前完成了相当一部分 PCI 配置。
它可能已经:
Linux 启动后,可以读取这些结果并建立自己的设备模型。只要固件配置合理,就没有必要把所有内容推倒重来。
Alpha AXP 等非传统 PC 平台的情况可能不同。
平台固件不一定提供相同的 BIOS 服务,或者只完成了部分配置。Linux 的平台代码需要承担更多责任,包括:
所以,所谓 PCI BIOS 层并不一定意味着机器里真的存在一套与 PC 完全相同的 BIOS。更准确地说,它代表一组统一的 PCI 配置访问和管理接口。
对上层扫描代码而言,无论底层是 Intel BIOS、Alpha 支持芯片组还是其他平台机制,最终都应该能够完成相同的基本操作:
设备驱动程序也因此不必为每种处理器平台重新理解整套 PCI 配置机制。