IOMMU (Input-Output Memory Management Unit)
IOMMU를 DMA 주소 변환(Address Translation)과 디바이스 격리(Isolation)를 담당하는 핵심 보안 계층으로 심층 분석합니다. Intel VT-d, AMD-Vi, ARM SMMU의 구조 차이와 페이지 테이블(Page Table) 모델, DMA remapping/interrupt remapping 동작, ATS/PASID/SVA 기반 고급 공유 주소 공간(Address Space), VFIO 가상화(Virtualization) 환경의 격리 보장, 성능 저하를 줄이기 위한 TLB flush 전략, DMA fault 디버깅(Debugging)과 우회 경로(SWIOTLB)까지 고성능 장치와 멀티테넌트 환경에서 필요한 운영 포인트를 다룹니다.
핵심 요약
- IOMMU — 디바이스 DMA 주소를 물리 주소(Physical Address)로 변환하고 접근 제어(Access Control)하는 하드웨어입니다.
- DMA Remapping — 디바이스의 DMA 주소를 가상화하여 메모리 보호와 격리를 제공합니다.
- IOMMU Domain — 디바이스 그룹이 공유하는 주소 공간과 페이지 테이블입니다.
- VFIO — IOMMU를 활용하여 가상 머신에 디바이스를 안전하게 할당합니다.
- PASID/SVA — CPU와 디바이스가 동일한 가상 주소 공간을 공유합니다.
단계별 이해
- IOMMU 필요성 — 디바이스가 임의 메모리에 접근하면 보안 위협이므로, IOMMU로 DMA 주소를 제한합니다.
악의적인 디바이스나 버그가 있는 드라이버로부터 메모리를 보호합니다.
- 주소 변환 — IOMMU는 디바이스가 사용하는 IOVA를 물리 주소로 변환합니다.
IOMMU 없으면 DMA 주소 = 물리 주소, IOMMU 있으면 DMA 주소 = IOVA (가상화)
- Domain과 Group — 디바이스를 Domain(주소 공간)에 할당하고, Group(격리 단위)으로 관리합니다.
VFIO는 Group 단위로 디바이스를 가상 머신에 할당합니다.
- 실전 활용 — KVM/QEMU 가상화에서 GPU, NVMe 등 물리 디바이스를 VM에 직접 할당(passthrough)할 때 IOMMU가 필수입니다.
이를 통해 VM이 거의 네이티브 성능으로 디바이스를 사용할 수 있습니다.
개요
IOMMU(Input-Output Memory Management Unit)는 CPU의 MMU와 유사하게 I/O 디바이스가 메모리에 접근할 때 주소 변환과 보호를 제공합니다.
주요 목적
- DMA Remapping — 디바이스가 사용하는 DMA 주소를 가상화하여 물리 메모리(Physical Memory) 보호
- Interrupt Remapping — 인터럽트(Interrupt) 메시지 보안 강화 (interrupt injection 공격 방지)
- Device Isolation — 디바이스 간 메모리 격리로 보안 강화
- VM Device Passthrough — 가상 머신에 물리 디바이스 직접 할당
- SVA (Shared Virtual Addressing) — CPU와 디바이스가 동일한 가상 주소 공간 공유
하드웨어 구현
| 벤더 | 기술 | 설명 |
|---|---|---|
| Intel | VT-d (Virtualization Technology for Directed I/O) | DMA/인터럽트 remapping, PASID 지원 |
| AMD | AMD-Vi (I/O Virtualization) | IOMMU v2/v3, GCR3 테이블 |
| ARM | SMMU (System Memory Management Unit) | SMMUv2/v3, Stream ID 기반 매핑 |
IOMMU 아키텍처
주요 구성 요소
IOMMU Domain
IOMMU domain은 동일한 주소 공간을 공유하는 디바이스 그룹입니다.
struct iommu_domain {
unsigned int type; /* IOMMU_DOMAIN_* */
const struct iommu_ops *ops;
unsigned long pgsize_bitmap; /* 지원 페이지 크기 */
struct iommu_domain_geometry geometry;
void *iova_cookie; /* IOVA allocator */
struct iommu_iotlb_gather iotlb_gather;
};
/* Domain Types */
#define IOMMU_DOMAIN_BLOCKED 0 /* 모든 DMA 차단 */
#define IOMMU_DOMAIN_IDENTITY 1 /* 1:1 매핑 (passthrough) */
#define IOMMU_DOMAIN_UNMANAGED 2 /* 수동 관리 (VFIO) */
#define IOMMU_DOMAIN_DMA 3 /* DMA API 통합 */
#define IOMMU_DOMAIN_DMA_FQ 4 /* Flush Queue 최적화 */
코드 설명
include/linux/iommu.h에 정의된 IOMMU 주소 공간의 핵심 추상화 구조체입니다.
- type
IOMMU_DOMAIN_BLOCKED(모든 DMA 차단),IOMMU_DOMAIN_IDENTITY(1:1 패스스루),IOMMU_DOMAIN_UNMANAGED(VFIO 수동 관리),IOMMU_DOMAIN_DMA(DMA API 통합),IOMMU_DOMAIN_DMA_FQ(Flush Queue 지연 무효화) 중 하나입니다. 부팅 시 커널 파라미터(iommu=pt등)와 드라이버 요청에 따라 결정됩니다. - ops벤더별 IOMMU 드라이버가 등록하는
iommu_ops콜백 테이블 포인터입니다.map,unmap,attach_dev등 실제 하드웨어 조작은 이 콜백을 통해 수행됩니다. - pgsize_bitmap해당 domain이 지원하는 페이지 크기를 비트맵으로 표현합니다. 예를 들어 4KB/2MB/1GB를 지원하면 해당 비트가 설정됩니다.
iommu_map()은 이 값을 참조하여 huge page 매핑 여부를 결정합니다. - iova_cookieDMA domain에서 IOVA 할당자(
iova_domain) 또는 MSI cookie를 가리킵니다.drivers/iommu/dma-iommu.c의iommu_get_dma_cookie()로 초기화됩니다. - iotlb_gatherIOTLB 무효화를 배치 처리하기 위한 누적 구조체입니다.
iommu_unmap()시 범위를 모아두었다가iommu_iotlb_sync()에서 한 번에 flush하여 성능을 높입니다.
IOMMU Group
IOMMU group은 격리 가능한 최소 디바이스 단위입니다. 같은 그룹의 디바이스는 서로를 구분할 수 없습니다.
struct iommu_group {
int id;
struct kobject kobj;
struct kobject *devices_kobj;
struct list_head devices; /* 그룹 내 디바이스 */
struct iommu_domain *domain;
void *iommu_data;
};
/* IOMMU Group 확인 */
# ls -l /sys/kernel/iommu_groups/
drwxr-xr-x 2 root root 0 Jan 1 00:00 0
drwxr-xr-x 2 root root 0 Jan 1 00:00 1
drwxr-xr-x 2 root root 0 Jan 1 00:00 2
# ls /sys/kernel/iommu_groups/0/devices/
0000:00:00.0 0000:00:00.1
lspci -vvv | grep "Access Control Services"로 ACS 지원 여부를 확인하세요.
서버용 마더보드는 대부분 ACS를 지원하지만, 소비자용 보드는 미지원이 많습니다.
Default Domain 정책
커널은 부팅 시 각 디바이스에 기본 IOMMU domain을 할당합니다. 이 정책은 커널 파라미터와 드라이버 요청에 따라 결정됩니다.
/* Default domain 타입 결정 흐름 */
/*
* 1. iommu=pt → IOMMU_DOMAIN_IDENTITY (1:1, 변환 없음)
* 2. iommu.passthrough=1 → IOMMU_DOMAIN_IDENTITY
* 3. 기본값 → IOMMU_DOMAIN_DMA (DMA API 통합)
* 4. VFIO가 그룹을 잡으면 → IOMMU_DOMAIN_UNMANAGED
*/
/* sysfs에서 domain 타입 확인/변경 (커널 6.2+) */
# cat /sys/kernel/iommu_groups/1/type
DMA
# 런타임에 identity로 변경 (디바이스에 드라이버가 없을 때만)
# echo identity > /sys/kernel/iommu_groups/1/type
Nested Translation (2단계 변환)
가상화 환경에서 게스트 OS도 IOMMU를 사용할 수 있도록 2단계 주소 변환을 지원합니다. CPU의 EPT/NPT(Extended/Nested Page Tables)와 유사한 개념입니다.
| 단계 | 변환 | 관리 주체 |
|---|---|---|
| Stage 1 (First Level) | GVA/GIOVA → GPA | 게스트 OS |
| Stage 2 (Second Level) | GPA → HPA | 하이퍼바이저 (호스트) |
/* Nested domain 할당 (커널 6.7+) */
struct iommu_domain *nested;
nested = iommu_domain_alloc_user(parent_domain,
IOMMU_HWPT_ALLOC_NEST_PARENT, user_data);
/* Stage 1: 게스트가 관리하는 페이지 테이블 */
/* Stage 2: 호스트가 관리하는 페이지 테이블 */
/* 최종 물리 주소 = Stage2(Stage1(GIOVA)) */
IOMMU Operations
iommu_ops 구조체(Struct)
struct iommu_ops {
bool (*capable)(enum iommu_cap);
struct iommu_domain *(*domain_alloc)(unsigned int type);
void (*domain_free)(struct iommu_domain *domain);
int (*attach_dev)(struct iommu_domain *domain,
struct device *dev);
void (*detach_dev)(struct iommu_domain *domain,
struct device *dev);
int (*map)(struct iommu_domain *domain,
unsigned long iova, phys_addr_t paddr,
size_t size, int prot, gfp_t gfp);
size_t (*unmap)(struct iommu_domain *domain,
unsigned long iova, size_t size,
struct iommu_iotlb_gather *gather);
phys_addr_t (*iova_to_phys)(struct iommu_domain *domain,
dma_addr_t iova);
struct iommu_device *(*probe_device)(struct device *dev);
void (*release_device)(struct device *dev);
void (*iotlb_sync)(struct iommu_domain *domain,
struct iommu_iotlb_gather *gather);
int (*enable_nesting)(struct iommu_domain *domain);
/* SVA (Shared Virtual Addressing) */
int (*sva_bind)(struct device *dev, struct mm_struct *mm,
void *drvdata);
void (*sva_unbind)(struct device *dev, struct mm_struct *mm);
/* Page Request Interface (PRI) */
int (*page_response)(struct device *dev,
struct iommu_fault_event *evt,
struct iommu_page_response *msg);
};
코드 설명
include/linux/iommu.h에 정의된 IOMMU 드라이버 인터페이스입니다. Intel VT-d, AMD-Vi, ARM SMMU 등 각 벤더가 이 콜백을 구현합니다.
- domain_alloc / domain_freeIOMMU domain(주소 공간)을 할당/해제합니다. 할당 시 벤더별 페이지 테이블 루트를 생성합니다. Intel은 PML4, ARM SMMU는 CD(Context Descriptor)를 초기화합니다.
- attach_dev / detach_dev디바이스를 domain에 연결/분리합니다. 연결 시 하드웨어 테이블(Intel Context Table, AMD DTE, ARM STE)에 디바이스→domain 매핑을 기록하고 IOTLB를 flush합니다.
- map / unmapIOVA→물리 주소 매핑을 페이지 테이블에 기록/삭제합니다.
prot인자로 읽기/쓰기 권한을 설정합니다.unmap은iotlb_gather에 무효화 범위를 누적합니다. - iova_to_physIOVA를 물리 주소로 변환합니다. 페이지 테이블을 소프트웨어로 워크하여 결과를 반환합니다. 디버깅과 VFIO의 DMA 매핑 검증에 사용됩니다.
- probe_device / release_device디바이스가 IOMMU에 등록/해제될 때 호출됩니다.
probe_device는 디바이스의 IOMMU group 결정과 기본 domain 할당을 트리거합니다. - sva_bind / sva_unbindSVA(Shared Virtual Addressing)를 위해 디바이스를 프로세스의
mm_struct에 바인딩합니다. PASID Table Entry에 프로세스 페이지 테이블 주소를 기록합니다. - page_responsePRI(Page Request Interface) 페이지 폴트에 대한 응답을 디바이스에 전달합니다. 커널이 요청된 페이지를 할당한 후 이 콜백으로 완료를 알립니다.
IOMMU 매핑
/* IOMMU domain 할당 */
struct iommu_domain *domain;
domain = iommu_domain_alloc(&pci_bus_type);
/* 디바이스를 domain에 연결 */
iommu_attach_device(domain, dev);
/* IOVA → Physical Address 매핑 */
unsigned long iova = 0x100000;
phys_addr_t paddr = virt_to_phys(buffer);
size_t size = PAGE_SIZE;
int prot = IOMMU_READ | IOMMU_WRITE;
iommu_map(domain, iova, paddr, size, prot);
/* IOVA로 물리 주소 조회 */
phys_addr_t phys = iommu_iova_to_phys(domain, iova);
/* 매핑 해제 */
iommu_unmap(domain, iova, size);
/* 디바이스 분리 및 domain 해제 */
iommu_detach_device(domain, dev);
iommu_domain_free(domain);
코드 설명
drivers/iommu/iommu.c의 IOMMU 매핑 API를 사용한 전형적인 수동 매핑 시퀀스입니다. VFIO나 커스텀 드라이버에서 DMA API 대신 직접 IOMMU를 제어할 때 이 패턴을 사용합니다.
- iommu_domain_alloc()버스 타입(예:
pci_bus_type)에 연결된 IOMMU 드라이버의domain_alloc콜백을 호출하여 새 주소 공간을 생성합니다. 반환된 domain은IOMMU_DOMAIN_UNMANAGED타입입니다. - iommu_attach_device()디바이스를 domain에 연결합니다. 내부적으로 기존 domain을 detach하고 벤더별
attach_dev콜백으로 하드웨어 테이블을 갱신합니다. - iommu_map()IOVA
0x100000을 버퍼의 물리 주소로 매핑합니다.IOMMU_READ | IOMMU_WRITE플래그로 읽기/쓰기 권한을 부여합니다. 벤더 드라이버의map콜백이 페이지 테이블 엔트리를 기록합니다. - iommu_iova_to_phys()매핑이 올바르게 설정되었는지 검증할 때 사용합니다. 소프트웨어 페이지 테이블 워크로 IOVA에 대응하는 물리 주소를 반환합니다.
- iommu_unmap() → detach → free역순으로 정리합니다.
unmap으로 페이지 테이블 엔트리를 제거하고,detach로 하드웨어 연결을 해제한 뒤,domain_free로 페이지 테이블 메모리를 해제합니다.
DMA API 통합 경로
디바이스 드라이버가 dma_map_single()을 호출하면, IOMMU가 활성화된 환경에서는 iommu_dma_ops를 통해 IOVA 할당 → 페이지 테이블 기록 → IOTLB flush가 자동으로 수행됩니다. 이 섹션에서는 DMA API에서 IOMMU 벤더 드라이버까지의 전체 호출 경로를 추적합니다.
iommu_dma_ops 설치 경로
/* drivers/iommu/dma-iommu.c */
/* IOMMU가 디바이스를 probe할 때 DMA ops 설치 */
void iommu_setup_dma_ops(struct device *dev)
{
/* 1. iommu_probe_device()에서 호출 */
/* 2. DMA domain이면 iommu_dma_ops 설치 */
/* 3. IDENTITY domain이면 dma_direct_ops 유지 */
dev->dma_ops = &iommu_dma_ops;
}
/* iommu_dma_ops 구조체 */
static const struct dma_map_ops iommu_dma_ops = {
.map_page = iommu_dma_map_page,
.unmap_page = iommu_dma_unmap_page,
.map_sg = iommu_dma_map_sg,
.unmap_sg = iommu_dma_unmap_sg,
.alloc = iommu_dma_alloc,
.free = iommu_dma_free,
.mmap = iommu_dma_mmap,
};
iommu_dma_map_page() 코드 흐름
static dma_addr_t iommu_dma_map_page(
struct device *dev, struct page *page,
unsigned long offset, size_t size,
enum dma_data_direction dir, unsigned long attrs)
{
struct iommu_domain *domain = ...;
phys_addr_t phys = page_to_phys(page) + offset;
dma_addr_t iova;
int prot;
/* 1. IOVA 할당 (rcache → rb-tree fallback) */
iova = iommu_dma_alloc_iova(domain, size, dma_limit, dev);
if (!iova)
return DMA_MAPPING_ERROR;
/* 2. 보호 속성 결정 */
prot = dma_info_to_prot(dir, dev_is_dma_coherent(dev), attrs);
/* DMA_TO_DEVICE → IOMMU_READ */
/* DMA_FROM_DEVICE → IOMMU_WRITE */
/* DMA_BIDIRECTIONAL → IOMMU_READ | IOMMU_WRITE */
/* 3. IOMMU 페이지 테이블에 매핑 기록 */
ret = iommu_map(domain, iova, phys, size, prot, GFP_ATOMIC);
/* 4. Strict 모드면 IOTLB flush (lazy면 생략) */
return iova + offset;
}
dma_direct vs iommu_dma 경로 비교
| 항목 | dma_direct (IOMMU 없음/passthrough) | iommu_dma (IOMMU 활성) |
|---|---|---|
| DMA 주소 | 물리 주소 그대로 반환 | IOVA (가상 DMA 주소) 반환 |
| 주소 변환 | 없음 | IOVA → PA (페이지 테이블 워크) |
| IOVA 할당 | 불필요 | alloc_iova_fast() (rcache/rb-tree) |
| 보안 | 없음 (디바이스가 전체 메모리 접근 가능) | 도메인 격리 (허가된 범위만 접근) |
| 오버헤드(Overhead) | 최소 (~수 ns) | IOVA 할당 + PT 기록 (~수백 ns~수 µs) |
| 바운스 버퍼(Buffer) | 필요할 수 있음 (SWIOTLB) | 불필요 (IOVA로 재매핑) |
dma_map_single(dev, ...) → dma_map_page_attrs() →
dev->dma_ops가 NULL이면 dma_direct_map_page(), 설정되어 있으면 ops->map_page() 호출.
IOMMU가 활성화되고 DMA domain이 할당된 디바이스는 iommu_dma_ops가 자동 설치됩니다.
iommu_map()에 GFP_ATOMIC이 전달됩니다. 페이지 테이블 할당이 실패하면
DMA_MAPPING_ERROR가 반환되며, 드라이버는 이를 반드시 확인해야 합니다.
대량 DMA 매핑이 예상되면 사전에 페이지 테이블을 pre-allocate하세요.
IOVA 할당자
IOMMU 서브시스템은 디바이스가 사용할 I/O 가상 주소(IOVA)를 관리하기 위해 전용 할당자를 사용합니다. IOVA 할당자는 DMA API(dma_map_*)가 호출될 때 자동으로 IOVA를 할당하고, unmap 시 반환합니다.
IOVA Domain 구조
/* include/linux/iova.h */
struct iova_domain {
spinlock_t iova_rbtree_lock;
struct rb_root rbroot; /* Red-Black Tree로 IOVA 관리 */
struct rb_node *cached_node; /* 마지막 할당 노드 캐싱 */
struct rb_node *cached32_node; /* 32-bit 장치용 캐시 */
unsigned long granule; /* 최소 할당 단위 (보통 PAGE_SIZE) */
unsigned long start_pfn; /* IOVA 시작 PFN */
unsigned long dma_32bit_pfn; /* 32-bit DMA 경계 */
struct iova_rcache *rcaches; /* per-CPU 재활용 캐시 */
};
struct iova {
struct rb_node node;
unsigned long pfn_hi; /* IOVA 범위 상한 PFN */
unsigned long pfn_lo; /* IOVA 범위 하한 PFN */
};
IOVA Magazine 캐시(Cache)
IOVA 할당/해제가 빈번한 고성능 네트워크·스토리지 워크로드에서는 RB-tree 잠금 경합(Lock Contention)이 병목(Bottleneck)이 됩니다. 이를 완화하기 위해 per-CPU magazine cache (rcache)를 사용합니다.
/* IOVA 할당 흐름 */
/* 1. per-CPU rcache에서 먼저 시도 (잠금 없음) */
/* 2. 캐시 미스 → RB-tree에서 할당 (spinlock 획득) */
/* 3. 해제 시 rcache에 반환 (캐시 가득 차면 RB-tree로) */
struct iova *iova = alloc_iova(&iovad, size >> iova_shift(&iovad),
dma_limit >> iova_shift(&iovad), true);
if (!iova)
return DMA_MAPPING_ERROR;
/* IOVA 해제 */
free_iova(&iovad, iova->pfn_lo);
iommu.strict=0(Flush Queue 모드)과 rcache를 함께 사용하면, IOTLB 무효화(Invalidation)를 지연(Latency)시키고 IOVA를 per-CPU 캐시에서 빠르게 재활용(Recycling)하여 DMA 매핑 오버헤드를 크게 줄일 수 있습니다.
IOVA 할당자 심층 분석
IOVA 할당은 DMA 매핑 성능의 핵심 요소입니다. 고빈도 네트워크/스토리지 워크로드에서 초당 수백만 건의 IOVA 할당/해제가 발생하므로, per-CPU magazine cache (rcache)를 통한 잠금(Lock) 없는 빠른 경로가 필수적입니다.
iova_magazine 구조
/* drivers/iommu/iova.c */
#define IOVA_MAG_SIZE 128 /* 매거진당 IOVA 슬롯 수 */
struct iova_magazine {
unsigned long size; /* 현재 저장된 IOVA 수 */
unsigned long pfns[IOVA_MAG_SIZE]; /* IOVA PFN 배열 */
};
struct iova_cpu_rcache {
spinlock_t lock;
struct iova_magazine *loaded; /* 현재 활성 매거진 */
struct iova_magazine *prev; /* 이전 매거진 (백업) */
};
struct iova_rcache {
struct iova_cpu_rcache __percpu *cpu_rcaches;
struct iova_magazine *depot[MAX_GLOBAL_MAGS]; /* 전역 매거진 풀 */
unsigned long depot_size;
spinlock_t depot_lock;
};
/* 크기별 rcache 인덱스: 1 페이지, 2 페이지, ..., 128 페이지 */
/* 요청 크기 → log2 버킷 → 해당 rcache에서 검색 */
alloc_iova_fast() 코드 경로
unsigned long alloc_iova_fast(
struct iova_domain *iovad,
unsigned long size, /* 요청 크기 (페이지 단위) */
unsigned long limit_pfn, /* DMA 주소 상한 */
bool flush_rcache)
{
unsigned long iova_pfn;
/* Fast path: per-CPU rcache에서 할당 (잠금 최소) */
iova_pfn = iova_rcache_get(iovad, size, limit_pfn);
if (iova_pfn)
return iova_pfn;
/* Slow path: rb-tree에서 할당 (spinlock 필요) */
iova_pfn = __alloc_and_insert_iova_range(iovad, size, limit_pfn);
return iova_pfn;
}
/* 해제 */
void free_iova_fast(struct iova_domain *iovad,
unsigned long pfn, unsigned long size)
{
/* rcache에 반환 시도 */
if (iova_rcache_insert(iovad, pfn, size))
return;
/* rcache 가득 참 → rb-tree에 직접 반환 */
free_iova(iovad, pfn);
}
코드 설명
drivers/iommu/iova.c에 구현된 IOVA 할당/해제 함수입니다. per-CPU magazine cache(rcache)를 우선 사용하여 잠금(Lock) 경합을 최소화하는 2단계 설계입니다.
- alloc_iova_fast() — Fast path
iova_rcache_get()으로 현재 CPU의 magazine cache에서 요청 크기에 맞는 IOVA를 꺼냅니다. spinlock 없이 동작하므로 고빈도 DMA 워크로드에서 핵심 성능 경로입니다. - alloc_iova_fast() — Slow pathrcache 미스 시
__alloc_and_insert_iova_range()로 RB-tree에서 할당합니다.iova_rbtree_lockspinlock을 획득해야 하므로 멀티코어 환경에서 경합이 발생할 수 있습니다. - free_iova_fast() — rcache 반환
iova_rcache_insert()로 해제된 IOVA를 per-CPU cache에 반환합니다. 다음alloc_iova_fast()호출 시 즉시 재활용되어 RB-tree 접근을 피합니다. - free_iova_fast() — RB-tree fallbackmagazine이 가득 차면
free_iova()로 RB-tree에 직접 반환합니다. magazine 크기는 고정이므로 캐시 오버플로우 시에만 이 경로를 탑니다.
32-bit DMA 별도 경로
32-bit DMA 장치는 4GB 이하 IOVA만 사용할 수 있으므로, iova_domain에 cached32_node를 별도로 유지합니다.
dma_32bit_pfn = 4GB >> PAGE_SHIFTlimit_pfn이dma_32bit_pfn이하이면 별도 캐시를 사용합니다.- 이는 32-bit와 64-bit 장치가 혼재할 때 서로의 IOVA 공간을 침범하지 않도록 분리하기 위함입니다.
rb-tree에서 검색 시:
- 64-bit:
cached_node부터 탑다운으로 검색합니다. - 32-bit:
cached32_node부터 탑다운으로 검색합니다.
| 경로 | 잠금 | 레이턴시 | 비고 |
|---|---|---|---|
| rcache loaded hit | 없음 (per-CPU) | ~10-30 ns | 최적 경로 |
| rcache prev hit | CPU-local spinlock | ~30-50 ns | loaded/prev 스왑(Swap) |
| depot → magazine | depot spinlock | ~50-100 ns | 전역 풀 접근 |
| rb-tree fallback | iova_rbtree_lock | ~100-500 ns | 최악 경로 (경합(Contention) 시) |
/sys/kernel/debug/iova/에서 확인할 수 있습니다(debugfs 활성 시).
rcache 히트율이 90% 미만이면 매거진 크기 조정이나 Flush Queue 모드(iommu.strict=0) 활성화를 검토하세요.
Flush Queue는 IOVA 재사용을 지연시켜 rcache에 반환되는 IOVA를 늘려 히트율을 높입니다.
IOMMU Group 형성 알고리즘
IOMMU Group은 하드웨어적으로 격리 가능한 최소 디바이스 집합입니다. 같은 그룹의 디바이스는 서로의 DMA 트래픽을 볼 수 있어, 보안 관점에서 하나의 단위로 취급해야 합니다. 그룹 형성은 PCIe 토폴로지(Topology)와 ACS(Access Control Services) 지원 여부에 의해 결정됩니다.
pci_device_group() 로직
/* drivers/iommu/iommu.c */
struct iommu_group *pci_device_group(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
/* 1. DMA alias 확인 */
/* 일부 디바이스는 다른 BDF로 DMA 수행 (quirk) */
pci_for_each_dma_alias(pdev, get_pci_alias_or_group, &data);
/* 2. alias 그룹이 이미 있으면 합류 */
if (data.group)
return data.group;
/* 3. PCI Express가 아닌 디바이스 */
/* (레거시 PCI: 같은 버스의 모든 장치가 한 그룹) */
if (!pci_is_pcie(pdev))
return pci_bus_group(pdev);
/* 4. Root Port까지 ACS 경로 확인 */
if (pci_acs_path_enabled(pdev, NULL, REQ_ACS_FLAGS))
return iommu_group_alloc(); /* 새 독립 그룹 */
/* 5. ACS 미지원 → 상위 브리지의 기존 그룹에 합류 */
return find_or_create_bridge_group(pdev);
}
pci_acs_path_enabled() — ACS 체크 체인
/* drivers/pci/acs.c */
bool pci_acs_path_enabled(struct pci_dev *start,
struct pci_dev *end,
u16 acs_flags)
{
/* start부터 end(NULL이면 Root Port)까지 경로의
* 모든 브리지/스위치가 요청된 ACS 비트를 지원하는지 확인 */
struct pci_dev *pdev = start;
while (pdev != end) {
if (!pci_acs_enabled(pdev, acs_flags))
return false;
pdev = pci_upstream_bridge(pdev);
}
return true;
}
/* 필수 ACS 플래그 */
#define REQ_ACS_FLAGS (PCI_ACS_SV | PCI_ACS_RR | PCI_ACS_CR | PCI_ACS_UF)
/* SV: Source Validation — 소스 ID 검증 */
/* RR: P2P Request Redirect — P2P 요청을 상위로 리디렉트 */
/* CR: P2P Completion Redirect — P2P 완료를 상위로 리디렉트 */
/* UF: Upstream Forwarding — 상위로 포워딩 차단 */
IOMMU Group 확인 실전
# 시스템의 모든 IOMMU group과 디바이스 목록
for g in /sys/kernel/iommu_groups/*/devices/*; do
echo "Group $(basename $(dirname $(dirname $g))): $(basename $g)"
done
# ACS 지원 확인
# lspci -vvv -s 01:00.0 | grep -A5 "Access Control Services"
Capabilities: [2d0] Access Control Services
ACSCap: SrcValid+ TransBlk+ ReqRedir+ CmpltRedir+ UpstreamFwd+ EgressCtrl+ DirectTrans+
ACSCtl: SrcValid+ TransBlk- ReqRedir+ CmpltRedir+ UpstreamFwd- EgressCtrl- DirectTrans-
# ACS override (보안 위험! 개발/테스트 전용)
# 커널 파라미터: pci=noacs
# 패치: acs_override quirk (비공식)
| 디바이스 유형 | Group 형성 규칙 | 일반적 결과 |
|---|---|---|
| Root Port 직접 연결 | 항상 독립 그룹 | GPU, NVMe 등 개별 passthrough 가능 |
| PCIe 스위치 뒤 (ACS 지원) | 각각 독립 그룹 | 서버 보드에서 흔함 |
| PCIe 스위치 뒤 (ACS 미지원) | 스위치 뒤 전체 병합 | 소비자 보드에서 흔함 (VFIO 제약) |
| 레거시 PCI (non-Express) | 같은 버스(Bus) 전체 병합 | PCI 브리지(Bridge) 뒤 모든 장치 |
| 멀티펑션 디바이스 | 함수 간 ACS에 따라 결정 | 대부분 같은 그룹 |
| Non-PCI (platform) | 디바이스별 독립 또는 DT/ACPI 정의 | ARM SoC에서 흔함 |
pci=noacs나 ACS override 패치(Patch)는 IOMMU group을 인위적으로 분리합니다.
이는 실제 하드웨어 격리를 우회하므로, 같은 스위치 뒤의 악의적 디바이스(또는 VM)가
다른 디바이스의 DMA 트래픽을 가로채거나 조작할 수 있습니다.
운영 환경에서는 절대 사용하지 마세요. 개발/테스트에서만 임시로 사용하세요.
find /sys/kernel/iommu_groups/*/devices/ -name "*.0" | wc -l로 독립 group 수를 파악할 수 있습니다.
Intel VT-d
VT-d 데이터 구조
Intel VT-d는 계층적 페이지 테이블 구조를 사용합니다.
VT-d MMIO 레지스터(Register)
/* VT-d 레지스터 오프셋 */
#define DMAR_VER_REG 0x00 /* Version */
#define DMAR_CAP_REG 0x08 /* Capability */
#define DMAR_ECAP_REG 0x10 /* Extended Capability */
#define DMAR_GCMD_REG 0x18 /* Global Command */
#define DMAR_GSTS_REG 0x1c /* Global Status */
#define DMAR_RTADDR_REG 0x20 /* Root Table Address */
#define DMAR_CCMD_REG 0x28 /* Context Command */
#define DMAR_IVA_REG 0x50 /* Invalidate Address */
#define DMAR_IOTLB_REG 0x58 /* IOTLB Invalidate */
#define DMAR_IRTA_REG 0xb8 /* Interrupt Remapping Table */
/* Capability 비트 */
#define DMA_CAP_SAGAW ((1 << 8) - 1) /* Supported AGW */
#define DMA_CAP_CM (1 << 7) /* Caching Mode */
#define DMA_CAP_PI (1 << 59) /* Posted Interrupts */
DMAR (DMA Remapping)
ACPI DMAR 테이블은 VT-d 하드웨어 유닛을 기술합니다.
/* ACPI DMAR 테이블 파싱 */
# dmesg | grep -i dmar
DMAR: Host address width 39
DMAR: DRHD base: 0x00000000fed90000 flags: 0x1
DMAR: RMRR base: 0x0000000079800000 end: 0x000000007affffff
DMAR: IOMMU enabled
/* 커널 파라미터 */
intel_iommu=on # VT-d 활성화
intel_iommu=on,sm_on # Scalable Mode 활성화
intel_iommu=off # VT-d 비활성화
iommu=pt # Pass-through 모드 (성능 우선)
Intel Scalable Mode (확장 모드)
Intel VT-d의 Scalable Mode는 Legacy Mode를 대체하는 확장 아키텍처로, PASID 기반 주소 공간 분리와 First/Second Level 페이지 테이블을 지원합니다. SVA, Nested Translation, Posted Interrupt 등 현대적 기능은 Scalable Mode에서만 동작합니다.
Legacy vs Scalable 모드 비교
| 항목 | Legacy Mode | Scalable Mode |
|---|---|---|
| Context Entry | 64-bit | 128-bit |
| PASID 지원 | 제한적 (Extended Context) | 네이티브 (PASID Table) |
| First Level PT | 미지원 | 지원 (CPU PTE 호환) |
| Nested Translation | 미지원 | 지원 (PGTT=011) |
| SVA/SVM | 미지원 | 지원 (First Level + PASID) |
| 5-Level PT (57-bit) | 미지원 | 지원 |
| 커널 파라미터 | 기본 (구형 HW) | intel_iommu=sm_on |
PASID Table Entry 구조 (64 bytes)
/* drivers/iommu/intel/pasid.c */
/* PASID Table Entry — 512-bit (64 bytes, 8 × u64) */
struct pasid_entry {
u64 val[8];
};
/* 주요 필드 (val[0]) */
#define PASID_ENTRY_P (1ULL << 0) /* Present */
#define PASID_ENTRY_PGTT (7ULL << 6) /* Page Table Type [8:6] */
#define PASID_ENTRY_SRE (1ULL << 1) /* Supervisor Request Enable */
#define PASID_ENTRY_EAFE (1ULL << 7) /* Execute Access Fault Enable */
/* PGTT (Page Table Type) 값 */
#define PASID_PGTT_FL_ONLY 1 /* First Level Only (SVA) */
#define PASID_PGTT_SL_ONLY 2 /* Second Level Only (DMA) */
#define PASID_PGTT_NESTED 3 /* Nested (FL→SL, 가상화) */
#define PASID_PGTT_PT 4 /* Pass-through (변환 없음) */
/* First Level PT 포인터 (val[2]) */
/* FLPTPTR[63:12] = First Level PML5/PML4 주소 */
/* SVA: 프로세스의 CR3 값을 직접 기록 */
/* Second Level PT 포인터 (val[0]) */
/* SLPTPTR[63:12] = Second Level PML4 주소 */
Scalable Mode 설정 코드
/* drivers/iommu/intel/iommu.c */
static int intel_iommu_init(void)
{
/* ECAP_SMTS 비트로 Scalable Mode 지원 확인 */
if (ecap_smts(iommu->ecap)) {
/* Root Table의 RTADDR_REG[11] = TTM (Translation Table Mode) */
/* TTM=01: Scalable Mode */
iommu->root_entry[bus].hi |= ROOT_ENTRY_TABLE_TYPE;
}
}
/* Scalable Mode Context Entry 설정 */
static int domain_context_mapping_one(...)
{
/* 128-bit Context Entry 기록 */
context->lo = PASID_TABLE_ADDR | CONTEXT_PRESENT;
context->hi = CONTEXT_TT_DEV_IOTLB | /* ATS 지원 */
CONTEXT_PASIDE; /* PASID 활성화 */
}
코드 설명
drivers/iommu/intel/iommu.c에 구현된 Intel VT-d Scalable Mode 초기화 흐름입니다. Scalable Mode는 PASID, SVA, Nested Translation 등 현대적 기능의 전제 조건입니다.
- ecap_smts()ECAP(Extended Capability) 레지스터의 SMTS 비트를 확인하여 하드웨어가 Scalable Mode를 지원하는지 검사합니다. 미지원 시 Legacy Mode로 동작합니다.
- ROOT_ENTRY_TABLE_TYPE
RTADDR_REG(Root Table Address Register)의 TTM(Translation Table Mode) 비트를01로 설정하여 Scalable Mode를 활성화합니다. Legacy Mode는 TTM=00입니다. - domain_context_mapping_one()Scalable Mode에서는 Context Entry가 128비트로 확장됩니다.
lo필드에 PASID Table의 물리 주소와 Present 비트를,hi필드에 ATS 지원(TT_DEV_IOTLB)과 PASID 활성화(PASIDE) 플래그를 기록합니다.
intel_iommu=sm_on 커널 파라미터로 활성화합니다.
최신 커널(6.0+)에서는 하드웨어가 지원하면 기본 활성화됩니다 (CONFIG_INTEL_IOMMU_SCALABLE_MODE_DEFAULT_ON=y).
dmesg | grep "Scalable"으로 활성화 여부를 확인하세요.
CR3 값(pgd 물리 주소)을 직접 기록합니다. IOMMU가 CPU와 동일한 x86 PTE를 워크하므로,
디바이스는 유저 공간 가상 주소로 직접 DMA할 수 있습니다. 페이지 폴트(Page Fault) 시 PRI를 통해 커널이 페이지(Page)를 할당합니다.
IOMMU 페이지 테이블 워크
IOMMU의 페이지 테이블 워크는 IOVA를 물리 주소로 변환하는 핵심 과정입니다. Intel, AMD, ARM 세 아키텍처 모두 계층적 페이지 테이블을 사용하지만, 엔트리 포맷과 레벨 구조가 다릅니다.
Intel 페이지 테이블 워크
/* drivers/iommu/intel/iommu.c */
/* IOVA → PTE 조회 (Second Level) */
static struct dma_pte *pfn_to_dma_pte(
struct dmar_domain *domain,
unsigned long pfn,
int *target_level)
{
struct dma_pte *parent, *pte;
int level = agaw_to_level(domain->agaw);
parent = domain->pgd; /* PML4 (최상위) */
while (level > 0) {
int offset = pfn_level_offset(pfn, level);
pte = &parent[offset];
/* 대형 페이지 확인: SP 비트 설정이면 워크 중단 */
if (dma_pte_superpage(pte) || level == 1) {
*target_level = level;
return pte;
}
/* 다음 레벨 테이블 주소 추출 */
parent = phys_to_virt(dma_pte_addr(pte));
level--;
}
return NULL;
}
ARM SMMUv3 Stream Table 구조
/* ARM SMMUv3: Linear vs 2-Level Stream Table */
/* Linear Stream Table: SID 직접 인덱싱 */
/* 크기 = num_sids × 64 bytes (STE 크기) */
/* 적은 디바이스 수에 적합 */
/* 2-Level Stream Table: SID를 2단계로 분해 */
/* L1: SID[N:8] → L1 Descriptor (L2 테이블 주소) */
/* L2: SID[7:0] → STE (Stream Table Entry) */
/* 많은 디바이스에서 메모리 절약 */
struct arm_smmu_ste {
__le64 data[8]; /* 512-bit (64 bytes) */
};
/* STE → Context Descriptor → TTB (Translation Table Base) */
/* Stage 1: TTB0 → LPAE 워크 (VA→IPA) */
/* Stage 2: VMID + VTTBR → LPAE 워크 (IPA→PA) */
주소 폭(AGAW) 비교
| 아키텍처 | 주소 폭 | 레벨 수 | 디바이스 인덱싱 |
|---|---|---|---|
| Intel VT-d | 39/48/57-bit | 3/4/5-level | Root Table [Bus] → Context [Dev:Fn] |
| AMD-Vi | 48/57-bit | 4/5-level | Device Table [BDF] 직접 |
| ARM SMMUv3 | 32/36/40/42/44/48-bit | 1~4-level | Stream Table [SID] → CD [SSID] |
| 대형 페이지 | Intel VT-d | AMD-Vi | ARM SMMU |
|---|---|---|---|
| 2MB | Level 2 (SP=1) | Level 2 (NL=0) | Level 2 Block |
| 1GB | Level 3 (SP=1) | Level 3 (NL=0) | Level 1 Block |
| 효과 | 워크 레벨 단축 + IOTLB 커버리지 증가 → 성능 향상 | ||
iommu_map() 호출 시 pgsize_bitmap에 따라 자동으로 최적 페이지 크기를 선택합니다.
AMD-Vi (AMD I/O Virtualization)
AMD-Vi는 AMD 플랫폼의 IOMMU 구현으로, Intel VT-d와 유사한 기능을 제공하지만 다른 하드웨어 구조를 사용합니다.
AMD-Vi 데이터 구조
AMD-Vi는 Device Table을 사용하여 디바이스별 설정을 관리합니다. Intel의 Root/Context 2단계 구조와 달리, 단일 Device Table에서 BDF(Bus/Device/Function)를 직접 인덱싱합니다.
/* AMD IOMMU Device Table Entry (drivers/iommu/amd/amd_iommu_types.h) */
struct dev_table_entry {
u64 data[4]; /* 256-bit (32 바이트) */
};
/* DTE 주요 비트 필드 */
#define DTE_FLAG_V (1ULL << 0) /* Valid */
#define DTE_FLAG_TV (1ULL << 1) /* Translation Valid */
#define DTE_FLAG_IR (1ULL << 61) /* Interrupt Remapping */
#define DTE_FLAG_IW (1ULL << 62) /* Interrupt Write */
#define DTE_FLAG_IOTLB (1ULL << 9) /* IOTLB 지원 (ATS) */
#define DTE_FLAG_GV (1ULL << 2) /* Guest Translation Valid (v2) */
AMD-Vi 커널 파라미터
# AMD IOMMU 활성화 (대부분 자동 감지)
amd_iommu=on # 명시적 활성화
amd_iommu=off # 비활성화
amd_iommu=force_isolation # 모든 디바이스 격리 강제
iommu=pt # pass-through 모드 (Intel/AMD 공통)
# IVRS (I/O Virtualization Reporting Structure) 확인
# dmesg | grep -i amd
AMD-Vi: Found IOMMU at 0000:00:00.2 cap 0x40
AMD-Vi: Interrupt remapping enabled
AMD-Vi: Lazy IO/TLB flushing enabled
Interrupt Remapping
Interrupt remapping은 MSI/MSI-X 인터럽트를 IOMMU가 가로채서 재매핑합니다. 이는 interrupt injection 공격을 방지합니다.
Interrupt Remapping Table Entry (IRTE)
struct irte {
u64 present : 1;
u64 fpd : 1; /* Fault Processing Disable */
u64 dst_mode : 1; /* Destination Mode (physical/logical) */
u64 redir_hint : 1; /* Redirection Hint */
u64 trigger_mode : 1; /* Trigger Mode (edge/level) */
u64 dlvry_mode : 3; /* Delivery Mode */
u64 avail : 4;
u64 reserved : 3;
u64 irte_mode : 1; /* 0=Remapped, 1=Posted */
u64 vector : 8; /* Interrupt Vector */
u64 reserved2 : 8;
u64 dest_id : 32; /* Destination APIC ID */
};
/* Interrupt remapping 활성화 */
# dmesg | grep -i "interrupt remapping"
DMAR-IR: Enabled IRQ remapping in x2apic mode
Posted Interrupts
Posted Interrupts는 가상화 환경에서 인터럽트를 VM에 직접 전달하여 VM-exit를 줄입니다.
struct pi_desc {
u32 pir[8]; /* Posted Interrupt Requests (256-bit) */
u32 control; /* ON, SN, NV */
u32 rsvd[7];
} __aligned(64);
/* VT-d Posted Interrupt Descriptor Address */
#define IRTE_PI_ADDR_MASK 0xfffffffffff00000
PASID (Process Address Space ID)
PASID는 단일 디바이스가 여러 주소 공간(프로세스)을 구분할 수 있게 합니다.
PASID Table
struct pasid_entry {
u64 present : 1;
u64 pwt : 1; /* Page-level Write-Through */
u64 pcd : 1; /* Page-level Cache Disable */
u64 reserved : 9;
u64 slptptr : 52; /* Second Level Page Table Pointer */
u64 pgtt : 3; /* Page Table Type */
u64 reserved2 : 61;
};
/* PASID 할당 */
int pasid;
pasid = iommu_sva_alloc_pasid(dev, 1, (1 << 20) - 1);
SVA (Shared Virtual Addressing)
SVA는 CPU와 디바이스가 동일한 가상 주소 공간을 공유합니다. CPU의 페이지 테이블을 IOMMU가 직접 사용합니다.
#include <linux/iommu.h>
/* SVA 바인딩 */
struct iommu_sva *sva;
sva = iommu_sva_bind_device(dev, current->mm, NULL);
if (IS_ERR(sva))
return PTR_ERR(sva);
/* PASID 획득 */
int pasid = iommu_sva_get_pasid(sva);
/* 디바이스는 이제 user space 가상 주소 직접 사용 가능 */
void *user_buf = mmap(...);
device_submit_command(user_buf, pasid); /* CPU와 동일 VA */
/* SVA 언바인딩 */
iommu_sva_unbind_device(sva);
ATS / PRI (PCIe IOMMU 가속)
ATS(Address Translation Services)와 PRI(Page Request Interface)는 PCIe 디바이스가 IOMMU와 직접 상호작용하여 성능을 높이는 PCIe 확장 기능입니다.
ATS (Address Translation Services)
ATS를 지원하는 디바이스는 IOMMU에 주소 변환을 요청하여 결과를 디바이스 내부 ATC(Address Translation Cache)에 캐싱합니다. 이후 DMA 요청 시 IOMMU 페이지 테이블 워크를 건너뛸 수 있어 레이턴시가 줄어듭니다.
/* ATS 활성화 (drivers/pci/ats.c) */
int pci_enable_ats(struct pci_dev *dev, int ps)
{
u16 ctrl;
if (!dev->ats_cap)
return -EINVAL;
pci_read_config_word(dev, dev->ats_cap + PCI_ATS_CTRL, &ctrl);
ctrl |= PCI_ATS_CTRL_ENABLE;
ctrl |= PCI_ATS_CTRL_STU(ps); /* Smallest Translation Unit */
pci_write_config_word(dev, dev->ats_cap + PCI_ATS_CTRL, ctrl);
dev->ats_enabled = 1;
return 0;
}
/* ATS 무효화 (IOMMU가 매핑 변경 시) */
void pci_ats_invalidate(struct pci_dev *dev, u16 did,
unsigned long addr, unsigned int size);
PRI (Page Request Interface)
PRI는 SVA 환경에서 디바이스가 매핑되지 않은 페이지에 접근할 때 page fault를 발생시키고, OS가 페이지를 할당한 뒤 디바이스에 응답하는 메커니즘입니다. CPU의 demand paging과 유사합니다.
/* PRI 활성화 */
pci_enable_pri(pdev, 32); /* 최대 32개 outstanding page request */
/* Page Request 처리 흐름 */
/* 1. 디바이스가 IOVA에 접근 → IOMMU 변환 실패 */
/* 2. IOMMU가 Page Request 큐에 이벤트 기록 */
/* 3. 커널이 페이지 할당/매핑 후 Page Response 전송 */
/* 4. 디바이스가 DMA 재시도 */
/* IOMMU 드라이버에서 PRI 이벤트 처리 */
static irqreturn_t priq_event_thread(int irq, void *data)
{
struct arm_smmu_device *smmu = data;
/* Page Request Queue에서 이벤트 읽기 */
/* handle_mm_fault() 호출 → 페이지 할당 */
/* iommu_page_response() 로 디바이스에 응답 */
return IRQ_HANDLED;
}
pci=noats로 전역 비활성화할 수 있습니다.
IOPF (I/O Page Fault) 프레임워크
IOPF(I/O Page Fault)는 SVA/PRI 환경에서 디바이스가 매핑되지 않은 가상 주소에 접근할 때 발생하는 복구 가능한 fault를 처리하는 커널 프레임워크입니다. CPU의 demand paging과 유사하게, 디바이스 DMA도 lazy하게 페이지를 할당받을 수 있습니다.
IOPF 주요 구조체
/* drivers/iommu/io-pgfault.c */
struct iopf_fault {
struct iommu_fault fault; /* fault 정보 */
struct list_head list; /* 그룹 fault 연결 */
};
struct iopf_group {
struct iopf_fault last_fault; /* 마지막 fault (응답 대상) */
struct list_head faults; /* 같은 Group ID의 fault 목록 */
struct list_head pending; /* 보류 큐 */
struct device *dev;
struct work_struct work; /* workqueue에서 처리 */
};
/* Fault 유형 */
struct iommu_fault {
u32 type; /* IOMMU_FAULT_DMA_UNRECOV 또는
* IOMMU_FAULT_PAGE_REQ */
struct iommu_fault_page_request prm; /* page request 정보 */
};
struct iommu_fault_page_request {
u32 flags; /* IOMMU_FAULT_PAGE_REQUEST_PASID_VALID 등 */
u32 pasid; /* 요청 프로세스 PASID */
u32 grpid; /* Group ID (관련 fault 묶기) */
u32 perm; /* 요청 권한 (R/W/X) */
u64 addr; /* fault 주소 (가상 주소) */
};
IOPF 핸들러(Handler) 코드 경로
/* IOMMU 드라이버가 fault 보고 */
iommu_report_device_fault(dev, &fault_evt);
/* → iopf_group에 fault 추가 */
/* → last_fault(LPSG=1)이면 workqueue 스케줄 */
/* workqueue에서 실행 */
static void iopf_handler(struct work_struct *work)
{
struct iopf_group *group = ...;
/* SVA 핸들러 호출 */
ret = iommu_sva_handle_iopf(group);
/* 내부:
* 1. PASID → mm_struct 조회
* 2. mmap_read_lock(mm)
* 3. handle_mm_fault(vma, addr, flags)
* → 일반 page fault 처리 (demand paging)
* 4. mmap_read_unlock(mm)
*/
/* 응답 전송 */
struct iommu_page_response resp = {
.code = ret ? IOMMU_PAGE_RESP_FAILURE
: IOMMU_PAGE_RESP_SUCCESS,
.grpid = group->last_fault.fault.prm.grpid,
.pasid = group->last_fault.fault.prm.pasid,
};
iommu_page_response(dev, &group->last_fault.fault, &resp);
}
IOPF 지원 매트릭스
| 구성 요소 | Intel VT-d | AMD-Vi v2 | ARM SMMUv3 |
|---|---|---|---|
| PRI 지원 | Scalable Mode 필요 | PPR (Peripheral Page Request) | PRIQ (PRI Queue) |
| PASID 필수 | 예 | 예 | 예 (SSID) |
| fault 보고 경로 | QI → prq_event_thread | PPR Log → ppr_event_thread | PRIQ → priq_event_thread |
| 응답 경로 | QI: Page Group Response | Command Buffer: COMPLETE_PPR | CMDQ: CMD_PRI_RESP |
| 커널 설정 | CONFIG_INTEL_IOMMU_SVM |
CONFIG_AMD_IOMMU_V2 |
CONFIG_ARM_SMMU_V3_SVA |
Group ID로 묶어 보고하며, 마지막 fault에 LAST_PAGE(LPSG) 플래그를 설정합니다.
커널은 그룹의 모든 fault를 처리한 후 한 번의 page_response()로 응답합니다.
mlock()이나 pin_user_pages()로 페이지를 고정하여 IOPF 발생을 최소화하세요.
VFIO (Virtual Function I/O)
VFIO는 IOMMU를 활용하여 userspace에서 안전하게 디바이스를 제어할 수 있게 합니다.
VFIO Container & Group
/* VFIO 사용 예시 (QEMU/KVM) */
# modprobe vfio-pci
# PCI 디바이스를 vfio-pci로 바인딩
# echo 0000:01:00.0 > /sys/bus/pci/devices/0000\:01\:00.0/driver/unbind
# echo 8086 10fb > /sys/bus/pci/drivers/vfio-pci/new_id
# IOMMU group 확인
# readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group
../../../kernel/iommu_groups/1
# QEMU에서 디바이스 패스스루
qemu-system-x86_64 \
-device vfio-pci,host=01:00.0 \
...
VFIO API
#include <linux/vfio.h>
/* VFIO container 열기 */
int container = open("/dev/vfio/vfio", O_RDWR);
/* IOMMU group 열기 */
int group = open("/dev/vfio/1", O_RDWR);
/* Group을 container에 추가 */
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
/* IOMMU type 설정 (Type1 = DMA remapping) */
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
/* DMA 매핑 */
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)buffer,
.iova = 0x100000,
.size = 4096,
};
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
코드 설명
VFIO 유저스페이스 API를 사용한 디바이스 패스스루(passthrough) DMA 매핑 시퀀스입니다. QEMU/KVM 가상화에서 물리 디바이스를 VM에 직접 할당할 때 이 패턴을 사용합니다.
- /dev/vfio/vfioVFIO container를 엽니다. Container는 하나의 IOMMU domain을 나타내며, 여러 IOMMU group을 포함할 수 있습니다.
- /dev/vfio/1IOMMU group 1을 엽니다. 그룹 번호는
/sys/kernel/iommu_groups/에서 확인할 수 있으며, 같은 그룹의 모든 디바이스가 함께 할당됩니다. - VFIO_GROUP_SET_CONTAINERgroup을 container에 추가합니다. 커널 내부에서
iommu_group_claim_dma_owner()가 호출되어 기존 DMA domain이 해제됩니다. - VFIO_SET_IOMMU
VFIO_TYPE1_IOMMU를 설정하여 DMA remapping 모드를 활성화합니다. 커널이IOMMU_DOMAIN_UNMANAGEDdomain을 할당하고 디바이스를 attach합니다. - VFIO_IOMMU_MAP_DMA유저 버퍼의 가상 주소(
vaddr)를 IOVA0x100000에 매핑합니다. 커널이 유저 페이지를 pin하고iommu_map()으로 IOVA→물리 주소 매핑을 설정합니다. VM에서 디바이스가 이 IOVA로 DMA하면 호스트 물리 메모리에 접근합니다.
Domain Attach/Switch 경로
IOMMU domain의 attach/detach는 디바이스의 DMA 주소 공간을 전환하는 핵심 동작입니다. 부팅 시 DMA domain → VFIO 바인딩 시 UNMANAGED domain → VFIO 해제 시 DMA domain 복귀가 전형적인 라이프사이클입니다.
iommu_attach_device() 내부
/* drivers/iommu/iommu.c */
int iommu_attach_device(struct iommu_domain *domain,
struct device *dev)
{
struct iommu_group *group = dev->iommu_group;
/* 1. 현재 domain detach */
__iommu_detach_device(group->domain, dev);
/* 2. 벤더별 attach 콜백 */
ret = domain->ops->attach_dev(domain, dev);
/* Intel: intel_iommu_attach_device()
* → context entry 기록 + root table 업데이트
* → qi_flush_context() + qi_flush_iotlb()
* AMD: amd_iommu_attach_device()
* → DTE(Device Table Entry) 업데이트
* → command buffer로 DTE + IOTLB flush
* ARM: arm_smmu_attach_dev()
* → STE(Stream Table Entry) 업데이트
* → CMDQ: CFGI_STE + TLBI_NSNH_ALL */
/* 3. group->domain 갱신 */
group->domain = domain;
return ret;
}
코드 설명
drivers/iommu/iommu.c의 domain attach 핵심 함수입니다. 디바이스의 DMA 주소 공간을 전환하는 원자적(atomic) 동작입니다.
- __iommu_detach_device()현재 domain에서 디바이스를 분리합니다. 벤더 드라이버의
detach_dev콜백이 하드웨어 테이블에서 디바이스 엔트리를 제거하고 IOTLB를 flush합니다. - domain->ops->attach_dev()새 domain의 벤더별 attach 콜백을 호출합니다. Intel VT-d는 Context Entry를 기록하고 QI(Queued Invalidation)로 flush하며, AMD는 DTE(Device Table Entry)를 갱신하고 Command Buffer로 flush합니다. ARM SMMU는 STE(Stream Table Entry)를 갱신하고 CMDQ로
CFGI_STE+TLBI_NSNH_ALL명령을 전송합니다. - group->domain = domain그룹의 현재 domain 포인터를 갱신합니다. 이후 같은 그룹의 다른 디바이스도 이 domain의 주소 공간을 공유합니다.
VFIO DMA Owner Claim (커널 5.18+)
/* VFIO가 디바이스 그룹을 점유할 때 */
int iommu_group_claim_dma_owner(struct iommu_group *group,
void *owner)
{
/* 1. 그룹 내 모든 디바이스가 바인딩된 드라이버 없는지 확인 */
/* (커널 드라이버 → vfio-pci로 전환 완료 후 호출) */
/* 2. 기존 DMA domain detach */
/* 3. BLOCKED domain 임시 attach (DMA 차단) */
/* 4. owner 기록 → UNMANAGED domain attach 허용 */
}
/* VFIO 해제 시 */
void iommu_group_release_dma_owner(struct iommu_group *group)
{
/* 1. UNMANAGED domain detach */
/* 2. 기본 DMA domain 복원 attach */
/* 3. owner 해제 */
}
코드 설명
drivers/iommu/iommu.c에 구현된 VFIO DMA 소유권 관리 함수입니다. 커널 5.18+에서 도입되어, VFIO가 디바이스 그룹을 점유/해제할 때 domain 전환을 안전하게 관리합니다.
- iommu_group_claim_dma_owner()VFIO가 그룹을 점유할 때 호출됩니다. 먼저 그룹 내 모든 디바이스에 커널 드라이버가 바인딩되어 있지 않은지 확인합니다(vfio-pci 전환 완료 후). 기존 DMA domain을 detach하고
IOMMU_DOMAIN_BLOCKEDdomain을 임시 attach하여 DMA를 완전히 차단합니다. 이후 VFIO가IOMMU_DOMAIN_UNMANAGEDdomain을 attach할 수 있게 됩니다. - iommu_group_release_dma_owner()VFIO 해제 시 호출됩니다. UNMANAGED domain을 detach하고, 부팅 시 할당된 기본 DMA domain을 복원합니다. 이후 커널 드라이버가 다시 바인딩되면 정상적인 DMA API를 사용할 수 있습니다.
sysfs Domain 타입 전환 (커널 6.2+)
# 현재 domain 타입 확인
# cat /sys/kernel/iommu_groups/3/type
DMA
# DMA → identity 전환 (디바이스에 바인딩된 드라이버 없을 때만)
# echo identity > /sys/kernel/iommu_groups/3/type
# 허용 타입 목록
# cat /sys/kernel/iommu_groups/3/available_types
DMA DMA-FQ identity
# Flush Queue 모드로 전환
# echo DMA-FQ > /sys/kernel/iommu_groups/3/type
iommu_probe_device()가 호출되어 기본 domain에 자동 attach됩니다.
그러나 같은 IOMMU group 내 다른 디바이스가 이미 VFIO에 할당된 경우, 새 디바이스의 group 배치와 domain 전환에 충돌이 발생할 수 있습니다.
핫플러그 시나리오에서는 group 구성을 사전에 검증하세요.
iommufd 기반 API가 더 효율적입니다.
ARM SMMU
ARM System Memory Management Unit는 ARM 플랫폼의 IOMMU 구현입니다.
SMMU 아키텍처
SMMU 버전
| 버전 | 특징 |
|---|---|
| SMMUv2 | Stream ID (SID) 기반, Context Bank, Stage 1/2 translation |
| SMMUv3 | 향상된 확장성, Command Queue, Event Queue, PRI/ATS 지원 |
| SMMUv3.1 | Sub-streams, HTTU (Hardware Translation Table Update) |
SMMU 드라이버
/* Device Tree에서 SMMU 정의 */
smmu: iommu@2b400000 {
compatible = "arm,smmu-v3";
reg = <0x0 0x2b400000 0x0 0x100000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 75 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 76 IRQ_TYPE_EDGE_RISING>;
interrupt-names = "eventq", "priq", "gerror";
#iommu-cells = <1>;
};
/* 디바이스가 SMMU 사용 */
pcie@40000000 {
iommus = <&smmu 0>; /* Stream ID = 0 */
};
SMMUv3 Command/Event Queue
SMMUv3는 소프트웨어와 하드웨어 간 통신을 위해 3개의 링 버퍼(Ring Buffer) 큐를 사용합니다. Command Queue(CMDQ)는 SW→HW 명령 전달, Event Queue(EVENTQ)는 HW→SW 오류 보고, PRI Queue(PRIQ)는 HW→SW 페이지 요청을 처리합니다.
arm_smmu_queue 구조체
/* drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h */
struct arm_smmu_queue {
struct arm_smmu_ll_queue llq; /* prod/cons + max_n_shift */
dma_addr_t base_dma; /* 큐 물리 주소 */
void *base; /* 큐 가상 주소 */
struct arm_smmu_cmdq_ent *entries; /* 엔트리 배열 */
};
struct arm_smmu_ll_queue {
union {
u64 val;
struct {
u32 prod; /* Producer 인덱스 + wrap */
u32 cons; /* Consumer 인덱스 + wrap */
};
};
u32 max_n_shift; /* log2(큐 크기) */
};
/* 큐 사이즈: 2^max_n_shift 엔트리 */
/* CMDQ 엔트리: 16 바이트 (128-bit) */
/* EVENTQ/PRIQ 엔트리: 32 바이트 (256-bit) */
CMDQ 주요 Opcode
| Opcode | 값 | 설명 | 인자 |
|---|---|---|---|
CMDQ_OP_CFGI_STE |
0x03 | Stream Table Entry 캐시 무효화 | SID |
CMDQ_OP_CFGI_CD |
0x05 | Context Descriptor 캐시 무효화 | SID, SSID |
CMDQ_OP_CFGI_ALL |
0x04 | 모든 설정 캐시 무효화 | - |
CMDQ_OP_TLBI_NH_ASID |
0x11 | ASID 기반 TLB 무효화 | ASID |
CMDQ_OP_TLBI_NH_VA |
0x12 | VA 기반 TLB 무효화 | ASID, VA |
CMDQ_OP_TLBI_S2_IPA |
0x2a | Stage 2 IPA TLB 무효화 | VMID, IPA |
CMDQ_OP_TLBI_NSNH_ALL |
0x30 | Non-secure Non-hypervisor 전체 무효화 | - |
CMDQ_OP_CMD_SYNC |
0x46 | 이전 명령 완료 대기 (배리어) | MSI addr (선택) |
CMDQ 배치 제출
/* 배치 명령 제출 — 여러 명령을 모아서 한 번에 전송 */
struct arm_smmu_cmdq_batch cmds;
arm_smmu_cmdq_batch_init(smmu, &cmds, &cmd_ent);
/* TLB 무효화 명령 배치 추가 */
for (i = 0; i < nr_pages; i++) {
cmd_ent.opcode = CMDQ_OP_TLBI_NH_VA;
cmd_ent.tlbi.asid = asid;
cmd_ent.tlbi.addr = iova + i * SZ_4K;
arm_smmu_cmdq_batch_add(smmu, &cmds, &cmd_ent);
}
/* CMD_SYNC를 마지막에 추가 → HW 완료 보장 */
arm_smmu_cmdq_batch_submit(smmu, &cmds);
/* 내부적으로:
* 1. CMDQ 공간 확보 (CONS 따라잡기 대기)
* 2. 엔트리 기록 → PROD 전진 (writel)
* 3. CMD_SYNC 대기 (polling 또는 MSI)
*/
EVENTQ Fault 처리
/* Event Queue 처리 스레드 */
static irqreturn_t arm_smmu_evtq_thread(int irq, void *dev)
{
struct arm_smmu_device *smmu = dev;
struct arm_smmu_queue *q = &smmu->evtq.q;
u64 evt[EVTQ_ENT_DWORDS]; /* 256-bit 이벤트 */
while (!queue_empty(q)) {
queue_read(evt, q, q->llq.cons);
arm_smmu_handle_evt(smmu, evt);
queue_inc_cons(q);
}
return IRQ_HANDLED;
}
/* 이벤트 타입 파싱 */
/* evt[0] bits[7:0] = event ID */
#define EVT_ID_TRANSLATION_FAULT 0x10
#define EVT_ID_ADDR_SIZE_FAULT 0x11
#define EVT_ID_ACCESS_FAULT 0x12
#define EVT_ID_PERMISSION_FAULT 0x13
| Event ID | 이름 | 원인 |
|---|---|---|
| 0x10 | Translation Fault | 페이지 테이블 워크 실패 (매핑 없음) |
| 0x11 | Address Size Fault | 입력 주소가 설정된 주소 폭 초과 |
| 0x12 | Access Fault | AF(Access Flag) 비트 미설정 |
| 0x13 | Permission Fault | 읽기/쓰기 권한 불일치 |
arm_smmu_priq_thread()에서 읽어 iommu_report_device_fault()를 호출합니다.
이는 커널의 IOPF 프레임워크(io-pgfault.c)로 전달되어 handle_mm_fault()를 통해 페이지를 할당한 뒤,
iommu_page_response()로 SMMU에 응답합니다. SVA/PRI가 활성화된 디바이스에서만 동작합니다.
2^19(512K 엔트리)이며,
DT/ACPI에서 큐 크기를 조정할 수 있습니다.
IOTLB 내부 구조와 무효화
IOTLB(I/O Translation Lookaside Buffer)는 IOMMU의 주소 변환 캐시입니다. CPU의 TLB와 동일한 원리로, 최근 사용된 IOVA→PA 변환 결과를 캐시하여 페이지 테이블 워크를 건너뜁니다. 매핑이 변경되면 반드시 무효화(invalidation)해야 하며, 이 비용이 IOMMU 성능의 핵심 요소입니다.
iommu_iotlb_gather — 범위 누적 배치 flush
/* include/linux/iommu.h */
struct iommu_iotlb_gather {
unsigned long start; /* 무효화 시작 IOVA */
unsigned long end; /* 무효화 끝 IOVA */
size_t pgsize; /* 사용된 페이지 크기 */
struct list_head freelist; /* 해제할 페이지 테이블 목록 */
bool queued; /* Flush Queue 사용 여부 */
};
/* unmap 시 gather에 범위 누적 */
iommu_iotlb_gather_add_range(&gather, iova, size);
/* 모든 unmap 후 한 번에 flush */
iommu_iotlb_sync(domain, &gather);
/* → domain->ops->iotlb_sync() 호출 */
/* → Intel: qi_flush_iotlb(), AMD: amd_iommu_flush_tlb_all() */
Intel Queued Invalidation (QI)
/* drivers/iommu/intel/qi.c */
struct qi_desc {
u64 qw0; /* Descriptor Type + 파라미터 */
u64 qw1; /* 추가 파라미터 */
u64 qw2; /* Wait: status_data */
u64 qw3; /* Wait: status_addr */
};
/* IOTLB 무효화 기술자 타입 */
#define QI_IOTLB_TYPE 0x2 /* IOTLB Invalidate */
#define QI_CC_TYPE 0x1 /* Context Cache Invalidate */
#define QI_IEC_TYPE 0x4 /* Interrupt Entry Cache */
#define QI_WAIT_TYPE 0x5 /* Wait Descriptor */
#define QI_PGRP_RESP_TYPE 0x7 /* Page Group Response */
/* 동기식 무효화 흐름 */
int qi_submit_sync(struct intel_iommu *iommu,
struct qi_desc *desc,
unsigned int count,
unsigned long options)
{
/* 1. QI 큐 tail에 desc 기록 */
/* 2. Wait Descriptor 추가 (status_data = QI_DONE) */
/* 3. tail 레지스터 업데이트 → HW 시작 */
/* 4. status_data가 QI_DONE이 될 때까지 spin/sleep */
}
AMD Command Buffer
/* AMD IOMMU Command Buffer — Intel QI와 유사한 링 버퍼 */
/* drivers/iommu/amd/iommu.c */
/* 주요 명령 타입 */
#define CMD_COMPL_WAIT 0x01 /* Completion Wait */
#define CMD_INV_DEV_ENTRY 0x02 /* Device Table Entry */
#define CMD_INV_IOMMU_PAGES 0x03 /* IOTLB Pages */
#define CMD_INV_IOTLB_PAGES 0x04 /* ATS Invalidation */
#define CMD_COMPLETE_PPR 0x07 /* PPR Response */
#define CMD_INV_IRT 0x0a /* Interrupt Remapping */
/* 페이지 무효화 명령 생성 */
static void build_inv_iommu_pages(struct iommu_cmd *cmd,
u64 address, size_t size, u16 domid, bool pde)
{
cmd->data[0] = CMD_INV_IOMMU_PAGES | (domid << 32);
cmd->data[1] = address | (pde ? (1ULL << 1) : 0);
/* pde=true: PDE 캐시도 함께 무효화 */
}
Flush Queue 동작
/* drivers/iommu/iova.c — Flush Queue (FQ) */
struct iova_fq {
struct iova_fq_entry entries[IOVA_FQ_SIZE]; /* 256개 엔트리 */
unsigned int head, tail;
spinlock_t lock;
};
struct iova_fq_entry {
unsigned long iova_pfn;
unsigned long pages;
u64 counter; /* 무효화 시점 카운터 */
};
/* FQ 흐름: */
/* 1. unmap → IOVA를 per-CPU FQ에 추가 (flush 보류) */
/* 2. 10ms 타이머 만료 → fq_flush_timeout() */
/* 3. 보류된 범위 일괄 IOTLB flush */
/* 4. IOVA를 rcache에 반환 (재사용 가능) */
| 아키텍처 | 무효화 메커니즘 | Page 무효화 레이턴시 | 동기 대기 |
|---|---|---|---|
| Intel VT-d | Queued Invalidation (QI) | ~1-5 µs | Wait Descriptor polling |
| AMD-Vi | Command Buffer | ~1-3 µs | Completion Wait |
| ARM SMMUv3 | Command Queue (CMDQ) | ~2-10 µs | CMD_SYNC + polling/MSI |
iommu.strict=1(기본값)에서는 매 unmap마다 즉시 IOTLB flush를 수행하여 보안성이 높지만,
고빈도 DMA 워크로드(10Gbps+ NIC, NVMe)에서 수십 % 성능 저하가 발생합니다.
iommu.strict=0으로 Flush Queue를 활성화하면 배치 무효화로 오버헤드를 크게 줄일 수 있습니다.
단, unmap~flush 사이 최대 10ms 동안 stale 매핑이 남아 있어, 이론적으로 use-after-free DMA 공격 윈도우가 존재합니다.
qi_flush_dev_iotlb(),
ARM은 CMDQ_OP_ATC_INV 명령을 추가로 전송합니다.
크로스 아키텍처 PTE 포맷 비교
IOMMU의 페이지 테이블 엔트리(PTE)는 CPU의 PTE와 유사하지만, 벤더별로 비트 레이아웃이 상이합니다. 이 섹션에서는 Intel VT-d Second Level, AMD I/O PTE, ARM LPAE Stage 2의 비트 필드를 비교합니다.
Intel VT-d PTE 상세
/* drivers/iommu/intel/iommu.h */
/* Second Level PTE — IOMMU 전용 주소 변환 */
#define DMA_PTE_READ (1ULL << 0) /* R: 읽기 허용 */
#define DMA_PTE_WRITE (1ULL << 1) /* W: 쓰기 허용 */
#define DMA_PTE_LARGE_PAGE (1ULL << 7) /* SP: Super Page (2MB/1GB) */
#define DMA_PTE_SNP (1ULL << 11) /* Snoop 비트 (캐시 일관성) */
#define DMA_PTE_TM (1ULL << 62) /* Transient Mapping */
/* First Level PTE — CPU x86 PTE와 동일 포맷 */
/* SVA에서 CPU 페이지 테이블을 IOMMU가 직접 워크 가능 */
#define FLPTE_PRESENT (1ULL << 0)
#define FLPTE_WRITE (1ULL << 1)
#define FLPTE_USER (1ULL << 2)
#define FLPTE_ACCESSED (1ULL << 5)
#define FLPTE_DIRTY (1ULL << 6)
#define FLPTE_NX (1ULL << 63)
AMD I/O PTE 상세
/* drivers/iommu/amd/amd_iommu_types.h */
#define IOMMU_PTE_PR (1ULL << 0) /* Present */
#define IOMMU_PTE_NL_0 (0ULL << 9) /* Next Level = 0 → 물리 페이지 */
#define IOMMU_PTE_NL_1 (1ULL << 9) /* Next Level = 1 → 다음 PT 레벨 */
#define IOMMU_PTE_FC (1ULL << 1) /* Force Coherency */
#define IOMMU_PTE_IR (1ULL << 59) /* I/O Read 허용 */
#define IOMMU_PTE_IW (1ULL << 60) /* I/O Write 허용 */
/* AMD 대형 페이지: NL(Next Level) 필드로 인코딩 */
/* NL=0 at Level 2 → 2MB 페이지 */
/* NL=0 at Level 3 → 1GB 페이지 */
/* Intel의 SP 비트와 달리 레벨별 NL=0으로 표현 */
ARM LPAE Stage 2 PTE 상세
/* arch/arm64/include/asm/pgtable-hwdef.h */
#define PTE_VALID (1ULL << 0)
#define PTE_TYPE_PAGE (3ULL << 0) /* [1:0] = 11 → 4KB 페이지 */
#define PTE_TYPE_BLOCK (1ULL << 0) /* [1:0] = 01 → 블록 (2MB/1GB) */
/* Stage 2 Access Permissions (S2AP) */
#define PTE_S2AP_NONE (0ULL << 6) /* 접근 불가 */
#define PTE_S2AP_READ (1ULL << 6) /* 읽기만 */
#define PTE_S2AP_WRITE (2ULL << 6) /* 쓰기만 */
#define PTE_S2AP_RW (3ULL << 6) /* 읽기+쓰기 */
#define PTE_AF (1ULL << 10) /* Access Flag */
#define PTE_CONT (1ULL << 52) /* Contiguous (TLB 힌트) */
#define PTE_XN (1ULL << 54) /* Execute Never */
/* Contiguous 힌트: 연속 16개 PTE가 같은 속성이면
* TLB가 하나의 엔트리로 캐시 가능 (16 × 4KB = 64KB)
* 또는 16 × 2MB = 32MB 블록 */
권한 인코딩 비교
| 권한 | Intel VT-d (2nd Level) | AMD I/O PTE | ARM LPAE Stage 2 |
|---|---|---|---|
| 읽기 | R[0]=1 |
IR[59]=1 |
S2AP[6]=1 |
| 쓰기 | W[1]=1 |
IW[60]=1 |
S2AP[7]=1 |
| 실행 금지 | First Level만 (NX[63]) |
해당 없음 | XN[54]=1 |
| 대형 페이지 | SP[7]=1 |
NL=0 (암시적) |
Type[1:0]=01 (Block) |
| Present | R[0] | W[1] |
PR[0]=1 |
Valid[0]=1 |
페이지 크기 지원 비교
| 페이지 크기 | Intel VT-d | AMD-Vi | ARM SMMU |
|---|---|---|---|
| 4KB | Level 1 PTE | Level 1 PTE | Level 3 PTE |
| 2MB | Level 2 SP | Level 2 (NL=0) | Level 2 Block |
| 1GB | Level 3 SP | Level 3 (NL=0) | Level 1 Block |
| 64KB | 미지원 | 미지원 | Contiguous 16×4KB |
| 주소 폭 (최대) | 57-bit (5-level) | 57-bit (v3) | 48-bit (4-level) |
CR3가 가리키는 페이지 테이블을 IOMMU가 직접 워크할 수 있습니다.
AMD는 GCR3(Guest CR3) 테이블을 통해 유사 기능을 제공하지만, I/O PTE 포맷이 다르므로 별도의 변환 레이어가 필요합니다.
ARM SMMUv3는 CPU LPAE 포맷과 Stage 1 PTE가 호환되어 SVA를 지원합니다.
SNP[11](Snoop) 비트는 CPU 캐시 일관성(Cache Coherency)을 제어합니다.
SNP=1이면 디바이스 DMA가 CPU 캐시를 자동으로 스누핑하여 일관성을 유지합니다.
대부분의 현대 시스템에서는 기본 활성화되지만, 성능에 민감한 RDMA 워크로드에서는 비활성화를 고려할 수 있습니다.
디버깅
sysfs 인터페이스
# IOMMU 정보 확인
# cat /sys/kernel/iommu_groups/0/type
intel-iommu
# ls /sys/kernel/iommu_groups/0/devices/
0000:00:00.0
# 디바이스별 IOMMU group
# find /sys/kernel/iommu_groups/ -type l | sort -V
/sys/kernel/iommu_groups/0/devices/0000:00:00.0
/sys/kernel/iommu_groups/1/devices/0000:00:02.0
커널 로그
# IOMMU 초기화 로그
# dmesg | grep -i iommu
DMAR: IOMMU enabled
DMAR-IR: Enabled IRQ remapping in x2apic mode
iommu: Default domain type: Translated
# IOMMU fault
DMAR: [DMA Read] Request device [01:00.0] fault addr 0xdeadbeef
DMAR: fault reason 0x06 (PTE Read access is not set)
Tracing
# IOMMU tracepoints
# cd /sys/kernel/debug/tracing
# echo 1 > events/iommu/enable
# 주요 tracepoint
iommu:map # IOVA 매핑
iommu:unmap # IOVA 언매핑
iommu:attach_device_to_domain
iommu:detach_device_from_domain
# cat trace
swapper/0-1 [000] .... 1.234: iommu_map: IOMMU:0000:00:00.0 iova=0x100000 paddr=0x50000000 size=0x1000
성능 고려사항
IOTLB (I/O TLB) 최적화
/* IOTLB 무효화 지연 (Flush Queue) */
#define IOMMU_DOMAIN_DMA_FQ 4 /* Flush Queue 사용 */
/* Strict mode vs Flush Queue */
iommu.strict=0 # Flush Queue 사용 (성능 우선)
iommu.strict=1 # Strict mode (보안 우선, unmap 즉시 IOTLB flush)
Pass-through 모드
/* Pass-through: IOMMU 우회 (1:1 매핑) */
iommu=pt # 기본 디바이스는 pass-through
/* 성능 비교 */
/* Pass-through: DMA latency 최소 */
/* Translation: 보안 강화, VM passthrough 필요 */
IOMMU 모드 비교
| 모드 | 커널 파라미터 | DMA 변환 | 보안 | 성능 | 용도 |
|---|---|---|---|---|---|
| Off | iommu=off |
없음 (DMA = PA) | 없음 | 최고 | 레거시, 신뢰 환경 |
| Pass-through | iommu=pt |
1:1 매핑 | 낮음 | 높음 | 베어메탈 서버 |
| Translated (strict) | iommu.strict=1 |
IOVA 변환 | 최고 | 낮음 | 보안 중시, VM |
| Translated (lazy) | iommu.strict=0 |
IOVA 변환 + FQ | 높음 | 중간 | 일반 운영 (권장) |
iommu=pt를 기본으로 설정하여 IOMMU 오버헤드를 피하고, VFIO passthrough가 필요한 디바이스만 선택적으로 vfio-pci 드라이버에 바인딩하여 DMA remapping을 적용합니다.
KVM 가상화를 사용할 때는 intel_iommu=on iommu=pt 조합이 일반적입니다.
SWIOTLB (소프트웨어 I/O TLB)
SWIOTLB(Software I/O Translation Lookaside Buffer)는 IOMMU가 없거나 디바이스의 DMA 주소 범위가 제한적일 때(예: 32-bit DMA만 지원하는 장치가 4GB 이상 메모리에 접근해야 할 때) 사용하는 바운스 버퍼 메커니즘입니다.
동작 원리
/* SWIOTLB 초기화 (부팅 시 저메모리 영역에 바운스 풀 할당) */
/* 기본 크기: 64MB */
/* 커널 파라미터로 조정: swiotlb=131072 (슬롯 수, 1 슬롯 = 2KB) */
/* DMA 매핑 시 SWIOTLB 사용 경로 */
dma_addr_t swiotlb_map(struct device *dev, phys_addr_t paddr,
size_t size, enum dma_data_direction dir)
{
/* 1. bounce pool에서 슬롯 할당 */
/* 2. DMA_TO_DEVICE이면 orig → bounce memcpy */
/* 3. bounce 물리 주소 반환 (디바이스 DMA 범위 내) */
}
/* DMA 완료 후 sync */
void swiotlb_sync_single_for_cpu(struct device *dev,
dma_addr_t addr, size_t size, enum dma_data_direction dir)
{
/* DMA_FROM_DEVICE이면 bounce → orig memcpy */
}
swiotlb buffer is full 오류가 발생합니다.
커널 파라미터 swiotlb=262144로 풀 크기를 늘리거나, IOMMU를 활성화하여 바운스 버퍼를 우회하세요.
DMA Fault 처리
IOMMU가 디바이스의 DMA 요청을 변환하지 못하거나 권한 검증에 실패하면 DMA fault가 발생합니다. 이는 드라이버 버그, 잘못된 IOMMU 설정, 악의적 디바이스 접근 등에 의해 발생할 수 있습니다.
Fault 유형
| Fault 유형 | 원인 | 대응 |
|---|---|---|
| Translation Fault | IOVA에 대한 매핑이 없음 | DMA 매핑 누락 확인, iommu_map() 호출 검증 |
| Permission Fault | 읽기/쓰기 권한 불일치 | DMA 방향(DMA_TO_DEVICE 등) 확인 |
| External Abort | 물리 주소가 유효하지 않음 | 해제된 메모리에 DMA 시도 여부 확인 |
| ATS Fault | 디바이스 ATC에 캐시된 변환이 무효화됨 | ATS invalidation 타이밍 검증 |
Fault 핸들러
/* IOMMU fault 핸들러 등록 */
int iommu_set_fault_handler(struct iommu_domain *domain,
iommu_fault_handler_t handler,
void *token);
/* Fault 콜백 시그니처 */
typedef int (*iommu_fault_handler_t)(struct iommu_domain *domain,
struct device *dev, unsigned long iova, int flags, void *token);
/* Intel VT-d fault 로그 예시 */
/* DMAR: [DMA Read no-pasid] Request device [01:00.0] */
/* fault addr 0x3fff0000 [fault reason 0x06] */
/* PTE Read access is not set */
/* 일반적인 디버깅 단계 */
/* 1. fault addr → 어떤 IOVA가 문제인지 확인 */
/* 2. Request device → 어떤 PCIe 디바이스가 요청했는지 */
/* 3. fault reason → 매핑 누락 / 권한 오류 / 페이지 크기 불일치 */
/* 4. 드라이버의 dma_map_single/dma_map_page 호출 검증 */
dmesg의 DMAR/AMD-Vi fault 로그를 모니터링하고, IOMMU 그룹 구성이 올바른지 정기적으로 검증하세요.
커널 설정
CONFIG_IOMMU_SUPPORT=y
CONFIG_IOMMU_API=y
# Intel VT-d
CONFIG_INTEL_IOMMU=y
CONFIG_INTEL_IOMMU_DEFAULT_ON=y
CONFIG_INTEL_IOMMU_SCALABLE_MODE_DEFAULT_ON=y
CONFIG_INTEL_IOMMU_SVM=y # SVA 지원
# AMD-Vi
CONFIG_AMD_IOMMU=y
CONFIG_AMD_IOMMU_V2=y
# ARM SMMU
CONFIG_ARM_SMMU=y
CONFIG_ARM_SMMU_V3=y
CONFIG_ARM_SMMU_V3_SVA=y
# VFIO
CONFIG_VFIO=y
CONFIG_VFIO_PCI=y
CONFIG_VFIO_IOMMU_TYPE1=y
# 디버깅
CONFIG_IOMMU_DEBUG=y
CONFIG_IOMMU_DEBUGFS=y
참고자료
커널 공식 문서
- Linux IOMMU Documentation — IOMMU 드라이버 API 공식 문서
- x86 IOMMU 지원 — x86 플랫폼 IOMMU 설정 및 부트 옵션
- IOMMU 유저스페이스 API — IOMMU 사용자 공간 인터페이스
- VFIO 드라이버 API — VFIO 프레임워크 공식 문서
커널 소스 코드
drivers/iommu/iommu.c— IOMMU 코어 프레임워크drivers/iommu/iova.c— I/O Virtual Address 할당기drivers/iommu/dma-iommu.c— DMA-IOMMU 통합 계층drivers/iommu/intel/iommu.c— Intel IOMMU 드라이버drivers/iommu/amd/iommu.c— AMD IOMMU 드라이버drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c— ARM SMMUv3 드라이버drivers/vfio/vfio_iommu_type1.c— VFIO Type1 IOMMUinclude/linux/iommu.h— IOMMU API 정의include/linux/iova.h— IOVA 할당기 인터페이스
규격 문서
- Intel VT-d Specification — Intel 가상화 기술 (VT-d)
- ARM SMMU Architecture Specification — ARM 시스템 메모리 관리 유닛
- AMD IOMMU Specification — AMD I/O 가상화 기술 규격
외부 자료
- IOMMU groups (LWN) — IOMMU 그룹 개념 및 격리
- Scalable IOMMU design for the future (LWN) — 확장 가능한 IOMMU 설계
- Shared Virtual Addressing for IOMMU (LWN) — SVA(공유 가상 주소 지정) 기능
관련 문서
- DMA — DMA 매핑 API, coherent/streaming 매핑, DMA 방향과 sync
- PCI/PCIe — PCIe BAR, BDF 주소 지정, MSI/MSI-X, SR-IOV
- VFIO & mdev (디바이스 패스스루) — IOMMU 그룹 격리, mdev 프레임워크, GPU passthrough
- Scatter/Gather I/O — scatterlist, sg_table, DMA 매핑과 IOMMU 통합
- 디바이스 드라이버 (Device Drivers) — Linux Device Model, platform driver, deferred probe
- DMA Engine (DMA 컨트롤러 프레임워크) — DMA Engine 프레임워크, dma_device/dma_chan 구조체
- MMU & TLB — CPU 측 주소 변환, IOMMU와의 비교
- 가상화 (KVM) — VFIO 디바이스 패스스루, nested virtualization
- Device Tree — DTS의 iommus/iommu-map 속성, SMMU 바인딩