가상 메모리
프로세스마다 독립된 주소 공간을 주는 메커니즘
가상 메모리는 프로세스마다 주소 0부터 시작하는 독립된 주소 공간을 제공하고, 페이지 테이블로 가상 주소를 물리 주소로 번역한다. TLB가 번역을 캐싱해 성능을 유지하고, RAM이 부족하면 스왑으로 디스크를 활용한다. mmap은 파일을 가상 메모리에 매핑해서 복사 없이 커널 버퍼에 직접 접근하는 방식이다.
개념
가상 메모리는 프로세스마다 주소 0부터 시작하는 독립된 주소 공간을 주는 메커니즘이다. 프로세스 A의 주소 1000번과 프로세스 B의 주소 1000번은 실제 물리 RAM에서 다른 위치를 가리킨다. 프로세스는 자기 가상 주소만 알고, 실제 물리 주소는 모른다.
가상 메모리가 없으면 프로그램들이 물리 주소를 직접 쓰게 되어 주소 충돌이 발생하고, 다른 프로세스의 메모리를 읽을 수 있어 보안 문제도 생긴다.
페이지 테이블과 TLB
가상 주소를 물리 주소로 번역하는 표가 페이지 테이블이다. 번역 단위는 페이지(보통 4KB)이고, 프로세스마다 자기만의 페이지 테이블을 가진다.
프로세스 A: 가상 1000 → 물리 50000
프로세스 B: 가상 1000 → 물리 80000
페이지 테이블은 RAM에 있어서 메모리 접근마다 RAM을 두 번 찔러야 한다(번역 1회 + 실제 데이터 1회). 이걸 해결하려고 CPU 안에 번역 결과를 캐싱하는 TLB(Translation Lookaside Buffer) 가 있다. 평소 적중률이 99% 이상이라 대부분의 접근에서 페이지 테이블까지 안 내려간다.
프로세스 간 컨텍스트 스위칭 시 페이지 테이블이 바뀌므로 TLB가 플러시된다. 이것이 프로세스 스위칭이 스레드 스위칭보다 비싼 이유 중 하나다.
페이지 폴트와 스왑
RAM이 8GB인데 프로세스들이 16GB를 쓰려고 하면 — 커널이 당장 안 쓰는 페이지를 디스크로 옮긴다. 이것이 스왑(swap) 이다.
나중에 프로세스가 디스크로 옮겨진 페이지에 접근하면 물리 RAM에 없으므로 페이지 폴트(page fault) 가 발생한다. 커널이 디스크에서 해당 페이지를 다시 RAM으로 가져온다.
느려지는 이유가 여기에 있다. RAM 접근은 ~100ns인데 디스크는 SSD도 ~100μs, HDD는 10ms. 1000배100000배 느린 저장소를 RAM 대신 쓰는 셈이다.
스왑이 과도하게 일어나서 CPU가 실제 일은 못 하고 페이지 옮기기만 반복하는 상태를 스래싱(thrashing) 이라고 한다. 책상이 너무 작아서 한 줄 읽을 때마다 책을 서랍에서 바꿔야 하는 상황이다.
mmap
보통 파일을 읽으면 read() 호출 시 디스크 → 커널 버퍼 → 프로세스 메모리로 복사가 두 번 일어난다.
mmap은 파일을 가상 메모리에 매핑해서, 프로세스가 커널 버퍼에 직접 접근할 수 있게 한다. 프로세스 메모리로의 복사가 없다.
import mmap
f = open("data.bin", "rb")
m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
print(m[42]) # 배열처럼 접근 — 필요한 부분만 그때 디스크에서 가져옴
OS가 알아서 해주는 게 아니라 개발자가 선택적으로 쓰는 것이다. 매핑만 해놓고 실제로 접근할 때 페이지 폴트가 발생하면서 그때 디스크에서 로드된다.
- 복사가 없으니 대용량 파일에 효율적
- 4GB 파일이어도 전부 RAM에 올릴 필요 없이 필요한 부분만 로드
- 여러 프로세스가 같은 파일을 mmap하면 물리 메모리 공유 가능
fork의 COW도 같은 가상 메모리 메커니즘 위에서 동작하고, exec()로 실행 파일을 메모리에 올릴 때도 내부적으로 mmap이 쓰인다.