4. 错误码 -1 (EPERM):操作不被允许,权限不足或设备未就绪
在 PCIe BAR 空间操作中,EPERM(Error PERMission,错误码 -1)是驱动开发者最常遇到的“硬拒绝”信号。它意味着内核或硬件明确拒绝了当前操作,原因通常归结为权限不足或设备未就绪。与资源不足(ENOMEM)不同,EPERM 往往指向逻辑层面的约束,而非物理资源耗尽。
4.1 错误码定义与典型场景
EPERM 在 Linux 内核中的定义位于 include/uapi/asm-generic/errno-base.h,其值为 1,表示“Operation not permitted”。在 PCIe 驱动上下文中,触发 EPERM 的典型场景包括:
- 用户态直接访问 BAR 空间:通过
mmap映射 BAR 后,尝试ioread32/iowrite32但未获得 CAP_SYS_RAWIO 权限。 - 设备处于 D3cold 状态:BAR 空间对应的电源域未上电,硬件无法响应事务。
- PCIe 配置空间锁定:某些关键寄存器(如 Base Address Register 本身)被 BIOS 或内核锁定,写操作被拒绝。
- IOMMU/SMMU 映射未建立:DMA 重映射层未允许当前进程访问该 BAR 区域。
- 驱动未绑定或设备被移除:操作时设备已从总线逻辑上断开。
| 触发场景 | 典型错误上下文 | 直接原因 |
|---|---|---|
| 用户态 mmap + 直接 I/O | ioread32() 返回 -1 |
缺少 CAP_SYS_RAWIO 或 /dev/mem 权限 |
| 设备 D3cold 下访问 BAR | pci_read_config_dword() 返回 -1 |
电源管理状态未恢复 |
| 写 BAR 寄存器被拒 | pci_write_config_dword() 返回 -1 |
配置空间写保护或锁定 |
| IOMMU 未映射 | dma_map_single() 返回错误 |
IOMMU 域未授权该物理地址 |
| 设备热拔出 | pci_ioremap_bar() 返回 NULL |
设备已从 PCI 总线移除 |
4.2 权限不足:用户态与内核态的分界
用户态程序通过 mmap 映射 PCIe BAR 空间后,获得的是一段用户虚拟地址。直接对该地址进行读写操作时,内核会检查当前进程是否拥有 CAP_SYS_RAWIO 能力。若没有,则返回 EPERM。
mmap 成功就代表可以自由访问 BAR。实际上,mmap 只建立页表映射,真正的访问权限检查发生在第一次读写触发缺页异常时。如果此时权限不足,缺页处理程序会返回 SIGSEGV 或 EPERM。
内核态驱动同样可能遇到 EPERM:当驱动尝试通过 pci_write_config_dword() 修改 BAR 寄存器时,如果该配置空间被标记为只读(例如 BIOS 已锁定),内核 PCI 子系统会直接返回 EPERM,不会发送配置写事务到总线。
// 用户态示例:缺少 CAP_SYS_RAWIO 时触发 EPERM
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("open /dev/mem");
return -1;
}
void *bar_vaddr = mmap(NULL, bar_size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, bar_phys_addr);
if (bar_vaddr == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
// 以下操作可能返回 EPERM
uint32_t val = ioread32(bar_vaddr); // 若权限不足,返回 -1 并设置 errno = EPERM
4.3 设备未就绪:电源状态与链路状态
PCIe 设备可能处于多种低功耗状态(D1、D2、D3hot、D3cold)。当设备处于 D3cold 时,其主电源被切断,BAR 空间对应的寄存器不再有效。此时任何访问都会返回 EPERM,因为硬件无法完成事务。
链路状态(Link Status)也会影响 BAR 访问。如果链路处于 DL_Down 状态(例如物理链路断开、训练失败),配置空间和 BAR 空间均不可访问。
lspci -vvv -s <bus:dev.func> 查看设备状态。如果 DevSta 中的 Transactions Pending 位被置位,或 LinkSta 显示 Link not active,则 EPERM 很可能由设备未就绪引起。
4.4 调试思路与工具链
面对 EPERM,建议按照以下步骤进行排查:
- 确认操作上下文:区分是用户态还是内核态操作。用户态优先检查
CAP_SYS_RAWIO和/dev/mem权限。 - 检查设备电源状态:通过
cat /sys/bus/pci/devices/<domain:bus:dev.func>/power_state查看当前 D-State。 - 验证链路完整性:使用
setpci -s <bus:dev.func> CAP_EXP+0x10.L读取链路状态寄存器。 - 检查内核日志:
dmesg | grep -i "EPERM\|permission\|BAR"可能包含更详细的拒绝原因。 - 尝试恢复操作:对于 D3cold 设备,先执行
pci_set_power_state(dev, PCI_D0)唤醒;对于链路问题,尝试pci_reset_bus()。
pci_set_power_state 恢复 D0 状态。修复后 EPERM 消失。
4.5 避免 EPERM 的最佳实践
| 实践建议 | 具体措施 | 预期效果 |
|---|---|---|
| 用户态访问前提升权限 | 以 root 运行,或使用 setcap cap_sys_rawio+ep 赋予二进制文件 |
避免 mmap 后读写返回 EPERM |
| 内核态操作前检查设备状态 | 调用 pci_is_enabled() 和 pci_dev_is_present() |
防止对已移除或未使能设备操作 |
| 电源管理回调完整实现 | 在 resume 中恢复 D0 并重新 ioremap BAR | 避免 D3cold 导致的 EPERM |
| 配置空间操作前解锁 | 检查 BIOS 设置,或使用 pci_cfg_access_lock() 系列函数 |
确保写配置空间不被拒绝 |
| IOMMU 映射提前建立 | 在 probe 阶段完成 dma_set_mask_and_coherent() |
避免 DMA 映射时权限拒绝 |
EPERM 是 PCIe 驱动开发中“最友好”的错误之一——它明确告诉你操作被拒绝,而不是静默失败或产生不可预测行为。理解其背后的权限模型和设备状态机,是写出健壮驱动的关键一步。