[Daily morning study] mmap과 파일 I/O — 메모리 맵 파일
#daily morning study
mmap이란
mmap()은 파일이나 디바이스를 프로세스의 가상 주소 공간에 직접 매핑하는 시스템 콜이다. 파일의 내용을 메모리처럼 읽고 쓸 수 있게 해주는 메커니즘으로, 커널이 중간에서 데이터를 복사하는 과정을 줄여 성능을 높일 수 있다.
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
| 파라미터 | 설명 |
|---|---|
addr | 매핑할 가상 주소 (NULL이면 커널이 선택) |
length | 매핑 크기 (바이트) |
prot | 접근 권한 (PROT_READ, PROT_WRITE, PROT_EXEC) |
flags | 매핑 종류 (MAP_SHARED, MAP_PRIVATE, MAP_ANONYMOUS) |
fd | 매핑할 파일 디스크립터 |
offset | 파일 내 시작 오프셋 (페이지 크기의 배수여야 함) |
기존 read/write 방식과의 비교
전통적인 read() 방식의 흐름
read()시스템 콜 호출 → 유저 모드에서 커널 모드로 전환- 커널이 디스크에서 데이터를 페이지 캐시(Page Cache)에 로드
- 페이지 캐시에서 유저 버퍼로 데이터 복사
- 커널 모드에서 유저 모드로 복귀
디스크 → [커널 페이지 캐시] → [유저 버퍼]
복사 발생
mmap 방식의 흐름
mmap()호출 → 가상 주소 공간에 파일 매핑 등록 (실제 로드는 아직 없음)- 해당 주소에 접근 → 페이지 폴트 발생
- 커널이 페이지 캐시에서 가상 주소로 직접 매핑 (복사 없음)
- 이후 접근은 페이지 캐시와 가상 주소 공간을 직접 공유
디스크 → [커널 페이지 캐시]
↕ 직접 매핑 (복사 없음)
[프로세스 가상 주소 공간]
복사 단계가 하나 사라진다는 것이 핵심이다. 대용량 파일을 반복적으로 읽는 경우 성능 차이가 뚜렷하게 나타난다.
MAP_SHARED vs MAP_PRIVATE
MAP_SHARED
매핑된 메모리에 쓰기 연산을 하면 파일에도 반영된다. 같은 파일을 mmap한 다른 프로세스들도 변경 사항을 볼 수 있다. 프로세스 간 공유 메모리(IPC)로도 활용된다.
// 파일을 쓰기 가능하게 공유 매핑
int fd = open("data.bin", O_RDWR);
char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'X'; // 파일에 직접 반영됨
msync(p, 4096, MS_SYNC); // 명시적 동기화
MAP_PRIVATE
쓰기 발생 시 Copy-on-Write(CoW) 로 처리된다. 원본 파일은 변경되지 않고, 해당 프로세스만 사용하는 별도의 페이지가 생성된다. 파일을 읽기 전용으로 사용하면서 임시 수정이 필요한 경우에 적합하다.
Write 발생 전: [프로세스 A] → [페이지 캐시]
Write 발생 후: [프로세스 A] → [복사된 프라이빗 페이지]
[페이지 캐시] ← 원본 그대로
익명 매핑 (MAP_ANONYMOUS)
파일과 무관하게 메모리만 할당하고 싶을 때 사용한다. fd는 -1로 넘기고 MAP_ANONYMOUS 플래그를 추가한다. malloc()도 내부적으로 큰 메모리 요청에는 mmap()을 사용한다 (glibc 기준 128KB 이상).
// 1MB 메모리 익명 매핑
void *mem = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
페이지 폴트와 Demand Paging
mmap()은 호출 시점에 모든 데이터를 메모리에 올리지 않는다. 가상 주소 공간에 매핑 정보만 등록하고, 실제로 접근할 때 페이지 폴트가 발생하면서 커널이 해당 페이지를 로드한다. 이를 Demand Paging이라 한다.
mmap() 호출 → 페이지 테이블에 매핑 정보만 기록 (물리 메모리 없음)
첫 번째 접근 → 페이지 폴트 발생 → 커널이 디스크에서 페이지 로드
이후 접근 → 이미 메모리에 있으므로 폴트 없이 바로 접근
대용량 파일에서 일부만 읽는 경우, 접근한 페이지만 메모리에 올라오므로 불필요한 I/O를 피할 수 있다.
msync() — 변경 사항 동기화
MAP_SHARED로 매핑된 메모리에 쓴 내용은 즉시 디스크에 반영되지 않는다. 커널이 적절한 시점에 플러시하지만, 명시적으로 동기화하려면 msync()를 사용한다.
int msync(void *addr, size_t length, int flags);
// MS_SYNC : 동기적으로 플러시 완료까지 대기
// MS_ASYNC : 비동기적으로 플러시 예약
munmap() — 매핑 해제
int munmap(void *addr, size_t length);
프로세스 종료 시 자동으로 해제되지만, 명시적으로 해제하는 것이 좋다. free()처럼 그냥 버리면 메모리 릭이 된다.
mmap의 활용 사례
대용량 파일 처리
로그 파일이나 바이너리 데이터를 파싱할 때, 전체 파일을 버퍼에 읽어들이는 대신 mmap으로 매핑하고 필요한 부분만 접근하면 메모리와 I/O 효율이 높아진다.
공유 메모리 IPC
MAP_SHARED와 익명 매핑 또는 파일을 조합하면 프로세스 간 메모리를 공유할 수 있다. POSIX 공유 메모리(shm_open)도 내부적으로 mmap을 사용한다.
// 프로세스 A와 B가 같은 파일을 MAP_SHARED로 매핑하면 공유 메모리처럼 동작
실행 파일 로딩
Linux에서 ELF 실행 파일을 로드할 때 커널은 mmap을 사용한다. .text, .data, .bss 섹션을 각각 적절한 권한(PROT_EXEC, PROT_READ|PROT_WRITE)으로 매핑한다.
데이터베이스 스토리지 엔진
SQLite, LMDB 같은 임베디드 DB는 mmap으로 데이터 파일에 직접 접근한다. 커널의 페이지 캐시를 DB의 버퍼 풀로 그대로 활용하는 셈이라 별도의 버퍼 관리 로직이 간단해진다.
주의점
파일 크기와 매핑 크기 파일 크기보다 큰 범위를 매핑하고 접근하면 SIGBUS가 발생한다. 파일을 미리 충분한 크기로 ftruncate()해두어야 한다.
스왑 압박 mmap으로 매핑된 페이지도 메모리 부족 시 커널이 스왑 아웃할 수 있다. MAP_LOCKED 플래그나 mlock()으로 고정할 수 있지만 일반적으로 필요 없다.
TLB 플러시 오버헤드 매핑/언매핑이 잦으면 TLB 플러시 비용이 생긴다. 한 번 매핑한 영역을 오래 유지하는 패턴이 유리하다.
read/write vs mmap 선택 기준
| 상황 | 권장 방식 |
|---|---|
| 소용량 파일, 단순 순차 읽기 | read() |
| 대용량 파일, 랜덤 접근 | mmap() |
| 프로세스 간 공유 메모리 | mmap() + MAP_SHARED |
| 네트워크 소켓 | read() / recv() |
| DB 스토리지 엔진 | mmap() |
| 스트리밍 처리 (한 번만 순차 읽기) | read() + O_DIRECT |
mmap이 항상 빠른 건 아니다. 접근 패턴이 불규칙하고 데이터 크기가 클 때 페이지 폴트 비용이 쌓이면 오히려 느려질 수 있다. 용도에 맞게 골라 쓰는 것이 중요하다.