DMA (Direct Memory Access)
Linux DMA 서브시스템을 디바이스-메모리 데이터 경로의 성능과 안전성 관점에서 심층 분석합니다. Coherent/Streaming/SG/Pool 매핑 API의 수명주기, IOMMU(VT-d/AMD-Vi/SMMU)와 SWIOTLB fallback 경로, CMA와 장치 제약 메모리 할당, DMA-BUF 기반 서브시스템 간 버퍼 공유, P2P DMA와 NUMA 배치, 캐시 일관성 및 배리어 규칙, 데이터 손상 방지를 위한 동기화 절차, debugfs/tracepoint로 병목을 추적하는 방법까지 실전 드라이버에서 자주 마주치는 이슈를 폭넓게 다룹니다.
핵심 요약
- DMA — CPU 개입 없이 디바이스가 메모리에 직접 읽기/쓰기하는 메커니즘으로, 시스템 처리량을 크게 향상시킵니다.
- Coherent vs Streaming — Coherent DMA는 CPU-디바이스 간 항상 동기화, Streaming은 명시적 sync가 필요하지만 더 효율적입니다.
- IOMMU — 디바이스의 DMA 주소를 물리 주소로 변환하는 하드웨어(VT-d, AMD-Vi, SMMU)입니다.
- DMA-BUF — GPU, 카메라 등 여러 디바이스 간에 DMA 버퍼를 공유하는 프레임워크입니다.
단계별 이해
- DMA가 필요한 이유 — CPU가 바이트 단위로 데이터를 복사(PIO)하면 CPU 시간을 낭비합니다.
네트워크 카드, 디스크 컨트롤러 등 대용량 전송 디바이스에서 DMA는 핵심 메커니즘입니다.
- DMA 매핑 이해 — 디바이스가 접근할 "DMA 주소"를 할당하는 과정입니다.
dma_alloc_coherent()또는dma_map_single()을 사용합니다.IOMMU가 있으면 DMA 주소 ≠ 물리 주소일 수 있습니다.
- 캐시 일관성 — DMA 전송 시 CPU 캐시와 메모리 내용이 다를 수 있으므로, Streaming 매핑에서는 반드시
dma_sync_*API로 동기화합니다.Coherent 매핑은 하드웨어가 자동 동기화하므로 sync가 불필요합니다.
DMA 개요
DMA(Direct Memory Access)는 CPU 개입 없이 디바이스가 시스템 메모리에 직접 읽기/쓰기를 수행하는 메커니즘입니다. CPU는 전송을 설정한 뒤 다른 작업을 수행할 수 있어 시스템 전체 처리량이 크게 향상됩니다.
PIO vs DMA 비교
| 특성 | PIO (Programmed I/O) | DMA |
|---|---|---|
| 데이터 이동 주체 | CPU (in/out 명령) | DMA 엔진 / 디바이스 버스마스터 |
| CPU 사용률 | 전송 동안 100% 점유 | 설정·완료 인터럽트만 처리 |
| 전송 속도 | CPU 클럭 의존, 느림 | 버스 대역폭 활용, 빠름 |
| 캐시 일관성 | 자동 보장 (CPU 접근) | 명시적 sync 필요 (아키텍처 의존) |
| 주요 사용처 | 소형 레거시 디바이스 | NIC, 스토리지, GPU, 사운드카드 |
DMA 발전사
| 세대 | 메커니즘 | 특징 |
|---|---|---|
| ISA DMA | 8237A DMA 컨트롤러 | 24비트 주소(16MB), 4/8채널, CPU가 컨트롤러 프로그래밍 |
| PCI 버스마스터 | 디바이스가 버스 마스터 | 32/64비트 주소, 디바이스가 직접 메모리 접근 |
| PCIe + IOMMU | TLP 기반 메모리 Read/Write | 가상 주소 변환, 디바이스 격리, ATS/PRI/PASID |
커널 소스 트리 구조
| 경로 | 역할 |
|---|---|
kernel/dma/ | DMA 매핑 코어: mapping.c, direct.c, swiotlb.c, pool.c, contiguous.c |
include/linux/dma-mapping.h | DMA 매핑 API 헤더 |
include/linux/dma-map-ops.h | dma_map_ops 백엔드 인터페이스 |
drivers/iommu/ | IOMMU 프레임워크: intel/, amd/, arm-smmu-v3/ |
drivers/dma-buf/ | DMA-BUF 버퍼 공유 프레임워크 |
drivers/dma/ | DMA 엔진(slave DMA 컨트롤러) 드라이버 |
DMA 주소 공간과 매핑
DMA 프로그래밍에서 가장 혼란스러운 부분은 여러 주소 공간이 동시에 존재한다는 점입니다.
주소 유형
| 주소 유형 | C 타입 | 의미 | 변환 관계 |
|---|---|---|---|
| 가상 주소 | void * | CPU MMU를 통한 커널 가상 주소 | virt_to_phys() → 물리 |
| 물리 주소 | phys_addr_t | CPU가 보는 실제 RAM 주소 | 항상 고정 |
| 버스(DMA) 주소 | dma_addr_t | 디바이스가 보는 주소 | IOMMU 없으면 = 물리, 있으면 IOVA |
virt_to_bus()/bus_to_virt()는 폐기된 API입니다. 반드시 dma_map_*() API를 사용하십시오. 이 API만이 IOMMU, SWIOTLB, CMA 등 모든 백엔드를 올바르게 처리합니다.DMA 존(Zone)과 주소 마스크
| Zone | x86_64 범위 | 용도 |
|---|---|---|
ZONE_DMA | 0 ~ 16MB | ISA DMA 레거시 디바이스 (24비트) |
ZONE_DMA32 | 0 ~ 4GB | 32비트 DMA 디바이스 |
ZONE_NORMAL | 4GB 이상 | 64비트 DMA 디바이스 |
/* DMA 마스크 설정 — 디바이스의 주소 지정 능력을 커널에 알림 */
int ret;
/* 64비트 DMA 가능한 디바이스 */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
/* 64비트 실패 시 32비트로 폴백 */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(dev, "No suitable DMA available\\n");
return ret;
}
}
/* dma_set_mask_and_coherent()는 다음 두 호출의 조합: */
/* dma_set_mask(dev, mask) — streaming 매핑용 마스크 */
/* dma_set_coherent_mask(dev, mask) — coherent 할당용 마스크 */
DMA 매핑 백엔드 (dma_map_ops)
dma_map_single() 등 DMA 매핑 API는 내부적으로 struct dma_map_ops 백엔드를 통해 실제 매핑을 수행합니다. 시스템 구성에 따라 적절한 백엔드가 자동 선택됩니다.
| 백엔드 | 선택 조건 | 주소 변환 | fallback |
|---|---|---|---|
iommu_dma_ops | IOMMU 활성 + DMA 도메인 | IOVA → PA (IOMMU PT) | 없음 (IOMMU가 처리) |
dma_direct (NULL ops) | IOMMU 없음 또는 passthrough | DMA addr = PA | SWIOTLB (마스크 초과 시) |
swiotlb_dma_ops | 기밀 컴퓨팅 강제 | 공유 바운스 버퍼 | — |
xen_swiotlb_dma_ops | Xen 게스트 | Xen 그랜트 테이블 | — |
Device Tree의 dma-ranges
ARM/RISC-V 등 플랫폼에서 버스 주소와 물리 주소가 다를 수 있으며, dma-ranges 속성으로 변환을 정의합니다:
/* DTS 예시: 버스 주소 0x0이 물리 주소 0x80000000에 매핑 */
soc {
#address-cells = <1>;
#size-cells = <1>;
dma-ranges = <0x0 0x80000000 0x40000000>;
/* child_bus_addr parent_phys_addr length */
};
DMA 매핑 API 상세
Linux 커널은 DMA 매핑을 위해 5가지 주요 API를 제공합니다. 모든 API는 <linux/dma-mapping.h>에 선언되어 있습니다.
API 유형 비교
| API | 수명 | 캐시 일관성 | 사용 시나리오 |
|---|---|---|---|
| Coherent | 장기 (드라이버 수명) | HW 자동 보장 | 디스크립터 링, 명령 큐 |
| Streaming (single) | 단기 (I/O 단위) | 명시적 sync | 단일 버퍼 전송 |
| Streaming (SG) | 단기 (I/O 단위) | 명시적 sync | Scatter-Gather I/O |
| Pool | 장기 (반복 할당) | HW 자동 보장 | 고정 크기 소형 버퍼 풀 |
| Non-coherent | 장기 | 명시적 sync | 대용량 버퍼 (캐시 제어 필요) |
Coherent (Consistent) DMA
CPU와 디바이스가 동시에 접근하는 공유 메모리에 사용합니다. 하드웨어가 캐시 일관성을 보장합니다.
#include <linux/dma-mapping.h>
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096;
/* 할당: CPU 가상주소 + DMA 버스주소를 동시에 얻음 */
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
return -ENOMEM;
/* cpu_addr로 CPU 접근, dma_handle로 디바이스에 주소 전달 */
desc->buf_addr = cpu_to_le64(dma_handle);
/* 해제 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
Streaming DMA — 단일 버퍼
한 번의 I/O 전송 동안만 유효한 매핑입니다. 전송 후 반드시 unmap해야 합니다.
dma_addr_t dma_handle;
void *buf = kmalloc(4096, GFP_KERNEL);
/* 매핑: 방향(Direction)은 데이터 흐름을 나타냄 */
dma_handle = dma_map_single(dev, buf, 4096, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
dev_err(dev, "DMA mapping failed\\n");
kfree(buf);
return -EIO;
}
/* 디바이스에 dma_handle 전달, 전송 시작 */
start_device_transfer(dma_handle, 4096);
/* 전송 완료 후 unmap */
dma_unmap_single(dev, dma_handle, 4096, DMA_TO_DEVICE);
dma_map_single() 후에는 반드시 dma_mapping_error()로 오류를 확인하십시오. IOMMU가 활성화된 시스템에서는 IOVA 공간 고갈로 매핑이 실패할 수 있습니다.Streaming DMA — Scatter-Gather
물리적으로 비연속인 여러 페이지를 하나의 DMA 전송으로 묶는 기법입니다. NIC의 패킷 fragment, 블록 계층의 BIO segment, Crypto Framework (Crypto API)의 입출력 버퍼, GPU 커맨드 버퍼 등 거의 모든 DMA 드라이버에서 핵심 패턴으로 사용됩니다. 커널은 struct scatterlist와 struct sg_table을 통해 이러한 비연속 메모리 영역을 표현하고, IOMMU가 있는 경우 여러 SG 엔트리를 하나의 연속 IOVA 영역으로 병합하여 DMA 효율을 극대화합니다.
struct scatterlist / struct sg_table
Scatter-gather의 기본 데이터 구조는 struct scatterlist와 이를 감싸는 struct sg_table입니다.
struct scatterlist {
unsigned long page_link; /* 페이지 포인터 + bit0:chain + bit1:last */
unsigned int offset; /* 페이지 내 시작 오프셋 */
unsigned int length; /* 데이터 길이 (바이트) */
dma_addr_t dma_address; /* DMA 매핑 후 디바이스 주소 */
unsigned int dma_length; /* DMA 매핑 후 길이 (병합 시 변경) */
};
struct sg_table {
struct scatterlist *sgl; /* 첫 번째 scatterlist 포인터 */
unsigned int nents; /* DMA 매핑 후 엔트리 수 */
unsigned int orig_nents; /* 원본(매핑 전) 엔트리 수 */
};
| 필드 | 설명 | 비고 |
|---|---|---|
page_link | 페이지 구조체 포인터 + 제어 비트 | bit0: chain 마커, bit1: last 마커 |
offset | 페이지 내 데이터 시작 위치 | 0 ~ PAGE_SIZE-1 |
length | 이 엔트리가 가리키는 데이터 길이 | CPU 관점의 원본 길이 |
dma_address | DMA 매핑 후 디바이스가 사용할 주소 | dma_map_sg() 후 유효 |
dma_length | DMA 매핑 후 길이 | IOMMU 병합 시 length와 다를 수 있음 |
page_link의 하위 2비트는 체이닝과 종단 마커로 사용됩니다. 절대 sg->page_link를 직접 접근하지 마십시오. 반드시 sg_page(), sg_next(), sg_is_chain(), sg_is_last() 접근자를 사용하십시오.SG 체이닝 메커니즘
단일 scatterlist 배열의 크기는 스택이나 슬랩 할당의 제약을 받습니다. 대용량 I/O(수백~수천 세그먼트)를 위해 커널은 여러 scatterlist 배열을 연결하는 체이닝 메커니즘을 제공합니다. 배열의 마지막 엔트리에서 page_link의 bit0(chain)을 설정하고, 페이지 포인터 대신 다음 배열의 주소를 저장합니다.
struct scatterlist sg1[4], sg2[4];
/* sg1, sg2 각각 초기화 */
sg_init_table(sg1, 4);
sg_init_table(sg2, 4);
/* sg1[3]을 chain 엔트리로 설정하여 sg2를 연결 */
sg_chain(sg1, 4, sg2);
/* 이제 sg1부터 sg_next()로 sg2까지 연속 순회 가능 */
struct scatterlist *s;
int i;
for_each_sg(sg1, s, 7, i) {
/* sg1[0..2] → sg2[0..3] 순서로 7개 순회 */
pr_info("sg[%d]: page=%p offset=%u len=%u\\n",
i, sg_page(s), s->offset, s->length);
}
sg_alloc_table()은 내부적으로 SG_MAX_SINGLE_ALLOC(보통 128) 단위로 배열을 할당하고 자동 체이닝합니다. 드라이버가 직접 sg_chain()을 호출할 일은 드뭅니다.SG 테이블 API
Raw scatterlist[] 배열 대신 struct sg_table을 사용하면 할당, 해제, 체이닝을 자동으로 처리합니다.
| 항목 | raw scatterlist[] | sg_table |
|---|---|---|
| 할당 | 수동 kmalloc/스택 | sg_alloc_table() 자동 관리 |
| 체이닝 | 수동 sg_chain() | 자동 (SG_MAX_SINGLE_ALLOC 초과 시) |
| 엔트리 수 추적 | 별도 변수 필요 | nents/orig_nents 내장 |
| 해제 | 수동 kfree | sg_free_table() |
| 최신 API 호환 | dma_map_sg() | dma_map_sgtable() |
struct sg_table sgt;
int ret;
/* 방법 1: 빈 SG 테이블 할당 후 수동 설정 */
ret = sg_alloc_table(&sgt, nfrags, GFP_KERNEL);
if (ret)
return ret;
/* 방법 2: 페이지 배열로부터 직접 SG 테이블 생성 */
ret = sg_alloc_table_from_pages(&sgt, pages, n_pages,
offset, size, GFP_KERNEL);
/* 해제 */
sg_free_table(&sgt);
sg_alloc_table_from_pages()는 물리적으로 연속인 페이지들을 자동으로 하나의 SG 엔트리로 병합합니다. 예를 들어 16개 연속 페이지는 16개가 아닌 1개의 SG 엔트리로 표현되어 DMA 매핑 오버헤드를 줄입니다.기본 SG 매핑 워크플로
SG 매핑의 기본 흐름은 5단계로 구성됩니다: (1) SG 테이블 초기화 → (2) 각 엔트리에 페이지 설정 → (3) dma_map_sg()로 DMA 매핑 → (4) for_each_sg()로 매핑 결과 순회 → (5) dma_unmap_sg()로 해제.
struct scatterlist sg[MAX_FRAGS];
int nents, mapped_nents;
sg_init_table(sg, nfrags);
for (i = 0; i < nfrags; i++)
sg_set_page(&sg[i], pages[i], len[i], offset[i]);
/* 매핑: IOMMU가 있으면 여러 SG를 하나의 연속 IOVA로 병합 가능 */
mapped_nents = dma_map_sg(dev, sg, nfrags, DMA_FROM_DEVICE);
if (!mapped_nents) {
dev_err(dev, "SG DMA mapping failed\\n");
return -EIO;
}
/* mapped_nents ≤ nfrags (IOMMU 병합 시 줄어듦) */
for_each_sg(sg, s, mapped_nents, i) {
hw_desc[i].addr = sg_dma_address(s);
hw_desc[i].len = sg_dma_len(s);
}
/* 전송 완료 후 */
dma_unmap_sg(dev, sg, nfrags, DMA_FROM_DEVICE);
dma_unmap_sg()의 세 번째 인자는 매핑 전 원본 엔트리 수(nfrags)를 전달해야 합니다. dma_map_sg()가 반환한 mapped_nents가 아닙니다. IOMMU 병합으로 반환값이 줄어들더라도 내부적으로는 원본 수만큼의 매핑을 해제해야 합니다.dma_map_sgtable() (최신 API)
커널 5.8 이후 도입된 dma_map_sgtable()은 sg_table을 직접 받아 nents/orig_nents를 자동 관리하고, 오류 시 음수 errno를 반환하여 기존 API의 불편함을 해소합니다.
| Legacy API | Modern API | 차이점 |
|---|---|---|
dma_map_sg() | dma_map_sgtable() | 반환값: 0=실패 vs 음수 errno |
dma_unmap_sg() | dma_unmap_sgtable() | nents 인자 불필요 |
for_each_sg() | for_each_sgtable_dma_sg() | nents 자동 사용 |
| 별도 nents 변수 관리 | sg_table 내 자동 추적 | nents/orig_nents 혼동 방지 |
struct sg_table sgt;
int ret;
/* 페이지 배열로부터 SG 테이블 생성 */
ret = sg_alloc_table_from_pages(&sgt, pages, n_pages,
offset, size, GFP_KERNEL);
if (ret)
return ret;
/* 최신 API로 DMA 매핑 — 음수 errno 반환 */
ret = dma_map_sgtable(dev, &sgt, DMA_BIDIRECTIONAL, 0);
if (ret) {
dev_err(dev, "sgtable DMA map failed: %d\\n", ret);
sg_free_table(&sgt);
return ret;
}
/* DMA 매핑된 엔트리 순회 (sgt.nents 자동 사용) */
struct scatterlist *s;
int i;
for_each_sgtable_dma_sg(&sgt, s, i) {
hw_desc[i].addr = sg_dma_address(s);
hw_desc[i].len = sg_dma_len(s);
}
/* 전송 완료 후 해제 — nents 인자 불필요 */
dma_unmap_sgtable(dev, &sgt, DMA_BIDIRECTIONAL, 0);
sg_free_table(&sgt);
dma_map_sgtable()/dma_unmap_sgtable()을 사용하십시오. nents 관리 오류를 원천 차단하고, 표준 errno 처리 패턴을 따릅니다.SG 순회 매크로
SG 엔트리를 순회할 때는 CPU 관점(원본 엔트리)과 DMA 관점(매핑 후 엔트리)을 구분해야 합니다. IOMMU 병합으로 인해 두 관점의 엔트리 수가 다를 수 있습니다.
| 매크로 | 관점 | 순회 수 | 용도 |
|---|---|---|---|
for_each_sg(sglist, sg, nr, i) | 수동 지정 | nr | 범용 (raw scatterlist) |
for_each_sgtable_sg(&sgt, sg, i) | CPU | orig_nents | 매핑 전/후 CPU 접근 |
for_each_sgtable_dma_sg(&sgt, sg, i) | DMA | nents | HW 디스크립터 설정 |
for_each_sgtable_page(&sgt, piter, i) | CPU | 페이지 단위 | 페이지 단위 순회 |
/* CPU 관점: 원본 페이지에 대해 sync/접근 */
for_each_sgtable_sg(&sgt, s, i) {
struct page *page = sg_page(s);
void *vaddr = kmap_local_page(page);
memset(vaddr + s->offset, 0, s->length);
kunmap_local(vaddr);
}
/* DMA 관점: HW 디스크립터에 DMA 주소 설정 */
for_each_sgtable_dma_sg(&sgt, s, i) {
hw_desc[i].addr = sg_dma_address(s);
hw_desc[i].len = sg_dma_len(s);
}
for_each_sgtable_sg()(CPU 관점)를 사용하면 안 됩니다. IOMMU가 병합한 경우 orig_nents와 nents가 달라 디스크립터 수가 맞지 않게 됩니다. 반드시 for_each_sgtable_dma_sg()(DMA 관점)를 사용하십시오.IOMMU SG 병합
IOMMU가 활성화된 시스템에서 dma_map_sg()는 물리적으로 비연속인 여러 페이지를 하나의 연속 IOVA(I/O Virtual Address) 영역에 매핑할 수 있습니다. 이를 SG 병합(merging)이라 하며, 디바이스에게는 하나의 큰 연속 버퍼로 보이게 합니다.
병합이 일어나면 dma_map_sg()의 반환값(mapped_nents)이 원본 엔트리 수(nfrags)보다 작아집니다. IOMMU가 없는 시스템에서는 병합이 불가능하므로 mapped_nents == nfrags가 됩니다.
/* 4개의 비연속 페이지를 SG로 매핑 */
mapped = dma_map_sg(dev, sg, 4, DMA_TO_DEVICE);
/* IOMMU 있는 시스템: mapped = 1 (4페이지 → 1개 연속 IOVA) */
/* IOMMU 없는 시스템: mapped = 4 (각 페이지 독립) */
for_each_sg(sg, s, mapped, i) {
pr_info("DMA seg[%d]: addr=0x%llx len=%u\\n",
i, sg_dma_address(s), sg_dma_len(s));
}
/* IOMMU: DMA seg[0]: addr=0xf0000 len=16384 */
/* 직접: DMA seg[0..3]: 각각 4096 */
오류 처리 패턴
SG DMA 매핑은 IOVA 공간 고갈, 메모리 부족 등으로 실패할 수 있습니다. Legacy API와 Modern API의 오류 반환 방식이 다르므로 주의해야 합니다.
struct sg_table sgt;
int ret;
/* 1. SG 테이블 할당 */
ret = sg_alloc_table_from_pages(&sgt, pages, n_pages,
0, total_size, GFP_KERNEL);
if (ret)
return ret;
/* 2. DMA 매핑 (최신 API) */
ret = dma_map_sgtable(dev, &sgt, DMA_FROM_DEVICE, 0);
if (ret)
goto err_free_sgt;
/* 3. HW 디스크립터 설정 및 전송 */
setup_hw_descriptors(&sgt);
ret = start_dma_transfer(dev);
if (ret)
goto err_unmap;
/* 4. 전송 완료 대기 후 정리 */
wait_for_completion(&done);
err_unmap:
dma_unmap_sgtable(dev, &sgt, DMA_FROM_DEVICE, 0);
err_free_sgt:
sg_free_table(&sgt);
return ret;
sg_free_table()은 반드시 호출해야 합니다. DMA 매핑 실패 시 dma_unmap_*은 호출하지 않습니다.성능 고려사항
SG DMA의 성능은 세그먼트 수, HW 제한, IOMMU 설정에 크게 영향받습니다.
| 파라미터 | 의미 | 확인 방법 |
|---|---|---|
max_segments | 디바이스가 처리 가능한 최대 SG 수 | dma_get_max_seg_count(dev) |
max_segment_size | 단일 SG 엔트리의 최대 크기 | dma_get_max_seg_size(dev) |
dma_max_mapping_size() | 단일 매핑의 최대 총 크기 | SWIOTLB 제한 포함 |
SG_MAX_SINGLE_ALLOC | 체이닝 없이 할당 가능한 SG 수 | 보통 128 (PAGE_SIZE / sizeof(sg)) |
- SG 수 최소화:
sg_alloc_table_from_pages()로 연속 페이지를 자동 병합하여 SG 엔트리를 줄이십시오. - HW 제한 확인:
max_segments/max_segment_size를 초과하면 매핑이 실패합니다. 블록 계층은 이를 자동 분할하지만 직접 호출 시 직접 확인해야 합니다. - IOTLB 압박 방지: 과도한 SG 매핑은 IOMMU의 IOTLB(TLB) 미스를 증가시킵니다. 가능하면 매핑을 재사용하십시오.
- 매핑 재사용: 반복 전송 시
dma_sync_sg_for_cpu()/dma_sync_sg_for_device()로 기존 매핑을 재사용하면 map/unmap 오버헤드를 피할 수 있습니다. - Atomic 컨텍스트 주의:
dma_map_sg()는 IOMMU 페이지 테이블 잠금을 획득합니다. 대량 SG 매핑은 인터럽트 컨텍스트에서 지연을 유발할 수 있습니다.
DMA Pool
고정 크기 소형 coherent 버퍼를 효율적으로 반복 할당/해제할 때 사용합니다. 내부적으로 dma_alloc_coherent()에서 큰 블록을 할당 후 분할합니다.
struct dma_pool *pool;
dma_addr_t dma_handle;
void *buf;
/* 풀 생성: 256바이트 블록, 16바이트 정렬, 4KB 경계 내 */
pool = dma_pool_create("my_desc", dev, 256, 16, 4096);
/* 풀에서 할당 */
buf = dma_pool_alloc(pool, GFP_KERNEL, &dma_handle);
/* 반환 */
dma_pool_free(pool, buf, dma_handle);
/* 풀 해제 */
dma_pool_destroy(pool);
Non-coherent DMA
대용량 버퍼에서 캐시를 명시적으로 제어하여 성능을 최적화할 때 사용합니다:
/* 비캐시-일관 할당: 아키텍처에 따라 uncached 또는 write-combine */
cpu_addr = dma_alloc_noncoherent(dev, size, &dma_handle,
DMA_FROM_DEVICE, GFP_KERNEL);
/* CPU가 읽기 전에 캐시 무효화 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 디바이스에 다시 넘기기 전에 캐시 플러시 */
dma_sync_single_for_device(dev, dma_handle, size, DMA_FROM_DEVICE);
dma_free_noncoherent(dev, size, cpu_addr, dma_handle, DMA_FROM_DEVICE);
DMA 속성과 특수 매핑
DMA 매핑 API는 attrs 파라미터를 통해 매핑 동작을 세밀하게 제어할 수 있습니다. <linux/dma-mapping.h>에 정의된 DMA_ATTR_* 플래그를 비트 OR로 결합하여 전달합니다.
DMA_ATTR_* 플래그 종합
| 플래그 | 효과 | 주요 사용처 |
|---|---|---|
DMA_ATTR_WRITE_COMBINE | Write-Combine 매핑 (캐시 비활성, WC 버퍼 사용) | 프레임버퍼, GPU 커맨드 버퍼 |
DMA_ATTR_NO_KERNEL_MAPPING | CPU 커널 매핑 생략 (DMA 주소만 할당) | 대용량 비디오/카메라 버퍼 |
DMA_ATTR_SKIP_CPU_SYNC | map/unmap 시 캐시 sync 건너뜀 | 드라이버가 직접 sync 관리 |
DMA_ATTR_FORCE_CONTIGUOUS | 물리 연속 메모리 강제 | IOMMU 없는 디바이스 |
DMA_ATTR_ALLOC_SINGLE_PAGES | compound page 대신 개별 페이지 할당 | DMA-BUF heap, 메모리 효율 |
DMA_ATTR_NO_WARN | 할당 실패 시 경고 메시지 억제 | 선택적 대용량 할당 시도 |
DMA_ATTR_PRIVILEGED | IOMMU에서 특권 모드 접근 표시 | ARM SMMU의 privileged 트랜잭션 |
Write-Combine 매핑
Write-Combine(WC)은 CPU 캐시를 사용하지 않으면서도 쓰기 버퍼를 결합하여 순차 쓰기 성능을 극대화하는 메모리 타입입니다. 프레임버퍼, GPU 커맨드 버퍼, MMIO 레지스터 스트리밍 등에 적합합니다.
| 매핑 유형 | 캐시 | 순차 쓰기 | 랜덤 읽기 | CPU sync |
|---|---|---|---|---|
| Coherent (cached) | HW snoop | 보통 | 빠름 | 불필요 |
| Write-Combine | 없음 (WC 버퍼) | 매우 빠름 | 매우 느림 | wmb() 필요 |
| Uncached | 없음 | 느림 | 느림 | 불필요 |
/* Write-Combine 할당 — 전용 API */
void *wc_buf;
dma_addr_t dma_handle;
wc_buf = dma_alloc_wc(dev, size, &dma_handle, GFP_KERNEL);
if (!wc_buf)
return -ENOMEM;
/* 순차 쓰기에 최적 — GPU 커맨드 스트리밍 */
memcpy_toio(wc_buf, cmd_data, cmd_len);
wmb(); /* WC 버퍼 flush 보장 */
dma_free_wc(dev, size, wc_buf, dma_handle);
/* 또는 속성 기반 할당 */
cpu_addr = dma_alloc_attrs(dev, size, &dma_handle,
GFP_KERNEL, DMA_ATTR_WRITE_COMBINE);
- WC 영역의 읽기는 극도로 느립니다 (캐시 미스마다 전체 라인 fetch). 읽기가 필요한 데이터에는 사용하지 마십시오.
- WC 쓰기는 순서가 보장되지 않습니다. 순서가 중요하면
wmb()또는dma_wmb()를 삽입하십시오. - x86에서는
sfence가 WC 버퍼를 flush합니다. ARM에서는DSB ST가 이에 해당합니다.
NO_KERNEL_MAPPING — 커널 주소 공간 절약
비디오 프레임이나 카메라 캡처 버퍼처럼 CPU가 직접 접근할 필요 없는 대용량 DMA 버퍼에서는 커널 가상 주소 매핑을 생략하여 vmalloc 공간을 절약할 수 있습니다.
/* 커널 VA 매핑 없이 DMA 전용 버퍼 할당 */
struct page *page;
dma_addr_t dma_handle;
page = dma_alloc_pages(dev, size, &dma_handle,
DMA_FROM_DEVICE,
GFP_KERNEL | __GFP_ZERO);
if (!page)
return -ENOMEM;
/* CPU가 잠시 접근해야 할 때만 kmap */
void *vaddr = kmap_local_page(page);
process_header(vaddr, header_len);
kunmap_local(vaddr);
/* 유저스페이스에 mmap으로 노출 가능 */
dma_mmap_pages(dev, vma, size, page);
dma_free_pages(dev, size, page, dma_handle, DMA_FROM_DEVICE);
SKIP_CPU_SYNC — 수동 캐시 제어
대량의 SG 엔트리를 매핑할 때 기본 sync 동작을 건너뛰고, 실제 데이터 접근 시점에만 선택적으로 sync하여 오버헤드를 줄입니다.
/* map 시 sync 건너뜀 — 나중에 직접 sync */
dma_handle = dma_map_single_attrs(dev, buf, len,
DMA_FROM_DEVICE,
DMA_ATTR_SKIP_CPU_SYNC);
/* 디바이스 전송 완료 후, 필요한 부분만 sync */
dma_sync_single_range_for_cpu(dev, dma_handle,
offset, partial_len,
DMA_FROM_DEVICE);
/* offset ~ offset+partial_len 구간만 CPU가 읽음 */
process_data(buf + offset, partial_len);
Managed (Devres) DMA API
디바이스 해제 시 자동으로 정리되는 관리형 DMA API입니다. probe()에서 할당하고 별도 remove() 정리 코드가 필요 없어 리소스 누수를 방지합니다.
| 일반 API | Managed API | 자동 정리 |
|---|---|---|
dma_alloc_coherent() | dmam_alloc_coherent() | dev 해제 시 자동 free |
dma_pool_create() | dmam_pool_create() | dev 해제 시 자동 destroy |
dma_alloc_pages() | dmam_alloc_pages() | dev 해제 시 자동 free |
static int my_probe(struct pci_dev *pdev, ...)
{
struct device *dev = &pdev->dev;
/* Managed 할당 — remove()에서 free 불필요 */
priv->ring = dmam_alloc_coherent(dev, RING_SIZE,
&priv->ring_dma,
GFP_KERNEL);
if (!priv->ring)
return -ENOMEM;
priv->pool = dmam_pool_create("my_pool", dev,
256, 16, 0);
if (!priv->pool)
return -ENOMEM;
/* priv->ring과 priv->pool은 pci_dev 해제 시 자동 정리 */
return 0;
}
pcim_enable_device() + dmam_alloc_coherent() 조합으로 모든 리소스를 managed로 관리하십시오. remove() 함수가 빈 함수가 되어 정리 누락 버그를 원천 방지합니다.dma_map_resource() — MMIO 주소 매핑
시스템 RAM이 아닌 디바이스 MMIO 영역(BAR)을 다른 디바이스가 DMA로 접근할 수 있도록 매핑합니다. P2P DMA의 기초입니다.
/* NIC가 GPU BAR 영역에 직접 DMA하기 위한 매핑 */
dma_addr_t dma_handle;
phys_addr_t gpu_bar_phys = pci_resource_start(gpu_pdev, 0);
size_t gpu_bar_len = pci_resource_len(gpu_pdev, 0);
dma_handle = dma_map_resource(nic_dev, gpu_bar_phys,
gpu_bar_len, DMA_BIDIRECTIONAL, 0);
if (dma_mapping_error(nic_dev, dma_handle))
return -EIO;
/* NIC 디스크립터에 gpu_bar의 DMA 주소 설정 → P2P 전송 */
dma_unmap_resource(nic_dev, dma_handle, gpu_bar_len,
DMA_BIDIRECTIONAL, 0);
dma_map_resource()는 캐시 유지보수를 수행하지 않습니다 (MMIO는 캐시 불가). RAM이 아닌 주소에만 사용하고, dma_sync_*를 호출하면 안 됩니다.DMA 방향과 캐시 일관성
DMA 방향 상수
| 상수 | 데이터 흐름 | map 시 캐시 동작 | unmap 시 캐시 동작 |
|---|---|---|---|
DMA_TO_DEVICE | CPU → 디바이스 | 캐시 → 메모리 flush | (없음) |
DMA_FROM_DEVICE | 디바이스 → CPU | 캐시 라인 invalidate | 캐시 라인 invalidate |
DMA_BIDIRECTIONAL | 양방향 | flush + invalidate | invalidate |
DMA_NONE | 디버깅 전용 | — | — |
DMA_BIDIRECTIONAL은 flush + invalidate를 모두 수행하므로 오버헤드가 큽니다. 가능하면 정확한 방향을 지정하십시오.아키텍처별 캐시 일관성 모델
| 아키텍처 | DMA 캐시 일관성 | 설명 |
|---|---|---|
| x86/x86_64 | HW coherent | 스누프 프로토콜이 캐시 일관성을 자동 보장. sync API는 no-op (컴파일러 배리어만) |
| ARM (non-coherent) | SW managed | flush/invalidate 필수. dma_sync_* 호출 시 실제 캐시 유지보수 명령 실행 |
| ARM (coherent) | HW coherent | CCI/CHI 인터커넥트가 IO-coherent 포트 제공 시 |
| RISC-V | 플랫폼 의존 | SiFive U74는 non-coherent, 향후 IOPMP/IOMMU로 개선 |
Sync API 상세
Streaming 매핑을 unmap하지 않고 CPU ↔ 디바이스 간 소유권을 전환할 때 사용합니다:
/* 수신 링 버퍼 — 반복 사용 패턴 */
dma_addr = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
while (running) {
/* 디바이스가 데이터 기록 완료 (인터럽트 등) */
dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE);
/* CPU가 buf의 데이터를 읽음 */
process_data(buf, len);
/* 다시 디바이스에 소유권 넘김 */
dma_sync_single_for_device(dev, dma_addr, len, DMA_FROM_DEVICE);
/* 디바이스에 버퍼 재사용 통보 */
notify_device_reuse(dma_addr);
}
dma_unmap_single(dev, dma_addr, len, DMA_FROM_DEVICE);
DMA 메모리 배리어와 순서 보장
DMA 프로그래밍에서 메모리 배리어는 CPU 쓰기가 디바이스에 올바른 순서로 보이게 하고, 디바이스 쓰기가 CPU에 올바르게 보이게 하는 핵심 메커니즘입니다. 배리어 누락은 극히 재현 어려운 데이터 손상 버그를 유발합니다.
DMA 관련 배리어 유형
| 배리어 | C API | 용도 | x86 | ARM64 |
|---|---|---|---|---|
| DMA 읽기 배리어 | dma_rmb() | 디바이스 쓰기 → CPU 읽기 순서 | 컴파일러 배리어 | dmb(oshld) |
| DMA 쓰기 배리어 | dma_wmb() | CPU 쓰기 → 디바이스 읽기 순서 | 컴파일러 배리어 | dmb(oshst) |
| DMA 전체 배리어 | dma_mb() | 양방향 순서 보장 | 컴파일러 배리어 | dmb(osh) |
| MMIO 쓰기 배리어 | wmb() | 메모리/WC → MMIO 쓰기 순서 | sfence | dsb(st) |
| MMIO 읽기 배리어 | rmb() | MMIO 읽기 순서 | lfence | dsb(ld) |
dma_rmb()/dma_wmb()는 컴파일러 배리어만으로 충분합니다. 그러나 ARM, RISC-V 등 약한 메모리 모델에서는 실제 하드웨어 명령이 발행됩니다. 이식성을 위해 항상 적절한 배리어를 사용하십시오.디스크립터 링 배리어 패턴
NIC, NVMe 등 디스크립터 기반 DMA에서 가장 흔한 배리어 사용 패턴입니다:
/* ── 송신 경로: CPU가 디스크립터 작성 → 디바이스가 읽기 ── */
/* 1. 디스크립터 데이터 필드 작성 */
desc->buf_addr = cpu_to_le64(dma_handle);
desc->buf_len = cpu_to_le32(len);
desc->flags = cpu_to_le32(TX_FLAGS);
/* 2. DMA 쓰기 배리어: 위의 쓰기가 아래보다 먼저 보이게 */
dma_wmb();
/* 3. ownership 비트 설정 (디바이스에 소유권 이전) */
desc->status = cpu_to_le32(DESC_OWN);
/* 4. MMIO doorbell: 디바이스에 새 디스크립터 통보 */
wmb(); /* DMA 메모리 쓰기가 MMIO 전에 완료되도록 */
writel(tail_idx, priv->doorbell_reg);
/* ── 수신 경로: 디바이스가 디스크립터 작성 → CPU가 읽기 ── */
/* 1. ownership 비트 확인 (디바이스가 완료했는지) */
if (!(le32_to_cpu(desc->status) & DESC_DONE))
return; /* 아직 미완료 */
/* 2. DMA 읽기 배리어: status 읽기 후 데이터 필드 읽기 순서 보장 */
dma_rmb();
/* 3. 디스크립터 데이터 필드 읽기 */
pkt_len = le32_to_cpu(desc->pkt_len);
pkt_addr = le64_to_cpu(desc->buf_addr);
readl/writel vs ioread32/iowrite32
Linux MMIO 접근 함수들은 내장된 배리어 수준이 다릅니다:
| 함수 | 내장 배리어 | 성능 | 사용 시나리오 |
|---|---|---|---|
writel() | 앞: wmb(), 뒤: 없음 | 안전 | 기본 MMIO 쓰기 |
writel_relaxed() | 없음 | 빠름 | 연속 MMIO 쓰기 (마지막에 writel) |
readl() | 앞: 없음, 뒤: rmb() | 안전 | 기본 MMIO 읽기 |
readl_relaxed() | 없음 | 빠름 | 연속 MMIO 읽기 |
/* 성능 최적화: 여러 레지스터를 연속 쓸 때 relaxed 사용 */
writel_relaxed(val1, reg + 0x00);
writel_relaxed(val2, reg + 0x04);
writel_relaxed(val3, reg + 0x08);
writel(val4, reg + 0x0C); /* 마지막만 정규 writel → 모든 쓰기 완료 보장 */
배리어 관련 흔한 실수
| 실수 | 증상 | 수정 |
|---|---|---|
디스크립터 데이터와 OWN 비트 사이에 dma_wmb() 누락 | 디바이스가 stale 데이터로 DMA, 간헐적 데이터 손상 | OWN 비트 설정 전에 dma_wmb() 삽입 |
DONE 비트 확인 후 데이터 읽기 전에 dma_rmb() 누락 | CPU가 이전 전송의 stale 데이터를 읽음 | DONE 확인 후 dma_rmb() 삽입 |
DMA 메모리 쓰기 후 doorbell writel() 전에 wmb() 누락 | 디바이스가 doorbell을 먼저 보고 미완성 데이터 처리 | writel()이 내장 wmb()를 포함하지만 WC 영역은 별도 필요 |
mb() 남용 (모든 곳에 full barrier) | 불필요한 성능 저하 | 방향에 맞는 최소 배리어 사용 (dma_rmb/dma_wmb) |
IOMMU 심화
IOMMU(Input/Output Memory Management Unit)는 디바이스의 DMA 요청에 대해 가상→물리 주소 변환을 수행하고, 디바이스 간 메모리 접근을 격리합니다.
주요 IOMMU 구현
| 구현 | 벤더 | 커널 소스 | 특징 |
|---|---|---|---|
| VT-d | Intel | drivers/iommu/intel/ | DMAR 테이블, 2레벨/확장 페이지 테이블, Interrupt Remapping |
| AMD-Vi | AMD | drivers/iommu/amd/ | IVRS 테이블, v2 페이지 테이블 (중첩 페이지 테이블 공유) |
| SMMU v3 | ARM | drivers/iommu/arm/arm-smmu-v3/ | STE/CD 2단계, Stage 1+2, HTTU, MSI doorbell |
IOMMU 동작 모드
| 모드 | 커널 파라미터 | 동작 | 사용 시나리오 |
|---|---|---|---|
| Passthrough | iommu.passthrough=1 | 주소 변환 없음 (DMA = PA) | 성능 최우선, 신뢰된 디바이스 |
| DMA API (lazy) | iommu=lazy | IOVA 해제 지연 (배치 flush) | 기본 모드, 성능과 격리의 균형 |
| DMA API (strict) | iommu=strict | unmap 즉시 IOTLB flush | 보안 최우선, 기밀 컴퓨팅 |
| VFIO (user) | VFIO 드라이버 | 유저스페이스가 매핑 제어 | DPDK, GPU passthrough |
IOMMU 그룹과 격리
IOMMU 그룹은 하드웨어적으로 격리 가능한 최소 단위입니다. 같은 그룹 내 디바이스는 서로의 DMA를 볼 수 있으므로 VFIO passthrough 시 그룹 단위로 할당해야 합니다.
/* IOMMU 그룹 조회 */
$ ls /sys/kernel/iommu_groups/*/devices/
/* 출력 예시 */
/sys/kernel/iommu_groups/1/devices/0000:03:00.0
/sys/kernel/iommu_groups/1/devices/0000:03:00.1
/sys/kernel/iommu_groups/2/devices/0000:01:00.0
/* ACS(Access Control Services) 확인 — 격리 수준 판단 */
$ lspci -vvs 03:00.0 | grep -i acs
ATS, PRI, PASID
| 기능 | PCIe 확장 | 역할 |
|---|---|---|
| ATS | Address Translation Service | 디바이스가 IOMMU에 변환 요청, 결과를 디바이스 TLB에 캐시 |
| PRI | Page Request Interface | 디바이스가 페이지 폴트를 IOMMU에 보고, 온디맨드 매핑 |
| PASID | Process Address Space ID | 디바이스가 프로세스별 주소 공간을 구분, SVA(Shared Virtual Addressing) 지원 |
/* SVA(Shared Virtual Addressing) — 유저 가상 주소를 디바이스가 직접 사용 */
#include <linux/iommu.h>
struct iommu_sva *sva;
/* 디바이스를 현재 프로세스 주소 공간에 바인딩 */
sva = iommu_sva_bind_device(dev, current->mm);
pasid = iommu_sva_get_pasid(sva);
/* 디바이스에 PASID와 유저 VA 전달 → 디바이스가 직접 접근 */
hw_submit_work(pasid, user_va, len);
/* 언바인드 */
iommu_sva_unbind_device(sva);
IOMMU 도메인 API
struct iommu_domain은 IOMMU 주소 공간의 단위입니다. 같은 도메인에 연결된 디바이스들은 동일한 IOVA→PA 매핑을 공유합니다. DMA API는 내부적으로 도메인을 관리하지만, VFIO 등에서는 직접 도메인 API를 사용합니다.
| 도메인 유형 | 상수 | 용도 |
|---|---|---|
| DMA | IOMMU_DOMAIN_DMA | 커널 DMA API 자동 관리, lazy/strict flush |
| DMA-FQ | IOMMU_DOMAIN_DMA_FQ | Flush Queue 기반 lazy IOTLB 무효화 |
| Identity | IOMMU_DOMAIN_IDENTITY | 1:1 매핑 (passthrough), IOVA = PA |
| Unmanaged | IOMMU_DOMAIN_UNMANAGED | 유저스페이스(VFIO) 직접 관리 |
| SVA | IOMMU_DOMAIN_SVA | 프로세스 주소 공간 공유 (PASID) |
| Blocked | IOMMU_DOMAIN_BLOCKED | 모든 DMA 차단 (초기 상태, 보안) |
/* VFIO 스타일: 유저스페이스 관리 도메인 */
struct iommu_domain *domain;
/* 1. Unmanaged 도메인 생성 */
domain = iommu_domain_alloc(dev->bus);
if (!domain)
return -ENOMEM;
/* 2. 디바이스를 도메인에 연결 */
ret = iommu_attach_device(domain, dev);
if (ret)
goto free_domain;
/* 3. IOVA → PA 매핑 추가 (VFIO가 유저 메모리를 매핑) */
ret = iommu_map(domain, iova, phys_addr, size,
IOMMU_READ | IOMMU_WRITE, GFP_KERNEL);
/* 4. 매핑 해제 */
iommu_unmap(domain, iova, size);
/* 5. 정리 */
iommu_detach_device(domain, dev);
free_domain:
iommu_domain_free(domain);
IOMMU 페이지 테이블 포맷
주요 IOMMU는 각각 고유한 페이지 테이블 구조를 사용하지만, 기본 원리(다단계 테이블, 4KB/2MB/1GB 페이지)는 CPU MMU와 유사합니다.
IOTLB 관리와 Flush 전략
IOMMU도 CPU TLB처럼 변환 캐시(IOTLB)를 사용합니다. 매핑 해제 시 IOTLB를 무효화해야 하며, 이 전략이 DMA 성능에 큰 영향을 미칩니다.
| 전략 | 커널 설정 | IOTLB Flush 시점 | 보안 | 성능 |
|---|---|---|---|---|
| Strict | iommu.strict=1 | unmap 즉시 | 최고 (stale TLB 방지) | 느림 (빈번한 flush) |
| Lazy (FQ) | iommu=lazy (기본) | Flush Queue 배치 | 보통 (짧은 취약 창) | 빠름 (배치 flush) |
| Passthrough | iommu.passthrough=1 | 해당 없음 (1:1 매핑) | 없음 | 최고 |
/* IOTLB flush가 DMA 성능에 미치는 영향 확인 */
/* perf로 IOTLB miss 추적 (Intel VT-d) */
$ perf stat -e dTLB-load-misses,dTLB-store-misses \
-e iommu/iommu_tlb_inv_tlb/ dd if=/dev/nvme0n1 of=/dev/null bs=4k count=10000
/* lazy vs strict 전환 (재부팅 불필요, 도메인 단위) */
$ echo 1 > /sys/kernel/debug/iommu/intel/dmar0/strict
$ echo 0 > /sys/kernel/debug/iommu/intel/dmar0/strict
Default Domain 정책 (커널 6.x)
커널 6.x부터 IOMMU default domain 정책이 디바이스별로 세분화되었습니다:
/* sysfs로 디바이스별 도메인 유형 확인/변경 */
$ cat /sys/bus/pci/devices/0000:03:00.0/iommu_group/type
DMA
/* 도메인 유형 변경 (모든 디바이스가 unbind 상태여야) */
$ echo identity > /sys/bus/pci/devices/0000:03:00.0/iommu_group/type
/* 가능한 값: DMA, DMA-FQ, identity */
SWIOTLB 바운스 버퍼
SWIOTLB(Software I/O TLB)는 DMA 주소 제한을 소프트웨어적으로 해결하는 바운스 버퍼 메커니즘입니다.
사용 조건
| 조건 | 설명 |
|---|---|
| DMA 마스크 부족 | 디바이스가 32비트 DMA만 지원하지만 버퍼가 4GB 이상에 있을 때 |
| IOMMU 없음 | IOMMU가 없는 시스템에서 주소 변환 불가 시 |
| 기밀 컴퓨팅 | AMD SEV, Intel TDX에서 암호화된 게스트 메모리 ↔ 디바이스 간 전송 |
설정과 모니터링
/* 부팅 파라미터 */
swiotlb=65536 /* 슬랩 수 (기본 64MB = 32768 × 2KB) */
swiotlb=force /* 강제 사용 (디버깅) */
swiotlb=noforce /* 비활성화 */
/* 모니터링 */
$ cat /sys/kernel/debug/swiotlb/io_tlb_nslabs /* 전체 슬랩 수 */
$ cat /sys/kernel/debug/swiotlb/io_tlb_used /* 사용 중 슬랩 수 */
$ dmesg | grep -i swiotlb /* 부팅 로그 확인 */
기밀 컴퓨팅과 SWIOTLB
AMD SEV/Intel TDX 환경에서는 게스트 메모리가 암호화되어 있어 디바이스가 직접 접근할 수 없습니다. SWIOTLB가 공유(비암호화) 바운스 버퍼를 통해 데이터를 중계합니다:
- TDX/SEV에서 SWIOTLB 동작 흐름 */
- 바운스 버퍼는 shared(비암호화) 메모리 영역에 할당 */
- CPU: 암호화된 원본 → 복호화 → 바운스 버퍼에 복사 */
- 디바이스: 바운스 버퍼에서 DMA 읽기 */
- 수신: 디바이스 → 바운스 → 암호화하여 원본에 복사 */
CMA (Contiguous Memory Allocator)
CMA는 물리적으로 연속된 대용량 메모리를 런타임에 할당할 수 있게 하는 메커니즘입니다. IOMMU가 없는 임베디드 시스템에서 DMA 버퍼 할당에 특히 중요합니다.
CMA와 DMA의 관계
IOMMU가 없으면 물리적으로 연속된 메모리가 DMA에 필수입니다. CMA는 부팅 시 예약된 영역을 평상시 이동 가능(movable) 페이지로 사용하다가, DMA 요청 시 페이지를 이주(migrate)시키고 연속 블록을 확보합니다.
/* DMA 할당이 CMA를 자동 사용하는 경로 */
dma_alloc_coherent(dev, size, &handle, GFP_KERNEL)
→ dma_alloc_attrs()
→ dma_direct_alloc() /* IOMMU 없는 경우 */
→ dma_alloc_from_contiguous()
→ cma_alloc() /* CMA 영역에서 할당 */
CMA 설정
/* 커널 부팅 파라미터 */
cma=256M /* 기본 CMA 영역 크기 */
cma=256M@0-4G /* 4GB 이하에 256MB 예약 (32비트 DMA) */
/* Device Tree 설정 (ARM/임베디드) */
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x10000000>; /* 256MB */
linux,cma-default;
};
/* 디바이스 전용 CMA */
gpu_cma: gpu_reserved {
compatible = "shared-dma-pool";
reusable;
reg = <0x50000000 0x8000000>; /* 128MB at 1.25GB */
};
};
gpu@10000000 {
memory-region = <&gpu_cma>; /* 디바이스별 CMA 연결 */
};
/* CMA 상태 모니터링 */
$ cat /proc/meminfo | grep Cma
CmaTotal: 262144 kB
CmaFree: 245760 kB
$ cat /sys/kernel/debug/cma/cma-*/count /* 할당 횟수 */
$ cat /sys/kernel/debug/cma/cma-*/used /* 사용 중 페이지 */
DMA-BUF 버퍼 공유
DMA-BUF는 서로 다른 디바이스 드라이버 간 DMA 버퍼를 파일 디스크립터(fd)로 공유하는 커널 프레임워크입니다. GPU 렌더링 결과를 디스플레이 컨트롤러·비디오 인코더·카메라와 제로카피로 공유하는 데 핵심적입니다.
Exporter: dma_buf_ops
#include <linux/dma-buf.h>
static const struct dma_buf_ops my_dmabuf_ops = {
.attach = my_attach, /* importer 연결 시 */
.detach = my_detach,
.map_dma_buf = my_map_dma_buf, /* SG 테이블 제공 */
.unmap_dma_buf = my_unmap_dma_buf,
.release = my_release, /* 마지막 참조 해제 */
.mmap = my_mmap, /* 유저스페이스 mmap */
.vmap = my_vmap, /* 커널 가상 매핑 */
};
/* DMA-BUF 내보내기 */
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
exp_info.ops = &my_dmabuf_ops;
exp_info.size = buf_size;
exp_info.priv = my_private_data;
struct dma_buf *dmabuf = dma_buf_export(&exp_info);
int fd = dma_buf_fd(dmabuf, O_CLOEXEC); /* fd로 변환 → 유저스페이스에 전달 */
Importer: 버퍼 가져오기
/* fd에서 dma_buf 획득 */
struct dma_buf *dmabuf = dma_buf_get(fd);
/* 디바이스에 연결 */
struct dma_buf_attachment *attach = dma_buf_attach(dmabuf, dev);
/* SG 테이블 획득 — 디바이스가 DMA로 접근할 주소 */
struct sg_table *sgt = dma_buf_map_attachment(attach, DMA_FROM_DEVICE);
/* 디바이스에 SG 주소 프로그래밍, DMA 전송 수행 */
/* 해제 */
dma_buf_unmap_attachment(attach, sgt, DMA_FROM_DEVICE);
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);
dma_fence와 dma_resv
dma_fence는 비동기 GPU/DMA 작업의 완료를 시그널링하는 동기화 프리미티브입니다. dma_resv(reservation object)는 DMA-BUF당 하나씩 내장되어 있으며, 여러 fence를 관리합니다.
/* fence 대기 */
struct dma_resv *resv = dmabuf->resv;
/* 모든 exclusive fence 대기 (쓰기 완료 대기) */
ret = dma_resv_wait_timeout(resv, DMA_RESV_USAGE_WRITE,
true, MAX_SCHEDULE_TIMEOUT);
/* implicit sync: 커널이 자동으로 fence를 삽입/확인 */
/* explicit sync: DRM_IOCTL_SYNCOBJ_* 등으로 유저스페이스가 직접 관리 */
DMA-BUF Heaps
유저스페이스에서 직접 DMA 가능한 버퍼를 할당하는 인터페이스입니다. Android ION 할당자의 후속:
/* 유저스페이스 예시 */
int heap_fd = open("/dev/dma_heap/system", O_RDWR);
struct dma_heap_allocation_data data = {
.len = 4096,
.fd_flags = O_CLOEXEC | O_RDWR,
};
ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);
/* data.fd — DMA-BUF fd를 받음 */
Android: ION에서 DMA-BUF Heaps로
Android는 GPU, 카메라, 디스플레이 등 다중 디바이스 간 버퍼 공유를 위해 독자적인 ION allocator를 사용했다. ION은 힙 유형별(system, CMA, carveout 등) 메모리 할당과 캐시 관리를 제공했으나, 메인라인에 통합되지 못하고 staging에 머물렀다.
커널 5.6부터 ION의 기능을 메인라인 DMA-BUF Heaps 프레임워크로 대체하기 시작했으며, 커널 5.11에서 ION은 완전히 제거되었다. DMA-BUF Heaps는 ION 대비 깔끔한 API와 메인라인 지원이라는 장점이 있다.
| 특성 | ION (deprecated) | DMA-BUF Heaps |
|---|---|---|
| 인터페이스 | /dev/ion + ioctl | /dev/dma_heap/* + ioctl |
| 커널 위치 | staging/android/ion | drivers/dma-buf/dma-heap |
| 메인라인 | staging (미통합) | 완전 통합 (5.6+) |
| 힙 유형 | system, cma, carveout 등 | system, cma (확장 가능) |
| Android HAL | Gralloc 2.0 | Gralloc 4.0+ (mapper) |
/* Android HAL에서의 DMA-BUF Heaps 사용 (Gralloc) */
/* 카메라 HAL이 DMA-BUF를 할당하고, GPU/디스플레이와 공유 */
int heap_fd = open("/dev/dma_heap/system", O_RDWR);
struct dma_heap_allocation_data alloc = { .len = frame_size, .fd_flags = O_RDWR | O_CLOEXEC };
ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc);
/* alloc.fd를 Binder로 GPU/Display HAL에 전달 → 제로카피 공유 */
Android의 DMA-BUF 활용, Gralloc HAL 구조, 카메라/GPU 버퍼 공유 등 심화 내용은 Android 커널 — HAL과 커널 인터페이스를 참고하라.
DMA Engine 프레임워크
DMA Engine(dmaengine)은 SoC 내장 DMA 컨트롤러를 추상화하는 커널 서브시스템입니다. 버스마스터 DMA와 달리, 별도의 DMA 컨트롤러 하드웨어가 CPU를 대신하여 메모리↔메모리 또는 메모리↔페리퍼럴 데이터 전송을 수행합니다. SPI, I2C, UART, 오디오 코덱, NAND 플래시 등 임베디드 페리퍼럴에서 광범위하게 사용됩니다.
- 버스마스터 DMA — 디바이스가 직접 PCIe 버스를 마스터링 (NIC, NVMe, GPU).
dma_map_*API 사용. - Slave DMA (DMA Engine) — 별도 DMA 컨트롤러가 대행 (SoC 내장).
dmaengine_*API 사용.
아키텍처 개요
전송 유형
| 전송 유형 | API | 방향 | 사용 예 |
|---|---|---|---|
| Slave SG | dmaengine_prep_slave_sg() | MEM↔DEV | SPI, UART, I2C 버퍼 전송 |
| Cyclic | dmaengine_prep_dma_cyclic() | MEM↔DEV (순환) | 오디오 DMA, ADC 연속 샘플링 |
| Interleaved | dmaengine_prep_interleaved_dma() | MEM↔DEV/MEM | 2D/3D 블록 전송 (프레임버퍼) |
| Memcpy | dmaengine_prep_dma_memcpy() | MEM→MEM | 대용량 메모리 복사 오프로드 |
| Memset | dmaengine_prep_dma_memset() | MEM | 메모리 초기화 오프로드 |
| XOR | dmaengine_prep_dma_xor() | MEM | RAID5/6 패리티 계산 |
클라이언트 API — 전형적인 Slave DMA 흐름
#include <linux/dmaengine.h>
struct dma_chan *chan;
struct dma_async_tx_descriptor *desc;
struct dma_slave_config cfg;
dma_cookie_t cookie;
/* ① 채널 획득 (Device Tree binding 기반) */
chan = dma_request_chan(dev, "tx");
if (IS_ERR(chan))
return PTR_ERR(chan);
/* ② 슬레이브 설정 — 방향, FIFO 주소, 버스 폭 */
memset(&cfg, 0, sizeof(cfg));
cfg.direction = DMA_MEM_TO_DEV;
cfg.dst_addr = spi_fifo_phys; /* 페리퍼럴 FIFO 물리 주소 */
cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
cfg.dst_maxburst = 16; /* FIFO burst 크기 */
dmaengine_slave_config(chan, &cfg);
/* ③ 디스크립터 준비 — SG 리스트 기반 */
desc = dmaengine_prep_slave_sg(chan, sg_list, sg_nents,
DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc) {
dev_err(dev, "Failed to prepare DMA descriptor\\n");
goto release;
}
/* ④ 완료 콜백 설정 */
desc->callback = my_dma_complete;
desc->callback_param = priv;
/* ⑤ 디스크립터 제출 (큐에 추가) */
cookie = dmaengine_submit(desc);
if (dma_submit_error(cookie)) {
dev_err(dev, "DMA submit failed\\n");
goto release;
}
/* ⑥ DMA 전송 시작 (pending → active) */
dma_async_issue_pending(chan);
/* ⑦ 완료 대기 (또는 콜백으로 비동기 처리) */
enum dma_status status;
status = dma_async_is_tx_complete(chan, cookie, NULL, NULL);
release:
dma_release_channel(chan);
Cyclic DMA — 오디오 스트리밍
순환 DMA는 고정 크기 버퍼를 계속 반복 전송하며, 각 period 완료 시 인터럽트를 발생시킵니다. 오디오 PCM 재생/캡처의 핵심 패턴입니다.
/* Cyclic DMA: 오디오 PCM 재생 */
size_t buf_size = periods * period_size;
desc = dmaengine_prep_dma_cyclic(chan,
dma_buf_addr, /* DMA 버퍼 물리 주소 */
buf_size, /* 전체 버퍼 크기 */
period_size, /* period당 크기 (IRQ 단위) */
DMA_MEM_TO_DEV, /* 메모리 → I2S FIFO */
DMA_PREP_INTERRUPT); /* period마다 인터럽트 */
desc->callback = audio_period_elapsed;
cookie = dmaengine_submit(desc);
dma_async_issue_pending(chan);
/* 콜백: 각 period 완료 시 호출 */
static void audio_period_elapsed(void *param)
{
struct snd_pcm_substream *substream = param;
snd_pcm_period_elapsed(substream); /* ALSA에 period 완료 통보 */
}
Device Tree DMA 바인딩
/* DTS: 페리퍼럴과 DMA 채널 연결 */
spi0: spi@44000000 {
compatible = "vendor,spi";
reg = <0x44000000 0x400>;
interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
dmas = <&dma1 11 &dma1 12>; /* TX: ch11, RX: ch12 */
dma-names = "tx", "rx";
};
dma1: dma-controller@40020000 {
compatible = "vendor,dma-controller";
reg = <0x40020000 0x400>;
interrupts = <GIC_SPI 56 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>; /* 채널 번호 1개 인자 */
dma-channels = <8>;
dma-requests = <16>;
};
dmaengine_*)와 DMA Mapping API(dma_map_*)를 혼동하지 마십시오. DMA Engine은 별도 DMA 컨트롤러를 프로그래밍하는 API이고, DMA Mapping은 버스마스터 디바이스가 메모리에 접근할 DMA 주소를 설정하는 API입니다. 둘은 독립적으로 사용되며, 때로는 조합됩니다 (예: DMA Engine이 SG 리스트를 전송할 때 내부적으로 DMA 매핑 사용).P2P DMA (Peer-to-Peer DMA)
PCIe Peer-to-Peer DMA는 두 PCIe 디바이스 간 CPU/시스템 메모리를 거치지 않고 직접 데이터를 전송합니다.
사용 사례
| 사용 사례 | 설명 |
|---|---|
| GPUDirect RDMA | GPU VRAM ↔ NIC 간 직접 전송 (NVIDIA, AMD ROCm) |
| GPUDirect Storage | NVMe ↔ GPU VRAM 간 직접 전송 |
| NVMe-oF P2P | NVMe 디바이스 간 타깃 오프로드 |
| FPGA ↔ GPU | 데이터 파이프라인 가속 |
pci_p2pdma API
#include <linux/pci-p2pdma.h>
/* P2P DMA 가능 여부 확인 */
int dist = pci_p2pdma_distance(provider, client, true);
if (dist < 0) {
dev_warn(dev, "P2P DMA not supported between devices\\n");
return -ENODEV;
}
/* P2P 메모리 할당 (provider의 BAR에서) */
void *p2p_buf = pci_alloc_p2pmem(provider, size);
/* SG 테이블에 P2P 페이지 설정 */
sg_set_page(&sg[0], virt_to_page(p2p_buf), size, 0);
mapped = dma_map_sg(client_dev, sg, 1, DMA_BIDIRECTIONAL);
/* 해제 */
pci_free_p2pmem(provider, p2p_buf, size);
NUMA 인식 DMA
다중 소켓 서버에서 DMA 성능은 NUMA 토폴로지에 크게 영향받습니다. 디바이스가 연결된 NUMA 노드의 메모리에 DMA 버퍼를 할당하면 QPI/UPI 인터커넥트 트래버설을 피해 지연 시간을 줄이고 대역폭을 극대화할 수 있습니다.
NUMA 인식 DMA 할당
/* 디바이스의 NUMA 노드 확인 */
int node = dev_to_node(dev);
pr_info("Device on NUMA node %d\\n", node);
/* NUMA 노드 인식 메모리 할당 → DMA 매핑 */
buf = kmalloc_node(size, GFP_KERNEL, node);
dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
/* 또는 페이지 단위 */
page = alloc_pages_node(node, GFP_KERNEL, order);
dma_handle = dma_map_page(dev, page, 0, size, DMA_FROM_DEVICE);
/* dma_alloc_coherent()는 자동으로 디바이스의 NUMA 노드에서 할당 시도 */
/* 인터럽트 CPU 어피니티도 같은 NUMA 노드로 설정 */
irq_set_affinity_hint(irq, cpumask_of_node(node));
NUMA DMA 성능 최적화 체크리스트
| 항목 | 확인 명령 | 최적화 |
|---|---|---|
| 디바이스 NUMA 노드 | cat /sys/bus/pci/devices/ADDR/numa_node | 할당을 해당 노드에 고정 |
| IRQ 어피니티 | cat /proc/irq/IRQ/smp_affinity_list | 같은 NUMA 노드 CPU에 바인딩 |
| 메모리 할당 노드 | numastat -p PID | kmalloc_node() 또는 mbind 사용 |
| RX 큐 CPU 매핑 | cat /sys/class/net/eth0/queues/rx-0/rps_cpus | RSS + IRQ 어피니티 일치 |
| DPDK hugepage | numactl --show | numactl --membind=N으로 실행 |
가상화와 DMA
가상화 환경에서 DMA는 추가적인 주소 변환 계층과 보안 과제를 수반합니다. 게스트의 DMA 요청이 호스트 물리 메모리에 올바르게 도달하려면 IOMMU의 2단계 변환 또는 paravirtualized DMA가 필요합니다.
가상화 DMA 모드
| 모드 | 메커니즘 | 성능 | 보안 | 사용처 |
|---|---|---|---|---|
| 에뮬레이션 | 모든 DMA를 하이퍼바이저가 트랩·에뮬레이트 | 매우 느림 | 완전 격리 | 레거시, 테스트 |
| Paravirt (virtio) | 게스트가 virtqueue로 I/O 명령 전달 | 좋음 | 하이퍼바이저 관리 | KVM/QEMU 기본 |
| VFIO Passthrough | 디바이스를 게스트에 직접 할당, IOMMU Stage-2 | 네이티브급 | IOMMU 격리 | SR-IOV NIC, GPU |
| vDPA | virtio 데이터 경로를 HW가 가속 | 네이티브급 | IOMMU | SmartNIC |
virtio DMA 경로
virtio 디바이스는 virtqueue(공유 메모리 링)를 통해 게스트↔호스트 데이터를 교환합니다. 게스트 내 DMA 매핑은 SWIOTLB 또는 IOMMU를 통해 처리됩니다.
VFIO 디바이스 Passthrough
/* SR-IOV VF를 게스트에 직접 할당하는 절차 */
/* ① SR-IOV VF 생성 */
$ echo 4 > /sys/bus/pci/devices/0000:03:00.0/sriov_numvfs
/* ② VF를 vfio-pci 드라이버에 바인딩 */
$ echo 0000:03:02.0 > /sys/bus/pci/devices/0000:03:02.0/driver/unbind
$ echo vfio-pci > /sys/bus/pci/devices/0000:03:02.0/driver_override
$ echo 0000:03:02.0 > /sys/bus/pci/drivers/vfio-pci/bind
/* ③ IOMMU 그룹 확인 */
$ readlink /sys/bus/pci/devices/0000:03:02.0/iommu_group
/* ../../../kernel/iommu_groups/15 */
/* ④ QEMU에 VF 전달 */
$ qemu-system-x86_64 ... \
-device vfio-pci,host=0000:03:02.0
기밀 컴퓨팅 DMA (TDX/SEV)
AMD SEV-SNP와 Intel TDX에서 게스트 메모리는 암호화되어 디바이스가 직접 읽을 수 없습니다. DMA는 반드시 공유(비암호화) 메모리를 통해 이루어지며, 커널은 이를 SWIOTLB로 자동 처리합니다.
| 기술 | 메모리 암호화 | DMA 경로 | SWIOTLB 역할 |
|---|---|---|---|
| AMD SEV | SME/SEV-ES: C-bit | 공유 페이지 마킹 | 공유 바운스 버퍼 |
| AMD SEV-SNP | RMP 기반 소유권 | 명시적 공유 변환 | 공유 바운스 버퍼 |
| Intel TDX | MKTME + SEPT | shared bit 설정 | 공유 바운스 버퍼 |
| ARM CCA | Realm 메모리 | NS 마킹 | NS 바운스 버퍼 |
/* TDX/SEV 환경에서의 SWIOTLB 동작 (커널 내부) */
/* 1. DMA 매핑 시 원본이 private(암호화) 메모리이면 */
/* 2. SWIOTLB shared 풀에서 바운스 버퍼 할당 */
/* 3. 원본 → 바운스: 복호화 복사 */
/* 4. 디바이스는 바운스 버퍼(비암호화)에서 DMA */
/* 5. 수신: 바운스 → 원본: 암호화 복사 */
/* 부팅 시 SWIOTLB 크기 증가 필요 (기본 64MB → 256MB+) */
/* TDX 게스트는 자동으로 swiotlb=force 설정 */
$ dmesg | grep -i swiotlb
[ 0.001] software IO TLB: SWIOTLB bounce buffer size adjusted to 256MB
DMA 보안
DMA 공격 벡터
| 공격 | 설명 | 방어 |
|---|---|---|
| DMA Attack | 악의적 디바이스(Thunderbolt/PCIe)가 시스템 메모리 전체에 접근 | IOMMU 활성화 |
| TOCTOU | CPU가 DMA 버퍼 검증 후, DMA가 내용을 변경 | 바운스 버퍼 사용 |
| Iago Attack | 악의적 하이퍼바이저가 DMA 매핑 결과 조작 | 기밀 컴퓨팅(SEV/TDX) |
IOMMU 보호
IOMMU는 DMA 보안의 핵심입니다. 디바이스가 접근 가능한 메모리 영역을 제한합니다:
/* IOMMU strict 모드 강제 (보안 최우선) */
/* 커널 파라미터: iommu.passthrough=0 iommu.strict=1 */
/* Thunderbolt/외장 디바이스 연결 시 IOMMU 보호 확인 */
$ dmesg | grep -i iommu
[ 0.123] DMAR: IOMMU enabled
[ 1.456] thunderbolt 0-1: IOMMU DMA protection enabled
Thunderbolt/USB4 DMA 보호
Thunderbolt는 PCIe 터널링을 지원하므로 외부 디바이스가 시스템 메모리에 DMA 접근할 수 있습니다. 이는 심각한 보안 위협입니다.
| 보호 메커니즘 | 커널 지원 | 설명 |
|---|---|---|
| IOMMU 기반 격리 | 기본 (VT-d/AMD-Vi) | Thunderbolt 디바이스를 개별 IOMMU 도메인에 격리 |
| Security Level | thunderbolt 드라이버 | none/user/secure/dponly 레벨 설정 |
| Pre-boot DMA | BIOS/UEFI | OS 부팅 전 DMA 보호 (커널 부팅 전 취약 구간) |
| Kernel DMA Protection | ACPI DMAR | BIOS가 부팅 시 IOMMU를 미리 활성화 |
/* Thunderbolt 보안 레벨 확인 */
$ cat /sys/bus/thunderbolt/devices/domain0/security
secure
/* Kernel DMA Protection 상태 확인 */
$ dmesg | grep -i "kernel dma protection"
[ 0.000] Kernel DMA protection enabled
/* IOMMU가 Thunderbolt 디바이스를 격리하는지 확인 */
$ dmesg | grep -i thunderbolt | grep -i iommu
[ 2.345] thunderbolt 0-1: IOMMU DMA protection enabled
DMA TOCTOU 공격과 방어
TOCTOU(Time-of-Check to Time-of-Use)는 CPU가 DMA 버퍼의 데이터를 검증한 후, 악의적 디바이스가 DMA로 데이터를 변경하는 공격입니다. Thunderbolt, FireWire 등 외부 디바이스에서 발생 가능합니다.
/* ❌ 취약한 코드: 공유 DMA 버퍼에서 직접 검증 */
if (dma_buf->length <= MAX_SIZE) { /* 검증 시점 */
/* 디바이스가 여기서 length를 0xFFFFFFFF로 변경! */
memcpy(dst, dma_buf->data, dma_buf->length); /* 사용 시점: 버퍼 오버플로! */
}
/* ✅ 안전한 코드: 바운스 버퍼로 복사 후 검증 */
size_t len;
memcpy(&local_header, dma_buf, sizeof(local_header)); /* 로컬 복사 */
len = local_header.length; /* 로컬 변수 사용 */
if (len <= MAX_SIZE) {
memcpy(dst, dma_buf->data, len); /* 안전: len은 디바이스가 변경 불가 */
}
Restricted DMA (Device Tree)
IOMMU가 없는 임베디드 시스템에서 restricted-dma-pool을 사용하여 디바이스 DMA 접근 범위를 제한합니다:
reserved-memory {
restricted_dma: restricted-dma {
compatible = "restricted-dma-pool";
size = <0x0 0x400000>; /* 4MB */
};
};
pcie@10000000 {
memory-region = <&restricted_dma>;
/* 이 디바이스의 DMA는 지정된 영역으로 제한 */
};
DMA 디버깅과 최적화
DMA_API_DEBUG
커널 빌드 시 CONFIG_DMA_API_DEBUG=y를 활성화하면 DMA API 오용을 런타임에 탐지합니다:
/* 탐지하는 오류 유형 */
/* - map/unmap 짝 불일치 */
/* - 이미 해제된 DMA 매핑에 접근 */
/* - 잘못된 방향(direction) 사용 */
/* - sync 호출 없이 CPU/디바이스 간 전환 */
/* - dma_mapping_error() 미확인 */
/* 활성화 */
$ echo 1 > /sys/kernel/debug/dma-api/enable
/* 로그 확인 */
$ dmesg | grep DMA-API
DMA-API: device driver tries to free DMA memory it has not allocated
/* 매핑 통계 */
$ cat /sys/kernel/debug/dma-api/num_errors
$ cat /sys/kernel/debug/dma-api/driver_filter /* 특정 드라이버만 추적 */
성능 최적화 가이드
| 기법 | 효과 | 적용 방법 |
|---|---|---|
| IOMMU lazy 모드 | IOTLB flush 배치 처리 | iommu=lazy (기본값) |
| 대형 IOVA 할당 | TLB 효율 향상 | 2MB/1GB hugepage DMA 할당 |
| SG 병합 | DMA 엔트리 수 감소 | IOMMU가 자동 병합, max_segment_size 확인 |
| DMA Pool | 반복 할당 오버헤드 제거 | 고정 크기 디스크립터에 dma_pool 사용 |
| Streaming 재사용 | map/unmap 횟수 감소 | dma_sync_*로 소유권만 전환 |
| 정확한 방향 지정 | 불필요한 캐시 유지보수 제거 | DMA_BIDIRECTIONAL 피하기 |
Tracepoint / ftrace 디버깅
DMA 서브시스템은 다양한 tracepoint를 제공하여 매핑/언매핑/바운스 등의 이벤트를 실시간 추적할 수 있습니다.
/* 사용 가능한 DMA 관련 tracepoint 확인 */
$ ls /sys/kernel/debug/tracing/events/dma/
dma_map_page dma_unmap_page dma_map_sg dma_unmap_sg
dma_alloc dma_free dma_map_resource dma_sync_single
/* DMA 매핑 추적 활성화 */
$ echo 1 > /sys/kernel/debug/tracing/events/dma/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
/* 출력 예시 */
nvme0n1-23 [003] .... 1234.567: dma_map_sg: dev=0000:04:00.0 nents=32 mapped=1
nvme0n1-23 [003] .... 1234.568: dma_unmap_sg: dev=0000:04:00.0 nents=32
/* SWIOTLB 바운스 추적 */
$ echo 1 > /sys/kernel/debug/tracing/events/swiotlb/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
/* swiotlb_bounce가 빈번하면 IOMMU 활성화 또는 DMA 마스크 확인 */
/* IOMMU 관련 tracepoint */
$ ls /sys/kernel/debug/tracing/events/iommu/
map unmap io_page_fault attach_device detach_device
/* IOMMU 페이지 폴트 추적 */
$ echo 1 > /sys/kernel/debug/tracing/events/iommu/io_page_fault/enable
/* io_page_fault 발생 시: 디바이스가 매핑되지 않은 IOVA에 접근 시도 */
성능 분석 도구
| 도구 | 용도 | 사용법 |
|---|---|---|
perf stat | DMA 관련 PMU 카운터 | perf stat -e dTLB-load-misses cmd |
perf trace | DMA syscall/tracepoint 추적 | perf trace -e 'dma:*' cmd |
bpftrace | DMA 지연 시간 히스토그램 | 커스텀 bpftrace 스크립트 |
/proc/meminfo | CMA/SWIOTLB 메모리 상태 | grep -E 'Cma|Bounce' /proc/meminfo |
iommu debugfs | IOMMU 도메인/매핑 상태 | cat /sys/kernel/debug/iommu/*/ |
dmabuf debugfs | DMA-BUF 할당 목록 | cat /sys/kernel/debug/dma_buf/bufinfo |
/* bpftrace: DMA 매핑 지연 측정 */
$ bpftrace -e '
kprobe:dma_map_sg_attrs { @start[tid] = nsecs; }
kretprobe:dma_map_sg_attrs /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
/* DMA-BUF 전체 할당 현황 */
$ cat /sys/kernel/debug/dma_buf/bufinfo
/* Dma-buf Objects:
size flags mode count exp_name
4194304 00000002 00080007 3 i915 */
/* IOMMU 그룹별 디바이스와 도메인 정보 */
$ find /sys/kernel/iommu_groups/ -type l | while read l; do
echo "$(dirname $l): $(readlink $l)"; done
핵심 주의사항 10가지
- dma_mapping_error() 필수 확인 — map 후 반드시 오류 검사, IOVA 고갈 가능
- DMA 마스크 먼저 설정 —
dma_set_mask_and_coherent()를 probe 초기에 호출 - 방향 일치 — map과 unmap의 direction이 반드시 동일해야 함
- 크기 일치 — map과 unmap의 size가 반드시 동일해야 함
- 매핑 중 CPU 접근 금지 — streaming 매핑 후 sync 없이 CPU가 접근하면 stale data
- kmalloc 사용 — streaming DMA에 stack/global 변수 사용 금지 (캐시라인 공유 문제)
- SG unmap에 원본 nents 전달 —
dma_unmap_sg()에는mapped_nents가 아닌 원본nents전달 - coherent 크기 제한 —
dma_alloc_coherent()는 수 MB 이내, 대용량은 CMA 확인 - 바운스 버퍼 성능 저하 — SWIOTLB 사용 시 이중 복사, dmesg로 경고 확인
- IOMMU 그룹 인식 — VFIO passthrough 시 그룹 단위 할당 필수
실전 예제
네트워크 드라이버 — Ring Buffer DMA
고성능 NIC 드라이버의 전형적인 DMA 패턴입니다:
struct my_ring {
struct my_desc *desc; /* coherent: 디스크립터 링 */
dma_addr_t desc_dma; /* 디스크립터 링 DMA 주소 */
struct my_buf *buf; /* 패킷 버퍼 메타 정보 */
int count;
};
/* 초기화: 디스크립터 링은 coherent 할당 */
static int my_ring_alloc(struct device *dev, struct my_ring *ring)
{
size_t desc_size = ring->count * sizeof(struct my_desc);
ring->desc = dma_alloc_coherent(dev, desc_size,
&ring->desc_dma, GFP_KERNEL);
if (!ring->desc)
return -ENOMEM;
ring->buf = kcalloc(ring->count, sizeof(*ring->buf), GFP_KERNEL);
if (!ring->buf) {
dma_free_coherent(dev, desc_size, ring->desc, ring->desc_dma);
return -ENOMEM;
}
return 0;
}
/* 수신: 패킷 버퍼는 streaming 매핑 */
static int my_rx_alloc_buf(struct device *dev, struct my_ring *ring, int idx)
{
struct page *page = alloc_page(GFP_KERNEL);
dma_addr_t dma;
if (!page)
return -ENOMEM;
dma = dma_map_page(dev, page, 0, PAGE_SIZE, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma)) {
__free_page(page);
return -EIO;
}
ring->buf[idx].page = page;
ring->buf[idx].dma = dma;
/* coherent 디스크립터에 DMA 주소 기록 */
ring->desc[idx].addr = cpu_to_le64(dma);
ring->desc[idx].len = cpu_to_le16(PAGE_SIZE);
return 0;
}
/* NAPI 수신 처리 */
static int my_rx_poll(struct napi_struct *napi, int budget)
{
struct my_ring *ring = container_of(napi, ...);
int processed = 0;
while (processed < budget) {
struct my_desc *desc = &ring->desc[ring->next];
if (!(desc->status & DESC_DONE))
break;
/* CPU가 읽기 전 sync */
dma_sync_single_for_cpu(dev, ring->buf[ring->next].dma,
PAGE_SIZE, DMA_FROM_DEVICE);
/* 패킷 데이터 처리 후 디바이스에 반환 */
dma_sync_single_for_device(dev, ring->buf[ring->next].dma,
PAGE_SIZE, DMA_FROM_DEVICE);
processed++;
}
return processed;
}
블록 디바이스 — Scatter-Gather DMA
static blk_status_t my_blk_submit(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
struct scatterlist *sg;
struct req_iterator iter;
struct bio_vec bvec;
int nents = 0, mapped;
/* bio에서 SG 리스트 구성 */
sg_init_table(sg_list, MAX_SG);
rq_for_each_segment(bvec, rq, iter) {
sg_set_page(&sg_list[nents], bvec.bv_page,
bvec.bv_len, bvec.bv_offset);
nents++;
}
/* DMA 매핑 */
mapped = dma_map_sg(dev, sg_list, nents,
rq_data_dir(rq) == WRITE ?
DMA_TO_DEVICE : DMA_FROM_DEVICE);
if (!mapped)
return BLK_STS_IOERR;
/* 하드웨어 SG 디스크립터 프로그래밍 */
for_each_sg(sg_list, sg, mapped, i) {
hw_desc[i].addr = sg_dma_address(sg);
hw_desc[i].len = sg_dma_len(sg);
}
start_hw_transfer(hw_desc, mapped);
return BLK_STS_OK;
}
SPI + DMA Engine — 임베디드 전송
SPI 마스터 드라이버가 DMA Engine을 사용하여 대용량 SPI 전송을 수행하는 패턴입니다.
static int my_spi_transfer_dma(struct spi_master *master,
struct spi_transfer *xfer)
{
struct my_spi *priv = spi_master_get_devdata(master);
struct dma_async_tx_descriptor *desc;
dma_cookie_t cookie;
/* TX DMA 설정 */
struct dma_slave_config tx_cfg = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = priv->phys_base + SPI_TX_FIFO,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.dst_maxburst = priv->fifo_depth / 2,
};
dmaengine_slave_config(priv->tx_chan, &tx_cfg);
/* SG 리스트로 전송 준비 */
sg_init_one(&priv->tx_sg, xfer->tx_buf, xfer->len);
dma_handle = dma_map_sg(priv->dev, &priv->tx_sg, 1, DMA_TO_DEVICE);
desc = dmaengine_prep_slave_sg(priv->tx_chan, &priv->tx_sg, 1,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
desc->callback = my_spi_dma_done;
desc->callback_param = priv;
cookie = dmaengine_submit(desc);
dma_async_issue_pending(priv->tx_chan);
/* SPI 하드웨어에 DMA 모드 활성화 */
writel(SPI_DMA_TX_EN, priv->regs + SPI_DMA_CTRL);
return 0;
}
디바이스 probe — DMA 설정 패턴
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct device *dev = &pdev->dev;
int ret;
/* ① PCI 활성화 + 버스마스터 설정 */
ret = pcim_enable_device(pdev);
if (ret)
return ret;
pci_set_master(pdev);
/* ② DMA 마스크 설정 */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret)
return ret;
}
/* ③ Coherent DMA 할당 (디스크립터 링) */
priv->desc_ring = dma_alloc_coherent(dev, DESC_RING_SIZE,
&priv->desc_dma, GFP_KERNEL);
if (!priv->desc_ring)
return -ENOMEM;
/* ④ DMA Pool 생성 (소형 명령 버퍼) */
priv->cmd_pool = dma_pool_create("my_cmd", dev, 64, 64, 0);
if (!priv->cmd_pool) {
dma_free_coherent(dev, DESC_RING_SIZE,
priv->desc_ring, priv->desc_dma);
return -ENOMEM;
}
/* ⑤ 디바이스 초기화 및 인터럽트 등록 */
...
return 0;
}
제로카피와 DMA
제로카피(Zero-Copy)는 CPU가 데이터를 복사하지 않고 DMA만으로 데이터를 전달하는 기법입니다. 네트워킹과 스토리지에서 CPU 사용률을 극적으로 줄입니다.
제로카피 기법 비교
| 기법 | 경로 | 복사 횟수 | DMA 역할 |
|---|---|---|---|
| 일반 read/write | 디바이스→커널버퍼→유저버퍼→커널버퍼→디바이스 | 4 (DMA×2 + CPU×2) | 양 끝단 |
| sendfile() | 디바이스→커널버퍼→디바이스 | 2 (DMA×2, CPU×0) | 양 끝단 직접 |
| splice() | 파이프로 페이지 이동 (복사 없음) | 2 (DMA×2, CPU×0) | 양 끝단 직접 |
| MSG_ZEROCOPY | 유저 페이지를 직접 DMA 매핑 | 1 (DMA×1) | 유저 메모리 직접 |
| io_uring + fixed buf | 사전 등록 버퍼 재사용 | 1 (DMA×1) | 고정 매핑 재사용 |
sendfile() / splice() DMA 경로
/* sendfile(): 디스크 → NIC 제로카피 */
/* 커널 내부 경로: */
/* 1. do_sendfile() → splice_file_to_pipe() */
/* 2. Page Cache 페이지를 파이프에 참조 추가 (복사 없음) */
/* 3. pipe_to_sendpage() → tcp_sendpage() */
/* 4. Page Cache 페이지를 sk_buff의 frag로 등록 */
/* 5. NIC가 SG DMA로 Page Cache 페이지를 직접 전송 */
/* 유저스페이스 사용 */
off_t offset = 0;
sendfile(sock_fd, file_fd, &offset, file_size);
/* NIC 요구사항: NETIF_F_SG + NETIF_F_HIGHDMA 필요 */
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
MSG_ZEROCOPY 송신
/* 소켓 옵션 활성화 */
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &val, sizeof(val));
/* 유저 버퍼를 직접 DMA로 전송 */
send(fd, user_buf, len, MSG_ZEROCOPY);
/* NIC DMA 완료 시 errqueue로 통보 → 유저 버퍼 해제 가능 시점 알림 */
struct msghdr msg = {};
struct cmsghdr *cm;
char cbuf[64];
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
recvmsg(fd, &msg, MSG_ERRQUEUE);
cm = CMSG_FIRSTHDR(&msg);
/* cm->cmsg_type == SO_EE_ORIGIN_ZEROCOPY → 완료 통보 */
DMA 완료 통보 메커니즘
DMA 전송 완료를 CPU에 알리는 방법은 성능과 지연 시간의 핵심 요소입니다.
완료 통보 방식
| 방식 | 지연 | CPU 사용 | 처리량 | 사용처 |
|---|---|---|---|---|
| 인터럽트 | ~μs | 낮음 (대기 중 sleep) | 보통 | 일반 디바이스 |
| 폴링 | ~100ns | 높음 (busy-wait) | 높음 | NVMe 폴링, DPDK |
| 인터럽트 병합 | ~10-100μs | 매우 낮음 | 매우 높음 | 고성능 NIC |
| 하이브리드 | 적응형 | 적응형 | 최적 | NAPI, NVMe 적응형 |
/* 인터럽트 방식 — 기본 패턴 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_dev *priv = data;
u32 status = readl(priv->regs + IRQ_STATUS);
if (status & DMA_COMPLETE) {
writel(DMA_COMPLETE, priv->regs + IRQ_ACK);
dma_unmap_single(priv->dev, priv->dma_handle,
priv->len, DMA_FROM_DEVICE);
complete(&priv->done); /* 대기 중인 스레드 깨움 */
return IRQ_HANDLED;
}
return IRQ_NONE;
}
/* 폴링 방식 — NVMe io_poll */
while (!nvme_cqe_pending(cq)) {
cpu_relax(); /* hint: 다른 하이퍼스레드에 양보 */
}
dma_rmb(); /* CQE 데이터 읽기 전 배리어 */
process_cqe(cq);
/* 인터럽트 병합 (NIC ethtool 설정) */
$ ethtool -C eth0 rx-usecs 50 rx-frames 64
/* 50μs 또는 64프레임마다 한 번 인터럽트 */
커널 설정 종합 (Kconfig)
DMA 관련 Kconfig 옵션
| 옵션 | 기본값 | 역할 |
|---|---|---|
CONFIG_HAS_DMA | y (자동) | DMA 지원 가능 플랫폼 |
CONFIG_DMA_API_DEBUG | n | DMA API 오용 런타임 탐지 |
CONFIG_DMA_API_DEBUG_SG | n | SG 매핑 추가 검증 |
CONFIG_SWIOTLB | y (x86_64) | SWIOTLB 바운스 버퍼 |
CONFIG_DMA_CMA | y (ARM) | CMA 기반 DMA 할당 |
CONFIG_CMA_SIZE_MBYTES | 0~256 | 기본 CMA 크기 (MB) |
CONFIG_IOMMU_SUPPORT | y | IOMMU 프레임워크 |
CONFIG_IOMMU_DEFAULT_DMA_LAZY | y | IOMMU 기본 lazy 모드 |
CONFIG_IOMMU_DEFAULT_PASSTHROUGH | n | IOMMU 기본 passthrough 모드 |
CONFIG_INTEL_IOMMU | y (Intel) | Intel VT-d 드라이버 |
CONFIG_AMD_IOMMU | y (AMD) | AMD-Vi 드라이버 |
CONFIG_ARM_SMMU_V3 | y (ARM64) | ARM SMMUv3 드라이버 |
CONFIG_IOMMU_SVA | n | SVA(Shared Virtual Addressing) 지원 |
CONFIG_DMA_SHARED_BUFFER | y | DMA-BUF 프레임워크 |
CONFIG_DMABUF_HEAPS | n | DMA-BUF Heaps 유저스페이스 할당 |
CONFIG_DMABUF_HEAPS_SYSTEM | n | System heap |
CONFIG_DMABUF_HEAPS_CMA | n | CMA heap |
CONFIG_PCI_P2PDMA | n | PCIe P2P DMA 지원 |
CONFIG_DMA_RESTRICTED_POOL | n | Restricted DMA 보안 풀 |
CONFIG_DMADEVICES | n | DMA Engine 프레임워크 활성화 |
CONFIG_DMA_ENGINE | n | DMA Engine 핵심 (DMADEVICES 하위) |
CONFIG_DMA_VIRTUAL_CHANNELS | n | 가상 DMA 채널 지원 |
CONFIG_ASYNC_TX_DMA | n | 비동기 TX DMA 오프로드 (RAID XOR 등) |
CONFIG_VFIO | n | VFIO (디바이스 passthrough) 프레임워크 |
CONFIG_VFIO_PCI | n | PCI 디바이스 VFIO 드라이버 |
CONFIG_IOMMU_DMA | y | IOMMU 기반 DMA API 백엔드 |
CONFIG_IOMMU_IOVA | y | IOVA 할당자 |
CONFIG_VIRTIO_IOMMU | n | virtio IOMMU (가상화) |
권장 설정 프로필
| 사용 시나리오 | 필수 설정 | 권장 설정 |
|---|---|---|
| 서버 (보안) | IOMMU_SUPPORT, INTEL_IOMMU/AMD_IOMMU | DMA_API_DEBUG, iommu=strict |
| 서버 (성능) | IOMMU_SUPPORT | IOMMU_DEFAULT_DMA_LAZY, PCI_P2PDMA |
| 데스크탑 | IOMMU_SUPPORT | IOMMU_DEFAULT_DMA_LAZY |
| 임베디드 (ARM) | DMA_CMA, ARM_SMMU_V3 | CMA_SIZE_MBYTES=64~256 |
| 가상화 호스트 | IOMMU_SUPPORT, VFIO, VFIO_PCI | IOMMU_SVA (PASID 필요 시) |
| 기밀 컴퓨팅 | SWIOTLB, AMD_MEM_ENCRYPT/INTEL_TDX | swiotlb=force, 크기 증가 |
DMA API 빠른 참조
가장 자주 사용되는 DMA API를 기능별로 정리합니다.
할당/해제 API
| API | 용도 | 반환값 |
|---|---|---|
dma_alloc_coherent(dev, size, &handle, gfp) | Coherent DMA 버퍼 할당 | CPU VA (NULL=실패) |
dma_free_coherent(dev, size, va, handle) | Coherent DMA 버퍼 해제 | void |
dma_alloc_wc(dev, size, &handle, gfp) | Write-Combine DMA 버퍼 | CPU VA |
dma_alloc_noncoherent(dev, size, &handle, dir, gfp) | Non-coherent DMA 버퍼 | CPU VA |
dma_alloc_pages(dev, size, &handle, dir, gfp) | DMA 페이지 할당 (커널 VA 없음) | struct page * |
dmam_alloc_coherent(dev, size, &handle, gfp) | Managed coherent (자동 해제) | CPU VA |
dma_pool_create(name, dev, size, align, boundary) | DMA 풀 생성 | pool * |
dma_pool_alloc(pool, gfp, &handle) | 풀에서 블록 할당 | CPU VA |
매핑/해제 API
| API | 용도 | 반환값 |
|---|---|---|
dma_map_single(dev, va, size, dir) | 단일 버퍼 매핑 | dma_addr_t |
dma_unmap_single(dev, handle, size, dir) | 단일 버퍼 해제 | void |
dma_map_page(dev, page, offset, size, dir) | 페이지 매핑 | dma_addr_t |
dma_map_sg(dev, sg, nents, dir) | SG 리스트 매핑 | mapped_nents (0=실패) |
dma_unmap_sg(dev, sg, nents, dir) | SG 리스트 해제 (원본 nents!) | void |
dma_map_sgtable(dev, &sgt, dir, attrs) | SG 테이블 매핑 (최신) | 0=성공, 음수=errno |
dma_map_resource(dev, phys, size, dir, attrs) | MMIO 주소 매핑 | dma_addr_t |
dma_mapping_error(dev, handle) | 매핑 오류 확인 (필수!) | bool |
동기화 API
| API | 용도 |
|---|---|
dma_sync_single_for_cpu(dev, handle, size, dir) | 디바이스→CPU 소유권 전환 |
dma_sync_single_for_device(dev, handle, size, dir) | CPU→디바이스 소유권 전환 |
dma_sync_single_range_for_cpu(dev, handle, offset, size, dir) | 부분 범위 CPU sync |
dma_sync_sg_for_cpu(dev, sg, nents, dir) | SG 리스트 CPU sync |
dma_sync_sg_for_device(dev, sg, nents, dir) | SG 리스트 디바이스 sync |
dma_sync_sgtable_for_cpu(dev, &sgt, dir) | SG 테이블 CPU sync (최신) |
마스크/제한 API
| API | 용도 |
|---|---|
dma_set_mask_and_coherent(dev, mask) | DMA 주소 마스크 설정 (streaming + coherent) |
dma_set_mask(dev, mask) | Streaming 매핑용 마스크만 |
dma_set_coherent_mask(dev, mask) | Coherent 할당용 마스크만 |
dma_get_required_mask(dev) | 디바이스에 필요한 최소 마스크 조회 |
dma_max_mapping_size(dev) | 단일 매핑 최대 크기 (SWIOTLB 제한 포함) |
dma_set_max_seg_size(dev, size) | 최대 세그먼트 크기 설정 |
관련 문서
DMA와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.