ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PCIe Device号分配机制:硬件拓扑与枚举规则深度解析

PCIe Device号分配机制:硬件拓扑与枚举规则深度解析 1. 从一张网卡引发的思考Device号到底是谁给定的如果你折腾过Linux下lspci的输出大概率见过这样的画面——同一台机器上网卡是02:00.0显卡是01:00.0NVMe硬盘是03:00.0。但如果你把显卡换一个插槽它的Device号可能就从01:00.0变成了05:00.0而声卡和USB控制器却永远老老实实待在00:1f.x这种位置。这里面的门道就是PCI总线体系结构里最容易被忽视、却最影响调试心情的机制——Device号的分配。先说个我早年踩过的坑。有一回帮朋友处理一台老机器板载的Realtek RTL8111系列网卡在系统里偶尔消失Windows设备管理器里显示该设备无法启动Linux下lspci则干脆看不到PCIe设备列表里有这张网卡。折腾半天最后发现不是驱动问题而是PCI枚举阶段这张卡的Device号分配出现了冲突导致配置空间读写异常。从那以后我才真正意识到搞清楚Device号是怎么来的比背一串PCI ID有用得多。这篇文章不会堆PCIe协议流水账只聚焦一件事在主板上PCI总线的Device号究竟由谁决定、如何分配、为什么不同设备长得不一样、以及我们做驱动和调试时该怎么利用这个规律。无论你是写驱动的、搞BSP的还是只想弄明白自己电脑硬件拓扑的好奇党这篇都值得花十分钟读完。2. 为什么Device号看起来乱七八糟却又有规律2.1 先分清三个号Bus号、Device号、Function号在进入Device号的分配逻辑之前得先把BDF这个概念摆清楚。PCIe体系里每个设备在系统中的唯一标识是总线号:设备号:功能号缩写就是BDF。比如最常见的00:1f.2代表Bus 0、Device 31、Function 2。这三个号的分配逻辑完全不是一个层面的事情Bus号是软件在枚举过程中动态分配的由Host Bridge也叫Root ComplexRC从0开始往下数每发现一条新的PCIe链路就分配一个新的Bus号。Device号是半硬件半软件的硬件上由设备在总线上的物理连接位置决定准确地说是由AD线采样决定软件枚举时只是读出来再填到配置空间里。Function号则是设备内部自己声明的一颗芯片里集成了几个独立功能就占用几个Function号。很多人搞混的地方在于以为Device号跟插槽编号是一一对应的。实际上同一个插槽在不同平台、不同拓扑下拿到的Device号可能完全不同。真正决定Device号的是PCI总线上的IDSEL机制或者PCIe时代继承下来的AD[31:11]地址线采样机制。2.2 传统PCI时代Device号由IDSEL信号决定在多一点理解历史能帮你更好接受PCIe那套机制。在传统并行PCI总线时代总线上的每个设备都有一个独立的信号线叫IDSELInitialization Device Select初始化设备选择线。Host在发起配置读写周期时会把目标Device号编码到地址线AD[31:11]上然后断言FRAME#和IDSEL。具体采样方式是这样的总线枚举器往某个特定地址写配置读命令时地址总线的高位会携带Device号信息。设备端的IDSEL引脚如果被拉到这条地址线的高位比如AD[11]、AD[12]一直到AD[31]那么在配置周期中只要Host发送的地址里那个对应的AD位为高该设备的IDSEL就会被激活从而响应这次配置读写。换句话说传统PCI时代硬件设计者想给某颗设备定一个Device号最简单粗暴的办法是把这颗设备的IDSEL引脚接到某一根特定的AD线上——接到AD[11]就是Device 0接到AD[12]就是Device 1依此类推。这完全是个物理连接层面的决定所以传统PCI时代经常能看到设备跳线帽来设置Device号的老古董设计。2.3 PCIe时代的变化从IDSEL到配置请求路由PCIe引入了点对点串行链路原来的并行总线被彻底抛弃IDSEL这种靠并行地址线采样的机制自然也跟着淘汰了。但Device号并没有消失它依然存在于配置请求的地址字段里只不过现在这件事变成了两部分配合完成Host侧Root Complex在发起Type 0配置读写请求时会把目标Device号放在请求头部的地址字段里并且把AD[31:11]的采样规则保留了下来——或者说PC平台上的Host Bridge以一种约定俗成的方式把Device号映射到了配置请求的地址位。设备侧PCIe设备通过链路训练后知道自己连在哪条Bus上但Device号并不是链路训练学来的而是从配置请求的地址字段中解码出来的。当一个Type 0配置请求到达设备所在的总线时设备会检查请求头里的Device号是否与自己的硬件连接位置匹配匹配则响应不匹配则忽略。这里最核心的一点是在PCIe时代Device号本质上是一个连接位置编号而不是设备出厂时写死的固件编号。它由设备物理连接到哪个Downstream Port、以及该Port在总线枚举时被分配到哪个Device号共同决定。3. Downstream Port的Device号是怎么来的3.1 从Root Complex说起要真正搞懂Device号分配就必须把视角从设备挪到端口Port上来。Root Complex内部通常集成了多个Root Port每个Root Port向外拉出一条PCIe链路插在这个链路另一端的设备或者Switch上行口就跟这个Root Port形成了父子关系。在PCIe拓扑中每个Downstream Port下游端口自己会被分配一个Device号。这个Device号由谁分配答案是硬件固定加上电初始化逻辑共同决定。主板厂商在设计电路时会通过Root Complex内部寄存器、引脚复用和桥接逻辑给每一个Root Port定好它在Bus 0上的Device号。举个例子Intel桌面平台的典型布局中Root Complex自带的PCIe Root Port通常分布在Device 1、Device 2、Device 3这种位置上声卡HDA一般挂在Device 31或Device 27附近USB控制器挂在Device 20、Device 21之类的编号上。这些编号不是Linux内核随手写的而是Intel芯片组内部硬件连线决定的。3.2 硬件固定还是软件可配答案是两者兼有关键问题来了既然Device号跟硬件连接有关那是不是完全不能改从纯硬件角度讲Device号对应的是这个端口在Bus 0上的位置是芯片内部的逻辑布线出厂就定死了。但从BIOS/UEFI和操作系统角度看并不是完全没有操作空间对于Root Port来说它当Downstream Port角色时所在的Bus和Device号由RC固件在开机早期初始化时枚举并写入寄存器。这里的Device号通常是RC内部寄存器中预先定义的端口索引一般不允许软件随意修改因为改了就找不着设备了。对于PCIe Switch交换机来说情况稍微宽松一点。Switch的每个下行端口在初始化时也会分配一个Device号但这个号是从哪个范围取、怎么排取决于Switch芯片厂商的设计。有的Switch允许通过配置寄存器修改下行端口的Device号有的则固定用端口号偏移量的规律。实际开发中我遇到的情况是主板自带Root Port的Device号基本不可变而PCIe Switch下挂的设备的Device号则要依Switch的配置而定有些场合真的可以通过修改Switch寄存器让Device号改头换面。这里有个特别容易混淆的点虽然Device号的最终解释权在硬件但软件枚举时看到的Device号其实是配置请求的地址字段解码结果。也就是说即便硬件固定了某个端口的Device号最终系统里能不能正确看到它还得看Host在枚举时是否会按照这个号发起配置请求。如果枚举代码逻辑有bug把Device号算错了可能整个设备就凭空消失了——这就是我文章开头提到的那台Realtek网卡问题的根源。3.3 一个真实拓扑的例子拿一台典型的消费级主机来看开机后用lspci -t打印树状拓扑大概率长这样-[0000:00]--00.0 Intel Host Bridge -01.0-[01]----00.0 NVIDIA GeForce RTX -1b.0 Intel HD Audio -1c.0-[02]----00.0 Realtek RTL8111/8168 -1d.0 USB Controller -1f.0 ISA Bridge -1f.2 SATA Controller -1f.3 SMBus注意这个拓扑里00:01.0是PCIe Root Port它的Bus号0、Device号1往下挂了一整条Bus 1显卡就在01:00.0上。00:1c.0是另一个Root PortDevice号28十六进制1c就是28往下拖着Bus 2Realtek网卡在02:00.0。00:1f.2是SATA控制器Device号31它不是一个PCIe Root Port而是传统LPC/PCI设备直接以功能设备形式挂在Bus 0上。这个布局能让你直观体会到Device号本质上是一个主干道上的门牌号不同门牌号通向不同的下游总线下游总线上挂着的设备再各自从0号开始排。4. 枚举时Device号分配的具体过程4.1 总线枚举的起点系统上电后RC固件和操作系统引导过程中要做一项基础工作扫描PCIe配置空间建立完整的设备树。这个扫描动作在Linux内核里由PCI核心子系统的枚举逻辑完成关键入口是从Bus 0开始逐Device、逐Function读取每个设备的Vendor ID寄存器配置空间偏移0x00处。流程可以简化成这样从Bus 0开始对Device 0到Device 31、Function 0到Function 7逐一发起配置读请求。如果读到Vendor ID不为0xFFFF说明这个BDF位置上存在设备记录设备信息。如果读到的Header Type表示这是一个PCIe-to-PCIe桥即Downstream Port或Switch上行口就说明桥后面的链路上还有新的总线需要为新总线分配Bus号然后递归扫描。新分配的Bus号从1开始递增扫完一个桥的下游再接下一个。关键细节在于第2步枚举器怎么知道该去访问哪个Device号答案是挨个试。PCI规范规定了一条总线上最多32个Device所以枚举器从0试到31。不同Device号的响应与否取决于设备硬件是否真的连接在对应位置——而设备怎么感知这个配置请求是发给我的就回到了前面讲的AD[31:11]采样规则。4.2 Type 0配置请求与Device号的地址映射在PCIe配置请求中Device号被编码到地址字段的AD[31:11]上。一个标准的Type 0配置读请求地址格式大致是这样的31 20 19 15 14 11 10 9 8 2 1 0 ------------------------------------------------------- | Reserved(或bus) | Device | Function | 0 | 0 | Register | 0 | -------------------------------------------------------Device号占据AD[19:15]这5位最多表示0到31。Function号占据AD[14:12]寄存器偏移在AD[11:2]。当配置请求到达设备所在总线时设备其实通常是该总线上的桥会解码AD[19:15]中携带的Device号和自己这棵子树中某个下游端口的位置编号做匹配匹配成功即可把请求转发下去。这里有个历史包袱为什么是AD[19:15]而不是更低的位因为传统PCI的IDSEL机制中设备被建议连接到AD[16]到AD[31]这些高位上对应Device 16到31而低位的AD[11]到AD[15]对应Device 0到4通常预留给特殊设备。到了PCIe时代配置请求的地址字段依然沿用这套编码所以你在x86平台上看到的绝大多数设备Device号都落在0~31这个范围很少有超出。4.3 一个例子枚举时怎么找到Realtek网卡的再回到文章开头那个Realtek网卡案例。在PCIe平台上这张网卡通常被主板的Root Port挂在Bus 2上拓扑类似Bus 0 - Root Port (00:1c.0) - Bus 2 - Realtek网卡 (02:00.0)Linux枚举流程大致是从Bus 0开始枚举到Device 280x1c时发现这个位置存在一个PCIe桥设备Vendor ID是Intel。读取这个桥的配置空间发现它的Secondary Bus Number是2说明桥后面拖了一条Bus 2。Linux为Bus 2分配总线号然后继续枚举Bus 2上的设备。这次还是从Device 0开始一个个试。在Bus 2上访问Device 0时读到Vendor ID 0x10ECRealtek于是确定了这块网卡的BDF是02:00.0。所以网卡的Device号是0这是因为它正好挂在桥下游的第一个位置——也就是该PCIe链路的Device 0。注意这不是网卡自己出厂设置的而是因为PCIe点对点链路中每个下行端口在同一时刻只有一个设备枚举时自然优先从Device 0开始试。绝大多数PCIe链路末端直连的设备枚举出来都是xx:00.x。但如果你用的是一个带多端口Switch的扩展卡情况就不一样了。Switch下行挂多个设备时不同下行端口会对应不同的Device号。常见的一个结构是Switch上行口占用Device 0下行端口从Device 1、Device 2开始排。这时候下挂设备就不是xx:00.x这么简单了可能是03:01.0、03:02.0这种。5. 实战中怎么判断和自己有关的Device号5.1 通过lspci分析设备拓扑在Linux系统上排查时我用得最多的两条命令是lspci -tv lspci -vvv -s 02:00.0第一条打印树状拓扑能看到Bus、Device、Function的层级关系第二条打印指定BDF设备的详细配置空间信息能拿到链路速率、带宽、Capability列表等关键数据。举个例子想确认某张Realtek网卡挂在哪个Root Port下面直接敲lspci -tv输出类似于-[0000:00]--00.0 Intel Xeon E3-1200 v6/7th Gen Core Host Bridge -01.0-[01-08]-----00.0 NVIDIA GP104 [GeForce GTX 1080] | \-00.1 NVIDIA GP104 High Definition Audio -1c.0-[02]----00.0 Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller这个输出告诉我们网卡距Root Complex只有一级桥总线号2Device号0Function号0。如果这块网卡在Windows下和Linux下显示出的Device号不一致那就要怀疑是不是BIOS在两种引导模式下Legacy vs UEFI对PCI资源分配策略不同。5.2 分清实际Device号和驱动里的Device ID这里必须强调一个高频混淆点PCI Device号DevNum和PCI Device ID也就是跟Vendor ID配套的那两个字节比如Realtek RTL8111的Device ID通常是0x8168完全不是一回事。Device号设备在总线上的位置编号由硬件拓扑和枚举过程决定是BDF三元组里的第二个字段。Device ID设备厂商给自己的产品打的身份证号存在配置空间偏移0x02处的16位寄存器里用来区分同一厂商的不同芯片型号。调试时经常有新人问我这张网卡的Device号是0x8168吗这种问题就像问你家的门牌号是你的身份证号吗概念混淆得厉害。判断Device号看的是lspci输出的BDF字段判断芯片型号看的是lspci -n输出的[10ec:8168]这种厂商ID:设备ID组合。5.3 怎么知道某个设备占用了哪个Device号在Windows系统上可以通过设备管理器→查看→按连接查看设备能看到设备树。但这个树状视图显示的是驱动模型关系不完全等于PCI拓扑。更直观的做法是打开设备属性→详细信息→位置路径Windows会给出类似PCI bus 2, device 0, function 0的描述这就是BDF号。在Linux系统上更直接几条命令就够# 查看所有PCI设备BDF lspci -D # 查看指定厂商的设备 lspci -d 10ec: # 实时监控PCIe热插拔事件 udevadm monitor --property如果做过热插拔或NVMe设备管理还经常会用lspci -s来定位新插入的设备。比如刚插入一个NVMe SSDlspci | grep -i nvme之后看到它在04:00.0那这个4就是新设备被分配到的Bus号0则是Device号。6. 常见Device号分配问题与排查技巧6.1 现象一设备出现在奇怪的Device号上有时候你会看到某张设备卡出现在02:02.0或者01:03.0这种位置第一反应往往是是不是驱动识别偏了。其实大部分情况下这不是错误而是正常的拓扑结果。出现非0的Device号常见原因有这么几个设备挂在一个多端口PCIe Switch下面Switch的每个下行端口占一个Device号下挂设备就分布在Switch占用的总线下的不同位置。主板的后置PCIe x1/x4插槽走的是一条独立的Root Port链路但这个Root Port内部可能占用了多个Device号资源导致设备枚举位置偏移。BIOS开启了PCIe Slot Bifurcation分叉模式一条x16物理插槽被拆分成x8x8或x4x4x4x4每个分叉逻辑上对应不同的Downstream Port于是插在同一物理槽位的设备在不同的分叉配置下Device号会变。排查技巧是先用lspci -tv看整棵树确认这个奇怪的Device号到底挂在哪一级。如果它是直接从00:xx这种Root Port下面出来的那这个Device号就是Root Port的位置编号如果是从某个Switch下面出来的那就是Switch下行端口的位置编号。只要树的层级关系对得上就没有问题。6.2 现象二设备枚举不到Vendor ID读成0xFFFF这是最让人头大的问题之一。lspci里看不到设备或者看到的Vendor ID是ffff表示该位置无设备。可能的原因设备没有正确复位链路训练失败对配置请求无响应。设备所在的Downstream Port被BIOS禁用Bus号没有分配到位。设备挂在总线上但AD[19:15]的Device号解码和预期不符枚举器访问的Device号位置不对。这里有一个不少人忽略的点如果设备所在的桥Bridge本身没有被正确枚举那么桥后面所有设备的配置请求都是无效的。所以排查思路应该是自顶向下# 先确认桥设备是否正常 lspci -vvv -s 00:1c.0 # 确认桥的次级总线号是否分配正确 setpci -v -s 00:1c.0 0x18.w # Secondary Bus Number setpci -v -s 00:1c.0 0x1a.w # Subordinate Bus Number如果桥的Secondary Bus Number是0说明桥没有被正确初始化后面的设备自然全部不可见。这时候问题往往出在BIOS或系统的PCI资源分配逻辑上而不是设备本身。6.3 现象三不同OS / BIOS版本下Device号发生变化这可能是最迷惑人的情况。同一台机器更新BIOS版本之后某块网卡从02:00.0变成了03:00.0或者Windows下是01:00.0、Linux下变成了02:00.0。变化的原因通常是BIOS版本升级改变了一组PCIe端口的初始化顺序导致Bus号和Device号的分配发生了平移。系统开启了新的PCIe功能比如Resizable BAR、SR-IOV后保留了一部分总线号资源后续设备被顺延。Windows和Linux对Bus号资源的分配起点不同Windows可能从1开始分配新Bus号Linux则可能从某个固定值比如0x80开始。遇到这种情况我的处理办法是不要指望设备的Device号固定不变这本身就是对PCIe机制的误解。写驱动或脚本时尽量用Vendor IDDevice IDPCIe Capability的组合来识别设备而不是硬编码BDF。如果必须锁定某个设备可以通过ACPI的PCI路由表或者设备所在的总线拓扑关系来判断。6.4 独家经验用00:1f.2的位置反推老化平台布局在Intel老平台上有个经典规律声卡和SATA控制器几乎永远在00:1f.x。这个位置的Device号是31Function号0到5不等。如果你看到某个设备出现在00:1f.x基本可以断定它是传统PCI设备不是PCIe设备是直接挂在Host Bridge内部的传统I/O控制器。这个规律在Intel 6系列到现在的600系列芯片组上都适用因为Intel一直保持传统I/O控制器挂在Device 31位置的兼容性设计。实际排查时我经常用这个规律区分设备是不是走PCIe链路如果一张网卡在00:1f.6这种位置说明它不是PCIe网卡而是传统PCI接口的骨灰级设备如果它在03:00.0这种位置说明它是走PCIe链路的现代设备。这对判断设备是否支持热插拔、是否支持PCIe电源管理有直接影响。7. 总结成一张表方便查阅为了方便大家日常查证我把Device号相关的关键知识点整理成一张速查表概念决定者特点操作系统呈现位置Bus号BIOS/UEFI OS枚举器动态分配从0开始向后递增BDF中的第一个数字Device号硬件拓扑 枚举器解码由AD[19:15]解码每总线最多32个BDF中的第二个数字Function号设备芯片内部设计最多8个支持多功能设备BDF中的第三个数字Device ID芯片厂商出厂固化固定不变用于区分芯片型号lspci -n 输出中的第二项Root Port位置主板/芯片组布线相对固定不同平台差异大树状拓扑的第一层Switch下行端口位置Switch芯片设计可配置影响下游设备的Device号树状拓扑的中间层这张表每次帮我定位问题都能节省很多时间。记住核心一句话Bus号看软件Device号看拓扑Function号看芯片。8. 最后分享一点实际体会做PCIe调试这几年我觉得最应该培养的习惯不是背寄存器、背BDF规律而是建立自顶向下的排查思路。不管看到多诡异的Device号先画树再逐层验证。确认了桥和总线的分配关系设备之谜自然就解开了。至于遇到Realtek网卡这类设备在Device号上表现出的差异只要熟悉了本文这套机制你会发现它只是PCIe体系里最基础的一环——真正精彩的部分还在配置空间、中断路由和DMA映射那一大摊子事里等着你。
返回列表