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)과 성능 최적화까지 종합적으로 다룹니다.
핵심 요약
- CMA -- 물리적으로 연속된 대용량 메모리를 할당하는 커널 서브시스템으로, 예약 영역과 이동 가능 페이지를 공존시킵니다.
- MIGRATE_CMA -- Buddy Allocator에서 CMA 전용으로 관리되는 마이그레이션 타입으로, 이동 가능 페이지만 배치됩니다.
- cma_alloc() -- CMA 영역에서 연속 물리 메모리를 할당하며, 필요하면 기존 페이지를 마이그레이션합니다.
- dma_alloc_coherent() -- DMA API가 내부적으로 CMA를 fallback 경로로 사용하는 통합 지점입니다.
- per-device CMA -- 디바이스별 전용 CMA 영역을 Device Tree로 지정하여 경합(Contention)을 줄일 수 있습니다.
단계별 이해
- 연속 메모리가 필요한 이유
Scatter-Gather를 지원하지 않는 DMA 컨트롤러는 물리적으로 연속된 버퍼가 필수입니다. 시스템 운영 시간이 길어지면 메모리 단편화로 큰 연속 블록 확보가 어려워집니다. - CMA 영역 예약
부팅 시 Device Tree 또는 커널 파라미터로 CMA 영역을 예약합니다. 이 영역은 Buddy Allocator에 MIGRATE_CMA 타입으로 등록됩니다. - 평상시 공유
CMA 영역에는 이동 가능 페이지(유저 프로세스(Process) 데이터, 페이지 캐시(Page Cache) 등)가 배치되어 메모리가 낭비되지 않습니다. - 할당 시 마이그레이션
cma_alloc()호출 시, 요청 범위 내의 기존 페이지를 다른 영역으로 마이그레이션하여 연속 공간을 확보합니다. - 해제 후 재활용(Recycling)
cma_release()이후 해당 영역은 다시 이동 가능 페이지로 채워질 수 있습니다.
개요
CMA(Contiguous Memory Allocator)는 물리적으로 연속된 큰 메모리 블록을 효율적으로 할당하기 위해 설계된 커널 서브시스템입니다. 2012년 Michal Nazarewicz가 Samsung에서 개발하여 Linux 3.5에 최초 통합되었으며, mm/cma.c와 mm/cma.h에 핵심 구현이 위치합니다.
연속 메모리가 필요한 이유
현대 SoC의 많은 디바이스는 물리적으로 연속된 메모리 버퍼를 요구합니다.
| 디바이스 | 연속 메모리 요구 이유 | 일반적 크기 |
|---|---|---|
| 비디오 디코더 | 프레임 버퍼를 연속 물리 주소(Physical Address)로 DMA 접근 | 4~32 MB |
| 카메라 ISP | 이미지 센서 데이터를 연속 버퍼에 기록 | 8~64 MB |
| GPU 프레임버퍼 | 디스플레이 스캔아웃에 연속 물리 메모리 필요 | 16~128 MB |
| 디스플레이 컨트롤러 | Scatter-Gather 미지원 LCDC | 2~16 MB |
| DSP/가속기 | 전용 DMA 엔진이 연속 버퍼만 처리 | 1~8 MB |
기존 접근 방식의 한계
CMA 이전에는 연속 메모리 확보를 위해 다음 방법을 사용했지만, 각각 심각한 단점이 있었습니다.
| 방식 | 접근 | 단점 |
|---|---|---|
memblock_reserve() | 부팅 시 고정 영역 예약 | 미사용 시에도 다른 용도 사용 불가 (메모리 낭비) |
alloc_pages(GFP_DMA) | DMA zone에서 할당 | 단편화 시 대용량 연속 블록 확보 실패 |
vmalloc() + IOMMU | 가상 연속 메모리 | IOMMU 미지원 디바이스에서 사용 불가 |
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 할당 시에만 기존 페이지를 마이그레이션합니다.
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 타입의 핵심 규칙은 다음과 같습니다.
- 할당 제한:
MIGRATE_MOVABLE요청에만 페이지를 제공합니다.MIGRATE_UNMOVABLE(슬랩, 커널 스택)은 CMA 영역에 배치하지 않습니다. - fallback 경로: Buddy Allocator의 fallback 목록에서
MIGRATE_CMA는MIGRATE_MOVABLE의 fallback 대상에 포함됩니다. - 마이그레이션 보장: CMA 영역의 모든 페이지는 이동 가능하므로,
cma_alloc()시 반드시 마이그레이션 가능합니다.
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"
초기화 코드 상세
/* 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_MBYTESKconfig 값을 사용합니다. -
18-20행
dma_contiguous_reserve_area()는 내부적으로cma_declare_contiguous()를 호출하여 memblock에서 물리 메모리를 예약하고struct cma를 초기화합니다.
CMA 할당 메커니즘
cma_alloc()은 CMA 영역에서 연속 물리 메모리를 할당하는 핵심 함수입니다. 내부적으로 alloc_contig_range()를 호출하여 지정 범위의 페이지를 격리하고 마이그레이션합니다.
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() 핵심 코드
/* 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 동작입니다.
/* 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 영역이 "사용 가능한 빈 메모리"로 인식되어 시스템 메모리 효율이 높아집니다.
MIGRATE_UNMOVABLE 페이지가 배치되면 마이그레이션이 불가능해져 cma_alloc()이 실패합니다. 이것이 Buddy Allocator의 fallback 제한이 핵심인 이유입니다.
DMA-CMA 통합
DMA 서브시스템은 dma_alloc_coherent()를 통해 CMA를 자동으로 활용합니다. 디바이스 드라이버는 CMA를 직접 호출할 필요 없이 표준 DMA API를 사용하면 됩니다.
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_CMAfree_list에 다시 들어갑니다. -
21행
비트맵을 클리어하여 해당 영역이 다시
cma_alloc()에 의해 할당될 수 있도록 합니다.
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_unmap_*() 또는 DMA 전송 완료 확인 후 dma_free_coherent()를 호출하십시오.
per-device CMA와 글로벌 CMA
리눅스 커널은 단일 글로벌 CMA 영역과 디바이스별 전용 CMA 영역을 모두 지원합니다.
| 특성 | 글로벌 CMA | per-device CMA |
|---|---|---|
| 설정 방법 | cma= 파라미터 / linux,cma-default | Device Tree memory-region |
| 구조체 | dma_contiguous_default_area | dev->cma_area |
| 공유 | 모든 디바이스가 공유 | 특정 디바이스 전용 |
| 장점 | 설정 간단, 메모리 효율적 | 경합 방지, 할당 실패율 감소 |
| 단점 | 여러 디바이스 경합 가능 | 전용 영역 크기만큼 고정 예약 |
| 적합한 경우 | DMA 사용량이 적은 시스템 | 실시간(Real-time) 미디어, 카메라 파이프라인(Pipeline) |
디바이스별 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-memory의 reusable 속성이 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 특성에 따라 결정됩니다.
IOMMU 환경에서의 DMA 할당 경로
| 조건 | 할당 경로 | 물리 연속 여부 | CMA 사용 |
|---|---|---|---|
| IOMMU 없음 | dma_direct_alloc() | 필수 | 사용 |
| IOMMU 있음 + S/G 지원 | iommu_dma_alloc() | 불필요 | 미사용 |
| IOMMU 있음 + S/G 미지원 | iommu_dma_alloc() → CMA fallback | 필요 | 사용 |
| DMA_ATTR_FORCE_CONTIGUOUS | CMA 강제 | 필수 | 사용 |
| 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 블록을 할당합니다.
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은 CMA 방식으로 이동 가능 페이지와 공존하며, no-map은 커널 페이지 테이블(Page Table)에서 완전히 제외하여 보안 펌웨어(Firmware) 등 전용 용도로 사용합니다. CMA는 반드시 reusable을 사용합니다.
성능 최적화와 단편화 방지
CMA 할당 성능은 주로 마이그레이션 비용에 좌우됩니다. 다음 전략으로 할당 지연과 실패율을 줄일 수 있습니다.
실전 최적화 팁
/* 최적화 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 요구량 |
|---|---|---|---|---|
| 카메라 ISP (12MP RAW10) | 4032×3024 | ~15 MB | 4 | 60 MB |
| 카메라 ISP (48MP RAW10) | 8000×6000 | ~60 MB | 4 | 240 MB |
| 비디오 디코더 (1080p H.264) | 1920×1080 | ~3 MB | 16+4 | 60 MB |
| 비디오 디코더 (4K H.265) | 3840×2160 | ~12 MB | 16+4 | 240 MB |
| 비디오 인코더 (4K) | 3840×2160 | ~12 MB | 4 | 48 MB |
| 디스플레이 (1080p ARGB) | 1920×1080 | ~8 MB | 3 | 24 MB |
| 디스플레이 (4K ARGB) | 3840×2160 | ~32 MB | 3 | 96 MB |
| GPU 프레임버퍼 | 다양 | 8~64 MB | 2~3 | 16~192 MB |
| DSP/NPU 가속기 | N/A | 1~16 MB | 2~4 | 2~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.2x | per-device CMA (전용 영역) | 경합 없음, 단편화 최소 |
| 1.5x | 글로벌 CMA (다수 디바이스 공유) | 동시 할당 경합, 마이그레이션 지연 |
| 2.0x | 장시간 운영 임베디드 시스템 | 단편화 누적, pinned page 증가 |
| 2.5x+ | 프로덕션 안정성 최우선 시스템 | 최악의 경우 대비 |
/proc/meminfo의 CmaFree 값을 모니터링하여 피크 사용량을 확인하십시오. 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 메모리 유형을 통해 CMA를 추상화합니다. 대부분의 미디어 드라이버는 이 프레임워크를 통해 간접적으로 CMA를 사용합니다.
DMA-BUF Heaps 통합
Linux 5.6부터 Android의 ION 할당자를 대체하는 DMA-BUF Heaps 프레임워크가 커널 메인라인에 통합되었습니다. CMA heap은 /dev/dma_heap/ 디렉터리를 통해 유저스페이스에서 직접 연속 메모리를 할당할 수 있는 표준 인터페이스를 제공합니다.
ION에서 DMA-BUF Heaps로의 전환
| 특성 | ION (deprecated) | DMA-BUF Heaps (v5.6+) |
|---|---|---|
| 인터페이스 | /dev/ion (단일 디바이스) | /dev/dma_heap/<name> (heap별 디바이스) |
| 할당 | ION_IOC_ALLOC + heap_id | DMA_HEAP_IOCTL_ALLOC |
| 반환 | DMA-BUF fd | DMA-BUF fd |
| 캐시(Cache) 관리 | ION 자체 sync ioctl | DMA_BUF_IOCTL_SYNC 표준 |
| 메인라인 | staging → 제거 (v5.11) | 메인라인 표준 |
| Android GKI | GKI 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;
}
/dev/dma_heap/system(산재 페이지)와 /dev/dma_heap/linux,cma(연속 메모리)가 기본 제공되며, 벤더별 커스텀 heap은 dma_heap_add()로 등록합니다.
Android 미디어 파이프라인과 CMA
Android의 카메라, 비디오 코덱, 디스플레이 파이프라인은 대용량 연속 메모리 버퍼를 집중적으로 사용합니다. HAL(Hardware Abstraction Layer)부터 커널 CMA까지의 전체 경로를 이해하면 메모리 할당 실패와 성능 병목(Bottleneck)을 효과적으로 진단할 수 있습니다.
카메라 HAL → ISP → CMA 경로
Android Camera2 API의 버퍼 할당 흐름은 다음과 같습니다.
| 단계 | 컴포넌트 | 동작 |
|---|---|---|
| 1 | Camera2 API | CameraCaptureSession.capture() 요청 |
| 2 | Camera HAL3 | Gralloc을 통해 DMA-BUF 버퍼 할당 요청 |
| 3 | Gralloc/BufferAllocator | /dev/dma_heap/linux,cma에서 연속 버퍼 할당 |
| 4 | 커널 CMA heap | cma_alloc() → 연속 물리 페이지 확보 |
| 5 | ISP 드라이버 | DMA-BUF fd를 받아 물리 주소로 ISP 레지스터(Register)에 기록 |
| 6 | ISP 하드웨어 | 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_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의 핵심 알고리즘을 단계별로 분석합니다.
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비트가 커버하는 페이지 order | 0 (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 전송 오류의 흔한 원인입니다.
CMA 단편화 진단
CMA 영역도 일반 메모리와 마찬가지로 내부 단편화 문제를 겪습니다. 이동 가능 페이지와 공존하는 CMA의 특성상, 특정 조건에서 마이그레이션이 불가능한 페이지가 CMA 영역에 잔류하면 연속 할당이 실패합니다.
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% |
| 비트맵 최대 연속 free | bitmap 파싱 | > 최대 할당 요청 크기 |
| pinned page 수 | /proc/vmstat + ftrace | CMA 영역 대비 < 5% |
| CMA 영역 사용률 | used / total | < 70% |
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 page | struct folio |
|---|---|---|
| 단위 | 단일 4KB 페이지 | 1개 이상 연속 페이지 (base page 또는 compound) |
| tail page | head/tail 구분 필요 | 항상 head page만 참조 |
| compound 처리 | compound_head() 호출 필수 | 컴파일 타임에 보장 |
| API | alloc_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_page → migrate_folio) |
| 2단계 | 6.x~ | LRU 관리 folio 전환 (page_to_folio() 호출 제거) |
| 3단계 | 논의 중 | cma_alloc_folio() API 도입 검토 |
| 4단계 | 장기 | struct page 참조 최소화, folio 기반 CMA 완전 전환 |
CMA와 HugeTLB 연동
Linux 5.7부터 hugetlb_cma= 커널 파라미터를 통해 CMA 영역에서 HugeTLB 페이지를 할당할 수 있습니다. 이 기능은 부팅 시 고정 예약 대신 CMA의 지연 할당 방식으로 HugeTLB 페이지를 관리하여 메모리 유연성을 크게 향상시킵니다.
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 / 1GB | 2MB (일반), 64KB (ARM64 contpte) |
| CMA 사용 | hugetlb_cma= 필요 | CMA 영역에서 이동 가능 페이지로 간접 사용 |
| 실패 시 | 할당 실패 (fallback 없음) | 4KB 폴백 |
| 예약 | 명시적 (nr_hugepages) | 자동 (khugepaged) |
hugetlb_cma=를 사용하면 런타임에도 CMA를 통해 1GB 페이지를 할당할 수 있어, 데이터베이스나 DPDK 같은 대용량 메모리 워크로드에서 유연성이 크게 향상됩니다.
NUMA 환경 CMA
NUMA(Non-Uniform Memory Access) 시스템에서 CMA 영역의 배치는 DMA 성능에 직접적인 영향을 미칩니다. 디바이스와 동일한 NUMA 노드에 CMA 영역을 배치하면 메모리 접근 지연 시간을 크게 줄일 수 있습니다.
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 (동일 노드) | ~50us | 100% (기준) | 최적 |
| Remote CMA (다른 노드) | ~120us | 60~70% | 인터커넥트 지연 |
| 글로벌 CMA (노드 무관) | ~50~120us | 60~100% | 비결정적 |
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_VM | VM 디버그 검증 (CMA 포함) | 중간 |
CONFIG_PAGE_OWNER | 페이지 소유자 추적 (CMA pinned page 진단) | 높음 |
CMA 스트레스 테스트 방법론
| 테스트 | 목적 | 방법 |
|---|---|---|
| 단일 대용량 할당 | 최대 연속 블록 확인 | CMA 영역 90% 크기 할당 |
| 반복 할당/해제 | 비트맵 일관성 검증 | 10,000회 반복, 랜덤 크기 |
| 병렬 할당 경합 | mutex 경합 성능 | 8개 스레드(Thread) 동시 할당 |
| 메모리 압박 + CMA | 마이그레이션 부하 | stress-ng --vm + CMA 할당 |
| 장시간 운영 | 단편화 누적 확인 | 24시간 랜덤 할당/해제 |
CONFIG_CMA_DEBUGFS=y와 CONFIG_DMA_CMA=y를 커널 설정에 추가하고, QEMU 환경에서 cma=128M 파라미터로 부팅합니다. selftests/mm 스위트가 자동으로 CMA 기능을 검증합니다.
CMA 할당 실패 진단 플레이북
CMA 할당 실패는 임베디드/모바일 시스템에서 카메라, 비디오, 디스플레이 장애의 가장 흔한 원인입니다. 체계적인 진단 절차를 통해 근본 원인을 빠르게 파악할 수 있습니다.
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
단계별 진단 체크리스트
| 단계 | 확인 사항 | 명령 | 예상 원인 |
|---|---|---|---|
| 1 | CMA 총 크기 vs 요청 크기 | cat /proc/meminfo | grep Cma | CMA 영역 자체 부족 |
| 2 | debugfs 실패 카운터 | cat .../cma/*/alloc_pages_fail | 반복 실패 여부 |
| 3 | ftrace 재시도 횟수 | cma_alloc_busy_retry 카운트 | 단편화 심각도 |
| 4 | pinned page 확인 | page_owner + pfn 범위 | GUP/mlock 원인 |
| 5 | 마이그레이션 실패 원인 | migrate ftrace events | 특정 페이지 타입 문제 |
CONFIG_PAGE_OWNER는 메모리 오버헤드가 크므로 (페이지당 ~64바이트) 프로덕션 커널에서는 비활성화하고, 디버그 빌드에서만 사용하십시오. 대안으로 ftrace의 cma 이벤트만으로도 대부분의 진단이 가능합니다.
sysfs/debugfs 인터페이스
CMA 서브시스템은 debugfs와 sysfs 두 가지 경로를 통해 상세한 모니터링 인터페이스를 제공합니다. 각 CMA 영역별로 독립적인 카운터와 정보를 확인할 수 있습니다.
debugfs 파일 상세 설명
| 파일 | 타입 | 설명 | 활용 |
|---|---|---|---|
alloc_pages_success | counter | 성공 할당 누적 횟수 | 할당 빈도 모니터링 |
alloc_pages_fail | counter | 실패 할당 누적 횟수 | 실패율 알림 설정 |
bitmap | hex dump | CMA 비트맵 전체 덤프(Dump) | 단편화 시각화 |
base_pfn | hex | CMA 영역 시작 PFN | 물리 주소 매핑 |
count | decimal | 총 관리 페이지 수 | 영역 크기 확인 |
order_per_bit | decimal | 비트맵 1비트 = 2^N 페이지 | 비트맵 해석 |
used | decimal | 현재 할당된 페이지 수 | 사용률 모니터링 |
maxchunk | decimal | 최대 연속 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);
}
node_exporter의 textfile collector를 사용하면 위 모니터링 스크립트의 출력을 Prometheus 메트릭으로 변환하여 Grafana 대시보드에서 CMA 상태를 실시간 시각화할 수 있습니다. cma_usage_ratio, cma_alloc_fail_total, cma_max_free_chunk_pages 등의 메트릭을 권장합니다.
CONFIG_CMA_SYSFS)는 프로덕션 환경에서도 안정적으로 접근 가능한 기본 카운터를 제공합니다.
CMA와 메모리 압박(Memory Pressure)
CMA 영역은 이동 가능 페이지와 공존하므로, 시스템 메모리 압박 상황에서 CMA 영역의 동작과 OOM 킬러와의 상호작용을 이해하는 것이 중요합니다.
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()에 유리 |
slabtop과 /proc/meminfo의 SUnreclaim을 모니터링하십시오.
CMA 흔한 실수와 안티패턴
CMA 관련 개발에서 자주 발생하는 실수 패턴과 올바른 해결책을 정리합니다. 이러한 안티패턴은 할당 실패, 성능 저하, 메모리 누수의 원인이 됩니다.
안티패턴 상세
| # | 안티패턴 | 증상 | 올바른 패턴 |
|---|---|---|---|
| 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;
}
CONFIG_PAGE_OWNER=y를 활성화하고 /sys/kernel/debug/page_owner에서 CMA PFN 범위의 페이지 소유자를 확인하십시오. pin_user_pages() 호출 스택이 보이면 해당 드라이버가 CMA 영역을 pin하고 있는 것입니다.
CMA 커널 버전별 변화 연표
CMA는 Linux 3.5에서 처음 도입된 이후 지속적으로 기능 확장과 성능 개선이 이루어지고 있습니다.
| 커널 버전 | 연도 | 주요 변경 | 핵심 커밋/기능 |
|---|---|---|---|
| 3.5 | 2012 | CMA 최초 도입 | Michal Nazarewicz (Samsung), mm/cma.c 추가 |
| 3.17 | 2014 | CMA debugfs 인터페이스 | CONFIG_CMA_DEBUGFS, 할당/실패 카운터 |
| 4.6 | 2016 | per-device CMA 지원 강화 | Device Tree memory-region + shared-dma-pool |
| 4.15 | 2018 | CMA 비트맵 정렬 개선 | cma_bitmap_aligned_offset() 추가, 정렬 검색 최적화 |
| 5.6 | 2020 | DMA-BUF Heaps 통합 | ION 대체, /dev/dma_heap/linux,cma 표준화 |
| 5.7 | 2020 | HugeTLB CMA 지원 | hugetlb_cma= 파라미터, 런타임 1GB hugepage 할당 |
| 5.11 | 2021 | ION 제거 | staging/android/ion 완전 삭제, DMA-BUF Heaps로 일원화 |
| 5.16 | 2022 | CMA sysfs 인터페이스 | CONFIG_CMA_SYSFS, /sys/kernel/mm/cma/ |
| 5.16+ | 2022 | folio 기반 마이그레이션 시작 | migrate_folio() 콜백 도입, CMA 마이그레이션 경로 영향 |
| 6.1 | 2022 | CMA 퍼센트 예약 지원 | cma=10% 파라미터: 전체 메모리 비율로 CMA 크기 지정 |
| 6.2 | 2023 | alloc_contig_range 개선 | 마이그레이션 재시도 로직 개선, EBUSY 처리 강화 |
| 6.5 | 2023 | CMA 통계 강화 | sysfs alloc_pages_attempt 카운터 추가 |
| 6.8 | 2024 | pageblock 격리 최적화 | start_isolate_page_range() 경합 감소 |
| 6.10+ | 2024 | CMA 성능 개선 지속 | 비트맵 검색 최적화, drain_all_pages 범위 한정 |
hugetlb_cma=는 5.7+, DMA-BUF Heaps는 5.6+, CMA sysfs는 5.16+에서만 사용 가능합니다.
참고자료
- DMA API HOWTO -- Linux Kernel Documentation
- 커널 파라미터 -- cma= 옵션
- LWN: A deep dive into CMA (2012)
- LWN: CMA and compaction (2016)
- CMA: Contiguous Memory Allocator -- ELCE 2012 슬라이드 (Michal Nazarewicz)
- 커널 소스:
mm/cma.c,mm/cma.h,kernel/dma/contiguous.c,mm/page_alloc.c - Device Tree 바인딩:
Documentation/devicetree/bindings/reserved-memory/shared-dma-pool.yaml - Page Migration -- Linux Kernel Documentation
- DMA-BUF Heaps User API -- Linux Kernel Documentation
- LWN: DMA-BUF heaps (2020)
- LWN: CMA and hugetlb (2021)
- LWN: Memory folios (2021)
- 커널 소스:
drivers/dma-buf/heaps/cma_heap.c,mm/hugetlb_cma.c,tools/testing/selftests/mm/ - DMA API 가이드 — Linux Kernel Documentation
- CMA debugfs 인터페이스 — Linux Kernel Documentation
- mm/cma.c — CMA 핵심 구현 소스
- mm/cma.h — CMA 내부 헤더
- include/linux/cma.h — CMA API 헤더
- kernel/dma/contiguous.c — DMA CMA 통합 구현
- DMA (Direct Memory Access) -- DMA 매핑 API와 IOMMU 통합
- 페이지 할당자 (Buddy Allocator) -- CMA의 기반이 되는 물리 메모리 관리
- 메모리 관리 개요 -- 단편화, compaction, 메모리 압축(Memory Compaction)
- DMA Engine -- DMA 전송 엔진 프레임워크
- GPU (DRM/KMS) -- DRM GEM CMA 헬퍼와 GPU 메모리
- Memory Compaction -- CMA 할당의 핵심 메커니즘인 페이지 마이그레이션과 단편화 해소
- Device Tree -- CMA 영역 선언과 per-device 바인딩