Chapter 09

가상 메모리

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

모두가 실제 주소를 쓴다면

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

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

호텔 객실 번호

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

메신저의 가상 공간 페이지 0 페이지 1 페이지 2 게임의 가상 공간 페이지 0 페이지 1 페이지 2 실제 메모리 (RAM) 프레임 0 프레임 1 프레임 2 (빈칸) 프레임 4 프레임 5 SSD (스왑 공간) 두 프로그램 모두“페이지 0”을 쓰지만실제 위치는 다르다. 순서도 뒤죽박죽,일부는 디스크에.프로그램은 모른다.
그림 9-1. 가상 메모리의 대응. 각 프로세스는 0번부터 이어진 깔끔한 주소 공간을 보지만, 실제로는 RAM 여기저기와 SSD에 흩어져 있다. 이 대응표가 페이지 테이블이다.

페이지와 페이지 테이블

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

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

가상 주소 3250 3 250 페이지 번호오프셋 (그대로) 페이지 테이블 2 → 13 → 64 → 디스크 오프셋은 바뀌지 않고 그대로 붙는다 실제 주소 6250 6 250
그림 9-2. 주소 변환은 앞부분만 바꿔 끼우는 일이다. 실제 컴퓨터에서는 페이지가 4,096바이트(2¹²)라서 주소의 오른쪽 12비트가 오프셋, 나머지가 페이지 번호가 된다. 10진수 대신 2진수로 자를 뿐 원리는 같다.
SIMULATOR

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

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

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

TLB: 변환 결과의 캐시

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

CPU 코어 가상 주소 TLB최근 변환 수십~수천 개 ① 적중 (99% 이상) · 약 1박자 실제 주소로 캐시·RAM 접근6장의 메모리 계층 ② TLB에 없음 페이지 테이블 확인메모리에서 읽기 · 수십~수백 ns 찾음 → TLB에 기록 ③ 디스크에 있음 페이지 폴트 → 운영체제가 처리 (수십 µs~ms) 아래 단계로 갈수록 수십~수만 배 느려진다.다행히 대부분은 ①에서 끝난다.
그림 9-3. 주소 변환의 세 갈래. MMU는 먼저 TLB를 보고, 없으면 메모리의 페이지 테이블을, 그래도 실제 메모리에 없으면 운영체제를 부른다.
SIMULATOR

TLB 실험: 접근 방식에 따라 적중률이 어떻게 달라질까

메모리 접근 방식
페이지 크기
TLB 적중률—
TLB 실패 횟수—
평균 변환 비용—
64 MB 범위에서 메모리를 2만 번 읽는 실험이다(적중 1박자, 실패 100박자로 계산). 위 그래프는 접근 순서대로 적중(초록)과 실패(빨강)를 찍은 것이다. 해볼 것: “4 KB씩 건너뛰며”는 매번 새 페이지를 건드려 TLB가 쓸모없어진다. 이때 페이지 크기를 2 MB로 키우면? 데이터베이스와 AI 프로그램이 “큰 페이지”를 즐겨 쓰는 이유다.

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

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

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

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

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

① 앱이 주소 접근MMU: “디스크에 있음!” ② 운영체제 호출앱은 잠시 대기 상태 ③ 빈 프레임 찾기없으면 하나를 SSD로 ④ SSD에서 읽기가장 오래 걸림 ⑤ 페이지 테이블 고침“이제 프레임 n에 있음” ⑥ 같은 명령 다시 실행이번엔 성공 앱 입장에서는한 명령이 조금 늦게끝났을 뿐이다.
그림 9-4. 페이지 폴트를 처리하는 여섯 단계. ④에서 SSD를 기다리는 동안 CPU는 놀지 않고 다른 프로세스를 실행한다(8장의 스케줄링).
SIMULATOR

프로그램을 계속 열면: 스래싱 체험

RAM 크기
필요한 메모리 합—
초당 페이지 폴트—
CPU가 실제로 일한 비율—
프로그램 하나가 자주 쓰는 메모리(작업 집합)를 1.5 GB로 가정한 단순 모델이다. 프로그램이 늘수록 CPU 활용률이 오르다가, 필요한 메모리가 RAM을 넘는 순간 페이지 폴트가 폭증하며 절벽처럼 떨어진다. 이 절벽이 스래싱이다. RAM을 늘리면 절벽이 오른쪽으로 밀려난다.

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

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

SIMULATOR

페이지 교체 대결

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

보호와 공유

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

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

예측해 보기

모든 프로그램이 하나의 페이지 테이블을 함께 쓴다면 가장 먼저 생길 문제는?

9장 처음의 ‘모두가 실제 주소를 쓴다면’을 떠올려 보자.

가상 메모리의 가장 큰 선물은 프로세스마다 다른 지도를 주는 것이다. 지도를 공유하면 보호의 벽이 사라진다. 실제로 CPU 설계의 작은 틈으로 다른 프로세스나 커널의 메모리를 엿볼 수 있었던 보안 사고(2018년 멜트다운 등)는 큰 소동을 일으켰다.

핵심 정리

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

확인 퀴즈

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

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

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

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

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

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

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

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

메모리를 4 KB씩 건너뛰며 읽으면 TLB 적중률이 크게 떨어진다. 가장 효과적인 해결책은?

페이지가 크면 TLB 한 칸이 덮는 범위가 넓어져, 같은 칸 수로도 훨씬 넓은 메모리를 적중시킬 수 있다.

프로그램을 너무 많이 열었더니 CPU 사용률은 낮은데 컴퓨터가 매우 느려졌다. 가장 그럴듯한 원인은?

CPU는 SSD에서 페이지가 오기를 기다리느라 놀고 있다. 프로그램을 닫거나 RAM을 늘리면 해결된다.