PCI / PCIe 서브시스템
PCI/PCIe 서브시스템을 하드웨어 링크 계층부터 Linux 커널 드라이버 통합 지점까지 end-to-end로 정리합니다. config space/BAR/MSI-X 같은 기본 메커니즘, 열거와 리소스 할당, IOMMU 연계 DMA 경로, AER 오류 보고와 복구, ASPM/D-state 전원관리, SR-IOV 및 VF 분리 운용, hotplug와 리셋 플로우, sysfs/lspci/setpci/tracepoint 기반 문제 분석까지 서버와 임베디드 환경 모두에서 필요한 실전 포인트를 다룹니다.
핵심 요약
- PCI Topology — Bus/Device/Function(BDF) 3계층 주소 체계로 장치를 식별합니다.
lspci -tv로 트리 구조를 확인할 수 있습니다. - BAR (Base Address Register) — PCI 설정 공간(Configuration Space)의 BAR 레지스터가 장치의 MMIO/IO 포트 주소 범위를 정의하며, 커널이 부팅 시 자원을 할당합니다.
- MSI/MSI-X Interrupts — 전통적 INTx 핀 대신 메시지 기반 인터럽트를 사용하여 인터럽트 라우팅을 단순화하고, 멀티 큐 장치에서 CPU별 인터럽트 분배를 가능하게 합니다.
- PCI Configuration Space — 256바이트(PCI) 또는 4KB(PCIe) 설정 공간으로, Vendor/Device ID, Command/Status, Capability 리스트 등 장치 메타데이터를 보유합니다.
- pci_driver Model —
pci_register_driver()로 등록하며,id_table매칭 후probe()가 호출되는 리눅스 디바이스 모델 기반 드라이버 구조입니다. - DMA와 IOMMU — PCI 장치의 DMA 접근을 IOMMU가 주소 변환·격리하여 보안과 가상화를 지원합니다.
dma_map_single()/dma_alloc_coherent()가 핵심 API입니다. - PCIe Link Training — PCIe 장치 연결 시 속도·레인 수 협상 과정으로,
lspci -vv의 LnkSta에서 현재 링크 상태를 확인합니다. - SR-IOV — Single Root I/O Virtualization으로, 하나의 물리 장치(PF)에서 여러 가상 장치(VF)를 생성하여 VM에 직접 할당할 수 있습니다.
단계별 이해
- PCI 토폴로지와 열거(Enumeration) 이해 — 부팅 시 커널이 PCI 버스를 재귀 탐색하여 모든 장치를 발견하고 자원을 할당하는 과정을 파악합니다.
lspci -tv로 버스 트리를 확인하고,lspci -vvv -s BDF로 개별 장치의 설정 공간과 Capability를 살펴봅니다. - BAR 할당과 MMIO 매핑 추적 — 커널이 BAR에 물리 주소를 할당하고, 드라이버가
pci_iomap()/pci_resource_start()로 접근하는 흐름을 따라갑니다./proc/iomem에서 PCI 장치에 할당된 메모리 범위를 확인할 수 있습니다. - pci_driver 등록과 probe/remove 흐름 파악 —
pci_register_driver()→id_table매칭 →probe()호출 순서를 코드 수준에서 추적합니다.pci_enable_device(),pci_request_regions(),pci_set_master()등 probe 내 필수 초기화 단계를 확인합니다. - MSI/MSI-X 설정과 인터럽트 처리 —
pci_alloc_irq_vectors()로 MSI-X 벡터를 할당하고, 멀티 큐 장치에서 CPU 친화도를 설정하는 방법을 익힙니다./proc/interrupts에서 PCI 장치 인터럽트 분포를 확인하고,irqbalance동작과 비교합니다. - DMA 매핑과 IOMMU 연동 점검 — streaming DMA와 coherent DMA의 차이를 이해하고, IOMMU가 활성화된 환경에서 주소 변환 경로를 확인합니다.
dmesg | grep -i iommu로 IOMMU 그룹 할당을 확인하고, SR-IOV VF의 IOMMU 격리 상태를 점검합니다.
PCI/PCIe 개요
PCI (Peripheral Component Interconnect)는 1992년 Intel이 제안한 로컬 버스 규격으로, 이후 PCI Express (PCIe)로 진화하며 현대 시스템의 사실상 표준 인터커넥트가 되었습니다. Linux 커널은 PCI 서브시스템을 통해 디바이스 열거(enumeration), 리소스 할당, 드라이버 바인딩, 전원 관리(Power Management)를 통합적으로 처리합니다.
| 규격 | 연도 | 토폴로지(Topology) | 최대 대역폭 (단방향) |
|---|---|---|---|
| PCI 2.x | 1993 | 공유 병렬 버스 (32/64-bit) | 133 / 533 MB/s |
| PCI-X 2.0 | 2003 | 공유 병렬 버스 (64-bit) | 4.3 GB/s (DDR 533) |
| PCIe 1.0 | 2003 | Point-to-point 직렬 | 250 MB/s × lane |
| PCIe 2.0 | 2007 | Point-to-point 직렬 | 500 MB/s × lane |
| PCIe 3.0 | 2010 | Point-to-point 직렬 | ~1 GB/s × lane |
| PCIe 4.0 | 2017 | Point-to-point 직렬 | ~2 GB/s × lane |
| PCIe 5.0 | 2019 | Point-to-point 직렬 | ~4 GB/s × lane |
| PCIe 6.0 | 2022 | Point-to-point 직렬 | ~8 GB/s × lane (PAM4) |
PCIe 아키텍처
토폴로지
PCIe는 기존 PCI의 공유 병렬 버스를 point-to-point 직렬 링크로 대체합니다. 트리 구조의 핵심 구성 요소:
| 구성 요소 | 역할 | Config Header |
|---|---|---|
| Root Complex (RC) | CPU/메모리 ↔ PCIe 패브릭 연결, 최상위 노드 | Type 0/1 |
| Switch | 업스트림 포트 1개 + 다운스트림 포트 N개, 패킷(Packet) 라우팅(Routing) | Type 1 (Bridge) |
| Endpoint | NVMe, NIC, GPU 등 최종 디바이스 | Type 0 |
| Bridge | PCI/PCI-X ↔ PCIe 변환 | Type 1 |
PCIe 계층 구조
PCIe는 네트워크 프로토콜과 유사한 3계층 모델을 사용합니다:
| 계층 | 역할 | 주요 단위 |
|---|---|---|
| Transaction Layer | 읽기/쓰기 요청, Completion, 메시지 생성 | TLP (Transaction Layer Packet) |
| Data Link Layer | 시퀀스 번호, CRC, ACK/NAK 기반 신뢰성 보장 | DLLP (Data Link Layer Packet) |
| Physical Layer | 전기 신호, 인코딩 (8b/10b, 128b/130b, PAM4), 레인 본딩(Bonding) | Ordered Set |
TLP (Transaction Layer Packet)
TLP는 PCIe 통신의 핵심 단위입니다. 주요 TLP 타입:
| TLP 타입 | 약어 | 용도 |
|---|---|---|
| Memory Read | MRd | MMIO 읽기, DMA 읽기 |
| Memory Write | MWr | MMIO 쓰기, DMA 쓰기 |
| Configuration Read/Write | CfgRd/CfgWr | Configuration Space 접근 |
| I/O Read/Write | IORd/IOWr | 레거시 I/O 포트 접근 |
| Completion | Cpl/CplD | 읽기 요청에 대한 응답 (데이터 포함) |
| Message | Msg/MsgD | 인터럽트(MSI), 전원, 에러 시그널(Signal)링 |
TLP 헤더 포맷 상세
TLP 헤더는 3 DW (12 바이트) 또는 4 DW (16 바이트)로 구성됩니다. 첫 번째 DW의 Fmt[2:0] 필드가 헤더 길이와 데이터 유무를, Type[4:0] 필드가 트랜잭션 종류를 결정합니다:
| Fmt[2:0] | 의미 | 헤더 | 데이터 | 예시 |
|---|---|---|---|---|
000 | 3DW, no data | 12B | 없음 | MRd (32-bit), CfgRd, IORd |
001 | 4DW, no data | 16B | 없음 | MRd (64-bit) |
010 | 3DW, with data | 12B | 있음 | MWr (32-bit), CfgWr, IOWr |
011 | 4DW, with data | 16B | 있음 | MWr (64-bit) |
100 | TLP Prefix | — | — | PCIe 3.0+ TLP Prefix |
Requester ID / Completer ID
TLP 라우팅의 핵심 식별자는 16비트 BDF (Bus:Device:Function)입니다:
| 필드 | 비트 | 설명 |
|---|---|---|
| Bus Number | [15:8] | 버스 번호 (0~255) |
| Device Number | [7:3] | 디바이스 번호 (0~31) |
| Function Number | [2:0] | 펑션 번호 (0~7) |
- Requester ID: TLP를 발행한 디바이스의 BDF. Non-Posted 트랜잭션에서 Completion을 돌려받을 대상을 식별합니다.
- Completer ID: Completion TLP에 포함되어 어느 디바이스가 응답했는지 표시합니다.
- Tag: 동일 Requester가 여러 Non-Posted 요청을 동시 발행할 때 각 요청을 구분하는 8/10/14비트 식별자입니다.
Extended Tag(PCIe 2.0, 10비트)와14-bit Tag(PCIe 6.0)가 미해결 요청 수를 확장합니다.
크레딧 기반 흐름 제어 (Flow Control)
PCIe는 수신 측 버퍼 오버플로(Buffer Overflow)우를 방지하기 위해 크레딧 기반 흐름 제어를 사용합니다. 송신 측은 수신 측에서 통지한 크레딧만큼만 TLP를 전송할 수 있으며, 수신 측이 버퍼(Buffer)를 비운 후 UpdateFC DLLP로 크레딧을 반환합니다.
| 크레딧 타입 | 헤더 단위 | 데이터 단위 | 설명 |
|---|---|---|---|
| Posted (P) | 1 크레딧 = 1 TLP 헤더 슬롯 | 1 크레딧 = 16 바이트 (4 DW) | MWr — 완료 응답 불필요, 크레딧만 있으면 즉시 전송 |
| Non-Posted (NP) | 1 크레딧 = 1 TLP 헤더 슬롯 | 1 크레딧 = 16 바이트 | MRd, CfgRd/Wr — Completion 대기, 크레딧 + outstanding request 제한 |
| Completion (Cpl) | 1 크레딧 = 1 TLP 헤더 슬롯 | 1 크레딧 = 16 바이트 | CplD — 무한 크레딧(infinite credits) 가능 (송신 측이 이미 NP 크레딧으로 조절됨) |
lspci -vvv의 DevSta: CorrErr+가 빈번하다면 흐름 제어 문제를 의심할 수 있습니다. 특히 NP 크레딧 부족은 MMIO 읽기(uncacheable) 지연으로 이어지며, 드라이버에서 readl() 호출이 수 마이크로초 이상 블로킹될 수 있습니다.가상 채널과 트래픽 클래스 (VC/TC)
PCIe는 QoS 지원을 위해 TC (Traffic Class, 0~7)와 VC (Virtual Channel, 0~7)를 정의합니다. TC-to-VC 매핑(Mapping)은 VC Capability에서 설정하며, 각 VC는 독립적인 크레딧과 버퍼를 가집니다:
| TC | 기본 VC 매핑 | 용도 |
|---|---|---|
| 0 | VC0 | 일반 트래픽 (기본, 모든 디바이스 필수 지원) |
| 1~7 | VC1~VC7 (선택) | 실시간(Real-time) 오디오/비디오, 등시성(Isochronous) 전송 등 |
pci_enable_vc()로 추가 VC를 활성화할 수 있지만, 일반적인 서버/데스크탑 환경에서는 거의 사용되지 않습니다.Data Link Layer — DLLP와 ACK/NAK
Data Link Layer는 TLP의 신뢰성 보장을 담당합니다. 모든 TLP에 시퀀스 번호와 LCRC를 추가하고, ACK/NAK 프로토콜로 전송 오류를 복구합니다:
| DLLP 타입 | 용도 | 설명 |
|---|---|---|
| Ack | TLP 수신 확인 | 정상 수신된 TLP의 시퀀스 번호를 확인, 송신 측 replay 버퍼 해제 |
| Nak | TLP 재전송(Retransmission) 요청 | CRC 에러 감지 시 Nak 전송, 송신 측은 replay 버퍼에서 재전송 |
| InitFC1/InitFC2 | 초기 크레딧 교환 | 링크 초기화 시 수신 버퍼 크기를 상대에게 광고 |
| UpdateFC | 크레딧 갱신 | 수신 버퍼 소비 후 사용 가능한 크레딧을 송신 측에 통지 |
| PM (Power Mgmt) | 전원 관리 | PM_Enter_L1, PM_Request_Ack 등 ASPM 상태 전환 |
| Vendor Specific | 벤더 확장 | 벤더 정의 DLLP |
AER의 Correctable Error 카운터(Bad TLP, Bad DLLP, Replay Number Rollover)가 증가합니다.LTSSM (Link Training and Status State Machine)
PCIe 물리 계층의 링크 초기화와 속도 협상은 LTSSM이 관리합니다. 링크 업부터 전원 절약 상태까지 모든 전환을 11개 주요 상태로 모델링합니다:
PCIe 등화 (Equalization) — Gen3+
PCIe Gen3 (8 GT/s) 이상에서는 고속 신호의 ISI(Inter-Symbol Interference)를 보상하기 위해 송수신 등화(Equalization)가 필수입니다:
| 등화 단계 | Phase | 설명 |
|---|---|---|
| TX De-emphasis | Phase 0/1 | 송신 측이 11개 프리셋(P0~P10) 중 하나를 선택하여 고주파 신호를 강조. 커서 전/후 비율 조정 |
| RX Adaptation | Phase 2/3 | 수신 측이 CTLE(Continuous Time Linear Equalizer)/DFE(Decision Feedback Equalizer)를 조정하여 eye 다이어그램 최적화 |
| Speed Change | — | 등화 완료 후 Recovery → L0로 전환, 협상된 최고 속도로 동작 시작 |
lspci -vvv에서 LnkCap: Speed 16GT/s인데 LnkSta: Speed 8GT/s이면 등화 실패를 의심하세요. 물리적 원인(케이블 품질, PCB 트레이스 길이, 커넥터 접촉)이 대부분이며, 커널 파라미터 pci=noaer로 AER를 비활성화하면 등화 재시도 로그가 사라져 근본 원인을 놓칠 수 있으므로 주의가 필요합니다.PCIe 세대별 인코딩과 Gen6 FLIT 모드
| 세대 | 전송률 | 인코딩 | 인코딩 오버헤드(Overhead) | 유효 대역폭 (×1) | 유효 대역폭 (×16) |
|---|---|---|---|---|---|
| Gen1 | 2.5 GT/s | 8b/10b | 20% | 250 MB/s | 4 GB/s |
| Gen2 | 5 GT/s | 8b/10b | 20% | 500 MB/s | 8 GB/s |
| Gen3 | 8 GT/s | 128b/130b | ~1.5% | ~985 MB/s | ~15.75 GB/s |
| Gen4 | 16 GT/s | 128b/130b | ~1.5% | ~1.97 GB/s | ~31.5 GB/s |
| Gen5 | 32 GT/s | 128b/130b | ~1.5% | ~3.94 GB/s | ~63 GB/s |
| Gen6 | 64 GT/s | PAM4 + 1b/1b (FLIT) | ~3% (FEC + FLIT) | ~7.56 GB/s | ~121 GB/s |
| Gen7 | 128 GT/s | PAM4 + FLIT | ~3% | ~15.1 GB/s | ~242 GB/s |
- FLIT 크기: 256 바이트 고정. 여러 TLP를 하나의 FLIT에 패킹 가능 (작은 TLP의 효율 향상)
- PAM4 (Pulse Amplitude Modulation 4-level): NRZ(2레벨) 대신 4레벨 신호로 레인당 2비트/심볼 전송. 동일 주파수에서 2배 대역폭
- FEC (Forward Error Correction): PAM4의 낮은 SNR을 보완하기 위해 필수. CRC-3 + Reed-Solomon 같은 FEC 코드 사용
- L0p (Low Power): Gen6 전용 저전력 상태. 레인 수를 동적으로 줄여 (예: x16 → x8 → x4) 유휴 시 전력 절약. 기존 ASPM L0s보다 세밀한 제어
- CXL 3.0과의 관계: CXL 3.0은 PCIe 6.0의 256B FLIT과 별도의 CXL 68B FLIT 모두 지원하여 코히런시 프로토콜과 I/O를 다중화(Multiplexing)
PCIe 6.0 FLIT 모드
PCIe 6.0(Gen6)은 기존 TLP 바이트 스트림 프레이밍(Framing)을 FLIT(Flow Control Unit) 기반 전송으로 대체합니다. 이는 PCIe 도입 이후 가장 큰 프로토콜 변화입니다.
256바이트 FLIT 구조
하나의 FLIT는 고정 256바이트로 구성되며, 기존의 가변 길이 TLP/DLLP 프레이밍 대신 일정한 전송 단위를 사용합니다:
PCIe 5.0 vs 6.0 비교
| 항목 | PCIe 5.0 (Gen5) | PCIe 6.0 (Gen6) |
|---|---|---|
| 전송률 | 32 GT/s | 64 GT/s |
| 신호 방식 | NRZ (2레벨) | PAM4 (4레벨) |
| 인코딩 | 128b/130b | 1b/1b (FLIT 기반) |
| 전송 단위 | 가변 길이 TLP + STP/END 프레이밍 | 고정 256바이트 FLIT |
| 오류 정정 | CRC + DLLP ACK/NAK (재전송) | CRC + FEC (정방향 정정) + ACK/NAK |
| 유효 대역폭 (×1) | ~3.94 GB/s | ~7.56 GB/s |
| 유효 대역폭 (×16) | ~63 GB/s | ~121 GB/s |
| 저전력 모드 | ASPM L0s/L1 | L0p (레인 수 동적 축소) |
lspci에서 64 GT/s 표시), L0p 저전력 상태 지원, FEC 관련 카운터 노출 등입니다. Config Space 접근이나 드라이버 API에는 영향이 없습니다.Configuration Space
접근 방식
PCI Configuration Space에 접근하는 두 가지 메커니즘:
| 방식 | 포트 / 메모리 | 공간 크기 | 설명 |
|---|---|---|---|
| I/O Port (CAM) | 0xCF8 (addr) / 0xCFC (data) | 256 바이트 | 레거시 PCI, Bus/Dev/Func 인코딩 |
| MMIO (ECAM) | MCFG ACPI 테이블로 베이스 주소 결정 | 4 KB (PCIe) | PCIe 확장 Config Space (4096 바이트) |
/* I/O 포트 방식 Configuration Space 접근 (레거시 PCI) */
/* CONFIG_ADDRESS (0xCF8):
* [31] Enable bit
* [23:16] Bus number
* [15:11] Device number
* [10:8] Function number
* [7:2] Register number (DWORD-aligned)
* [1:0] 항상 0
*/
static u32 pci_conf1_read(u8 bus, u8 dev, u8 func, u8 reg)
{
u32 addr = (1 << 31) |
((u32)bus << 16) |
((u32)dev << 11) |
((u32)func << 8) |
(reg & 0xFC);
outl(addr, 0xCF8);
return inl(0xCFC);
}
/* ECAM (Enhanced Configuration Access Mechanism)
* MMIO 베이스 + (Bus << 20 | Dev << 15 | Func << 12 | Reg)
* 각 Function에 4 KB 매핑 → 4096 바이트 Config Space 전체 접근 */
코드 설명
- CONFIG_ADDRESS 레지스터레거시 PCI Configuration Mechanism #1에서 사용하는 I/O 포트(
0xCF8) 주소 구성입니다. 비트 31(Enable)을 1로 설정하고, Bus/Device/Function/Register 번호를 각 필드에 조합하여 32비트 주소를 만듭니다. 이 메커니즘은arch/x86/pci/direct.c에 구현되어 있습니다. - outl/inl
outl(addr, 0xCF8)로 접근할 레지스터 주소를 지정하고,inl(0xCFC)로 해당 Configuration 레지스터의 32비트 값을 읽습니다. 이 두 I/O 포트 접근은 원자적이지 않으므로 커널은pci_lock스핀락으로 보호합니다. - ECAMPCIe에서 도입된 Enhanced Configuration Access Mechanism은 MMIO 기반으로 동작합니다. 각 Function에 4 KB를 매핑하여 전체 4096바이트 확장 Config Space에 직접 메모리 접근이 가능합니다. ACPI MCFG 테이블에서 ECAM 베이스 주소를 획득하며,
drivers/pci/ecam.c에서 처리합니다.
ECAM 주소 계산
ECAM(Enhanced Configuration Access Mechanism)은 PCIe Configuration Space 전체(4096 바이트)를 MMIO로 직접 접근할 수 있게 합니다. ACPI MCFG 테이블에서 ECAM 베이스 주소를 획득하며, Bus/Device/Function/Register를 비트 시프트(Bit Shift)로 조합하여 물리 주소를 계산합니다:
cat /sys/firmware/acpi/tables/MCFG | hexdump -C 또는 dmesg | grep ECAM으로 커널이 인식한 ECAM 영역을 확인하세요. 세그먼트(Segment) 그룹을 지원하는 시스템에서는 세그먼트별로 별도의 ECAM 영역이 할당됩니다.
Configuration Header 구조
모든 PCI/PCIe 디바이스는 Configuration Header (처음 64 바이트)를 가집니다:
| Offset | Size | Field |
|---|---|---|
| 00h | 2 | Vendor ID |
| 02h | 2 | Device ID |
| 04h | 2 | Command |
| 06h | 2 | Status |
| 08h | 1 | Revision ID |
| 09h | 3 | Class Code (PI + Sub + Base) |
| 0Ch | 1 | Cache Line Size |
| 0Dh | 1 | Latency Timer |
| 0Eh | 1 | Header Type (00h=Endpoint, 01h=Bridge) |
| 0Fh | 1 | BIST |
| 10h-27h | 24 | BAR 0-5 (Type 0) 또는 BAR 0-1 + Bridge 정보 (Type 1) |
| 28h | 4 | CardBus CIS Pointer / Subsystem IDs |
| 2Ch | 4 | Subsystem Vendor ID / Subsystem ID |
| 30h | 4 | Expansion ROM Base Address |
| 34h | 1 | Capabilities Pointer (Capability 연결 리스트(Linked List) 시작) |
| 3Ch | 4 | Interrupt Line / Pin / Min_Gnt / Max_Lat |
Command 레지스터 (오프셋 04h)
드라이버가 디바이스를 제어하는 핵심 레지스터:
| 비트 | 이름 | 설명 |
|---|---|---|
| 0 | I/O Space | I/O BAR 접근 활성화 |
| 1 | Memory Space | Memory BAR 접근 활성화 |
| 2 | Bus Master | DMA(버스 마스터링) 활성화 |
| 6 | Parity Error Response | 패리티 에러 보고 |
| 8 | SERR# Enable | 시스템 에러 보고 활성화 |
| 10 | INTx Disable | 레거시 INTx 인터럽트 비활성화 (MSI 사용 시) |
Capability 구조
오프셋(Offset) 34h의 Capabilities Pointer부터 시작하는 연결 리스트(linked list)로, 각 Capability는 ID + Next Pointer + 데이터로 구성됩니다:
| Cap ID | 이름 | 설명 |
|---|---|---|
| 01h | Power Management | D0/D1/D2/D3hot 상태 전환 |
| 05h | MSI | Message Signaled Interrupt |
| 10h | PCIe | PCIe Capability (Link/Slot 정보) |
| 11h | MSI-X | 확장 MSI (벡터 테이블 기반) |
PCIe 4 KB 확장 공간(256~4095)에는 Extended Capabilities가 위치합니다:
| Ext Cap ID | 이름 | 설명 |
|---|---|---|
| 0001h | AER | Advanced Error Reporting |
| 0003h | Serial Number | 디바이스 고유 일련번호 |
| 000Dh | ACS | Access Control Services (IOMMU 격리(Isolation)) |
| 0010h | SR-IOV | Single Root I/O Virtualization |
| 001Eh | L1 PM Substates | L1 세부 전원 절약 상태 |
| 0023h | DOE | Data Object Exchange (CXL 등) |
BAR (Base Address Registers)
BAR은 디바이스가 CPU에 노출하는 메모리/I/O 영역의 시작 주소를 정의합니다. Endpoint(Type 0)는 최대 6개, Bridge(Type 1)는 2개의 BAR을 가집니다.
BAR 크기 결정 알고리즘
BAR 크기는 다음 절차로 결정됩니다. 이는 PCI 스펙에 정의된 표준 메커니즘입니다:
/* 커널 내부: BAR 크기 결정 (drivers/pci/probe.c — 간략화) */
static u64 pci_size(u64 base, u64 maxbase, u64 mask)
{
u64 size = mask & maxbase; /* 타입 비트 마스킹 */
if (!size)
return 0;
/* 1의 보수 + 1 = 2의 보수 = 크기
* 예: size = 0xFFF00000 → ~size + 1 = 0x00100000 = 1 MB */
size = (size & ~(size - 1)) - 1; /* 최하위 set 비트 ~ 위 모두 포함 */
if (base == maxbase && ((base | (size - 1)) & mask) != mask)
return 0;
return size + 1;
}
코드 설명
- pci_size()
drivers/pci/probe.c에 정의된 BAR 크기 결정 함수입니다. 커널은 BAR에 전체 1을 기록한 후 다시 읽어서, 하드웨어가 고정한 비트 패턴으로부터 요청 크기를 역산합니다. - mask & maxbase
mask는 BAR 유형 비트(하위 4비트)를 제외하는 마스크입니다.maxbase는 BAR에 올-1을 기록 후 읽은 값이며, 하드웨어가 디코딩하는 비트만 1로 남깁니다. - 2의 보수 변환
(size & ~(size - 1)) - 1은 최하위 set 비트를 분리하여 BAR 영역의 실제 크기를 구합니다. 예를 들어0xFFF00000이면 결과는0x00100000(1 MB)입니다. 이 기법은 PCI 스펙의 BAR 크기 탐색(sizing) 알고리즘을 구현한 것입니다.
BAR 유형과 속성
| BAR 유형 | 비트 [2:1] | 비트 [3] | 비트 [0] | 설명 |
|---|---|---|---|---|
| 32-bit MMIO | 00 | 0/1 | 0 | 4 GB 이하 주소. 단일 BAR 사용 |
| 64-bit MMIO | 10 | 0/1 | 0 | 연속 2개 BAR 사용 (BAR[i] + BAR[i+1]). 4 GB 이상 주소 가능 |
| I/O Port | — | — | 1 | I/O 포트 주소. 최대 64 KB. 레거시, PCIe에서는 비권장 |
- Prefetchable (P=1): 읽기 시 부작용(side-effect)이 없는 영역. CPU가 투기적 읽기(speculative read)와 쓰기 결합(write combining)을 수행할 수 있습니다. 프레임버퍼, DMA 버퍼 등에 사용. Bridge 윈도우에서 별도 prefetchable 윈도우로 라우팅됩니다.
- Non-Prefetchable (P=0): 읽기 시 부작용이 있을 수 있는 영역 (예: 상태 레지스터 읽기가 값을 변경). 반드시 요청한 크기와 주소 그대로 접근해야 합니다. 디바이스 레지스터 영역에 사용.
- 주의: Non-Prefetchable BAR은 32-bit Bridge 윈도우만 통과할 수 있어, 4 GB 이상 주소에 배치가 제한될 수 있습니다. 이는 대용량 메모리 시스템에서 BAR 할당 실패의 원인이 됩니다.
/* 커널에서 BAR 정보 접근 */
struct pci_dev *pdev;
/* BAR의 물리 시작 주소 */
resource_size_t bar_start = pci_resource_start(pdev, 0);
/* BAR 영역 크기 */
resource_size_t bar_len = pci_resource_len(pdev, 0);
/* BAR 플래그 (IORESOURCE_MEM, IORESOURCE_IO 등) */
unsigned long bar_flags = pci_resource_flags(pdev, 0);
/* MMIO BAR을 가상 주소로 매핑 */
void __iomem *regs = pci_iomap(pdev, 0, bar_len);
if (!regs)
return -ENOMEM;
/* MMIO 읽기/쓰기 */
u32 val = ioread32(regs + 0x10);
iowrite32(0x1, regs + 0x14);
/* 해제 */
pci_iounmap(pdev, regs);
코드 설명
- pci_resource_start/len/flags커널이 BAR 열거 시
struct resource에 저장한 물리 주소, 크기, 플래그를 조회하는 헬퍼 매크로입니다. 인덱스0은 BAR 0을 가리킵니다.include/linux/pci.h에 정의되어 있으며, 내부적으로pdev->resource[bar]에 접근합니다. - pci_iomap()BAR의 MMIO 물리 주소를 커널 가상 주소(
void __iomem *)로 매핑합니다. 내부적으로ioremap()을 호출하며, I/O 포트 BAR인 경우에도 투명하게 처리합니다.lib/pci_iomap.c에 구현되어 있습니다. - ioread32/iowrite32매핑된 MMIO 영역에 대한 32비트 읽기/쓰기입니다. 직접 포인터 역참조 대신 이 API를 사용해야 컴파일러 최적화 방지와 바이트 순서 변환이 보장됩니다. 플랫폼별로 메모리 배리어(Memory Barrier) 시맨틱이 다를 수 있습니다.
- pci_iounmap()매핑을 해제합니다. Managed API인
pcim_iomap()을 사용하면 드라이버 해제 시 자동으로 언매핑되므로 명시적 호출이 불필요합니다.
Linux 커널 PCI 서브시스템
핵심 자료구조
| 구조체(Struct) | 헤더 | 역할 |
|---|---|---|
struct pci_dev | <linux/pci.h> | PCI 디바이스 인스턴스 (vendor/device, BARs, IRQ 등) |
struct pci_driver | <linux/pci.h> | PCI 드라이버 (probe/remove, ID 테이블) |
struct pci_bus | <linux/pci.h> | PCI 버스 (하위 디바이스 리스트, 브릿지 정보) |
struct pci_host_bridge | <linux/pci.h> | Host Bridge (Root Complex 대응) |
struct resource | <linux/ioport.h> | I/O, MMIO, IRQ 리소스 추상화 |
struct pci_dev 주요 필드
struct pci_dev {
struct pci_bus *bus; /* 디바이스가 연결된 버스 */
unsigned int devfn; /* Device + Function 번호 (8비트) */
unsigned short vendor, device; /* Vendor ID, Device ID */
unsigned short subsystem_vendor; /* Subsystem Vendor ID */
unsigned short subsystem_device; /* Subsystem Device ID */
unsigned int class; /* Class Code (24비트) */
u8 revision; /* Revision ID */
struct resource resource[PCI_NUM_RESOURCES]; /* BAR 리소스 */
unsigned int irq; /* 할당된 IRQ 번호 */
pci_power_t current_state; /* D0, D1, D2, D3hot, D3cold */
unsigned int is_busmaster:1; /* Bus Master 활성화 여부 */
unsigned int msi_enabled:1; /* MSI 활성화 여부 */
unsigned int msix_enabled:1; /* MSI-X 활성화 여부 */
struct pci_driver *driver; /* 바인딩된 드라이버 */
struct device dev; /* 범용 디바이스 모델 */
/* ... */
};
코드 설명
- bus이 디바이스가 직접 연결된
pci_bus포인터. 버스 번호, 리소스 윈도우, 부모 브릿지 정보를 담습니다. - devfnDevice[7:3] + Function[2:0]을 합친 8비트 값.
PCI_DEVFN(dev, fn)매크로로 생성하고PCI_SLOT(devfn)/PCI_FUNC(devfn)으로 분리합니다. - vendor, devicePCI Config Space 오프셋 0x00/0x02에서 읽어오는 16비트 식별자. 드라이버 매칭의 일차 기준입니다.
- subsystem_vendor, subsystem_device카드 제조사(OEM)가 부여하는 서브시스템 ID. 동일한 칩셋을 사용하는 여러 보드를 구별하는 데 활용됩니다.
- class상위 8비트 Base Class, 중간 8비트 Sub-Class, 하위 8비트 Prog-If로 구성된 24비트 코드. 예:
0x020000= Ethernet 컨트롤러. - resource[]BAR 0~5, Expansion ROM, Bridge Window 등 최대
PCI_NUM_RESOURCES개의struct resource. 커널 리소스 트리에 등록됩니다. - irq레거시 INTx 인터럽트 번호. MSI/MSI-X를 사용하면 이 필드 대신
pci_irq_vector(pdev, n)으로 벡터별 IRQ를 얻습니다. - current_statePCI 전원 상태: D0(완전 동작) ~ D3cold(완전 오프).
pci_set_power_state()로 변경합니다. - is_busmasterDMA 전송을 위해 버스 마스터 권한이 활성화됐는지 나타내는 1비트 플래그.
pci_set_master()가 설정합니다. - driver현재 이 디바이스에 바인딩된
pci_driver포인터. 드라이버가 없으면NULL. sysfs의driver심볼릭 링크와 동기화됩니다. - dev범용
struct device를 포함(embed)하여 드라이버 모델, sysfs, PM, devm 리소스 관리 등 커널 코어 인프라를 재사용합니다.
struct pci_driver 주요 필드
/* include/linux/pci.h */
struct pci_driver {
const char *name; /* 드라이버 이름 (sysfs, modinfo) */
const struct pci_device_id *id_table; /* 지원 디바이스 목록 (NULL이면 모든 디바이스) */
int (*probe)(struct pci_dev *dev,
const struct pci_device_id *id); /* 디바이스 발견·초기화 */
void (*remove)(struct pci_dev *dev); /* 드라이버 언바인드·리소스 해제 */
int (*suspend)(struct pci_dev *dev,
pm_message_t state); /* 레거시 suspend (PM_SLEEP) */
int (*resume)(struct pci_dev *dev); /* 레거시 resume */
void (*shutdown)(struct pci_dev *dev); /* 시스템 종료 시 호출 */
int (*sriov_configure)(struct pci_dev *dev,
int num_vfs); /* VF 수 설정 진입점 */
int (*sriov_set_msix_vec_count)(struct pci_dev *vf,
int msix_vec_count); /* VF MSI-X 벡터 수 조정 */
const struct pci_error_handlers *err_handler; /* AER 오류 복구 콜백 */
const struct attribute_group **groups; /* sysfs 속성 그룹 */
const struct attribute_group **dev_groups; /* 디바이스별 sysfs 그룹 */
struct device_driver driver; /* 드라이버 모델 기반 구조체 */
};
코드 설명
- name드라이버 이름 문자열. sysfs의
/sys/bus/pci/drivers/디렉터리명과modinfo출력에 표시됩니다. 보통KBUILD_MODNAME매크로로 지정합니다. - id_table드라이버가 지원하는 디바이스 목록.
MODULE_DEVICE_TABLE(pci, ...)로 모듈 메타데이터에도 내장되어modprobe의 자동 로드 트리거로 사용됩니다. - probe드라이버 바인딩 핵심 콜백. 디바이스 활성화, BAR 매핑, DMA 설정, 인터럽트 등록을 이곳에서 수행합니다. 성공하면 0, 실패하면 음수 에러 코드를 반환합니다.
- remove드라이버 언바인드 또는 디바이스 제거 시 호출.
pcim_*/devm_*를 사용하면 자동 해제되므로 이 함수를 최소화할 수 있습니다. - sriov_configuresysfs의
sriov_numvfs쓰기 이벤트를 처리하는 SR-IOV 진입점. 양수이면 VF 활성화, 0이면 모든 VF 비활성화를 의미합니다. 내부에서pci_enable_sriov()를 호출합니다. - err_handlerAER(Advanced Error Reporting) 오류 복구 콜백 테이블.
error_detected→slot_reset→resume순서로 복구 단계를 처리합니다. - driver범용
struct device_driver를 포함(embed).pci_driver를 커널 드라이버 모델에 연결하는 실질적인 기반입니다. 모듈 소유권, PM ops, ACPI ops가 여기 연결됩니다.
PCI 열거 (Enumeration)
커널 부팅 시 PCI 서브시스템은 다음 순서로 디바이스를 발견합니다:
- ACPI/DT에서 Host Bridge 정보 수집 — MCFG 테이블(ECAM 베이스), _CRS(리소스 윈도우)
- Bus 0부터 재귀적 스캔 — 각 Bus/Device/Function에 대해 Vendor ID 읽기 (0xFFFF이면 존재하지 않음)
- Bridge(Type 1) 발견 시 — Secondary/Subordinate Bus Number 할당 후 하위 버스 재귀 탐색
- BAR 크기 결정 및 리소스 할당 — 커널의 리소스 관리자가 주소 범위 배정
- 드라이버 매칭 —
pci_device_id테이블 기반으로 드라이버probe()호출
/* 커널 내부: PCI 디바이스 스캔 핵심 경로 (simplified) */
/* drivers/pci/probe.c */
struct pci_dev *pci_scan_single_device(struct pci_bus *bus, int devfn)
{
struct pci_dev *dev;
dev = pci_get_slot(bus, devfn);
if (dev)
return dev; /* 이미 스캔됨 */
dev = pci_scan_device(bus, devfn);
if (!dev)
return NULL;
pci_device_add(dev, bus);
return dev;
}
코드 설명
- pci_get_slot()버스의 디바이스 리스트를 탐색하여
devfn이 이미 등록된pci_dev가 있는지 확인합니다. 발견되면 참조 카운트를 증가시키고 반환하여 중복 등록을 방지합니다. - pci_scan_device()Config Space의 오프셋 0x00을 읽어 Vendor ID가
0xFFFF가 아닌지 확인한 후,pci_dev구조체를 할당하고 Vendor/Device/Class/Revision 등 기본 필드를 채웁니다. - pci_device_add()할당된
pci_dev를 버스의 디바이스 리스트에 연결하고,device_initialize()로kobject를 초기화한 뒤 sysfs 경로(/sys/bus/pci/devices/)를 생성합니다. 여기서 드라이버 매칭은 일어나지 않습니다.
pci_scan_bus() 호출 체인
커널 부팅 시 PCI 열거의 최상위 진입점은 pci_scan_bus()입니다. 이 함수는 버스 번호를 받아 하위 모든 디바이스를 재귀적으로 탐색하고 pci_dev 계층을 구성합니다.
/* drivers/pci/probe.c — 열거 핵심 3단계 호출 체인 */
/* [1단계] 버스 전체 스캔: 0~31번 슬롯 × 0~7번 function 순회 */
unsigned int pci_scan_child_bus(struct pci_bus *bus)
{
unsigned int devfn, fn, pass, max = bus->busn_res.start;
struct pci_dev *dev;
for (devfn = 0; devfn < 256; devfn += 8)
pci_scan_slot(bus, devfn); /* 슬롯 하나 처리 */
/* pass 0: 단말 디바이스(Type 0) 먼저 */
/* pass 1: 브릿지(Type 1) 재귀 탐색 */
for (pass = 0; pass < 2; pass++)
list_for_each_entry(dev, &bus->devices, bus_list)
if (pci_is_bridge(dev))
max = pci_scan_bridge(bus, dev, max, pass);
return max;
}
/* [2단계] 슬롯 스캔: Multi-Function 디바이스 처리 */
static int pci_scan_slot(struct pci_bus *bus, int devfn)
{
int fn, nr = 0;
struct pci_dev *dev;
dev = pci_scan_single_device(bus, devfn); /* Function 0 먼저 */
if (!dev)
return 0;
if (!dev->multifunction)
return 1; /* 단일 function이면 종료 */
for (fn = 1; fn < 8; fn++) { /* Function 1~7 추가 탐색 */
dev = pci_scan_single_device(bus, devfn + fn);
if (dev)
nr++;
}
return nr;
}
/* [3단계] 단일 디바이스 등록: pci_dev 생성 및 버스 연결 */
struct pci_dev *pci_scan_single_device(struct pci_bus *bus, int devfn)
{
struct pci_dev *dev;
dev = pci_scan_device(bus, devfn); /* Config Space 읽기 + pci_dev 할당 */
if (!dev)
return NULL;
pci_device_add(dev, bus); /* kobject + sysfs 등록 */
return dev;
}
코드 설명
- pci_scan_child_bus()버스 내 모든 슬롯(256개 devfn 값)을 8씩 증가시키며 순회합니다. 두 번의 패스(pass)로 단말 디바이스를 먼저 처리하고, 두 번째 패스에서 브릿지를 재귀 탐색하여 Secondary Bus를 할당합니다.
- pci_scan_slot() — multifunctionFunction 0의 Header Type bit 7이 1이면 Multi-Function 디바이스임을 표시합니다. 이 경우 Function 1~7을 추가로 탐색합니다. bit 7이 0이면 Function 0만 존재합니다.
- pci_scan_device()Vendor ID
0xFFFF를 읽으면 디바이스 없음으로 판단하고NULL을 반환합니다. 디바이스가 존재하면kzalloc으로pci_dev를 할당하고 Config Space Header(64바이트)를 캐싱합니다. - pci_device_add()
device_initialize()후dev->dev.parent를 버스 디바이스로 설정하고,pci_configure_device()로 권능/버그 쿼크(quirk)를 적용합니다. 아직 드라이버 바인딩은 일어나지 않으며pci_bus_add_devices()가 호출될 때device_add()로 sysfs에 공개되고 드라이버 매칭이 시작됩니다.
PCI 드라이버 작성
드라이버 골격
#include <linux/module.h>
#include <linux/pci.h>
#define MY_VENDOR_ID 0x1234
#define MY_DEVICE_ID 0x5678
struct my_priv {
void __iomem *regs;
/* 디바이스별 private 데이터 */
};
static int my_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct my_priv *priv;
int err;
/* 1. PCI 디바이스 활성화 */
err = pcim_enable_device(pdev);
if (err)
return err;
/* 2. BAR 리소스 요청 (managed) */
err = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
if (err)
return err;
/* 3. private 데이터 할당 */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 4. BAR 0 매핑 주소 획득 */
priv->regs = pcim_iomap_table(pdev)[0];
/* 5. DMA 마스크 설정 */
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (err)
return err;
/* 6. Bus Master 활성화 (DMA 사용 시 필수) */
pci_set_master(pdev);
/* 7. MSI-X 인터럽트 설정 */
err = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSIX | PCI_IRQ_MSI);
if (err < 0)
return err;
pci_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "probed successfully\\n");
return 0;
}
static void my_remove(struct pci_dev *pdev)
{
/* pcim_* / devm_* 사용 시 자동 정리 — 명시적 해제 불필요 */
pci_free_irq_vectors(pdev);
dev_info(&pdev->dev, "removed\\n");
}
/* PCI Device ID 테이블 — 드라이버가 지원하는 디바이스 목록 */
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) },
{ PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00) },
{ 0, } /* 종료 마커 */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
static struct pci_driver my_driver = {
.name = KBUILD_MODNAME,
.id_table = my_pci_ids,
.probe = my_probe,
.remove = my_remove,
};
module_pci_driver(my_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example PCI driver");
코드 설명
- pcim_enable_device()PCI 디바이스를 활성화하는 Managed 버전입니다.
pci_enable_device()와 달리 드라이버 해제 시 자동으로pci_disable_device()가 호출됩니다. 이 함수는 디바이스의 Command 레지스터에서 Memory Space와 I/O Space 비트를 활성화합니다. - pcim_iomap_regions()
pci_request_regions()+pci_iomap()을 결합한 Managed API입니다.BIT(0)은 BAR 0만 요청함을 의미합니다.drivers/pci/devres.c에 구현되어 있으며, 리소스 충돌 시 에러를 반환합니다. - dma_set_mask_and_coherent()디바이스가 지원하는 DMA 주소 비트 폭을 커널에 알립니다.
DMA_BIT_MASK(64)는 64비트 전체 주소 공간을 사용할 수 있음을 선언합니다. IOMMU가 없는 시스템에서는 이 값에 따라 bounce buffer 사용 여부가 결정됩니다. - pci_set_master()PCI Command 레지스터의 Bus Master 비트를 설정하여 디바이스가 DMA 전송을 개시할 수 있게 합니다. 이 호출 없이는 디바이스의 메모리 쓰기 요청이 차단됩니다.
- pci_alloc_irq_vectors()MSI-X 우선, MSI 폴백으로 최소 1개~최대 16개 인터럽트 벡터를 할당합니다. 반환값은 실제 할당된 벡터 수이며,
drivers/pci/msi/api.c에 구현되어 있습니다. - module_pci_driver()
module_init()/module_exit()에서pci_register_driver()/pci_unregister_driver()를 호출하는 보일러플레이트를 한 줄로 대체하는 매크로입니다.include/linux/pci.h에 정의되어 있습니다. - PCI_DEVICE / MODULE_DEVICE_TABLE
PCI_DEVICE()는 Vendor/Device ID 쌍으로pci_device_id엔트리를 생성합니다.MODULE_DEVICE_TABLE(pci, ...)은 모듈 별칭(alias)을 생성하여udev가 디바이스 삽입 시 올바른 드라이버를 자동 로드할 수 있게 합니다.
Managed API (pcim_* / devm_*)
커널은 Managed Device Resource API를 제공하여 드라이버 해제 시 자동 정리합니다:
| Managed API | 수동 API | 용도 |
|---|---|---|
pcim_enable_device() | pci_enable_device() | 디바이스 활성화 |
pcim_iomap_regions() | pci_request_regions() + pci_iomap() | BAR 리소스 요청 + MMIO 매핑 |
devm_kzalloc() | kzalloc() + kfree() | 메모리 할당 |
devm_request_irq() | request_irq() + free_irq() | 인터럽트 핸들러(Handler) 등록 |
dmam_alloc_coherent() | dma_alloc_coherent() + dma_free_coherent() | DMA 버퍼 할당 |
pcim_* / devm_* API를 사용하면 에러 경로에서의 리소스 누수를 방지하고 remove() 함수를 간소화할 수 있습니다. 새 드라이버에서는 항상 managed API를 우선 사용하세요.PCI ID 매칭 매크로(Macro)
| 매크로 | 매칭 기준 |
|---|---|
PCI_DEVICE(vendor, device) | Vendor + Device ID |
PCI_DEVICE_SUB(vendor, device, subvendor, subdevice) | + Subsystem IDs |
PCI_DEVICE_CLASS(class, class_mask) | Class Code 기반 매칭 |
PCI_VDEVICE(vendor, device) | PCI_DEVICE 축약 (벤더 prefix 자동) |
PCI_ANY_ID | 와일드카드 (모든 값 매칭) |
드라이버 매칭 내부 구현
pci_register_driver() 호출 시 커널 드라이버 모델이 이미 등록된 PCI 디바이스와 새 드라이버의 id_table을 매칭합니다. 새 디바이스가 발견될 때도 동일한 경로가 역방향으로 동작합니다.
/* drivers/pci/pci-driver.c — 드라이버 매칭 핵심 경로 */
/* [1단계] 버스 레벨 매칭: pci_bus_match()
* 디바이스 모델이 driver_attach() → __driver_attach() → driver_match_device()
* 를 통해 호출합니다. */
static int pci_bus_match(struct device *dev, struct device_driver *drv)
{
struct pci_dev *pci_dev = to_pci_dev(dev);
struct pci_driver *pci_drv = to_pci_driver(drv);
const struct pci_device_id *found_id;
/* id_table의 각 엔트리를 순회하며 vendor/device/class 매칭 */
found_id = pci_match_device(pci_drv, pci_dev);
return found_id != NULL;
}
/* [2단계] ID 매칭: pci_match_device()
* 드라이버의 id_table + 동적 ID 리스트 양쪽을 검사합니다. */
static const struct pci_device_id *
pci_match_device(struct pci_driver *drv, struct pci_dev *dev)
{
const struct pci_device_id *id;
/* 정적 id_table 검색 */
id = pci_match_id(drv->id_table, dev);
if (id)
return id;
/* sysfs new_id를 통해 런타임 추가된 동적 ID 검색 */
spin_lock(&drv->dynids.lock);
list_for_each_entry(dynid, &drv->dynids.list, node) {
if (pci_match_one_device(&dynid->id, dev)) {
id = &dynid->id;
break;
}
}
spin_unlock(&drv->dynids.lock);
return id;
}
/* [3단계] 개별 ID 비교: pci_match_one_device()
* vendor/device/subvendor/subdevice/class 5개 필드를 비교합니다. */
static inline const struct pci_device_id *
pci_match_one_device(const struct pci_device_id *id,
const struct pci_dev *dev)
{
if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) &&
(id->device == PCI_ANY_ID || id->device == dev->device) &&
(id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) &&
(id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) &&
!((id->class ^ dev->class) & id->class_mask))
return id;
return NULL;
}
코드 설명
- pci_bus_match()커널 디바이스 모델의
bus_type.match콜백입니다.driver_attach()가 등록된 모든 디바이스를 순회하며 이 함수를 호출합니다. 반환값이 0이 아니면 매칭 성공으로, 다음 단계인pci_device_probe()로 진행합니다. - pci_match_device()정적
id_table을 먼저 검사하고, 매칭되지 않으면dynids리스트를 검사합니다. 동적 ID는echo "VEND DEV" > /sys/bus/pci/drivers/드라이버명/new_id로 런타임에 추가할 수 있으며, VFIO 패스스루 바인딩 시 자주 사용됩니다. - pci_match_one_device()5개 필드(vendor, device, subvendor, subdevice, class)를 각각 비교합니다.
PCI_ANY_ID는 와일드카드로 해당 필드를 무시합니다.class_mask는 class 코드의 비교 범위를 결정합니다. 예를 들어class_mask=0xFFFF00이면 Prog-If 바이트를 무시하고 Base+Sub Class만 비교합니다.
probe() 호출 내부 경로
매칭이 성공하면 pci_device_probe()가 호출되어 드라이버의 probe() 콜백을 실행합니다. 이 과정에서 디바이스 활성화와 리소스 준비가 이루어집니다.
/* drivers/pci/pci-driver.c — probe 호출 체인 (simplified) */
static int pci_device_probe(struct device *dev)
{
struct pci_dev *pci_dev = to_pci_dev(dev);
struct pci_driver *drv = to_pci_driver(dev->driver);
const struct pci_device_id *id;
int error;
/* 디바이스의 NUMA 노드에서 probe 실행 (캐시 친화도) */
error = pci_call_probe(drv, pci_dev, id);
return error;
}
static int pci_call_probe(struct pci_driver *drv,
struct pci_dev *dev,
const struct pci_device_id *id)
{
int error, node, cpu;
/* NUMA 노드 기반 CPU 선택 → work_on_cpu()로 해당 노드에서 실행 */
node = dev_to_node(&dev->dev);
if (node >= 0 && node != numa_node_id()) {
cpu = cpumask_any_and(cpumask_of_node(node),
cpu_online_mask);
if (cpu < nr_cpu_ids)
error = work_on_cpu(cpu, local_pci_probe, &ddi);
else
error = local_pci_probe(&ddi);
} else {
error = local_pci_probe(&ddi);
}
return error;
}
static long local_pci_probe(void *_ddi)
{
struct drv_dev_and_id *ddi = _ddi;
struct pci_dev *pci_dev = ddi->dev;
struct pci_driver *pci_drv = ddi->drv;
int rc;
/* PM 런타임 레퍼런스 획득 */
pm_runtime_get_sync(&pci_dev->dev);
/* ★ 드라이버의 probe() 콜백 호출 */
rc = pci_drv->probe(pci_dev, ddi->id);
if (rc)
pm_runtime_put_sync(&pci_dev->dev);
return rc;
}
코드 설명
- pci_device_probe()
pci_bus_type.probe콜백으로, 디바이스 모델의really_probe()에서 호출됩니다. 매칭된pci_device_id를 가져와서pci_call_probe()에 전달합니다. - pci_call_probe() — NUMA 최적화PCI 디바이스가 연결된 NUMA 노드의 CPU에서
probe()를 실행합니다. 이는probe()내에서 할당하는 메모리가 디바이스와 같은 NUMA 노드에 배치되도록 하여, DMA 전송 시 크로스 노드 트래픽을 줄입니다.work_on_cpu()는 지정된 CPU에서 함수를 실행하는 커널 유틸리티입니다. - local_pci_probe()실제 드라이버
probe()콜백을 호출하는 최종 함수입니다. PM 런타임 레퍼런스를 먼저 획득하여 디바이스가 절전 상태에서 깨어나도록 보장합니다.probe()가 0을 반환하면 바인딩 성공, 음수이면 실패로 처리됩니다.
pci_register_driver() → driver_register() → bus_add_driver() → driver_attach() → __driver_attach() (각 디바이스마다) → driver_match_device() → pci_bus_match() → pci_match_device() → 매칭 성공 시 → driver_probe_device() → really_probe() → pci_device_probe() → pci_call_probe() → local_pci_probe() → drv->probe()드라이버 언바인드와 sysfs 제어
# 드라이버 수동 언바인드
echo "0000:03:00.0" > /sys/bus/pci/drivers/nvme/unbind
# 다른 드라이버에 바인딩 (예: vfio-pci)
echo "vfio-pci" > /sys/bus/pci/devices/0000:03:00.0/driver_override
echo "0000:03:00.0" > /sys/bus/pci/drivers_probe
# driver_override 초기화 (자동 매칭으로 복원)
echo "" > /sys/bus/pci/devices/0000:03:00.0/driver_override
# 동적 ID 추가 (런타임에 드라이버가 새 디바이스를 지원하도록)
echo "8086 1572" > /sys/bus/pci/drivers/vfio-pci/new_id
# → pci_match_device()가 dynids 리스트에서 이 ID를 찾음
# 동적 ID 제거
echo "8086 1572" > /sys/bus/pci/drivers/vfio-pci/remove_id
DMA 매핑
PCI 디바이스가 시스템 메모리에 직접 접근하려면 DMA (Direct Memory Access)를 사용합니다. 커널은 두 가지 DMA 매핑 모델을 제공합니다:
Coherent (Consistent) DMA
CPU와 디바이스가 동시에 접근하는 공유 메모리에 적합합니다. 캐시(Cache) 코히런시가 하드웨어적으로 보장됩니다.
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096;
/* 할당: CPU 가상 주소 + DMA 버스 주소 반환 */
cpu_addr = dma_alloc_coherent(&pdev->dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
return -ENOMEM;
/* 디바이스에 DMA 주소 전달 (레지스터에 기록) */
iowrite32(lower_32_bits(dma_handle), regs + 0x20);
iowrite32(upper_32_bits(dma_handle), regs + 0x24);
/* 해제 */
dma_free_coherent(&pdev->dev, size, cpu_addr, dma_handle);
Streaming DMA
일시적 단방향 전송에 적합합니다. 매핑 전후 명시적 캐시 동기화가 필요합니다.
dma_addr_t dma_handle;
void *buf = kmalloc(4096, GFP_KERNEL);
/* 매핑: CPU 버퍼 → DMA 주소 */
dma_handle = dma_map_single(&pdev->dev, buf, 4096, DMA_TO_DEVICE);
if (dma_mapping_error(&pdev->dev, dma_handle)) {
kfree(buf);
return -EIO;
}
/* 디바이스가 DMA 전송 수행... */
/* 언매핑 (반드시 전송 완료 후) */
dma_unmap_single(&pdev->dev, dma_handle, 4096, DMA_TO_DEVICE);
kfree(buf);
DMA 방향
| 상수 | 의미 |
|---|---|
DMA_TO_DEVICE | CPU → 디바이스 (TX) |
DMA_FROM_DEVICE | 디바이스 → CPU (RX) |
DMA_BIDIRECTIONAL | 양방향 |
DMA_NONE | 디버깅용 |
Scatter-Gather DMA
물리적으로 불연속적인 메모리 페이지를 하나의 DMA 전송으로 처리합니다:
struct scatterlist sg[MAX_SG];
int nents, mapped;
sg_init_table(sg, MAX_SG);
/* sg 엔트리에 페이지/오프셋/길이 설정 */
sg_set_page(&sg[0], page0, 4096, 0);
sg_set_page(&sg[1], page1, 4096, 0);
/* 매핑: IOMMU가 있으면 연속 DMA 주소로 합칠 수 있음 */
mapped = dma_map_sg(&pdev->dev, sg, nents, DMA_FROM_DEVICE);
/* 언매핑 */
dma_unmap_sg(&pdev->dev, sg, nents, DMA_FROM_DEVICE);
IOMMU / DMA Remapping
IOMMU(Intel VT-d, AMD-Vi)는 디바이스의 DMA 주소를 물리 주소(Physical Address)로 변환하는 하드웨어입니다:
| 기능 | 설명 |
|---|---|
| 주소 변환(Address Translation) | 디바이스별 페이지 테이블(Page Table)로 DMA 접근 범위 제한 |
| 격리 | 디바이스가 허가되지 않은 메모리에 접근하는 것을 차단 |
| SG 합치기 | 불연속 물리 페이지를 연속 DMA 주소로 매핑 |
| 가상화(Virtualization) | 게스트 VM에 디바이스 직접 할당 (VFIO passthrough) |
커널 부팅 파라미터:
intel_iommu=on # Intel VT-d 활성화
iommu=pt # Passthrough 모드 (성능 우선, 격리 최소)
iommu.strict=1 # Strict 모드 (보안 우선, 즉시 IOTLB 무효화)
MSI / MSI-X 인터럽트
PCI 레거시 인터럽트(INTA#~INTD#)는 공유 인터럽트 라인 문제가 있습니다. MSI (Message Signaled Interrupt)와 MSI-X는 메모리 쓰기(TLP) 방식으로 인터럽트를 전달하여 이 문제를 해결합니다.
| 특성 | Legacy INTx | MSI | MSI-X |
|---|---|---|---|
| 시그널링 | 전용 핀 (공유) | Memory Write TLP | Memory Write TLP |
| 벡터 수 | 4 (INTA~D) | 1~32 | 1~2048 |
| 벡터별 타겟 CPU | 불가 | 제한적 | 개별 지정 가능 |
| Config Space | IRQ Line/Pin | Capability (ID=05h) | Capability (ID=11h) |
| 공유 | 여러 디바이스 공유 | 전용 | 전용 |
MSI Capability 구조 (Cap ID 05h)
MSI Capability는 PCI Configuration Space의 Capability List에 연결되며, Cap ID 05h로 식별됩니다. Message Control Register의 설정에 따라 32비트/64비트 주소와 Per-Vector Masking 지원 여부가 결정됩니다.
Message Control Register (Offset 02h)
| 비트 | 필드 | 설명 |
|---|---|---|
| 0 | MSI Enable | 1=MSI 활성화, INTx 자동 비활성화 |
| 3:1 | Multiple Message Capable | 디바이스가 요청하는 벡터 수 (2^N, 최대 32) |
| 6:4 | Multiple Message Enable | 소프트웨어가 할당한 벡터 수 (≤ Capable) |
| 7 | 64-bit Address Capable | 1=64비트 Message Address 지원 |
| 8 | Per-Vector Masking | 1=Mask/Pending 비트 지원 |
| 9 | Extended Message Data Capable | PCIe 4.0+ 확장 데이터 (32비트) |
| 10 | Extended Message Data Enable | 확장 데이터 활성화 |
| 15:11 | Reserved | 예약 (0) |
MSI Capability 레이아웃
Message Address Register (MAR) — x86 APIC 인코딩
x86에서 MSI Message Address는 LAPIC의 메모리 매핑 영역(0xFEExxxxx)을 가리킵니다.
비트 31:20 0xFEE (고정 — LAPIC 기본 주소 상위 12비트)
비트 19:12 Destination ID (target APIC ID)
비트 11 Reserved
비트 10 Reserved
비트 4 Redirection Hint (RH)
0 = Destination ID가 직접 타겟 지정
1 = 로직 모드에서 lowest-priority 가능
비트 3 Destination Mode (DM)
0 = Physical Mode (APIC ID 직접)
1 = Logical Mode (클러스터/flat)
비트 2:0 Reserved
Message Data Register (MDR)
비트 7:0 Vector (IDT 엔트리 번호, 0x10~0xFE)
비트 10:8 Delivery Mode
000 = Fixed, 001 = Lowest Priority
010 = SMI, 100 = NMI
101 = INIT, 111 = ExtINT
비트 11 Reserved
비트 12 Reserved
비트 13 Reserved
비트 14 Level (edge: 무시, level: 0=deassert, 1=assert)
비트 15 Trigger Mode (0=Edge, 1=Level)
비트 31:16 Reserved (PCIe 4.0 확장 시 사용)
MSI-X Capability 구조 (Cap ID 11h)
MSI-X는 MSI의 제한(최대 32벡터, 단일 MAR/MDR)을 극복하기 위해 도입되었습니다. 별도의 BAR 영역에 벡터 테이블과 PBA를 배치하여, 최대 2048개 벡터를 각각 독립적으로 구성할 수 있습니다.
MSI-X Message Control Register (Offset 02h)
| 비트 | 필드 | 설명 |
|---|---|---|
| 10:0 | Table Size | 벡터 테이블 크기 - 1 (최대 2047 → 2048 엔트리) |
| 13:11 | Reserved | 예약 |
| 14 | Function Mask | 1=모든 벡터 마스크 (글로벌) |
| 15 | MSI-X Enable | 1=MSI-X 활성화, INTx 자동 비활성화 |
MSI-X Capability + Table Entry 레이아웃
PBA (Pending Bit Array)
PBA는 벡터가 마스크된 동안 발생한 인터럽트를 기록합니다. 각 비트가 테이블 엔트리에 1:1 대응하며, 벡터 언마스크 시 pending 비트가 설정되어 있으면 인터럽트가 즉시 전달됩니다. PBA는 읽기 전용(Read-Only)이며, 하드웨어가 자동으로 관리합니다.
/* 커널 내부: MSI-X 벡터 개별 마스킹 (drivers/pci/msi/msi.c) */
static void msix_mask_irq(struct msi_desc *desc)
{
void __iomem *addr = desc->pci.mask_base +
desc->msi_index * PCI_MSIX_ENTRY_SIZE +
PCI_MSIX_ENTRY_VECTOR_CTRL;
u32 ctrl = readl(addr);
ctrl |= PCI_MSIX_ENTRY_CTRL_MASKBIT;
writel(ctrl, addr);
/* readl() — 쓰기가 디바이스에 도달했음을 보장 (flush) */
readl(addr);
}
static void msix_unmask_irq(struct msi_desc *desc)
{
void __iomem *addr = desc->pci.mask_base +
desc->msi_index * PCI_MSIX_ENTRY_SIZE +
PCI_MSIX_ENTRY_VECTOR_CTRL;
u32 ctrl = readl(addr);
ctrl &= ~PCI_MSIX_ENTRY_CTRL_MASKBIT;
writel(ctrl, addr);
readl(addr);
}
코드 설명
- mask_base + msi_index * PCI_MSIX_ENTRY_SIZEMSI-X 테이블은 BAR 영역에 배치되며, 각 엔트리는
PCI_MSIX_ENTRY_SIZE(16바이트) 크기입니다.mask_base는 MSI-X Capability의 Table Offset/BIR에서 계산된 MMIO 베이스 주소이며,msi_index로 개별 벡터의 Vector Control 레지스터에 접근합니다. - PCI_MSIX_ENTRY_CTRL_MASKBITVector Control 레지스터의 비트 0이 Mask 비트입니다. 1로 설정하면 해당 벡터의 인터럽트 전달이 차단되고, 0으로 클리어하면 전달이 재개됩니다.
drivers/pci/msi/msi.c에서 인터럽트 비활성화/활성화 시 이 함수들을 호출합니다. - readl() flush
writel()후readl()을 수행하는 것은 PCI 쓰기가 posted transaction이기 때문입니다. 읽기를 수행하면 이전 쓰기가 디바이스에 도달할 때까지 대기하게 되어, 마스크 변경이 즉시 반영됨을 보장합니다. 이 패턴은 PCI 드라이버에서 매우 일반적입니다.
인터럽트 전달 메커니즘
MSI/MSI-X 인터럽트는 디바이스가 특정 주소로 Memory Write TLP를 전송하는 방식으로 동작합니다. 플랫폼에 따라 전달 경로가 다릅니다.
x86: LAPIC 기반 전달
디바이스가 0xFEE00000 범위의 주소로 Memory Write TLP를 전송하면, PCIe Root Complex가 이를 메모리 트랜잭션이 아닌 인터럽트 메시지로 인식하여 대상 CPU의 LAPIC에 전달합니다.
ARM: ITS (Interrupt Translation Service) 기반 전달
ARM GICv3/v4에서는 ITS가 MSI 메시지를 수신하여 DeviceID + EventID를 LPI(Locality-specific Peripheral Interrupt) 번호로 변환합니다. ITS Command Queue를 통해 매핑 테이블이 관리됩니다.
IRR(Interrupt Request Register): LAPIC 내부 레지스터로, 아직 서비스되지 않은 대기 중인 인터럽트 벡터 비트를 저장합니다. CPU가 인터럽트를 처리하기 시작하면 해당 비트가 ISR(In-Service Register)로 이동합니다.
0xFEE00000~0xFEEFFFFF (1MB) 영역은 LAPIC용으로 예약됩니다. 이 영역이 일반 메모리 또는 MMIO BAR과 겹치면 시스템이 부팅되지 않거나 인터럽트가 손실될 수 있습니다. BIOS/펌웨어(Firmware)가 이 영역을 Reserved로 E820 맵에 보고하는지 확인하세요.커널 MSI 서브시스템
리눅스 커널의 MSI 서브시스템은 irq_domain 계층 구조를 기반으로, 플랫폼별 인터럽트 컨트롤러(Interrupt Controller)와 PCI MSI를 추상화합니다.
주요 구조체
| 구조체 | 헤더 | 역할 |
|---|---|---|
struct msi_desc | include/linux/msi.h | MSI/MSI-X 디스크립터 — 벡터 정보, affinity, 마스크 상태 |
struct msi_msg | include/linux/msi.h | Message Address/Data 값 (실제 HW에 프로그래밍되는 값) |
struct msi_domain_info | include/linux/msi.h | irq_domain 레벨 MSI 연산 정의 |
struct msi_domain_ops | include/linux/msi.h | MSI 도메인 콜백(Callback): prepare, set_desc 등 |
struct irq_chip | include/linux/irq.h | 인터럽트 마스크/언마스크/EOI 등 HW 연산 |
pci_alloc_irq_vectors() 내부 구현
드라이버가 pci_alloc_irq_vectors()를 호출하면 커널은 MSI-X → MSI → Legacy INTx 순서로 시도합니다. 내부 호출 체인을 추적하면 다음과 같습니다.
/* drivers/pci/msi/api.c — 진입점 */
int pci_alloc_irq_vectors(struct pci_dev *dev,
unsigned int min_vecs,
unsigned int max_vecs,
unsigned int flags)
{
return pci_alloc_irq_vectors_affinity(dev, min_vecs,
max_vecs, flags, NULL);
}
/* drivers/pci/msi/api.c — affinity 포함 벡터 할당 */
int pci_alloc_irq_vectors_affinity(
struct pci_dev *dev,
unsigned int min_vecs, unsigned int max_vecs,
unsigned int flags, struct irq_affinity *affd)
{
struct irq_affinity msi_default_affd = { 0 };
int nvecs = -ENOSPC;
if (flags & PCI_IRQ_AFFINITY) {
if (!affd)
affd = &msi_default_affd;
} else {
affd = NULL;
}
/* 1순위: MSI-X 시도 */
if (flags & PCI_IRQ_MSIX) {
nvecs = __pci_enable_msix_range(dev, NULL,
min_vecs, max_vecs, affd, flags);
if (nvecs > 0)
return nvecs;
}
/* 2순위: MSI 폴백 */
if (flags & PCI_IRQ_MSI) {
nvecs = __pci_enable_msi_range(dev,
min_vecs, max_vecs, affd);
if (nvecs > 0)
return nvecs;
}
/* 3순위: Legacy INTx 폴백 (min_vecs가 1이어야 가능) */
if (flags & PCI_IRQ_LEGACY) {
if (min_vecs == 1 && dev->irq)
return 1;
}
return nvecs;
}
코드 설명
- PCI_IRQ_MSIX → __pci_enable_msix_range()
drivers/pci/msi/msi.c에 구현된 MSI-X 활성화 핵심 함수입니다. MSI-X Capability를 확인하고,min_vecs~max_vecs범위에서 가능한 최대 벡터 수를 할당합니다. 디바이스의 MSI-X Table Size보다 큰 수는 요청할 수 없습니다. 내부적으로msix_capability_init()→msix_setup_interrupts()를 거쳐irq_domain_alloc_irqs()로 Linux IRQ를 생성합니다. - PCI_IRQ_MSI → __pci_enable_msi_range()MSI Capability를 사용하는 폴백 경로입니다. MSI는 최대 32벡터까지만 지원하며, 실제 할당 가능 수는
2^N(1, 2, 4, 8, 16, 32) 중 하나입니다. 내부적으로msi_capability_init()이 Message Control 레지스터의 MME 필드를 설정합니다. - PCI_IRQ_LEGACY 폴백MSI-X/MSI 모두 실패하면 레거시 INTx를 사용합니다.
dev->irq가 0이 아니면 이미 ACPI/DT에 의해 할당된 IRQ가 있으므로 1을 반환합니다.pci_irq_vector(pdev, 0)이dev->irq를 반환하게 됩니다. - PCI_IRQ_AFFINITY이 플래그가 설정되면
irq_create_affinity_masks()가 온라인 CPU 수를 기반으로 벡터를 분배합니다. NUMA 토폴로지를 고려하여 디바이스가 연결된 노드의 CPU에 우선 할당합니다.pre_vectors/post_vectors로 관리용 벡터를 분배 대상에서 제외할 수 있습니다.
/* drivers/pci/msi/msi.c — MSI-X 활성화 내부 핵심 경로 (simplified) */
static int msix_capability_init(struct pci_dev *dev,
struct msix_entry *entries,
int nvec,
struct irq_affinity *affd)
{
void __iomem *base;
int ret;
u16 control;
/* MSI-X Capability의 Message Control 읽기 */
pci_read_config_word(dev,
dev->msix_cap + PCI_MSIX_FLAGS, &control);
/* MSI-X 테이블의 BAR 영역을 MMIO 매핑 */
base = msix_map_region(dev, msix_table_size(control));
if (!base)
return -ENOMEM;
/* 모든 벡터를 먼저 마스크 (Function Mask 비트 설정) */
pci_write_config_word(dev,
dev->msix_cap + PCI_MSIX_FLAGS,
control | PCI_MSIX_FLAGS_MASKALL);
/* irq_domain을 통해 Linux IRQ 벡터 할당 */
ret = msix_setup_interrupts(dev, base, entries, nvec, affd);
if (ret)
goto out_free;
/* MSI-X Enable 비트 설정 + Function Mask 해제 */
pci_write_config_word(dev,
dev->msix_cap + PCI_MSIX_FLAGS,
control | PCI_MSIX_FLAGS_ENABLE);
/* INTx 비활성화 (MSI-X와 공존 불가) */
pci_intx_for_msi(dev, 0);
dev->msix_enabled = 1;
return 0;
out_free:
pci_write_config_word(dev,
dev->msix_cap + PCI_MSIX_FLAGS, control);
return ret;
}
코드 설명
- msix_map_region()MSI-X Capability의 Table BIR(BAR Indicator Register)과 Table Offset으로부터 MSI-X 테이블이 위치한 BAR 영역을
ioremap()합니다. 이 매핑을 통해 커널이 각 벡터의 Message Address/Data/Vector Control을 직접 읽고 쓸 수 있습니다. - PCI_MSIX_FLAGS_MASKALLFunction Mask 비트(bit 14)를 설정하여 모든 벡터의 인터럽트 전달을 일시 차단합니다. 벡터 설정 중 예기치 않은 인터럽트 발생을 방지하기 위한 안전 조치입니다.
- msix_setup_interrupts()
irq_domain프레임워크를 통해 각 벡터에 Linux IRQ 번호를 할당하고,msi_desc디스크립터를 생성합니다. affinity가 지정되면irq_set_affinity()로 CPU 배분을 설정합니다. 플랫폼별irq_chip(예: x86에서는pci_msi_controller)이 실제 하드웨어 프로그래밍을 담당합니다. - PCI_MSIX_FLAGS_ENABLEMSI-X Enable 비트(bit 15)를 설정하고 Function Mask를 해제합니다. 이 시점부터 디바이스가 MSI-X 테이블에 프로그래밍된 주소로 Memory Write TLP를 전송하여 인터럽트를 시그널링할 수 있습니다.
- pci_intx_for_msi()Command 레지스터의 Interrupt Disable 비트를 설정하여 레거시 INTx를 비활성화합니다. PCI 스펙상 MSI-X와 INTx는 동시에 활성화할 수 없으며, MSI-X Enable이 1이면 INTx assertion이 자동 차단됩니다.
pci_alloc_irq_vectors() → pci_alloc_irq_vectors_affinity() → __pci_enable_msix_range() → msix_capability_init() → msix_map_region() (BAR MMIO 매핑) → msix_setup_interrupts() → msi_domain_alloc_irq_at() → irq_domain_alloc_irqs() (Linux IRQ 생성) → __irq_domain_alloc_irqs() → irq_domain_activate_irq() (플랫폼별 HW 프로그래밍)msi_msg / msi_desc 코드
/* include/linux/msi.h */
struct msi_msg {
union {
u64 address;
struct {
u32 address_lo; /* MAR 하위 32비트 */
u32 address_hi; /* MAR 상위 32비트 (64-bit) */
};
};
u32 data; /* MDR — 벡터, 전달 모드 등 */
};
struct msi_desc {
unsigned int irq; /* Linux IRQ 번호 */
unsigned int nvec_used; /* 사용 벡터 수 */
struct device *dev; /* 소유 디바이스 */
struct msi_msg msg; /* 현재 프로그래밍된 메시지 */
struct irq_affinity_desc *affinity; /* CPU affinity */
struct {
u32 masked; /* MSI: Mask Bits 캐시 */
struct {
u8 is_64; /* 64비트 주소 지원 */
u16 entry_nr; /* MSI-X 테이블 인덱스 */
void __iomem *mask_base; /* MSI-X 테이블 VA */
} pci;
};
u16 msi_index; /* 벡터 인덱스 */
};
pci_alloc_irq_vectors() 내부 흐름
드라이버가 pci_alloc_irq_vectors()를 호출하면, 커널은 다음 순서로 벡터를 할당합니다.
커널 소스 경로
| 경로 | 설명 |
|---|---|
drivers/pci/msi/msi.c | PCI MSI 코어 — 할당, 마스킹, 해제 |
drivers/pci/msi/irqdomain.c | PCI MSI irq_domain 인터페이스 |
drivers/pci/msi/api.c | 드라이버 API — pci_alloc_irq_vectors() 등 |
kernel/irq/msi.c | 범용 MSI irq_domain 프레임워크 |
arch/x86/kernel/apic/msi.c | x86 APIC MSI 도메인 구현 |
drivers/irqchip/irq-gic-v3-its-pci-msi.c | ARM GICv3 ITS PCI-MSI 도메인 |
include/linux/msi.h | MSI 구조체 및 API 헤더 |
벡터 할당과 CPU Affinity
멀티코어 시스템에서 인터럽트를 적절한 CPU에 분배하는 것은 성능에 결정적입니다. 커널은 NUMA 토폴로지를 고려한 자동 affinity 할당을 지원합니다.
pci_alloc_irq_vectors 플래그
| 플래그 | 값 | 설명 |
|---|---|---|
PCI_IRQ_MSIX | 0x04 | MSI-X 사용 시도 |
PCI_IRQ_MSI | 0x02 | MSI 사용 시도 |
PCI_IRQ_LEGACY | 0x01 | 레거시 INTx 폴백 |
PCI_IRQ_AFFINITY | 0x10 | 자동 CPU affinity 할당 (커널이 분배) |
PCI_IRQ_ALL_TYPES | 0x07 | MSI-X | MSI | LEGACY 전부 시도 |
NVMe 스타일 affinity 벡터 할당
/* MSI-X 할당 — 커널이 최적 벡터 수 결정 + 자동 affinity */
struct irq_affinity affd = {
.pre_vectors = 1, /* Admin Queue 전용 벡터 (affinity 제외) */
.post_vectors = 0,
};
int nvecs = pci_alloc_irq_vectors_affinity(pdev,
1, /* 최소 벡터 수 */
num_queues, /* 최대 벡터 수 (보통 I/O 큐 수 + 1) */
PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_AFFINITY,
&affd);
if (nvecs < 0)
return nvecs;
/* 각 벡터에 대한 IRQ 번호 획득 및 핸들러 등록 */
for (int i = 0; i < nvecs; i++) {
int irq = pci_irq_vector(pdev, i);
err = devm_request_irq(&pdev->dev, irq, my_isr, 0,
KBUILD_MODNAME, &queues[i]);
if (err)
goto err_free;
}
/* ... 드라이버 운영 ... */
/* 해제 — devm 사용 시 자동 해제, 수동 시: */
pci_free_irq_vectors(pdev);
코드 설명
- struct irq_affinity affd인터럽트 affinity 자동 분배 설정 구조체입니다.
pre_vectors는 affinity 분배에서 제외할 앞쪽 벡터 수(예: NVMe Admin Queue 전용 벡터),post_vectors는 뒤쪽 제외 벡터 수를 지정합니다.include/linux/interrupt.h에 정의되어 있습니다. - pci_alloc_irq_vectors_affinity()
PCI_IRQ_AFFINITY플래그와 함께 호출하면, 커널이 온라인 CPU에 벡터를 자동으로 분배합니다. MSI-X를 먼저 시도하고 실패 시 MSI로 폴백합니다. 내부적으로drivers/pci/msi/api.c의__pci_enable_msix_range()를 거쳐irq_create_affinity_masks()로 CPU 마스크를 계산합니다. - pci_irq_vector()할당된 벡터 인덱스에 대응하는 Linux IRQ 번호를 반환합니다. 이 번호를
devm_request_irq()에 전달하여 인터럽트 핸들러를 등록합니다. MSI-X의 경우 각 벡터가 독립적인 IRQ를 가지므로 큐별 전용 핸들러 할당이 가능합니다. - pci_free_irq_vectors()할당된 모든 MSI/MSI-X 벡터를 해제합니다.
devm_request_irq()로 등록한 핸들러는 자동 해제되지만, 벡터 자체는 이 함수로 명시적으로 반환해야 합니다.
sysfs affinity 인터페이스
# 특정 IRQ의 현재 affinity 확인
cat /proc/irq/<irq_num>/smp_affinity_list
# 출력 예: 0-3 (CPU 0~3에 분배)
# affinity를 CPU 4로 변경
echo 4 > /proc/irq/<irq_num>/smp_affinity_list
# managed_irq인 경우 (커널이 관리, 사용자 변경 불가)
cat /proc/irq/<irq_num>/effective_affinity_list
# NUMA 노드별 인터럽트 분포 확인
for irq in /proc/irq/*/smp_affinity_list; do
echo "$(dirname $irq | xargs basename): $(cat $irq)"
done | sort -t: -k2 -n
PCI_IRQ_AFFINITY 플래그 사용 시, 커널은 디바이스가 연결된 NUMA 노드의 CPU를 우선 할당합니다. 이는 NVMe, 고성능 NIC 등에서 캐시 미스와 크로스 노드 트래픽을 줄여 지연시간을 최소화합니다. irq_set_affinity_hint()로 드라이버가 힌트를 제공할 수도 있습니다.Interrupt Remapping (IOMMU)
Intel VT-d 또는 AMD-Vi의 Interrupt Remapping (IR)은 디바이스가 전송하는 MSI 메시지를 IOMMU가 중간에서 변환하여, 보안 격리와 유연한 인터럽트 라우팅을 제공합니다.
호환 형식 vs 리매핑 형식
| 특성 | 호환 형식 (Compatibility) | 리매핑 형식 (Remappable) |
|---|---|---|
| Message Address | APIC ID 직접 인코딩 | IRTE 인덱스 인코딩 |
| 보안 | 디바이스가 임의 CPU에 인터럽트 전송 가능 | IRTE 테이블로 제한됨 |
| Address[4] | Redirection Hint | Interrupt Format (1=remappable) |
| Address[3] | Destination Mode | SHV (Sub-Handle Valid) |
| Address[19:5] | Destination ID 상위 | IRTE Handle (인덱스) |
| Data[15:0] | Vector + Delivery 직접 | Sub-Handle (하위 인덱스) |
| IRTE 참조 | 없음 | handle + sub-handle → IRTE |
Posted Interrupts (VM 패스스루)
VT-d Posted Interrupts는 물리 디바이스의 MSI가 VM의 가상 APIC에 직접 주입되도록 합니다. vCPU가 실행 중이면 VM-Exit 없이 인터럽트가 전달되어, VFIO 패스스루 성능을 크게 향상시킵니다.
Posted Interrupt 흐름:
Device → MSI TLP → IOMMU → IRTE (Posted Interrupt Descriptor 참조)
├─ vCPU 실행 중: PI Notification → 가상 APIC 직접 주입 (no VM-Exit)
└─ vCPU 대기 중: Outstanding Notification → wakeup → VM-Entry 시 주입
intel_iommu=on + intremap=on으로 IR을 활성화하세요.멀티큐 드라이버 패턴
최신 고성능 디바이스는 하드웨어 멀티큐를 지원하며, 각 큐에 개별 MSI-X 벡터를 할당하여 CPU별 독립적인 인터럽트 처리가 가능합니다.
디바이스별 큐 구조
| 디바이스 | 벡터 배분 | 일반적 벡터 수 | 비고 |
|---|---|---|---|
| NVMe | 1 Admin + N I/O 큐 | CPU 수 + 1 | 각 I/O 큐가 CPU에 바인딩 |
| NIC (e.g. mlx5) | N RX + N TX (또는 Combined) | CPU 수 × 1~2 | ethtool -L로 조정 |
| RDMA (mlx5_ib) | N Completion 큐 | CPU 수 | CQ 완료 인터럽트 |
| GPU (AMDGPU) | 기능별 분리 | 디바이스 정의 | Compute, GFX, SDMA 등 |
멀티큐 드라이버 코드 패턴
static int mydev_setup_irqs(struct pci_dev *pdev,
struct mydev_priv *priv)
{
int num_cpus = num_online_cpus();
int nvecs, i;
struct irq_affinity affd = {
.pre_vectors = 1, /* 관리용 벡터 */
.post_vectors = 0,
};
/* MSI-X 우선 → MSI 폴백 → Legacy 폴백 */
nvecs = pci_alloc_irq_vectors_affinity(pdev,
2, /* 최소: admin + I/O 1개 */
num_cpus + 1, /* 최대: admin + CPU당 1개 */
PCI_IRQ_ALL_TYPES | PCI_IRQ_AFFINITY,
&affd);
if (nvecs < 0)
return nvecs;
priv->num_io_queues = nvecs - 1;
/* 벡터 0: Admin Queue */
if (devm_request_irq(&pdev->dev,
pci_irq_vector(pdev, 0),
mydev_admin_isr, 0,
"mydev-admin", priv))
goto err_free;
/* 벡터 1~N: I/O Queues */
for (i = 0; i < priv->num_io_queues; i++) {
int irq = pci_irq_vector(pdev, i + 1);
if (devm_request_irq(&pdev->dev, irq,
mydev_io_isr, 0,
"mydev-io", &priv->io_queues[i]))
goto err_free;
}
return 0;
err_free:
pci_free_irq_vectors(pdev);
return -ENOMEM;
}
pci_alloc_irq_vectors()가 요청보다 적은 벡터를 반환할 수 있습니다. 드라이버는 반환된 벡터 수에 맞춰 큐 수를 동적으로 조정해야 합니다. NVMe 드라이버는 min(num_possible_cpus(), max_hw_queues)로 최대 요청 수를 결정합니다.디버깅(Debugging)과 모니터링
MSI/MSI-X 관련 문제를 진단할 때 사용하는 주요 도구와 인터페이스입니다.
/proc/interrupts 해석
# MSI-X 벡터별 인터럽트 카운트 확인
grep "nvme" /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 36: 15234 0 0 0 IR-PCI-MSIX 0-edge nvme0q0
# 37: 0 28451 0 0 IR-PCI-MSIX 1-edge nvme0q1
# 38: 0 0 31205 0 IR-PCI-MSIX 2-edge nvme0q2
# 39: 0 0 0 29876 IR-PCI-MSIX 3-edge nvme0q3
# → IR-PCI-MSIX: Interrupt Remapping + PCI MSI-X
# → 각 큐가 서로 다른 CPU에 할당되어 있음 확인
lspci -vvv MSI Capability 디코딩
# MSI/MSI-X Capability 상세 확인
lspci -vvv -s 01:00.0 | grep -A 10 -E "MSI:|MSI-X:"
# Capabilities: [68] MSI: Enable- Count=1/1 Maskable- 64bit+
# Address: 0000000000000000 Data: 0000
# Capabilities: [a0] MSI-X: Enable+ Count=65 Masked-
# Vector table: BAR=0 offset=00002000
# PBA: BAR=0 offset=00003000
# → Enable+: MSI-X 활성화 상태
# → Count=65: 65개 벡터 테이블 엔트리
# → BAR=0 offset=00002000: BAR 0의 0x2000에 벡터 테이블
sysfs + dmesg 확인
# 디바이스의 MSI 활성화 상태 확인
cat /sys/bus/pci/devices/0000:01:00.0/msi_irqs/*
# 또는
ls /sys/bus/pci/devices/0000:01:00.0/msi_irqs/
# 36 37 38 39 → 할당된 IRQ 번호 목록
# MSI 활성화 여부
cat /sys/bus/pci/devices/0000:01:00.0/msi_bus
# 커널 부팅 시 MSI 관련 메시지 확인
dmesg | grep -i "msi\|irq.*vector"
# [ 1.234] nvme 0000:01:00.0: enabling device (0000 -> 0002)
# [ 1.235] nvme 0000:01:00.0: PCI->APIC IRQ transform: INT A -> IRQ 36
# [ 1.236] nvme nvme0: 4/0/0 default/read/poll queues
디버깅 도구 요약
| 도구/경로 | 용도 |
|---|---|
/proc/interrupts | CPU별 인터럽트 카운트, 벡터 타입 확인 |
lspci -vvv | MSI/MSI-X Capability 디코딩, BAR 오프셋 |
/sys/bus/pci/devices/*/msi_irqs/ | 할당된 IRQ 번호 목록 |
/proc/irq/*/smp_affinity_list | IRQ → CPU affinity 매핑 |
/proc/irq/*/effective_affinity_list | 실제 적용된 affinity (managed_irq) |
dmesg | MSI 할당/실패 메시지, irq_domain 로그 |
ftrace (irq_handler_entry) | 인터럽트 핸들러 호출 추적(Call Trace) |
perf stat -e irq_vectors:* | 인터럽트 벡터 이벤트 통계 |
SR-IOV (Single Root I/O Virtualization)
SR-IOV는 하나의 물리 PCIe 디바이스(PF)를 여러 개의 가상 기능(VF)으로 분할하여, 각 VF를 독립적인 VM 또는 컨테이너에 직접 할당(passthrough)할 수 있게 하는 하드웨어 가상화 기술입니다.
AER (Advanced Error Reporting)
PCIe AER는 에러를 Correctable과 Uncorrectable (Non-Fatal / Fatal)로 분류하여 보고합니다:
| 분류 | 예시 | 처리 |
|---|---|---|
| Correctable | Bad TLP (CRC 오류 후 재전송 성공), Replay Timer Timeout | 하드웨어가 자동 복구, 카운터 증가 |
| Uncorrectable Non-Fatal | Completion Timeout, Unexpected Completion | 트랜잭션 실패, 디바이스 리셋 가능 |
| Uncorrectable Fatal | Data Link Protocol Error, Malformed TLP, Poisoned TLP | 링크 다운, 디바이스 리셋 필수 |
/* PCI 드라이버에서 AER 에러 복구 콜백 등록 */
static pci_ers_result_t
my_error_detected(struct pci_dev *pdev, pci_channel_state_t state)
{
if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;
/* I/O 중단, 리소스 정리 */
my_stop_io(pdev);
return PCI_ERS_RESULT_NEED_RESET;
}
static pci_ers_result_t
my_slot_reset(struct pci_dev *pdev)
{
/* 디바이스 재초기화 */
if (my_reinit_hw(pdev))
return PCI_ERS_RESULT_DISCONNECT;
return PCI_ERS_RESULT_RECOVERED;
}
static void my_resume(struct pci_dev *pdev)
{
/* I/O 재개 */
my_restart_io(pdev);
}
static const struct pci_error_handlers my_err_handler = {
.error_detected = my_error_detected,
.slot_reset = my_slot_reset,
.resume = my_resume,
};
static struct pci_driver my_driver = {
/* ... */
.err_handler = &my_err_handler,
};
error_detected() → (FLR 또는 Secondary Bus Reset) → slot_reset() → resume(). 드라이버가 err_handler를 등록하지 않으면 커널은 디바이스를 비활성화합니다.AER Extended Capability 레지스터
AER Extended Capability (ID 0001h)는 PCIe 확장 Config Space (오프셋 0x100+)에 위치하며, 에러 감지/보고/마스킹을 제어합니다:
| 오프셋 | 레지스터 | 설명 |
|---|---|---|
| +00h | AER Enhanced Capability Header | ID=0001h, Version, Next Pointer |
| +04h | Uncorrectable Error Status | 비트별 에러 발생 여부 (RW1C — 1 쓰기로 클리어) |
| +08h | Uncorrectable Error Mask | 마스크된 에러는 보고되지 않음 |
| +0Ch | Uncorrectable Error Severity | 0=Non-Fatal, 1=Fatal 지정 |
| +10h | Correctable Error Status | Correctable 에러 발생 여부 (RW1C) |
| +14h | Correctable Error Mask | 마스크 제어 |
| +18h | Advanced Error Capabilities and Control | ECRC 생성/확인 활성화, First Error Pointer |
| +1Ch~+28h | Header Log | 에러를 유발한 TLP 헤더 4 DW 캡처 |
| +2Ch | Root Error Command | Root Port 전용: CE/NFE/FE 인터럽트 활성화 |
| +30h | Root Error Status | Root Port 전용: 에러 수신 상태 |
| +34h~+38h | Error Source ID | 에러를 보고한 디바이스의 BDF |
| +3Ch~+44h | TLP Prefix Log | TLP Prefix 포함 에러 시 캡처 (PCIe 3.0+) |
| Uncorrectable Error 비트 | 이름 | 기본 Severity | 설명 |
|---|---|---|---|
| 4 | Data Link Protocol Error | Fatal | DLLP/TLP 시퀀스 에러, 링크 레벨 실패 |
| 5 | Surprise Down Error | Fatal | 예기치 않은 링크 다운 (핫 리무브 등) |
| 12 | Poisoned TLP Received | Non-Fatal | EP 비트가 설정된 TLP 수신 (데이터 오염) |
| 13 | Flow Control Protocol Error | Fatal | 크레딧 프로토콜 위반 |
| 14 | Completion Timeout | Non-Fatal | Non-Posted 요청에 대한 Completion 미수신 |
| 15 | Completer Abort | Non-Fatal | Completer가 요청을 거부 (CA status) |
| 16 | Unexpected Completion | Non-Fatal | 요청하지 않은 Completion 수신 |
| 18 | Malformed TLP | Fatal | TLP 형식 오류 (잘못된 길이, 필드 등) |
| 19 | ECRC Error | Non-Fatal | End-to-End CRC 검증 실패 |
| 20 | Unsupported Request Error | Non-Fatal | 디바이스가 처리할 수 없는 TLP 수신 (UR) |
| 21 | ACS Violation | Non-Fatal | ACS 정책에 의해 차단된 트랜잭션 |
# AER 에러 카운터 확인 (sysfs)
cat /sys/bus/pci/devices/0000:03:00.0/aer_dev_correctable
# RxErr 0 BadTLP 12 BadDLLP 0 Rollover 0 Timeout 0 NonFatalErr 0 ...
cat /sys/bus/pci/devices/0000:03:00.0/aer_dev_nonfatal
# Undefined 0 DLP 0 SDES 0 TLP 0 FCP 0 CmpltTO 3 CmpltAbrt 0 ...
# AER 에러 주입 (디버깅/테스트용)
echo 1 > /sys/kernel/debug/pci-error-inject/0000:03:00.0/inject_uncorrectable
# (CONFIG_PCIEAER_INJECT=y 필요)
# AER 트레이스포인트 활성화
echo 1 > /sys/kernel/debug/tracing/events/ras/aer_event/enable
cat /sys/kernel/debug/tracing/trace_pipe
# aer_event: 0000:03:00.0 PCIe Bus Error: severity=Corrected, type=Data Link Layer ...
AER 에러 처리 전체 흐름
DPC (Downstream Port Containment)
DPC는 PCIe 3.1에서 도입된 에러 격리 메커니즘으로, Uncorrectable Error 발생 시 해당 다운스트림 포트를 자동으로 비활성화하여 에러가 상위 버스로 전파되는 것을 방지합니다:
| 구분 | AER만 사용 | AER + DPC |
|---|---|---|
| 에러 감지 | Root Port에서 ERR_* 메시지 수신 | 다운스트림 포트에서 즉시 감지 |
| 에러 전파 | 상위 버스에 영향 가능 | 포트 비활성화로 격리 (하위만 영향) |
| 복구 범위 | 전체 버스 리셋 가능 | 해당 포트 하위만 리셋 |
| 트리거 | 소프트웨어 (커널 AER 드라이버) | 하드웨어 자동 (DPC Trigger Status) |
| 커널 드라이버 | aer.c | dpc.c (PCIe Port Service) |
DPC Extended Capability (ID 001Dh) 핵심 레지스터:
DPC Control Register:
| 비트 | 필드 | 설명 |
|---|---|---|
| [1:0] | DPC Trigger Enable | 00 = 비활성화, 01 = ERR_FATAL만, 10 = ERR_NONFATAL 또는 ERR_FATAL |
| [2] | Completion Control | 1 = DPC 트리거 시 UR Completion 반환 |
| [3] | Interrupt Enable | DPC 이벤트 인터럽트 활성화 |
| [4] | ERR_COR Enable | Correctable 에러도 DPC 트리거 (RP만) |
DPC Status Register:
| 비트 | 필드 | 설명 |
|---|---|---|
| [0] | Trigger Status | 1 = DPC가 트리거됨 (포트 비활성화 상태) |
| [1] | Trigger Reason | 0 = ERR_FATAL, 1 = ERR_NONFATAL |
| [2] | Interrupt Status | 인터럽트 발생 여부 |
| [4:3] | Trigger Reason Extension | RP PIO 에러 세부 이유 |
Linux DPC 드라이버 (drivers/pci/pcie/dpc.c) 트리거 발생 시 처리 흐름:
dpc_irq()ISR 호출 → 워크큐로 지연 처리dpc_process_error()— Header Log 읽기, 에러 원인 로깅pcie_do_recovery()호출 — AER와 동일한 복구 경로 사용- 복구 성공 시 DPC Trigger Status 클리어 → 포트 재활성화
# DPC Capability 확인
lspci -vvv -s 00:1c.0 | grep -A 5 "Downstream Port Containment"
# DpcCap: INT Msg #0, RPExt+ PoisonedTLP+ SwTrigger+ RP PIO Log 4 ...
# DpcCtl: Trigger:2 Cmpl+ INT+ ERR_COR- ...
# DpcSta: Trigger- Reason:00 ...
# DPC 이벤트 로그
dmesg | grep -i dpc
# pcieport 0000:00:1c.0: DPC: containment event, status:0x1f01 source:0x0300
ACS (Access Control Services)
ACS는 PCIe 디바이스 간의 직접 통신(peer-to-peer)을 제어하여 IOMMU 격리를 강화합니다. 가상화 환경에서 VF 간의 트래픽이 IOMMU를 우회하는 것을 방지합니다:
| ACS 비트 | 이름 | 설명 |
|---|---|---|
| V | ACS Source Validation | Requester ID가 업스트림 포트의 것인지 검증 |
| B | ACS Translation Blocking | ATS(Address Translation Services) 번역된 TLP 차단 |
| R | ACS P2P Request Redirect | 피어 간 직접 요청을 업스트림으로 강제 리다이렉트 |
| C | ACS P2P Completion Redirect | 피어 간 Completion을 업스트림으로 리다이렉트 |
| U | ACS Upstream Forwarding | 업스트림으로 전달되어야 할 TLP의 포워딩 허용 |
| E | ACS P2P Egress Control | 특정 피어로의 직접 전송을 비트맵(Bitmap)으로 제어 |
| T | ACS Direct Translated P2P | ATS 번역된 P2P 요청에 대한 세밀한 제어 |
- IOMMU는 디바이스 메모리 접근을 격리하지만, PCIe 스위치 내부에서 피어 디바이스 간 직접 통신(P2P)이 가능하면 IOMMU를 우회할 수 있습니다.
- ACS는 이러한 P2P 트래픽을 Root Complex 방향(업스트림)으로 강제 리다이렉트하여 IOMMU를 반드시 거치게 합니다.
- ACS가 없는 PCIe 스위치에 연결된 디바이스들은 같은 IOMMU 그룹에 배치되어 개별 격리가 불가능합니다.
- 커널은
pci_acs_enabled()로 ACS 상태를 확인하며, VFIO는 IOMMU 그룹 단위로 디바이스를 할당합니다. - ACS Override: ACS를 지원하지 않는 하드웨어에서
pcie_acs_override=downstream,multifunction커널 파라미터로 강제 분리할 수 있지만, 보안 위험이 있습니다.
# ACS Capability 확인
lspci -vvv -s 00:1c.0 | grep -A 3 "Access Control Services"
# ACSCap: SrcValid+ TransBlk+ ReqRedir+ CmpltRedir+ UpFwd- EgressCtrl- DirectTrans-
# ACSCtl: SrcValid+ TransBlk+ ReqRedir+ CmpltRedir+ UpFwd- EgressCtrl- DirectTrans-
# IOMMU 그룹 확인 (ACS의 결과)
find /sys/kernel/iommu_groups/ -type l | sort -V
# /sys/kernel/iommu_groups/1/devices/0000:03:00.0
# /sys/kernel/iommu_groups/2/devices/0000:04:00.0 ← ACS 덕분에 별도 그룹
전원 관리
PCI 전원 상태 (D-States)
| 상태 | 설명 | 복원 시간 |
|---|---|---|
| D0 | 완전 동작 상태 | 즉시 |
| D1 | 절전 (일부 컨텍스트 유지, 선택적) | 빠름 |
| D2 | 더 깊은 절전 (선택적) | 중간 |
| D3hot | 소프트웨어 절전, Vaux 유지, Config Space 접근 가능 | 10ms+ |
| D3cold | 전원 완전 차단, 재열거 필요 | 100ms+ |
/* PCI 드라이버 전원 관리 콜백 */
static int my_suspend(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
/* 디바이스 I/O 중단, 상태 저장 */
my_stop_hw(pdev);
pci_save_state(pdev);
pci_disable_device(pdev);
pci_set_power_state(pdev, PCI_D3hot);
return 0;
}
static int my_resume(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
pci_set_power_state(pdev, PCI_D0);
pci_restore_state(pdev);
pci_enable_device(pdev);
pci_set_master(pdev);
my_start_hw(pdev);
return 0;
}
static DEFINE_SIMPLE_DEV_PM_OPS(my_pm_ops, my_suspend, my_resume);
static struct pci_driver my_driver = {
/* ... */
.driver.pm = pm_sleep_ptr(&my_pm_ops),
};
코드 설명
- my_suspend()시스템 절전(suspend) 진입 시 호출됩니다.
pci_save_state()로 Configuration Space 전체(BAR, Command, Interrupt Line 등)를 메모리에 백업하고,pci_disable_device()로 디바이스를 비활성화한 뒤,pci_set_power_state(pdev, PCI_D3hot)으로 D3hot 저전력 상태로 전환합니다. - my_resume()시스템 복원(resume) 시 호출됩니다.
PCI_D0으로 전원 상태를 복원하고,pci_restore_state()로 저장된 Configuration Space를 디바이스에 다시 기록합니다. 이후pci_enable_device()와pci_set_master()로 디바이스를 재활성화합니다. 이 순서가 바뀌면 디바이스가 정상 동작하지 않을 수 있습니다. - DEFINE_SIMPLE_DEV_PM_OPSsuspend/resume 콜백 쌍을
dev_pm_ops구조체로 래핑하는 매크로입니다.include/linux/pm.h에 정의되어 있으며,CONFIG_PM_SLEEP비활성 시 콜백을 NULL로 최적화합니다. - pm_sleep_ptr()
CONFIG_PM_SLEEP이 꺼져 있으면 NULL을 반환하여 불필요한 코드가 링크되지 않도록 합니다. PCI 드라이버에서.driver.pm에 이 매크로를 사용하는 것이 현재 권장 패턴입니다.
ASPM (Active State Power Management)
PCIe 링크가 유휴 상태(Idle State)일 때 자동으로 저전력 링크 상태로 전환합니다:
| 상태 | 설명 | 진입/탈출 지연 |
|---|---|---|
| L0 | 완전 동작 (정상 전송) | — |
| L0s | 빠른 저전력, 단방향 | ~1 μs |
| L1 | 깊은 저전력, 양방향 링크 비활성 | 2~10 μs |
| L1.1 | L1 Substate, PLL 꺼짐 | ~32 μs |
| L1.2 | L1 Substate, 공통 모드 전압 제거 | ~32~100 μs |
| L2/L3 | 보조 전원 / 전원 차단 | ms 단위 |
# ASPM 정책 확인 및 설정
cat /sys/module/pcie_aspm/parameters/policy
# [default] performance powersave powersupersave
# 커널 부팅 파라미터
pcie_aspm=off # ASPM 완전 비활성화 (저지연 요구 시)
pcie_aspm.policy=powersave # 절전 우선
pcie_aspm=off를 고려하세요.L1 서브스테이트 (L1.1 / L1.2) 상세
PCIe 3.1에서 도입된 L1 PM Substates (Extended Capability ID 001Eh)는 L1 상태 내에서 더 세밀한 전력 절약을 제공합니다:
# L1 Substates Capability 확인
lspci -vvv -s 03:00.0 | grep -A 10 "L1 PM Substates"
# L1SubCap: PCI-PM_L1.2+ PCI-PM_L1.1+ ASPM_L1.2+ ASPM_L1.1+ L1_PM_Substates+
# PortCommonModeRestoreTime=60us PortTPowerOnTime=10us
# L1SubCtl1: PCI-PM_L1.2+ PCI-PM_L1.1+ ASPM_L1.2+ ASPM_L1.1+
# T_CommonMode=0us LTR1.2_Threshold=163840ns
# LTR (Latency Tolerance Reporting) 확인
lspci -vvv -s 03:00.0 | grep LTR
# LTR+ DevCtl2: ... LTRen+
# LTR Max snoop latency: 3145728ns
# LTR Max no snoop latency: 3145728ns
# CLKREQ# 핀 상태 확인 (플랫폼 의존)
# L1.2 진입에는 CLKREQ# 핀이 물리적으로 연결되어야 합니다.
# 서버에서는 CLKREQ#가 연결되지 않아 L1.2 불가능한 경우가 많습니다.
PCIe 리셋 메커니즘
PCIe는 여러 수준의 리셋을 정의하며, 각 리셋의 범위와 복원 수준이 다릅니다:
| 리셋 유형 | 트리거 방법 | 범위 | Config Space | 커널 API |
|---|---|---|---|---|
| Fundamental (Cold) | 전원 차단 → 재인가 | 전체 시스템 | 완전 초기화 | — |
| Fundamental (Warm) | PERST# 핀 assert | 전체 PCIe 계층 | 완전 초기화 | — |
| Hot Reset | Bridge: Secondary Bus Reset | 하위 버스 전체 | 완전 초기화 | pci_reset_bus() |
| FLR | Device Control: Initiate FLR | 단일 펑션 | 대부분 보존 | pci_reset_function() |
| PM Reset | D3hot → D0 전환 | 단일 펑션 | 대부분 보존 | pci_set_power_state() |
| AF FLR | Advanced Features FLR | 단일 펑션 | 대부분 보존 | pci_reset_function() |
/* 커널에서 PCIe 리셋 수행 */
/* 1. FLR (Function Level Reset) — 가장 세밀한 리셋 */
int ret = pci_reset_function(pdev);
/* FLR → AF FLR → PM Reset → Bus Reset 순서로 시도
* pci_probe_reset_function()으로 지원 여부 확인 가능 */
/* 2. 잠금 없는 FLR (드라이버 내부에서 직접 호출 시) */
pci_reset_function_locked(pdev);
/* 3. Secondary Bus Reset — 하위 버스 전체 리셋 */
pci_reset_bus(pdev);
/* Bridge의 Secondary Bus Reset 비트를 설정/해제
* 하위 버스의 모든 디바이스가 리셋됨 — 다른 디바이스에 영향 주의! */
/* 4. sysfs를 통한 리셋 */
/* echo 1 > /sys/bus/pci/devices/0000:03:00.0/reset */
/* 5. FLR 지원 여부 확인 */
if (pdev->has_flr) {
pr_info("Device supports FLR\n");
pcie_flr(pdev); /* FLR 직접 수행 (100ms 대기 포함) */
}
/* 리셋 후 주의사항:
* - FLR 후에도 BAR 매핑은 유지되지만, 디바이스 내부 상태는 초기화됨
* - MSI/MSI-X는 재설정 필요: pci_alloc_irq_vectors() 재호출
* - Bus Master, Memory Space 비트 재활성화 필요
* - DMA 매핑은 유효하지만, 디바이스가 DMA를 중단한 상태이므로 재시작 필요
*/
- Bus Reset은 같은 버스의 모든 디바이스에 영향을 줍니다. SR-IOV VF를 포함한 모든 펑션이 리셋됩니다.
- FLR 후 100ms 대기가 필수입니다. 디바이스가 내부 초기화를 완료할 시간이 필요합니다.
- VFIO 패스스루 환경에서 VM 재부팅 시 FLR을 사용하여 이전 VM의 상태를 완전히 클리어해야 합니다. FLR을 지원하지 않는 디바이스는 패스스루에 적합하지 않습니다.
- D3cold → D0은 Fundamental Reset과 동등하며, 디바이스 재열거가 필요할 수 있습니다.
PCI 핫플러그 (Hotplug)
PCIe 디바이스의 런타임 삽입/제거를 지원하는 메커니즘:
| 메커니즘 | 설명 |
|---|---|
| Native PCIe Hotplug | PCIe Slot Capability 기반, 커널 pciehp 드라이버 |
| ACPI Hotplug | ACPI _HPP/_HPX 메서드 기반, 서버 플랫폼 |
| Thunderbolt/USB4 | PCIe tunneling, 동적 디바이스 연결/해제 |
| Surprise Removal | 사전 알림 없는 제거, 드라이버가 적절히 처리해야 함 |
# 수동 핫플러그 (sysfs)
# 디바이스 제거
echo 1 > /sys/bus/pci/devices/0000:03:00.0/remove
# 버스 재스캔 (새 디바이스 탐지)
echo 1 > /sys/bus/pci/rescan
# 특정 브릿지 하위만 재스캔
echo 1 > /sys/bus/pci/devices/0000:00:1c.0/rescan
pciehp 드라이버 내부 구현
PCIe Native Hotplug는 drivers/pci/hotplug/pciehp*.c에 구현된 pciehp 드라이버가 담당합니다. Root Port나 Downstream Port의 Slot Capability에 정의된 하드웨어 이벤트를 감지하고 처리합니다.
/* drivers/pci/hotplug/pciehp_ctrl.c — 핫플러그 이벤트 처리 (simplified) */
/* Slot Capability의 인터럽트로 호출되는 워크 핸들러 */
static void pciehp_handle_presence_or_link_change(
struct controller *ctrl, u32 events)
{
if (events & PCI_EXP_SLTSTA_PDC) {
/* Presence Detect Changed — 카드 삽입/제거 감지 */
bool present = pciehp_card_present(ctrl);
if (present)
pciehp_enable_slot(ctrl); /* 새 디바이스 열거 */
else
pciehp_disable_slot(ctrl); /* 디바이스 제거 처리 */
}
if (events & PCI_EXP_SLTSTA_DLLSC) {
/* Data Link Layer State Changed — 링크 업/다운 */
bool link_active = pciehp_check_link_active(ctrl);
if (link_active)
pciehp_enable_slot(ctrl);
else
pciehp_disable_slot(ctrl);
}
}
/* 디바이스 활성화: 전원 → 링크 → 열거 → 드라이버 바인딩 */
static void pciehp_enable_slot(struct controller *ctrl)
{
struct pci_bus *parent = ctrl->pcie->port->subordinate;
/* 1. 슬롯 전원 공급 (Power Controller 지원 시) */
pciehp_power_on_slot(ctrl);
/* 2. 링크 훈련(Training) 대기 (최대 1초) */
pciehp_check_link_status(ctrl);
/* 3. 하위 버스 디바이스 스캔 + 리소스 할당 */
pci_lock_rescan_remove();
pci_scan_slot(parent, PCI_DEVFN(0, 0));
pci_bus_assign_resources(parent);
/* 4. 드라이버 매칭 및 probe() 호출 */
pci_bus_add_devices(parent);
pci_unlock_rescan_remove();
}
/* 디바이스 비활성화: 드라이버 해제 → 디바이스 제거 → 전원 차단 */
static void pciehp_disable_slot(struct controller *ctrl)
{
struct pci_bus *parent = ctrl->pcie->port->subordinate;
pci_lock_rescan_remove();
/* 하위 모든 디바이스의 remove() 호출 + sysfs 제거 */
pci_stop_and_remove_bus_device(parent->self);
pci_unlock_rescan_remove();
/* 슬롯 전원 차단 */
pciehp_power_off_slot(ctrl);
}
코드 설명
- PCI_EXP_SLTSTA_PDCSlot Status 레지스터의 Presence Detect Changed 비트입니다. 슬롯에 카드가 물리적으로 삽입되거나 제거되면 이 비트가 설정되고 MSI/MSI-X 인터럽트가 발생합니다. Electromechanical Interlock이 있는 서버 슬롯에서는 Attention Button 이벤트와 함께 사용됩니다.
- PCI_EXP_SLTSTA_DLLSCData Link Layer State Changed 비트입니다. PCIe 링크가 훈련을 완료하여 DL_Up 상태가 되거나, 링크가 다운되면 발생합니다. Surprise Removal 시 PDC보다 먼저 감지되는 경우가 많습니다.
- pciehp_enable_slot()핫플러그 삽입 처리의 핵심입니다. 전원 공급 → 링크 훈련 대기 →
pci_scan_slot()으로 디바이스 열거 →pci_bus_assign_resources()로 BAR 할당 →pci_bus_add_devices()로 sysfs 등록 및 드라이버 매칭의 순서로 진행합니다. - pci_stop_and_remove_bus_device()디바이스의
remove()콜백을 호출하고, sysfs에서 제거하며,pci_dev구조체를 해제합니다. 하위 버스가 있는 브리지인 경우 재귀적으로 모든 하위 디바이스를 먼저 제거합니다.
Slot Capability 레지스터
| 필드 | 설명 |
|---|---|
| Attention Button Present | 슬롯에 Attention Button이 있음 (서버 핫플러그) |
| Power Controller Present | 소프트웨어 제어 가능한 전원 스위치 존재 |
| MRL Sensor Present | Manually-operated Retention Latch 센서 존재 |
| Attention Indicator Present | 주의 LED 존재 (황색) |
| Power Indicator Present | 전원 LED 존재 (녹색) |
| Hot-Plug Surprise | 사전 알림 없는 제거(Surprise Removal) 지원 |
| Hot-Plug Capable | 핫플러그 지원 슬롯 |
| Electromechanical Interlock | 카드 잠금 장치 존재 |
| No Command Completed | Command Completed 인터럽트 미지원 |
| Physical Slot Number | 슬롯 번호 (섀시 표시와 매칭) |
# Slot Capability 확인
lspci -vvv -s 00:1c.0 | grep -A 15 "Slot\|SltCap\|SltCtl\|SltSta"
# SltCap: AttnBtn+ PwrCtrl+ MRL- AttnInd+ PwrInd+ HotPlug+ Surprise-
# Slot #1, PowerLimit 75W; Interlock- NoCompl-
# SltCtl: Enable: AttnBtn+ PwrFlt+ MRL- PresDet+ CmdCplt- HPIrq+ LinkChg+
# Control: AttnInd Off, PwrInd On, Power On, Interlock-
# SltSta: Status: AttnBtn- PowerFlt- MRL- CmdCplt- PresDet+ Interlock-
# Changed: MRL- PresDet+ LinkState+
Surprise Removal 드라이버 대응
Thunderbolt/USB4 외부 디바이스나 핫스왑(Hot-Swap) 환경에서 사전 알림 없이 디바이스가 제거될 수 있습니다. 드라이버는 이 상황에서 크래시 없이 정상적으로 정리해야 합니다.
/* Surprise Removal에 안전한 PCI 드라이버 패턴 */
static bool mydev_is_removed(struct pci_dev *pdev)
{
u16 vendor;
/* Config Space 읽기 — 제거된 디바이스는 0xFFFF 반환 */
pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor);
return vendor == (u16)~0;
}
static irqreturn_t mydev_isr(int irq, void *data)
{
struct mydev_priv *priv = data;
u32 status;
/* MMIO 읽기 — 제거 시 0xFFFFFFFF 반환 */
status = ioread32(priv->regs + MYDEV_INTR_STATUS);
if (status == ~(u32)0) {
/* 디바이스 제거 감지 — 인터럽트 폭주(storm) 방지 */
return IRQ_HANDLED;
}
/* 정상 인터럽트 처리 ... */
return IRQ_HANDLED;
}
static void mydev_remove(struct pci_dev *pdev)
{
struct mydev_priv *priv = pci_get_drvdata(pdev);
/* 하드웨어 접근 전에 제거 상태 확인 */
if (!mydev_is_removed(pdev)) {
/* 디바이스가 존재하면 정상 종료 시퀀스 */
mydev_shutdown_hw(priv);
}
/* 디바이스 존재 여부와 무관한 정리 (메모리 해제 등) */
pci_free_irq_vectors(pdev);
/* pcim_*/devm_* 사용 시 나머지는 자동 해제 */
}
코드 설명
- Vendor ID 0xFFFF 확인PCIe 디바이스가 물리적으로 제거되면 Config Space 읽기가 Unsupported Request(UR)을 발생시키고, Root Complex가 올-1(0xFFFF)을 반환합니다. 이는 디바이스 존재 여부를 판별하는 표준 방법입니다.
- MMIO 읽기 0xFFFFFFFF매핑된 BAR 영역에 대한 읽기도 제거 시 올-1을 반환합니다. ISR에서 상태 레지스터 값이 올-1이면 Surprise Removal로 판단하고, 하드웨어 접근을 중단해야 합니다. 이를 무시하면 인터럽트 폭주(storm)가 발생할 수 있습니다.
- remove()에서 존재 확인pciehp가 Surprise Removal을 감지하면 드라이버의
remove()를 호출합니다. 이때 디바이스는 이미 물리적으로 없으므로, MMIO/Config Space 접근이 실패합니다.remove()는 하드웨어 접근 전에 반드시 디바이스 존재를 확인해야 합니다.
- ISR에서 상태 레지스터 값이
0xFFFFFFFF인지 확인하여 인터럽트 폭주를 방지합니다. remove()에서 하드웨어 접근 전 Vendor ID를 확인합니다.pcim_*/devm_*Managed API를 사용하면 리소스 해제가 자동으로 처리됩니다.- 워커(Worker) 스레드나 타이머에서 디바이스에 접근하는 경우,
remove()에서 먼저 취소해야 합니다. - DMA 전송 중 제거되면 IOMMU가 DMA를 차단합니다. 완료되지 않은 DMA를 안전하게 정리하세요.
핫플러그 디버깅
# pciehp 디버그 메시지 활성화
echo 'module pciehp +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w | grep pciehp
# 핫플러그 이벤트 모니터링
udevadm monitor --subsystem-match=pci
# Slot Status 변화 추적
watch -n 1 'lspci -vvv -s 00:1c.0 | grep SltSta'
# Thunderbolt/USB4 핫플러그 확인
dmesg | grep -i "thunderbolt\|usb4"
cat /sys/bus/thunderbolt/devices/*/authorized
PCIe 리셋 종류
PCIe 디바이스를 초기 상태로 되돌리는 여러 리셋 메커니즘이 있습니다. 리셋 범위와 영향이 다르므로 상황에 맞는 리셋을 선택해야 합니다:
| 리셋 종류 | 범위 | 영향 | 커널 API | 사용 시점 |
|---|---|---|---|---|
| Fundamental Reset | 링크(Link) + 모든 Function | 전원 차단/재인가 또는 PERST# 어서트(Assert). Config Space 전체 초기화, 링크 재훈련 필요. BAR 할당 소실 | pci_reset_bus() (간접) |
D3cold 전환, 시스템 재부팅, 하드웨어 행(Hang) 복구 |
| Hot Reset | 링크(Link) + 모든 Function | 업스트림 포트가 TS1에 Hot Reset 비트 설정. Config Space 초기화 (BAR 값 소실), 링크 재훈련. Fundamental Reset과 유사하나 전원은 유지 | pci_reset_bus() |
같은 버스의 모든 디바이스를 리셋해야 할 때, 브릿지 하위 전체 복구 |
| FLR (Function Level Reset) | 단일 Function | 해당 Function의 내부 상태만 초기화. Config Space의 BAR, Command, Status 등은 유지. 같은 디바이스의 다른 Function에 영향 없음 | pci_reset_function()__pci_reset_function_locked() |
VFIO 패스스루 VM 재부팅, 단일 Function 오류 복구, SR-IOV VF 리셋 |
| SBR (Secondary Bus Reset) | 브릿지 하위 전체 버스 | Bridge Control 레지스터의 비트 6을 설정/해제. 하위 버스의 모든 디바이스에 Hot Reset 전달 | pci_reset_bus()pci_bridge_secondary_bus_reset() |
PCIe Switch 하위 전체 리셋, FLR 미지원 디바이스의 대안 |
| D3hot → D0 | 단일 디바이스 | 전원 상태(Power State) 전환. 내부 상태 일부 초기화될 수 있으나, 표준에서 보장하지 않음. Config Space는 유지 | pci_set_power_state() |
PM 기반 소프트 리셋, FLR 미지원 시 대안 (신뢰도 낮음) |
/* PCIe 리셋 API 사용 예시 */
#include <linux/pci.h>
/* 1. FLR (Function Level Reset) — 가장 세밀한 리셋 */
int reset_single_function(struct pci_dev *pdev)
{
int ret;
/* FLR 지원 여부 확인 */
if (!pdev->has_flr) {
dev_warn(&pdev->dev, "FLR not supported\n");
return -ENOTTY;
}
/* FLR 수행: 드라이버 해제 → 리셋 → 100ms 대기 → 복구 */
ret = pci_reset_function(pdev);
if (ret)
dev_err(&pdev->dev, "FLR failed: %d\n", ret);
/* 리셋 후: Bus Master + Memory Space 재활성화 */
pci_set_master(pdev);
return ret;
}
/* 2. SBR (Secondary Bus Reset) — 브릿지 하위 전체 리셋 */
int reset_downstream_bus(struct pci_dev *bridge)
{
/* 브릿지의 Secondary Bus에 연결된 모든 디바이스 리셋 */
return pci_reset_bus(bridge);
}
/* 3. 리셋 메서드 우선순위 (커널 내부):
* pci_reset_function()은 다음 순서로 시도:
* (1) 디바이스 고유 리셋 (dev->reset_fn)
* (2) FLR (has_flr 플래그)
* (3) AF FLR (Advanced Features)
* (4) PM Reset (D3hot → D0 전환)
* 모두 실패하면 -ENOTTY 반환
*/
코드 설명
- pci_reset_function()단일 Function을 리셋하는 최상위 API입니다. 내부적으로 FLR, AF FLR, PM Reset 순으로 사용 가능한 메커니즘을 시도합니다. 리셋 전에 해당 디바이스의 드라이버를 해제(unbind)하고, 리셋 후 재바인딩합니다.
drivers/pci/pci.c에 구현되어 있습니다. - __pci_reset_function_locked()
pci_reset_function()과 동일하지만, 호출자가 이미device_lock()을 잡고 있을 때 사용합니다. VFIO 드라이버 내부에서 주로 호출됩니다. - pci_reset_bus()디바이스가 연결된 버스 전체를 리셋합니다. 내부적으로 SBR(Secondary Bus Reset)을 수행하며, 같은 버스의 모든 디바이스에 영향을 줍니다. SR-IOV VF를 포함한 하위 모든 Function이 리셋됩니다.
- 리셋 후 복구FLR 후 Config Space의 Command 레지스터가 초기화되므로 Bus Master 비트와 Memory Space Enable 비트를 재설정해야 합니다. MSI/MSI-X도 재설정이 필요하며,
pci_alloc_irq_vectors()를 다시 호출해야 합니다.
- VFIO 패스스루: FLR이 가장 이상적입니다. VM 재부팅 시 이전 상태를 완전히 클리어하면서 다른 Function에 영향을 주지 않습니다.
- FLR 미지원 디바이스: PM Reset(D3hot → D0)을 시도하되, 이는 모든 디바이스에서 완전한 리셋을 보장하지 않습니다. SBR이 더 확실하지만 같은 버스의 다른 디바이스에도 영향을 줍니다.
- AER 복구 흐름: 커널 AER 드라이버가 오류 심각도에 따라 FLR 또는 SBR을 자동으로 선택합니다. 드라이버는
pci_error_handlers의slot_reset()콜백에서 복구 로직을 구현합니다.
VFIO (Virtual Function I/O)
VFIO는 PCI 디바이스를 유저스페이스(또는 VM)에 안전하게 직접 노출하는 프레임워크입니다. IOMMU 기반 격리를 통해 호스트 메모리 보호를 보장합니다.
| 개념 | 설명 |
|---|---|
| VFIO Container | /dev/vfio/vfio — IOMMU 컨텍스트 관리 |
| VFIO Group | /dev/vfio/<N> — IOMMU 그룹 단위 디바이스 집합 |
| VFIO Device | 그룹 내 개별 디바이스 — Config Space, BAR, 인터럽트 접근 |
| IOMMU Group | 동일 IOMMU 도메인을 공유하는 디바이스 집합 (ACS 기반 분리) |
# VFIO 디바이스 패스스루 설정 (QEMU/KVM)
# 1. 디바이스를 vfio-pci에 바인딩
modprobe vfio-pci
echo "8086 1572" > /sys/bus/pci/drivers/vfio-pci/new_id
# 2. QEMU에서 디바이스 할당
qemu-system-x86_64 \
-device vfio-pci,host=0000:03:00.0 \
...
# IOMMU 그룹 확인
ls -l /sys/bus/pci/devices/0000:03:00.0/iommu_group/devices/
유저스페이스 VFIO 프로그래밍
VFIO API를 사용하면 유저스페이스에서 PCI 디바이스의 Config Space, BAR, 인터럽트, DMA를 직접 제어할 수 있습니다. DPDK, SPDK, QEMU 등이 이 방식으로 고성능 I/O를 구현합니다.
/* VFIO를 이용한 PCI 디바이스 직접 제어 (유저스페이스 C 코드) */
#include <linux/vfio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
int vfio_pci_example(void)
{
int container, group, device;
struct vfio_group_status group_status = {
.argsz = sizeof(group_status)
};
/* 1. VFIO 컨테이너 열기 — IOMMU 컨텍스트 관리 */
container = open("/dev/vfio/vfio", O_RDWR);
/* 2. IOMMU 그룹 열기 (디바이스의 iommu_group 번호 사용) */
group = open("/dev/vfio/26", O_RDWR);
/* 3. 그룹을 컨테이너에 연결 */
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
/* 4. IOMMU 모델 설정 (Type1 = 페이지 단위 DMA 매핑) */
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
/* 5. 디바이스 FD 획득 */
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD,
"0000:03:00.0");
/* ── Config Space 읽기 ── */
struct vfio_region_info config_info = {
.argsz = sizeof(config_info),
.index = VFIO_PCI_CONFIG_REGION_INDEX,
};
ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &config_info);
u16 vendor;
pread(device, &vendor, 2, config_info.offset + PCI_VENDOR_ID);
/* vendor == 0x8086 등 */
/* ── BAR 0 MMIO 매핑 ── */
struct vfio_region_info bar0_info = {
.argsz = sizeof(bar0_info),
.index = VFIO_PCI_BAR0_REGION_INDEX,
};
ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &bar0_info);
void *bar0 = mmap(NULL, bar0_info.size,
PROT_READ | PROT_WRITE, MAP_SHARED,
device, bar0_info.offset);
/* bar0[offset]으로 디바이스 레지스터 직접 접근 */
/* ── DMA 메모리 매핑 (IOMMU 설정) ── */
void *dma_buf = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
struct vfio_iommu_type1_dma_map dma_map = {
.argsz = sizeof(dma_map),
.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
.vaddr = (__u64)dma_buf,
.iova = 0x100000, /* 디바이스가 볼 DMA 주소 */
.size = 4096,
};
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
/* 이제 디바이스가 IOVA 0x100000으로 DMA 시 dma_buf에 도달 */
/* ── MSI-X 인터럽트 설정 ── */
struct vfio_irq_info irq_info = {
.argsz = sizeof(irq_info),
.index = VFIO_PCI_MSIX_IRQ_INDEX,
};
ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq_info);
/* eventfd를 벡터 0에 연결 → epoll로 대기 가능 */
int efd = eventfd(0, EFD_NONBLOCK);
struct vfio_irq_set *irq_set;
size_t sz = sizeof(*irq_set) + sizeof(int);
irq_set = calloc(1, sz);
irq_set->argsz = sz;
irq_set->flags = VFIO_IRQ_SET_DATA_EVENTFD |
VFIO_IRQ_SET_ACTION_TRIGGER;
irq_set->index = VFIO_PCI_MSIX_IRQ_INDEX;
irq_set->start = 0;
irq_set->count = 1;
*(int *)irq_set->data = efd;
ioctl(device, VFIO_DEVICE_SET_IRQS, irq_set);
/* eventfd로 인터럽트 대기 (epoll 또는 read) */
uint64_t count;
read(efd, &count, sizeof(count));
/* 인터럽트 수신 완료 — 데이터 처리 */
/* 정리 */
munmap(bar0, bar0_info.size);
close(device);
close(group);
close(container);
return 0;
}
코드 설명
- Container / Group / DeviceVFIO는 3단계 계층 구조입니다. Container(
/dev/vfio/vfio)는 IOMMU 컨텍스트를, Group(/dev/vfio/N)은 IOMMU 그룹 단위 디바이스 집합을, Device FD는 개별 PCI 디바이스를 나타냅니다. 그룹 내 모든 디바이스가 VFIO에 바인딩되어야 그룹을 사용할 수 있습니다. - VFIO_PCI_CONFIG_REGION_INDEX
pread()/pwrite()로 PCI Configuration Space를 읽고 쓸 수 있습니다. 오프셋은region_info.offset을 기준으로 합니다. 커널의vfio-pci드라이버가 Config Space 접근을 중재하여 안전성을 보장합니다. - BAR MMIO mmap()BAR 영역을 유저스페이스에 직접 매핑합니다. 이 매핑을 통해 커널 컨텍스트 스위칭 없이 디바이스 레지스터에 접근할 수 있습니다. DPDK가 NIC의 TX/RX 큐 doorbell을 누르는 것이 이 방식입니다.
- VFIO_IOMMU_MAP_DMA유저스페이스 가상 주소를 IOMMU의 IOVA(I/O Virtual Address)에 매핑합니다. 디바이스가 IOVA
0x100000으로 DMA를 수행하면, IOMMU가 이를dma_buf의 물리 주소로 변환합니다. 매핑되지 않은 주소로의 DMA는 IOMMU가 차단합니다. - eventfd 인터럽트MSI-X 벡터를
eventfd에 연결하면, 디바이스가 인터럽트를 발생시킬 때 eventfd에 이벤트가 기록됩니다.epoll()을 사용하여 여러 벡터와 다른 이벤트를 동시에 대기할 수 있으며, 이는 DPDK/SPDK의 이벤트 루프 구현 방식입니다.
P2P DMA (Peer-to-Peer DMA)
PCIe Peer-to-Peer DMA는 두 PCIe 디바이스 간 CPU/시스템 메모리를 거치지 않고 직접 데이터를 전송합니다:
| 사용 사례 | 설명 |
|---|---|
| NVMe → GPU | GPUDirect Storage — 스토리지에서 GPU 메모리로 직접 전송 |
| NVMe → NVMe | NVMe 컨트롤러 간 직접 복사 |
| NVMe → RDMA NIC | NVMe-oF 타겟에서 네트워크로 직접 전송 |
/* P2P DMA 사용 (커널 5.x+) */
#include <linux/pci-p2pdma.h>
/* P2P 가능 여부 확인 */
if (pci_p2pdma_distance(provider, client, false) < 0)
return -EOPNOTSUPP; /* 다른 RC 하위이거나 Switch 미지원 */
/* P2P 메모리 할당 (provider 디바이스의 BAR에서) */
void *p2p_mem = pci_alloc_p2pmem(provider, size);
/* DMA 매핑 (client 디바이스 관점) */
dma_addr_t dma = pci_p2pdma_map_sg(...);
P2P DMA 커널 내부 구현
drivers/pci/p2pdma.c의 P2P DMA 프레임워크는 두 디바이스 간의 토폴로지를 분석하여 P2P 전송 가능 여부를 판단합니다.
/* drivers/pci/p2pdma.c — P2P 거리 계산 (simplified) */
/* 두 디바이스 간 P2P 가능 여부와 거리를 판단 */
int pci_p2pdma_distance(struct pci_dev *provider,
struct pci_dev *client,
bool verbose)
{
struct pci_dev *a = provider, *b = client;
/* 양쪽의 Root Complex까지 상위 경로를 추적 */
while (a) {
while (b) {
if (a == b) {
/* 공통 조상(common ancestor) 발견
* → 같은 Switch/RC 하위 = P2P 가능 */
return dist;
}
if (pci_is_root_bus(b->bus))
break;
b = b->bus->self;
}
if (pci_is_root_bus(a->bus))
break;
a = a->bus->self;
b = client;
}
/* 공통 조상 없음 → 다른 RC 하위 = P2P 불가능
* (일부 플랫폼은 RC 간 P2P를 호스트 브리지로 라우팅) */
return -1;
}
코드 설명
- pci_p2pdma_distance()두 PCI 디바이스의 버스 계층을 위로 추적하여 공통 조상(common ancestor) PCIe 스위치 또는 Root Port를 찾습니다. 공통 조상이 존재하면 P2P TLP가 해당 스위치에서 라우팅 가능하므로 양수 거리를 반환합니다. 공통 조상이 없으면(다른 Root Complex 하위) -1을 반환합니다.
- 거리(distance)반환되는 거리 값은 두 디바이스 사이의 PCIe 스위치 홉(hop) 수입니다. 거리가 가까울수록 P2P 전송 지연이 짧습니다.
pci_p2pdma_distance()는 내부적으로 ACS(Access Control Services) 설정과 호스트 브리지의 P2P 라우팅 지원 여부도 확인합니다.
NVMe 타겟 P2P DMA 구현 패턴
NVMe-oF(NVMe over Fabrics) 타겟에서 P2P DMA를 활용하면 NVMe SSD 데이터를 시스템 메모리를 거치지 않고 RDMA NIC으로 직접 전송할 수 있습니다.
/* NVMe 타겟 스타일 P2P DMA 패턴 (simplified) */
#include <linux/pci-p2pdma.h>
struct nvmet_req {
struct pci_dev *nvme_pdev; /* NVMe SSD (provider) */
struct pci_dev *rdma_pdev; /* RDMA NIC (client) */
void *p2p_buf;
size_t len;
};
static int nvmet_p2p_setup(struct nvmet_req *req)
{
int dist;
/* 1. P2P 가능 여부 확인 */
dist = pci_p2pdma_distance(req->nvme_pdev,
req->rdma_pdev, true);
if (dist < 0) {
pr_warn("P2P not possible, falling back to host memory\n");
return -EOPNOTSUPP;
}
/* 2. P2P 메모리 할당 (NVMe CMB 또는 provider BAR에서) */
req->p2p_buf = pci_alloc_p2pmem(req->nvme_pdev, req->len);
if (!req->p2p_buf)
return -ENOMEM;
/* 3. SGL/PRP에 P2P 주소 설정
* → NVMe는 CMB 주소로 읽기 수행
* → RDMA NIC은 같은 주소로 DMA 읽기하여 네트워크 전송
* → 시스템 메모리 미경유! */
return 0;
}
static void nvmet_p2p_cleanup(struct nvmet_req *req)
{
if (req->p2p_buf)
pci_free_p2pmem(req->nvme_pdev, req->p2p_buf, req->len);
}
코드 설명
- pci_alloc_p2pmem()provider 디바이스의 BAR 영역에서 P2P용 메모리를 할당합니다. NVMe에서는 CMB(Controller Memory Buffer)가 이 역할을 합니다. 할당된 메모리의 PCI 버스 주소는 두 디바이스 모두 접근할 수 있어야 합니다.
- 데이터 경로NVMe SSD가 데이터를 CMB에 쓰고(내부 DMA), RDMA NIC이 같은 CMB 영역에서 데이터를 읽어(P2P DMA) 네트워크로 전송합니다. 데이터가 시스템 메모리를 경유하지 않으므로 CPU 캐시 오염이 없고, 메모리 대역폭을 절약합니다. GPUDirect Storage도 동일한 원리로 NVMe → GPU 메모리 직접 전송을 구현합니다.
P2P DMA 제약 조건과 호환성
| 제약 조건 | 설명 | 확인 방법 |
|---|---|---|
| 토폴로지 | 두 디바이스가 동일 Root Complex 또는 PCIe Switch 하위에 있어야 합니다 | lspci -tv로 트리 확인 |
| ACS | 중간 스위치의 ACS(Access Control Services)가 P2P를 차단할 수 있습니다 | lspci -vvv | grep ACS |
| IOMMU | IOMMU가 활성화되어 있으면 P2P 주소 변환이 필요합니다 | dmesg | grep IOMMU |
| Root Complex | 일부 RC는 Peer-to-Peer TLP 라우팅을 지원하지 않습니다 | CONFIG_PCI_P2PDMA 활성 + 커널 로그 |
| CMB | NVMe P2P DMA는 CMB(Controller Memory Buffer)가 필요합니다 | nvme id-ctrl /dev/nvme0 | grep cmbsz |
| BAR 크기 | Provider의 P2P 할당 가능 BAR 영역이 충분해야 합니다 | cat /sys/bus/pci/devices/*/resource |
# P2P DMA 지원 확인
cat /sys/bus/pci/devices/0000:03:00.0/p2pmem/size
# 16777216 (16 MB P2P 메모리 사용 가능)
# P2P 가능한 디바이스 쌍 확인 (커널 5.8+)
cat /sys/bus/pci/devices/0000:03:00.0/p2pmem/available
# NVMe CMB 지원 확인
nvme id-ctrl /dev/nvme0 | grep -i cmb
# cmbsz : 0x100 → CMB 지원 (크기: 256 × 4KB = 1 MB)
# NVMe-oF 타겟에서 P2P 활성화
echo 1 > /sys/kernel/config/nvmet/subsystems/nqn/namespaces/1/p2pmem
NTB (Non-Transparent Bridge)
NTB는 두 개의 독립적인 PCIe 계층(hierarchy)을 연결하는 특수 PCIe 브리지로, 양쪽 호스트가 Scratchpad, Doorbell, Memory Window 메커니즘을 통해 데이터를 교환합니다.
CXL (Compute Express Link)
CXL은 PCIe 물리 계층 위에 구축된 인터커넥트로, CPU-디바이스 간 캐시 코히런트 메모리 공유를 지원합니다:
| 프로토콜 | 용도 | 설명 |
|---|---|---|
| CXL.io | I/O | PCIe 호환 — 디바이스 열거, Config Space, MMIO |
| CXL.cache | 디바이스→호스트 캐시 | 디바이스가 호스트 메모리를 캐시 코히런트하게 접근 |
| CXL.mem | 호스트→디바이스 메모리 | 디바이스 메모리를 시스템 주소 공간에 매핑 (Type 2/3) |
| CXL 디바이스 타입 | 프로토콜 | 예시 |
|---|---|---|
| Type 1 | CXL.io + CXL.cache | SmartNIC, 가속기 |
| Type 2 | CXL.io + CXL.cache + CXL.mem | GPU, FPGA (디바이스 메모리 포함) |
| Type 3 | CXL.io + CXL.mem | 메모리 확장기 (Memory Expander) |
# Linux CXL 서브시스템 (커널 5.18+)
# CXL 디바이스 확인
ls /sys/bus/cxl/devices/
# cxl-cli 도구 (ndctl 패키지)
cxl list # CXL 토폴로지 표시
cxl list -M # 메모리 디바이스 목록
cxl create-region -t ram ... # CXL 메모리 리전 생성 → NUMA 노드로 노출
Google TPU (Tensor Processing Unit)
TPU는 Google이 설계한 ASIC 기반 텐서 연산 가속기로, PCIe 또는 자체 인터커넥트(ICI)를 통해 호스트에 연결됩니다. Linux에서는 Gasket/ACCEL 드라이버 프레임워크를 통해 관리합니다.
sysfs 인터페이스
PCI 디바이스 정보는 /sys/bus/pci/ 아래에 노출됩니다:
PCIe Performance 튜닝
PCIe 대역폭을 최대로 활용하려면 TLP 오버헤드를 최소화하고, 흐름 제어 병목(Bottleneck)을 해소해야 합니다. 주요 튜닝 포인트:
MPS / MRRS 최적화
# 현재 MPS/MRRS 확인
lspci -vvv -s 03:00.0 | grep -E "MaxPayload|MaxReadReq"
# DevCap: MaxPayload 512 bytes, PhantFunc 0, Latency L0s <512ns, L1 <1us
# DevCtl: ... MaxPayload 256 bytes, MaxReadReq 512 bytes
# 주의: MaxPayload(DevCtl) ≤ MaxPayload(DevCap)
# MPS 정책 설정 (커널 부팅 파라미터)
pci=pcie_bus_safe # 링크 경로상 최소 MPS로 설정 (가장 안전)
pci=pcie_bus_perf # 각 버스 도메인 내 최대 MPS 설정 (성능 우선)
pci=pcie_bus_peer2peer # 전체 계층에서 최소 MPS (P2P DMA 호환성)
# setpci로 MPS 직접 변경 (주의: 양단 일치 필요)
# DevCtl [7:5] = MPS: 000=128, 001=256, 010=512, 011=1024, 100=2048, 101=4096
setpci -s 03:00.0 CAP_EXP+8.W=0x2820 # MPS=256, MRRS=4096 (예시)
# MRRS 변경 (sysfs)
echo 4096 > /sys/bus/pci/devices/0000:03:00.0/max_read_request_size
Relaxed Ordering / No Snoop
| TLP 속성 | 비트 | 효과 | 사용 시나리오 |
|---|---|---|---|
| Relaxed Ordering (RO) | Attr[1] | 같은 TC의 Posted 쓰기가 이전 Non-Posted 읽기를 추월 가능. 파이프라인 효율 향상 | NIC DMA, 스토리지 — 순서 의존성 없는 대량 데이터 전송 |
| ID-based Ordering (IDO) | Attr[2] | 서로 다른 Requester ID 간의 TLP 순서 완화. 멀티 VF 환경에서 유용 | SR-IOV 가상화, 다수 VF의 독립적 DMA 스트림 |
| No Snoop (NS) | Attr[0] | 호스트 CPU 캐시 스누핑 생략. DMA 대상이 non-cacheable 영역일 때 지연 감소 | GPU 메모리, 프레임버퍼 — CPU 캐시와 무관한 DMA |
1a4e0c4). 문제 발생 시 pci=norelaxedordering 커널 파라미터로 RO를 비활성화할 수 있습니다.Completion Timeout 튜닝
Non-Posted 요청(MRd, CfgRd/Wr)에 대한 Completion이 일정 시간 내에 도착하지 않으면 Completion Timeout 에러가 발생합니다:
| 타임아웃 범위 | 값 | 용도 |
|---|---|---|
| 50 µs ~ 50 ms | Range A | 기본. 대부분의 디바이스에 적합 |
| 50 ms ~ 10 s | Range B/C/D | 느린 디바이스 (NVMe D3→D0 복구, FPGA 재구성) |
| 비활성화 | Disable | 영원히 대기 (데드락 위험, 디버깅용) |
# Completion Timeout 범위 확인
lspci -vvv -s 03:00.0 | grep "CmpltTO"
# DevCap2: Completion Timeout: Range ABC, TimeoutDis+
# DevCtl2: Completion Timeout: 50us to 50ms, TimeoutDis-
# Completion Timeout 비활성화 (디버깅 시)
setpci -s 03:00.0 CAP_EXP+28.W=0x0010 # DevCtl2: Timeout Disable bit
대역폭 실측 및 병목 분석
# 1. 실제 링크 속도/폭 확인
lspci -vvv -s 03:00.0 | grep -E "LnkCap|LnkSta"
# LnkCap: Port #0, Speed 16GT/s (Gen4), Width x16
# LnkSta: Speed 16GT/s (ok), Width x16 (ok)
# 만약 LnkSta가 LnkCap보다 낮으면 → 등화 실패, 물리적 문제 의심
# 2. PCIe 대역폭 계측 (fio NVMe 예시)
fio --name=seqread --ioengine=io_uring --direct=1 --bs=128k \
--numjobs=4 --iodepth=64 --filename=/dev/nvme0n1 --rw=read \
--runtime=10 --time_based
# Gen4 x4 NVMe: 이론 ~7.9 GB/s, 실측 ~6.5-7.0 GB/s (→ 82-89% 효율)
# 3. GPU DMA 대역폭 (CUDA bandwidthTest 예시)
# Gen4 x16: 이론 ~31.5 GB/s, H2D 실측 ~25-27 GB/s (→ 79-86% 효율)
# 4. PCIe 성능 카운터 (Intel perf uncore)
perf stat -e uncore_iio_0/event=0x83,umask=0x04/ -a sleep 5
# 플랫폼별 PCIe 카운터로 실시간 대역폭 모니터링
# 5. 병목 진단 체크리스트
# □ LnkSta Speed/Width가 LnkCap과 일치하는가?
# □ MPS가 DevCap 최대값으로 설정되었는가?
# □ MRRS가 충분히 큰가? (DMA 버스트 크기 ≤ MRRS)
# □ AER Correctable Error 카운터가 빠르게 증가하지 않는가?
# □ ASPM이 활성화되어 레이턴시를 유발하지 않는가?
# □ NUMA locality — DMA 버퍼가 PCIe RC와 같은 NUMA 노드에 있는가?
# □ IOMMU 오버헤드 — 패스스루 시 intel_iommu=pt로 바이패스
# 디바이스의 NUMA 노드 확인
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
# 0
# 로컬 CPU 목록 확인
cat /sys/bus/pci/devices/0000:03:00.0/local_cpulist
# 0-15,32-47
# 인터럽트를 로컬 CPU에 바인딩
echo 0-15 > /proc/irq/<IRQ_NUM>/smp_affinity_list
디버깅 도구
lspci
# 기본 디바이스 목록
lspci
# 상세 정보 (-v: verbose, -vv: very verbose)
lspci -vvv -s 03:00.0
# 커널 드라이버 및 모듈 표시
lspci -k
# 트리 구조 (Bus 토폴로지)
lspci -tv
# 숫자 코드 + 이름
lspci -nn
# Configuration Space 16진수 덤프
lspci -xxx -s 03:00.0 # 256 바이트
lspci -xxxx -s 03:00.0 # 4096 바이트 (PCIe 확장)
# Capability 상세 (PCIe Link, MSI-X, AER 등)
lspci -vvv -s 03:00.0 | grep -A 10 "LnkCap\|LnkSta\|MSI-X\|AER"
setpci
# Configuration Space 직접 읽기
setpci -s 03:00.0 VENDOR_ID # Vendor ID 읽기
setpci -s 03:00.0 04.W # Command 레지스터 (2바이트)
# Configuration Space 직접 쓰기 (주의!)
setpci -s 03:00.0 04.W=0x0007 # I/O + Memory + Bus Master 활성화
lspci -vvv 출력 완전 해석
실제 lspci -vvv 출력의 핵심 항목을 라인별로 해석합니다. NVMe SSD를 예시로 사용:
## 기본 식별 정보
03:00.0 Non-Volatile memory controller: Samsung Electronics Co Ltd
NVMe SSD Controller PM9A1 (rev 01)
# 03:00.0 → BDF (Bus:03, Device:00, Function:0)
# Non-Volatile memory controller → Class Code 0x010802
# rev 01 → Revision ID
## Subsystem ID — OEM 변형 식별
Subsystem: Lenovo Device 5023
# 같은 컨트롤러라도 서브시스템 ID로 노트북/서버 변형 구분
## Control (Command 레지스터의 현재 활성 비트)
Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- ...
# Mem+ → Memory Space 활성 (MMIO BAR 접근 가능)
# BusMaster+ → DMA 활성화 상태
# I/O- → I/O Port 비활성 (NVMe는 MMIO만 사용)
## Status (Status 레지스터)
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast
>TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
# Cap+ → Capabilities List 있음 (오프셋 34h)
# INTx- → 레거시 인터럽트 비활성 (MSI-X 사용 중)
## BAR 정보
Region 0: Memory at 6a500000 (64-bit, non-prefetchable) [size=16K]
# BAR0: MMIO, 64-bit, 16 KB (NVMe 레지스터 공간)
# non-prefetchable → 디바이스 레지스터 (side-effect 있음)
## PCIe Capability — 링크 정보 (가장 중요)
Capabilities: [70] Express (v2) Endpoint, MSI 00
DevCap: MaxPayload 512 bytes, PhantFunc 0
ExtTag+ AttnBtn- AttnInd- PwrInd- RBE+ FLR+ SlotPowerLimit 75W
# MaxPayload 512 → 이 디바이스가 지원하는 최대 MPS (DevCap)
# FLR+ → Function Level Reset 지원
DevCtl: CorrErr+ NonFatalErr+ FatalErr+ UnsuppReq+
RlxdOrd+ ExtTag+ PhantFunc- AuxPwr- NoSnoop+ MaxPayload 256 bytes
MaxReadReq 512 bytes
# MaxPayload 256 → 현재 설정된 MPS (DevCap보다 작을 수 있음 — 경로상 최소값)
# MaxReadReq 512 → MRRS 설정값
# RlxdOrd+ → Relaxed Ordering 활성
# NoSnoop+ → No Snoop 활성
LnkCap: Port #0, Speed 16GT/s (Gen4), Width x4
ASPM L1, Exit Latency L1 <64us
ClockPM+ Surprise- LLActRep- BwNot- ASPMOptComp+
# Speed 16GT/s, Width x4 → 디바이스의 최대 링크 능력
# ASPM L1 → L1만 지원 (L0s 미지원)
LnkSta: Speed 16GT/s (ok), Width x4 (ok)
TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
# ★ Speed/Width가 LnkCap과 일치 → 링크 정상
# 불일치 시 → 등화 실패, PCB 문제, 슬롯 문제 의심
## MSI-X Capability
Capabilities: [b0] MSI-X: Enable+ Count=128 Masked-
Vector table: BAR=0 offset=00002000
PBA: BAR=0 offset=00003000
# MSI-X Enable+ → 현재 MSI-X 활성 상태
# Count=128 → 최대 128개 벡터 (NVMe: QP당 1벡터)
# Vector table @ BAR0+0x2000, PBA @ BAR0+0x3000
## L1 PM Substates (확장 Capability)
Capabilities: [900 v1] L1 PM Substates
L1SubCap: PCI-PM_L1.2+ PCI-PM_L1.1+ ASPM_L1.2+ ASPM_L1.1+
PortCommonModeRestoreTime=60us PortTPowerOnTime=10us
L1SubCtl1: PCI-PM_L1.2+ PCI-PM_L1.1+ ASPM_L1.2+ ASPM_L1.1+
T_CommonMode=0us LTR1.2_Threshold=163840ns
# L1.2 ASPM 활성 → 가장 깊은 절전 (레퍼런스 클럭까지 차단)
# PortTPowerOnTime=10us → L1.2→L0 복귀에 10µs 소요
## Kernel driver/module
Kernel driver in use: nvme
Kernel modules: nvme
# 현재 바인딩된 커널 드라이버
커널 디버깅
# PCI 관련 커널 메시지
dmesg | grep -i pci
# PCI 리소스 할당 정보
cat /proc/iomem | grep -i pci
cat /proc/ioports | grep -i pci
# PCIe 링크 속도/폭 모니터링
cat /sys/bus/pci/devices/0000:03:00.0/current_link_speed
cat /sys/bus/pci/devices/0000:03:00.0/current_link_width
# AER 에러 카운터
cat /sys/bus/pci/devices/0000:03:00.0/aer_dev_correctable
cat /sys/bus/pci/devices/0000:03:00.0/aer_dev_nonfatal
cat /sys/bus/pci/devices/0000:03:00.0/aer_dev_fatal
# FLR (Function Level Reset)
echo 1 > /sys/bus/pci/devices/0000:03:00.0/reset
# 커널 Dynamic Debug
echo 'module pci +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module pcieport +p' > /sys/kernel/debug/dynamic_debug/control
lspci -tv 출력 예시
PCIe 트레이스포인트
# 사용 가능한 PCI 트레이스포인트 목록
ls /sys/kernel/debug/tracing/events/pci/
# pci_bus_read_config, pci_bus_write_config
# AER 트레이스포인트
ls /sys/kernel/debug/tracing/events/ras/
# aer_event
# Config Space 접근 추적 (디버깅/성능 분석)
echo 1 > /sys/kernel/debug/tracing/events/pci/pci_bus_read_config/enable
echo 1 > /sys/kernel/debug/tracing/events/pci/pci_bus_write_config/enable
cat /sys/kernel/debug/tracing/trace_pipe
# nvme-0 [003] .... 12345.678: pci_bus_read_config: 03:00.0 read 2 bytes @ 4 = 0x0006
# AER 이벤트 모니터링
echo 1 > /sys/kernel/debug/tracing/events/ras/aer_event/enable
cat /sys/kernel/debug/tracing/trace_pipe
# aer_event: 0000:03:00.0 PCIe Bus Error: severity=Corrected, type=Data Link Layer ...
# trace-cmd를 이용한 기록
trace-cmd record -e pci -e ras sleep 10
trace-cmd report | less
# PCIe Port Service 드라이버 상태
lspci -vvv -s 00:1c.0 | grep "Kernel driver"
# Kernel driver in use: pcieport
# pcieport 드라이버가 AER, DPC, Hotplug, PME, Bandwidth Notification 서비스 담당
ls /sys/bus/pci/drivers/pcieport/
# 0000:00:01.0 0000:00:1c.0 ... (Root Port / Switch Port 목록)
자주 발생하는 PCIe 문제와 진단
| 증상 | 가능한 원인 | 진단 명령 | 해결 방법 |
|---|---|---|---|
| LnkSta Speed가 LnkCap보다 낮음 | 등화 실패, PCB 문제, 라이저 카드 불량 | lspci -vvv | grep LnkSta | 슬롯 교체, 케이블 점검, BIOS 업데이트 |
| LnkSta Width가 x1로 강등 | 레인 접촉 불량, EMI, 슬롯 오정렬 | lspci -vvv | grep Width | 카드 재장착, 다른 슬롯 시도 |
| Correctable Error 누적 | 신호 무결성(Integrity) 저하, 케이블 문제 | cat aer_dev_correctable | 물리적 점검, MPS 축소 시도 |
| Completion Timeout | 디바이스 응답 불가, IOMMU 매핑 오류 | dmesg | grep CmpltTO | 디바이스 FLR, IOMMU 도메인 확인 |
| BAR 할당 실패 | 주소 공간 부족 (32-bit 제한) | dmesg | grep "can't claim BAR" | BIOS "Above 4G Decoding" 활성화 |
| 디바이스 미인식 (Vendor=ffff) | 전원 미공급, PERST# 미해제, 물리 결함 | lspci -nn | 전원/슬롯/BIOS 설정 확인 |
| MSI-X 인터럽트 미수신 | INTx Disable 미설정, 벡터 미할당 | cat /proc/interrupts | pci_alloc_irq_vectors() 확인 |
| DMA 실패 (IOMMU fault) | DMA 매핑 누락, IOMMU 도메인 오류 | dmesg | grep DMAR | dma_map_* 반환값 확인 |
커널 소스 구조
| 경로 | 설명 |
|---|---|
drivers/pci/ | PCI 코어 서브시스템 |
drivers/pci/probe.c | 디바이스 열거, BAR 크기 결정 |
drivers/pci/pci-driver.c | pci_register_driver(), 매칭 로직 |
drivers/pci/msi/ | MSI/MSI-X 서브시스템 |
drivers/pci/pcie/ | PCIe 서비스 (AER, hotplug, PME, DPC) |
drivers/pci/iov.c | SR-IOV 지원 |
drivers/pci/p2pdma.c | Peer-to-Peer DMA |
drivers/pci/controller/ | 플랫폼별 Host Bridge 드라이버 |
drivers/vfio/pci/ | VFIO PCI 드라이버 |
drivers/cxl/ | CXL 서브시스템 |
drivers/staging/gasket/ | Gasket 프레임워크 (Edge TPU) |
drivers/accel/ | ACCEL 서브시스템 (ML/AI 가속기) |
include/drm/drm_accel.h | ACCEL API 헤더 |
include/linux/pci.h | PCI API 헤더 |
include/uapi/linux/pci_regs.h | PCI/PCIe 레지스터 상수 정의 |
주요 Kconfig 옵션
| 옵션 | 설명 |
|---|---|
CONFIG_PCI | PCI 서브시스템 활성화 |
CONFIG_PCI_MSI | MSI/MSI-X 지원 |
CONFIG_PCIEPORTBUS | PCIe Port Bus 드라이버 (AER, Hotplug, PME) |
CONFIG_PCIEAER | PCIe AER (Advanced Error Reporting) |
CONFIG_HOTPLUG_PCI_PCIE | PCIe Native Hotplug |
CONFIG_PCIE_ASPM | ASPM (Active State Power Management) |
CONFIG_PCI_IOV | SR-IOV 지원 |
CONFIG_PCI_P2PDMA | Peer-to-Peer DMA |
CONFIG_VFIO_PCI | VFIO PCI 드라이버 |
CONFIG_CXL_BUS | CXL 버스 지원 |
CONFIG_STAGING_GASKET_FRAMEWORK | Gasket 프레임워크 (Edge TPU) |
CONFIG_STAGING_APEX_DRIVER | Edge TPU (Apex) 드라이버 |
CONFIG_DRM_ACCEL | ACCEL 서브시스템 (6.2+) |
CONFIG_INTEL_IOMMU | Intel VT-d IOMMU |
CONFIG_AMD_IOMMU | AMD-Vi IOMMU |
참고자료
커널 공식 문서
- Linux PCI 문서 인덱스 — PCI 관련 커널 문서 모음
- How To Write Linux PCI Drivers — PCI 드라이버 작성 가이드
- MSI/MSI-X HOWTO — MSI/MSI-X 인터럽트 설정 가이드
- PCI sysfs 인터페이스 — sysfs를 통한 PCI 디바이스 접근
- PCI Express Port Bus Driver — PCIe 포트 버스 드라이버 가이드
- PCI SR-IOV HOWTO — SR-IOV 설정 및 사용 가이드
- ACPI & PCI Host Bridge — ACPI 기반 PCI 호스트 브리지 정보
커널 소스 코드
drivers/pci/pci.c— PCI 코어 구현drivers/pci/pci-driver.c— PCI 드라이버 등록/매칭drivers/pci/probe.c— PCI 디바이스 열거(enumeration)drivers/pci/msi/msi.c— MSI/MSI-X 구현drivers/pci/iov.c— SR-IOV 구현include/linux/pci.h— pci_dev, pci_driver 정의include/uapi/linux/pci_regs.h— PCI 레지스터 상수drivers/pci/pcie/— PCIe 서비스 (AER, PME, hotplug)
규격 문서
- PCI-SIG 공식 규격 — PCI/PCIe 공식 표준 규격 목록
- PCIe Base Specification — PCIe 기본 규격 문서
외부 자료
- An introduction to PCI device assignment with VFIO — LWN, VFIO를 통한 PCI 디바이스 할당 소개
- How to access PCI device resources through sysfs — LWN, sysfs를 통한 PCI 리소스 접근
- OSDev PCI 위키 — PCI 하드웨어 및 프로그래밍 기초
관련 문서
PCI/PCIe와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.