news 2026/4/18 14:19:29

Linux PCI 总线驱动:初始化、设备扫描、资源管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux PCI 总线驱动:初始化、设备扫描、资源管理

1. 简介

我们可以把 Linux PCI 子系统理解成两件事:

  1. 把设备找出来:把整条 PCI/PCIe 链路上有什么设备、挂在哪一层桥下面都摸清楚,形成一棵“总线树”。
  2. 把地址分好:给每个设备分配它需要的 I/O 地址和内存地址(也就是我们常说的 BAR),并把这些地址放到内核的“资源树”里统一管理,避免冲突。

这篇文章的目标是把主链路理清:

  • 系统在启动时做了哪些步骤
  • 资源(window / BAR / bus number)是怎么从设备树一路流转到最终分配结果的
  • 遇到资源冲突/设备不工作时,应该先看哪里

文中会在描述关键动作时顺便点出对应的函数名,方便需要时再回源码定位。

2. PCI 总线初始化

2.1 平台驱动入口:把 PCI 主桥“准备好”

这一步可以理解成:把“PCI 主桥(host bridge)”这块硬件的基础信息准备好,让后续“扫设备、分资源”能顺利进行。

这里抓住 3 件关键事情即可:

  1. 准备一份“可用资源清单”

    • 这份清单来自设备树ranges/bus-range,内容是:
      • 这条 PCI 总线对外能提供哪些 I/O window / 内存 window
      • 这条总线允许使用哪些 bus number 范围
    • 这份清单之后会挂到 host bridge 上(后续会“搬运”到 root bus)。
  2. 准备“配置空间访问方式”

    • 扫设备之前,内核必须能读写 PCI 配置空间(例如 vendor/device ID、BAR 寄存器都在配置空间里)。
    • 在很多 ARM/SoC 平台上,这一层就是 ECAM:把一段 MMIO 区域映射成“配置空间窗口”,后续用内存读写访问配置寄存器。
  3. 确定后续资源策略:只沿用固件,还是允许重分配

    • 如果固件已经把 BAR 都配好且希望完全沿用,就进入“只认领、不重排”的模式。
    • 如果固件没配好/有冲突/希望内核重新做一遍分配,就允许重分配。

这些准备动作主要发生在平台驱动的pci_host_common_probe()中:它会分配主桥对象(devm_pci_alloc_host_bridge())、解析设备树窗口(devm_of_pci_bridge_init())、建立 ECAM 配置空间访问(gen_pci_init()/pci_ecam_create()),并根据设备树选择是否进入“只探测不重分配(PCI_PROBE_ONLY)”(读取由of_pci_check_probe_only()控制)。

2.2 解析设备树:把 ranges 变成“可用窗口清单”

这一步是:把设备树里描述的“地址窗口”翻译成内核能理解的数据结构,并且先做一次“占位/防冲突”。

可以把设备树ranges看成一张“地址翻译表”:

  • 设备侧(PCI 总线侧)看到的地址是什么(PCI bus address)
  • CPU 侧真实的物理地址是什么(CPU physical address)
  • 这段窗口多大、属于 I/O 还是内存

内核解析后,会得到两类结果:

  1. 一组 window(I/O window / MEM window):后面给 BAR 分配地址时的“候选池”。
  2. 一段 bus-range:告诉内核“这条总线最多可以编号到多少”。

同时,内核会把这些 window先放进全局资源树里占住位置,避免多个 host bridge 把同一段物理地址当成自己的窗口。

这一步对应的实现通常是:devm_of_pci_bridge_init()触发解析流程,窗口与 bus-range 的细节解析由devm_of_pci_get_host_bridge_resources()完成,而把窗口“先占位”到全局资源树的动作由devm_request_pci_bus_resources()完成。

2.3 PCI 核心对象:数据结构关系

  1. 资源(resource):一段“地址范围”。可以是内存、I/O 端口,也可以是 bus number 范围。
  2. 窗口(window):host bridge 或桥设备提供的一段“可分配池”,后面所有 BAR 都要从窗口里分配。
  3. 总线(bus):设备的“容器”,一条 bus 上挂着多个设备,也有自己的可用窗口。
  4. 设备(dev):最终使用资源的人,它的 BAR/ROM/桥窗口都会以“资源”的形式记录下来。

一句话串起来:先有窗口(能分配的池),再有需求(设备要多少),最后把需求放进窗口里(分配/认领)。

2.4 关键结构体字段速查

名称意义常见字段
资源(resource)一段地址范围(内存/I/O/bus number)start/end/flags
资源列表节点(resource_entry)“窗口清单”里的一个条目res(资源本体)、offset(地址翻译差值)
主桥(host bridge)这条 PCI 根总线的“入口”和“总窗口提供者”windowsops/sysdata
总线(bus)一层拓扑节点,挂设备也挂窗口resourcesbusn_res
设备(dev)最终需要 BAR 的对象resource[](BAR/窗口结果)

对应结构体:struct resourcestruct resource_entrystruct pci_host_bridgestruct pci_busstruct pci_dev

2.5 设备树 demo:用ranges/bus-range理解 window 与 offset

下面这个设备树 demo 只用来“建立直觉”:ranges 描述的是“PCI 侧地址”和“CPU 物理地址”之间的对应关系

不需要记住每个 cell 的细节,只要理解:

  • 它最终会生成若干段I/O window / 内存 window
  • 这些 window 会变成“后续给 BAR 分配地址的候选池”
pcie@10000000 { compatible = "pci-host-ecam-generic"; device_type = "pci"; /* ECAM 配置空间:用于访问 PCI 配置空间 */ reg = <0x0 0x10000000 0x0 0x01000000>; /* 16MB */ /* 根总线 bus-range:决定本主桥可用的总线号范围 */ bus-range = <0x00 0xff>; #address-cells = <3>; #size-cells = <2>; /* * ranges:描述 "PCI bus address -> CPU 物理地址" 的 window * Linux 解析后生成 struct resource (CPU 地址区间) 并计算 resource_entry->offset */ ranges = /* IO space: PCI 0x00000000..0x000fffff -> CPU 0x3f000000..0x3f0fffff */ <0x01000000 0x0 0x00000000 0x0 0x3f000000 0x0 0x00100000> /* MEM space: PCI 0x00000000..0x0fffffff -> CPU 0x40000000..0x4fffffff */ <0x02000000 0x0 0x00000000 0x0 0x40000000 0x0 0x10000000>; };
  • offset:如果某个 window 的 CPU 物理起始地址是0x40000000,而 PCI 侧起始地址是0x0,那 offset 就是0x40000000
  • 日志打印:内核打印 root bus resource 时,常会同时打印 CPU 区间和 bus address 区间,二者差值就是 offset。

计算方式:

  1. offset 计算
    • offset = cpu_addr - pci_addr
  2. 把 CPU 侧资源反推出 PCI 侧地址
    • pci_start = res.start - offset
    • pci_end = res.end - offset

offset 的计算发生在解析窗口时:devm_of_pci_get_host_bridge_resources()会把 ranges 翻译成资源条目,并通过pci_add_resource_offset()记录 CPU 地址与 PCI 地址之间的差值。

3. PCI 设备扫描

3.1 扫描入口:先找设备、再处理资源、最后交给驱动

这一阶段发生的事情非常直观:先把设备都找出来,再决定资源是“沿用”还是“重新分配”,最后把设备交给驱动框架。

可以把它拆成 3 步理解:

  1. 建“总线树”:从 root bus 往下扫描,把所有设备和桥都挂到内核的拓扑结构上。
  2. 处理资源
    • 如果系统处于“只沿用固件”的模式:把固件已经配好的 BAR/窗口登记到资源树里(不改配置空间)。
    • 如果系统允许重分配:计算桥需要的窗口大小,然后为每个设备分配 BAR,并把结果写回配置空间。
  3. 让驱动开始工作:把pci_dev注册到 driver model,触发驱动匹配与probe()

这三步的入口在pci_host_probe():它先调用pci_scan_root_bus_bridge()完成 root bus 注册与扫描;如果处于“只探测不重分配(PCI_PROBE_ONLY)”就通过pci_bus_claim_resources()认领资源;如果允许内核重分配,则依次执行pci_bus_size_bridges()pci_bus_assign_resources();最后用pci_bus_add_devices()把设备加入驱动框架。

3.2 root bus 注册:把“窗口清单”挂到根总线上

这一阶段的目的可以一句话概括:把“设备树给的窗口清单”搬到 root bus 名下,让后续扫描/分配都以 root bus 为起点。

完成后,会得到 3 个非常关键的“结果”:

  1. root bus 被创建并注册:可以在 sysfs 中看到对应的 bus。
  2. root bus 拥有自己的 window 列表:后续 BAR 分配时,会从这个列表里找可用地址。
  3. root bus 的 bus number 范围被登记:避免同一个 domain 内 bus number 冲突。

还有一点:为了减少碎片,内核会尽量把相邻且属性一致的窗口合并成更大的窗口(这对后续分配成功率有帮助)。

这里对应的实现路径是:pci_register_host_bridge()创建并注册 root bus,并通过pci_bus_add_resource()把 IO/MEM 窗口挂到bus->resources,通过pci_bus_insert_busn_res()登记 bus number 范围。

3.3 扫描:从 bus -> device -> function

扫描阶段最重要的产出是两件事:

  1. 一棵完整的拓扑树:谁挂在谁下面、哪些是桥、哪些是普通设备。
  2. 每个设备“想要什么资源”:比如某个 BAR 需要多大的内存空间、是不是 64 位、是不是 prefetch。

扫描方法可以粗略理解为:在每条 bus 上按 slot/function 去探测设备是否存在;如果探测到的是桥,就会创建子 bus 并递归向下。

实现上,扫描的递归入口是pci_scan_child_bus();当发现桥设备时,会通过pci_alloc_child_bus()创建子总线对象并继续向下。

3.4 配置空间访问:读写设备信息的“唯一入口”

扫描与资源读取都离不开一件事:能访问 PCI 配置空间

在 ECAM 模式下,配置空间其实就是一段 MMIO:内核先把它映射出来,后续所有“读 vendor/device、读写 BAR”都通过这段映射完成。

为了把“硬件访问方法”说清楚,可以把 ECAM 访问理解为一次确定性的地址计算(以每个 function 4KB 配置空间为例):

输入:cfg_base(ECAM 基址),bus, dev, fn, reg 配置空间访问地址: cfg_addr = cfg_base + (bus << 20) + (dev << 15) + (fn << 12) + reg 含义: - 每个 bus 占 1MB - 每个 device 占 32KB - 每个 function 占 4KB

不同平台可能不是 ECAM,而是其他pci_ops实现(例如 x86 的 legacy I/O port 机制),但对上层来说都抽象成pci_read_config_*()/pci_write_config_*()

这里需要记住:配置空间访问方式由 host bridge 决定,不同平台只是实现不同,但上层扫描/分配流程基本一致。

驱动或 PCI core 调用pci_read_config_*()/pci_write_config_*()时,最终都会走到 host bridge 提供的配置空间访问实现(例如 ECAM)。

3.5 驱动什么时候会 probe:扫描结束之后

当扫描与资源处理结束后,内核会把这些“已经准备好的 PCI 设备”交给统一的驱动框架:

  • 设备会出现在 sysfs 中
  • 内核会去匹配对应的驱动
  • 匹配成功后,驱动的probe()才会被调用

把设备正式交给驱动框架的动作发生在pci_bus_add_devices()

4. 资源管理

4.1 根窗口(window):从设备树一路传到“可分配池”

资源管理里最重要的概念是“窗口(window)”:

  • 窗口是“池子”:后续所有设备 BAR 都要从这个池子里分配地址。
  • 窗口来自 host bridge(以及中间的桥):根窗口一般来自设备树ranges

资源从上到下的流转:

  1. 设备树给出 root window(I/O、内存、bus-range)。
  2. 内核先把这些 window 记录在 host bridge 上。
  3. root bus 创建时,把 window “搬运”到 root bus 的资源列表里,作为后续分配入口。

offset 是读日志时非常实用的“线索”:它表示“同一段窗口在 CPU 侧和 PCI 侧看起来各是什么地址”。很多平台出问题时,只要 offset 不合理,后续 BAR 就必然出问题。

窗口解析来自devm_of_pci_get_host_bridge_resources(),而窗口在 root bus 上的落点由pci_register_host_bridge()完成,并通过pci_bus_add_resource()进入bus->resources

4.1.1 谁保存“窗口”,谁保存“结果”(对象关系)
+---------------------------------------------------+ | 主桥对象:保存“根窗口清单”和“配置空间访问方式” | | (struct pci_host_bridge) | |---------------------------------------------------| | windows: 根窗口清单(IO/MEM/bus-range) | | dma_ranges: inbound 窗口清单 | | ops/sysdata: 配置空间访问上下文(ECAM 等) | | busnr: 根总线号 | | bus: 指向 root bus | +---------------------------+-----------------------+ | | 注册 root bus(会搬运窗口) | (pci_register_host_bridge) v +---------------------------------------------------+ | 根总线对象:后续“扫描/分配”的起点 | | (struct pci_bus, root) | |---------------------------------------------------| | resources: 可用窗口列表(从 windows 搬运而来) | | busn_res: 可用 bus number 范围 | | devices: 本层发现的设备列表 | +---------------------+-----------------------------+ | | 扫描会把设备挂上来 v +---------------------------------------------------+ | 设备对象:记录“我需要什么/我被分到了什么” | | (struct pci_dev) | |---------------------------------------------------| | resource[]: BAR/ROM/桥窗口(最终结果都在这里) | | subordinate: 如果是桥,指向下游 child bus | +---------------------+-----------------------------+ | | 桥连接出的下游总线 v +---------------------------------------------------+ | 子总线对象:拥有自己的窗口与设备列表 | | (struct pci_bus, child) | |---------------------------------------------------| | resources/busn_res/devices | +---------------------------------------------------+ 补充说明: 1) host_bridge.windows 里每个节点是 resource_entry -> resource 2) pci_register_host_bridge() 会把 windows 中的 IO/MEM 挂到 bus->resources 3) 设备扫描阶段填充 pci_dev.resource[](BAR/桥窗口),分配阶段把它们“落到” bus 的 window 中
4.1.2 窗口地址翻译(offset 的意义)

术语对照:bridge->windowsstruct resource_entryresource_entry->offset

设备树 ranges 的一条记录(概念): PCI bus address: [ pci_addr ................ pci_addr + size - 1 ] CPU phys addr : [ cpu_addr ................ cpu_addr + size - 1 ] 内核会把它翻译成一条“窗口记录”: - 这段窗口在 CPU 侧的范围: [ cpu_addr ........ cpu_addr + size - 1 ] - 这段窗口在 PCI 侧的范围: [ pci_addr ........ pci_addr + size - 1 ] - 两者的差值(offset): cpu_addr - pci_addr CPU 地址 = PCI bus address + offset PCI bus address = CPU 地址 - offset 这也是为什么内核打印 root bus resource 时,经常会带一段 bus address: root bus resource [CPU-start..CPU-end] (bus address [CPU-start-offset .. CPU-end-offset])
4.1.3 资源树长什么样(谁挂在谁下面)
Linux 全局资源树(抽象,重点看“谁在谁下面”): 1) 内存资源树(iomem_resource) iomem_resource +-- [RAM/Reserved/...] (省略) +-- [PCI Host Window #1] <--- devm_request_pci_bus_resources() 申请 window | +-- [PCI Bus 00 Window] <--- claim/assign 阶段把下游资源插入到 window 下 | +-- [Dev 00:01.0 BAR0] | +-- [Dev 00:02.0 BAR2] | +-- [Bridge 00:1c.0 MEM Window] | +-- [Dev 01:00.0 BAR0] | +-- ... +-- [PCI Host Window #2] 2) I/O 端口资源树(ioport_resource) ioport_resource +-- [PCI Host IO Window] +-- [Dev xx:yy.z I/O BAR] 3) bus number 资源(IORESOURCE_BUS) per-domain busn resource (get_pci_domain_busn_res()) +-- [root bus busn_res: 00-ff] +-- [bridge child busn_res: 01-1f] +-- ... 要点: 1) 设备树解析阶段:先把“根窗口”占住,避免窗口之间互相踩。 2) 资源处理阶段:再把“设备 BAR/桥窗口”作为子节点挂进去,形成最终的资源树。

4.2 bus number 资源:为什么需要单独管理

很多时候只关注“内存/I/O 地址”,但实际上bus number 也是资源,而且冲突会导致非常诡异的问题(比如扫描不到设备、桥下面的设备编号不稳定)。

可以把它理解为:

  • root bus 会先拿到一个“可用编号范围”(例如00-ff
  • 后续每过一个桥,就会从父范围里切一段给子总线
  • 这样每条 bus 都知道自己的编号边界,避免互相覆盖

实现上,bus number 范围会通过pci_bus_insert_busn_res()插入资源树;如果固件没给足范围,扫描结束后会用pci_bus_update_busn_res_end()更新边界。

4.3 设备 BAR 解析:把“我需要多少空间”变成资源需求

BAR 解析的目标是:弄清楚“这个设备想要多大空间、是什么类型”,并把它变成一个“资源需求”。

可以用 5 个动作理解它(不需要读每个寄存器细节):

  1. 先读一次 BAR:看看现在的地址长什么样。
  2. 用硬件机制反推大小:把 BAR 临时写成全 1,再读回来,就能知道哪些位是“地址位”,从而算出这个 BAR 的 decode size。
  3. 判断类型:它是 I/O 端口还是内存?是否 64 位?是否 prefetch?
  4. 把“需求”记录下来:用struct resource保存 size、类型等信息。
  5. 做一次地址转换校验:如果平台的“PCI 地址 <-> CPU 物理地址”翻译对不上,就把这个 BAR 标记为“需要重新分配”。

这些动作主要由__pci_read_base()完成。

这里补齐一个在实际系统里非常常见、也最容易影响分配结果的组合:MEM64 + Prefetchable(下面简称MEM64 PREF)。

  • 在配置空间里怎么识别 MEM64 PREF

PCI 规范里,内存 BAR 的低位既包含地址,也包含类型位:

内存 BAR(Memory BAR)低位含义(概念表达): bit0: 0 表示 Memory BAR bit2:1: 内存类型(00=32-bit, 10=64-bit) bit3: 1 表示 Prefetchable 因此: - MEM64:BAR 类型位 == 64-bit - PREF:prefetchable 位 == 1 - MEM64 PREF:两者同时成立
  • 在内核里怎么表示 MEM64 PREF

内核最终会把它落到资源标志上(dev->resource[i].flags):

IORESOURCE_MEM | IORESOURCE_MEM_64 | IORESOURCE_PREFETCH

这三个标志会直接影响后续“选窗口 + 找洞”的策略:

  • IORESOURCE_PREFETCH决定它应该尽量放进prefetchable window(PREF_MEM)
  • IORESOURCE_MEM_64决定它具备超过 4GB 的地址可达性(允许分到高地址窗口),同时 BAR 寄存器本身会占用两个 dword(低 32 位 + 高 32 位)。

把第 2 步“反推大小”写成可计算的形式(典型硬件机制,适用于大多数 BAR):

输入:BAR 寄存器当前值 old 步骤: 1) 保存 old 2) 向 BAR 写入全 1(0xFFFFFFFF 或对 64-bit BAR 写两个 dword) 3) 读回 mask 4) 恢复 old 大小计算(概念表达): - 对内存 BAR: addr_mask = mask & 0xFFFFFFF0 size = (~addr_mask) + 1 - 对 I/O BAR: addr_mask = mask & 0xFFFFFFFC size = (~addr_mask) + 1 备注: - BAR 低位包含类型位/属性位,先用掩码去掉,剩下的才是“可变地址位”。 - 64-bit BAR 需要把高 32 位一起纳入地址/掩码计算。

4.4 桥窗口(bridge window)的资源表示

桥设备(PCI-to-PCI bridge / PCIe port)本身也有资源窗口(I/O、MEM、prefetch MEM),用于下游设备地址转发。

可以把桥窗口理解成“桥给下游开的转发表范围”:

  • 下游设备的 BAR 必须落在桥窗口里,否则地址无法被桥转发。
  • 桥的窗口同样会以struct resource形式记录,并最终参与后续的资源树管理。
  • 在某些特殊桥(subtractive/transparent)场景下,下游 bus 会“继承”父 bus 的窗口,这会让资源可用范围看起来更大。

补充一个“硬件原理”的直觉:桥的窗口寄存器本质上就是一组比较器。

当一条 PCIe TLP/PCI 事务携带地址 addr: - 如果 addr 落在桥的 MEM/IO/PREF window 范围内,桥就把事务转发到下游 - 否则就不转发(可能上抛/完成错误/走默认路径,取决于类型与平台)

桥窗口的读取与整理发生在pci_read_bridge_bases()/pci_read_bridge_windows()

4.5 资源策略:沿用固件还是内核重分配

  1. 沿用固件(只认领)

    • 适合:固件已经把 BAR 配好,且系统希望完全沿用固件的地址规划
    • 结果:内核把这些地址“登记进资源树”,用于冲突检测与可视化,但不会改动配置空间
  2. 内核接管(重分配)

    • 适合:固件没配好、存在冲突、或希望内核统一重新分配
    • 结果:内核会计算桥窗口需求、在窗口池里为每个 BAR 找位置,并写回设备配置空间

对应到实现就是:沿用固件时走pci_bus_claim_resources();重分配时先pci_bus_size_bridges()pci_bus_assign_resources()

其中“计算桥窗口需求(sizing)”可以描述为:

输入:一棵以 bridge 为根的子树,子树中每个设备都有若干资源请求 request_i 输出:该 bridge 对下游需要提供的窗口集合 window_type_j(MEM/IO/PREF 等)及其大小 过程(抽象表达): for each window_type in {IO, MEM, PREF_MEM}: size = 0 for each request in subtree where request.type == window_type: size = align_up(size, request.align) size = size + request.size bridge_window[window_type].size = align_up(size, bridge_granularity) 说明: - align_up 规则与 BAR 分配一致(按对齐要求累加) - 现实实现里还会考虑最小窗口粒度、prefetch/non-pref 分组、32/64-bit 约束等

4.6 BAR 分配:在“窗口池子”里给设备找一块合适的空位

分配 BAR 这件事,可以把它理解成“在一堆可用窗口里找车位”:

  • 先选对停车场:I/O BAR 必须进 I/O 窗口;内存 BAR 必须进内存窗口。
  • 再考虑车辆限制:有的 BAR 需要 64 位地址可达性(MEM64)、有的需要更大对齐、有的要求可预取(prefetch)。
  • 最后找空位:从窗口里找一段连续的空闲区间放进去。

关键结论是:分配一定会遵守类型与属性约束(I/O/内存、可预取/不可预取、32 位/64 位),否则设备可能“看起来有地址但实际上不可用”。

把约束说得更明确一些(尤其是 MEM64 PREF):

  • MEM32(非 64-bit):地址上限通常受限于 32-bit,可抽象为max <= 0xFFFFFFFF
  • MEM64:允许分配到 4GB 以上的窗口(上限由对应 window 决定)。
  • PREF(prefetchable):应当优先/必须放进 prefetchable 的窗口池(桥/主桥的 PREF_MEM window);把 PREF BAR 塞进 non-pref window 会让桥的转发属性不匹配,轻则性能问题,重则直接不可用。

真正“在窗口里找空位”的过程由pci_bus_alloc_resource()负责。

其中“找空位”可以描述为:

输入: - request: (size, align, min, max, flags) - windows: 一组可分配窗口(来自 bus->resources),每个窗口可视为区间 [start, end] - occupied: 已被占用的子区间集合(资源树中的子节点) 输出: - assigned: 为 request 选出的 [addr, addr + size - 1],或失败 补充:max 的设置通常直接来自地址可达性约束: - 如果 request 不是 64-bit 可达(没有 MEM64),则 max 至少要收紧到 32-bit 上限 - 如果 request 是 MEM64(尤其是 MEM64 PREF),则 max 可以跟随候选窗口上限 算法(区间分配 / first-fit 的直觉表达): for each window in windows: if window.flags 与 request.flags 不兼容:continue candidate = align_up(max(window.start, request.min), request.align) while candidate + size - 1 <= min(window.end, request.max): if [candidate, candidate + size - 1] 与 occupied 无冲突:return assigned candidate = align_up(next_gap_start(candidate), request.align) return FAIL

其中align_up(x, a)的计算是:

align_up(x, a) = (x + a - 1) & ~(a - 1)

4.7 提高分配成功率:先排序、再多轮尝试

内核为了提高“分配成功率”和“减少碎片”,通常不会简单地按设备枚举顺序分配,而是会做两件事:

  1. 按“最难放进去的”优先分配(通常是对齐要求更大的资源)

    • BAR 对齐越大,越容易造成碎片,先分配能提高成功率
  2. 多轮尝试(尽量更紧凑)

    • 直觉上就是:不要一次就放死,必要时会分多轮把布局“挤一挤”,减少碎片
    • “分阶段满足约束”:
      • 第一阶段只满足必需的资源请求(required)
      • 第二阶段在不破坏第一阶段结果的前提下,尝试满足可选的扩展目标(optional,例如更好的对齐/更大的窗口)

可以把它写成一个更贴近实现的步骤化过程:

输入:devices(按拓扑展开),requests(每个设备的 BAR/桥窗口请求) 输出:每个 request 的分配结果(写回 dev->resource[] 并最终写回配置空间) 1) 收集所有 requests 2) 按 key 排序(典型 key:对齐从大到小、size 从大到小、类型/属性分组) 3) 第一轮:对每个 request 调用“区间分配”在对应窗口中找位置 4) 第二轮(可选):对允许扩展的 request 进行尝试;失败则回退到第一轮已满足的最小可用结果 5) 将分配结果写回: - 设备 BAR:写 BAR 寄存器 - 桥窗口:写 bridge window 寄存器

这里的分配排序与多轮尝试,最终都收敛到pci_bus_assign_resources()这条主路径上。

4.8 驱动侧怎么“用”这些资源:申请、映射、释放

内核把 BAR 分配好并写回配置空间之后,驱动要做的事情其实就三句话:

  1. 确认地址和长度:这个 BAR 最终被分到了哪里、多大。
  2. 申请占用:告诉内核“本驱动要使用这段地址”,避免多个驱动重复占用。
  3. 映射后访问:把 BAR 映射成 CPU 可直接读写的地址,再去访问寄存器。

补充一个基本原理:

  • I/O BAR通常对应端口地址空间,访问可能走特殊的 I/O 指令路径(不同架构实现不同)。
  • MEM BAR对应 MMIO 地址空间,需要映射后用普通的 load/store 访问寄存器(常见接口是pci_iomap()/pcim_iomap_regions())。

为了减少“申请了但忘了释放”的 bug,更推荐优先使用托管接口(pcim/devm),卸载时会自动清理。

常用接口在驱动里会这样出现:读取地址长度用pci_resource_start()/pci_resource_len(),申请占用用pci_request_region(),映射访问用pci_iomap();如果想省心,直接用托管的pcim_iomap_regions()

5. PCI 扫描与资源管理流程示意

下面描述主链路与两个分支(沿用固件 vs 内核重分配)

平台驱动入口 | v +---------------------------------------------+ | 第一步:准备主桥(入口) | | - 解析设备树/固件,得到根窗口清单 | | - 建立配置空间访问方式(ECAM 等) | +---------------------------------------------+ | v +---------------------------------------------+ | 第二步:创建 root bus 并扫描拓扑 | | - 把窗口清单挂到 root bus | | - 扫描所有设备与桥,得到“资源需求” | +---------------------------------------------+ v +---------------------------+-------------------------------+ | | | 选择资源策略:沿用还是重分配? | | | +---------------------------+-------------------------------+ | yes | no v v +---------------------------------------+ +---------------------------------------+ | 沿用固件:只登记,不改配置空间 | | 内核接管:计算 + 分配 + 写回 | +--------------------+------------------+ | - 资源排序、多轮尝试、按规则找洞 | | +--------------------+------------------+ | | +---------------------------+-------------------+ v +-------------------------------------------+ | 设备加入驱动框架,触发驱动 probe | +-------------------------------------------+ | v 驱动申请/映射 BAR(通常优先用 pcim/devm) 图中“资源相关关键产物”对照: 1) 根窗口清单:bridge->windows(来自设备树/固件) 2) 可分配窗口:bus->resources(分配 BAR 的“池子”) 3) 设备资源结果:dev->resource[](排障时最常看的最终结果) 4) 全局资源树:iomem_resource/ioport_resource(冲突检测与可视化依据)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 6:53:14

万物识别模型推理.py使用详解:参数设置与路径修改步骤说明

万物识别模型推理.py使用详解&#xff1a;参数设置与路径修改步骤说明 1. 这个模型到底能认出什么&#xff1f; 你可能已经见过不少图片识别工具&#xff0c;但“万物识别-中文-通用领域”这个模型有点不一样——它不是只认猫狗、汽车或logo的专才&#xff0c;而是真正面向日…

作者头像 李华
网站建设 2026/4/7 9:22:55

解锁Ryzen隐藏潜力:开源硬件调试工具深度探索

解锁Ryzen隐藏潜力&#xff1a;开源硬件调试工具深度探索 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https://gitcode.co…

作者头像 李华
网站建设 2026/4/17 18:10:38

RS485 Modbus协议源代码在STM32中的实时性优化策略

以下是对您提供的博文内容进行 深度润色与工程化重构后的版本 。我以一位资深嵌入式系统工程师兼技术博主的身份&#xff0c;将原文从“教科书式说明”彻底转化为 真实项目现场的语言风格 &#xff1a;有痛点、有踩坑、有取舍、有实测数据支撑&#xff0c;同时剔除所有AI腔…

作者头像 李华
网站建设 2026/4/17 20:55:51

校园毕业照自动增强系统:GPEN轻量级部署实战

校园毕业照自动增强系统&#xff1a;GPEN轻量级部署实战 毕业季一到&#xff0c;校园里到处都是穿学士服、戴方帽的青春身影。可翻看手机相册里的合影&#xff0c;总有些遗憾&#xff1a;光线不足导致脸发灰、像素太低看不清表情、背景杂乱抢了主角风头……有没有一种方法&…

作者头像 李华
网站建设 2026/4/17 7:24:54

魔兽争霸III技术优化指南:现代系统适配与性能增强方案

魔兽争霸III技术优化指南&#xff1a;现代系统适配与性能增强方案 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 一、问题诊断&#xff1a;现代环境下…

作者头像 李华
网站建设 2026/4/17 0:52:06

Nucleus Co-Op:让单机游戏秒变分屏多人体验的神奇工具

Nucleus Co-Op&#xff1a;让单机游戏秒变分屏多人体验的神奇工具 【免费下载链接】nucleuscoop Starts multiple instances of a game for split-screen multiplayer gaming! 项目地址: https://gitcode.com/gh_mirrors/nu/nucleuscoop 你是否遇到过这些游戏联机痛点&a…

作者头像 李华