Scatter/Gather I/O 심화
Scatter/Gather I/O는 물리적으로 비연속적인 메모리 영역을 하나의 논리적 전송 단위로 묶어
DMA 전송, 블록 I/O, 네트워크 패킷 처리, 사용자 공간 벡터 I/O 등 리눅스 커널 전반에서
핵심적으로 활용되는 메커니즘입니다. scatterlist 구조체와 sg_table
관리 API부터 DMA 매핑, IOMMU 통합, 블록 계층 bio_vec, 네트워크 스택
skb_frag_t, NVMe SGL/PRP, readv/writev, splice 제로 카피,
Crypto API 연동, 드라이버 구현 패턴, 성능 최적화와 디버깅까지 전 영역을 상세히 다룹니다.
핵심 요약
- Scatter/Gather -- 비연속 메모리 조각들을 하나의 논리적 버퍼로 묶어 한 번의 DMA 전송으로 처리하는 기법
- scatterlist -- 페이지, 오프셋, 길이로 메모리 조각 하나를 기술하는 커널 구조체
- sg_table -- scatterlist 배열과 메타데이터를 관리하는 컨테이너 구조체
- DMA 매핑 --
dma_map_sg()로 scatterlist를 디바이스가 접근 가능한 DMA 주소로 변환 - IOMMU 병합 -- 물리적으로 비연속인 여러 세그먼트를 IOMMU가 하나의 연속 DMA 주소로 병합 가능
단계별 이해
- 메모리 단편화 인식
커널이 오래 실행되면 연속된 큰 메모리를 할당하기 어렵습니다. 대신 흩어진 페이지들을 모아서 사용합니다. - SG 리스트 구성
sg_alloc_table()로 테이블을 할당하고, 각 엔트리에 페이지와 오프셋, 길이를 설정합니다. - DMA 매핑 수행
dma_map_sgtable()으로 모든 엔트리를 한 번에 DMA 주소로 변환합니다. IOMMU가 있으면 인접한 엔트리를 병합합니다. - 디바이스 전송
디바이스에 DMA 주소 목록(디스크립터 링 등)을 전달하면, 디바이스가 독립적으로 모든 조각을 순회하며 데이터를 전송합니다. - 매핑 해제 및 정리
전송 완료 후dma_unmap_sgtable()로 매핑을 해제하고,sg_free_table()로 테이블을 반환합니다.
Scatter/Gather 개요
Scatter/Gather I/O(이하 SG I/O)는 물리적으로 연속되지 않은 여러 메모리 영역을 단일 I/O 작업으로 처리하는 기법입니다. "Scatter"는 하나의 데이터 소스를 여러 대상 버퍼에 분산하여 기록하는 것이고, "Gather"는 여러 소스 버퍼의 데이터를 모아 하나의 대상으로 전송하는 것입니다.
왜 Scatter/Gather가 필요한가
운영체제가 오래 실행되면 물리 메모리는 점차 단편화됩니다. 대규모 연속 메모리 할당은 실패할 확률이 높아지므로, 커널은 흩어진 페이지 단위의 메모리를 활용해야 합니다. 만약 SG I/O가 없다면 다음과 같은 비효율이 발생합니다:
| 방식 | 문제점 | 성능 영향 |
|---|---|---|
| 바운스 버퍼 복사 | 비연속 페이지를 연속 버퍼에 복사 후 DMA | CPU 시간 낭비, 메모리 대역폭 2배 소모 |
| 페이지별 개별 DMA | 각 페이지마다 별도 DMA 요청 발행 | DMA 설정 오버헤드 N배, 인터럽트 폭풍 |
| 연속 메모리 강제 할당 | CMA/대형 order 할당 시도 | 단편화 시 할당 실패, OOM 위험 |
| Scatter/Gather DMA | 비연속 페이지 목록을 하드웨어에 전달 | 복사 없음, 단일 DMA 전송, 최소 오버헤드 |
SG I/O가 활용되는 커널 하위 시스템
| 서브시스템 | SG 표현 | 핵심 구조체 | 용도 |
|---|---|---|---|
| DMA 매핑 계층 | scatterlist / sg_table |
struct scatterlist |
디바이스 DMA 주소 매핑 |
| 블록 I/O | bio_vec |
struct bio |
디스크 I/O 요청의 페이지 벡터 |
| 네트워크 | skb_frag_t |
struct sk_buff |
패킷 paged data, GSO/GRO |
| NVMe | SGL / PRP | struct nvme_sgl_desc |
NVMe 명령 데이터 전송 |
| 사용자 공간 벡터 I/O | iovec / iov_iter |
struct iov_iter |
readv/writev, splice, sendmsg |
| Crypto API | scatterlist |
struct scatterlist |
암호화/해시 입출력 버퍼 |
| DRM/GPU | sg_table |
struct sg_table |
GEM 객체 페이지 매핑 |
커널 소스 트리 위치
| 경로 | 역할 |
|---|---|
include/linux/scatterlist.h | scatterlist, sg_table 정의 및 인라인 헬퍼 |
lib/scatterlist.c | sg_alloc_table, sg_free_table, sg_copy 등 구현 |
include/linux/dma-mapping.h | dma_map_sg, dma_unmap_sg 등 DMA SG API |
kernel/dma/mapping.c | DMA 매핑 코어 구현 |
include/linux/uio.h | iovec, iov_iter 정의 |
lib/iov_iter.c | iov_iter 순회/복사 구현 |
include/linux/bio.h | bio_vec, bio 구조체 정의 |
fs/splice.c | splice, tee, vmsplice 구현 |
핵심 개념: 비연속 메모리 전송
리눅스 커널에서 물리 메모리는 페이지(일반적으로 4KB) 단위로 관리됩니다. 버디 할당자(buddy allocator)가 연속된 물리 페이지를 반환하지만, 시스템이 오래 실행될수록 고차(high-order) 할당은 점점 어려워집니다. Scatter/Gather는 이 문제를 근본적으로 해결합니다.
물리 메모리 단편화와 SG 전송
64KB 데이터를 디바이스에 전송해야 하는 상황을 가정합니다. 물리 메모리가 단편화되어 16개의 연속 페이지(order-4)를 얻을 수 없다면, 커널은 4KB 페이지 16개를 개별적으로 할당합니다. 이 16개 페이지는 물리 주소가 비연속적이지만, SG 리스트로 묶으면 하드웨어가 한 번의 DMA 작업으로 모두 전송할 수 있습니다.
연속 vs 비연속 메모리 DMA 비교
| 특성 | 연속 메모리 DMA | Scatter/Gather DMA |
|---|---|---|
| 메모리 요구 | 물리적 연속 버퍼 필수 | 비연속 페이지 허용 |
| 할당 실패 확률 | 높음 (고차 할당) | 매우 낮음 (페이지 단위) |
| DMA API | dma_map_single() |
dma_map_sg() |
| 하드웨어 요구 | 기본 DMA | SG 지원 DMA 엔진 |
| 전송 효율 | 단일 전송 | 단일 전송 (SG 목록 전체) |
| CPU 복사 | 바운스 버퍼 시 필요 | 불필요 (제로 카피) |
| IOMMU 활용 | 단일 매핑 | 병합 매핑으로 세그먼트 수 감소 |
Gather 방향과 Scatter 방향
용어 사용에 주의가 필요합니다. "Gather"는 여러 소스로부터 데이터를 수집하는 방향이고,
"Scatter"는 하나의 소스에서 여러 대상으로 분배하는 방향입니다.
DMA에서는 양 방향 모두 같은 scatterlist 구조체를 사용하며,
DMA_TO_DEVICE(Gather)와 DMA_FROM_DEVICE(Scatter)로 방향만 구분합니다.
scatterlist 구조체
struct scatterlist은 리눅스 커널에서 SG I/O의 기본 빌딩 블록입니다.
각 엔트리는 하나의 메모리 조각(페이지 + 오프셋 + 길이)을 기술하며,
DMA 매핑 후에는 DMA 주소와 DMA 길이 필드가 추가로 설정됩니다.
구조체 정의
/* include/linux/scatterlist.h */
struct scatterlist {
unsigned long page_link; /* 페이지 포인터 + 플래그 (하위 2비트) */
unsigned int offset; /* 페이지 내 시작 오프셋 */
unsigned int length; /* 바이트 단위 데이터 길이 */
dma_addr_t dma_address; /* DMA 매핑 후 디바이스 주소 */
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length; /* DMA 매핑 후 길이 (병합 시 length와 다름) */
#endif
#ifdef CONFIG_NEED_SG_DMA_FLAGS
unsigned int dma_flags; /* DMA 플래그 (6.0+) */
#endif
};
코드 설명
-
3행
page_link는struct page *포인터의 상위 비트와 하위 2비트 플래그를 합친 값입니다. 비트 0(SG_CHAIN)은 체이닝 마커, 비트 1(SG_END)은 리스트 종료 마커로 사용됩니다. -
4행
페이지 내에서 데이터가 시작되는 바이트 오프셋입니다. 0부터
PAGE_SIZE - 1까지 가능합니다. - 5행 이 scatterlist 엔트리가 기술하는 데이터의 바이트 길이입니다. 페이지 경계를 넘어갈 수 없습니다(단일 페이지 기준).
-
6행
dma_map_sg()호출 후 설정되는 디바이스가 볼 수 있는 DMA 버스 주소입니다. -
8행
IOMMU가 여러 SG 엔트리를 병합하면 DMA 길이가 원래
length와 달라지므로 별도 필드가 필요합니다.
page_link 필드의 비트 레이아웃
page_link 필드는 단순 포인터가 아니라, 하위 2비트를 플래그로 활용합니다.
struct page는 최소 4바이트 정렬이므로 하위 2비트가 항상 0이라는 점을 이용합니다.
| 비트 | 매크로 | 값 | 의미 |
|---|---|---|---|
| 비트 0 | SG_CHAIN |
0x01 |
이 엔트리는 다음 scatterlist 배열을 가리키는 체인 포인터 |
| 비트 1 | SG_END |
0x02 |
이 엔트리가 scatterlist의 마지막 엔트리 |
| 비트 2~63 | - | - | 실제 struct page * 포인터 (마스킹으로 추출) |
체이닝 메커니즘
커널은 대규모 SG 리스트를 위해 체이닝(chaining)을 지원합니다.
하나의 scatterlist 배열이 가득 차면 마지막 엔트리에 SG_CHAIN 플래그를 설정하고
page_link에 다음 scatterlist 배열의 주소를 저장합니다.
이를 통해 연결 리스트처럼 여러 배열을 이어 붙일 수 있으며, SG_ALLOC_SIZE
(기본 PAGE_SIZE) 크기의 배열을 여러 개 연결하여 수천 개의 엔트리를 관리합니다.
핵심 인라인 헬퍼 함수
/* 페이지 포인터 추출 (하위 2비트 마스킹) */
static inline struct page *sg_page(struct scatterlist *sg)
{
return (struct page *)((sg->page_link) & ~(0x3));
}
/* 페이지 설정 */
static inline void sg_set_page(struct scatterlist *sg,
struct page *page,
unsigned int len,
unsigned int offset)
{
sg->page_link = (unsigned long)page | (sg->page_link & 0x3);
sg->offset = offset;
sg->length = len;
}
/* 커널 가상 주소로 직접 설정 */
static inline void sg_set_buf(struct scatterlist *sg,
const void *buf,
unsigned int buflen)
{
sg_set_page(sg, virt_to_page(buf), buflen,
offset_in_page(buf));
}
/* 가상 주소 변환 */
static inline void *sg_virt(struct scatterlist *sg)
{
return page_address(sg_page(sg)) + sg->offset;
}
/* DMA 주소 접근 */
#define sg_dma_address(sg) ((sg)->dma_address)
#define sg_dma_len(sg) ((sg)->dma_length)
코드 설명
-
2-5행
sg_page()는page_link의 하위 2비트를 마스킹하여 실제struct page *포인터를 추출합니다. -
8-15행
sg_set_page()는 기존 플래그(SG_CHAIN, SG_END)를 보존하면서 페이지, 길이, 오프셋을 설정합니다. -
18-23행
sg_set_buf()는 커널 가상 주소를 페이지+오프셋으로 변환하여 설정하는 편의 함수입니다. kmalloc 버퍼 등에 유용합니다. -
26-29행
sg_virt()는 scatterlist에서 커널 가상 주소를 역으로 계산합니다. lowmem 페이지에서만 안전합니다. -
32-33행
sg_dma_address()와sg_dma_len()은 DMA 매핑 후에만 유효한 값을 반환합니다. 매핑 전에는 사용하면 안 됩니다.
sg_table과 SG 관리 API
struct sg_table은 scatterlist 배열과 관련 메타데이터를 하나로 묶는 컨테이너입니다.
체이닝된 여러 scatterlist 배열을 투명하게 관리하며, DMA 매핑 전후의 엔트리 수를 별도로 추적합니다.
sg_table 구조체
/* include/linux/scatterlist.h */
struct sg_table {
struct scatterlist *sgl; /* 첫 번째 scatterlist 엔트리 포인터 */
unsigned int nents; /* 실제 scatterlist 엔트리 수 */
unsigned int orig_nents;/* 원래 엔트리 수 (체인 포함하기 전) */
};
코드 설명
-
3행
sgl은 체이닝된 scatterlist의 첫 번째 엔트리를 가리킵니다. 이 포인터부터 순회를 시작합니다. -
4행
nents는 DMA 매핑 후 실제 DMA 세그먼트 수입니다. IOMMU 병합으로orig_nents보다 작을 수 있습니다. -
5행
orig_nents는 DMA 매핑 전 원래 scatterlist 엔트리 수입니다.dma_unmap_sg()호출 시 이 값을 전달합니다.
SG 관리 API 총정리
| 함수 | 용도 | 반환/효과 |
|---|---|---|
sg_alloc_table(sgt, nents, gfp) |
sg_table 할당 및 초기화 | 0 성공, 음수 오류. 체이닝 자동 처리 |
sg_free_table(sgt) |
sg_table 해제 | 체이닝된 모든 배열 해제 |
sg_alloc_table_from_pages(sgt, pages, n, off, size, max_seg, gfp) |
페이지 배열로부터 sg_table 생성 | 인접 페이지 자동 병합, 최대 세그먼트 크기 제한 |
sg_init_table(sgl, nents) |
정적 scatterlist 배열 초기화 | 모든 엔트리 0으로 초기화, 마지막에 SG_END 설정 |
sg_init_one(sgl, buf, buflen) |
단일 버퍼용 scatterlist 초기화 | 1개 엔트리의 scatterlist 설정 |
sg_set_page(sg, page, len, off) |
개별 엔트리에 페이지 설정 | 플래그 보존하면서 page/len/off 설정 |
sg_set_buf(sg, buf, buflen) |
개별 엔트리에 가상 주소 설정 | virt_to_page + offset_in_page로 변환 |
sg_mark_end(sg) |
엔트리를 마지막으로 표시 | SG_END 비트 설정 |
sg_unmark_end(sg) |
SG_END 표시 제거 | 리스트 확장 시 사용 |
SG 테이블 생성 전체 예제
/* 4개 페이지로 구성된 SG 테이블 생성 예제 */
struct sg_table sgt;
struct scatterlist *sg;
struct page *pages[4];
int i, ret;
/* 1. 페이지 할당 */
for (i = 0; i < 4; i++) {
pages[i] = alloc_page(GFP_KERNEL);
if (!pages[i])
goto err_free_pages;
}
/* 2. SG 테이블 할당 (4개 엔트리) */
ret = sg_alloc_table(&sgt, 4, GFP_KERNEL);
if (ret)
goto err_free_pages;
/* 3. 각 엔트리에 페이지 설정 */
for_each_sgtable_sg(&sgt, sg, i)
sg_set_page(sg, pages[i], PAGE_SIZE, 0);
/* 4. DMA 매핑 */
ret = dma_map_sgtable(dev, &sgt, DMA_TO_DEVICE, 0);
if (ret)
goto err_free_sgt;
/* 5. DMA 전송 수행... */
for_each_sgtable_dma_sg(&sgt, sg, i) {
dma_addr_t addr = sg_dma_address(sg);
unsigned int len = sg_dma_len(sg);
/* 디바이스 디스크립터에 addr, len 설정 */
setup_dma_descriptor(desc++, addr, len);
}
/* 6. 정리 */
dma_unmap_sgtable(dev, &sgt, DMA_TO_DEVICE, 0);
err_free_sgt:
sg_free_table(&sgt);
err_free_pages:
for (i = 0; i < 4; i++)
if (pages[i])
__free_page(pages[i]);
코드 설명
- 8-11행 4개의 개별 페이지를 할당합니다. 각 페이지는 물리적으로 비연속일 수 있습니다.
-
15행
sg_alloc_table()이 4개 엔트리의 scatterlist 배열을 할당하고 체이닝/종료 마커를 자동 설정합니다. -
20-21행
for_each_sgtable_sg()로 원본 SG 엔트리를 순회하며 각각에 페이지를 설정합니다. -
24행
dma_map_sgtable()은 모든 엔트리를 한 번에 DMA 매핑합니다. IOMMU가 인접 엔트리를 병합할 수 있어 DMA 세그먼트 수가 줄어들 수 있습니다. -
29행
for_each_sgtable_dma_sg()는 DMA 매핑 후의 세그먼트를 순회합니다. 병합이 발생하면 원본보다 엔트리 수가 적습니다. -
37행
DMA 매핑 해제 후
sg_free_table()로 SG 테이블을 해제하고, 페이지도 개별 반환합니다.
sg_alloc_table_from_pages 활용
/* 페이지 배열로부터 최적화된 SG 테이블 생성 */
struct sg_table sgt;
struct page **pages;
int num_pages = 256; /* 1MB (256 x 4KB) */
int ret;
pages = kvmalloc_array(num_pages, sizeof(*pages), GFP_KERNEL);
if (!pages)
return -ENOMEM;
/* 페이지 할당 (예: pin_user_pages 등) */
for (int i = 0; i < num_pages; i++)
pages[i] = alloc_page(GFP_KERNEL);
/* 인접한 물리 페이지를 자동으로 병합하여 SG 엔트리 최소화 */
ret = sg_alloc_table_from_pages(&sgt, pages, num_pages,
0, /* 시작 오프셋 */
(size_t)num_pages << PAGE_SHIFT,
GFP_KERNEL);
if (ret) {
pr_err("SG table alloc failed: %d\n", ret);
goto err;
}
/* 결과: 256 페이지가 물리적으로 연속이면 엔트리 1개,
완전히 분산되면 최대 256개 엔트리 */
pr_info("orig_nents=%u (최대 %d에서 병합)\n",
sgt.orig_nents, num_pages);
코드 설명
-
16행
sg_alloc_table_from_pages()는 인접한 물리 페이지를 자동 감지하여 하나의 SG 엔트리로 병합합니다. 256개 페이지가 전부 연속이면 엔트리 1개만 생성됩니다. -
27행
orig_nents는 병합 후의 실제 SG 엔트리 수를 반영합니다. 물리 메모리 단편화 수준에 따라 달라집니다.
SG 리스트 순회
scatterlist 순회에는 여러 매크로가 제공됩니다.
커널 5.x 이후에는 for_each_sgtable_* 계열을 권장하며,
이전의 for_each_sg()도 여전히 사용 가능합니다.
순회 매크로 비교
| 매크로 | 순회 대상 | 용도 |
|---|---|---|
for_each_sg(sglist, sg, nents, i) |
원본 SG 엔트리 | 레거시: 직접 sglist와 nents를 전달 |
for_each_sgtable_sg(sgt, sg, i) |
원본 SG 엔트리 | sg_table 기반: sgt->sgl, sgt->orig_nents 사용 |
for_each_sgtable_dma_sg(sgt, sg, i) |
DMA 매핑된 세그먼트 | DMA 매핑 후: sgt->sgl, sgt->nents 사용 |
for_each_sgtable_page(sgt, piter, i) |
개별 페이지 | 각 SG 엔트리를 페이지 단위로 분해하여 순회 |
for_each_sgtable_dma_page(sgt, diter, i) |
DMA 매핑된 페이지 | DMA 세그먼트를 페이지 단위로 분해하여 순회 |
순회 매크로 내부 구현
/* for_each_sg: 기본 순회 매크로 */
#define for_each_sg(sglist, sg, nr, __i) \
for (__i = 0, sg = (sglist); __i < (nr); __i++, sg = sg_next(sg))
/* sg_next: 체이닝을 투명하게 처리하는 다음 엔트리 반환 */
static inline struct scatterlist *sg_next(struct scatterlist *sg)
{
if (sg_is_last(sg))
return NULL;
sg++;
if (sg_is_chain(sg))
sg = sg_chain_ptr(sg); /* 체인 포인터 따라감 */
return sg;
}
/* sg_table 전용 매크로 (권장) */
#define for_each_sgtable_sg(sgt, sg, i) \
for_each_sg((sgt)->sgl, sg, (sgt)->orig_nents, i)
#define for_each_sgtable_dma_sg(sgt, sg, i) \
for_each_sg((sgt)->sgl, sg, (sgt)->nents, i)
코드 설명
-
2-3행
for_each_sg()는 매 반복마다sg_next()를 호출하여 체이닝을 투명하게 처리합니다. -
6-16행
sg_next()는 다음 엔트리로 이동할 때SG_CHAIN비트를 확인하고, 체인 엔트리면 포인터를 따라 다음 배열로 점프합니다. -
20-21행
for_each_sgtable_sg()는orig_nents를 사용하여 원본 SG 엔트리를 순회합니다. -
23-24행
for_each_sgtable_dma_sg()는nents(DMA 매핑 후 세그먼트 수)를 사용합니다. IOMMU 병합으로nents <= orig_nents입니다.
for_each_sgtable_sg()로 원본 엔트리를 순회하고,
DMA 매핑 후 디바이스 디스크립터를 설정할 때는 반드시 for_each_sgtable_dma_sg()를 사용하십시오.
for_each_sgtable_sg()로 DMA 주소를 읽으면 IOMMU 병합 시 잘못된 주소/길이를 참조합니다.
실전 순회 패턴: 디바이스 디스크립터 설정
/* DMA 매핑 후 디바이스 디스크립터 링에 SG 엔트리 기록 */
struct scatterlist *sg;
struct my_dma_desc *desc;
int i, nents;
nents = dma_map_sgtable(dev, &sgt, DMA_TO_DEVICE, 0);
if (nents < 0)
return nents;
desc = ring->next_free;
for_each_sgtable_dma_sg(&sgt, sg, i) {
desc->dma_addr = sg_dma_address(sg);
desc->dma_len = sg_dma_len(sg);
desc->flags = (i == sgt.nents - 1) ? DESC_LAST : 0;
desc = next_desc(ring, desc);
}
/* 디바이스에 전송 시작 알림 */
writel(sgt.nents, dev->regs + DOORBELL_REG);
DMA SG 매핑
DMA SG 매핑은 CPU가 보는 물리 주소를 디바이스가 접근 가능한 DMA 버스 주소로 변환하는 과정입니다. IOMMU 유무에 따라 매핑 결과가 크게 달라지며, IOMMU가 있으면 비연속 물리 페이지를 연속된 DMA 주소 공간으로 병합할 수 있습니다.
DMA SG 매핑 API
| API | 용도 | 비고 |
|---|---|---|
dma_map_sg(dev, sglist, nents, dir) |
레거시 SG 매핑 | 매핑된 DMA 세그먼트 수 반환 |
dma_unmap_sg(dev, sglist, nents, dir) |
레거시 SG 매핑 해제 | nents는 원본 엔트리 수 (dma_map_sg 반환값 아님) |
dma_map_sgtable(dev, sgt, dir, attrs) |
sg_table 기반 매핑 (권장) | sgt->nents에 DMA 세그먼트 수 저장, 오류 코드 반환 |
dma_unmap_sgtable(dev, sgt, dir, attrs) |
sg_table 기반 매핑 해제 (권장) | sgt->orig_nents를 자동 사용 |
dma_sync_sgtable_for_cpu(dev, sgt, dir) |
CPU 접근 전 동기화 | 캐시 무효화 (DMA_FROM_DEVICE) |
dma_sync_sgtable_for_device(dev, sgt, dir) |
디바이스 접근 전 동기화 | 캐시 플러시 (DMA_TO_DEVICE) |
DMA 방향 플래그
| 방향 | 의미 | 예시 |
|---|---|---|
DMA_TO_DEVICE |
메모리 → 디바이스 (Gather) | 네트워크 송신, 디스크 쓰기 |
DMA_FROM_DEVICE |
디바이스 → 메모리 (Scatter) | 네트워크 수신, 디스크 읽기 |
DMA_BIDIRECTIONAL |
양방향 | 명령 + 응답이 같은 버퍼 |
DMA_NONE |
디버깅/검증용 | 실제 DMA 전송 없음 |
dma_map_sgtable 내부 동작
/* kernel/dma/mapping.c (간략화) */
int dma_map_sgtable(struct device *dev, struct sg_table *sgt,
enum dma_data_direction dir,
unsigned long attrs)
{
int nents;
/* 백엔드별 매핑 수행 (직접/IOMMU/SWIOTLB) */
nents = dma_map_sg_attrs(dev, sgt->sgl,
sgt->orig_nents, dir, attrs);
if (nents < 0)
return nents;
if (nents == 0)
return -EIO;
sgt->nents = nents; /* DMA 세그먼트 수 저장 */
return 0;
}
dma_map_sg()/dma_unmap_sg() 대신
dma_map_sgtable()/dma_unmap_sgtable()을 사용하십시오.
sg_table 기반 API는 orig_nents/nents 관리가 자동이므로
unmap 시 잘못된 엔트리 수를 전달하는 흔한 버그를 방지합니다.
IOMMU와 SG 통합
IOMMU(Input/Output Memory Management Unit)는 디바이스의 DMA 주소를 물리 주소로 변환하는 하드웨어입니다. SG I/O에서 IOMMU의 가장 중요한 역할은 세그먼트 병합(coalescing)입니다. 물리적으로 비연속인 여러 페이지를 IOMMU 페이지 테이블에서 연속 IOVA(I/O Virtual Address)로 매핑하면, 디바이스는 하나의 연속된 DMA 주소 범위로 인식합니다.
IOMMU SG 병합 원리
| 단계 | 동작 | 결과 |
|---|---|---|
| 1. IOVA 할당 | 전체 SG 크기 합산 → IOVA 범위 할당 | 연속 IOVA 구간 확보 |
| 2. 페이지 테이블 매핑 | 각 물리 페이지를 IOVA 페이지 테이블에 순서대로 매핑 | 비연속 물리 → 연속 IOVA |
| 3. IOTLB 플러시 | IOMMU TLB 무효화하여 새 매핑 반영 | 디바이스가 새 주소 사용 가능 |
| 4. SG 엔트리 병합 | 연속 IOVA의 SG 엔트리들을 하나로 합침 | nents 감소, 디바이스 처리 효율 증가 |
IOMMU 백엔드별 특성
| IOMMU | 플랫폼 | SG 병합 | 최대 DMA 세그먼트 크기 |
|---|---|---|---|
| Intel VT-d | x86 (Intel) | 지원 | IOVA 연속이면 무제한 (디바이스 max_segment_size까지) |
| AMD-Vi | x86 (AMD) | 지원 | 동일 |
| ARM SMMU v3 | ARM64 | 지원 | 동일 |
| SWIOTLB | 모든 (IOMMU 없을 때) | 미지원 | 바운스 버퍼 크기 제한 |
| 직접 매핑 | IOMMU 비활성 | 미지원 | DMA 주소 = 물리 주소 |
SWIOTLB 바운스 버퍼와 SG
IOMMU가 없고 디바이스의 DMA 주소 마스크가 물리 메모리보다 작은 경우(예: 32비트 디바이스가 4GB 이상 메모리에 접근), SWIOTLB(Software I/O TLB)가 바운스 버퍼를 사용합니다. 이 경우 SG 엔트리마다 별도의 바운스 버퍼가 필요하며, 병합이 일어나지 않습니다. CPU가 데이터를 바운스 버퍼로 복사하므로 제로 카피 이점이 사라집니다.
CONFIG_DMA_API_DEBUG를 활성화하고 dmesg에서
"using SWIOTLB buffer" 메시지가 나타나는지 확인하십시오.
가능하면 64비트 DMA를 지원하는 디바이스를 사용하거나 IOMMU를 활성화하십시오.
블록 I/O에서의 Scatter/Gather
블록 계층은 struct bio와 struct bio_vec를 사용하여 디스크 I/O 요청을
구성합니다. bio_vec는 본질적으로 scatterlist와 동일한 역할을 하며,
하나의 bio는 여러 비연속 페이지에 걸친 I/O 요청을 표현합니다.
bio_vec 구조체
/* include/linux/bvec.h */
struct bio_vec {
struct page *bv_page; /* 페이지 포인터 */
unsigned int bv_len; /* 바이트 길이 */
unsigned int bv_offset; /* 페이지 내 오프셋 */
};
/* scatterlist와 대응 관계:
* bio_vec.bv_page ↔ scatterlist.page_link (sg_page)
* bio_vec.bv_len ↔ scatterlist.length
* bio_vec.bv_offset ↔ scatterlist.offset
*/
bio 구조체의 SG 특성
/* include/linux/bio.h (핵심 필드만) */
struct bio {
struct block_device *bi_bdev; /* 대상 블록 디바이스 */
sector_t bi_iter.bi_sector; /* 시작 섹터 */
unsigned short bi_vcnt; /* bio_vec 배열 엔트리 수 */
unsigned short bi_max_vecs; /* bio_vec 배열 최대 크기 */
struct bio_vec *bi_io_vec; /* bio_vec 배열 포인터 */
/* ... */
};
blk_rq_map_sg 변환 과정
/* 블록 드라이버에서 request → scatterlist 변환 */
static int my_blk_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
struct scatterlist sglist[MAX_SG_ENTRIES];
int nents;
sg_init_table(sglist, MAX_SG_ENTRIES);
/* bio_vec → scatterlist 변환 + 인접 세그먼트 병합 */
nents = blk_rq_map_sg(rq->q, rq, sglist);
/* DMA 매핑 */
nents = dma_map_sg(dev, sglist, nents, rq_dma_dir(rq));
if (!nents)
return BLK_STS_RESOURCE;
/* 디바이스 전송... */
return BLK_STS_OK;
}
블록 디바이스 SG 제한 파라미터
| 파라미터 | 설정 함수 | 기본값 | 의미 |
|---|---|---|---|
max_segments |
blk_queue_max_segments() |
128 | 하나의 요청에 허용되는 최대 SG 세그먼트 수 |
max_segment_size |
blk_queue_max_segment_size() |
65536 | 단일 세그먼트의 최대 바이트 크기 |
max_hw_sectors |
blk_queue_max_hw_sectors() |
255 | 하나의 요청에 허용되는 최대 섹터 수 |
seg_boundary_mask |
blk_queue_segment_boundary() |
0xFFFFFFFF | SG 세그먼트가 넘을 수 없는 주소 경계 |
virt_boundary_mask |
blk_queue_virt_boundary() |
0 | 가상 주소 연속성 보장을 위한 마스크 (NVMe) |
네트워크 스택의 Scatter/Gather
네트워크 스택은 struct sk_buff의 paged data 영역에서 SG를 활용합니다.
skb_frag_t는 패킷의 비선형(non-linear) 데이터를 기술하며,
NETIF_F_SG 피처를 지원하는 NIC은 이 조각들을 하드웨어 레벨에서 직접 처리합니다.
skb_frag_t 구조체
/* include/linux/skbuff.h */
typedef struct skb_frag {
struct {
struct page *p; /* 페이지 포인터 */
} bv_page;
__u32 bv_len; /* 조각 길이 */
__u32 bv_offset; /* 페이지 내 오프셋 */
} skb_frag_t;
/* sk_buff의 paged data:
* skb->data_len = sum of all frag lengths
* skb->nr_frags = 실제 사용 중인 frag 수
* skb_shinfo(skb)->frags[0..nr_frags-1] = skb_frag_t 배열
*/
네트워크 SG 관련 피처 플래그
| 피처 | 의미 | SG와의 관계 |
|---|---|---|
NETIF_F_SG |
NIC이 SG DMA 지원 | skb frag들을 개별 DMA 매핑하여 전송 |
NETIF_F_GSO |
Generic Segmentation Offload | 대형 패킷을 SG 조각으로 분할하여 NIC 전달 |
NETIF_F_GRO |
Generic Receive Offload | 수신 패킷을 SG 리스트로 병합 |
NETIF_F_HIGHDMA |
HIGHMEM 페이지 DMA 가능 | SG frag가 HIGHMEM에 있어도 바운스 버퍼 불필요 |
NETIF_F_SG_ENCRYPTED |
kTLS SG 지원 | 암호화된 SG 데이터를 NIC이 직접 처리 |
네트워크 드라이버 SG 송신 코드 패턴
/* NIC 드라이버 ndo_start_xmit 핵심 SG 처리 */
static netdev_tx_t my_nic_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
struct my_tx_ring *ring = &priv->tx_ring;
int nr_frags = skb_shinfo(skb)->nr_frags;
int i;
/* 1. linear 데이터 (헤더) 매핑 */
ring->desc[ring->head].addr =
dma_map_single(dev, skb->data, skb_headlen(skb),
DMA_TO_DEVICE);
ring->desc[ring->head].len = skb_headlen(skb);
ring->head = (ring->head + 1) % RING_SIZE;
/* 2. paged frag 매핑 (SG) */
for (i = 0; i < nr_frags; i++) {
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
ring->desc[ring->head].addr =
skb_frag_dma_map(dev, frag, 0,
skb_frag_size(frag),
DMA_TO_DEVICE);
ring->desc[ring->head].len = skb_frag_size(frag);
ring->desc[ring->head].flags =
(i == nr_frags - 1) ? TX_DESC_EOP : 0;
ring->head = (ring->head + 1) % RING_SIZE;
}
/* 3. 도어벨: 하드웨어에 전송 알림 */
writel(ring->head, priv->regs + TX_DOORBELL);
return NETDEV_TX_OK;
}
NVMe SGL과 PRP
NVMe 프로토콜은 호스트와 컨트롤러 간 데이터 전송을 위해 두 가지 주소 지정 방식을 정의합니다: PRP(Physical Region Page)와 SGL(Scatter Gather List). PRP는 NVMe 1.0부터 지원되는 기본 방식이고, SGL은 NVMe 1.1에서 추가된 보다 유연한 방식입니다.
PRP vs SGL 비교
| 특성 | PRP (Physical Region Page) | SGL (Scatter Gather List) |
|---|---|---|
| 도입 | NVMe 1.0 | NVMe 1.1 |
| 주소 단위 | 페이지 정렬 필수 (첫 PRP 제외) | 임의 오프셋/길이 가능 |
| 엔트리 크기 | 8바이트 (주소만) | 16바이트 (주소 + 길이 + 타입) |
| 체이닝 | PRP 리스트 포인터 | SGL 세그먼트 디스크립터 |
| 최소 지원 | 모든 NVMe 컨트롤러 필수 | 선택적 (SGLS 필드 확인) |
| 효율성 | 페이지 정렬이면 효율적 | 비정렬 데이터에 유리 |
| 커널 기본 | NVMe 블록 드라이버 기본 | NVMe-oF(Fabrics)에서 주로 사용 |
NVMe SGL 디스크립터 구조
/* include/linux/nvme.h */
struct nvme_sgl_desc {
__le64 addr; /* 데이터 또는 세그먼트 주소 */
__le32 length; /* 바이트 길이 */
__u8 rsvd[3];
__u8 type; /* SGL 디스크립터 타입 */
};
/* SGL 디스크립터 타입 */
#define NVME_SGL_FMT_DATA_DESC 0x00 /* 데이터 블록 */
#define NVME_SGL_FMT_SEG_DESC 0x02 /* SGL 세그먼트 (체이닝) */
#define NVME_SGL_FMT_LAST_SEG_DESC 0x03 /* 마지막 세그먼트 */
/* PRP 엔트리 (비교용) */
struct nvme_prp_entry {
__le64 prp; /* 물리 주소 (8바이트) */
};
scatterlist에서 PRP/SGL 변환
/* drivers/nvme/host/pci.c (간략화) */
static blk_status_t nvme_map_data(struct nvme_dev *dev,
struct request *req,
struct nvme_command *cmnd)
{
struct nvme_iod *iod = blk_mq_rq_to_pdu(req);
int nr_mapped;
/* bio_vec → scatterlist 변환 */
iod->sg = mempool_alloc(dev->iod_mempool, GFP_ATOMIC);
sg_init_table(iod->sg, blk_rq_nr_phys_segments(req));
iod->nents = blk_rq_map_sg(req->q, req, iod->sg);
/* DMA 매핑 */
nr_mapped = dma_map_sg_attrs(dev->dev, iod->sg,
iod->nents, rq_dma_dir(req),
DMA_ATTR_NO_WARN);
/* 컨트롤러 능력에 따라 PRP 또는 SGL 선택 */
if (nvme_pci_use_sgls(dev, req, nr_mapped))
return nvme_pci_setup_sgls(dev, req, cmnd, nr_mapped);
else
return nvme_pci_setup_prps(dev, req, cmnd);
}
readv/writev와 사용자 공간 SG
사용자 공간에서도 Scatter/Gather I/O를 직접 활용할 수 있습니다.
readv()/writev() 시스템 콜은 struct iovec 배열을 통해
비연속 버퍼에 대한 벡터 I/O를 수행합니다. 커널 내부에서는 struct iov_iter로
다양한 벡터 타입(iovec, bvec, kvec, pipe, xarray)을 통합하여 처리합니다.
iovec 구조체
/* include/uapi/linux/uio.h */
struct iovec {
void __user *iov_base; /* 사용자 공간 버퍼 시작 주소 */
__kernel_size_t iov_len;/* 버퍼 길이 */
};
/* 사용자 공간 사용 예:
* struct iovec iov[3];
* iov[0] = { .iov_base = header_buf, .iov_len = 64 };
* iov[1] = { .iov_base = data_buf, .iov_len = 4096 };
* iov[2] = { .iov_base = footer_buf, .iov_len = 32 };
* writev(fd, iov, 3);
*/
iov_iter 구조체 (커널 내부 통합 순회자)
/* include/linux/uio.h */
struct iov_iter {
u8 iter_type; /* ITER_IOVEC, ITER_BVEC, ITER_KVEC, ... */
bool nofault; /* 페이지 폴트 허용 여부 */
bool data_source; /* 0=읽기(from iter), 1=쓰기(to iter) */
size_t iov_offset; /* 현재 iov 내 소비된 바이트 */
size_t count; /* 남은 총 바이트 수 */
union {
const struct iovec *__iov; /* 사용자 공간 벡터 */
const struct kvec *kvec; /* 커널 공간 벡터 */
const struct bio_vec *bvec; /* 페이지 벡터 */
struct xarray *xarray; /* XArray 기반 */
void __user *ubuf; /* 단일 사용자 버퍼 */
};
union {
unsigned long nr_segs; /* 세그먼트 수 */
};
};
코드 설명
-
3행
iter_type은 어떤 종류의 벡터를 순회하는지 결정합니다. 타입에 따라 union의 다른 멤버가 활성화됩니다. -
6행
iov_offset은 현재 벡터 엔트리에서 이미 처리된 바이트 수를 추적합니다. 부분 I/O 시 중간부터 재개할 수 있습니다. -
7행
count는 전체 남은 바이트 수입니다. I/O가 진행될수록 감소합니다. - 9-13행 union으로 다양한 벡터 타입을 지원합니다. iovec(사용자), kvec(커널), bvec(블록), xarray(페이지 캐시) 등을 동일한 인터페이스로 처리합니다.
iov_iter 타입별 사용 문맥
| 타입 | 매크로 | 사용 문맥 | 버퍼 위치 |
|---|---|---|---|
| IOVEC | ITER_IOVEC |
readv/writev, sendmsg/recvmsg | 사용자 공간 |
| KVEC | ITER_KVEC |
커널 내부 벡터 I/O | 커널 공간 |
| BVEC | ITER_BVEC |
블록 I/O (bio) | 페이지 기반 |
| XARRAY | ITER_XARRAY |
페이지 캐시 직접 접근 | 페이지 캐시 |
| UBUF | ITER_UBUF |
단일 사용자 버퍼 (read/write) | 사용자 공간 |
사용자 공간 벡터 I/O 예제
/* 사용자 공간에서 writev() 활용 예제 */
#include <sys/uio.h>
struct iovec iov[3];
char header[] = "HTTP/1.1 200 OK\r\nContent-Length: 4096\r\n\r\n";
char body[4096];
char footer[] = "\r\n--END--\r\n";
/* 3개의 비연속 버퍼를 하나의 write로 전송 */
iov[0].iov_base = header;
iov[0].iov_len = sizeof(header) - 1;
iov[1].iov_base = body;
iov[1].iov_len = sizeof(body);
iov[2].iov_base = footer;
iov[2].iov_len = sizeof(footer) - 1;
ssize_t n = writev(sockfd, iov, 3);
/* 커널은 3개 iovec을 iov_iter로 변환하여 소켓 계층에 전달
* TCP는 SG를 활용하여 복사 없이 skb frag로 매핑 가능 */
splice와 제로 카피
splice() 시스템 콜은 파이프를 매개체로 사용하여 두 파일 디스크립터 사이에서
데이터를 전송합니다. 핵심은 실제 데이터 복사 없이 페이지 참조만 이동시키는
제로 카피(zero-copy) 방식입니다. 이는 본질적으로 Scatter/Gather 개념의
사용자 공간 확장입니다.
제로 카피 시스템 콜 비교
| 시스템 콜 | 프로토타입 | 동작 |
|---|---|---|
splice() |
splice(fd_in, off_in, fd_out, off_out, len, flags) |
파이프 ↔ 파일/소켓 간 페이지 참조 이동 |
tee() |
tee(fd_in, fd_out, len, flags) |
파이프 간 데이터 복제 (페이지 참조 공유) |
vmsplice() |
vmsplice(fd, iov, nr_segs, flags) |
사용자 버퍼(iovec) → 파이프 (제로 카피 또는 복사) |
sendfile() |
sendfile(out_fd, in_fd, offset, count) |
파일 → 소켓 전송 (내부적으로 splice 사용) |
copy_file_range() |
copy_file_range(fd_in, off_in, fd_out, off_out, len, flags) |
파일 → 파일 (서버 사이드 복사 가능) |
splice 구현 핵심 경로
/* fs/splice.c (간략화) — sendfile → splice 내부 경로 */
/* 파일 → 파이프: 페이지 캐시 참조를 파이프 버퍼에 삽입 */
static ssize_t splice_read(struct file *in,
struct pipe_inode_info *pipe, ...)
{
/* 페이지 캐시에서 페이지 참조 획득 */
struct page *page = find_get_page(mapping, index);
/* 파이프 버퍼에 페이지 참조 저장 (복사 없음) */
buf->page = page;
buf->offset = offset;
buf->len = len;
buf->ops = &page_cache_pipe_buf_ops;
pipe->nrbufs++;
}
/* 파이프 → 소켓: 파이프 버퍼의 페이지를 skb frag로 매핑 */
static ssize_t splice_to_socket(struct pipe_inode_info *pipe,
struct socket *sock, ...)
{
/* 파이프 버퍼의 페이지를 skb의 paged frag로 전달 */
skb_fill_page_desc(skb, frag_idx,
buf->page, buf->offset, buf->len);
/* NIC이 NETIF_F_SG 지원 시 이 페이지를 직접 DMA 전송 */
}
sendfile()을
사용하여 디스크에서 네트워크로 파일을 전송할 때 CPU 복사를 제거합니다.
이는 Scatter/Gather의 대표적인 사용자 공간 활용 사례입니다.
Crypto API의 SG 활용
리눅스 Crypto API는 암호화, 해시, 압축 등의 입출력 버퍼로 scatterlist를 직접 사용합니다.
이를 통해 비연속 메모리에 분산된 데이터를 한 번의 암호화 작업으로 처리할 수 있습니다.
특히 네트워크(IPSec, kTLS)와 스토리지(dm-crypt) 암호화에서 SG는 필수적입니다.
Crypto API의 SG 사용 패턴
| 알고리즘 타입 | API | SG 사용 |
|---|---|---|
| 대칭 암호 (skcipher) | crypto_skcipher_encrypt/decrypt() |
src_sg, dst_sg (입출력 SG 별도) |
| AEAD | crypto_aead_encrypt/decrypt() |
AAD + 평문/암호문 + 태그를 하나의 SG로 |
| 해시 | crypto_shash_digest() |
입력 버퍼를 SG로 전달 |
| AHASH (비동기 해시) | crypto_ahash_digest() |
SG 기반 비동기 처리 |
SG 기반 대칭 암호화 예제
/* scatterlist를 활용한 AES-GCM 암호화 예제 */
struct crypto_aead *tfm;
struct aead_request *req;
struct scatterlist sg_src[3], sg_dst[3];
u8 *aad, *plaintext, *tag;
u8 iv[12];
/* 1. 변환 할당 */
tfm = crypto_alloc_aead("gcm(aes)", 0, 0);
crypto_aead_setkey(tfm, key, key_len);
crypto_aead_setauthsize(tfm, 16); /* 128비트 태그 */
/* 2. SG 설정: AAD + 평문 + 태그 공간 */
sg_init_table(sg_src, 3);
sg_set_buf(&sg_src[0], aad, aad_len); /* AAD */
sg_set_buf(&sg_src[1], plaintext, pt_len); /* 평문 */
sg_set_buf(&sg_src[2], tag, 16); /* 태그 출력 공간 */
sg_init_table(sg_dst, 3);
sg_set_buf(&sg_dst[0], aad, aad_len);
sg_set_buf(&sg_dst[1], ciphertext, pt_len); /* 암호문 출력 */
sg_set_buf(&sg_dst[2], tag, 16);
/* 3. 요청 설정 및 암호화 */
req = aead_request_alloc(tfm, GFP_KERNEL);
aead_request_set_crypt(req, sg_src, sg_dst, pt_len, iv);
aead_request_set_ad(req, aad_len);
int ret = crypto_aead_encrypt(req);
/* ret == 0: 성공, 암호문과 태그가 sg_dst에 기록됨
* ret == -EINPROGRESS: 비동기 처리 중 (하드웨어 가속) */
SG 유틸리티 함수
| 함수 | 용도 |
|---|---|
sg_copy_to_buffer(sgl, nents, buf, buflen) |
SG 리스트 → 연속 버퍼 복사 |
sg_copy_from_buffer(sgl, nents, buf, buflen) |
연속 버퍼 → SG 리스트 복사 |
sg_pcopy_to_buffer(sgl, nents, buf, buflen, skip) |
SG 리스트에서 오프셋 건너뛰고 복사 |
sg_pcopy_from_buffer(sgl, nents, buf, buflen, skip) |
연속 버퍼에서 SG 리스트로 오프셋 건너뛰고 복사 |
sg_nents(sgl) |
SG 리스트의 총 엔트리 수 계산 |
sg_nents_for_len(sgl, len) |
지정 길이를 커버하는 데 필요한 엔트리 수 |
sg_miter_start(miter, sgl, nents, flags) |
SG 매핑 반복자 시작 (kmap 기반) |
sg_miter_next(miter) |
다음 SG 세그먼트로 이동 (자동 kmap/kunmap) |
디바이스 드라이버 SG 구현 패턴
디바이스 드라이버에서 SG I/O를 구현할 때는 일관된 패턴을 따라야 합니다. DMA 디스크립터 링과 SG 리스트의 연동, 오류 처리, 매핑 해제 순서 등을 정확히 지켜야 메모리 누수와 데이터 손상을 방지할 수 있습니다.
완전한 DMA SG 드라이버 패턴
/* 범용 DMA SG 전송 함수 패턴 */
struct my_sg_request {
struct sg_table sgt;
struct my_dma_desc *first_desc;
int desc_count;
enum dma_data_direction dir;
void (*callback)(void *data);
void *cb_data;
};
static int my_submit_sg_transfer(struct device *dev,
struct my_sg_request *sgr,
struct page **pages,
int num_pages,
enum dma_data_direction dir)
{
struct scatterlist *sg;
struct my_dma_desc *desc;
int i, ret;
/* 1. SG 테이블 할당 */
ret = sg_alloc_table_from_pages(&sgr->sgt, pages, num_pages,
0, (size_t)num_pages << PAGE_SHIFT,
GFP_KERNEL);
if (ret)
return ret;
/* 2. DMA 매핑 */
ret = dma_map_sgtable(dev, &sgr->sgt, dir, 0);
if (ret)
goto err_free_sgt;
/* 3. DMA 디스크립터 설정 */
sgr->desc_count = 0;
sgr->dir = dir;
desc = alloc_dma_descs(dev, sgr->sgt.nents);
if (!desc) {
ret = -ENOMEM;
goto err_unmap;
}
sgr->first_desc = desc;
for_each_sgtable_dma_sg(&sgr->sgt, sg, i) {
desc[i].src_addr = sg_dma_address(sg);
desc[i].length = sg_dma_len(sg);
desc[i].next = (i < sgr->sgt.nents - 1)
? &desc[i + 1] : NULL;
desc[i].flags = (i == sgr->sgt.nents - 1)
? DESC_IRQ | DESC_LAST : 0;
sgr->desc_count++;
}
/* 4. 하드웨어에 전송 시작 */
writel(desc_dma_addr(sgr->first_desc),
priv->regs + DMA_DESC_ADDR_REG);
writel(DMA_START, priv->regs + DMA_CTRL_REG);
return 0;
err_unmap:
dma_unmap_sgtable(dev, &sgr->sgt, dir, 0);
err_free_sgt:
sg_free_table(&sgr->sgt);
return ret;
}
/* DMA 완료 인터럽트 핸들러 */
static irqreturn_t my_dma_irq(int irq, void *data)
{
struct my_sg_request *sgr = data;
/* CPU에서 데이터 읽기 전 동기화 (DMA_FROM_DEVICE) */
if (sgr->dir == DMA_FROM_DEVICE)
dma_sync_sgtable_for_cpu(dev, &sgr->sgt, sgr->dir);
/* 콜백 호출 */
if (sgr->callback)
sgr->callback(sgr->cb_data);
/* 정리: 반드시 unmap → free 순서 */
dma_unmap_sgtable(dev, &sgr->sgt, sgr->dir, 0);
sg_free_table(&sgr->sgt);
free_dma_descs(dev, sgr->first_desc, sgr->desc_count);
return IRQ_HANDLED;
}
코드 설명
-
22-26행
sg_alloc_table_from_pages()로 페이지 배열을 최적화된 SG 테이블로 변환합니다. 인접 페이지는 자동 병합됩니다. - 29-31행 DMA 매핑 실패 시 SG 테이블을 해제하고 반환합니다. 오류 경로에서 자원 누수를 방지합니다.
-
41-50행
for_each_sgtable_dma_sg()로 DMA 매핑 후의 세그먼트를 순회하며 하드웨어 디스크립터를 설정합니다. 마지막 디스크립터에 인터럽트 플래그를 설정합니다. -
69-71행
DMA_FROM_DEVICE 방향에서 CPU가 데이터를 읽기 전에
dma_sync_sgtable_for_cpu()로 캐시를 무효화합니다. - 77-79행 정리 순서가 중요합니다: DMA 매핑 해제 → SG 테이블 해제 → 디스크립터 해제. 순서가 바뀌면 use-after-free가 발생할 수 있습니다.
흔한 SG 드라이버 버그와 해결책
| 버그 | 증상 | 해결책 |
|---|---|---|
| orig_nents로 DMA 순회 | IOMMU 병합 시 잘못된 주소/길이 참조 | for_each_sgtable_dma_sg() 사용 |
| dma_map_sg 반환값으로 unmap | 일부 엔트리 매핑 해제 누락 | orig_nents (또는 dma_unmap_sgtable()) 사용 |
| 매핑 해제 전 SG 테이블 해제 | DMA 매핑 누수, IOMMU 자원 고갈 | 반드시 unmap 먼저, free 나중에 |
| DMA 동기화 누락 | 캐시 비일관성으로 데이터 손상 | dma_sync_sgtable_for_cpu/device() 호출 |
| max_segments 초과 | DMA 전송 실패 또는 하드웨어 오류 | 디바이스 큐 파라미터 올바르게 설정 |
| sg_set_page에 복합(compound) 페이지 | DMA 매핑 시 페이지 경계 오류 | 복합 페이지는 개별 하위 페이지로 분할하거나 길이 주의 |
성능 최적화와 튜닝
Scatter/Gather I/O의 성능은 SG 엔트리 수, 세그먼트 크기, IOMMU 매핑 오버헤드, 디바이스 하드웨어 능력 등 여러 요소에 의해 결정됩니다. 적절한 튜닝으로 처리량을 크게 향상시킬 수 있습니다.
SG 세그먼트 병합 최적화
| 최적화 기법 | 효과 | 적용 방법 |
|---|---|---|
| 인접 페이지 병합 | SG 엔트리 수 감소 → 디스크립터 오버헤드 감소 | sg_alloc_table_from_pages() 사용 |
| IOMMU 병합 활용 | 비연속 페이지도 단일 DMA 세그먼트로 | IOMMU 활성화, iommu=on |
| 대형 페이지 사용 | 2MB/1GB 페이지로 SG 엔트리 수 대폭 감소 | hugepage 할당, compound page |
| max_segment_size 증가 | 단일 세그먼트에 더 많은 데이터 | blk_queue_max_segment_size() |
| max_segments 증가 | 더 큰 I/O 요청 가능 | blk_queue_max_segments() |
| DMA 주소 연속성 힌트 | IOMMU 매핑 최적화 | DMA_ATTR_FORCE_CONTIGUOUS |
SG 처리 성능 벤치마크 참고 데이터
| 시나리오 | SG 엔트리 수 | 전송 크기 | 처리량 (참고값) | 비고 |
|---|---|---|---|---|
| NVMe 4KB 랜덤 읽기 | 1 | 4 KB | ~7 GB/s (Gen4) | 단일 PRP, 최소 오버헤드 |
| NVMe 128KB 순차 읽기 | 32 (4KB 페이지) | 128 KB | ~7 GB/s (Gen4) | PRP 리스트 사용 |
| NVMe 128KB (IOMMU 병합) | 1 (32에서 병합) | 128 KB | ~7 GB/s (Gen4) | IOMMU 병합으로 PRP 오버헤드 감소 |
| 10GbE 64KB TSO | 16 (skb frags) | 64 KB | ~1.2 GB/s | NETIF_F_SG + TSO |
| sendfile 정적 파일 | 가변 | 가변 | ~9.5 GB/s (메모리 대역폭) | 제로 카피, CPU 복사 없음 |
| dm-crypt AES-XTS | 32 (128KB) | 128 KB | ~3 GB/s (AES-NI) | SG 기반 in-place 암호화 |
IOMMU 성능 영향
IOMMU는 SG 병합이라는 큰 이점을 제공하지만, 페이지 테이블 관리와 IOTLB 미스로 인한 오버헤드도 존재합니다. 다음은 IOMMU 관련 성능 튜닝 옵션입니다:
| 커널 파라미터 | 효과 | 장단점 |
|---|---|---|
iommu=on |
IOMMU 활성화 | SG 병합 가능, 약간의 IOTLB 오버헤드 |
iommu=pt |
패스스루 모드 | DMA 주소 = 물리 주소, 병합 불가 |
iommu.forcedac=1 |
64비트 DMA 강제 | ZONE_DMA 회피, 바운스 버퍼 방지 |
intel_iommu=sm_on |
확장 모드 활성화 | PASID/SVA 지원, 오버헤드 증가 가능 |
블록 장치 SG 파라미터 확인 및 튜닝
/* sysfs를 통한 블록 디바이스 SG 파라미터 확인 */
$ cat /sys/block/nvme0n1/queue/max_segments
128
$ cat /sys/block/nvme0n1/queue/max_segment_size
65536
$ cat /sys/block/nvme0n1/queue/max_hw_sectors_kb
512
/* IOMMU 그룹 확인 */
$ ls /sys/kernel/iommu_groups/
0 1 2 3 ...
/* 특정 디바이스의 IOMMU 그룹 */
$ readlink /sys/bus/pci/devices/0000:03:00.0/iommu_group
../../kernel/iommu_groups/15
/* DMA 매핑 통계 (DMA_API_DEBUG 활성 시) */
$ cat /sys/kernel/debug/dma-api/driver_filter
$ cat /sys/kernel/debug/dma-api/num_errors
디버깅과 트러블슈팅
SG I/O 관련 버그는 데이터 손상, DMA 매핑 누수, IOMMU 폴트 등 심각한 문제를 유발합니다. 커널은 이를 진단하기 위한 다양한 디버깅 도구를 제공합니다.
DMA-debug 프레임워크
CONFIG_DMA_API_DEBUG를 활성화하면 커널이 모든 DMA 매핑/해제 작업을 추적하고
다음과 같은 오류를 자동으로 감지합니다:
| 감지 항목 | 오류 메시지 (dmesg) | 원인 |
|---|---|---|
| 이중 매핑 해제 | DMA-API: device driver tries to free DMA memory it has not allocated |
dma_unmap_sg() 중복 호출 |
| 매핑 누수 | DMA-API: leak, device driver has X mappings |
dma_unmap_sg() 호출 누락 |
| 잘못된 방향 | DMA-API: device driver maps memory with wrong direction |
매핑 시 방향과 해제 시 방향 불일치 |
| 동기화 없이 접근 | DMA-API: device driver accesses DMA mapped region without sync |
dma_sync_* 호출 누락 |
| 해제 후 접근 | DMA-API: device driver frees DMA memory with different size |
매핑과 해제의 크기/주소 불일치 |
ftrace를 활용한 DMA SG 추적
# DMA 매핑 이벤트 추적 활성화
$ echo 1 > /sys/kernel/debug/tracing/events/dma/enable
# SG 매핑 이벤트만 필터링
$ echo 1 > /sys/kernel/debug/tracing/events/dma/map_sg/enable
# 추적 로그 확인
$ cat /sys/kernel/debug/tracing/trace
# 출력 예시:
# kworker/0:1-123 [000] .... 1234.567: map_sg: nvme0n1
# nents=32 mapped=4 dir=DMA_TO_DEVICE
# sg[0]: dma=0xfffe0000 len=32768
# sg[1]: dma=0xffff0000 len=32768
# sg[2]: dma=0x100000000 len=32768
# sg[3]: dma=0x100010000 len=32768
IOMMU 폴트 디버깅
# IOMMU 폴트 메시지 (dmesg)
# DMAR: [DMA Read] Request device [03:00.0] PASID ffffffff
# fault addr 7f800000 [fault reason 06] PTE Read access is not set
# IOMMU 디버깅 활성화
$ echo 1 > /sys/kernel/debug/iommu/intel/dmar_perf
# IOMMU 매핑 덤프
$ cat /sys/kernel/debug/iommu/intel/dmar0/domain_translation_struct
# IOMMU 통계
$ cat /sys/kernel/debug/iommu/intel/ir_translation_struct
SG 관련 커널 경고 메시지 해석
| 경고 메시지 | 의미 | 조치 |
|---|---|---|
WARNING: sg_alloc_table failed |
메모리 할당 실패 | GFP 플래그 확인, 메모리 부족 조사 |
BUG: scatter list overflow |
SG 엔트리 수 초과 | max_segments 설정 확인 |
WARNING: at lib/scatterlist.c |
SG API 잘못된 사용 | SG_END/SG_CHAIN 설정 확인 |
swiotlb buffer is full |
SWIOTLB 바운스 버퍼 고갈 | swiotlb=65536 또는 IOMMU 활성화 |
커널 설정
Scatter/Gather I/O와 관련된 커널 설정 옵션들입니다. 올바른 설정이 성능과 안정성에 직접적인 영향을 미칩니다.
필수/권장 커널 설정
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_NEED_SG_DMA_LENGTH |
자동 | IOMMU 병합 시 dma_length 필드 활성화. IOMMU 사용 시 자동 설정 |
CONFIG_NEED_SG_DMA_FLAGS |
자동 | DMA SG 플래그 필드 활성화 (6.0+) |
CONFIG_DMA_API_DEBUG |
N | DMA 매핑 디버깅. 개발 중 Y 권장, 프로덕션에서는 오버헤드 유의 |
CONFIG_DMA_API_DEBUG_SG |
N | SG 전용 추가 디버깅 검사 |
CONFIG_IOMMU_SUPPORT |
Y | IOMMU 프레임워크 (SG 병합에 필수) |
CONFIG_INTEL_IOMMU |
Y (x86) | Intel VT-d IOMMU 드라이버 |
CONFIG_AMD_IOMMU |
Y (x86 AMD) | AMD-Vi IOMMU 드라이버 |
CONFIG_ARM_SMMU_V3 |
Y (ARM64) | ARM SMMU v3 드라이버 |
CONFIG_SWIOTLB |
자동 | 소프트웨어 I/O TLB (IOMMU 없을 때 폴백) |
CONFIG_CRYPTO_USER_API_AEAD |
M | 사용자 공간 AEAD crypto (SG 기반) |
CONFIG_BLK_DEV_INTEGRITY |
Y | 블록 무결성 확장 (SG 기반 메타데이터) |
부팅 파라미터
| 파라미터 | 효과 |
|---|---|
iommu=on |
IOMMU 활성화 (SG 병합 가능) |
iommu=pt |
IOMMU 패스스루 (주소 변환 없음, 병합 불가) |
iommu=off |
IOMMU 비활성화 |
iommu.strict=0 |
lazy IOTLB 플러시 (성능 향상, 보안 약간 감소) |
swiotlb=65536 |
SWIOTLB 버퍼 크기 증가 (슬롯 수) |
intel_iommu=on |
Intel VT-d 명시적 활성화 |
amd_iommu=on |
AMD-Vi 명시적 활성화 |
참고자료
커널 공식 문서
- DMA API Guide (kernel.org) -- DMA 매핑 API 공식 문서
- DMA API HOWTO (kernel.org) -- DMA 매핑 실전 가이드
- DMA-BUF (kernel.org) -- DMA 버퍼 공유 프레임워크
- Block Layer Documentation (kernel.org) -- 블록 I/O 계층 문서
- Crypto API (kernel.org) -- 커널 암호화 프레임워크
커널 소스 코드
include/linux/scatterlist.h-- scatterlist, sg_table 정의lib/scatterlist.c-- SG 관리 함수 구현include/linux/dma-mapping.h-- DMA 매핑 API 헤더kernel/dma/mapping.c-- DMA 매핑 코어kernel/dma/direct.c-- 직접 DMA 매핑 (IOMMU 없음)kernel/dma/swiotlb.c-- SWIOTLB 바운스 버퍼drivers/iommu/-- IOMMU 프레임워크 및 드라이버include/linux/bio.h-- bio, bio_vec 정의include/linux/skbuff.h-- sk_buff, skb_frag_t 정의drivers/nvme/host/pci.c-- NVMe PCI 드라이버 (SG/PRP/SGL 변환)fs/splice.c-- splice, sendfile 구현lib/iov_iter.c-- iov_iter 순회 구현
규격 및 표준
- NVMe Base Specification -- NVMe SGL/PRP 상세 규격
- Intel VT-d Specification -- Intel IOMMU DMA 리매핑 규격
- AMD I/O Virtualization Technology (IOMMU) Specification
관련 문서 (이 사이트)
- DMA (Direct Memory Access) -- DMA 기초 및 전체 API
- Block I/O -- 블록 계층 아키텍처
- sk_buff -- 네트워크 버퍼 관리
- NVMe -- NVMe 드라이버 상세
- io_uring -- 비동기 I/O 프레임워크
- GSO/GRO -- 네트워크 오프로드
- IOMMU -- IOMMU 프레임워크
- Crypto API -- 커널 암호화 프레임워크
- Direct I/O & Buffered I/O -- 파일 I/O 경로
- DMA Engine -- 소프트웨어 DMA 엔진