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 바인딩, 디버깅과 성능 최적화까지 종합적으로 다룹니다.
핵심 요약
- CMA -- 물리적으로 연속된 대용량 메모리를 할당하는 커널 서브시스템으로, 예약 영역과 이동 가능 페이지를 공존시킵니다.
- MIGRATE_CMA -- Buddy Allocator에서 CMA 전용으로 관리되는 마이그레이션 타입으로, 이동 가능 페이지만 배치됩니다.
- cma_alloc() -- CMA 영역에서 연속 물리 메모리를 할당하며, 필요하면 기존 페이지를 마이그레이션합니다.
- dma_alloc_coherent() -- DMA API가 내부적으로 CMA를 fallback 경로로 사용하는 통합 지점입니다.
- per-device CMA -- 디바이스별 전용 CMA 영역을 Device Tree로 지정하여 경합을 줄일 수 있습니다.
단계별 이해
- 연속 메모리가 필요한 이유
Scatter-Gather를 지원하지 않는 DMA 컨트롤러는 물리적으로 연속된 버퍼가 필수입니다. 시스템 운영 시간이 길어지면 메모리 단편화로 큰 연속 블록 확보가 어려워집니다. - CMA 영역 예약
부팅 시 Device Tree 또는 커널 파라미터로 CMA 영역을 예약합니다. 이 영역은 Buddy Allocator에 MIGRATE_CMA 타입으로 등록됩니다. - 평상시 공유
CMA 영역에는 이동 가능 페이지(유저 프로세스 데이터, 페이지 캐시 등)가 배치되어 메모리가 낭비되지 않습니다. - 할당 시 마이그레이션
cma_alloc()호출 시, 요청 범위 내의 기존 페이지를 다른 영역으로 마이그레이션하여 연속 공간을 확보합니다. - 해제 후 재활용
cma_release()이후 해당 영역은 다시 이동 가능 페이지로 채워질 수 있습니다.
개요
CMA(Contiguous Memory Allocator)는 물리적으로 연속된 큰 메모리 블록을 효율적으로 할당하기 위해 설계된 커널 서브시스템입니다. 2012년 Michal Nazarewicz가 Samsung에서 개발하여 Linux 3.5에 최초 통합되었으며, mm/cma.c와 mm/cma.h에 핵심 구현이 위치합니다.
연속 메모리가 필요한 이유
현대 SoC의 많은 디바이스는 물리적으로 연속된 메모리 버퍼를 요구합니다.
| 디바이스 | 연속 메모리 요구 이유 | 일반적 크기 |
|---|---|---|
| 비디오 디코더 | 프레임 버퍼를 연속 물리 주소로 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 핵심 구조체
/* 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 할당 시에만 기존 페이지를 마이그레이션합니다.
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행
정렬 요구사항을 비트맵 마스크/오프셋으로 변환합니다.
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를 유지합니다.
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 서브시스템이 자동으로 결정합니다.
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()에 의해 할당될 수 있도록 합니다.
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 사용량이 적은 시스템 | 실시간 미디어, 카메라 파이프라인 |
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은 CMA 방식으로 이동 가능 페이지와 공존하며, no-map은 커널 페이지 테이블에서 완전히 제외하여 보안 펌웨어 등 전용 용도로 사용합니다. 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는 주로 임베디드/모바일 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 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 (Direct Memory Access) -- DMA 매핑 API와 IOMMU 통합
- 페이지 할당자 (Buddy Allocator) -- CMA의 기반이 되는 물리 메모리 관리
- 메모리 관리 (심화) -- 단편화, compaction, 메모리 압축
- DMA Engine -- DMA 전송 엔진 프레임워크
- GPU (DRM/KMS) -- DRM GEM CMA 헬퍼와 GPU 메모리