CMA (Contiguous Memory Allocator)

리눅스 커널의 CMA(Contiguous Memory Allocator)는 물리적으로 연속된 대용량 메모리 블록을 효율적으로 할당하는 메커니즘입니다. DMA 하드웨어가 요구하는 연속 메모리를 사전 예약 영역에서 제공하면서도, 평상시에는 해당 영역을 이동 가능 페이지(movable pages)로 활용하여 메모리 낭비를 방지합니다. cma_alloc/cma_release API, 페이지 마이그레이션, Buddy Allocator 통합, DMA-CMA 연동, per-device CMA, Device Tree 바인딩, 디버깅(Debugging)과 성능 최적화까지 종합적으로 다룹니다.

관련 하드웨어: 비디오 디코더, 카메라 ISP, GPU 프레임버퍼, 디스플레이 컨트롤러 등 Scatter-Gather DMA를 지원하지 않는 디바이스가 대표적인 CMA 사용자입니다. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.
교차 참조: DMA 매핑(Mapping) 기초는 DMA, Buddy Allocator 구조는 페이지 할당자(Page Allocator), 메모리 단편화(Fragmentation)와 compaction은 메모리, Device Tree 문법은 Device Tree 페이지를 참조하십시오.
전제 조건: 메모리 관리(Memory Management)DMA 문서를 먼저 읽으세요. CMA는 물리 메모리(Physical Memory) 레이아웃, 페이지 할당, DMA 주소 변환(Address Translation)에 대한 기본 이해가 필요합니다.
일상 비유: CMA는 공항 비상 활주로와 비슷합니다. 평상시에는 소형 항공기(이동 가능 페이지)가 자유롭게 주기하지만, 대형 수송기(DMA 버퍼(Buffer))가 착륙해야 하면 소형기를 다른 주기장으로 이동시켜 넓은 연속 공간을 확보합니다. 전용 활주로를 항상 비워두면 낭비이므로, 평소에는 공유하되 필요할 때만 비우는 전략입니다.

핵심 요약

  • CMA -- 물리적으로 연속된 대용량 메모리를 할당하는 커널 서브시스템으로, 예약 영역과 이동 가능 페이지를 공존시킵니다.
  • MIGRATE_CMA -- Buddy Allocator에서 CMA 전용으로 관리되는 마이그레이션 타입으로, 이동 가능 페이지만 배치됩니다.
  • cma_alloc() -- CMA 영역에서 연속 물리 메모리를 할당하며, 필요하면 기존 페이지를 마이그레이션합니다.
  • dma_alloc_coherent() -- DMA API가 내부적으로 CMA를 fallback 경로로 사용하는 통합 지점입니다.
  • per-device CMA -- 디바이스별 전용 CMA 영역을 Device Tree로 지정하여 경합(Contention)을 줄일 수 있습니다.

단계별 이해

  1. 연속 메모리가 필요한 이유
    Scatter-Gather를 지원하지 않는 DMA 컨트롤러는 물리적으로 연속된 버퍼가 필수입니다. 시스템 운영 시간이 길어지면 메모리 단편화로 큰 연속 블록 확보가 어려워집니다.
  2. CMA 영역 예약
    부팅 시 Device Tree 또는 커널 파라미터로 CMA 영역을 예약합니다. 이 영역은 Buddy Allocator에 MIGRATE_CMA 타입으로 등록됩니다.
  3. 평상시 공유
    CMA 영역에는 이동 가능 페이지(유저 프로세스(Process) 데이터, 페이지 캐시(Page Cache) 등)가 배치되어 메모리가 낭비되지 않습니다.
  4. 할당 시 마이그레이션
    cma_alloc() 호출 시, 요청 범위 내의 기존 페이지를 다른 영역으로 마이그레이션하여 연속 공간을 확보합니다.
  5. 해제 후 재활용(Recycling)
    cma_release() 이후 해당 영역은 다시 이동 가능 페이지로 채워질 수 있습니다.

개요

CMA(Contiguous Memory Allocator)는 물리적으로 연속된 큰 메모리 블록을 효율적으로 할당하기 위해 설계된 커널 서브시스템입니다. 2012년 Michal Nazarewicz가 Samsung에서 개발하여 Linux 3.5에 최초 통합되었으며, mm/cma.cmm/cma.h에 핵심 구현이 위치합니다.

연속 메모리가 필요한 이유

현대 SoC의 많은 디바이스는 물리적으로 연속된 메모리 버퍼를 요구합니다.

디바이스연속 메모리 요구 이유일반적 크기
비디오 디코더프레임 버퍼를 연속 물리 주소(Physical Address)로 DMA 접근4~32 MB
카메라 ISP이미지 센서 데이터를 연속 버퍼에 기록8~64 MB
GPU 프레임버퍼디스플레이 스캔아웃에 연속 물리 메모리 필요16~128 MB
디스플레이 컨트롤러Scatter-Gather 미지원 LCDC2~16 MB
DSP/가속기전용 DMA 엔진이 연속 버퍼만 처리1~8 MB

기존 접근 방식의 한계

CMA 이전에는 연속 메모리 확보를 위해 다음 방법을 사용했지만, 각각 심각한 단점이 있었습니다.

방식접근단점
memblock_reserve()부팅 시 고정 영역 예약미사용 시에도 다른 용도 사용 불가 (메모리 낭비)
alloc_pages(GFP_DMA)DMA zone에서 할당단편화 시 대용량 연속 블록 확보 실패
vmalloc() + IOMMU가상 연속 메모리IOMMU 미지원 디바이스에서 사용 불가
CMA의 핵심 아이디어: 예약 영역을 완전히 잠그지 않고, 이동 가능한 페이지와 공존시킵니다. 연속 메모리가 필요할 때만 기존 페이지를 마이그레이션하여 공간을 확보합니다. 이 "지연(Latency) 격리(lazy isolation)" 방식으로 메모리 활용도와 연속 할당 보장을 동시에 달성합니다.

CMA 핵심 구조체(Struct)

/* mm/cma.h */
struct cma {
    unsigned long   base_pfn;       /* CMA 영역 시작 PFN */
    unsigned long   count;          /* 총 페이지 수 */
    unsigned long   *bitmap;        /* 할당 상태 비트맵 */
    unsigned int    order_per_bit;  /* 비트맵 1비트 = 2^order 페이지 */
    spinlock_t      lock;           /* 비트맵 보호 락 */
    struct mutex    cma_mutex;      /* 할당/해제 직렬화 */
#ifdef CONFIG_CMA_DEBUGFS
    struct hlist_head mem_head;    /* 디버그: 할당 추적 리스트 */
    spinlock_t      mem_head_lock;  /* 디버그: 추적 리스트 보호 */
#endif
    const char      *name;          /* CMA 영역 이름 */
};
코드 설명
  • 3행 base_pfn: CMA 영역의 시작 페이지 프레임(Page Frame) 번호(PFN). 물리 주소 = base_pfn * PAGE_SIZE.
  • 4행 count: CMA 영역에 포함된 총 페이지 수. 영역 크기 = count * PAGE_SIZE.
  • 5행 bitmap: 각 비트가 2^order_per_bit 페이지의 할당 상태를 추적하는 비트맵(Bitmap).
  • 6행 order_per_bit: 비트맵 해상도. 0이면 페이지 단위, 4이면 16페이지(64KB) 단위 추적.
  • 7-8행 lock은 비트맵 읽기/쓰기를, cma_mutex는 할당/해제 연산 전체를 직렬화(Serialization)합니다.

CMA 아키텍처

CMA의 핵심 설계 원리는 예약 영역(Reserved Area)과 이동 가능 페이지(Movable Pages)의 공존입니다. CMA 영역은 Buddy Allocator에 MIGRATE_CMA 타입으로 등록되어, 평상시에는 이동 가능 할당 요청을 처리하고, CMA 할당 시에만 기존 페이지를 마이그레이션합니다.

CMA 아키텍처: 예약 영역과 Movable 페이지 공존 물리 메모리 (Physical Memory) ZONE_NORMAL + ZONE_DMA + ... CMA 예약 영역 (MIGRATE_CMA) base_pfn ~ base_pfn + count 평상시 (Idle) 유저 페이지 페이지 캐시 anon 페이지 cma_alloc() 시 마이그레이션 격리(Isolate) DMA 버퍼 Buddy Allocator MIGRATE_CMA 타입 CMA 코어 cma_alloc / cma_release 페이지 마이그레이션 migrate_pages() DMA API dma_alloc_coherent 이동 가능 페이지 CMA 할당 (DMA 버퍼) 마이그레이션 대상

MIGRATE_CMA 타입

CMA는 Buddy Allocator의 마이그레이션 타입 체계에 MIGRATE_CMA를 추가합니다.

/* include/linux/mmzone.h */
enum migratetype {
    MIGRATE_UNMOVABLE,      /* 이동 불가: 슬랩, 커널 스택 */
    MIGRATE_MOVABLE,        /* 이동 가능: 유저 페이지, 페이지 캐시 */
    MIGRATE_RECLAIMABLE,    /* 회수 가능: 파일 캐시 */
#ifdef CONFIG_CMA
    MIGRATE_CMA,            /* CMA 전용: movable만 배치 허용 */
#endif
    MIGRATE_PCPTYPES,
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
    MIGRATE_ISOLATE,        /* 격리: alloc_contig_range용 */
    MIGRATE_TYPES
};
코드 설명
  • 7행 MIGRATE_CMA는 CONFIG_CMA 활성화 시에만 존재합니다. 이 타입의 페이지 블록은 MIGRATE_MOVABLE 요청에 대해서만 할당을 허용합니다.
  • 10행 MIGRATE_ISOLATE는 CMA 할당 과정에서 alloc_contig_range()가 해당 범위를 임시로 격리할 때 사용합니다.

MIGRATE_CMA 타입의 핵심 규칙은 다음과 같습니다.

CMA 영역 선언과 초기화

CMA 영역은 부팅 초기 단계에서 선언되며, 두 가지 주요 경로로 설정합니다.

Device Tree를 통한 선언

/* 디바이스 트리에서 CMA 예약 메모리 선언 */
/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        /* 글로벌 기본 CMA 영역: 256MB */
        linux,cma {
            compatible = "shared-dma-pool";
            reusable;
            size = <0x0 0x10000000>;  /* 256 MB */
            alignment = <0x0 0x2000>;   /* 8KB 정렬 */
            linux,cma-default;
        };

        /* 디바이스 전용 CMA 영역: 고정 주소 */
        vpu_cma: vpu_reserved {
            compatible = "shared-dma-pool";
            reusable;
            reg = <0x0 0x60000000 0x0 0x08000000>;  /* 0x60000000 ~ 128MB */
        };
    };

    /* VPU 디바이스에 전용 CMA 영역 연결 */
    vpu: video-codec@38300000 {
        compatible = "nxp,imx8mq-vpu";
        memory-region = <&vpu_cma>;
    };
};
코드 설명
  • 9-15행 글로벌 기본 CMA 영역. linux,cma-default 속성이 있으면 dma_contiguous_default_area로 등록됩니다. reusable은 이동 가능 페이지 공존을 의미합니다.
  • 18-23행 reg 속성으로 고정 물리 주소(0x60000000)에 128MB 영역을 예약합니다. IOMMU가 없는 디바이스에서 특정 주소 범위가 필요할 때 사용합니다.
  • 27-29행 memory-region 속성으로 VPU 디바이스에 전용 CMA 영역을 연결합니다. 이 디바이스의 dma_alloc_coherent()는 글로벌 CMA 대신 vpu_cma를 사용합니다.

커널 파라미터를 통한 선언

# 글로벌 CMA 크기 지정 (부트 파라미터)
cma=256M              # 256MB 글로벌 CMA 영역
cma=256M@0x80000000   # 0x80000000에서 시작하는 256MB 고정 위치 CMA */
cma=10%               # 전체 메모리의 10%를 CMA로 예약 (커널 6.1+) */

# GRUB 설정 예시
GRUB_CMDLINE_LINUX="cma=256M"
CMA 초기화 흐름 1. Device Tree 파싱 reserved-memory 노드 2. memblock 예약 memblock_reserve() 3. cma_declare() struct cma 초기화 4. cma_init_reserved() 비트맵 할당, 이름 설정 5. Buddy 등록 MIGRATE_CMA 설정 6. 사용 가능 cma_alloc() 호출 가능 커널 내부 초기화 호출 체인 start_kernel() setup_arch() dma_contiguous_reserve() cma_declare_contiguous() mm_core_init() cma_init_reserved_areas() init_cma_reserved_pageblock()

초기화 코드 상세

/* kernel/dma/contiguous.c */
void __init dma_contiguous_reserve(phys_addr_t limit)
{
    phys_addr_t selected_size = 0;
    phys_addr_t selected_base = 0;
    phys_addr_t selected_limit = limit;

    /* 커널 파라미터 또는 Kconfig 기본값에서 크기 결정 */
    if (size_cmdline != -1)
        selected_size = size_cmdline;
    else
        selected_size = (phys_addr_t)size_bytes;

    if (selected_size) {
        pr_debug("%s: reserving %ld MiB for global area\n",
                 __func__, (unsigned long)selected_size / SZ_1M);

        dma_contiguous_reserve_area(selected_size,
            selected_base, selected_limit,
            &dma_contiguous_default_area, true);
    }
}
코드 설명
  • 2행 __init 섹션 함수로, 부팅 완료 후 메모리에서 해제됩니다. setup_arch()에서 호출됩니다.
  • 9-12행 커널 커맨드라인의 cma= 파라미터가 우선하며, 없으면 CONFIG_CMA_SIZE_MBYTES Kconfig 값을 사용합니다.
  • 18-20행 dma_contiguous_reserve_area()는 내부적으로 cma_declare_contiguous()를 호출하여 memblock에서 물리 메모리를 예약하고 struct cma를 초기화합니다.

CMA 할당 메커니즘

cma_alloc()은 CMA 영역에서 연속 물리 메모리를 할당하는 핵심 함수입니다. 내부적으로 alloc_contig_range()를 호출하여 지정 범위의 페이지를 격리하고 마이그레이션합니다.

cma_alloc() 할당 경로 cma_alloc(cma, count, align) 비트맵 검색 bitmap_find_next_zero_area() 비트맵 마킹 bitmap_set() - 임시 예약 표시 alloc_contig_range(pfn, pfn+count) 격리 + 마이그레이션 + 할당 성공: 페이지 반환 pfn_to_page(pfn) 실패: 재시도 bitmap_clear() + 다음 영역 검색 재시도 루프 NULL 반환 (모든 영역 실패) struct page* 반환 성공 실패

cma_alloc() 구현

/* mm/cma.c */
struct page *cma_alloc(struct cma *cma,
                       unsigned long count, unsigned int align,
                       bool no_warn)
{
    unsigned long mask, offset;
    unsigned long pfn = -1;
    unsigned long start = 0;
    unsigned long bitmap_maxno, bitmap_no, bitmap_count;
    size_t i;
    struct page *page = NULL;
    int ret = -ENOMEM;

    if (!cma || !cma->count || !cma->bitmap)
        return NULL;

    pr_debug("%s(cma %p, name: %s, count %lu, align %d)\n",
             __func__, (void *)cma, cma->name, count, align);

    if (!count)
        return NULL;

    mask = cma_bitmap_aligned_mask(cma, align);
    offset = cma_bitmap_aligned_offset(cma, align);
    bitmap_maxno = cma_bitmap_maxno(cma);
    bitmap_count = cma_bitmap_pages_to_bits(cma, count);

    if (bitmap_count > bitmap_maxno)
        return NULL;

    for (;;) {
        spin_lock_irq(&cma->lock);
        bitmap_no = bitmap_find_next_zero_area_off(
            cma->bitmap, bitmap_maxno, start,
            bitmap_count, mask, offset);
        if (bitmap_no >= bitmap_maxno) {
            spin_unlock_irq(&cma->lock);
            break;
        }
        bitmap_set(cma->bitmap, bitmap_no, bitmap_count);
        spin_unlock_irq(&cma->lock);

        pfn = cma->base_pfn + (bitmap_no << cma->order_per_bit);
        mutex_lock(&cma->cma_mutex);
        ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
                                GFP_KERNEL | (no_warn ? __GFP_NOWARN : 0));
        mutex_unlock(&cma->cma_mutex);

        if (ret == 0) {
            page = pfn_to_page(pfn);
            break;
        }

        cma_clear_bitmap(cma, pfn, count);
        if (ret != -EBUSY)
            break;

        start = bitmap_no + mask + 1;
    }

    return page;
}
코드 설명
  • 23-26행 정렬 요구사항을 비트맵 마스크/오프셋(Offset)으로 변환합니다. order_per_bit에 따라 비트맵 해상도가 달라집니다.
  • 33-35행 bitmap_find_next_zero_area_off()로 요청 크기만큼 연속된 빈 비트를 검색합니다.
  • 40행 검색된 비트 영역을 먼저 마킹하여 동시 할당 요청과의 충돌을 방지합니다.
  • 45-46행 alloc_contig_range()가 핵심입니다. 해당 PFN 범위를 격리하고, 기존 페이지를 마이그레이션한 후, 연속 물리 메모리를 확보합니다.
  • 53행 실패 시 비트맵을 클리어하고, -EBUSY(마이그레이션 실패)면 다음 영역에서 재시도합니다.

페이지 마이그레이션과 격리

CMA 할당의 핵심 메커니즘은 페이지 격리(isolation)마이그레이션(migration)입니다. alloc_contig_range()는 요청 범위의 페이지를 3단계로 처리합니다.

alloc_contig_range() 3단계 처리 CMA 영역 원본 상태 (이동 가능 페이지들) Page A Page B Page C 빈 공간 Page D Page E 빈 공간 1단계: 격리 (Isolate) start_isolate_page_range() MIGRATE_ISOLATE 전환: Buddy 할당 차단 2단계: 마이그레이션 __alloc_contig_migrate_range() Page A CMA 외부로 이동 Page B 이동 3단계: 버디 해제+회수 undo_isolate_page_range() 결과: 연속 물리 메모리 확보 연속 DMA 버퍼 (pfn ~ pfn+count) 마이그레이션 실패 원인 - 페이지가 pinned 상태 (get_user_pages) - writeback 진행 중인 페이지 - LRU에 없는 페이지 - MIGRATE_UNMOVABLE 페이지 (CMA에서 불가)

alloc_contig_range() 핵심 코드

/* mm/page_alloc.c */
int alloc_contig_range(unsigned long start, unsigned long end,
                       unsigned migratetype, gfp_t gfp_mask)
{
    unsigned long outer_start, outer_end;
    int order;
    int ret = 0;

    /* 1단계: 페이지 블록을 MIGRATE_ISOLATE로 전환 */
    ret = start_isolate_page_range(start, end,
                                   migratetype, 0, gfp_mask);
    if (ret)
        goto done;

    /* 2단계: 격리된 범위 내 사용 중인 페이지를 마이그레이션 */
    ret = __alloc_contig_migrate_range(&cc, start, end);
    if (ret && ret != -EBUSY)
        goto done;

    /* 남은 페이지가 모두 free인지 확인 */
    order = 0;
    outer_start = start;
    while (!PageBuddy(pfn_to_page(outer_start))) {
        if (++order >= MAX_ORDER) {
            outer_start = start;
            break;
        }
        outer_start &= ~0UL << order;
    }

    /* 3단계: 격리 해제 후 연속 페이지 확보 */
    undo_isolate_page_range(start, end, migratetype);

    /* free 페이지를 Buddy에서 분리하여 할당 완료 */
    return isolate_freepages_range(&cc, outer_start, outer_end);

done:
    undo_isolate_page_range(start, end, migratetype);
    return ret;
}

CMA와 Buddy Allocator 통합

CMA 영역은 Buddy Allocator의 free 페이지 관리 체계에 완전히 통합되어 있습니다. 핵심은 MIGRATE_CMA 타입의 fallback 동작입니다.

Buddy Allocator의 CMA fallback 동작 alloc_pages(GFP_MOVABLE) 1. MIGRATE_MOVABLE free_list[MOVABLE] 검색 2. MIGRATE_CMA fallback: free_list[CMA] 3. RECLAIMABLE fallback: free_list[RECLAIMABLE] alloc_pages(GFP_KERNEL) -- UNMOVABLE CMA fallback 금지 MIGRATE_CMA에서 UNMOVABLE 할당 차단 이동 불가 페이지 진입 방지 -> 마이그레이션 보장 fallback 우선순위 테이블 MOVABLE: CMA -> RECLAIMABLE -> UNMOVABLE UNMOVABLE: RECLAIMABLE -> MOVABLE (CMA 제외) RECLAIMABLE: UNMOVABLE -> MOVABLE (CMA 제외)
/* mm/page_alloc.c - fallback 테이블 */
static int fallbacks[MIGRATE_TYPES][3] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_TYPES },
    [MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_TYPES },
#ifdef CONFIG_CMA
    [MIGRATE_CMA]         = { MIGRATE_TYPES },  /* CMA: fallback 없음 */
#endif
};

/* __rmqueue_fallback()에서 CMA -> MOVABLE 할당만 허용 */
static bool can_steal_fallback(unsigned int order, int start_mt)
{
    /* MIGRATE_CMA 블록은 MOVABLE 요청에만 steal 허용 */
    if (start_mt == MIGRATE_CMA)
        return false;  /* CMA 블록의 migratetype 변경 금지 */
    ...
}
코드 설명
  • 4행 MIGRATE_MOVABLE 요청의 fallback에 MIGRATE_CMA가 첫 번째로 위치합니다. 즉, MOVABLE free_list가 비면 CMA 영역에서 페이지를 가져옵니다.
  • 3행 MIGRATE_UNMOVABLE 요청의 fallback에는 MIGRATE_CMA가 없습니다. 슬랩, 커널 스택 등 이동 불가 페이지는 CMA 영역에 진입할 수 없습니다.
  • 15-16행 can_steal_fallback()에서 CMA 블록의 migratetype 변경(steal)을 금지합니다. CMA 영역은 항상 MIGRATE_CMA를 유지합니다.

CMA 영역의 Buddy 초기화 코드

CMA 영역은 부팅 시 Buddy Allocator에 MIGRATE_CMA 타입으로 등록됩니다. 이 과정에서 CMA 페이지가 일반 할당에도 사용 가능하되, 이동 불가 할당은 차단되는 핵심 메커니즘이 설정됩니다:

/* mm/cma.c - CMA 영역을 Buddy Allocator에 등록 */
static int __init cma_activate_area(struct cma *cma)
{
    unsigned long pfn = cma->base_pfn;
    unsigned long count = cma->count;
    unsigned int i = cma->count >> pageblock_order;

    while (i--) {
        /* 각 pageblock의 migratetype을 MIGRATE_CMA로 설정
         * → Buddy의 free_list[MIGRATE_CMA]에 등록됨 */
        init_cma_reserved_pageblock(
            pfn_to_page(pfn));
        pfn += pageblock_nr_pages;
    }

    /* 비트맵 초기화: 할당 상태 추적용 */
    bitmap_zero(cma->bitmap, cma->count);
    return 0;
}

/* mm/page_alloc.c - CMA 페이지를 Buddy free_list에 추가 */
void __init init_cma_reserved_pageblock(struct page *page)
{
    unsigned long pfn = page_to_pfn(page);
    struct zone *zone = page_zone(page);
    unsigned int order;

    for (order = 0; order < MAX_PAGE_ORDER; order++) {
        struct page *buddy = find_buddy_page_pfn(page, pfn, order, NULL);
        if (!buddy)
            break;
        /* 버디와 병합하여 더 큰 블록 생성 */
        del_page_from_free_list(buddy, zone, order);
    }

    /* MIGRATE_CMA 타입으로 free_list에 추가 */
    set_pageblock_migratetype(page, MIGRATE_CMA);
    __free_pages(page, pageblock_order);
    adjust_managed_page_count(page, pageblock_nr_pages);

    /* CMA 페이지는 zone->managed_pages에 포함
     * → /proc/meminfo의 MemFree에 반영됨
     * → MOVABLE 할당 요청 시 자유롭게 사용 가능 */
}
코드 설명
  • 10-12행 init_cma_reserved_pageblock()은 각 pageblock(보통 2MB)을 MIGRATE_CMA로 설정하여 Buddy에 등록합니다. 이 타입은 MOVABLE 요청의 fallback으로만 사용됩니다.
  • 36행 set_pageblock_migratetype(page, MIGRATE_CMA)는 pageblock 플래그를 설정합니다. Buddy Allocator는 이 플래그를 확인하여 UNMOVABLE 할당을 차단합니다.
  • 39행 adjust_managed_page_count()로 zone의 managed 페이지 수를 증가시킵니다. CMA 영역이 "사용 가능한 빈 메모리"로 인식되어 시스템 메모리 효율이 높아집니다.
주의: CMA 영역에 MIGRATE_UNMOVABLE 페이지가 배치되면 마이그레이션이 불가능해져 cma_alloc()이 실패합니다. 이것이 Buddy Allocator의 fallback 제한이 핵심인 이유입니다.

DMA-CMA 통합

DMA 서브시스템은 dma_alloc_coherent()를 통해 CMA를 자동으로 활용합니다. 디바이스 드라이버는 CMA를 직접 호출할 필요 없이 표준 DMA API를 사용하면 됩니다.

dma_alloc_coherent() -> CMA 경로 dma_alloc_coherent(dev, size, ...) dma_alloc_attrs() dma_direct_alloc() / iommu_dma_alloc() dma_alloc_from_contiguous() per-device CMA 또는 글로벌 CMA alloc_pages() CMA 없거나 소규모 할당 CMA 영역 존재 fallback cma_alloc() Buddy Allocator

DMA-CMA 연동 코드

/* kernel/dma/contiguous.c */
struct page *dma_alloc_from_contiguous(
    struct device *dev, size_t count,
    unsigned int align, bool no_warn)
{
    /* per-device CMA가 있으면 우선 사용 */
    if (dev && dev->cma_area)
        return cma_alloc(dev->cma_area, count, align, no_warn);

    /* 없으면 글로벌 기본 CMA 사용 */
    return cma_alloc(dma_contiguous_default_area, count, align, no_warn);
}

/* 드라이버에서의 사용 예시 */
static int my_driver_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    dma_addr_t dma_handle;
    void *vaddr;
    size_t size = SZ_4M;  /* 4MB 연속 버퍼 */

    /* CMA를 자동으로 사용 (드라이버는 CMA를 인식하지 않아도 됨) */
    vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
    if (!vaddr)
        return -ENOMEM;

    dev_info(dev, "CMA buffer: vaddr=%p dma=%pad size=%zu\n",
             vaddr, &dma_handle, size);

    /* 사용 후 해제 */
    dma_free_coherent(dev, size, vaddr, dma_handle);
    return 0;
}
코드 설명
  • 7-8행 dev->cma_area는 Device Tree의 memory-region 속성에서 설정됩니다. per-device CMA가 있으면 글로벌 CMA와 분리하여 경합을 줄입니다.
  • 11행 dma_contiguous_default_area는 부팅 시 linux,cma-default 또는 cma= 파라미터로 설정된 글로벌 CMA 영역입니다.
  • 23행 드라이버는 dma_alloc_coherent()만 호출하면 됩니다. CMA 사용 여부는 DMA 서브시스템이 자동으로 결정합니다.

DMA-BUF와 CMA 연동

DMA-BUF는 커널 드라이버 간 메모리 버퍼를 공유하는 프레임워크입니다. GPU, 카메라, 디스플레이 등 여러 디바이스가 동일한 물리 메모리를 참조할 때 사용합니다. CMA 기반 DMA-BUF heap을 통해 대용량 연속 버퍼를 효율적으로 공유할 수 있습니다:

/*
 * drivers/dma-buf/heaps/cma_heap.c
 * CMA heap: DMA-BUF를 통한 CMA 버퍼 공유
 */
static struct dma_buf *cma_heap_allocate(
    struct dma_heap *heap,
    unsigned long len,
    unsigned long fd_flags,
    unsigned long heap_flags)
{
    struct cma_heap *cma_heap = dma_heap_get_drvdata(heap);
    struct cma_heap_buffer *buffer;
    size_t size = PAGE_ALIGN(len);
    unsigned long nr_pages = size >> PAGE_SHIFT;

    buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);

    /* CMA에서 물리 연속 페이지 할당 */
    buffer->pages = cma_alloc(cma_heap->cma,
            nr_pages, 0, false);
    if (!buffer->pages) {
        kfree(buffer);
        return ERR_PTR(-ENOMEM);
    }

    /* DMA-BUF 생성: 여러 디바이스가 이 버퍼를 공유 가능 */
    DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
    exp_info.ops = &cma_heap_buf_ops;
    exp_info.size = size;
    exp_info.priv = buffer;

    buffer->dmabuf = dma_buf_export(&exp_info);
    return buffer->dmabuf;
}

/* 유저스페이스에서 CMA DMA-BUF 사용 예시 */
/*
 * 1. /dev/dma_heap/linux,cma 열기
 * 2. ioctl(DMA_HEAP_IOCTL_ALLOC)으로 fd 획득
 * 3. mmap()으로 유저공간 매핑
 * 4. 다른 디바이스와 fd 공유 (import)
 */
코드 설명
  • 19-20행 cma_alloc()으로 CMA 영역에서 물리 연속 페이지를 할당합니다. DMA-BUF heap은 이 페이지를 dma_buf 객체로 감싸서 여러 디바이스가 공유할 수 있게 합니다.
  • 27-31행 dma_buf_export()는 CMA 버퍼를 DMA-BUF 프레임워크에 등록합니다. GPU가 렌더링한 프레임을 디스플레이가 읽거나, 카메라가 캡처한 이미지를 ISP가 처리할 때 메모리 복사 없이 동일 물리 버퍼를 참조합니다.

CMA 해제 경로

cma_release()는 CMA에서 할당된 페이지를 해제하여 Buddy Allocator에 반환합니다. 해제된 페이지는 다시 MIGRATE_CMA free_list에 들어가 이동 가능 할당에 사용됩니다.

/* mm/cma.c */
bool cma_release(struct cma *cma,
                 const struct page *pages,
                 unsigned long count)
{
    unsigned long pfn;

    if (!cma || !pages)
        return false;

    pfn = page_to_pfn(pages);

    if (pfn < cma->base_pfn ||
        pfn >= cma->base_pfn + cma->count)
        return false;

    /* 페이지를 Buddy Allocator에 반환 */
    free_contig_range(pfn, count);

    /* CMA 비트맵에서 해당 영역 클리어 */
    cma_clear_bitmap(cma, pfn, count);

    return true;
}

/* DMA API를 통한 해제 */
bool dma_release_from_contiguous(
    struct device *dev, struct page *pages, int count)
{
    return cma_release(
        dev_get_cma_area(dev), pages, count);
}
코드 설명
  • 11-15행 해제할 페이지가 해당 CMA 영역에 속하는지 PFN 범위를 검증합니다.
  • 18행 free_contig_range()는 연속 페이지를 Buddy Allocator에 반환합니다. 반환된 페이지는 MIGRATE_CMA free_list에 다시 들어갑니다.
  • 21행 비트맵을 클리어하여 해당 영역이 다시 cma_alloc()에 의해 할당될 수 있도록 합니다.
CMA 해제 경로 흐름 dma_free_coherent(dev, size, vaddr, dma) dma_release_from_contiguous(dev, pages, count) cma_release(cma, pages, count) PFN 범위 검증 (base_pfn ~ base_pfn+count) free_contig_range(pfn, count) 연속 페이지를 Buddy에 반환 cma_clear_bitmap(cma, pfn, count) 비트맵 해당 비트 클리어 __free_pages() 반복 호출 각 페이지 refcount=0 확인 → free_list[CMA] 삽입 비트맵 갱신 완료 spin_lock(&cma->lock) → bitmap_clear() → unlock CMA 영역 재활용 가능 상태 MIGRATE_CMA free_list → 이동 가능 페이지 배치 허용

free_contig_range() 내부 동작

/* mm/page_alloc.c */
void free_contig_range(unsigned long pfn, unsigned long nr_pages)
{
    unsigned long count = 0;

    for (; nr_pages--; pfn++) {
        struct page *page = pfn_to_page(pfn);

        count += page_count(page) != 1;
        __free_page(page);  /* refcount 감소 → 0이면 Buddy 반환 */
    }
    WARN(count != 0, "%lu pages are still in use!\n", count);
}
코드 설명
  • 7행 각 페이지를 순회하며 __free_page()로 해제합니다. CMA에서 할당된 페이지는 alloc_contig_range()에서 refcount를 1로 설정했으므로 즉시 free됩니다.
  • 9행 page_count() != 1인 페이지는 다른 참조가 남아 있어 아직 사용 중입니다. 이런 경우 WARN을 출력하여 use-after-free 가능성을 경고합니다.
  • 11행 refcount가 0이 아닌 페이지가 존재하면 커널 경고가 발생합니다. 이는 드라이버가 CMA 버퍼를 완전히 해제하지 않은 버그를 나타냅니다.

해제 시 Buddy 병합 동작

__free_page()로 반환된 페이지는 Buddy Allocator의 병합 과정을 거칩니다. 연속 CMA 블록이 한 번에 해제되면, 인접 free 페이지와 병합되어 더 큰 order의 free 블록을 형성합니다.

해제 단계동작결과
1. refcount 감소put_page_testzero()refcount → 0 확인
2. free_list 삽입__free_one_page()MIGRATE_CMA free_list에 삽입
3. 버디 병합인접 페이지 order 비교동일 order free 블록과 병합
4. 반복 병합병합 가능하면 order 증가최대 MAX_ORDER-1까지 병합
5. zone 통계 갱신__mod_zone_freepage_state()NR_FREE_CMA_PAGES 증가
해제 순서 주의: DMA 전송이 진행 중일 때 CMA 버퍼를 해제하면 DMA 컨트롤러가 이미 해제된 메모리에 쓰는 use-after-free 버그가 발생합니다. 반드시 dma_unmap_*() 또는 DMA 전송 완료 확인 후 dma_free_coherent()를 호출하십시오.
해제 후 재활용: 해제된 CMA 페이지는 즉시 이동 가능 할당(유저 프로세스, 페이지 캐시)에 사용될 수 있습니다. CMA의 "지연 격리" 설계 덕분에 메모리가 유휴 상태(Idle State)로 방치되지 않습니다.

per-device CMA와 글로벌 CMA

리눅스 커널은 단일 글로벌 CMA 영역과 디바이스별 전용 CMA 영역을 모두 지원합니다.

특성글로벌 CMAper-device CMA
설정 방법cma= 파라미터 / linux,cma-defaultDevice Tree memory-region
구조체dma_contiguous_default_areadev->cma_area
공유모든 디바이스가 공유특정 디바이스 전용
장점설정 간단, 메모리 효율적경합 방지, 할당 실패율 감소
단점여러 디바이스 경합 가능전용 영역 크기만큼 고정 예약
적합한 경우DMA 사용량이 적은 시스템실시간(Real-time) 미디어, 카메라 파이프라인(Pipeline)
글로벌 CMA vs per-device CMA 물리 메모리 글로벌 CMA (256MB) VPU CMA (128MB) Camera CMA (64MB) NIC Audio VPU (Video) Camera ISP 글로벌 글로벌 전용 전용 선택 기준 - NIC, Audio: 작은 DMA 버퍼 -> 글로벌 CMA로 충분 - VPU, Camera: 대용량 + 실시간 -> per-device CMA로 경합 방지 필수

디바이스별 CMA 영역 Device Tree 설정

per-device CMA 영역은 Device Tree의 reserved-memory 노드에 정의하고, 각 디바이스 노드에서 memory-region 속성으로 참조합니다. 아래는 비디오 프로세서(VPU)와 카메라(ISP)에 전용 CMA 영역을 할당하는 예시입니다:

/* Device Tree 예시: per-device CMA 영역 정의 */
/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        /* 글로벌 기본 CMA: 256MB */
        linux,cma {
            compatible = "shared-dma-pool";
            reusable;
            size = <0x0 0x10000000>;  /* 256MB */
            linux,cma-default;        /* 기본 CMA로 지정 */
        };

        /* VPU 전용 CMA: 128MB at 0x60000000 */
        vpu_cma: vpu_cma_region {
            compatible = "shared-dma-pool";
            reusable;
            reg = <0x0 0x60000000 0x0 0x08000000>;  /* 128MB */
            /* 고정 주소: 특정 하드웨어 주소 제약 충족 */
        };

        /* Camera ISP 전용 CMA: 64MB (동적 배치) */
        camera_cma: camera_cma_region {
            compatible = "shared-dma-pool";
            reusable;
            size = <0x0 0x04000000>;  /* 64MB */
            alignment = <0x0 0x00400000>;  /* 4MB 정렬 */
        };
    };

    /* 디바이스 노드에서 전용 CMA 참조 */
    vpu: video-codec@38300000 {
        compatible = "vendor,vpu";
        reg = <0x0 0x38300000 0x0 0x10000>;
        memory-region = <&vpu_cma>;  /* → dev->cma_area = vpu_cma */
    };

    camera: isp@32e00000 {
        compatible = "vendor,camera-isp";
        reg = <0x0 0x32e00000 0x0 0x4000>;
        memory-region = <&camera_cma>;  /* → dev->cma_area = camera_cma */
    };

    /* memory-region 없는 디바이스: 글로벌 CMA 사용 */
    ethernet: ethernet@30be0000 {
        compatible = "vendor,eth";
        reg = <0x0 0x30be0000 0x0 0x10000>;
        /* memory-region 없음 → dma_contiguous_default_area 사용 */
    };
};
코드 설명
  • linux,cma-default linux,cma-default 속성은 이 영역을 글로벌 기본 CMA로 지정합니다. memory-region을 명시하지 않은 모든 디바이스가 이 영역을 사용합니다.
  • vpu_cma: reg 속성 reg 속성으로 고정 물리 주소를 지정할 수 있습니다. VPU처럼 특정 주소 범위에만 DMA 가능한 하드웨어에 필요합니다. size만 지정하면 커널이 위치를 자동 결정합니다.
  • memory-region 참조 memory-region = <&vpu_cma>는 부팅 시 of_reserved_mem_device_init()을 통해 dev->cma_area를 설정합니다. 이후 dma_alloc_coherent() 호출 시 글로벌 대신 전용 CMA에서 할당합니다.
💡

설계 지침: per-device CMA는 경합을 방지하지만, 전용 크기만큼 메모리가 고정됩니다. 대용량 실시간 요구(비디오 디코딩, 카메라 스트리밍)에만 사용하고, 소규모 DMA(네트워크, 오디오)는 글로벌 CMA를 공유하는 것이 메모리 효율적입니다. reserved-memoryreusable 속성이 CMA의 핵심입니다. 이 속성이 없으면(no-map) 영역이 Buddy에 등록되지 않아 일반 할당에 사용할 수 없습니다.

per-device CMA 연결 코드

/* drivers/of/of_reserved_mem.c */
int of_reserved_mem_device_init_by_idx(
    struct device *dev,
    struct device_node *np, int idx)
{
    struct reserved_mem *rmem;

    rmem = of_reserved_mem_lookup(np);
    if (!rmem)
        return -ENODEV;

    /* rmem->ops->device_init()이 dev->cma_area 설정 */
    return rmem->ops->device_init(rmem, dev);
}

/* kernel/dma/contiguous.c */
static int rmem_cma_device_init(
    struct reserved_mem *rmem, struct device *dev)
{
    /* Device Tree memory-region에서 지정된 CMA 영역을 디바이스에 연결 */
    dev->cma_area = rmem->priv;  /* priv = struct cma* */
    return 0;
}

CMA와 IOMMU 상호작용

IOMMU(I/O Memory Management Unit)가 존재하면 디바이스는 가상 연속(virtually contiguous) DMA 주소를 사용할 수 있어, 물리적으로 연속된 CMA 버퍼가 반드시 필요하지 않습니다. CMA의 필요 여부는 디바이스의 IOMMU 지원과 DMA 특성에 따라 결정됩니다.

CMA vs IOMMU: 연속 메모리 요구 결정 트리 DMA 버퍼 할당 요청 IOMMU 존재? No CMA 필수 물리 연속 필요 Yes S/G 지원? Yes CMA 불필요 IOMMU S/G로 충분 No HW 연속 필수? Yes CMA 필수 IOMMU 있어도 No CMA 불필요 IOMMU 매핑 사용 IOMMU 존재 시에도 CMA가 필요한 경우 - 디스플레이 컨트롤러: HW가 물리 연속 스캔아웃 버퍼를 직접 요구 (IOMMU bypass) - 비디오 코덱 참조 프레임: HW 내부 DMA가 IOMMU를 우회하는 경로 - 보안 펌웨어 버퍼: TrustZone/TEE에서 IOMMU 없이 직접 물리 주소 접근

IOMMU 환경에서의 DMA 할당 경로

조건할당 경로물리 연속 여부CMA 사용
IOMMU 없음dma_direct_alloc()필수사용
IOMMU 있음 + S/G 지원iommu_dma_alloc()불필요미사용
IOMMU 있음 + S/G 미지원iommu_dma_alloc() → CMA fallback필요사용
DMA_ATTR_FORCE_CONTIGUOUSCMA 강제필수사용
IOMMU bypass 디바이스dma_direct_alloc()필수사용
/* kernel/dma/mapping.c -- IOMMU 존재 시 분기 */
static void *__dma_alloc_pages(
    struct device *dev, size_t size,
    dma_addr_t *dma_handle, gfp_t gfp)
{
    const struct dma_map_ops *ops = get_dma_ops(dev);

    if (ops && ops->alloc) {
        /* IOMMU ops: iommu_dma_alloc()
         * → 산재 페이지 + IOMMU 매핑으로 가상 연속 DMA 주소 생성
         * → CMA 불필요 (일반적) */
        return ops->alloc(dev, size, dma_handle, gfp, attrs);
    }

    /* IOMMU 없음: dma_direct_alloc()
     * → CMA 또는 Buddy에서 물리 연속 메모리 할당 */
    return dma_direct_alloc(dev, size, dma_handle, gfp, attrs);
}

/* 드라이버에서 강제 연속 할당 요청 */
vaddr = dma_alloc_attrs(dev, size, &dma_handle,
    GFP_KERNEL, DMA_ATTR_FORCE_CONTIGUOUS);
/* → IOMMU 존재와 무관하게 CMA에서 연속 블록 할당 */
코드 설명
  • 8-12행 IOMMU가 활성화된 디바이스는 iommu_dma_alloc()를 통해 산재(scattered) 페이지를 IOMMU 페이지 테이블에 매핑하여 DMA 주소 공간에서 연속으로 보이게 합니다.
  • 15-17행 IOMMU가 없으면 dma_direct_alloc()이 호출되며, 내부에서 CMA를 우선 시도합니다.
  • 21-22행 DMA_ATTR_FORCE_CONTIGUOUS 플래그를 사용하면 IOMMU 존재와 무관하게 물리적으로 연속된 CMA 블록을 할당합니다.
ARM SoC 실무: 대부분의 ARM SoC는 SMMU(System MMU)를 탑재하지만, 디스플레이 컨트롤러와 일부 비디오 코덱은 SMMU를 우회하는 경로로 DMA를 수행합니다. 이런 디바이스는 IOMMU가 있어도 반드시 per-device CMA를 설정해야 합니다. IOMMU 세부 내용은 IOMMU 페이지를 참조하십시오.

CMA 디버깅

CMA 관련 문제를 진단하기 위한 여러 도구와 인터페이스가 제공됩니다.

debugfs 인터페이스

# CONFIG_CMA_DEBUGFS=y 필요
# CMA 영역 목록 확인
ls /sys/kernel/debug/cma/

# 특정 CMA 영역 상태 확인
cat /sys/kernel/debug/cma/cma-reserved/alloc
cat /sys/kernel/debug/cma/cma-reserved/free
cat /sys/kernel/debug/cma/cma-reserved/base_pfn
cat /sys/kernel/debug/cma/cma-reserved/count
cat /sys/kernel/debug/cma/cma-reserved/order_per_bit
cat /sys/kernel/debug/cma/cma-reserved/bitmap

# CMA 할당 횟수/실패 통계
cat /sys/kernel/debug/cma/cma-reserved/alloc_pages_success
cat /sys/kernel/debug/cma/cma-reserved/alloc_pages_fail

커널 로그 (dmesg)

# 부팅 시 CMA 예약 메시지
dmesg | grep -i cma
# 출력 예시:
# [    0.000000] cma: Reserved 256 MiB at 0x0000000060000000
# [    0.000000] cma: Reserved 128 MiB at 0x0000000070000000 on node 0

# CMA 할당 실패 메시지
dmesg | grep "cma: alloc"
# [  123.456789] cma: cma_alloc: alloc failed, req-size: 1024 pages, ret: -16

# CONFIG_CMA_DEBUG=y 활성화 시 상세 로그
dmesg | grep "cma:"
# [  123.456789] cma: cma_alloc(cma 0xffff..., name: vpu, count 256, align 8)

/proc/meminfo CMA 항목

# 시스템 전체 CMA 사용량 확인
grep -i cma /proc/meminfo
# CmaTotal:         262144 kB    <- 전체 CMA 영역 크기
# CmaFree:          245760 kB    <- 현재 미사용 (이동 가능 페이지 포함)

# CmaFree 의미: CMA 비트맵에서 할당되지 않은 영역
# CmaTotal - CmaFree = 현재 CMA에서 DMA 목적으로 할당된 크기

ftrace를 이용한 CMA 추적

# CMA 관련 tracepoint 활성화
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_start/enable
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_finish/enable
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_release/enable

# CMA 할당 이벤트 추적
cat /sys/kernel/debug/tracing/trace_pipe
# v4l2-0  [002] 123.456: cma_alloc_start: name=vpu pfn=0x60000 count=1024 align=8
# v4l2-0  [002] 123.460: cma_alloc_finish: name=vpu pfn=0x60000 count=1024 page=0xffff...
디버깅 유의사항: CONFIG_CMA_DEBUG는 할당마다 상세 로그를 출력하므로 성능에 영향을 줍니다. 운영 환경에서는 비활성화하고, CONFIG_CMA_DEBUGFS만 활성화하여 통계 기반으로 모니터링하세요.

커널 설정과 Device Tree 바인딩

Kconfig 옵션

# CMA 핵심 옵션
CONFIG_CMA=y                    # CMA 서브시스템 활성화
CONFIG_DMA_CMA=y                # DMA-CMA 통합 활성화
CONFIG_CMA_SIZE_MBYTES=256      # 기본 CMA 크기 (MB)
CONFIG_CMA_SIZE_PERCENTAGE=0    # 전체 메모리 대비 % (0=미사용)
CONFIG_CMA_SIZE_SEL_MBYTES=y    # MB 단위 크기 선택
CONFIG_CMA_ALIGNMENT=8          # 최소 정렬 (2^8 = 256 페이지 = 1MB)

# CMA 디버깅 옵션
CONFIG_CMA_DEBUG=y              # 상세 커널 로그 출력
CONFIG_CMA_DEBUGFS=y            # debugfs 인터페이스 제공
CONFIG_CMA_SYSFS=y              # sysfs 인터페이스 (커널 5.16+)
CONFIG_CMA_AREAS=19             # 최대 CMA 영역 수 (글로벌 + per-device)

Device Tree 바인딩 상세

/* 다중 CMA 영역 구성 예시 */
/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        /* 글로벌 기본 CMA */
        default_cma: linux,cma {
            compatible = "shared-dma-pool";
            reusable;
            size = <0x0 0x10000000>;
            linux,cma-default;
        };

        /* GPU 전용 CMA: 특정 물리 주소 범위 필수 */
        gpu_cma: gpu_reserved {
            compatible = "shared-dma-pool";
            reusable;
            reg = <0x0 0x80000000 0x0 0x10000000>;
            /* 0x80000000에서 256MB */
        };

        /* no-map 영역: CMA가 아닌 전용 예약 (참고용) */
        secure_mem: secure_reserved {
            compatible = "shared-dma-pool";
            no-map;  /* reusable이 아님: 커널이 사용 불가 */
            reg = <0x0 0x90000000 0x0 0x01000000>;
        };
    };
};
reusable vs no-map: reusable은 CMA 방식으로 이동 가능 페이지와 공존하며, no-map은 커널 페이지 테이블(Page Table)에서 완전히 제외하여 보안 펌웨어(Firmware) 등 전용 용도로 사용합니다. CMA는 반드시 reusable을 사용합니다.

성능 최적화와 단편화 방지

CMA 할당 성능은 주로 마이그레이션 비용에 좌우됩니다. 다음 전략으로 할당 지연과 실패율을 줄일 수 있습니다.

CMA 성능 최적화 전략 사전 할당 (Pre-alloc) probe() 시점에 CMA 버퍼 미리 확보하여 지연 방지 per-device CMA 디바이스별 전용 영역으로 경합 제거 적절한 CMA 크기 너무 크면 메모리 낭비 너무 작으면 할당 실패 Pinned 페이지 최소화 get_user_pages() 사용 자제 마이그레이션 실패 원인 제거 정렬 최적화 order_per_bit 조정으로 비트맵 검색 효율 향상 compaction 연계 echo 1 > compact_memory 사전 단편화 해소 CMA 성능 벤치마크 참고 수치 - 이동 가능 페이지만 존재: cma_alloc 16MB -> 약 1~5ms (마이그레이션 비용) - Pinned 페이지 존재: cma_alloc 실패 후 재시도 -> 수십~수백 ms 또는 실패 - 빈 CMA 영역: cma_alloc -> 약 0.1ms 미만 (마이그레이션 불필요)

실전 최적화 팁

/* 최적화 1: probe 시점 사전 할당 */
static int camera_probe(struct platform_device *pdev)
{
    struct camera_dev *cam;
    int i;

    cam = devm_kzalloc(&pdev->dev, sizeof(*cam), GFP_KERNEL);

    /* 부팅 초기 (단편화 없을 때) 프레임 버퍼 미리 할당 */
    for (i = 0; i < NUM_BUFFERS; i++) {
        cam->bufs[i] = dma_alloc_coherent(
            &pdev->dev, FRAME_SIZE,
            &cam->dma_addrs[i], GFP_KERNEL);
        if (!cam->bufs[i])
            goto err_free;
    }

    return 0;

err_free:
    while (--i >= 0)
        dma_free_coherent(&pdev->dev, FRAME_SIZE,
                         cam->bufs[i], cam->dma_addrs[i]);
    return -ENOMEM;
}

CMA 크기 산정 실전 가이드

CMA 영역 크기를 적절히 산정하는 것은 시스템 안정성과 메모리 효율의 핵심입니다. 너무 작으면 할당 실패가 발생하고, 너무 크면 일반 메모리가 부족해집니다.

CMA 크기 산정 공식 CMA 크기 = Σ(디바이스별 최대 동시 버퍼) × 안전 마진(1.5~2.0x) 단편화 대비 + 마이그레이션 지연 허용 + 동시 사용 피크(Peak) 워크로드별 CMA 요구량 분석 카메라 ISP 해상도: 4032×3024 (12MP) 포맷: RAW10 = 15MB/frame 버퍼 수: 4 (HAL3 triple + 1) = 60MB 비디오 디코더 (4K) 해상도: 3840×2160 포맷: NV12 = 12MB/frame 참조 프레임: 16 (H.265) = 192MB 디스플레이 (4K) 해상도: 3840×2160 포맷: ARGB8888 = 32MB/frame 버퍼 수: 3 (triple buffer) = 96MB 합산: 60 + 192 + 96 = 348MB × 안전 마진 1.5 = 522MB → cma=512M 설정 시스템 유형별 CMA 권장 범위 IoT/임베디드(1080p): 64~128MB | 모바일 SoC(4K): 256~512MB | 서버/HPC: 1~4GB (HugeTLB 포함)

디바이스별 CMA 소비량 참조 테이블

디바이스/워크로드해상도버퍼당 크기동시 버퍼 수CMA 요구량
카메라 ISP (12MP RAW10)4032×3024~15 MB460 MB
카메라 ISP (48MP RAW10)8000×6000~60 MB4240 MB
비디오 디코더 (1080p H.264)1920×1080~3 MB16+460 MB
비디오 디코더 (4K H.265)3840×2160~12 MB16+4240 MB
비디오 인코더 (4K)3840×2160~12 MB448 MB
디스플레이 (1080p ARGB)1920×1080~8 MB324 MB
디스플레이 (4K ARGB)3840×2160~32 MB396 MB
GPU 프레임버퍼다양8~64 MB2~316~192 MB
DSP/NPU 가속기N/A1~16 MB2~42~64 MB

CMA 크기 산정 공식

# CMA 크기 산정 공식
CMA_SIZE = (Σ peak_usage_per_device) × safety_margin

# 예시 1: 모바일 SoC (카메라 + 비디오 + 디스플레이 동시 사용)
camera_12mp  = 4032 × 3024 × 10/8 × 4 buffers = 60 MB
decoder_4k   = 3840 × 2160 × 1.5  × 20 refs   = 240 MB
display_4k   = 3840 × 2160 × 4    × 3 buffers  = 96 MB
-------------------------------------------------
subtotal     = 396 MB
safety (1.3) = 515 MB → cma=512M

# 예시 2: 임베디드 IoT (카메라 + 소형 디스플레이)
camera_2mp   = 1920 × 1080 × 10/8 × 4 buffers = 10 MB
display_720p = 1280 × 720  × 4    × 3 buffers  = 11 MB
-------------------------------------------------
subtotal     = 21 MB
safety (2.0) = 42 MB → cma=64M

# 예시 3: 서버 (HugeTLB + GPU)
hugetlb_1g   = 1 GB × 4 pages  = 4 GB
gpu_fb       = 256 MB × 2      = 512 MB
-------------------------------------------------
subtotal     = 4.5 GB → hugetlb_cma=4G,4G (per-NUMA)

안전 마진(Safety Margin) 선정 기준

안전 마진적용 상황근거
1.2xper-device CMA (전용 영역)경합 없음, 단편화 최소
1.5x글로벌 CMA (다수 디바이스 공유)동시 할당 경합, 마이그레이션 지연
2.0x장시간 운영 임베디드 시스템단편화 누적, pinned page 증가
2.5x+프로덕션 안정성 최우선 시스템최악의 경우 대비
과도한 CMA 크기 주의: CMA 영역이 전체 메모리의 50%를 초과하면, 이동 불가 할당(커널 슬랩, 페이지 테이블)을 위한 공간이 부족해져 OOM이 발생할 수 있습니다. 일반적으로 전체 물리 메모리의 25~40%를 CMA 상한으로 권장합니다.
실측 기반 산정: 이론적 계산 후에는 반드시 실제 워크로드에서 /proc/meminfoCmaFree 값을 모니터링하여 피크 사용량을 확인하십시오. CmaTotal - CmaFree의 최대값이 실제 필요한 CMA 크기입니다.

실전 사용 사례

CMA는 주로 임베디드/모바일 SoC에서 멀티미디어 파이프라인에 사용됩니다.

1. 비디오 디코더 (V4L2)

/* drivers/media/ 계열 V4L2 비디오 디코더 */
static int vdec_queue_setup(
    struct vb2_queue *vq,
    unsigned int *nbuffers,
    unsigned int *nplanes,
    unsigned int sizes[],
    struct device *alloc_devs[])
{
    struct vdec_ctx *ctx = vb2_get_drv_priv(vq);

    /* 1080p YUV420: 1920*1080*1.5 = 약 3MB per frame */
    /* 4K YUV420: 3840*2160*1.5 = 약 12MB per frame */
    sizes[0] = ctx->width * ctx->height * 3 / 2;
    *nplanes = 1;
    *nbuffers = max(*nbuffers, (unsigned int)4);

    /* vb2-dma-contig 메모리 유형 -> CMA 자동 사용 */
    dev_info(ctx->dev, "CMA buffers: %u x %u bytes\n",
             *nbuffers, sizes[0]);

    return 0;
}

/* vb2-dma-contig.c 내부에서 CMA 할당 */
static void *vb2_dc_alloc(
    struct vb2_buffer *vb,
    struct device *dev,
    unsigned long size)
{
    /* 최종적으로 dma_alloc_coherent() -> CMA 경로 */
    buf->vaddr = dma_alloc_coherent(
        dev, size, &buf->dma_addr, GFP_KERNEL);
    ...
}

2. ARM SoC 카메라 파이프라인

/* Qualcomm MSM/SDM SoC 카메라 CMA 구성 */
reserved-memory {
    camera_cma: camera_reserved {
        compatible = "shared-dma-pool";
        reusable;
        size = <0x0 0x04000000>;  /* 64MB */
        alignment = <0x0 0x2000>;
    };
};

/* 카메라 ISP에 전용 CMA 연결 */
cam_isp: isp@ac00000 {
    compatible = "qcom,sdm845-isp";
    memory-region = <&camera_cma>;
    /* ISP가 dma_alloc_coherent() 호출 시 camera_cma에서 할당 */
};

3. GPU 프레임버퍼 (DRM)

/* DRM GEM CMA 헬퍼 */
struct drm_gem_cma_object *drm_gem_cma_create(
    struct drm_device *drm, size_t size)
{
    struct drm_gem_cma_object *cma_obj;

    cma_obj = kzalloc(sizeof(*cma_obj), GFP_KERNEL);

    /* CMA 기반 연속 메모리 할당 */
    cma_obj->vaddr = dma_alloc_wc(
        drm->dev, size,
        &cma_obj->dma_addr, GFP_KERNEL);

    /* Write-Combine 매핑: 디스플레이 스캔아웃 최적화 */
    ...
    return cma_obj;
}
vb2-dma-contig: V4L2의 videobuf2 프레임워크는 vb2-dma-contig 메모리 유형을 통해 CMA를 추상화합니다. 대부분의 미디어 드라이버는 이 프레임워크를 통해 간접적으로 CMA를 사용합니다.

DMA-BUF Heaps 통합

Linux 5.6부터 Android의 ION 할당자를 대체하는 DMA-BUF Heaps 프레임워크가 커널 메인라인에 통합되었습니다. CMA heap은 /dev/dma_heap/ 디렉터리를 통해 유저스페이스에서 직접 연속 메모리를 할당할 수 있는 표준 인터페이스를 제공합니다.

DMA-BUF Heaps 아키텍처 유저스페이스 open("/dev/dma_heap/system") ioctl(DMA_HEAP_IOCTL_ALLOC) CMA Heap 디바이스 open("/dev/dma_heap/linux,cma") ioctl(DMA_HEAP_IOCTL_ALLOC) dma-buf heaps framework (drivers/dma-buf/dma-heap.c) System Heap alloc_pages() + sg_table CMA Heap cma_alloc() + 연속 물리 메모리 물리 메모리 Buddy Allocator (산재 페이지) | CMA 영역 (연속 블록)

ION에서 DMA-BUF Heaps로의 전환

특성ION (deprecated)DMA-BUF Heaps (v5.6+)
인터페이스/dev/ion (단일 디바이스)/dev/dma_heap/<name> (heap별 디바이스)
할당ION_IOC_ALLOC + heap_idDMA_HEAP_IOCTL_ALLOC
반환DMA-BUF fdDMA-BUF fd
캐시(Cache) 관리ION 자체 sync ioctlDMA_BUF_IOCTL_SYNC 표준
메인라인staging → 제거 (v5.11)메인라인 표준
Android GKIGKI 1.0 (커스텀 확장)GKI 2.0 표준

CMA Heap 드라이버 구현

/* drivers/dma-buf/heaps/cma_heap.c */
static struct dma_buf *cma_heap_allocate(
    struct dma_heap *heap,
    unsigned long len,
    unsigned long fd_flags,
    unsigned long heap_flags)
{
    struct cma_heap *cma_heap = dma_heap_get_drvdata(heap);
    struct cma_heap_buffer *buffer;
    struct page *cma_pages;
    size_t size = PAGE_ALIGN(len);
    unsigned long nr_pages = size >> PAGE_SHIFT;

    buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);

    /* CMA 영역에서 연속 페이지 할당 */
    cma_pages = cma_alloc(cma_heap->cma, nr_pages,
                          get_order(size), GFP_KERNEL);
    if (!cma_pages)
        return ERR_PTR(-ENOMEM);

    /* sg_table 구성: 단일 엔트리 (연속이므로) */
    sg_alloc_table(&buffer->sg_table, 1, GFP_KERNEL);
    sg_set_page(buffer->sg_table.sgl, cma_pages,
                size, 0);

    /* DMA-BUF로 export */
    return dma_buf_export(&exp_info);
}
코드 설명
  • 3행cma_heap_allocate()는 DMA-BUF heaps 프레임워크의 allocate 콜백(Callback)입니다.
  • 12행cma_alloc()으로 CMA 영역에서 물리적으로 연속된 페이지를 할당합니다.
  • 17행연속 메모리이므로 sg_table은 단일 엔트리만 필요합니다.
  • 21행dma_buf_export()로 DMA-BUF fd를 유저스페이스에 반환합니다.

유저스페이스 CMA Heap 사용

#include <linux/dma-heap.h>
#include <linux/dma-buf.h>

int alloc_cma_buffer(size_t size) {
    int heap_fd, buf_fd;
    struct dma_heap_allocation_data alloc = {
        .len = size,
        .fd_flags = O_RDWR | O_CLOEXEC,
    };

    /* CMA heap 디바이스 오픈 */
    heap_fd = open("/dev/dma_heap/linux,cma", O_RDWR);

    /* CMA 연속 메모리 할당 → DMA-BUF fd 반환 */
    ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc);
    buf_fd = alloc.fd;

    /* mmap으로 유저스페이스 매핑 */
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_SHARED, buf_fd, 0);

    /* DMA-BUF sync (캐시 일관성) */
    struct dma_buf_sync sync = {
        .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE
    };
    ioctl(buf_fd, DMA_BUF_IOCTL_SYNC, &sync);
    /* ... 버퍼 사용 ... */
    sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE;
    ioctl(buf_fd, DMA_BUF_IOCTL_SYNC, &sync);

    return buf_fd;
}
Android GKI 2.0: Android 12+에서는 ION이 완전 제거되고 DMA-BUF Heaps가 표준입니다. /dev/dma_heap/system(산재 페이지)와 /dev/dma_heap/linux,cma(연속 메모리)가 기본 제공되며, 벤더별 커스텀 heap은 dma_heap_add()로 등록합니다.
교차 참조: DMA-BUF 공유 메커니즘은 DMA 페이지를, GPU 활용은 GPU (DRM/KMS) 페이지를 참조하십시오.

Android 미디어 파이프라인과 CMA

Android의 카메라, 비디오 코덱, 디스플레이 파이프라인은 대용량 연속 메모리 버퍼를 집중적으로 사용합니다. HAL(Hardware Abstraction Layer)부터 커널 CMA까지의 전체 경로를 이해하면 메모리 할당 실패와 성능 병목(Bottleneck)을 효과적으로 진단할 수 있습니다.

Android 미디어 파이프라인 → CMA 경로 유저스페이스 Camera2 API MediaCodec NDK SurfaceFlinger Gralloc HAL (mapper) HAL / Framework Camera HAL3 Provider OMX / C2 Codec HAL HWC HAL BufferAllocator DMA-BUF Heaps (/dev/dma_heap/linux,cma) DMA_HEAP_IOCTL_ALLOC → dma-buf fd cma_alloc() / alloc_contig_range() 페이지 마이그레이션 → 연속 블록 확보 CMA 물리 영역 ISP 버퍼 코덱 프레임 FB 스캔아웃 이동 가능 페이지

카메라 HAL → ISP → CMA 경로

Android Camera2 API의 버퍼 할당 흐름은 다음과 같습니다.

단계컴포넌트동작
1Camera2 APICameraCaptureSession.capture() 요청
2Camera HAL3Gralloc을 통해 DMA-BUF 버퍼 할당 요청
3Gralloc/BufferAllocator/dev/dma_heap/linux,cma에서 연속 버퍼 할당
4커널 CMA heapcma_alloc() → 연속 물리 페이지 확보
5ISP 드라이버DMA-BUF fd를 받아 물리 주소로 ISP 레지스터(Register)에 기록
6ISP 하드웨어CMA 영역에 직접 DMA write (이미지 데이터 기록)

비디오 코덱 연속 버퍼 요구

/* Android Codec2 (C2) HAL의 CMA 버퍼 할당 */
/* hardware/google/av/codec2/hidl/1.0/utils/ */

/* 4K H.265 디코딩 프레임 버퍼 크기 계산 */
/* YUV420: width * height * 1.5 */
/* 4K: 3840 * 2160 * 1.5 = 12,441,600 (~12MB per frame) */
/* 참조 프레임 16개: 약 200MB CMA 필요 */

struct c2_buffer_info {
    uint32_t width;       /* 3840 */
    uint32_t height;      /* 2160 */
    uint32_t stride;      /* 정렬된 stride */
    uint32_t num_ref;     /* 참조 프레임 수 */
    size_t frame_size;    /* stride * height * 1.5 */
    int dma_buf_fds[16]; /* CMA heap에서 할당된 fd */
};
CMA 크기 산정: 4K 비디오 코덱 + 카메라 동시 사용 시 최소 256~512MB CMA가 필요합니다. CMA 크기가 부족하면 cma_alloc 실패가 발생하고, 카메라 앱이 크래시하거나 비디오 재생이 끊깁니다. 디바이스 트리(Device Tree)에서 size 값을 충분히 확보해야 합니다.

SurfaceFlinger 프레임버퍼

SurfaceFlinger는 HWC(Hardware Composer) HAL을 통해 디스플레이 컨트롤러에 스캔아웃 버퍼를 제출합니다. Scatter-Gather를 지원하지 않는 디스플레이 컨트롤러에서는 CMA heap을 통해 연속 프레임버퍼를 할당합니다.

/* Gralloc → DMA-BUF Heaps 경로 (Android 12+) */
#include <BufferAllocator/BufferAllocator.h>

BufferAllocator alloc;
/* 연속 메모리가 필요한 디스플레이 버퍼 */
int fd = alloc.Alloc(
    "linux,cma",     /* CMA heap 이름 */
    framebuffer_size, /* 1920*1080*4 = ~8MB */
    0                 /* flags */
);
/* fd는 DMA-BUF fd → HWC HAL에 전달 */

cma_alloc() 내부 알고리즘

cma_alloc()의 내부 동작은 비트맵 탐색, 페이지 격리, 마이그레이션, 병합의 복잡한 과정으로 구성됩니다. 이 섹션에서는 mm/cma.c의 핵심 알고리즘을 단계별로 분석합니다.

cma_alloc() 내부 알고리즘 Phase 1: 비트맵 탐색 bitmap_find_next_zero_area_off() • order_per_bit 단위 검색 • PFN 정렬 요구사항 적용 • 연속 free 비트 영역 탐색 • bitmap_set()으로 선점 찾으면 → Phase 2 Phase 2: 격리/마이그레이션 alloc_contig_range() 호출 ① start_isolate_page_range() ② __alloc_contig_migrate_range() ③ test_pages_isolated() • LRU 페이지 → 다른 영역 격리 완료 → Phase 3 Phase 3: 병합/반환 post_alloc_hook() 처리 • 페이지 refcount 설정 • zone 통계 갱신 • pfn_to_page() → 반환 • struct page* 포인터 반환 성공 → page* 반환 실패 경로 (Retry Logic) Phase 2 실패 → bitmap_clear() → bitmap에서 다음 영역 탐색 (Phase 1로 복귀) 최대 재시도: bitmap 전체 스캔 완료까지 (순환 탐색) | pinned page 발견 시 해당 범위 skip CMA 비트맵 (order_per_bit=0 기준, 1비트 = 1페이지) 0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 = Free 1 = Allocated ← 4개 연속 free 비트 탐색: offset=4 에서 발견 →

alloc_contig_range() 3단계 분석

/* mm/page_alloc.c -- alloc_contig_range() 핵심 흐름 */
int alloc_contig_range(unsigned long start, unsigned long end,
                       unsigned migratetype, gfp_t gfp_mask)
{
    int ret;

    /* 1단계: 페이지 격리 (MIGRATE_ISOLATE로 전환) */
    ret = start_isolate_page_range(
        pfn_max_align_down(start),   /* pageblock 정렬 */
        pfn_max_align_up(end),
        migratetype, 0);
    if (ret)
        return ret;

    /* drain per-cpu pages: 격리 범위 내 PCP 페이지 반환 */
    drain_all_pages(cc.zone);

    /* 2단계: 페이지 마이그레이션 */
    ret = __alloc_contig_migrate_range(&cc, start, end);
    if (ret && ret != -EBUSY)
        goto done;

    /* LRU drain: 마이그레이션 완료 후 잔여 LRU 정리 */
    lru_add_drain_all();

    /* 3단계: 격리 완료 확인 */
    ret = test_pages_isolated(start, end, 0);
    if (ret)
        goto done;    /* 마이그레이션 불가 페이지 잔존 */

    /* 성공: 격리 범위의 모든 페이지가 free */
    ...
done:
    undo_isolate_page_range(
        pfn_max_align_down(start),
        pfn_max_align_up(end),
        migratetype);
    return ret;
}
코드 설명
  • 8행start_isolate_page_range()는 대상 범위를 MIGRATE_ISOLATE로 전환하여 새 할당을 방지합니다.
  • 9행pfn_max_align_down()은 pageblock 경계로 정렬합니다. CMA 할당은 반드시 pageblock 단위로 격리됩니다.
  • 16행drain_all_pages()는 per-cpu 페이지 캐시를 비워 격리 범위 내 페이지가 buddy로 반환되도록 합니다.
  • 19행__alloc_contig_migrate_range()는 격리 범위 내 사용 중인 모든 페이지를 다른 영역으로 마이그레이션합니다.
  • 25행test_pages_isolated()로 범위 내 모든 페이지가 실제 free인지 최종 확인합니다.

PFN 정렬 요구사항

파라미터설명일반적 값
align물리 주소 정렬 (order)0~9 (4KB~2MB)
order_per_bit비트맵 1비트가 커버하는 페이지 order0 (1페이지), 4 (16페이지)
pageblock_order격리 단위 (MAX_ORDER - 1)9 (2MB) 또는 13 (32MB, ARM64 CMA)
정렬 함정: cma_alloc(count, align, ...)에서 align은 2align 페이지 단위 정렬을 의미합니다. DMA 하드웨어가 1MB 정렬을 요구하면 align = 8 (2^8 * 4KB = 1MB)을 지정해야 합니다. 잘못된 정렬은 DMA 전송 오류의 흔한 원인입니다.
교차 참조: 페이지 마이그레이션 메커니즘은 Memory Compaction, Buddy Allocator 통합은 페이지 할당자 페이지를 참조하십시오.

CMA 단편화 진단

CMA 영역도 일반 메모리와 마찬가지로 내부 단편화 문제를 겪습니다. 이동 가능 페이지와 공존하는 CMA의 특성상, 특정 조건에서 마이그레이션이 불가능한 페이지가 CMA 영역에 잔류하면 연속 할당이 실패합니다.

CMA 내부 단편화 패턴 정상 상태: 모든 페이지 이동 가능 movable movable movable movable movable movable movable 단편화: pinned page 잔류 movable pinned movable (이동 가능) pinned movable pinned movable cma_alloc(256 pages) 요청 → 연속 256 페이지 필요 Pinned Page 원인 1. long-lived mmap (GUP pinned) 2. page_count > page_mapcount 3. mlocked VMA 페이지 해결 전략 1. per-device CMA로 경합 분리 2. CMA 영역 크기 증가 3. GUP pinning 최소화

debugfs/cma/ 카운터 분석

# CMA 영역별 debugfs 정보 확인
ls /sys/kernel/debug/cma/
# 출력: cma-reserved/  cma-default/  ...

# 할당/해제 통계
cat /sys/kernel/debug/cma/cma-default/alloc_pages_success
cat /sys/kernel/debug/cma/cma-default/alloc_pages_fail

# 현재 할당 상태 비트맵
cat /sys/kernel/debug/cma/cma-default/bitmap
# 출력: 0x00 0x00 0x3C 0x00 ... (할당된 비트=1)

# 최대 연속 free 블록 크기 확인 (스크립트)
python3 -c "
import sys
bitmap = open('/sys/kernel/debug/cma/cma-default/bitmap').read()
bits = bin(int(bitmap.replace(' ', '').replace('0x', ''), 16))
max_free = max(len(s) for s in bits.split('1'))
print(f'Max contiguous free: {max_free} pages ({max_free * 4}KB)')
"

단편화 진단 체크리스트

검사 항목명령/경로정상 기준
할당 실패율alloc_pages_fail / alloc_pages_success< 1%
비트맵 최대 연속 freebitmap 파싱> 최대 할당 요청 크기
pinned page 수/proc/vmstat + ftraceCMA 영역 대비 < 5%
CMA 영역 사용률used / total< 70%
ftrace 이벤트: echo 1 > /sys/kernel/debug/tracing/events/cma/enable로 CMA 할당/해제 이벤트를 추적할 수 있습니다. cma_alloc_start, cma_alloc_finish, cma_alloc_busy_retry 이벤트를 분석하면 단편화 패턴을 파악할 수 있습니다.

Folio 기반 전환

Linux 5.16부터 도입된 folio 추상화는 기존 struct page 기반 메모리 관리를 점진적으로 대체하고 있습니다. CMA 할당 경로도 folio 전환의 영향을 받으며, 특히 compound page 처리와 마이그레이션 경로에서 변화가 발생합니다.

struct page vs folio

특성struct pagestruct folio
단위단일 4KB 페이지1개 이상 연속 페이지 (base page 또는 compound)
tail pagehead/tail 구분 필요항상 head page만 참조
compound 처리compound_head() 호출 필수컴파일 타임에 보장
APIalloc_pages(), put_page()folio_alloc(), folio_put()
CMA 관련cma_alloc()struct page*향후 cma_alloc_folio() 논의 중

CMA 할당 경로의 folio 영향

/* 현재 cma_alloc()은 struct page* 반환 */
struct page *cma_alloc(struct cma *cma,
                        unsigned long count,
                        unsigned int align,
                        bool no_warn);

/* 마이그레이션 경로는 이미 folio 전환 진행 중 */
int migrate_folio(struct address_space *mapping,
                   struct folio *dst, struct folio *src,
                   enum migrate_mode mode);

/* alloc_contig_range() 내부: folio 기반 마이그레이션 */
static int __alloc_contig_migrate_range(
    struct compact_control *cc,
    unsigned long start, unsigned long end)
{
    /* folio_get() / folio_put()으로 전환됨 */
    struct folio *folio = folio_get_nontail_page(page);
    if (folio_test_large(folio)) {
        /* compound CMA 페이지: 전체를 하나의 단위로 마이그레이션 */
        ...
    }
}

page 기반 vs folio 기반 CMA 할당 비교

현재 CMA 할당은 struct page*를 반환하지만, 마이그레이션 경로는 이미 folio로 전환되었습니다. 아래 코드는 기존 page 기반 사용과 향후 folio 기반 사용 패턴을 비교합니다:

/*
 * [현재] page 기반 CMA 할당 패턴
 * - cma_alloc()은 struct page* 반환
 * - compound page일 경우 head/tail 구분 필요
 */
struct page *page;
unsigned long nr_pages = size >> PAGE_SHIFT;

page = cma_alloc(cma, nr_pages, get_order(size), false);
if (!page)
    return -ENOMEM;

/* compound page 처리: head page 확인 필요 */
if (PageCompound(page))
    page = compound_head(page);  /* 런타임 확인 필수 */

void *vaddr = page_address(page);
dma_sync_single_for_device(dev, page_to_phys(page), size, DMA_TO_DEVICE);

/* 해제 */
cma_release(cma, page, nr_pages);

/*
 * [향후] folio 기반 CMA 할당 패턴 (논의 중)
 * - folio는 head page만 참조하므로 compound_head() 불필요
 * - 컴파일 타임에 타입 안전성 보장
 */
/* 현재는 page → folio 변환으로 사용 */
struct page *page = cma_alloc(cma, nr_pages, order, false);
struct folio *folio = page_folio(page);  /* page → folio 변환 */

/* folio API 사용: compound_head() 호출 불필요 */
size_t folio_sz = folio_size(folio);        /* 전체 크기 (base × order) */
unsigned int order = folio_order(folio);    /* 페이지 order */
void *vaddr = folio_address(folio);

/* 마이그레이션 경로에서의 folio 활용 (이미 전환 완료) */
static int __alloc_contig_migrate_range(
    struct compact_control *cc,
    unsigned long start, unsigned long end)
{
    struct folio *folio;

    /* pfn 범위를 순회하며 마이그레이션 대상 folio 수집 */
    for (unsigned long pfn = start; pfn < end;) {
        folio = pfn_folio(pfn);
        if (folio_test_large(folio)) {
            /* large folio: 전체를 하나의 단위로 마이그레이션
             * - tail page 반복 순회 방지 (성능 향상)
             * - folio 크기만큼 pfn을 건너뜀 */
            pfn += folio_nr_pages(folio);
        } else {
            pfn++;
        }
        folio_get(folio);
        list_add_tail(&folio->lru, &cc->migratepages);
    }
    /* migrate_pages()가 folio 단위로 마이그레이션 수행 */
    return migrate_pages(&cc->migratepages, ...);
}
코드 설명
  • 14-15행 page 기반 코드에서는 PageCompound() 확인 후 compound_head()를 호출해야 합니다. 이 런타임 확인을 빠뜨리면 tail page를 head로 오인하여 버그가 발생합니다.
  • 29행 page_folio()는 page에서 folio로 안전하게 변환합니다. folio는 항상 head page를 가리키므로 compound_head() 호출이 불필요합니다.
  • 44-48행 folio 기반 마이그레이션은 large folio를 하나의 단위로 처리합니다. page 기반에서는 tail page마다 compound_head()를 호출했지만, folio는 folio_nr_pages()로 한 번에 건너뛸 수 있어 성능이 향상됩니다.

향후 전환 로드맵

단계커널 버전변경 사항
1단계5.16~6.x마이그레이션 경로 folio 전환 (migrate_pagemigrate_folio)
2단계6.x~LRU 관리 folio 전환 (page_to_folio() 호출 제거)
3단계논의 중cma_alloc_folio() API 도입 검토
4단계장기struct page 참조 최소화, folio 기반 CMA 완전 전환
compound page와 CMA: CMA가 할당하는 대용량 연속 블록은 내부적으로 compound page로 관리될 수 있습니다. folio는 이런 compound page를 자연스럽게 표현하므로, CMA의 folio 전환은 구조적으로 적합합니다. 관련 내용은 페이지 할당자메모리 관리 개요를 참조하십시오.

CMA와 HugeTLB 연동

Linux 5.7부터 hugetlb_cma= 커널 파라미터를 통해 CMA 영역에서 HugeTLB 페이지를 할당할 수 있습니다. 이 기능은 부팅 시 고정 예약 대신 CMA의 지연 할당 방식으로 HugeTLB 페이지를 관리하여 메모리 유연성을 크게 향상시킵니다.

CMA ↔ HugeTLB 연동 hugetlb_cma=4G (부팅 파라미터: per-NUMA-node CMA 예약) NUMA Node 0: hugetlb_cma[0] 2MB HugeTLB page 2MB HugeTLB page 이동 가능 페이지 NUMA Node 1: hugetlb_cma[1] 1GB HugeTLB page cma_alloc() → 262144 pages 이동 가능 페이지 echo 10 > /proc/sys/vm/nr_hugepages → alloc_contig_pages() via CMA 장점: 부팅 시 고정 예약 불필요 | 미사용 시 이동 가능 페이지로 활용 | NUMA-aware 분배

hugetlb_cma 설정

# 커널 부팅 파라미터
hugetlb_cma=4G          # 전체 4GB CMA for HugeTLB
hugetlb_cma=2G,2G       # NUMA node별: node0=2G, node1=2G

# 런타임 HugeTLB 할당 (CMA 경로 사용)
echo 20 > /proc/sys/vm/nr_hugepages         # 2MB hugepages
echo 2 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages  # 1GB

# CMA에서 할당된 HugeTLB 확인
cat /proc/meminfo | grep -E 'Huge|Cma'
# HugePages_Total:     20
# HugePages_Free:      20
# CmaTotal:       4194304 kB
# CmaFree:        4112384 kB

CMA와 THP 상호작용

특성HugeTLB (via CMA)THP (Transparent Huge Pages)
할당 경로cma_alloc() → 연속 보장alloc_pages(order=9) → best-effort
크기2MB / 1GB2MB (일반), 64KB (ARM64 contpte)
CMA 사용hugetlb_cma= 필요CMA 영역에서 이동 가능 페이지로 간접 사용
실패 시할당 실패 (fallback 없음)4KB 폴백
예약명시적 (nr_hugepages)자동 (khugepaged)
1GB HugeTLB: 전통적으로 1GB HugeTLB 페이지는 부팅 시점에만 할당 가능했습니다. hugetlb_cma=를 사용하면 런타임에도 CMA를 통해 1GB 페이지를 할당할 수 있어, 데이터베이스나 DPDK 같은 대용량 메모리 워크로드에서 유연성이 크게 향상됩니다.

NUMA 환경 CMA

NUMA(Non-Uniform Memory Access) 시스템에서 CMA 영역의 배치는 DMA 성능에 직접적인 영향을 미칩니다. 디바이스와 동일한 NUMA 노드에 CMA 영역을 배치하면 메모리 접근 지연 시간을 크게 줄일 수 있습니다.

NUMA 환경 per-node CMA 배치 NUMA Node 0 CPU 0~7 L3 Cache GPU (PCIe) DMA → Node 0 CMA CMA Region (Node 0): 512MB base_pfn: 0x100000 | count: 131072 일반 메모리 (Node 0) NUMA Node 1 CPU 8~15 L3 Cache NIC (PCIe) DMA → Node 1 CMA CMA Region (Node 1): 512MB base_pfn: 0x200000 | count: 131072 일반 메모리 (Node 1) Cross-node 지연 시간 2~3x Local DMA: 최적 경로 Remote DMA: 성능 저하

per-NUMA-node CMA 설정 (Device Tree)

/* NUMA 시스템 Device Tree: per-node CMA */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    /* Node 0: GPU용 CMA */
    cma_node0: cma@100000000 {
        compatible = "shared-dma-pool";
        reg = <0x1 0x00000000 0x0 0x20000000>; /* 512MB */
        reusable;
        linux,cma-default;
    };

    /* Node 1: NIC/가속기용 CMA */
    cma_node1: cma@200000000 {
        compatible = "shared-dma-pool";
        reg = <0x2 0x00000000 0x0 0x20000000>; /* 512MB */
        reusable;
    };
};

/* 디바이스별 NUMA-aware CMA 바인딩 */
gpu@10000000 {
    memory-region = <&cma_node0>;
    numa-node-id = <0>;
};

nic@20000000 {
    memory-region = <&cma_node1>;
    numa-node-id = <1>;
};

NUMA CMA 성능 비교

시나리오할당 지연DMA 처리량(Throughput)비고
Local CMA (동일 노드)~50us100% (기준)최적
Remote CMA (다른 노드)~120us60~70%인터커넥트 지연
글로벌 CMA (노드 무관)~50~120us60~100%비결정적
NUMA 밸런싱 주의: 글로벌 CMA(cma=256M 파라미터)는 단일 NUMA 노드에만 배치됩니다. 멀티 노드 시스템에서는 반드시 DT 또는 hugetlb_cma= per-node 설정을 사용하여 각 노드에 CMA 영역을 분배해야 합니다. 관련 내용은 Device Tree 페이지를 참조하십시오.

CMA 테스트 프레임워크

CMA 서브시스템의 안정성과 성능을 검증하기 위한 다양한 테스트 도구와 프레임워크가 커널에 내장되어 있습니다.

커널 CMA 테스트 모듈

/* lib/test_cma.c (또는 커스텀 테스트 모듈) */
#include <linux/cma.h>

static int cma_test_alloc(unsigned long count, unsigned int align)
{
    struct page *page;
    ktime_t start, end;

    start = ktime_get();
    page = cma_alloc(dma_contiguous_default_area,
                      count, align, false);
    end = ktime_get();

    if (!page) {
        pr_err("CMA alloc FAILED: %lu pages, align=%u\n",
               count, align);
        return -ENOMEM;
    }

    pr_info("CMA alloc OK: %lu pages (%lu KB), align=%u, "
            "latency=%lld us, pfn=0x%lx\n",
            count, count * 4, align,
            ktime_us_delta(end, start),
            page_to_pfn(page));

    cma_release(dma_contiguous_default_area, page, count);
    return 0;
}

selftests/mm CMA 테스트

# 커널 셀프테스트 빌드 및 실행
cd tools/testing/selftests/mm
make

# CMA 관련 테스트
./run_vmtests.sh -t cma

# 직접 CMA 스트레스 테스트 (유저스페이스)
# DMA-BUF Heaps를 통한 반복 할당/해제
for i in $(seq 1 1000); do
    # 16MB 연속 버퍼 할당 → 해제 반복
    ./dma_heap_test --heap linux,cma --size $((16*1024*1024))
done

# 병렬 스트레스: 여러 프로세스가 동시에 CMA 할당
for i in $(seq 1 8); do
    ./dma_heap_test --heap linux,cma --size $((8*1024*1024)) --iterations 500 &
done
wait

CONFIG_CMA_DEBUG

설정효과오버헤드(Overhead)
CONFIG_CMA_DEBUG할당/해제 시 상세 커널 로그 출력낮음 (로그만)
CONFIG_CMA_DEBUGFS/sys/kernel/debug/cma/ 인터페이스 활성화낮음
CONFIG_CMA_SYSFS/sys/kernel/mm/cma/ sysfs 인터페이스낮음
CONFIG_DEBUG_VMVM 디버그 검증 (CMA 포함)중간
CONFIG_PAGE_OWNER페이지 소유자 추적 (CMA pinned page 진단)높음

CMA 스트레스 테스트 방법론

테스트목적방법
단일 대용량 할당최대 연속 블록 확인CMA 영역 90% 크기 할당
반복 할당/해제비트맵 일관성 검증10,000회 반복, 랜덤 크기
병렬 할당 경합mutex 경합 성능8개 스레드(Thread) 동시 할당
메모리 압박 + CMA마이그레이션 부하stress-ng --vm + CMA 할당
장시간 운영단편화 누적 확인24시간 랜덤 할당/해제
CI 통합: CMA 테스트를 CI 파이프라인에 포함하려면 CONFIG_CMA_DEBUGFS=yCONFIG_DMA_CMA=y를 커널 설정에 추가하고, QEMU 환경에서 cma=128M 파라미터로 부팅합니다. selftests/mm 스위트가 자동으로 CMA 기능을 검증합니다.

CMA 할당 실패 진단 플레이북

CMA 할당 실패는 임베디드/모바일 시스템에서 카메라, 비디오, 디스플레이 장애의 가장 흔한 원인입니다. 체계적인 진단 절차를 통해 근본 원인을 빠르게 파악할 수 있습니다.

CMA 할당 실패 진단 플레이북 cma_alloc() 실패 발생 1단계: dmesg 패턴 확인 cma: alloc failed, req-size: N pages CMA 영역 부족? Yes 해결: CMA 크기 증가 cma= 파라미터 조정 No 단편화/pinned page 3단계 심층 진단 필요 3단계: ftrace 이벤트 분석 cma:cma_alloc_start → cma_alloc_busy_retry → cma_alloc_finish (실패 원인 확인) migration 실패? Yes 4단계: pinned page 확인 page_owner + GUP pinning → per-device CMA 분리 5단계: 검증 및 모니터링 debugfs 카운터 + ftrace 이벤트 지속 모니터링으로 재발 방지 진단 단계 해결 심층 진단 필요

dmesg 패턴 분석

# CMA 할당 실패 메시지 패턴
dmesg | grep -i "cma.*alloc"
# [  123.456789] cma: cma_alloc: linux,cma: alloc failed, req-size: 4096 pages, ret: -12
# [  123.456790] cma: number of available pages:
#    3500@100+500@3700+200@4200 => 4200 free (단편화)

# alloc_contig_range 실패 원인
dmesg | grep "alloc_contig"
# [  123.456800] alloc_contig_range: [0x10000, 0x11000) PFNs busy

# page_owner로 pinned page 소유자 확인
# (CONFIG_PAGE_OWNER=y 필요)
cat /sys/kernel/debug/page_owner
# Page allocated via order 0, mask 0x100dca, pid 1234
#  ... 스택 트레이스 ...

ftrace CMA 이벤트

# CMA ftrace 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_start/enable
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_finish/enable
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_busy_retry/enable

# 트레이스 확인
cat /sys/kernel/debug/tracing/trace
# <...>-1234 cma_alloc_start: name=linux,cma pfn=0x10000 count=1024 align=0
# <...>-1234 cma_alloc_busy_retry: name=linux,cma pfn=0x10000 count=1024
# <...>-1234 cma_alloc_busy_retry: name=linux,cma pfn=0x10400 count=1024
# <...>-1234 cma_alloc_finish: name=linux,cma pfn=0x10800 count=1024 errcode=0

# migrate 이벤트도 함께 추적
echo 1 > /sys/kernel/debug/tracing/events/migrate/enable

단계별 진단 체크리스트

단계확인 사항명령예상 원인
1CMA 총 크기 vs 요청 크기cat /proc/meminfo | grep CmaCMA 영역 자체 부족
2debugfs 실패 카운터cat .../cma/*/alloc_pages_fail반복 실패 여부
3ftrace 재시도 횟수cma_alloc_busy_retry 카운트단편화 심각도
4pinned page 확인page_owner + pfn 범위GUP/mlock 원인
5마이그레이션 실패 원인migrate ftrace events특정 페이지 타입 문제
프로덕션 주의: CONFIG_PAGE_OWNER는 메모리 오버헤드가 크므로 (페이지당 ~64바이트) 프로덕션 커널에서는 비활성화하고, 디버그 빌드에서만 사용하십시오. 대안으로 ftrace의 cma 이벤트만으로도 대부분의 진단이 가능합니다.
교차 참조: Memory Compaction 메커니즘은 Memory Compaction, 페이지 마이그레이션 상세는 페이지 할당자 페이지를 참조하십시오.

sysfs/debugfs 인터페이스

CMA 서브시스템은 debugfssysfs 두 가지 경로를 통해 상세한 모니터링 인터페이스를 제공합니다. 각 CMA 영역별로 독립적인 카운터와 정보를 확인할 수 있습니다.

CMA sysfs / debugfs 인터페이스 맵 debugfs (/sys/kernel/debug/cma/) cma-default/ alloc_pages_success (R) 성공 횟수 alloc_pages_fail (R) 실패 횟수 bitmap (R) 할당 비트맵 base_pfn (R) 시작 PFN count (R) 총 페이지 수 order_per_bit (R) 비트 단위 used (R) 사용 중 페이지 maxchunk (R) 최대 연속 free cma-camera/ (동일 파일 구조) cma-gpu/ (동일 파일 구조) CONFIG_CMA_DEBUGFS=y 필요 sysfs (/sys/kernel/mm/cma/) cma-default/ alloc_pages_attempt (R) 시도 횟수 alloc_pages_success (R) 성공 횟수 alloc_pages_fail (R) 실패 횟수 release_pages_success (R) 해제 횟수 cma-camera/ (동일 파일 구조) CONFIG_CMA_SYSFS=y 필요 /proc/meminfo CmaTotal: 524288 kB CmaFree: 491520 kB (전체 CMA 합산)

debugfs 파일 상세 설명

파일타입설명활용
alloc_pages_successcounter성공 할당 누적 횟수할당 빈도 모니터링
alloc_pages_failcounter실패 할당 누적 횟수실패율 알림 설정
bitmaphex dumpCMA 비트맵 전체 덤프(Dump)단편화 시각화
base_pfnhexCMA 영역 시작 PFN물리 주소 매핑
countdecimal총 관리 페이지 수영역 크기 확인
order_per_bitdecimal비트맵 1비트 = 2^N 페이지비트맵 해석
useddecimal현재 할당된 페이지 수사용률 모니터링
maxchunkdecimal최대 연속 free 블록 크기할당 가능 최대 크기

모니터링 스크립트

#!/bin/bash
# CMA 상태 모니터링 스크립트

CMA_BASE="/sys/kernel/debug/cma"

for cma_dir in $CMA_BASE/*/; do
    name=$(basename "$cma_dir")
    total=$(cat "$cma_dir/count")
    used=$(cat "$cma_dir/used" 2>/dev/null || echo "N/A")
    success=$(cat "$cma_dir/alloc_pages_success")
    fail=$(cat "$cma_dir/alloc_pages_fail")
    maxchunk=$(cat "$cma_dir/maxchunk" 2>/dev/null || echo "N/A")
    base=$(cat "$cma_dir/base_pfn")

    total_mb=$(( total * 4 / 1024 ))
    if [ "$used" != "N/A" ]; then
        used_mb=$(( used * 4 / 1024 ))
        usage_pct=$(( used * 100 / total ))
    fi

    echo "=== CMA: $name ==="
    echo "  Base PFN: $base"
    echo "  Total: ${total_mb}MB ($total pages)"
    echo "  Used: ${used_mb:-?}MB (${usage_pct:-?}%)"
    echo "  Max free chunk: $maxchunk pages"
    echo "  Alloc success/fail: $success / $fail"

    if [ "$fail" -gt 0 ] 2>/dev/null; then
        fail_rate=$(( fail * 100 / (success + fail) ))
        echo "  ⚠ Failure rate: ${fail_rate}%"
    fi
    echo
done

커널 모듈(Kernel Module)에서의 CMA 모니터링

/* 커널 모듈에서 CMA 상태 접근 */
#include <linux/cma.h>

void cma_status_report(struct cma *cma)
{
    unsigned long used, total;

    total = cma_get_size(cma) >> PAGE_SHIFT;

    /* bitmap에서 사용 중인 비트 카운트 */
    spin_lock(&cma->lock);
    used = bitmap_weight(cma->bitmap,
                          (unsigned int)cma_bitmap_maxno(cma));
    spin_unlock(&cma->lock);

    pr_info("CMA [%s]: %lu/%lu pages used (%lu%%)\n",
            cma_get_name(cma),
            used, total, used * 100 / total);
}
Prometheus 통합: node_exporter의 textfile collector를 사용하면 위 모니터링 스크립트의 출력을 Prometheus 메트릭으로 변환하여 Grafana 대시보드에서 CMA 상태를 실시간 시각화할 수 있습니다. cma_usage_ratio, cma_alloc_fail_total, cma_max_free_chunk_pages 등의 메트릭을 권장합니다.
debugfs vs sysfs: debugfs는 개발/디버깅용으로 비트맵, maxchunk 등 상세 정보를 제공하지만, 프로덕션 커널에서는 마운트(Mount)되지 않을 수 있습니다. sysfs(CONFIG_CMA_SYSFS)는 프로덕션 환경에서도 안정적으로 접근 가능한 기본 카운터를 제공합니다.

CMA와 메모리 압박(Memory Pressure)

CMA 영역은 이동 가능 페이지와 공존하므로, 시스템 메모리 압박 상황에서 CMA 영역의 동작과 OOM 킬러와의 상호작용을 이해하는 것이 중요합니다.

CMA 영역과 메모리 압박 상호작용 물리 메모리 전체 (Zone Normal) CMA 영역 (MIGRATE_CMA) 이동 가능 페이지 포함 일반 영역 (MOVABLE + UNMOVABLE + RECLAIMABLE) Zone 워터마크와 CMA 관계 free pages (CMA free 포함) HIGH LOW MIN 정상 상태 - CMA free 포함 워터마크 충족 - CMA 영역에 유저 페이지 배치 - cma_alloc() 성공률 높음 메모리 압박 (LOW 이하) - kswapd 활성화 - CMA 영역 페이지도 리클레임 대상 - cma_alloc()에 유리 (빈 공간 증가) 극심한 압박 (MIN 이하) - direct reclaim 활성화 - OOM killer 트리거 가능 - CMA 할당 중 마이그레이션 실패 CMA와 OOM 킬러 상호작용 1. CMA free 페이지는 Zone 워터마크 계산에 포함됨 → CMA 크기가 크면 OOM 발생이 지연될 수 있음 2. CMA 할당 실패는 OOM을 트리거하지 않음 → 드라이버가 NULL 반환을 직접 처리해야 함 3. CMA 영역이 DMA로 점유되면 이동 가능 할당이 줄어 → 간접적으로 메모리 압박 증가

CMA와 Zone 워터마크

CMA free 페이지는 NR_FREE_CMA_PAGES로 별도 추적되며, Zone 워터마크 계산에서 중요한 역할을 합니다.

/* mm/page_alloc.c -- 워터마크 체크에서 CMA 처리 */
static bool __zone_watermark_ok(
    struct zone *z, unsigned int order,
    unsigned long mark, int highest_zoneidx,
    unsigned int alloc_flags, long free_pages)
{
    long min = mark;

    free_pages -= (1 << order) - 1;

    /* UNMOVABLE 할당 시 CMA free 페이지 제외 */
    if (!(alloc_flags & ALLOC_CMA))
        free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES);

    /* free_pages가 워터마크 이하면 할당 거부 */
    if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
        return false;

    return true;
}
코드 설명
  • 12-13행 ALLOC_CMA 플래그가 없으면(즉, UNMOVABLE 할당) CMA free 페이지를 사용 가능한 free에서 제외합니다. 이렇게 하면 CMA 영역이 크더라도 커널 메모리 부족 시 OOM이 정확히 트리거됩니다.
  • 16행 free_pages가 워터마크 + lowmem_reserve보다 적으면 할당을 거부하여 리클레임이나 OOM을 유도합니다.

메모리 압박 시 CMA 동작 시나리오

시나리오CMA 영향시스템 응답
일반 메모리 부족 + CMA 여유CMA free 페이지에 이동 가능 페이지 배치 증가CMA가 메모리 압박을 흡수 → OOM 지연
일반 메모리 부족 + CMA 점유 높음CMA에서 이동 가능 페이지 수용 불가메모리 압박 가속 → OOM 발생 빨라짐
cma_alloc() 중 메모리 부족마이그레이션 대상 페이지 이동 공간 부족마이그레이션 실패 → cma_alloc() 실패
대량 CMA 할당 후 해제 지연이동 가능 페이지 수용 공간 감소일반 할당 성능 저하, 스와핑(Swapping) 증가
kswapd 리클레임 시CMA 영역의 파일 캐시 페이지도 회수 대상CMA 내부가 비워져 cma_alloc()에 유리
CMA 크기와 OOM 관계: CMA 영역이 전체 메모리의 상당 부분을 차지하면, UNMOVABLE 할당(커널 슬랩, 페이지 테이블) 관점에서 사용 가능한 메모리가 줄어듭니다. 예를 들어 4GB 시스템에서 CMA를 2GB로 설정하면, 커널이 사용 가능한 UNMOVABLE 메모리는 최대 2GB입니다. slabtop/proc/meminfoSUnreclaim을 모니터링하십시오.
교차 참조: OOM 킬러 메커니즘은 OOM Killer, 메모리 리클레임(Reclaim)은 Vmscan, 워터마크 상세는 페이지 할당자 페이지를 참조하십시오.

CMA 흔한 실수와 안티패턴

CMA 관련 개발에서 자주 발생하는 실수 패턴과 올바른 해결책을 정리합니다. 이러한 안티패턴은 할당 실패, 성능 저하, 메모리 누수의 원인이 됩니다.

CMA 흔한 실수와 해결책 1. 런타임 대용량 할당 장시간 운영 후 cma_alloc() 실패 probe() 시점 사전 할당 부팅 초기 단편화 없을 때 확보 2. CMA에 GUP pinning RDMA/GPU가 CMA 페이지 pin CMA 외부 메모리 사용 pin 대상은 일반 메모리에 할당 3. 해제 없이 반복 할당 에러 경로에서 dma_free 누락 devm_dma_alloc 사용 디바이스 제거 시 자동 해제 4. CMA 크기 미지정 기본값 사용 → 워크로드에 부족 5. DMA 중 CMA 버퍼 해제 use-after-free → 데이터 손상 6. 잘못된 align 값 HW 요구 정렬 미충족 → DMA 오류 안티패턴 진단 체크리스트 □ probe() 시점에 CMA 버퍼를 사전 할당하고 있는가? □ CMA 영역에 GUP pin이 발생하지 않는가? (RDMA, GPU direct, io_uring fixed buffers) □ 모든 에러 경로에서 dma_free_coherent()를 호출하는가? □ Device Tree에서 워크로드에 맞는 CMA 크기를 지정했는가? □ DMA 완료 확인 후에 버퍼를 해제하는가? (dma_fence_wait 또는 completion 사용)

안티패턴 상세

#안티패턴증상올바른 패턴
1 런타임 대용량 할당
시스템 가동 수시간 후 cma_alloc() 호출
단편화로 할당 실패, alloc_pages_fail 증가 probe() 시점에 필요한 버퍼를 미리 할당하여 pool로 관리
2 CMA 영역 GUP pinning
RDMA ibv_reg_mr()이 CMA 페이지를 pin
마이그레이션 불가 → 다른 CMA 할당 실패 pin 대상 메모리는 mmap(MAP_ANONYMOUS)으로 일반 영역 할당
3 에러 경로 해제 누락
dma_alloc_coherent() 후 에러 시 해제 미수행
CMA 비트맵 영구 점유, 메모리 누수 devm_dma_alloc_coherent() 사용 또는 goto err_free 패턴
4 CMA 크기 미지정
커널 기본값(CONFIG_CMA_SIZE_MBYTES) 그대로 사용
워크로드 대비 CMA 부족 → 할당 실패 Device Tree 또는 cma= 파라미터로 명시적 크기 설정
5 DMA 전송 중 버퍼 해제
DMA 완료 전 dma_free_coherent() 호출
데이터 손상, 커널 패닉(Panic), 하드웨어 행(Hang) dma_fence 또는 completion으로 DMA 완료 대기 후 해제
6 잘못된 정렬(alignment)
HW 요구 1MB 정렬인데 align=0 사용
DMA 전송 오류, 디스플레이 깨짐, 비디오 아티팩트(Artifact) HW 데이터시트 확인 후 정확한 align 값 지정

안티패턴 코드 예시와 수정

/* ❌ 안티패턴: 에러 경로에서 CMA 버퍼 해제 누락 */
static int bad_init(struct device *dev)
{
    void *buf1 = dma_alloc_coherent(dev, SZ_4M, &addr1, GFP_KERNEL);
    if (!buf1) return -ENOMEM;

    void *buf2 = dma_alloc_coherent(dev, SZ_4M, &addr2, GFP_KERNEL);
    if (!buf2) return -ENOMEM;  /* ← buf1 해제 누락! CMA 4MB 누수 */

    return 0;
}

/* ✅ 올바른 패턴: goto err 또는 devm API 사용 */
static int good_init(struct device *dev)
{
    void *buf1 = dma_alloc_coherent(dev, SZ_4M, &addr1, GFP_KERNEL);
    if (!buf1) return -ENOMEM;

    void *buf2 = dma_alloc_coherent(dev, SZ_4M, &addr2, GFP_KERNEL);
    if (!buf2) goto err_free_buf1;

    return 0;

err_free_buf1:
    dma_free_coherent(dev, SZ_4M, buf1, addr1);
    return -ENOMEM;
}

/* ✅ 더 나은 패턴: devm API (디바이스 제거 시 자동 해제) */
static int best_init(struct device *dev)
{
    void *buf1 = dmam_alloc_coherent(dev, SZ_4M, &addr1, GFP_KERNEL);
    if (!buf1) return -ENOMEM;

    void *buf2 = dmam_alloc_coherent(dev, SZ_4M, &addr2, GFP_KERNEL);
    if (!buf2) return -ENOMEM;
    /* dmam_: 디바이스 제거 시 자동 해제, 에러 경로에서도 안전 */

    return 0;
}
GUP pinning 진단: CMA 영역에 pinned page가 있는지 확인하려면 CONFIG_PAGE_OWNER=y를 활성화하고 /sys/kernel/debug/page_owner에서 CMA PFN 범위의 페이지 소유자를 확인하십시오. pin_user_pages() 호출 스택이 보이면 해당 드라이버가 CMA 영역을 pin하고 있는 것입니다.

CMA 커널 버전별 변화 연표

CMA는 Linux 3.5에서 처음 도입된 이후 지속적으로 기능 확장과 성능 개선이 이루어지고 있습니다.

커널 버전연도주요 변경핵심 커밋/기능
3.52012CMA 최초 도입Michal Nazarewicz (Samsung), mm/cma.c 추가
3.172014CMA debugfs 인터페이스CONFIG_CMA_DEBUGFS, 할당/실패 카운터
4.62016per-device CMA 지원 강화Device Tree memory-region + shared-dma-pool
4.152018CMA 비트맵 정렬 개선cma_bitmap_aligned_offset() 추가, 정렬 검색 최적화
5.62020DMA-BUF Heaps 통합ION 대체, /dev/dma_heap/linux,cma 표준화
5.72020HugeTLB CMA 지원hugetlb_cma= 파라미터, 런타임 1GB hugepage 할당
5.112021ION 제거staging/android/ion 완전 삭제, DMA-BUF Heaps로 일원화
5.162022CMA sysfs 인터페이스CONFIG_CMA_SYSFS, /sys/kernel/mm/cma/
5.16+2022folio 기반 마이그레이션 시작migrate_folio() 콜백 도입, CMA 마이그레이션 경로 영향
6.12022CMA 퍼센트 예약 지원cma=10% 파라미터: 전체 메모리 비율로 CMA 크기 지정
6.22023alloc_contig_range 개선마이그레이션 재시도 로직 개선, EBUSY 처리 강화
6.52023CMA 통계 강화sysfs alloc_pages_attempt 카운터 추가
6.82024pageblock 격리 최적화start_isolate_page_range() 경합 감소
6.10+2024CMA 성능 개선 지속비트맵 검색 최적화, drain_all_pages 범위 한정
CMA 발전 타임라인 3.5 CMA 도입 2012 4.6 per-device 2016 5.6 DMA-BUF Heaps 2020 5.7 HugeTLB CMA 2020 5.16 sysfs + folio 2022 6.1 cma=N% 2022 6.8+ 격리 최적화 2024 핵심 기능 도입 기능 강화 성능 개선
버전 호환성: 임베디드 시스템에서 LTS 커널(4.19, 5.4, 5.10, 5.15, 6.1, 6.6)을 사용하는 경우, 해당 버전에서 지원하는 CMA 기능을 확인하십시오. 특히 hugetlb_cma=는 5.7+, DMA-BUF Heaps는 5.6+, CMA sysfs는 5.16+에서만 사용 가능합니다.

참고자료

다음 학습: