Chapter 09

가상 메모리

프로그램 수십 개가 동시에 메모리를 쓴다. 그런데 각 프로그램은 마치 메모리 전체를 혼자 쓰는 것처럼 행동한다. 서로의 주소가 겹쳐도 문제가 없고, 실제 RAM보다 큰 메모리를 쓰기도 한다. 이 마법의 정체는 운영체제와 CPU가 함께 만드는 거대한 착시, 가상 메모리다.

모두가 실제 주소를 쓴다면

6장에서 메모리는 번호 붙은 사물함이라고 했다. 프로그램이 이 실제 번호(물리 주소)를 직접 쓴다고 해 보자.

해결책은 프로그램에게 진짜 주소를 알려 주지 않는 것이다. 프로그램은 0번지부터 시작하는 자기만의 가상 주소(virtual address)를 쓰고, 실제 메모리의 어디에 있는지는 운영체제와 CPU가 몰래 바꿔치기한다.

호텔 객실 번호

단체 여행객 두 팀에게 각각 “여러분 방은 1호, 2호, 3호입니다”라고 알려 준다. A팀의 1호는 실제로 507호이고 B팀의 1호는 812호다. 프런트(운영체제)만 이 대응표를 알고 있다. 손님은 자기 팀 번호만 쓰면 되고, 다른 팀 방에는 갈 방법조차 없다. 방이 모자라면 일부 짐을 지하 창고(SSD)에 맡겨 두었다가 필요할 때 꺼내 준다.

페이지와 페이지 테이블

주소를 한 바이트씩 바꿔치기하면 대응표가 메모리보다 커진다. 그래서 메모리를 일정한 크기의 덩어리로 나눠 덩어리 단위로 대응시킨다. 가상 주소 공간의 덩어리를 페이지(page), 실제 메모리의 같은 크기 덩어리를 프레임(frame)이라 한다. 실제 컴퓨터의 페이지는 보통 4,096바이트(4 KB)인데, 여기서는 계산하기 쉽게 1,000바이트로 하자.

그러면 가상 주소 3250은 “3번 페이지의 250번째 바이트”다. 앞부분이 페이지 번호, 뒷부분이 페이지 안에서의 위치(오프셋)다. 프로세스마다 하나씩 있는 페이지 테이블(page table)이 “3번 페이지 → 6번 프레임”이라고 알려 주면 실제 주소는 6250이 된다. 오프셋은 그대로 두고 앞부분만 바꾸면 된다.

SIMULATOR

주소 변환기: 가상 주소 → 실제 주소

어느 프로세스의 주소인가?
메신저의 가상 주소 공간
페이지 테이블
실제 메모리 (RAM 8칸)
해볼 것: 메신저와 게임에서 같은 가상 주소 3250을 넣어 보자. 같은 숫자인데 서로 다른 실제 위치로 간다. “디스크에 있음”인 페이지를 건드리면 페이지 폴트가 일어난다. 둘 다 ‘공유 라이브러리’ 페이지는 같은 프레임을 가리킨다.

이 변환은 메모리에 접근할 때마다, 즉 1초에 수십억 번 일어나야 한다. 그래서 소프트웨어가 아니라 CPU 안의 전용 회로인 MMU(Memory Management Unit)가 처리한다. 운영체제는 페이지 테이블을 만들어 두기만 하고, 실제 변환은 하드웨어가 번개처럼 해낸다.

TLB: 변환 결과의 캐시

페이지 테이블 자체도 메모리에 있다. 그러면 주소 하나를 쓰기 위해 메모리를 두 번(테이블 한 번, 실제 데이터 한 번) 가야 할까? 그렇게 되지 않도록 CPU는 최근에 쓴 변환 결과를 TLB라는 작은 캐시에 보관한다. 6장의 지역성 덕분에 TLB 적중률은 99%를 넘는다.

페이지 폴트와 스왑: RAM보다 큰 메모리

가상 메모리의 또 다른 선물은 실제 RAM보다 많은 메모리를 쓸 수 있다는 것이다. 당장 쓰지 않는 페이지는 SSD의 특별한 영역(스왑 공간, 페이지 파일)으로 내보내고, 페이지 테이블에 “지금은 디스크에 있음”이라고 적어 둔다.

프로그램이 그 페이지를 건드리면 MMU가 변환에 실패하고 운영체제를 부른다. 이것이 페이지 폴트(page fault)다. 운영체제는 ① 빈 프레임을 찾고(없으면 덜 쓰는 페이지 하나를 SSD로 쫓아내고) ② SSD에서 페이지를 읽어 오고 ③ 페이지 테이블을 고친 다음 ④ 프로그램을 멈췄던 바로 그 명령부터 다시 실행시킨다. 프로그램은 잠깐 느려졌다는 것 말고는 아무것도 눈치채지 못한다.

스래싱: 컴퓨터가 갑자기 버벅일 때

메모리가 많이 모자라면 페이지를 내보내자마자 다시 필요해지는 일이 반복된다. CPU는 거의 놀고 SSD만 바쁘게 페이지를 넣었다 뺐다 한다. 이를 스래싱(thrashing)이라 한다. 브라우저 탭을 수백 개 열었을 때 컴퓨터가 갑자기 굼떠지는 이유 중 하나다. 1장의 시간 감각으로 보면 RAM 접근(5분) 대신 SSD 접근(3일)을 계속 하는 셈이다.

누구를 내보낼까: 페이지 교체 알고리즘

프레임이 꽉 찼는데 새 페이지가 필요하면 누군가를 내보내야 한다. 다시 금방 쓸 페이지를 내보내면 곧 또 폴트가 난다. 좋은 선택 규칙이 성능을 좌우한다.

SIMULATOR

페이지 교체 대결

교체 방식
페이지 사용 순서
페이지 폴트—
적중률—
FIFO / LRU / 최적 폴트 수—
맨 윗줄은 프로그램이 차례로 사용하는 페이지 번호, 아래는 각 순간 프레임에 들어 있는 페이지다. 빨강 = 폴트로 새로 들어온 페이지, 초록 = 적중. 해볼 것: “교과서 예제”에서 FIFO로 프레임을 3개 → 4개로 늘려 보자. 프레임이 늘었는데 폴트가 오히려 늘어나는 이상한 현상(벨레이디의 역설)이 나타난다. LRU에서는 이런 일이 없다.

보호와 공유

페이지 테이블의 각 줄에는 주소만이 아니라 권한도 적혀 있다. “읽기만 가능”, “쓰기 가능”, “실행 가능”, “커널만 접근 가능” 같은 표시다. 프로그램이 권한 없는 페이지에 쓰려 하거나, 페이지 테이블에 아예 없는 주소를 건드리면 MMU가 즉시 막고 운영체제는 그 프로그램을 종료한다. 프로그램이 갑자기 꺼지며 나오는 “세그멘테이션 오류”, “메모리 접근 위반” 메시지가 바로 이것이다. 덕분에 버그가 있는 프로그램 하나가 다른 프로그램이나 운영체제를 망가뜨리지 못한다.

반대로 일부러 공유할 수도 있다. 여러 프로그램이 함께 쓰는 라이브러리 코드(예: 글꼴 그리기, 화면 창 관리)는 실제 메모리에 한 번만 올리고, 각 프로세스의 페이지 테이블이 같은 프레임을 가리키게 한다. 위 주소 변환기에서 메신저와 게임의 “공유 라이브러리” 페이지가 같은 프레임을 가리키는 것을 확인해 보자. 메모리를 크게 아낄 수 있다.

핵심 정리

  1. 프로그램은 실제 주소 대신 자기만의 가상 주소를 쓴다. 덕분에 서로 충돌하지 않고, 서로를 볼 수도 없다.
  2. 메모리를 페이지(보통 4 KB) 단위로 나누고, 페이지 테이블이 “가상 페이지 → 실제 프레임”을 대응시킨다. 오프셋은 그대로다.
  3. 변환은 CPU의 MMU가 하고, 최근 변환 결과는 TLB에 캐시한다.
  4. RAM에 없는 페이지를 건드리면 페이지 폴트가 나고, OS가 SSD에서 가져온다. 이 덕분에 RAM보다 큰 메모리를 쓸 수 있지만, 지나치면 스래싱이 생긴다.
  5. 프레임이 꽉 차면 FIFO, LRU 같은 규칙으로 내보낼 페이지를 고른다.
  6. 페이지 테이블의 권한 비트로 침범을 막고, 같은 프레임을 가리켜 라이브러리를 공유한다.

확인 퀴즈

페이지 크기가 1,000바이트이고 페이지 테이블에 “5번 페이지 → 2번 프레임”이 있다. 가상 주소 5072의 실제 주소는?

5072 = 5번 페이지의 72번째 바이트. 5번 페이지는 2번 프레임이므로 2000 + 72 = 2072.

두 프로세스가 같은 가상 주소 1000번지를 써도 충돌하지 않는 이유는?

가상 주소는 프로세스마다 독립적이다. 같은 숫자라도 각자의 페이지 테이블을 거쳐 다른 프레임으로 간다.

페이지 폴트가 일어났을 때 운영체제가 하는 일이 아닌 것은?

폴트를 일으킨 바로 그 명령부터 다시 실행한다. 프로그램은 중단된 줄도 모른다.

LRU 교체 방식이 내보내는 페이지는?

Least Recently Used. 최근에 쓰지 않은 것은 앞으로도 덜 쓸 것이라는 지역성 가정에 기반한다.