4. 错误码 -1 (EPERM):操作不被允许,权限不足或设备未就绪

在 PCIe BAR 空间操作中,EPERM(Error PERMission,错误码 -1)是驱动开发者最常遇到的“硬拒绝”信号。它意味着内核或硬件明确拒绝了当前操作,原因通常归结为权限不足或设备未就绪。与资源不足(ENOMEM)不同,EPERM 往往指向逻辑层面的约束,而非物理资源耗尽。

核心认知: 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 很可能由设备未就绪引起。
EPERM 触发路径与调试决策树 BAR 操作返回 EPERM 权限不足 用户态缺 CAP_SYS_RAWIO 内核态配置空间锁定 设备未就绪 D3cold 电源状态 链路 DL_Down 检查 /proc/self/status CapEff 检查 lspci -vvv LinkSta/DevSta 确认后重试或恢复设备 提示:EPERM 是保护性错误,优先排查权限和电源状态,而非直接怀疑硬件损坏

4.4 调试思路与工具链

面对 EPERM,建议按照以下步骤进行排查:

  1. 确认操作上下文:区分是用户态还是内核态操作。用户态优先检查 CAP_SYS_RAWIO 和 /dev/mem 权限。
  2. 检查设备电源状态:通过 cat /sys/bus/pci/devices/<domain:bus:dev.func>/power_state 查看当前 D-State。
  3. 验证链路完整性:使用 setpci -s <bus:dev.func> CAP_EXP+0x10.L 读取链路状态寄存器。
  4. 检查内核日志:dmesg | grep -i "EPERM\|permission\|BAR" 可能包含更详细的拒绝原因。
  5. 尝试恢复操作:对于 D3cold 设备,先执行 pci_set_power_state(dev, PCI_D0) 唤醒;对于链路问题,尝试 pci_reset_bus()。
实战案例: 某 NVMe 驱动在系统休眠唤醒后访问 BAR 返回 EPERM。调试发现设备在休眠时进入 D3cold,但驱动在 resume 回调中未正确调用 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 驱动开发中“最友好”的错误之一——它明确告诉你操作被拒绝,而不是静默失败或产生不可预测行为。理解其背后的权限模型和设备状态机,是写出健壮驱动的关键一步。