1. 项目概述:为什么需要深入理解PCIe配置空间?
如果你是一名硬件工程师、驱动开发者,或者正在捣鼓FPGA上的PCIe接口,那么“PCIe Configuration Space”这个词组对你来说一定不陌生。它就像一块硬件设备的“身份证”和“控制面板”,操作系统和BIOS在启动时,就是通过读取和配置这个空间,来识别你的显卡、网卡、FPGA加速卡,并让它们正常工作的。但很多时候,我们只是调用
lspci
命令看一眼,或者写驱动时照搬模板,对其内部机制一知半解。直到你遇到设备枚举失败、DMA传输异常、或者想手动配置某些高级功能时,才会发现这块“基石”没打牢,后面全是坑。
最近在社区里,我看到不少朋友在讨论相关的问题:FPGA的PCIe IP核怎么配?为什么消费级CPU的PCIe通道数只有24条?
lspci
看到的那些十六进制数到底什么意思?设备老报“Unsupported Request”错误怎么调试?这些问题,追根溯源,几乎都离不开对PCIe配置空间的深入理解。它不仅是协议规定的一部分,更是我们进行硬件调试、性能优化和功能开发的入口。这次,我就结合自己踩过的坑和调试经验,把PCIe配置空间的基础知识、核心结构、操作方式以及实战中如何利用它来定位问题,系统地梳理一遍。无论你是刚接触PCIe的新手,还是想深化理解的开发者,相信都能从中找到你需要的那把“钥匙”。
2. PCIe配置空间全景解读:从物理访问到数据结构
2.1 配置空间的访问机制:CPU如何与设备“对话”
在深入数据结构之前,必须先搞清楚操作系统和CPU是如何访问这个配置空间的。这不是简单的内存读写,而是一套专用的访问机制。对于x86体系结构,这主要通过两个IO端口来实现:
0xCF8
(CONFIG_ADDRESS) 和
0xCFC
(CONFIG_DATA)。
当你执行
lspci
命令时,它底层就是在通过Linux内核的PCI子系统,使用这种机制去轮询总线上的每一个可能的位置。其基本流程是:CPU首先向
0xCF8
端口写入一个目标地址,这个地址编码了总线号、设备号、功能号和寄存器偏移量。然后,CPU再从
0xCFC
端口读取或写入数据,此时PCI总线上的所有设备都会“聆听”,只有地址匹配的那个设备才会响应这次操作,完成对自身配置空间特定位置的读写。
注意 :这种IO端口方式属于“传统PCI配置空间访问机制”。在支持MMCONFIG(Memory Mapped Configuration,即PCI Express Enhanced Configuration Access Mechanism, ECAM)的现代系统中,配置空间会被映射到一段物理内存地址。操作系统更倾向于使用MMCONFIG,因为它更快,更像普通的内存访问。你可以通过
cat /proc/iomem | grep -i mmconfig来查看这段映射区域。
理解这一点对调试至关重要。例如,在嵌入式环境或没有成熟操作系统的情况下,你可能需要直接写代码通过这些端口来枚举设备。而在虚拟化环境(如你提到的PVE)中,透传PCIe设备给虚拟机时,虚拟化层必须正确模拟这套访问机制,否则虚拟机内的驱动就无法正确识别设备。
2.2 配置空间头区域详解:设备的“简历”
每个PCIe功能(Function)都有一个独立的256字节(PCI)或4KB(PCIe)的配置空间。其最开头的64字节被称为“配置空间头区域”,这是标准化的,所有设备都必须遵守。这64字节又分为两部分:前16字节是所有头类型共用的,后48字节根据头类型(Type 0 或 Type 1)有所不同。
通用头部(偏移 0x00 - 0x0F):
-
Vendor ID & Device ID (0x00)
: 这是设备的“品牌”和“型号”。
lspci -n看到的第一对数字就是它们。Vendor ID由PCI-SIG分配,比如Intel是0x8086,NVIDIA是0x10DE。驱动经常用这个ID来匹配是否需要加载。 -
Command & Status Register (0x04)
: 这是两个非常重要的控制与状态寄存器。
-
Command Register
: 控制设备的基本行为。例如,Bit 0是
I/O Space Enable,Bit 1是Memory Space Enable。 在驱动初始化设备时,通常需要先配置好BAR(基地址寄存器),然后再使能对应的Memory或I/O空间,否则设备无法响应CPU对其业务空间的访问。 Bit 2是Bus Master Enable,这是开启DMA能力的开关!如果这个bit没开,设备就无法发起DMA传输,这也是很多DMA驱动初始化失败的首要检查点。 -
Status Register
: 汇报设备状态。比如Bit 4是
Capabilities List,如果为1,表示该设备还有扩展能力链表(Capabilities List),这是PCIe设备功能的扩展,后面会讲到。Bit 3是Interrupt Status,表示中断是否 pending。
-
Command Register
: 控制设备的基本行为。例如,Bit 0是
Type 0 头(用于端点设备,如网卡、显卡): 这是工程师最常打交道的类型,它描述了一个具体的硬件设备。
-
Base Address Registers (BARs, 0x10 - 0x24)
: 这是
重中之重
。每个BAR定义了设备内部的一段内存或I/O空间在主机物理地址空间中的映射位置。当系统启动或驱动加载时,BIOS或操作系统会向BAR写入全1,然后读回,根据回读值来判断该BAR请求的地址空间大小和类型(是32位、64位内存空间,还是I/O空间)。然后系统分配一段合适的物理地址,写回BAR。驱动随后就可以通过访问这段映射后的内存来与设备进行数据交互(例如,控制寄存器、DMA描述符环、数据缓冲区等)。
-
实操心得
:在FPGA开发中,设计PCIe IP核时,你需要明确每个BAR的用途和期望的大小。比如,BAR0用于映射控制寄存器,大小4KB;BAR2用于映射大的数据缓冲区,大小64MB。在Linux驱动里,你可以用
pci_iomap()或ioremap()来获取这些映射到内核虚拟地址空间的指针。
-
实操心得
:在FPGA开发中,设计PCIe IP核时,你需要明确每个BAR的用途和期望的大小。比如,BAR0用于映射控制寄存器,大小4KB;BAR2用于映射大的数据缓冲区,大小64MB。在Linux驱动里,你可以用
- Interrupt Line & Interrupt Pin (0x3C) : 在传统的基于引脚的中断(INTx)时代,这两个寄存器用来关联设备的物理中断引脚和操作系统分配的中断向量号(IRQ)。在现代PCIe中,更多使用MSI或MSI-X中断,但为了兼容性,这些字段仍然存在。
Type 1 头(用于桥设备,如PCIe Switch或Root Port):
它描述了一个桥接设备,负责管理下游的总线。关键字段包括
Primary Bus Number
、
Secondary Bus Number
和
Subordinate Bus Number
,它们定义了该桥所管理的总线号范围。操作系统在枚举时,就是通过这些信息来构建出整个系统的PCIe拓扑树的。你提到的“BIOS拆分PCIe”,很多时候就是通过配置这些桥设备,将一条x16的链路逻辑上拆分成两个x8或四个x4的链路给不同的设备使用。
2.3 能力链表与扩展空间:PCIe的“技能包”
在标准头区域之后,就是PCIe的扩展能力区域。
Status Register
中的
Capabilities List
bit如果为1,那么在配置空间的
0x34
偏移处,会有一个指针(Capability Pointer),指向第一个能力结构(Capability Structure)。每个能力结构都有一个标准的头部:
Capability ID
和
Next Pointer
,形成一个链表。
对于PCIe设备,一些至关重要的能力结构包括:
-
PCI Express Capability (ID 0x10)
: 这是PCIe设备的“核心技能包”。它包含了链路能力(Link Capabilities)、链路状态(Link Status)、设备能力(Device Capabilities)等。从这里你可以读到设备的
PCIe版本
(Gen1, Gen2, Gen3, Gen4...)、
当前链路速度
、
当前链路宽度
(x1, x4, x8, x16)、支持的最大负载大小(Max Payload Size, MPS)、以及是否支持高级功能如ACS、ARI等。
lspci -vvv输出的很多信息就来源于此。 - MSI / MSI-X Capability (ID 0x05 / 0x11) : 消息信号中断能力。这是现代PCIe设备实现高性能、低延迟中断的关键。MSI允许设备将中断信息直接写入到主机内存的特定地址,而不是依赖物理中断线。MSI-X则更灵活,支持多个独立的中断向量和地址/数据对。在驱动中正确配置MSI-X是提升IO性能(尤其是网络和存储设备)的常见手段。
- Power Management Capability (ID 0x01) : 电源管理能力。支持设备在不同电源状态(D0, D1, D2, D3)间切换。
提示 :使用命令
lspci -vvv -s <BDF>可以详细查看设备的所有能力链表内容,这是软件调试的利器。例如,查看链路状态:lspci -vvv -s 03:00.0 | grep -A 10 -B 5 “LnkSta”。
3. 实战操作:如何观察与操控配置空间
理解了理论,我们就要上手操作。在Linux环境下,有一系列强大的工具可以让我们直观地查看和修改配置空间。
3.1 使用标准工具进行探查
-
lspci- 基础侦查工具 :-
lspci: 列出所有PCI设备的总线-设备-功能号(BDF)和简要描述。 -
lspci -v/lspci -vv/lspci -vvv: 显示详细信息,-vvv会显示所有能力结构、链路状态等最详尽的内容。 -
lspci -n: 显示设备的Vendor ID和Device ID,用于驱动匹配。 -
lspci -s 03:00.0: 只看BDF为03:00.0的特定设备。 -
lspci -t: 以树状图显示PCI拓扑结构,非常清晰地展示Root Port、Switch和Endpoint之间的连接关系,对于理解“BIOS拆分PCIe”后的拓扑尤其有用。
-
-
setpci- 底层配置空间读写利器 :setpci允许你直接读写配置空间的任何一个寄存器。 这是一个需要小心使用的强大工具,不当操作可能导致系统不稳定。-
读取命令寄存器(偏移0x04)的值:
sudo setpci -s 03:00.0 04.l-
-s指定设备。 -
04是偏移量(十六进制)。 -
.l表示以长字(4字节)为单位读取。也可以用.w(字,2字节)或.b(字节)。
-
-
开启设备的Bus Master功能(设置Command Register的Bit 2):
# 1. 先读取当前值 OLD_VAL=$(sudo setpci -s 03:00.0 04.l) # 2. 计算新值(设置bit2) NEW_VAL=$(( $OLD_VAL | 0x4 )) # 3. 写回(注意:直接写可能不安全,此处仅为示例) # sudo setpci -s 03:00.0 04.l=$NEW_VAL -
一个实战场景
:在调试DMA问题时,你可以用
setpci确认Bus Master Enable位是否已被驱动正确设置。如果没设置,设备根本无法发起DMA。
-
读取命令寄存器(偏移0x04)的值:
-
devmem2(或busybox devmem) - 直接访问物理内存 : 如果你想查看MMCONFIG映射区的原始内容,或者访问通过BAR映射到内存空间的设备寄存器,devmem2是个好选择。例如,假设你从lspci -vv中看到设备的某个BAR被映射到了物理地址0xfeb00000,你可以用sudo devmem2 0xfeb00000来读取该地址开始的4字节内容。
3.2 在驱动代码中访问配置空间
在Linux内核驱动中,我们使用PCI核心层提供的安全API,而不是直接操作IO端口。
-
读取配置寄存器 :
u16 vendor_id, device_id; u32 bar0; // 读取 16 位的 Vendor ID (偏移 0x00) pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor_id); // 读取 32 位的 BAR0 (偏移 0x10) pci_read_config_dword(pdev, PCI_BASE_ADDRESS_0, &bar0);PCI_VENDOR_ID、PCI_BASE_ADDRESS_0这些是内核定义好的偏移量宏,使用它们比直接写数字更安全、可读性更好。 -
写入配置寄存器 :
u16 cmd; // 读取当前 Command Register pci_read_config_word(pdev, PCI_COMMAND, &cmd); // 开启 Bus Master 和 Memory Space Enable cmd |= PCI_COMMAND_MASTER | PCI_COMMAND_MEMORY; // 写回 pci_write_config_word(pdev, PCI_COMMAND, cmd);重要原则 :修改配置空间寄存器时,尤其是Command Register,通常采用“读-修改-写回”的方式,避免无意中覆盖其他重要的位。
-
查找与遍历能力链表 :
struct pci_cap_saved_state *cap; int pos; // 查找 PCI Express 能力结构 pos = pci_find_capability(pdev, PCI_CAP_ID_EXP); if (pos > 0) { u16 lnk_cap, lnk_sta; // 读取链路能力寄存器 (偏移 pos + 0x0C) pci_read_config_word(pdev, pos + PCI_EXP_LNKCAP, &lnk_cap); // 读取链路状态寄存器 (偏移 pos + 0x12) pci_read_config_word(pdev, pos + PCI_EXP_LNKSTA, &lnk_sta); // 解析链路宽度和速度 int width = lnk_sta & PCI_EXP_LNKSTA_NLW; int speed = lnk_sta & PCI_EXP_LNKSTA_CLS; printk(KERN_INFO “Link Width: x%d, Speed: Gen%d\n”, width, speed); }
4. 核心概念深度解析与问题定位
4.1 BAR、内存映射与DMA的关系
这是理解PCIe设备如何与CPU协同工作的核心。我们用一个简单的“生产者-消费者模型”来类比,这个模型在你提供的热词中也出现了。
假设FPGA作为PCIe端点设备,是一个 数据生产者 (例如,采集视频流),而主机内存是 消费者 (存储或处理数据)。
- 控制通道(BAR映射) :FPGA上有一组控制状态寄存器(CSR),例如“开始采集”、“缓冲区状态”、“中断使能”等。这些寄存器通过一个BAR(比如BAR0)映射到主机的物理地址空间。主机CPU(通过驱动)就像“经理”,通过读写这段映射的内存(即FPGA的CSR)来 控制 FPGA的行为(下发命令)。
- 数据通道(DMA) :当FPGA需要将大量采集到的视频数据送到主机内存时,它使用DMA。首先,“经理”(驱动)在主机内存中准备好一块“仓库”(数据缓冲区),并把“仓库”的 物理地址 和大小告诉FPGA(通过写入BAR映射的某个DMA描述符寄存器)。然后,“经理”下达“开始送货”指令。接着,FPGA作为 Bus Master ,主动发起PCIe写事务(TLP包),将数据直接写入“仓库”(主机内存)。完成后,FPGA可以通过MSI中断通知“经理”:“货已送到”。
关键点 :
- BAR是CPU访问设备的“门” ,用于小数据量、控制型的访问。
- DMA是设备访问主机内存的“高速公路” ,用于大数据量传输,不占用CPU资源。
- Bus Master Enable是DMA的“开关” ,必须在驱动中打开。
-
驱动需要正确设置DMA掩码
(
dma_set_mask_and_coherent),以确保设备能寻址到为它准备的主机内存区域。
4.2 典型错误分析与排查思路
结合你提供的热词,我们分析几个常见问题:
-
“PCIe卡一直出Unsupported Request错误,怎么定位?”
- 什么是Unsupported Request (UR) ? 这是PCIe协议定义的一种错误状态。当接收方(RC或Switch)收到一个它无法理解或无法处理的TLP(事务层包)时,会将其标记为UR,并可能上报错误。
-
可能原因
:
- 地址错误 :设备DMA时使用了无效的地址(如未映射的地址、地址超出范围)。检查驱动中DMA缓冲区分配和地址传递的逻辑。
- 访问了未使能的BAR空间 :设备试图通过某个BAR访问其内部寄存器,但该BAR在主机侧未被正确映射或使能。检查Command Register中对应的Memory/I/O Space Enable位。
- 协议违规 :设备发送的TLP格式不符合规范。这可能源于FPGA PCIe IP核配置或设计错误。
- 配置空间访问错误 :在设备未完全初始化时,主机对其进行了非法配置访问(虽然较少见)。
-
定位方法
:
-
查看高级错误报告(AER)
:如果系统和设备支持AER,这是最佳工具。使用
sudo lspci -vvv -s <BDF>,在输出中寻找Advanced Error Reporting能力部分,查看Uncorrectable Error Status寄存器。lspci可能已经帮你解码了错误类型。 -
使用
perf监控PCIe错误 :sudo perf stat -e “uncore_imc_0/event=0x84/”(事件名因平台而异)可以统计PCIe相关错误事件。 -
检查驱动日志
:内核驱动通常会在DMA映射失败或收到错误时打印日志(
dmesg)。 - 简化测试 :在驱动中,先只进行BAR空间寄存器读写测试,确认控制通路正常。再逐步开启DMA,进行单次、小数据量的传输测试。
-
查看高级错误报告(AER)
:如果系统和设备支持AER,这是最佳工具。使用
-
“为什么消费级CPU只有24条PCIe通道数?” 这个问题与配置空间本身关系不大,但理解它有助于系统设计。消费级CPU(如Intel Core i系列)的PCIe通道通常直接由CPU提供,数量(16或24条)是物理设计决定的,受限于芯片面积、功耗和成本。这些通道通常分配给直连CPU的PCIe插槽(如x16显卡插槽)和高速芯片组(如PCH)。
-
与配置空间的关联
:在系统BIOS/UEFI中,会通过配置PCIe Root Port(Type 1头设备)的
PCI Express Capability结构,来设置每条链路的宽度(x16, x8, x4, x1)和速度。所谓的“BIOS拆分PCIe”,就是把一个物理x16的Root Port,在逻辑上配置成两个x8或四个x4的端口,分配给不同的设备。这个配置信息就存储在对应Root Port的配置空间里。
-
与配置空间的关联
:在系统BIOS/UEFI中,会通过配置PCIe Root Port(Type 1头设备)的
-
“PVE固定网卡名称防止增减PCIe设备失联” 这个问题源于Linux内核的网卡命名规则(如
eth0,eth1)。当系统PCIe设备拓扑发生变化(例如,增加或移除一块PCIe卡),可能导致总线号(Bus)、设备号(Device)重新分配,从而使得网卡的“BDF”标识改变,进而导致网络接口名称(如从enp3s0变成enp4s0)发生变化,影响依赖固定网卡名的配置(如/etc/network/interfaces)。-
解决方案
:使用基于设备永久属性的命名规则,如
MAC地址或PCIe插槽物理位置(通过udev规则实现)。这需要创建一个udev规则文件(如/etc/udev/rules.d/70-persistent-net.rules),根据网卡的PCIe地址(例如0000:03:00.0)或MAC地址,赋予其一个固定的名称(如lan0)。这样无论设备枚举顺序如何变化,该网卡的名字始终不变。 这里的核心是BDF的稳定性或使用更稳定的设备属性。
-
解决方案
:使用基于设备永久属性的命名规则,如
4.3 链路训练与状态机(LTSSM)
LTSSM是PCIe链路的“握手协议”和“健康监测器”,它工作在物理层和链路层。虽然其状态主要在
PCI Express Capability
的链路状态寄存器中体现,但理解它有助于诊断物理连接问题(如你提到的“PCIe的网卡老是掉线”)。
- 链路训练 :当设备上电或复位后,PCIe链路两端的设备会通过LTSSM自动协商链路宽度(x1, x2, ...)和速度(Gen1, Gen2, ...)。这个过程是硬件自动完成的。
-
状态查看
:使用
lspci -vvv -s <BDF>,查看LnkSta字段。你会看到Speed和Width。如果链路不稳定,你可能会看到速度或宽度在期望值以下(例如,本该是x4 Gen3,却只显示x1 Gen1)。 -
网卡掉线可能原因
:
- 物理连接问题 :金手指氧化、插槽松动、线缆(对于扩展卡)质量差。这是最常见原因。
- 电源问题 :设备供电不足,导致链路训练失败或工作中断。
-
热插拔支持问题
:如果是在热插拔过程中掉线,可能与设备的
Slot Capabilities和系统热插拔驱动支持有关。 - 驱动/固件Bug :设备固件或驱动在处理某些错误状态时发生问题,导致链路进入恢复状态(Recovery)甚至检测状态(Detection)而断开。
-
排查步骤
:
-
检查
dmesg日志,搜索与PCIe设备相关的错误信息(如AER,link down)。 -
多次执行
lspci -vvv,观察链路状态(LnkSta)是否频繁变化。 - 尝试将设备换到其他PCIe插槽,排除插槽问题。
- 更新主板BIOS和设备固件。
-
对于Linux,可以尝试在启动参数中给内核添加
pci=noaer(禁用高级错误报告)或pcie_aspm=off(禁用活动状态电源管理)来临时测试是否与某些电源管理特性冲突。
-
检查
5. 进阶话题:与系统及开发的联动
5.1 PCIe设备驱动开发中的配置空间操作
编写一个PCIe设备驱动,初始化的核心就是与配置空间打交道。一个典型的初始化流程如下:
-
探测(Probe) :
static int my_pci_driver_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; u16 vendor, device; // 1. 启用设备 ret = pci_enable_device(pdev); if (ret) { ... } // 2. 请求并映射BAR资源 ret = pci_request_regions(pdev, “my_driver”); if (ret) { ... } // 将BAR0映射到内核虚拟地址空间 drvdata->reg_base = pci_iomap(pdev, 0, 0); if (!drvdata->reg_base) { ... } // 3. 设置DMA掩码(非常重要!) ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); if (ret) { ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); if (ret) { ... } } // 4. 使能Bus Mastering pci_set_master(pdev); // 5. 配置MSI-X中断(如果支持) ret = pci_alloc_irq_vectors(pdev, num_irqs, num_irqs, PCI_IRQ_MSIX); if (ret > 0) { for (i = 0; i < ret; i++) { ret = request_irq(pci_irq_vector(pdev, i), my_isr, 0, “my_irq”, drvdata); ... } } // 6. 初始化设备硬件(通过映射的寄存器空间) my_hw_init(drvdata->reg_base); // 7. 注册字符设备、网络设备等... ... }关键点 :
pci_enable_device()内部会启用设备的I/O和Memory空间(设置Command Register)。pci_set_master()会设置Bus Master Enable位。顺序很重要,必须先映射BAR,再使能Bus Master。 -
移除(Remove) :
static void my_pci_driver_remove(struct pci_dev *pdev) { // 顺序与probe基本相反 // 1. 释放中断 free_irq(...); pci_free_irq_vectors(pdev); // 2. 停止DMA,确保没有进行中的传输 my_stop_dma_engine(); // 3. 取消Bus Master pci_clear_master(pdev); // 4. 取消映射,释放资源 iounmap(drvdata->reg_base); pci_release_regions(pdev); // 5. 禁用设备 pci_disable_device(pdev); }
5.2 系统工具与脚本化诊断
除了交互式命令,我们还可以编写脚本进行自动化诊断或监控。
-
监控链路状态变化脚本 :
#!/bin/bash DEVICE=“03:00.0” while true; do LNK_STA=$(lspci -vvv -s $DEVICE | grep -oP ‘LnkSta:\s+\S+ \S+’) echo “$(date): $LNK_STA” sleep 2 done这个简单的脚本可以每隔2秒记录一次指定设备的链路状态,用于捕捉间歇性的链路降级或断开。
-
批量检查设备DMA使能状态 :
#!/bin/bash for dev in $(lspci -D | awk ‘{print $1}’); do CMD=$(sudo setpci -s $dev 04.w) # 检查Command Register的Bit 2 (Bus Master) if (( (0x$CMD & 0x4) == 0 )); then echo “Device $dev has Bus Master DISABLED.” fi done
5.3 虚拟化环境下的考量
在像PVE这样的虚拟化环境中,PCIe设备透传(Passthrough)给虚拟机时,配置空间的访问会被虚拟化层(如QEMU/KVM)拦截和模拟。
- 对虚拟机的影响 :虚拟机内的驱动看到的配置空间,是虚拟化层提供的“视图”,可能与物理配置空间不完全一致(例如,MPS可能被虚拟化层调整为更安全的值)。但BAR映射、中断配置等核心操作,最终会由虚拟化层代理到物理设备上。
-
“固定网卡名称”问题的根源
:在宿主机上,PVE可能因为PCIe拓扑扫描顺序的变化,导致透传给虚拟机的设备BDF在每次启动时不同。在虚拟机内部,这就会导致网卡名变化。解决方案如前所述,在虚拟机内部使用
udev规则基于MAC地址固定网卡名,是更可靠的做法。 -
调试
:如果在虚拟机内遇到PCIe设备问题,首先应在宿主机上用
lspci -vvv确认物理设备状态正常。然后检查虚拟机的XML配置(对于Libvirt)或QEMU命令行参数,确保设备透传配置正确(例如,是否忽略了ROM,是否指定了正确的PCIe拓扑地址)。
248



被折叠的 条评论
为什么被折叠?



