가상 메모리 — 모두에게 "네 것"이라고 속이다
02편에서 각 프로세스는 0번지부터 시작하는 자기만의 깨끗한 메모리가 있는 줄 안다고 했다. 그런데 물리 RAM은 하나이고, 수십 개 프로세스가 모두 0번지를 쓴다면 충돌해야 정상이다. 충돌이 안 나는 비결이 가상 메모리다 — OS가 각 프로세스의 가상 주소를 서로 다른 물리 주소로 몰래 번역해, 모두에게 "이 메모리는 통째로 네 것"이라고 속인다. 이 속임수가 프로세스 격리의 실체이자, 02·03편에서 컨텍스트 스위치가 비쌌던 진짜 이유다.
페이지와 프레임 — 메모리를 조각으로
OS는 메모리를 통째로가 아니라 고정 크기 조각으로 다룬다 — 가상 메모리의 조각을 페이지(page), 물리 메모리의 조각을 **프레임(frame)**이라 부른다(보통 4KB). 그리고 어느 페이지가 어느 프레임에 들어 있나를 페이지 테이블이 기록한다. 즉 프로세스마다 자기 페이지 테이블이 있어 — 같은 "5번 페이지"라도 프로세스 A는 100번 프레임, B는 300번 프레임으로 번역된다. 이게 02편 격리의 비밀이다.
매번 표를 뒤지면 느리다 — TLB
문제는 — 모든 메모리 접근마다 페이지 테이블을 뒤져야 한다는 것이다. 그런데 페이지 테이블도 메모리에 있어서, 번역 한 번에 메모리 접근이 또 필요하다(게다가 x86-64는 4단계 페이지 테이블이라 한 번 번역에 메모리를 네 번 본다). 그래서 하드웨어가 **TLB(Translation Lookaside Buffer)**라는 작은 초고속 캐시에 최근 번역 결과를 저장한다. CPU는 번역할 때마다 TLB를 먼저 보고 — 있으면(TLB 히트) 즉시, 없으면(TLB 미스) 페이지 테이블을 walk해 채운다. (출처: TLB.)
여기서 02·03편의 수수께끼가 풀린다 — 프로세스를 전환하면 페이지 테이블이 통째로 바뀌므로 TLB를 비워야(flush) 하고, 그러면 한동안 TLB 미스로 느려진다. 반면 같은 프로세스의 스레드끼리는 페이지 테이블이 같아 TLB를 안 비워도 되니 전환이 싸다 — 03편에서 본 그 차이의 하드웨어적 뿌리가 바로 이 TLB다.
필요할 때만 가져온다 — 페이지 폴트와 demand paging
가상 메모리의 또 다른 마법은 — 프로세스의 모든 페이지가 물리 메모리에 다 올라와 있을 필요가 없다는 것이다. 페이지 테이블 항목에 "이 페이지는 아직 메모리에 없음" 표시를 둬, 프로그램이 그 페이지에 처음 접근하면 — CPU가 **페이지 폴트(page fault)**를 일으키고, OS가 그때서야 디스크에서 그 페이지를 메모리로 가져온다. 이 "실제로 쓸 때까지 미루는" 게 **demand paging(요구 페이징)**이다(필요할 때까지 미루는 지연 평가의 발상이다). 덕분에 물리 메모리보다 큰 프로그램도 돌릴 수 있고(자주 쓰는 페이지만 올려), 기동도 빨라진다.
왜 이게 캐시인가 — TLB는 주소 번역의 캐시이고, 페이지 폴트는 메모리의 캐시 미스다 — 즉 가상 메모리도 *캐시 계층(빠른 곳↔느린 곳)*의 한 층이다. "가까운 빠른 곳에 자주 쓰는 것을, 멀고 느린 곳에 나머지를"라는 캐시 원리가 — TLB↔페이지 테이블, *RAM↔디스크(swap)*로 그대로 반복된다. 그리고 프로그램이 다루는 메모리 주소는 모두 가상 주소라, 실제 물리 위치는 OS가 페이지 테이블로 관리한다 — 그래서 OS가 페이지를 물리적으로 옮겨도 프로그램에는 가상 주소가 일관되게 보인다.
정리하면, 가상 메모리는 각 프로세스에게 "메모리 전체가 네 것"이라고 속이는 추상이다 — 메모리를 페이지/프레임 조각으로 나누고 페이지 테이블로 가상→물리 번역해, 서로 다른 프로세스를 같은 가상 주소로도 격리한다(02편 격리의 실체). 번역이 느리니 TLB로 캐시하고(프로세스 전환 시 flush가 비용의 뿌리), demand paging으로 필요한 페이지만 그때그때 디스크에서 가져온다. 그런데 물리 메모리는 유한해서 — 가득 차면 무엇을 내보내고, 너무 부족하면 무슨 일이 나는지가 다음 편, 메모리 관리다.