Linux 内核 PCI 子系统:通过 sysfs 访问 PCI 设备资源的完整机制与实现剖析
2026/9/7 2:19:10 网站建设 项目流程

Linux 内核 PCI 子系统:通过 sysfs 访问 PCI 设备资源的完整机制与实现剖析

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

sysfs(通常挂载于/sys)是用户空间访问 PCI 设备资源的标准通道。本文以 Documentation/PCI/sysfs-pci.rst 为主体,逐节解析 PCI 设备目录下每个属性文件的用途、权限与操作方式,并深入 drivers/pci/pci-sysfs.c 源码,说明这些文件背后的 show/store/mmap 实现,最终覆盖从配置空间二进制读写、BAR 资源 mmap,到 legacy I/O 端口/内存访问以及平台侧支持要求的完整链路。

sysfs 中的 PCI 目录结构

sysfs 在支持的平台(CONFIG_PCI)上向用户空间暴露 PCI 资源。一个 PCI 域/总线在 sysfs 中呈现的目录结构如下(原文档示例):

/sys/devices/pci0000:17 |-- 0000:17:00.0 | |-- class | |-- config | |-- device | |-- enable | |-- irq | |-- local_cpus | |-- remove | |-- resource | |-- resource0 | |-- resource1 | |-- resource2 | |-- revision | |-- rom | |-- subsystem_device | |-- subsystem_vendor | `-- vendor `-- ...

最顶层目录名pci0000:17编码了 PCI 域号和总线号:上例中域号为0000、总线号为17,两者均为十六进制表示。该总线上只有一个位于 0 号插槽的函数设备0000:17:00.0,域号与总线号被重复写入设备名以便直接识别。设备目录下的每个文件各有其职责,下表是原文档给出的完整属性清单:

文件功能
classPCI 类代码(ascii,只读)
configPCI 配置空间(binary,可读写)
devicePCI 设备 ID(ascii,只读)
enable设备是否使能(ascii,可读写)
irq中断号(ascii,只读)
local_cpus就近 CPU 掩码(cpumask,只读)
remove将设备从内核 PCI 设备列表中移除(ascii,只写)
resourcePCI 资源的主机地址(ascii,只读)
resource0..NPCI 资源 N,若存在(binary,可 mmap,可读写¹)
resource0_wc..N_wcPCI WC 映射资源 N,若该 BAR 可预取(binary,可 mmap)
revisionPCI 修订号(ascii,只读)
romPCI ROM 资源,若存在(binary,只读)
subsystem_devicePCI 子系统设备 ID(ascii,只读)
subsystem_vendorPCI 子系统厂商 ID(ascii,只读)
vendorPCI 厂商 ID(ascii,只读)

¹rw仅对IORESOURCE_IO(I/O 端口)类型的区域有效。

符号含义:ro只读文件;rw可读可写;wo只写;mmap可被 mmap;ascii文本内容;binary二进制内容;cpumaskCPU 掩码类型。只读文件的信息是供参考的,对它们的写操作会被忽略(rom文件除外,见下文);可写文件用于对设备执行动作(例如修改配置空间、摘除设备);可 mmap 的文件通过偏移 0 的 mmap 提供用户态直接编程设备的能力。注意部分平台不支持对某些资源做 mmap,因此调用方必须检查 mmap 的返回值——其中最典型的是 I/O 端口资源,它们额外提供读/写访问。

值得说明的是,当前内核树中该目录的实际属性比上表更丰富。从 drivers/pci/pci-sysfs.c 的pci_dev_groupspci_dev_attr_groups可见,现代 PCI 设备目录还包含power_statemodaliasnuma_nodedma_mask_bitsmsi_busrescanresetreset_methodvpd、ACPI/DOE/TSM 相关组,以及 PCIe 设备特有的current_link_speedcurrent_link_widthmax_link_speedmax_link_width等属性;这些是文档成文后逐步加入的扩展,原文档表格描述的是该机制的核心稳定面。

只读属性文件:从配置空间字段到 CPU 亲和性

vendor/device 等身份类文件都由 drivers/pci/pci-sysfs.c 中的pci_config_attr()宏统一生成,每个宏展开为一个_show函数加一个DEVICE_ATTR_RO定义,格式串直接决定了你cat出来的文本形态:

#define pci_config_attr(field, format_string) \ static ssize_t \ field##_show(struct device *dev, struct device_attribute *attr, char *buf) \ { \ struct pci_dev *pdev; \ \ pdev = to_pci_dev(dev); \ return sysfs_emit(buf, format_string, pdev->field); \ } \ static DEVICE_ATTR_RO(field) pci_config_attr(vendor, "0x%04x\n"); pci_config_attr(device, "0x%04x\n"); pci_config_attr(subsystem_vendor, "0x%04x\n"); pci_config_attr(subsystem_device, "0x%04x\n"); pci_config_attr(revision, "0x%02x\n"); pci_config_attr(class, "0x%06x\n");

vendor/device/subsystem_vendor/subsystem_device输出 4 位十六进制、revision输出 2 位、class输出 6 位(高字节为 base class,中间为 sub class,低字节为 programming interface)。数据源是内核在枚举时缓存于struct pci_dev的字段(include/linux/pci.h),读取本身不触发新的配置空间访问,因此开销极低,这也是 udev/udev 规则匹配所依赖的modalias属性(格式为pci:v...d...sv...sd...bc...sc...i...,见 modalias_show)的数据基础。

irq文件的语义与CONFIG_PCI_MSI相关,见 irq_show:

/* * For MSI, show the first MSI IRQ; for all other cases including * MSI-X, show the legacy INTx IRQ. */ if (pdev->msi_enabled) return sysfs_emit(buf, "%u\n", pci_irq_vector(pdev, 0)); return sysfs_emit(buf, "%u\n", pdev->irq);

即 MSI 已使能时显示第一个 MSI 向量,否则(包括 MSI-X 的情况)显示传统 INTx 中断号。

local_cpus表示该设备 DMA 就近的 CPU 掩码。local_cpus_show 的实现是:开启 NUMA 时若设备所属 node 有效则取cpumask_of_node(dev_to_node(dev)),否则回退到cpu_online_mask;未开启 NUMA 时取cpumask_of_pcibus()。它对应的可读列表形式是local_cpulist

config 文件:配置空间的二进制读写

config是二进制属性(BIN_ATTR,权限 0644,见 drivers/pci/pci-sysfs.c),文件大小等于配置空间大小(普通设备 256 字节,支持能力列表的 PCIe 设备为 4096 字节,由 pci_dev_config_attr_bin_size 根据pdev->cfg_size决定)。读路径 pci_read_config 有几个关键行为:

  • 权限分级:只有具备CAP_SYS_ADMIN的进程能读取dev->cfg_size全量空间;普通用户只能读前 64 字节(CardBus 头类型放宽到 128 字节),注释里给出了原因——“部分芯片在读取未定义的配置空间时会锁死”;
  • 对齐拆分:按字节/字/双字拆分非对齐区间,分别调用pci_user_read_config_byte/word/dword完成访问,大循环中还调用cond_resched()防止长时间占用 CPU;
  • 电源管理:整个访问过程被pci_config_pm_runtime_get/put包裹,避免在设备处于 runtime 休眠状态时访问配置空间。

写路径 pci_write_config 的防御更严格:

ret = security_locked_down(LOCKDOWN_PCI_ACCESS); if (ret) return ret; if (resource_is_exclusive(&dev->driver_exclusive_resource, off, count)) { pci_warn_once(dev, "%s: Unexpected write to kernel-exclusive config offset %llx", current->comm, off); add_taint(TAINT_USER, LOCKDEP_STILL_OK); }

即内核 lockdown 开启LOCKDOWN_PCI_ACCESS时直接拒绝;向驱动标记为“内核独占”的配置偏移(如某些芯片的 BAR/命令寄存器)写入时,会打印告警并给内核打上TAINT_USER污点——这是给用户的明确信号:这类写入可能导致驱动状态不一致。写操作同样按字节/字/双字拆分为pci_user_write_config_*调用。

用户态典型用法是lseek到目标偏移再read/write1、2 或 4 字节,例如读取 Vendor ID:打开config后读偏移 0 处的 2 字节即可。

enable 文件:引用计数式的设备使能

enable文件是一个计数器:读它返回设备当前被使能的次数(sysfs_emit(buf, "%u\n", atomic_read(&pdev->enable_cnt)),见 enable_show)。若当前返回 4,向其中echo 1后读数变为 5;echo 0则递减。原文档特别提醒:即使计数回到 0,部分初始化操作也不会被逆转

写路径 enable_store 揭示了完整的约束:

/* this can crash the machine when done on the "wrong" device */ if (!capable(CAP_SYS_ADMIN)) return -EPERM; ... device_lock(dev); if (dev->driver) result = -EBUSY; else if (val) result = pci_enable_device(pdev); else if (pci_is_enabled(pdev)) pci_disable_device(pdev); else result = -EIO; device_unlock(dev);

要点:需要CAP_SYS_ADMIN(注释直言“对错误的设备操作可能让机器崩溃”);已绑定驱动的设备和用户态使能互斥(-EBUSY);写非零值调用pci_enable_device(),写 0 调用pci_disable_device()(且要求当前确实处于使能状态,否则-EIO)。这个文件的主要用途是:在驱动未绑定时,为用户态 ROM 读取等操作预先使能设备(见下一节)。

rom 文件:读取设备 Option ROM

rom是一个特殊文件:它对设备的 Option ROM 提供只读数据访问,但默认是禁用状态。应用必须先向文件写入字符串"1"启用,读完后再写"0"关闭;并且设备必须处于使能状态ROM 读取才能成功——如果驱动没有绑定,可借助上一节的enable文件使能。

源码中 pci_write_rom 只是用kstrtobool解析布尔值并设置pdev->rom_attr_enabled;pci_read_rom 则检查该标志:

if (!pdev->rom_attr_enabled) return -EINVAL; rom = pci_map_rom(pdev, &size); /* size starts out as PCI window size */ if (!rom || !size) return -EIO; ... memcpy_fromio(buf, rom + off, count); ... pci_unmap_rom(pdev, rom);

即未显式启用时读操作返回-EINVAL,实际数据经pci_map_rom()映射后从 I/O 内存拷出。文件权限为 0600(BIN_ATTR(rom, 0600, ...)),且仅当设备确实拥有 ROM 资源时才可见(pci_dev_rom_attr_is_visible 检查pci_resource_end(pdev, PCI_ROM_RESOURCE))。这一机制的典型消费者是pciutilslspci -x/lspci -vv以及 VGA 固件提取工具。

remove 文件:把设备从内核中摘除

remove文件写入非零整数即可将 PCI 设备从内核的设备列表中移除。原文明确强调:这不涉及任何热插拔功能,例如不会给设备断电。具体效果是:设备从内核 PCI 设备链表移除、其 sysfs 目录被删除、并从所有已绑定驱动上解绑。同时PCI 根总线的移除是被禁止的

实现见 remove_store:

if (val && device_remove_file_self(dev, attr)) pci_stop_and_remove_bus_device_locked(to_pci_dev(dev)); return count;

先删除 sysfs 属性文件自身,再调用pci_stop_and_remove_bus_device_locked()走完整的设备停机与解绑流程。配套的“找回”手段是设备目录或总线上的rescan文件:向 dev_rescan_store 写非零值会pci_rescan_bus(pdev->bus)重新枚举整条总线。另外注意 pci_dev_hp_attrs_are_visible:SR-IOV 虚拟功能(pdev->is_virtfn)不暴露remove/rescan,VF 的生命周期由 PF 的 sriov 属性统一管理。

resource 与 resourceN 文件:资源地址与 BAR 访问

resource是纯信息文件,resource_show 逐行输出每个 BAR(桥设备还有桥窗口)的起始地址 结束地址 标志位三个十六进制数,非桥设备输出 6 个 BAR。

resource0resource5是真正提供设备寄存器访问的二进制文件,每个 BAR 有三种变体,由同一组可见性函数按资源类型裁剪:

变体适用条件能力
resourceN(IO 属性)IORESOURCE_IO1/2/4 字节 read/write;平台支持arch_can_pci_mmap_io()时额外可 mmap
resourceN(UC 属性)IORESOURCE_MEMmmap 为 uncached 内存
resourceN_wcIORESOURCE_MEM \| IORESOURCE_PREFETCHarch_can_pci_mmap_wc()为真mmap 为 write-combining 内存

可见性判断在 __pci_resource_attr_is_visible:BAR 长度为 0、标志位不匹配、或设备被标记non_mappable_bars时该文件不出现;WC 变体额外要求arch_can_pci_mmap_wc()在运行时为真。

I/O 端口 BAR 的读写由 pci_resource_io 实现(需要CONFIG_HAS_IOPORT):只接受 1、2、4 字节访问,把文件偏移加上 BAR 起始端口号得到绝对端口,校验不越界后调用inb/outbinw/outwinl/outl。这正是原文档脚注“rw仅对IORESOURCE_IO区域有效”的出处。

mmap 路径统一走 pci_mmap_resource,其中的检查顺序值得留意:

ret = security_locked_down(LOCKDOWN_PCI_ACCESS); if (ret) return ret; if (!pci_resource_is_mem(pdev, bar) && !(pci_resource_is_io(pdev, bar) && arch_can_pci_mmap_io())) return -EIO; if (pci_resource_is_mem(pdev, bar) && iomem_is_exclusive(pci_resource_start(pdev, bar))) return -EINVAL; if (!pci_mmap_fits(pdev, bar, vma, PCI_MMAP_SYSFS)) return -EINVAL; mmap_type = pci_resource_is_mem(pdev, bar) ? pci_mmap_mem : pci_mmap_io; return pci_mmap_resource_range(pdev, bar, vma, mmap_type, write_combine);

lockdown、资源类型、内核独占区域(iomem_is_exclusive,如被保留的 VGA 内存窗口)逐一拦截后,最终委托pci_mmap_resource_range()完成映射。pci_mmap_resource_uc/wc分别以write_combine = 0/1进入同一入口。用户态 C 代码的标准用法即:open()mmap(fildes, bar_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0),然后解引用指针访问寄存器,务必检查 mmap 返回值(部分平台不支持)。

legacy_io 与 legacy_mem:通过 sysfs 访问传统 I/O 端口和 ISA 内存

若平台支持,legacy I/O 端口与 ISA 内存资源同样在 sysfs 中提供,它们位于PCI 类层级(pci_bus 设备)下,例如:

/sys/class/pci_bus/0000:17/ |-- bridge -> ../../../devices/pci0000:17 |-- cpuaffinity |-- legacy_io `-- legacy_mem

这一布局来自 drivers/pci/probe.c:struct bus_type pci_bus_type.dev_groups指向pcibus_groups,后者在 drivers/pci/pci-sysfs.c 中汇总了legacy_iolegacy_io_sparselegacy_memlegacy_mem_sparse四个二进制属性组。

  • legacy_io是读写文件,供应用做 legacy 端口 I/O:打开文件,lseek到目标端口(如0x3e8),再 read 或 write1、2 或 4 字节(其他长度直接-EINVAL,见 pci_read_legacy_io),底层经架构回调pci_legacy_read()/pci_legacy_write()完成实际访问。
  • legacy_mem应带目标内存偏移 mmap,例如偏移0xa0000对应 VGA 显存(frame buffer);映射成功后直接解引用返回指针即可访问 legacy 内存空间。mmap 实现 pci_mmap_legacy_mem 同样先过security_locked_down(LOCKDOWN_PCI_ACCESS),再调用pci_mmap_legacy_page_range()
  • 文件大小由 include/linux/pci.h 定义:PCI_LEGACY_IO_SIZE为 64K-1(即端口地址空间上限),PCI_LEGACY_MEM_SIZE为 1M(即 low memory 中经典的 A0000 显存/ROM 区)。
  • 属性可见性受LOCKDOWN_PCI_ACCESSpci_legacy_has_sparse()影响(__pci_legacy_is_visible):lockdown 生效时这些文件直接不可见,_sparse变体仅当平台声明存在稀疏 legacy 空间时出现。

平台侧要求:新平台如何支持 PCI 资源访问

要让上述 sysfs 资源映射在某个架构上工作,原文档给出的平台侧约定(与当前源码一一对应)是:

  1. 资源 mmap:理想做法是定义ARCH_GENERIC_PCI_MMAP_RESOURCE,复用通用实现(drivers/pci/pci-sysfs.c 与 #L1711 的两处#if defined(HAVE_PCI_MMAP) || defined(ARCH_GENERIC_PCI_MMAP_RESOURCE)宏条件即为此而设);x86(arch/x86/include/asm/pci.h)与 ARM(arch/arm/include/asm/pci.h)均走该通用路径。为兼容历史上/proc/bus/pci文件的mmap()接口,平台可另外设置HAVE_PCI_MMAP
  2. 自定义实现:设置HAVE_PCI_MMAP的平台也可以不提供ARCH_GENERIC_PCI_MMAP_RESOURCE,而是自行实现 include/linux/pci.h 声明的pci_mmap_resource_range()
  3. 能力开关:支持 WC 映射的平台必须定义arch_can_pci_mmap_wc(),运行时允许 WC 时求值为非零;支持 I/O 资源映射的平台类似地定义arch_can_pci_mmap_io()。两者在 include/linux/pci.h 都有默认值为 0 的兜底定义——这正是“并非所有平台都能 mmap 所有资源”的根因。
  4. Legacy 资源:由HAVE_PCI_LEGACY保护。平台希望支持 legacy 功能时应定义它,并提供pci_legacy_readpci_legacy_writepci_mmap_legacy_page_range三个函数(drivers/pci/pci-sysfs.c 起的整个 legacy 属性段都包裹在#ifdef HAVE_PCI_LEGACY内)。

实操要点小结

把文档语义与源码约束合并后,用户态工具(如 pciutils、udev 规则、设备调试脚本)应遵循以下操作规范:

# 1. 查看设备身份(只读,任意用户) cat /sys/bus/pci/devices/0000:17:00.0/vendor # 0x8086 cat /sys/bus/pci/devices/0000:17:00.0/resource # 各 BAR 的 起始/结束/标志 # 2. 读取配置空间(非 root 只能读前 64 字节,root 可读全空间) # 用 dd 或 C 程序 lseek + read,注意按 1/2/4 字节对齐访问 # 3. 读取 Option ROM(root,0600 文件) echo 1 > /sys/bus/pci/devices/0000:17:00.0/rom dd if=/sys/bus/pci/devices/0000:17:00.0/rom of=rom.bin echo 0 > /sys/bus/pci/devices/0000:17:00.0/rom # 前提:设备已使能(有驱动绑定,或 echo 1 > enable) # 4. 摘除/恢复设备(root;不会断电,根总线不可移除) echo 1 > /sys/bus/pci/devices/0000:17:00.0/remove echo 1 > /sys/bus/pci/devices/0000:17:00.0/rescan # 设备级 # 或 echo 1 > /sys/bus/pci/rescan # 全局重扫 # 5. BAR 访问(root,0600 文件) # 内存 BAR:mmap(fildes, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) # I/O BAR:lseek 到端口偏移后 read/write 1/2/4 字节

需要牢记的前提与限制:所有写操作类属性(enableremoveconfig写、resourceN_wc等)要求CAP_SYS_ADMIN;内核 lockdown(LOCKDOWN_PCI_ACCESS)开启时config写、resourceNmmap、legacy 访问全部被拒绝;向驱动标记为独占的配置偏移写会告警并污染内核(TAINT_USER);mmap 结果必须检查返回值,因为 I/O 端口资源的 mmap 仅在arch_can_pci_mmap_io()为真的平台上可用。

这套机制的价值在于:它把 PCI 枚举、资源分配、驱动绑定等全部状态以文件形式暴露给用户空间,同时又通过可见性函数、权限位、能力检查和 lockdown 钩子把“能看”与“能动”严格区分开。理解 Documentation/PCI/sysfs-pci.rst 描述的接口面与 drivers/pci/pci-sysfs.c 中的实现后,开发者既可以编写安全的设备管理工具,也可以按“平台侧要求”一节为自己的架构补齐 PCI 资源映射与 legacy 访问支持。相关背景可继续参考同目录的 Documentation/PCI/pci.rst 与 Documentation/PCI/index.rst。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询