가상 메모리
프로그램 수십 개가 동시에 메모리를 쓴다. 그런데 각 프로그램은 마치 메모리 전체를 혼자 쓰는 것처럼 행동한다. 서로의 주소가 겹쳐도 문제가 없고, 실제 RAM보다 큰 메모리를 쓰기도 한다. 이 마법의 정체는 운영체제와 CPU가 함께 만드는 거대한 착시, 가상 메모리다.
- 여러 프로그램이 실제 주소를 직접 쓰면 생기는 세 가지 문제를 설명한다.
- 가상 주소를 페이지 번호와 오프셋으로 나누고, 페이지 테이블로 실제 주소로 바꾼다.
- 페이지 폴트가 무엇이며, 메모리가 부족할 때 SSD를 빌려 쓰는 스왑의 원리를 안다.
- FIFO, LRU, 최적 교체 알고리즘을 시뮬레이터로 비교한다.
- 가상 메모리가 프로그램끼리의 침범을 막는 보호 장치가 되는 이유를 이해한다.
모두가 실제 주소를 쓴다면
6장에서 메모리는 번호 붙은 사물함이라고 했다. 프로그램이 이 실제 번호(물리 주소)를 직접 쓴다고 해 보자.
- 충돌: 메신저도, 게임도 “1000번지에 내 데이터를 둬야지”라고 만들어졌다면? 서로의 데이터를 덮어쓴다.
- 보안: 악성 프로그램이 은행 앱이 쓰는 주소를 읽으면 비밀번호가 새어 나간다.
- 부족: 메모리가 8 GB인데 프로그램들이 합쳐서 12 GB를 원하면? 누군가는 실행할 수 없다.
해결책은 프로그램에게 진짜 주소를 알려 주지 않는 것이다. 프로그램은 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이 된다. 오프셋은 그대로 두고 앞부분만 바꾸면 된다.
주소 변환기: 가상 주소 → 실제 주소
메신저의 가상 주소 공간
페이지 테이블
실제 메모리 (RAM 8칸)
이 변환은 메모리에 접근할 때마다, 즉 1초에 수십억 번 일어나야 한다. 그래서 소프트웨어가 아니라 CPU 안의 전용 회로인 MMU(Memory Management Unit)가 처리한다. 운영체제는 페이지 테이블을 만들어 두기만 하고, 실제 변환은 하드웨어가 번개처럼 해낸다.
페이지 테이블 자체도 메모리에 있다. 그러면 주소 하나를 쓰기 위해 메모리를 두 번(테이블 한 번, 실제 데이터 한 번) 가야 할까? 그렇게 되지 않도록 CPU는 최근에 쓴 변환 결과를 TLB라는 작은 캐시에 보관한다. 6장의 지역성 덕분에 TLB 적중률은 99%를 넘는다.
페이지 폴트와 스왑: RAM보다 큰 메모리
가상 메모리의 또 다른 선물은 실제 RAM보다 많은 메모리를 쓸 수 있다는 것이다. 당장 쓰지 않는 페이지는 SSD의 특별한 영역(스왑 공간, 페이지 파일)으로 내보내고, 페이지 테이블에 “지금은 디스크에 있음”이라고 적어 둔다.
프로그램이 그 페이지를 건드리면 MMU가 변환에 실패하고 운영체제를 부른다. 이것이 페이지 폴트(page fault)다. 운영체제는 ① 빈 프레임을 찾고(없으면 덜 쓰는 페이지 하나를 SSD로 쫓아내고) ② SSD에서 페이지를 읽어 오고 ③ 페이지 테이블을 고친 다음 ④ 프로그램을 멈췄던 바로 그 명령부터 다시 실행시킨다. 프로그램은 잠깐 느려졌다는 것 말고는 아무것도 눈치채지 못한다.
메모리가 많이 모자라면 페이지를 내보내자마자 다시 필요해지는 일이 반복된다. CPU는 거의 놀고 SSD만 바쁘게 페이지를 넣었다 뺐다 한다. 이를 스래싱(thrashing)이라 한다. 브라우저 탭을 수백 개 열었을 때 컴퓨터가 갑자기 굼떠지는 이유 중 하나다. 1장의 시간 감각으로 보면 RAM 접근(5분) 대신 SSD 접근(3일)을 계속 하는 셈이다.
누구를 내보낼까: 페이지 교체 알고리즘
프레임이 꽉 찼는데 새 페이지가 필요하면 누군가를 내보내야 한다. 다시 금방 쓸 페이지를 내보내면 곧 또 폴트가 난다. 좋은 선택 규칙이 성능을 좌우한다.
- FIFO (먼저 들어온 것 먼저): 가장 오래전에 들어온 페이지를 내보낸다. 단순하지만, 오래전에 들어왔어도 자주 쓰는 페이지를 내보낼 수 있다.
- LRU (가장 오래 안 쓴 것): 마지막으로 쓴 지 가장 오래된 페이지를 내보낸다. “최근에 안 쓴 것은 앞으로도 안 쓸 것”이라는 지역성에 기댄 방식이다.
- 최적 (OPT): 앞으로 가장 늦게 쓸 페이지를 내보낸다. 미래를 알아야 해서 실제로는 불가능하지만, 다른 방식이 얼마나 잘하는지 비교하는 기준이 된다.
페이지 교체 대결
보호와 공유
페이지 테이블의 각 줄에는 주소만이 아니라 권한도 적혀 있다. “읽기만 가능”, “쓰기 가능”, “실행 가능”, “커널만 접근 가능” 같은 표시다. 프로그램이 권한 없는 페이지에 쓰려 하거나, 페이지 테이블에 아예 없는 주소를 건드리면 MMU가 즉시 막고 운영체제는 그 프로그램을 종료한다. 프로그램이 갑자기 꺼지며 나오는 “세그멘테이션 오류”, “메모리 접근 위반” 메시지가 바로 이것이다. 덕분에 버그가 있는 프로그램 하나가 다른 프로그램이나 운영체제를 망가뜨리지 못한다.
반대로 일부러 공유할 수도 있다. 여러 프로그램이 함께 쓰는 라이브러리 코드(예: 글꼴 그리기, 화면 창 관리)는 실제 메모리에 한 번만 올리고, 각 프로세스의 페이지 테이블이 같은 프레임을 가리키게 한다. 위 주소 변환기에서 메신저와 게임의 “공유 라이브러리” 페이지가 같은 프레임을 가리키는 것을 확인해 보자. 메모리를 크게 아낄 수 있다.
핵심 정리
- 프로그램은 실제 주소 대신 자기만의 가상 주소를 쓴다. 덕분에 서로 충돌하지 않고, 서로를 볼 수도 없다.
- 메모리를 페이지(보통 4 KB) 단위로 나누고, 페이지 테이블이 “가상 페이지 → 실제 프레임”을 대응시킨다. 오프셋은 그대로다.
- 변환은 CPU의 MMU가 하고, 최근 변환 결과는 TLB에 캐시한다.
- RAM에 없는 페이지를 건드리면 페이지 폴트가 나고, OS가 SSD에서 가져온다. 이 덕분에 RAM보다 큰 메모리를 쓸 수 있지만, 지나치면 스래싱이 생긴다.
- 프레임이 꽉 차면 FIFO, LRU 같은 규칙으로 내보낼 페이지를 고른다.
- 페이지 테이블의 권한 비트로 침범을 막고, 같은 프레임을 가리켜 라이브러리를 공유한다.
확인 퀴즈
페이지 크기가 1,000바이트이고 페이지 테이블에 “5번 페이지 → 2번 프레임”이 있다. 가상 주소 5072의 실제 주소는?
두 프로세스가 같은 가상 주소 1000번지를 써도 충돌하지 않는 이유는?
페이지 폴트가 일어났을 때 운영체제가 하는 일이 아닌 것은?
LRU 교체 방식이 내보내는 페이지는?