← 문제로

11. Copy-on-Write와 fork, 페이지 캐시·mmap(메모리 매핑)

난이도 중
내 답안
모범답안

모범답안 — Copy-on-Write와 fork, 페이지 캐시·mmap(메모리 매핑)

난이도: 중상

1. Copy-on-Write(COW)와 fork

COW는 "복사를 미루는" 최적화다. 논리적으로는 메모리를 복제했지만, 실제 물리 페이지 복사는 누군가 그 페이지에 쓸 때까지 미룬다.

fork() 직후:

  • 자식은 부모의 주소공간을 복제하지만, 물리 페이지를 실제로 복사하지 않는다. 부모·자식의 페이지 테이블 항목(PTE)들이 같은 물리 페이지를 가리키도록 매핑되고, 그 페이지들은 읽기 전용(write-protected) 으로 표시된다(원래 쓰기 가능했던 페이지도).
  • 둘 중 어느 쪽이든 그 페이지에 쓰기를 시도하면, 읽기 전용이라 페이지 폴트(protection fault) 가 발생한다. 커널이 그 폴트를 처리하며 그 페이지만 새 물리 페이지로 복사하고, 쓴 쪽의 PTE를 새 페이지(쓰기 가능)로 갱신한다. 다른 쪽은 원본을 계속 본다.
  • 결과적으로 수정된 페이지만 실제 메모리를 추가로 쓰고, 손대지 않은 페이지는 계속 공유된다.

(읽기만 하는 페이지는 영원히 공유 → fork가 싸다.)

2. fork 후 스냅샷의 장단점

장점: 자식이 fork 시점의 일관된 메모리 스냅샷을 본다(부모가 이후 수정해도 자식이 보는 페이지는 COW로 보존). 부모는 거의 멈추지 않고 계속 서비스하며, 자식은 그 스냅샷을 천천히 디스크에 기록한다. 초기 비용은 페이지 테이블 복제 정도라 빠르다.

부모가 스냅샷 중 계속 수정하면:

  • 수정하는 페이지마다 COW 복사가 일어나 메모리 사용량이 증가한다. 최악의 경우(스냅샷 동안 전 페이지를 수정) 메모리가 거의 2배까지 늘 수 있다 → OOM 위험.
  • 각 첫 쓰기가 페이지 폴트 + 복사라 그 순간 지연 스파이크가 생긴다(쓰기 경로가 느려짐).

GC/할당기가 모든 페이지를 건드리면 COW가 무너진다: 추적형 GC가 마킹/압축을 위해 힙 전체를 순회·이동하거나, 객체 헤더(마크 비트)에 쓰면, 읽기만 할 페이지에도 쓰기가 발생해 거의 모든 페이지가 복사된다 → "공유로 싸게"라는 COW의 전제가 깨지고 메모리가 폭증한다. (그래서 Redis 같은 시스템은 스냅샷 중 큰 GC/리해시를 피하고, 일부 런타임은 카드/마킹을 별도 영역에 둬 데이터 페이지 오염을 줄인다.)

3. 페이지 캐시(page cache)

페이지 캐시는 커널이 디스크 파일의 내용을 RAM에 캐싱하는 영역이다. 파일 I/O는 대부분 이 캐시를 통한다:

  • read(): 해당 파일 페이지가 캐시에 있으면 메모리 복사만(빠름), 없으면 디스크에서 읽어 캐시에 채운 뒤 복사.
  • write(): 보통 페이지 캐시에 먼저 쓰고 dirty로 표시한 뒤 곧장 반환한다(write-back). 실제 디스크 반영은 커널이 나중에 지연 기록(writeback) 으로 수행한다.
  • dirty 페이지·writeback: 수정된(dirty) 페이지는 일정 조건(시간/양/메모리 압박)에서 백그라운드로 디스크에 기록된다. 그 전에 전원이 나가면 유실될 수 있다.
  • fsync: 특정 파일의 dirty 페이지(필요 시 메타데이터)를 디스크에 강제로 내려 영속화를 보장한다. 그래서 "썼다"와 "안전하게 저장됐다"는 다르다 — DB/세이브 경로는 fsync(또는 그 비용을 아는 정책)가 필요하다.

4. mmap(메모리 매핑)

mmap은 파일(또는 익명 메모리)을 프로세스 주소공간에 매핑해, 파일을 메모리처럼 포인터로 접근하게 한다.

  • 디맨드 페이징: 매핑 즉시 다 읽지 않고, 접근하는 페이지에서 페이지 폴트가 나면 그때 페이지 캐시를 통해 로드한다. 매핑된 페이지는 페이지 캐시와 같은 물리 페이지를 공유한다.
  • MAP_SHARED: 매핑에 쓰면 페이지 캐시에 반영되어 다른 매핑/프로세스에서도 보이고 결국 파일에 writeback된다(프로세스 간 공유 메모리·파일 변경에 사용).
  • MAP_PRIVATE(COW): 읽기는 파일 페이지를 공유하지만, 쓰면 COW로 사본이 만들어져 그 프로세스에만 보이고 파일에는 반영되지 않는다(읽기 전용 데이터에 적합).

게임 서버 활용:

  • 정적 데이터 파일(맵/리소스/룩업 테이블)을 MAP_PRIVATE로 매핑 → 여러 프로세스가 페이지 캐시를 공유해 메모리 절약, 필요한 페이지만 로드, read() 복사 오버헤드 감소.
  • 익명 mmap은 큰 할당/풀의 기반.

주의점:

  • 랜덤 접근 시 페이지 폴트 지연: 캐시에 없는 페이지를 처음 만지면 디스크 I/O가 동기적으로 일어나 프레임/요청 지연 스파이크. 핫 데이터는 미리 워밍업하거나 madvise(WILLNEED) 활용.
  • 매핑된 파일의 크기 변동/삭제 위험: 매핑 후 파일이 truncate되면 접근 시 SIGBUS. 공유 매핑에 동시 쓰기는 동기화 필요.
  • writeback·fsync 타이밍: MAP_SHARED 변경의 영속화 시점은 msync/정책에 달림 — 영속성이 중요한 데이터는 명시적 동기화.

정리

COW는 fork/MAP_PRIVATE의 핵심으로, "쓰기 전엔 공유"라 복제를 싸게 만들지만 수정이 많으면 페이지 폴트+복사로 메모리·지연이 늘고, GC가 전 페이지를 건드리면 이점이 사라진다. 페이지 캐시는 파일 I/O를 RAM으로 가속하되 영속성은 fsync로 보장해야 하고, mmap은 파일을 메모리처럼 다루되 페이지 폴트 지연과 파일 수명 관리에 주의해야 한다.