对于绝大多数现代操作系统(如 Linux、Windows、macOS),应用程序级别的开发者不应该、也不能直接通过 C 语言操作 PCI 硬件。

(图片来源网络,侵删)
原因如下:
- 权限问题:直接操作硬件需要最高级别的系统权限(内核 Ring 0),而应用程序运行在用户态(Ring 3),没有权限直接读写物理内存和 I/O 端口。
- 硬件抽象:操作系统已经提供了标准的、统一的硬件访问接口(API),开发者应该使用这些接口,而不是去绕过它们,这保证了系统的稳定性和安全性。
- 设备即插即用:现代操作系统负责设备的发现、配置、加载驱动和资源分配,直接操作会破坏这个流程。
PCI 编程主要分为两个层面:
- 内核驱动开发:这是最核心、最底层的 PCI 编程,开发者编写 C 代码,作为操作系统内核的一部分,直接与 PCI 设备交互,这是实现网卡、显卡、声卡等设备驱动的方式。
- 用户空间应用程序:开发者通过操作系统提供的库或接口,从用户空间查询和管理 PCI 设备信息,但无法直接驱动硬件。
下面我将分别从这两个层面,并结合具体的 C 语言代码示例来讲解。
内核驱动编程 (以 Linux 为例)
在 Linux 内核中,有一个 PCI 子系统,它提供了一组结构体和函数,用于驱动程序发现、配置和与 PCI 设备通信。

(图片来源网络,侵删)
核心概念和 API
struct pci_dev:内核中代表一个 PCI 设备的核心结构体,驱动程序通过它来访问设备的所有信息。pci_register_driver():向 PCI 子系统注册你的驱动程序,这个函数告诉内核你的驱动支持哪些设备(通过 ID 表)。pci_match_device():PCI 子系统在发现设备时,会调用此函数来检查你的驱动是否支持该设备。pci_enable_device():启用 PCI 设备,包括请求内存、I/O 端口等资源。pci_request_regions():请求设备占用的内存和 I/O 区域,防止其他驱动使用。pci_map_bar()/ioremap():将设备的 BAR (Base Address Register) 映射到内核的虚拟地址空间,以便 CPU 可以访问。readb(),readw(),readl(),writeb(),writew(),writel():用于向映射后的内存地址(通常是设备的配置空间或内存 BAR)读写数据的函数。pci_disable_device()/pci_release_regions():在驱动卸载时,释放之前请求的资源。
简单的 PCI 驱动示例
这个示例将创建一个最简单的 PCI 驱动,它只会在加载时打印出找到的设备信息,然后映射其内存 BAR 并进行一次简单的读写操作。
驱动代码 (pci_simple_driver.c)
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/io.h> // 用于 ioremap
#define PCI_VENDOR_ID_VIRTIO 0x1AF4
#define PCI_DEVICE_ID_VIRTIO_NET 0x1000
// 注意:我们使用一个已知的虚拟设备 ID 作为示例
// probe 函数:当 PCI 子系统找到一个匹配的设备时调用
static int pci_simple_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
int ret;
void __iomem *bar0_addr; // 用于存储映射后的 BAR 地址
resource_size_t bar0_size;
printk(KERN_INFO "pci_simple_driver: Found device %04x:%04x\n",
dev->vendor, dev->device);
// 1. 启用设备
ret = pci_enable_device(dev);
if (ret) {
printk(KERN_ERR "pci_simple_driver: Failed to enable device\n");
return ret;
}
// 2. 请求设备占用的资源
ret = pci_request_regions(dev, "pci_simple_driver");
if (ret) {
printk(KERN_ERR "pci_simple_driver: Failed to request regions\n");
pci_disable_device(dev);
return ret;
}
// 3. 获取 BAR0 的信息并映射
bar0_size = pci_resource_len(dev, 0); // BAR0 的大小
bar0_addr = pci_iomap(dev, 0, 0); // 映射 BAR0
if (!bar0_addr) {
printk(KERN_ERR "pci_simple_driver: Failed to map BAR0\n");
pci_release_regions(dev);
pci_disable_device(dev);
return -ENOMEM;
}
printk(KERN_INFO "pci_simple_driver: BAR0 mapped at %p, size %llu\n",
bar0_addr, (unsigned long long)bar0_size);
// 4. 读写操作示例 (假设 BAR0 是内存空间)
// 注意:直接读写硬件很危险,这里仅作演示
u32 original_value = readl(bar0_addr);
printk(KERN_INFO "pci_simple_driver: Original value at BAR0: 0x%08x\n", original_value);
u32 new_value = 0xDEADBEEF;
writel(new_value, bar0_addr);
printk(KERN_INFO "pci_simple_driver: Wrote new value 0x%08x to BAR0\n", new_value);
u32 read_back_value = readl(bar0_addr);
printk(KERN_INFO "pci_simple_driver: Read back value from BAR0: 0x%08x\n", read_back_value);
// 5. 清理映射和资源
pci_iounmap(dev, bar0_addr);
pci_release_regions(dev);
pci_disable_device(dev);
return 0;
}
// remove 函数:当设备被移除时调用
static void pci_simple_remove(struct pci_dev *dev)
{
printk(KERN_INFO "pci_simple_driver: Removing device %04x:%04x\n",
dev->vendor, dev->device);
}
// PCI 设备 ID 表,告诉内核我们的驱动支持哪些设备
static const struct pci_device_id pci_simple_ids[] = {
{ PCI_DEVICE(PCI_VENDOR_ID_VIRTIO, PCI_DEVICE_ID_VIRTIO_NET) },
{ 0, } // 结束标记
};
MODULE_DEVICE_TABLE(pci, pci_simple_ids);
// PCI 驱动结构体
static struct pci_driver pci_simple_driver = {
.name = "pci_simple_driver",
.id_table = pci_simple_ids,
.probe = pci_simple_probe,
.remove = pci_simple_remove,
};
// 模块初始化函数
static int __init pci_simple_init(void)
{
printk(KERN_INFO "pci_simple_driver: Loading driver\n");
return pci_register_driver(&pci_simple_driver);
}
// 模块退出函数
static void __exit pci_simple_exit(void)
{
printk(KERN_INFO "pci_simple_driver: Unloading driver\n");
pci_unregister_driver(&pci_simple_driver);
}
module_init(pci_simple_init);
module_exit(pci_simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple PCI driver example");
编译和加载
你需要一个内核开发环境来编译这个模块。
# 编译成 .ko 内核模块 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules # 加载模块 (需要 root 权限) sudo insmod pci_simple_driver.ko # 查看内核日志 dmesg | tail # 卸载模块 sudo rmmod pci_simple_driver
用户空间应用程序编程
在用户空间,我们不能直接驱动硬件,但可以查询 PCI 设备的详细信息,Linux 提供了两种主要方式:
/proc/bus/pci/:一个虚拟文件系统,直接暴露 PCI 配置空间。libpci库:更现代、更方便的方式,提供了 C API 来访问 PCI 信息。lspci命令就是用这个库实现的。
使用 libpci 库编程
这是推荐的用户空间编程方式。
安装开发库
# 对于 Debian/Ubuntu sudo apt-get install libpci-dev # 对于 Fedora/CentOS sudo yum install pciutils-devel
C 语言示例代码 (pci_info.c)
这个程序会列出系统中的所有 PCI 设备,并打印出它们的基本信息。
#include <stdio.h>
#include <stdlib.h>
#include <pci/pci.h> // libpci 的头文件
int main()
{
struct pci_access *pacc; // PCI 访问对象
struct pci_dev *dev; // PCI 设备对象
// 1. 初始化 PCI 访问
pacc = pci_alloc();
pci_init(pacc);
pci_scan_bus(pacc); // 扫描所有总线上的设备
// 2. 遍历所有找到的设备
for (dev = pacc->devices; dev; dev = dev->next) {
// 3. 填充设备信息 (从配置空间读取)
pci_fill_info(dev, PCI_FILL_IDENT | PCI_FILL_BASES | PCI_FILL_CLASS);
// 4. 打印设备信息
printf("Bus: %02d, Device: %02d, Function: %02d\n",
dev->bus, dev->dev, dev->func);
printf(" Device ID: %04x:%04x\n", dev->vendor_id, dev->device_id);
printf(" Class: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
PCI_LOOKUP_CLASS, dev->class_code));
printf(" Vendor: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
PCI_LOOKUP_VENDOR, dev->vendor_id, dev->device_id));
printf(" Device: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
PCI_LOOKUP_DEVICE, dev->vendor_id, dev->device_id));
if (dev->base_addr[0] != 0) {
printf(" BAR0: 0x%llx\n", (unsigned long long)dev->base_addr[0]);
}
printf(" IRQ: %d\n", dev->irq);
printf("--------------------------------\n");
}
// 5. 清理
pci_cleanup(pacc);
return 0;
}
编译和运行
# 编译,链接 libpci 库 gcc pci_info.c -o pci_info -lpci # 运行 (普通用户即可) ./pci_info
输出示例:
Bus: 00, Device: 00, Function: 00
Device ID: 8086:31e4
Class: Host bridge
Vendor: Intel Corporation
Device: 4th Gen Core Processor DRAM Controller
BAR0: 0x0
IRQ: 0
--------------------------------
Bus: 00, Device: 01, Function: 0
Device ID: 8086:31e6
Class: PCI bridge
Vendor: Intel Corporation
Device: 4th Gen Core Processor PCIe Root Port 1
BAR0: 0x0
IRQ: 16
--------------------------------
Bus: 01, Device: 00, Function: 0
Device ID: 1af4:1000
Class: Network controller
Vendor: Red Hat, Inc.
Device: VirtIO network card
BAR0: 0xfebfc000
IRQ: 19
--------------------------------
...
Windows 下的 PCI 编程
在 Windows 下,原理类似,但 API 完全不同。
- 内核驱动:使用 Windows Driver Model (WDM) 或 Windows Driver Framework (WDF),通过
Zw*系列函数(如ZwReadFile,ZwWriteFile)与用户态通信,或者直接使用WdfIoTargetSendInternalIoctlSynchronously等函数,访问硬件通常通过READ_PORT_UCHAR,WRITE_PORT_UCHAR等函数,但这只能在驱动中完成。 - 用户态应用程序:使用 SetupAPI 来枚举和获取设备信息(如硬件 ID、实例路径),或者,如果你有一个内核驱动提供了接口,你可以使用 DeviceIoControl 函数与驱动通信,间接控制硬件。
Windows 用户态示例 (使用 SetupAPI):
这个 C 程序会枚举所有 PCI 设备。
#include <windows.h>
#include <setupapi.h>
#include <tchar.h>
#include <stdio.h>
#pragma comment(lib, "setupapi.lib")
int main()
{
HDEVINFO hDevInfo;
SP_DEVINFO_DATA DeviceInfoData;
DWORD i;
// 1. 获取所有 PCI 设备的设备信息集
// "PCI" 是一个已知的类名
hDevInfo = SetupDiGetClassDevs(&GUID_DEVCLASS_PCI, NULL, NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
if (hDevInfo == INVALID_HANDLE_VALUE) {
printf("SetupDiGetClassDevs failed with error: %d\n", GetLastError());
return 1;
}
// 2. 遍历设备信息集中的每个设备
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &DeviceInfoData); i++)
{
TCHAR szHardwareID[MAX_PATH];
DWORD dwSize = MAX_PATH;
// 3. 获取设备的硬件 ID
if (SetupDiGetDeviceRegistryProperty(hDevInfo, &DeviceInfoData, SPDRP_HARDWAREID,
NULL, (PBYTE)szHardwareID, dwSize, &dwSize))
{
_tprintf(_T("Device %d Hardware ID: %s\n"), i, szHardwareID);
}
}
// 5. 清理
SetupDiDestroyDeviceInfoList(hDevInfo);
return 0;
}
| 层面 | 操作系统 | 核心方法 | 适用场景 |
|---|---|---|---|
| 内核驱动 | Linux | struct pci_dev, pci_register_driver, ioremap, readl/writel |
编写网卡、显卡、声卡等硬件的驱动程序。 |
| Windows | WDF/WDM, READ_PORT_UCHAR, DeviceIoControl |
编写 Windows 平台的硬件驱动。 | |
| 用户空间 | Linux | /proc/bus/pci (过时), libpci 库 (lspci 的实现) |
查询硬件信息、设备监控、诊断工具。 |
| Windows | SetupAPI, DeviceIoControl (与通信) |
查询硬件信息、开发设备管理软件、与自定义驱动通信。 |
如果你想在 C 语言中“编程 PCI”,首先要明确你的目标。
- 如果你想让硬件工作(写一个网卡驱动),你需要编写内核驱动,这非常复杂,需要对操作系统内核和硬件有深入的了解。
- 如果你想查看或管理 PCI 设备(写一个类似
lspci的工具),你应该在用户空间使用libpci(Linux) 或SetupAPI(Windows) 这样的库,这是相对安全且容易实现的。
