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 바인딩, 디버깅과 성능 최적화까지 종합적으로 다룹니다.

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

핵심 요약

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

단계별 이해

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

개요

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

연속 메모리가 필요한 이유

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

디바이스연속 메모리 요구 이유일반적 크기
비디오 디코더프레임 버퍼를 연속 물리 주소로 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의 핵심 아이디어: 예약 영역을 완전히 잠그지 않고, 이동 가능한 페이지와 공존시킵니다. 연속 메모리가 필요할 때만 기존 페이지를 마이그레이션하여 공간을 확보합니다. 이 "지연 격리(lazy isolation)" 방식으로 메모리 활용도와 연속 할당 보장을 동시에 달성합니다.

CMA 핵심 구조체

/* 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 영역의 시작 페이지 프레임 번호(PFN). 물리 주소 = base_pfn * PAGE_SIZE.
  • 4행 count: CMA 영역에 포함된 총 페이지 수. 영역 크기 = count * PAGE_SIZE.
  • 5행 bitmap: 각 비트가 2^order_per_bit 페이지의 할당 상태를 추적하는 비트맵.
  • 6행 order_per_bit: 비트맵 해상도. 0이면 페이지 단위, 4이면 16페이지(64KB) 단위 추적.
  • 7-8행 lock은 비트맵 읽기/쓰기를, cma_mutex는 할당/해제 연산 전체를 직렬화합니다.

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행 정렬 요구사항을 비트맵 마스크/오프셋으로 변환합니다. 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 영역에 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 서브시스템이 자동으로 결정합니다.

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 페이지는 즉시 이동 가능 할당(유저 프로세스, 페이지 캐시)에 사용될 수 있습니다. CMA의 "지연 격리" 설계 덕분에 메모리가 유휴 상태로 방치되지 않습니다.

per-device CMA와 글로벌 CMA

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

특성글로벌 CMAper-device CMA
설정 방법cma= 파라미터 / linux,cma-defaultDevice Tree memory-region
구조체dma_contiguous_default_areadev->cma_area
공유모든 디바이스가 공유특정 디바이스 전용
장점설정 간단, 메모리 효율적경합 방지, 할당 실패율 감소
단점여러 디바이스 경합 가능전용 영역 크기만큼 고정 예약
적합한 경우DMA 사용량이 적은 시스템실시간 미디어, 카메라 파이프라인
글로벌 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로 경합 방지 필수

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 디버깅

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은 커널 페이지 테이블에서 완전히 제외하여 보안 펌웨어 등 전용 용도로 사용합니다. 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는 주로 임베디드/모바일 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를 사용합니다.

참고자료

다음 학습: