在Linux系统中,PCI(Peripheral Component Interconnect)设备是最常见的硬件设备之一,如显卡、网卡、磁盘控制器等,要对PCI设备进行读写操作,需要理解Linux内核对PCI设备的管理机制,包括设备发现、资源分配、地址映射等核心步骤,本文将详细说明Linux环境下读写PCI设备的方法、工具及驱动开发实践。
Linux中PCI设备的基础表示
Linux内核通过PCI子系统管理所有PCI设备,每个PCI设备在内核中由pci_dev
结构体表示,其唯一标识符为“总线号:设备号:功能号”(如0000:00:1f.6),用户可通过/sys
文件系统查看PCI设备信息,
/sys/bus/pci/devices/
:列出所有PCI设备,每个设备目录包含vendor
(厂商ID)、device
(设备ID)、resource
(资源信息)等文件。/sys/bus/pci/drivers/
:显示已加载的PCI驱动程序。
命令行工具lspci
(需安装pciutils
包)是查看PCI设备的常用工具,例如lspci -v
可显示设备的详细信息(包括驱动、资源占用等),lspci -nn
可显示厂商和设备ID(格式为vendor:device
,如8086:1528
)。
PCI设备的配置空间读写
每个PCI设备都有256字节的配置空间,用于存储设备信息(如厂商ID、设备ID、BAR寄存器等),Linux提供了多种方式访问配置空间:
用户空间工具
setpci
:用于读写配置空间寄存器,读取设备0000:00:1f.6的 Vendor ID:setpci -s 0000:00:1f.6 00.w # 00.w 表示读取16位Vendor ID(0x00地址)
修改配置寄存器时需谨慎,例如禁用设备:
setpci -s 0000:00:1f.6 04.w=0x0000 # 将命令寄存器(0x04地址)置0
内核空间访问
在驱动开发中,可通过pci_read_config_word()
、pci_write_config_dword()
等函数读写配置空间,
u16 vendor_id; pci_read_config_word(dev, PCI_VENDOR_ID, &vendor_id); // 读取Vendor ID u32 bar_val; pci_write_config_dword(dev, PCI_BASE_ADDRESS_0, bar_val); // 写入BAR0
PCI设备的内存与I/O端口读写
PCI设备通过两种方式与CPU交互:内存映射I/O(MMIO)和端口I/O(PIO),现代PCI设备主要使用MMIO,传统设备(如ISA兼容设备)可能使用PIO。
内存映射I/O(MMIO)
MMIO将设备的寄存器或缓冲区映射到物理内存地址,通过读写内存地址访问设备,操作步骤如下:
(1)获取设备资源信息
通过lspci -v
可查看设备的内存区域(如“Memory at f0000000 (32-bit, non-prefetchable)”),其中f0000000
为基地址,size
为区域大小,内核中可通过pci_resource_start()
和pci_resource_len()
获取基地址和长度。
(2)映射内存到用户空间
用户空间可通过/dev/mem
设备文件将物理内存映射到进程地址空间,
#include <sys/mman.h> #include <fcntl.h> int fd = open("/dev/mem", O_RDWR); // 打开/dev/mem if (fd < 0) { /* 错误处理 */ } // 假设设备基地址为0xf0000000,大小为0x1000 void *virt_addr = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0xf0000000); if (virt_addr == MAP_FAILED) { /* 错误处理 */ } // 读写设备寄存器(如32位寄存器) volatile uint32_t *reg = (volatile uint32_t *)virt_addr; *reg = 0x12345678; // 写入 uint32_t val = *reg; // 读取 munmap(virt_addr, 0x1000); close(fd);
注意:直接操作/dev/mem
需要root权限,且可能存在安全风险(如误改系统内存),推荐通过内核驱动进行访问。
(3)内核空间映射
在驱动中,可通过request_mem_region()
申请内存资源,再用ioremap()
映射到虚拟地址:
void __iomem *virt_addr; resource_size_t start = pci_resource_start(dev, 0); // BAR0基地址 resource_size_t len = pci_resource_len(dev, 0); if (!request_mem_region(start, len, "my_pci_driver")) { /* 申请失败 */ return -EBUSY; } virt_addr = ioremap(start, len); if (!virt_addr) { release_mem_region(start, len); return -ENOMEM; } // 读写寄存器 writel(0x12345678, virt_addr); // 写32位 uint32_t val = readl(virt_addr); // 读32位 iounmap(virt_addr); release_mem_region(start, len);
端口I/O(PIO)
PIO通过特定的I/O端口地址访问设备,使用inb
、outb
等指令(x86架构),用户空间需使用iopl()
或ioperm()
获取权限,
#include <sys/io.h> if (iopl(3) != 0) { /* 获取最高I/O权限 */ } // 需root权限 // 读写端口0x3f8(串口COM1) outb(0x41, 0x3f8); // 写入字节0x41到端口0x3f8 uint8_t val = inb(0x3f8); // 读取端口0x3f8的值 iopl(0); // 释放权限
注意:PIO在现代x86系统中已较少使用,且64位架构下用户空间直接访问I/O端口受限,通常需通过内核驱动。
内存映射与I/O端口对比
特性 | 内存映射I/O(MMIO) | 端口I/O(PIO) |
---|---|---|
访问对象 | 物理内存地址 | I/O端口地址(0-0xFFFF) |
权限要求 | /dev/mem 需root,内核需申请资源 |
需iopl() /ioperm() 权限 |
访问速度 | 较快(通过内存总线) | 较慢(专用I/O指令) |
适用场景 | 现代PCI设备(显卡、网卡等) | 传统设备(串口、并口等) |
用户空间函数 | mmap() + 指针读写 |
inb() /outb() (需iopl) |
驱动开发中的关键步骤
若需长期稳定访问PCI设备,建议编写内核驱动,核心步骤如下:
- 注册PCI驱动:定义
pci_driver
结构体,实现probe()
和remove()
回调函数。 - 设备匹配:在
probe()
中通过pci_match_device()
匹配设备。 - 启用设备:调用
pci_enable_device()
设置设备状态。 - 申请资源:
request_mem_region()
(MMIO)或request_region()
(PIO)。 - 映射内存:
ioremap()
将物理地址映射为内核虚拟地址。 - 读写操作:使用
readl()
/writel()
(MMIO)或inb()
/outb()
(PIO)访问设备。 - 释放资源:在
remove()
中调用iounmap()
、release_mem_region()
等。
注意事项
- 权限与安全:直接操作
/dev/mem
可能导致系统崩溃,生产环境建议通过驱动访问。 - 资源冲突:通过
/proc/iomem
检查内存区域是否被占用,避免冲突。 - 大小端转换:若设备与CPU字节序不同(如网络设备),需使用
cpu_to_le32()
等函数转换数据。 - DMA缓冲区:对于需要高速数据传输的设备(如网卡),需使用
dma_alloc_coherent()
分配DMA缓冲区。
FAQs
Q1:普通用户如何安全读写PCI设备,而不需要root权限?
A:可通过udev
规则创建设备节点,并设置用户权限,为特定PCI设备(如Vendor ID=8086, Device ID=1528)创建/dev/my_pci
节点:
- 创建
/etc/udev/rules.d/99-my_pci.rules
为:SUBSYSTEM=="pci", ATTR{vendor}=="0x8086", ATTR{device}=="0x1528", GROUP="users", MODE="0664"
- 执行
udevadm control --reload-rules && udevadm trigger
使规则生效。 - 在用户空间通过
/dev/mem
或自定义驱动访问设备(需确保驱动支持非特权访问)。
Q2:驱动开发中,调用request_mem_region()
失败的原因及解决方法?
A:常见原因及解决方案如下:
- 资源已被占用:通过
/proc/iomem
查看内存区域是否被其他驱动使用,检查设备是否有多个功能或驱动冲突。 - 基地址错误:确认
pci_resource_start()
获取的BAR寄存器值正确(如BAR0对应resource[0]
),避免使用无效地址。 - 权限不足:内核模块需以root权限加载,或检查
MODULE_LICENSE()
是否声明(如MODULE_LICENSE("GPL")
)。 - 设备未启用:调用
pci_enable_device()
后再申请资源,确保设备处于正常状态。
原创文章,发布者:酷番叔,转转请注明出处:https://cloud.kd.cn/ask/16297.html