MMU 없는 시스템 (NOMMU)
개요 (Overview)
Linux 커널은 MMU(Memory Management Unit)가 없는 환경에서도 동작할 수 있도록 NOMMU 서브시스템을 제공합니다. 이는 주로 임베디드 시스템(uClinux), 특정 아키텍처(SuperH, RISC-V NOMMU, ARM uClinux, Xtensa 등), 그리고 일부 특수 하드웨어에서 사용됩니다. NOMMU 환경에서는 가상 메모리 주소 변환 없이 물리 주소를 직접 사용하므로, 멀티프로세싱(fork), COW(Copy-on-Write), 메모리 보호 같은 기능이 제한되거나 지원되지 않습니다.
mm/nommu.c는 MMU가 있을 때 mm/mmap.c, mm/vmalloc.c, mm/mlock.c 등에서 수행하는 역할을 대체하는 래퍼 함수들을 포함합니다. 핵심 메커니즘은 vm_region 구조체 기반의 전역 레드-블랙 트리(nommu_region_tree)로, 매핑된 메모리 영역을 추적하고 공유를 관리합니다. fork()가 없으므로 VMA 복제가 필요 없고, 대신 vm_region.vm_usage 참조 카운트로 영역 공유를 관리합니다.
소스 파일 경로:
├── mm/nommu.c ← NOMMU 메모리 관리 핵심 (1903줄)
├── include/linux/mm_types.h ← struct vm_region 정의 (L700-716)
├── include/linux/mm.h ← nommu_region_tree/sem 선언, is_nommu_shared_mapping()
├── mm/Makefile ← mmu-y := nommu.o (기본 빌드 대상)
├── mm/Kconfig ← NOMMU_INITIAL_TRIM_EXCESS, MMAP_ALLOW_UNINITIALIZED
├── Documentation/admin-guide/mm/nommu-mmap.rst ← 공식 문서
└── arch/arm/Kconfig-nommu ← ARM uClinux 설정
빠른 점검 명령
# NOMMU 커널 여부 확인
cat /proc/version | grep -i nommu || zcat /proc/config.gz 2>/dev/null | grep CONFIG_MMU || grep CONFIG_MMU /boot/config-$(uname -r) 2>/dev/null
# 시스템에 MMU 지원 여부 확인
getconf PAGESIZE
ls /proc/self/pagemap 2>/dev/null && echo "MMU 지원" || echo "MMU 미지원 또는 접근 불가"
# 현재 시스템의 메모리 매핑 구조 확인
cat /proc/self/maps | head -10
# vm_area_struct 관련 정보 확인
cat /proc/self/smaps | head -20
# 커널 설정에서 NOMMU 옵션 확인
zcat /proc/config.gz 2>/dev/null | grep -E "NOMMU|!MMU" || grep -E "NOMMU|!MMU" /boot/config-$(uname -r) 2>/dev/null
# 커널 빌드 시 nommu.o 포함 여부 확인
cat /lib/modules/$(uname -r)/build/.config 2>/dev/null | grep CONFIG_MMU
# 메모리 매핑 관련 커널 로그 확인
dmesg | grep -iE "nommu|mmap|region" | tail -5
# RISC-V NOMMU defconfig 존재 여부 확인 (크로스 컴파일 환경)
ls arch/riscv/configs/nommu* 2>/dev/null
# ARM NOMMU 관련 설정 확인
ls arch/arm/configs/*nommu* 2>/dev/null || ls arch/arm/Kconfig*nommu* 2>/dev/null
핵심 자료구조
1. `struct vm_region` (include/linux/mm_types.h:704)
NOMMU 환경에서 매핑된 메모리 영역을 추적하는 전역 구조체입니다. 전역 레드-블랙 트리에 저장되며, 여러 VMA가 하나의 region을 공유할 수 있습니다.
// include/linux/mm_types.h:704-716
struct vm_region {
struct rb_node vm_rb; // 전역 region 트리의 연결 노드
vm_flags_t vm_flags; // VMA vm_flags
unsigned long vm_start; // region 시작 주소
unsigned long vm_end; // region 초기화된 끝 주소
unsigned long vm_top; // region 할당된 끝 주소 (vm_end >= vm_top)
unsigned long vm_pgoff; // vm_file에서 vm_start에 대응하는 오프셋
struct file *vm_file; // 백업 파일 또는 NULL
int vm_usage; // region 참조 카운트 (nommu_region_sem 하에서 접근)
bool vm_icache_flushed : 1; // icache 플러시 여부
};
필드 설명:
vm_start / vm_end: 매핑된 주소 범위. vm_end는 실제 사용 가능한 끝 주소.vm_top: 실제 할당된 물리 페이지의 끝. vm_end <= vm_top이며, 예약된 공간을 포함.vm_usage: 이 region을 가리키는 VMA의 수. 0이 되면 해제됨.VM_MAPPED_COPY: 익명 매핑 또는 사본 매핑에 설정. 직접 매핑이 아님을 의미.2. `struct vm_area_struct`의 NOMMU 확장 (include/linux/mm.h:270-275)
NOMMU에서는 vm_area_struct에 전역 region 트리와 세마포어가 추가됩니다.
// include/linux/mm.h:270-275
#ifndef CONFIG_MMU
extern struct rb_root nommu_region_tree; // 전역 region 레드-블랙 트리
extern struct rw_semaphore nommu_region_sem; // region 트리 접근 세마포어
extern unsigned int kobjsize(const void *objp); // 할당 크기 반환
#endif
3. NOMMU 매핑 능력 플래그 (mm/nommu.c:730-826)
파일 매핑 시 지원 가능한 매핑 방식을 결정하는 플래그입니다.
// mm/nommu.c:738-746 (파일 타입별 기본 능력)
switch (file_inode(file)->i_mode & S_IFMT) {
case S_IFREG:
case S_IFBLK:
capabilities = NOMMU_MAP_COPY; // 사본 허용
break;
case S_IFCHR:
capabilities = NOMMU_MAP_DIRECT | // 직접 매핑
NOMMU_MAP_READ | // 읽기 가능
NOMMU_MAP_WRITE; // 쓰기 가능
break;
}
능력 플래그 역할:
NOMMU_MAP_DIRECT: 파일 시스템/드라이버가 직접 매핑을 지원하는 경우NOMMU_MAP_COPY: 파일 내용을 메모리로 복사하여 매핑하는 경우NOMMU_MAP_READ / NOMMU_MAP_WRITE / NOMMU_MAP_EXEC: 보호 수준별 직접 매핑 지원4. `is_nommu_shared_mapping()` (include/linux/mm.h:1944-1956)
공유 매핑 여부를 판단하는 인라인 함수입니다.
// include/linux/mm.h:1944-1956
#ifndef CONFIG_MMU
static inline bool is_nommu_shared_mapping(vm_flags_t flags)
{
// 공유 매핑: MAP_SHARED이거나 R/O MAP_PRIVATE 오버레이
return flags & (VM_MAYSHARE | VM_MAYOVERLAY);
}
#endif
5. `generic_file_vm_ops` (mm/nommu.c:56-57)
NOMMU에서 파일 기반 VM 연산의 빈 래퍼입니다.
// mm/nommu.c:56-57
const struct vm_operations_struct generic_file_vm_ops = {
};
핵심 함수
1. `do_mmap()` — 메모리 매핑 생성 (mm/nommu.c:1012-1247)
NOMMU 환경에서 mmap() 시스템 콜의 핵심 구현입니다. 매핑 요청을 검증하고, region을 할당하며, VMA를 생성합니다.
시그니처:
// mm/nommu.c:1012-1020
unsigned long do_mmap(struct file *file,
unsigned long addr,
unsigned long len,
unsigned long prot,
unsigned long flags,
vm_flags_t vm_flags,
unsigned long pgoff,
unsigned long *populate,
struct list_head *uf)
주요 분기 로직:
1. validate_mmap_request() → 매핑 가능 여부 판단
2. is_nommu_shared_mapping(vm_flags) → 공유 매핑이면 기존 region 탐색 (rb-tree 순회)
3. 기존 region 발견 시 vm_usage++로 공유, do_mmap_shared_file() 호출
4. 새 매핑 시 do_mmap_private()로 사본 할당 또는 do_mmap_shared_file()로 직접 매핑
5. add_nommu_region()으로 전역 트리에 추가
6. 익명 매핑且 MAP_UNINITIALIZED 미설정 시 memset(0)으로 초기화
2. `validate_mmap_request()` — 매핑 요청 검증 (mm/nommu.c:690-842)
매핑 요청의 유효성을 검사하고, 파일 타입과 권한에 따라 매핑 능력을 결정합니다.
시그니처:
// mm/nommu.c:690-696
static int validate_mmap_request(struct file *file,
unsigned long addr,
unsigned long len,
unsigned long prot,
unsigned long flags,
unsigned long pgoff,
unsigned long *_capabilities)
핵심 검증:
MAP_FIXED 사용 불가 (NOMMU에서는 주소 힌트 무시)MAP_PRIVATE 또는 MAP_SHARED만 허용can_mmap_file(), mmap_capabilities() 확인MAP_SHARED + 쓰기: 파일 쓰기 권한 필요path_noexec() → PROT_EXEC 거부READ_IMPLIES_EXEC personality 처리3. `do_mmap_private()` — 사본 매핑 (mm/nommu.c:914-1007)
MAP_PRIVATE 파일 매핑 또는 익명 매핑을 위해 물리 페이지를 할당하고 파일 내용을 복사합니다.
시그니처:
// mm/nommu.c:914-917
static int do_mmap_private(struct vm_area_struct *vma,
struct vm_region *region,
unsigned long len,
unsigned long capabilities)
흐름:
1. NOMMU_MAP_DIRECT이면 mmap_file() 시도 → 직접 매핑 성공 시 복사 불필요
2. 직접 매핑 실패 시 alloc_pages_exact()로 연속 물리 페이지 할당
3. VM_MAPPED_COPY 플래그 설정
4. 파일 매핑이면 kernel_read()로 내용 복사
5. 익명 매핑이면 vma_set_anonymous() 설정
4. `do_munmap()` — 매핑 해제 (mm/nommu.c:1426-1486)
매핑된 메모리 영역을 해제합니다. NOMMU에서는 단일 VMA로 덮여 있어야 합니다.
시그니처:
// mm/nommu.c:1426-1426
int do_munmap(struct mm_struct *mm, unsigned long start,
size_t len, struct list_head *uf)
분기:
split_vma()로 분할 후 vmi_shrink_vma()로 축소 가능delete_vma_from_mm() → delete_vma() → put_nommu_region() → free_page_series()5. `__put_nommu_region()` — region 참조 해제 (mm/nommu.c:530-551)
region의 참조 카운트를 감소시키고, 0이 되면 해제합니다.
시그니처:
// mm/nommu.c:530-531
static void __put_nommu_region(struct vm_region *region)
__releases(nommu_region_sem)
흐름:
1. vm_usage-- → 0이면 region 해제
2. delete_nommu_region()으로 전역 트리에서 제거
3. vm_file이 있으면 fput()으로 파일 참조 해제
4. VM_MAPPED_COPY이면 free_page_series()로 물리 페이지 해제
5. kmem_cache_free(vm_region_jar)로 slab 반환
호출 흐름
mmap() 시스템 콜 흐름
mmap() 시스템 콜
└─ ksys_mmap_pgoff() [mm/nommu.c:1249]
└─ vm_mmap_pgoff() [공통]
└─ do_mmap() [mm/nommu.c:1012]
├─ validate_mmap_request() [mm/nommu.c:690] ← 매핑 검증
├─ determine_vm_flags() [mm/nommu.c:848] ← VM 플래그 결정
├─ vm_area_alloc() [VMA 할당]
├─ [공유 매핑 검색] [rb-tree 순회]
│ ├─ 기존 region 발견 → vm_usage++
│ └─ do_mmap_shared_file() [mm/nommu.c:893]
├─ [새 매핑]
│ ├─ NOMMU_MAP_DIRECT → mmap_file()
│ └─ do_mmap_private() [mm/nommu.c:914]
│ └─ alloc_pages_exact() [물리 페이지 할당]
├─ add_nommu_region() [mm/nommu.c:471] ← 전역 트리 추가
├─ setup_vma_to_mm() [mm/nommu.c:562]
└─ vma_iter_store_new() [mm 트리에 VMA 추가]
munmap() 시스템 콜 흐름
munmap() 시스템 콜
└─ vm_munmap() [mm/nommu.c:1488]
└─ do_munmap() [mm/nommu.c:1426]
├─ [파일 매핑] → 전체 VMA만 해제
├─ [익명 매핑]
│ ├─ split_vma() [mm/nommu.c:1306] ← 필요 시 분할
│ └─ vmi_shrink_vma() [mm/nommu.c:1384] ← 축소
└─ delete_vma_from_mm() → delete_vma()
└─ put_nommu_region() [mm/nommu.c:556]
└─ __put_nommu_region() [mm/nommu.c:530]
├─ delete_nommu_region() [전역 트리 제거]
├─ free_page_series() [물리 페이지 해제]
└─ kmem_cache_free() [slab 반환]
vfree() / vmalloc() 흐름
vfree(addr)
└─ kfree(addr) [mm/nommu.c:108]
vmalloc(size)
└─ __vmalloc_noprof(size, GFP_KERNEL) [mm/nommu.c:112]
└─ kmalloc_noprof(size, ...) [NOMMU에서는 kmalloc으로 단순화]
vmalloc_to_page(addr)
└─ virt_to_page(addr) [mm/nommu.c:168]
조건별 비교
MMU vs NOMMU 비교
| 항목 | MMU 시스템 | NOMMU 시스템 |
|---|---|---|
| 가상 메모리 | 페이지 테이블 기반 주소 변환 | 물리 주소 직접 사용 |
| fork() | COW로 복제 | 미지원 (fork 불가, clone须 CLONE_VM) |
| mprotect() | 페이지 접근 권한 변경 | 미지원 |
| mmap() 주소 | 커널이 최적 주소 선택 | 주소 힌트 무시, 연속 할당 |
| 공유 메모리 | 페이지 공유 + COW | region 참조 카운트로 공유 |
| 메모리 보호 | PTE 기반 읽기/쓰기/실행 보호 | 없음 (프로세스 간 보호 없음) |
| 스왑 | 지원 | 미지원 |
| vmalloc | 별도 가상 영역 할당 | kmalloc 래퍼로 단순화 |
| VMA 관리 | 파일 기반 매핑 분리 | region 기반 전역 관리 |
매핑 타입별 동작 비교
| 매핑 타입 | MMU | NOMMU |
|---|---|---|
| Anonymous MAP_PRIVATE | 임의 페이지, COW | 연속 물리 페이지 할당 |
| Anonymous MAP_SHARED | 공유 + COW | MAP_PRIVATE와 동일 (fork 미지원) |
| 파일 MAP_PRIVATE (읽기만) | 페이지 캐시 공유 | 직접 매핑 또는 사본 복사 |
| 파일 MAP_PRIVATE (쓰기) | COW 사본 | 항상 사본 복사 |
| 파일 MAP_SHARED | 공유 페이지 | 직접 매핑만 가능 (드라이버 지원 필요) |
| 파일 MAP_SHARED (쓰기) | 백그라운드 쓰기 | 미지원 |
매핑 능력(NOMMU_MAP_*) 결정 흐름
| 조건 | 직접 매핑 (DIRECT) | 사본 (COPY) | 결과 |
|---|---|---|---|
| 파일 미지정 (익명) | — | 가능 | 사본 할당 |
| Regular 파일 | 불가 | 가능 | 파일 내용 복사 |
| Char 디바이스 | 가능 (드라이버 지원 시) | — | 드라이버 제공 주소에 직접 매핑 |
| Block 디바이스 | 불가 | 가능 | 사본 복사 |
| 공유 매핑 (MAP_SHARED) | 필수 | 불가 | 직접 매핑 실패 시 -ENODEV |
| 프라이빗 쓰기 | 불가 | 가능 | 항상 사본 |
아키텍처별 NOMMU 지원
| 아키텍처 | defconfig | 특징 |
|---|---|---|
| ARM uClinux | arch/arm/Kconfig-nommu | DRAM/FLASH 베이스, 벡터 리매핑, ARM MPU 지원 |
| RISC-V NOMMU | arch/riscv/configs/nommu_* | virt, K210 보드 지원 |
| SuperH | arch/sh/mm/Kconfig | `config NOMMU` 명시적 정의 |
| Xtensa | arch/xtensa/configs/nommu_* | KC705 보드 지원 |
| m68k | — | vmlinux-nommu.ldr 링커 스크립트 존재 |
Kconfig 옵션 비교
| 옵션 | 기본값 | 설명 | ||
|---|---|---|---|---|
| `CONFIG_MMAP_ALLOW_UNINITIALIZED` | n | `MAP_UNINITIALIZED`로 미초기화 메모리 허용 (EXPERT + !MMU) | ||
| `CONFIG_NOMMU_INITIAL_TRIM_EXCESS` | 1 | 부팅 시 mmap 잔여 공간 트리밍 (`/proc/sys/vm/nr_trim_pages`) | ||
| `CONFIG_NEED_PER_CPU_KM` | !SMP \ | \ | !MMU | km 기반 per-cpu 할당자 사용 |
vmalloc / vfree NOMMU 구현
NOMMU에서는 vmalloc()이 별도 가상 영역을 할당하지 않고 kmalloc()으로 단순화됩니다. 이는 가상-물리 주소 변환이 없으므로 굳이 별도 매핑이 필요 없기 때문입니다.
// mm/nommu.c:112-119
void *__vmalloc_noprof(unsigned long size, gfp_t gfp_mask)
{
// __GFP_HIGHMEM는 kmalloc에서 사용 불가 (논리 주소만 반환)
return kmalloc_noprof(size, (gfp_mask | __GFP_COMP) & ~__GFP_HIGHMEM);
}
// mm/nommu.c:106-109
void vfree(const void *addr)
{
kfree(addr); // NOMMU에서는 vfree가 kfree의 래퍼
}
지원 함수들:
vmalloc() → __vmalloc_noprof() → kmalloc_noprof()vzalloc() → __vmalloc_noprof(GFP_KERNEL | __GFP_ZERO)vmalloc_node() → vmalloc_noprof() (노드 무시)vmalloc_32() → __vmalloc_noprof()vmalloc_to_page() → virt_to_page() (직접 변환)vmalloc_to_pfn() → page_to_pfn(virt_to_page())사용 불가 함수들 (BUG() 호출):
vmap() → BUG() (별도 가상 매핑 불가)vunmap() → BUG()vm_map_ram() → BUG()vm_unmap_ram() → BUG()free_vm_area() → BUG()brk() / mremap() 제약
brk() 동작 (mm/nommu.c:380-403)
NOMMU brk()는 매우 단순합니다. mm->start_brk ~ mm->context.end_brk 범위 내에서만 크기 변경 가능합니다.
// mm/nommu.c:380-403
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
struct mm_struct *mm = current->mm;
if (brk < mm->start_brk || brk > mm->context.end_brk)
return mm->brk; // 범위 밖 → 현재 brk 반환
if (mm->brk == brk)
return mm->brk; // 변경 없음
if (brk <= mm->brk) {
mm->brk = brk; // 축소 허용
return brk;
}
flush_icache_user_range(mm->brk, brk); // 확장 시 icache 플러시
return mm->brk = brk;
}
mremap() 제약 (mm/nommu.c:1542-1576)
NOMMU에서는 MREMAP_FIXED가 지원되지 않으며, 같은 region 내에서만 크기 변경 가능합니다.
// mm/nommu.c:1567-1568
if (is_nommu_shared_mapping(vma->vm_flags))
return (unsigned long) -EPERM; // 공유 매핑은 재할당 불가
디버그 지원
region 트리 검증 (mm/nommu.c:437-466)
CONFIG_DEBUG_NOMMU_REGIONS 활성화 시 region 트리의 무결성을 검증합니다.
// mm/nommu.c:438-461
#ifdef CONFIG_DEBUG_NOMMU_REGIONS
static noinline void validate_nommu_regions(void)
{
// region 시작/끝 주소 유효성 확인
BUG_ON(region->vm_end <= region->vm_start);
BUG_ON(region->vm_top < region->vm_end);
// region 간 겹침 없음 확인
BUG_ON(region->vm_start < last->vm_top);
}
#endif
주요 디버그/sysctl
| 항목 | 경로 | 설명 |
|---|---|---|
| `nr_trim_pages` | `/proc/sys/vm/nr_trim_pages` | mmap 잔여 페이지 트리밍 기준 |
| `mmap_pages_allocated` | 커널 내부 | 할당된 mmap 페이지 수 |
| `sysctl_max_map_count` | `/proc/sys/vm/max_map_count` | 프로세스당 최대 매핑 수 |