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 연동, 드라이버 구현 패턴, 성능 최적화와 디버깅까지 전 영역을 상세히 다룹니다.

관련 문서: DMA 기초는 DMA, IOMMU 상세는 IOMMU, 블록 I/O 계층은 Block I/O, 네트워크 버퍼는 sk_buff, NVMe 상세는 NVMe, io_uring은 io_uring 페이지를 참조하십시오.
커널 버전: 이 문서는 Linux 6.x 안정 커널 기준으로 작성되었습니다. API 변경 사항은 각 섹션에서 별도로 표기합니다.
전제 조건: DMA메모리 관리 문서를 먼저 읽으세요. Scatter/Gather I/O는 물리 메모리 구조, 페이지 할당, DMA 주소 변환에 대한 이해가 필요합니다.
일상 비유: Scatter/Gather는 택배 묶음 배송과 비슷합니다. 택배 기사가 여러 건물(비연속 주소)에 있는 물건을 한 번의 트럭 운행으로 모두 수거(Gather)하거나, 한 트럭 분량의 물건을 여러 건물에 나눠 배달(Scatter)합니다. 각 건물의 주소와 물건 크기를 목록으로 만들어 기사에게 전달하면, 기사(DMA 엔진)는 목록만 보고 독립적으로 작업을 완료합니다.

핵심 요약

  • Scatter/Gather -- 비연속 메모리 조각들을 하나의 논리적 버퍼로 묶어 한 번의 DMA 전송으로 처리하는 기법
  • scatterlist -- 페이지, 오프셋, 길이로 메모리 조각 하나를 기술하는 커널 구조체
  • sg_table -- scatterlist 배열과 메타데이터를 관리하는 컨테이너 구조체
  • DMA 매핑 -- dma_map_sg()로 scatterlist를 디바이스가 접근 가능한 DMA 주소로 변환
  • IOMMU 병합 -- 물리적으로 비연속인 여러 세그먼트를 IOMMU가 하나의 연속 DMA 주소로 병합 가능

단계별 이해

  1. 메모리 단편화 인식
    커널이 오래 실행되면 연속된 큰 메모리를 할당하기 어렵습니다. 대신 흩어진 페이지들을 모아서 사용합니다.
  2. SG 리스트 구성
    sg_alloc_table()로 테이블을 할당하고, 각 엔트리에 페이지와 오프셋, 길이를 설정합니다.
  3. DMA 매핑 수행
    dma_map_sgtable()으로 모든 엔트리를 한 번에 DMA 주소로 변환합니다. IOMMU가 있으면 인접한 엔트리를 병합합니다.
  4. 디바이스 전송
    디바이스에 DMA 주소 목록(디스크립터 링 등)을 전달하면, 디바이스가 독립적으로 모든 조각을 순회하며 데이터를 전송합니다.
  5. 매핑 해제 및 정리
    전송 완료 후 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.hscatterlist, sg_table 정의 및 인라인 헬퍼
lib/scatterlist.csg_alloc_table, sg_free_table, sg_copy 등 구현
include/linux/dma-mapping.hdma_map_sg, dma_unmap_sg 등 DMA SG API
kernel/dma/mapping.cDMA 매핑 코어 구현
include/linux/uio.hiovec, iov_iter 정의
lib/iov_iter.ciov_iter 순회/복사 구현
include/linux/bio.hbio_vec, bio 구조체 정의
fs/splice.csplice, tee, vmsplice 구현

핵심 개념: 비연속 메모리 전송

리눅스 커널에서 물리 메모리는 페이지(일반적으로 4KB) 단위로 관리됩니다. 버디 할당자(buddy allocator)가 연속된 물리 페이지를 반환하지만, 시스템이 오래 실행될수록 고차(high-order) 할당은 점점 어려워집니다. Scatter/Gather는 이 문제를 근본적으로 해결합니다.

물리 메모리 단편화와 SG 전송

64KB 데이터를 디바이스에 전송해야 하는 상황을 가정합니다. 물리 메모리가 단편화되어 16개의 연속 페이지(order-4)를 얻을 수 없다면, 커널은 4KB 페이지 16개를 개별적으로 할당합니다. 이 16개 페이지는 물리 주소가 비연속적이지만, SG 리스트로 묶으면 하드웨어가 한 번의 DMA 작업으로 모두 전송할 수 있습니다.

Scatter/Gather I/O 개념 물리 메모리 (단편화) 페이지 A (사용중) (사용중) 페이지 B 페이지 C (사용중) (사용중) 페이지 D Scatter/Gather 리스트 sg[0]: 페이지A, 0, 4096 sg[1]: 페이지B, 0, 4096 sg[2]: 페이지C, 0, 4096 sg[3]: 페이지D, 0, 4096 DMA 엔진 SG 리스트 순회 전송 CPU 개입 없음 디바이스 NIC / SSD / GPU Gather (수집): 메모리 → 디바이스 흩어진 페이지들의 데이터를 모아서 디바이스로 전송 예: sendmsg(), 디스크 쓰기, NVMe Write Scatter (분산): 디바이스 → 메모리 디바이스에서 읽은 데이터를 여러 페이지에 분산 저장 예: recvmsg(), 디스크 읽기, NVMe Read SG 대상 페이지 다른 용도 사용중 페이지 단편화된 물리 메모리에서 SG 리스트로 비연속 페이지를 묶어 단일 DMA 전송을 수행하는 흐름

연속 vs 비연속 메모리 DMA 비교

특성연속 메모리 DMAScatter/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_linkstruct 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) 크기의 배열을 여러 개 연결하여 수천 개의 엔트리를 관리합니다.

scatterlist 체이닝 구조 scatterlist 배열 #1 sg[0]: page=A, off=0, len=4096 sg[1]: page=B, off=0, len=4096 sg[2]: page=C, off=0, len=2048 sg[3]: SG_CHAIN → 배열 #2 chain scatterlist 배열 #2 sg[0]: page=D, off=512, len=3584 sg[1]: page=E, off=0, len=4096 sg[2]: page=F, off=0, len=1024 sg[3]: SG_END (마지막) 데이터 엔트리 체인 엔트리 (SG_CHAIN) 종료 엔트리 (SG_END) SG_CHAIN 플래그가 설정된 엔트리는 데이터가 아닌 다음 배열의 주소를 저장합니다. sg_next()가 자동으로 체인을 따라가므로 순회 코드는 체이닝을 의식할 필요가 없습니다.

핵심 인라인 헬퍼 함수

/* 페이지 포인터 추출 (하위 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입니다.
주의 -- orig_nents vs nents 혼동: DMA 매핑 전에는 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 SG 매핑 흐름 원본 scatterlist (4개) page=0x1000, len=4096 page=0x2000, len=4096 page=0x5000, len=4096 page=0x6000, len=4096 dma_map_sgtable() DMA 주소 변환 IOMMU 병합 가능 IOMMU 없음 (직접 매핑) DMA[0]: 0x1000, len=4096 DMA[1]: 0x2000, len=4096 DMA[2]: 0x5000, len=4096 DMA[3]: 0x6000, len=4096 nents = 4 (변화 없음) IOMMU 있음 (병합 매핑) DMA[0]: IOVA=0xF000, len=8192 (0x1000+0x2000 연속 병합) DMA[1]: IOVA=0xF2000, len=8192 (0x5000+0x6000 연속 병합) nents = 2 (4에서 병합) IOMMU가 있으면 물리적으로 인접한 페이지를 하나의 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 SG 병합: 비연속 물리 → 연속 DMA 물리 메모리 PA: 0x0040_0000 (간격) PA: 0x0080_0000 (간격) PA: 0x00C0_0000 PA: 0x0100_0000 IOMMU 페이지 테이블 매핑: IOVA+0x0000 → PA 0x0040_0000 IOVA+0x1000 → PA 0x0080_0000 IOVA+0x2000 → PA 0x00C0_0000 IOVA+0x3000 → PA 0x0100_0000 DMA 주소 공간 (IOVA) 연속 IOVA 범위 0xFFFF_0000 (4KB) 0xFFFF_1000 (4KB) 0xFFFF_2000 (4KB) 0xFFFF_3000 (4KB) = 연속 16KB DMA 영역 디바이스가 보는 뷰 단일 DMA 세그먼트: addr=0xFFFF_0000, len=16384 4개의 비연속 물리 페이지(PA)가 IOMMU 페이지 테이블을 통해 연속 IOVA로 매핑됩니다. 디바이스는 1개의 연속된 16KB DMA 세그먼트만 인식하므로 처리 효율이 극대화됩니다. 결과: orig_nents=4 → nents=1 (IOMMU 병합으로 4개 → 1개)

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가 데이터를 바운스 버퍼로 복사하므로 제로 카피 이점이 사라집니다.

SWIOTLB 성능 경고: SWIOTLB 바운스 버퍼는 SG I/O의 제로 카피 이점을 무효화합니다. CONFIG_DMA_API_DEBUG를 활성화하고 dmesg에서 "using SWIOTLB buffer" 메시지가 나타나는지 확인하십시오. 가능하면 64비트 DMA를 지원하는 디바이스를 사용하거나 IOMMU를 활성화하십시오.

블록 I/O에서의 Scatter/Gather

블록 계층은 struct biostruct 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 배열 포인터 */
    /* ... */
};
블록 I/O Scatter/Gather 흐름 파일시스템 page cache 페이지 struct bio bi_vcnt = 4 bi_io_vec → [bv0, bv1, bv2, bv3] 블록 계층 bio_vec → scatterlist blk_rq_map_sg() 디바이스 드라이버 dma_map_sg() DMA 디스크립터 설정 bio_vec 배열 (비연속 페이지) bv[0]: page=cache_pg_1, off=0, len=4096 bv[1]: page=cache_pg_2, off=0, len=4096 bv[2]: page=cache_pg_5, off=0, len=4096 bv[3]: page=cache_pg_8, off=0, len=4096 scatterlist (DMA 매핑 후) sg[0]: dma=0x1000, len=8192 (bv0+bv1 병합) sg[1]: dma=0x5000, len=4096 sg[2]: dma=0x8000, len=4096 변환 blk_rq_map_sg()가 bio_vec를 scatterlist로 변환하고, 인접 세그먼트를 병합합니다 max_segments와 max_segment_size 제한을 준수합니다

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이 직접 처리
네트워크 스택 Scatter/Gather 송신 경로 sendmsg() 사용자 버퍼 iovec (비연속 사용자 메모리) sk_buff head/data: 헤더 (linear) frags[0..N]: 페이로드 (paged) GSO 처리 대형 skb 분할 frag 참조 공유 NIC 드라이버 dma_map_single() + skb_frag DMA 매핑 TX 디스크립터 링 (NIC 하드웨어) DESC[0]: 헤더 dma=skb->data (linear) DESC[1]: frag[0] dma=frag[0] 페이지 DESC[2]: frag[1] dma=frag[1] 페이지 DESC[3]: frag[2] dma=frag[2] 페이지 NIC 하드웨어 (SG DMA) 4개 디스크립터를 순회하여 단일 패킷 전송 sk_buff의 linear 헤더와 paged frag들이 각각 별도 TX 디스크립터로 매핑됩니다 NETIF_F_SG 피처가 활성이면 NIC이 SG 디스크립터를 순회하여 하나의 프레임으로 전송합니다

네트워크 드라이버 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 PRP vs SGL 데이터 전송 PRP (Physical Region Page) NVMe 명령 (SQE) PRP Entry 1: 0x1000 | PRP Entry 2: 0x2000 (또는 PRP List 주소) PRP List: [0x3000, 0x4000, 0x5000, ...] 4KB 4KB 4KB 4KB 각 PRP 엔트리는 페이지 정렬 필수 (첫 번째 제외) SGL (Scatter Gather List) NVMe 명령 (SQE) SGL Desc: type=DATA_BLOCK, addr=0x1000, len=8192 SGL Segment: [addr=0x5000,len=3072], [addr=0x9000,len=1024] 8192B 3072B 1024B 임의 오프셋과 길이 허용 (페이지 정렬 불필요) 리눅스 커널 NVMe 드라이버 blk_rq_map_sg() → scatterlist → nvme_setup_prps() 또는 nvme_setup_sgls() 커널은 scatterlist를 컨트롤러 능력에 따라 PRP 또는 SGL 형식으로 변환합니다 NVMe-oF(Fabrics)는 SGL을 기본으로 사용합니다

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) 파일 → 파일 (서버 사이드 복사 가능)
sendfile() 제로 카피 경로 (splice 기반) 전통적 방식: read() + write() = 4번 복사 디스크 DMA 커널 버퍼 CPU 사용자 버퍼 CPU 소켓 버퍼 DMA NIC sendfile() 방식: 0번 CPU 복사 (SG 활용) 디스크 DMA 읽기 DMA Page Cache 페이지 참조만 전달 (CPU 복사 없음) 참조 소켓 SG skb frag에 페이지 참조 (NETIF_F_SG) DMA NIC DMA 전송 전통적 방식 DMA 2회 + CPU 복사 2회 = 총 4회 데이터 이동 사용자/커널 컨텍스트 전환 4회 sendfile + SG DMA 2회 + CPU 복사 0회 = 총 2회 데이터 이동 사용자/커널 컨텍스트 전환 2회 sendfile()은 Page Cache 페이지 참조를 sk_buff의 SG frag로 직접 매핑합니다 NIC이 NETIF_F_SG를 지원하면 CPU 복사가 완전히 제거됩니다

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 전송 */
}
실전 활용: 정적 파일 서빙 웹 서버(nginx, Apache)는 sendfile()을 사용하여 디스크에서 네트워크로 파일을 전송할 때 CPU 복사를 제거합니다. 이는 Scatter/Gather의 대표적인 사용자 공간 활용 사례입니다.

Crypto API의 SG 활용

리눅스 Crypto API는 암호화, 해시, 압축 등의 입출력 버퍼로 scatterlist를 직접 사용합니다. 이를 통해 비연속 메모리에 분산된 데이터를 한 번의 암호화 작업으로 처리할 수 있습니다. 특히 네트워크(IPSec, kTLS)와 스토리지(dm-crypt) 암호화에서 SG는 필수적입니다.

Crypto API의 SG 사용 패턴

알고리즘 타입APISG 사용
대칭 암호 (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 암호화
참고: 위 수치는 일반적인 서버급 하드웨어(Intel Xeon, PCIe Gen4 NVMe, 10GbE NIC)에서의 참고 수치이며, 실제 성능은 하드웨어, 워크로드, 시스템 구성에 따라 크게 달라집니다.

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 -- DMA 매핑 전체 API와 IOMMU 상호작용을 더 깊이 학습
  • Block I/O -- 블록 계층의 bio/request 처리 흐름을 이해
  • io_uring -- 최신 비동기 I/O 프레임워크와 SG 활용