mmap — 파일과 공유메모리를 포인터로 붙이기
NOTE
mmap은 파일이나 메모리 덩어리를 내 프로세스의 가상 주소 공간에 “붙여주는” 시스템 콜이다. 붙이고 나면read/write없이 포인터로 바로 읽고 쓴다. POSIX 공유메모리는shm_open으로 fd를 얻고 이mmap으로 붙이는 방식이다.상위 허브: 공유메모리 한눈에 보기 · 전제 지식: 프로세스 가상 주소 공간
1. mmap이 하는 일 한 장 그림
[ 대상 ] [ 내 프로세스의 가상 주소 공간 ]
디스크 파일 / 공유메모리 / 빈 메모리 ┌──────────────┐
│ │ ... │
│ mmap(fd, 길이, 권한, 플래그) │ ▒ 붙인 구간 ▒│ ← 반환된 포인터 p
└────────────────────────────────────→ │ ... │
└──────────────┘
p[0] = 'A'; ← 포인터에 쓰면 곧 대상(메모리/파일)에 쓰는 것. read/write 호출 없음
C/Java 비교:
- C
malloc: “내 프로세스 전용 heap에서 빌리기” - C
mmap: “커널이 관리하는 어떤 대상(파일·공유메모리·빈 메모리)을 내 주소 공간에 연결하기” - Java:
FileChannel.map(...)이 돌려주는MappedByteBuffer가 바로 이mmap을 감싼 것이다.
2. 함수 시그니처 해독
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);| 인자 | 의미 | 자주 쓰는 값 |
|---|---|---|
addr | 붙일 위치 희망값 | NULL (커널이 알아서 고르게 함) |
length | 붙일 길이(바이트) | 공유메모리 크기 (내부적으로 페이지 단위 올림) |
prot | 접근 권한 | PROT_READ, PROT_WRITE, PROT_NONE |
flags | 공유 방식 | MAP_SHARED(다른 프로세스와 공유), MAP_PRIVATE, MAP_ANONYMOUS |
fd | 붙일 대상의 파일 디스크립터 | shm_open/open이 준 fd (MAP_ANONYMOUS면 -1) |
offset | 대상의 시작 위치 | 보통 0 (페이지 크기의 배수여야 함) |
| 반환값 | 붙은 구간의 시작 포인터 | 실패 시 NULL이 아니라 MAP_FAILED |
3. 무엇을 붙이느냐에 따른 종류
| 종류 | 붙이는 대상 | 용도 |
|---|---|---|
파일 매핑 + MAP_SHARED | 디스크 파일 | 큰 파일을 read 없이 접근, 수정이 파일에 반영됨 |
파일 매핑 + MAP_PRIVATE | 디스크 파일 | 내 복사본만 수정(copy-on-write). 실행 파일·공유 라이브러리 로딩이 이 방식 |
익명 매핑 (MAP_ANONYMOUS) | 파일 없는 빈 메모리 | malloc이 큰 크기를 요청받을 때 내부 사용 |
/dev/shm 파일 + MAP_SHARED | shm_open이 준 fd | POSIX 공유메모리의 실체 |
System V 공유메모리의
shmat은 이mmap에 해당하는 단계다(API만 다른 별도 방식). → 세그먼트 노트
4. 붙이는 순간 복사하지 않는다
mmap 호출 때는 가상 주소 구간만 예약되고, 데이터는 처음 접근할 때 페이지 폴트로 물리 메모리에 올라온다(Demand Paging, → 가상 주소 공간). 그래서 1GB 파일도 mmap 호출 자체는 즉시 끝난다. 같은 MAP_SHARED 대상을 여러 프로세스가 붙이면 각자 다른 가상 주소가 같은 물리 페이지를 가리킨다.
프로세스 A 가상: 0x7f3a1000 ──┐
├──→ 물리 페이지 1개 (RAM)
프로세스 B 가상: 0x7e90c000 ──┘
(가상 주소는 달라도 같은 물리 메모리)
5. 붙이고 나면 포인터로 그냥 쓴다
char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { /* 실패 처리 */ }
p[0] = 'A'; /* 배열처럼 */
strcpy(p + 10, "hello"); /* 문자열 함수도 가능 */
struct Msg *m = (struct Msg *)p; /* 구조체로 캐스팅해서 */
m->count++; /* 필드 접근도 가능 */
munmap(p, 4096); /* 다 쓰면 떼어냄 (free 아님!) */malloc으로 받은 포인터와 사용법이 같다. 다른 점은 “어디에 있는 메모리인가”와 “해제 방법”이다.
완전한 POSIX 공유메모리 예 (생성 측)
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
struct Shared { int count; char name[32]; };
int fd = shm_open("/myshm", O_CREAT | O_EXCL | O_RDWR, 0660); /* 이미 있으면 실패(EEXIST) */
ftruncate(fd, sizeof(struct Shared)); /* 크기 지정 (한 번) */
struct Shared *s = mmap(NULL, sizeof(*s), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd); /* fd를 닫아도 매핑은 유지됨 */
/* ... 사용 ... */
munmap(s, sizeof(*s));
shm_unlink("/myshm"); /* 생성자가 마지막에 삭제 */사용 측은 O_CREAT 없이 shm_open("/myshm", O_RDWR, 0) 후 mmap만 하고, 끝나면 munmap만 한다. (일부 구형 glibc에서는 링크 시 -lrt 필요)
6. 주의점
| 주의점 | 설명 |
|---|---|
| 포인터는 그 프로세스에서만 유효 | 같은 세그먼트를 다른 프로세스가 붙이면 다른 주소를 받는다(가상 주소 + ASLR). 포인터 값을 넘겨도 쓸 수 없다. 공유메모리 안에는 포인터 대신 오프셋(시작점 기준 상대 위치) 을 저장한다. |
PROT_* 위반 | PROT_READ만 줬는데 쓰면 SIGSEGV |
| 범위 초과 | length 밖을 건드리면 SIGSEGV거나 옆 메모리 오염 (버퍼 오버플로우와 동일) |
해제는 munmap | free(p)를 부르면 크래시. 짝은 munmap |
| 동기화는 직접 | 여러 프로세스가 같은 곳을 쓰면 락이 필요 → 공유메모리 동시성 |
| 구조체 캐스팅은 레이아웃 일치가 전제 | 컴파일 옵션·아키텍처가 다르면 패딩이 달라 필드가 어긋남 → 구조체 패딩 |
| 정렬 | mmap 반환 주소는 항상 페이지 경계(4KB) 에서 시작하므로 어떤 타입의 정렬 요구도 시작 주소에서는 만족한다. 구조체 내부 패딩만 신경 쓰면 된다. |
7. 한눈에 정리
| 질문 | 답 |
|---|---|
| mmap이 뭔가 | 파일/공유메모리/빈 메모리를 내 가상 주소 공간에 붙여 포인터로 접근하게 하는 시스템 콜 |
| 붙이고 바로 포인터로 쓰나 | 그렇다. malloc 포인터처럼 쓴다 (단, 그 프로세스 안에서만 유효) |
| 데이터를 복사하나 | 붙일 때는 복사 없음. 접근한 페이지만 물리 메모리에 올라옴 |
| 공유메모리와의 관계 | POSIX: shm_open + mmap / System V: shmget + shmat |
| 해제 | munmap (free 아님). 세그먼트 삭제는 shm_unlink(별도 단계) |
| 실패 반환값 | MAP_FAILED (NULL 아님) |