이전 챕터에서는, 메모리의 빈 공간을 어떻게 하면 효율적이고, 그리고 빠르게 관리할 수 있는지 알아보았습니다. 특히, 특정 요소의 최적화에 집중하게 되면 다른 쪽에서 추가적인 비용 또는 비효율이 발생하는 탓에, 여러가지 메모리 관리 기법이 등장하였음을 알아봤습니다. 이번에은, 여기서 조금 더 나아가 고정된 크기로 메모리를 나누고 이를 통해 보다 체계적이며 효율적으로 관리할 수 있는 '페이징(Paging)' 기법을 알아보겠습니다.
운영체제에서는, 앞에서 이야기한 메모리 공간 관리와 관련된 문제에서, 크게 두가지 방법으로 나뉘어 그 기법이 존재합니다. 하나는 이전에 배웠던, 가변 크기대로 분할해서 그때그때 필요한 크기에 맞춰, 알고리즘에 부합한 가변 크기 조각을 그대로 할당하거나 또는 다시 잘라 분할하는 등의 방식으로 이루어집니다. 하지만 이렇게 블록을 지속적으로 분할하는, 그리고 이것이 '가변'이어서 따로 초기의 크기 형태가 따로 정해진게 없다는 것은(물론 유독 많은 크기는 있겠지만요) 그 시스템 상, 필연적으로 단편화가 발생합니다. 그리고 나머지 하나의 방법은, 이런 가변 방식이 아닌 고정된 동일 크기대로 분할하는 기법이 존재합니다. 이것이 페이징 기법입니다. 고정 크기의 페이지(Page)로 나누어 보다 추적 또는 유지 등의 관리가 용이하고, 따라서 그 시스템이 유연하며 단순합니다.
페이징 기법 - 개요
보다 간단한 설명을 위해, 실제 메모리의 축소 버전 느낌으로, 128바이트 크기의 메모리를 예시로 설명하겠습니다. 먼저 페이징 기법은, 아래의 64바이트를 16바이트의 고정된 크기로 나눈 것처럼, 물리 메모리를 고정 크기로 등분하여 유지합니다.

특히 바로 다음의 그림처럼, 가상 주소 공간(Address Space)의 페이지들이 분산되어 메모리에 배치되며, OS까지도 최상단의 한 블록을 차지하는 것을 확인할 수 있습니다. 여기서 페이지 기법의 첫 장점을 확인할 수 있습니다. 바로 이 분산 배치에 대한 유연성입니다. 후술하겠지만 어떠한 추적 방식을 이용해서 가상주소만으로 실제 위치된 물리 주소를 추적할 수 있습니다. 이는 곧, 이전의 세분화 기법에서 가정을 통해 제한 했던, 힙 또는 스택의 증가의 제한을 받지 않습니다. 페이징 기법에서 힙이나 스택을 물리 메모리에 적재하게 되더라도, 결국 한 블록을 초과하게되면(또는 이에 근접하게되면) 추가적인 블록이 할당되고, 이는 '연속되지 않아도' 문제가 생기지 않습니다. 스택과 힙 자체는 연속적인 구조를 이루나, 실제 물리 주소만큼은 연속적이지 않게되는겁니다! 이러한 이유로 페이징 기법은 유연성 측면에서 굉장히 높은 장점을 보입니다. 약간의 비용이 들기는 하지만, 물리 메모리에 연속적으로 데이터가 존재하지 않아도 된다는 보장이 깔리니까요. 또한 이렇게 그림을 보면 알 수 있듯, 구현 자체는 난이도가 있을 지언정 개념과 시스템 자체는 굉장히 단순합니다. 어떠한 탐색 기법을 쓰거나, 최적의 블록을 찾는 방식이 아닌, 비어있는 블록 아무거나 찾아서 거기에 배치해주면 끝입니다. (물론 이제 모든 블록이 점유 중인 상태일 때, 어떤 블록을 탈락시켜 빈 공간을 만드느냐...의 규칙 같은건 별개지만)

페이지 테이블
그렇다면 이, '추적 방식'은 어떻게 존재할까요? 무엇을 사용하길래, 내 가상주소를 가지고 단순 최초 탐색된 랜덤 블록의 물리 주소를 단번에 파악할 수 있을까요? 어떤 정교한 알고리즘을 사용한다던가 그러는건 아쉽게도 아니고, 페이지 테이블(Page Table)이라는 자료 구조를 프로세스마다 유지합니다. 그리고 이 페이지 테이블에, 각 가상주소 공간의 페이지에 대한 주소 변환정보, 즉 물리 페이지 프레임 위치를 저장합니다. 위 그림 같은 경우에는 물리 페이지 3번 프레임이 가상페이지 0번, 물리 페이지 2번이 가상페이지 3번과 매핑되어 있습니다. 따라서 운영체제 입장에서는, 이 페이지 테이블을 통해 원하는 가상 페이지를 넣어 매핑된 물리 페이지를 간단하게 추적할 수 있습니다. 특히 우리는, 이 페이지 테이블이 '프로세스 마다' 유지됨을 상기하고 있어야합니다. 타 프로세스가 실행되게 된다면, 해당 프로세스에 맞는 페이지 테이블과 물리 페이지 프레임이 저장됩니다.
그리고 블록 추적까지 완료된다면, 실질적으로 우리가 찾고 싶어하는 단일 주소가 존재한다 해도 굉장히 쉽게 찾을 수 있습니다. 결국 이 페이지 단위로 끊어서 저장했으니, 이는 페이지 단위로 오프셋을 유지한다면, 가상 주소의 오프셋만큼 해당 매핑된 물리주소로부터 떨어진 거리에 우리가 찾고자 하는 데이터가 존재한다는 말이 됩니다. 예를 들어서, 위 그림을 예시로 이어가보겠습니다. 우리는 21바이트 째에 있는, 주소 공간의 데이터를 열어보고 싶습니다. 그렇다면 먼저 가상 주소를 형성합니다. 가상 주소는 가상페이지번호(Virtual Page Number, VPN)와 오프셋(offset)으로 나뉩니다. 이 예시의 경우 16바이트이니 오프셋은 4칸이면 충분하겠네요. 결국 아래의 그림처럼 1번페이지에서 오프셋 5(0101)만큼 떨어진(1페이지(=16) + 5 = 21) 주소를 뜻하는 VPN = 1 / offset = 0101의 주소가 생성됩니다. 그리고 우리는 이 VPN을 사용하여 실제 물리 주소를 추적합니다. VPN = 1이었고, 이 1이 저장된 위치는 페이지 프레임 7, 111입니다. 따라서 실제 물리주소는 이 111에 오프셋 5를 가상주소와 동일하게 이어붙여주면(1110101) 완성입니다. 최종적으로 우리는 물리주소 1110101을 산출해 낼 수 있게 됐습니다. 이렇게 굉장히 단순하고 효율적인 방법으로 주소 변환이 이루어짐을 확인할 수 있습니다.

그렇다면 한 번 더 나아가서, 이 페이지 테이블 자체는, 어디에 저장될까요? 매핑된 정보를 딕셔너리 형태를 저장하고, 그리고 이 크기는 메모리 크기에 비례할겁니다. 보통의 4KB 크기의 페이지는 12비트의 오프셋을 가집니다.(2^12 = 4096 = 4KB) 그리고 이를 32비트 주소 공간으로 대입한다면 나머지 20비트가 VPN을 위한 비트가 됩니다. 그렇다면 이는 운영체제가 관리해야할 규모 역시 2^20의 단위 라는 이야기가 됩니다. 심지어 이는 개수일 뿐입니다. 페이지 테이블에는 페이지 테이블 항목(Page Table Entry, PTE)이라는 필요 정보들이 저장되는데, 이들을 4바이트 크기 정도로만 가정해도 2^20 * 4 = 2^22 = 4MB라는 크기가 나옵니다. 요즘 세상에 4MB가 별거 아닌 것처럼 보일 수 있지만, 이 또한 상당히 큰겁니다. 우리는 위에서, 페이지 테이블은 '프로세스 마다' 유지 된다고 했습니다. 즉, 4MB가 다시 단위가 되어 그 개수만큼 곱해져 메모리를 점유하게 될 지도 모를 일입니다. (100개면 0.4GB입니다!) 이 이야기는 좀 더 깊게 다뤄보겠습니다. 아래가 페이지테이블까지 반영됐을 때의 메모리 모습입니다. 0번째 물리 프레임에 페이지 테이블이 적재됩니다.
우리는 페이지 테이블이 가상주소와 물리주소 매핑에 이용되는 자료구조임을 배웟습니다. 간단하게 생각하면 딕셔너리, 배열 형태입니다. VPN으로 PFN을 추적하고, 그 항목의 PTE를 검색하는 형식입니다. 그래서 이 PTE는 뭐냐, 여러 비트로 구성되어 정보를 유지하는 항목인데, 먼저 변환의 유효성을 판단하는(힙과 스택 사이 미사용 공간(invalid) 접근시 트랩(Trap)을 작동시키기 위한) Valid Bit, 페이지의 읽기나 쓰기, 실행 가능에 대한 권한을 유지하는 Protection Bit, 현재 물리 메모리에 존재하는지 또는 디스크로 스왑 아웃된 상태인지를 유지하는 Present Bit, 접근된 전적이 있는 페이지인지(오랫동안 접근되지 않은 페이지를 탐색할 때 사용합니다. 오래 안썼으니 계속 유지하고 있는건 비용낭비겠죠?) 조회할 수 있는 Reference Bit가 존재합니다.

아쉽게도, 단순한 페이징 기법은, 페이지 테이블의 크기를 매우 크게 증가시킵니다. 이는 단순한 메모리 낭비 뿐 아니라 처리 속도 저하로까지 영향을 끼칩니다. 아까 우리는 21을 물리 주소로 변환하여 1110101로 출력할 때, 그냥 알맞는 물리 프레임 찾아서 출력만 했습니다. 하지만 실제로는 총 세 단계를 거쳐 출력됩니다. 먼저 해당 프로세스의 페이지 테이블 기준 레지스터(PTBR)에 저장된 주소를 통해 조회한 페이지테이블에서 해당 VA에 대한 PTE를 읽어옵니다. 그런 뒤, 해당 PTE를 이용해 가상주소를 물리 주소로 변환시킵니다. 그리고 그 뒤에야, 변환된 물리 주소로 실제 메모리의 해당 주소에 있는 데이터를 읽어옵니다. 이게 뭐가 문제가 될까요? 당연한 것처럼 보이고, 별 이상이 없어보이지 않나요? 이 문제는, 페이지 테이블이 메모리에 위치해 있기 때문에 발생합니다!(사실 당연하죠. 저 거대한걸 레지스터에 담을 수 있는 것도 아니고...) 페이지 테이블에서 조회할 때 한번, 그리고 실제로 데이터를 읽을 때 한번, 주소 변환마다 메모리를 두번 거치는 비용이 발생합니다! 이는 굉장한 비효율을 야기합니다. 이를 해소할 방법은 없을까요?
Translation Lookaside Buffer(TLB)
메모리를 두번 거친다? 비용적이니 낭비가 있다? 제일 쉬운건 그냥 그 사이에 캐시를 하나 만들어버리는거죠. 이러한 문제를 해결하기 위해 우리의 컴퓨터에는 이미 TLB라는 이름의 하드웨어 캐시가 부착되어있습니다. 이름도 익숙합니다. 정글에서도 배운 친구이기도 하고, 이전 멜트다운에 대해 다룰 때 잠깐 등장한 캐시입니다. 그때도 이 TLB에 등록된 전적이 있냐 없냐에 따라 나타나는 실행 시간 차이로 커널 메모리 영역의 데이터를 탈취할 수 있다고 했었죠. 그렇듯 이 TLB는 CPU 내 MMU에 위치한 소형,고속 하드웨어 캐시입니다. 이 TLB에는 최근 사용된 가상 주소와 물리 주소 공간의 매핑 정보에 저장됩니다. TLB에 정보가 등록되어 있다면 이를 히트(Hit), 아닐 경우는 미스(Miss) 처리 하여, 히트 시 곧바로 물리주소를 얻어 메모리에 접근할 수 있게됩니다. 무엇보다 이는, 당연하게도 페이지 테이블을 조회하기 전에 일어납니다. 따라서 히트만 터지면, 메모리에 한번 접근하는 비용을 거의 상쇄할 수 있게 됩니다!

검색 과정에서 가상 주소가 제공되면, 해당 주소의 VPN을 추출하고 TLB에서 이 VPN에 해당하는 엔트리를 병렬적으로 검색합니다. 있는 경우는 즉시 hit 판정으로 qkfh PPN 변환 후 물리 주소 접근, 일치하는 엔트리가 존재하지 않을 경우 이를 Miss로 판단하여, 해당 주소를 TLB에 추가, 이후의 재 접근이 필요할 때 즉시 히트될 수 있도록 그 기반을 세팅합니다. 위 그림이 가장 이해가 쉬울 수 있습니다.
그렇다면 한번 더 나아가서, 이 TLB 미스는 어떻게 처리될까요? TLB 미스에 대한 처리는 크게 두가지, 하드웨어적인 관리 방법과 운영체제 측면의 관리 방법으로 나뉩니다. 하드웨어의 방식에는 주로 복잡한 명령어 집합인 CISC 구조에서 사용됩니다. 하드웨어 단위에서 TLB 미스를 처리하려면 해당 페이지 테이블 정보 유지가 필요합니다. 이 TLB 미스 발생 시, 하드웨어적 방법에서는, 해당 하드웨어가 페이지테이블에서 필요한 정보를 추출 후, TLB를 갱신시키고 다시 명령어를 실행시켜 종료합니다. 소프트웨어적 방식은, 시그널 발송 원리를 이요합니다. 가장 주로 사용되는 구조를 RISC 구조라고 부르는데, TLB 미스 발생시 하드웨어에서 예외 시그널을 발생, 이를 OS가 커널모드로 전환되어 트랩 핸들러를 동작시킵니다. 그리고 이 트랩 핸들러를 통해 페이지 테이블의 변환 정보를 조회, 이를 TLB에 갱신하고 위와 동일하게 명령어를 재실행시킵니다. 결국 이 미스 처리의 주체가 하드웨어냐(가장 하위 단위이기 때문에 오버해드가 매우 적습니다.), 운영체제냐(높은 유연성과 하드웨어 변화에 취햑하지 않습니다.)의 차이입니다.
TLB에는 일반적으로 총 5가지의 항목이 존재합니다. VPN과 물리 페이지 프레임 번호인 PPFN, 액세스 권한(읽기 쓰기 실행같은), 유효 비트, ASID(프로세스간 TLB 충돌 방지, Address Space Identifier) 가 포함됩니다. 그리고 프로세스마다 유지된다는 가상주소 특성 상, 콘텍스트 스위칭 발생 시, 현재 프로세스의 TLB 엔트리는 유효하지 않게 될 가능성이 존재합니다.
방금 TLB는 별도의 캐시를 만들어 배치했다 했습니다. 캐시입니다. 따라서 이 제한된 공간은, 정말 다시 재탐색될 수 있는 요소만 적재해야, 그리고 오랫동안 사용되지 않은 요소는 제거해야 그 비용적 효율이 가장 높을겁니다. TLB는 보통 다음과 같은 정책을 사용합니다. 최근에 사용하지 않은 항목을 먼저 교체하는 LRU(Least Recently Used), 최근 추가된 항목을 먼저 교체하는(FIFO) 마지막으로 교체 자체를 무작위로 선택하는 Random Replacement 가 존재합니다.
물리 메모리의 크기 극복
TLB의 크기 제한에 따른 교체 알고리즘을 봤겠다. 이번엔 메모리에선 이 교체가 어떻게 일어나는지 다루겠습니다. 먼저 물리 메모리에서는, 그 적재 가능 크기가 부족해 졌을 때, 디스크의 어떠한 특정 영역으로 현재의 값을 백업하고, 다시 여유가 될때 불러옵니다. 이 특정 영역을 스왑(SWAP) 공간 이라고 부릅니다. 이는 부족한 가상 메모리 공간 발생 시, 사용하지 않은 페이지의 백업으로 메모리 효율을 보다 높힙니다. 물론 디스크이기에, 속도는 메모리에 비해 현저히 느려 페이지 폴트 발생 시엔, 오히려 성능을 떨어뜨릴 위험이 있습니다.

페이지 폴트는, 프로세스가 메모리에 없을 때 사용자에게 알리는 예외 상황입니다. OS가 해당 페이지를 메모리에서 찾을 수 없어 페이지 폴트를 일으킨다면. 이는 다음과 같은 처리 과정으로 이루어집니다. OS는 해당 페이지를 스왑공간에서 메모리로 로드시킵니다. PT 업데이트 후, Present bit를 1로도 설정했습니다. 그런 뒤, 프로세스가 다시 해당 페이지를 접근 가능하도록 풀어주는 과정을 거칩니다. 무조건적인 페이지 폴트도 좋은 방법은 아닙니다. 매 페이지 폴트는 매 메모리 접근 지연을 야기합니다. 이는 결국 성능저하로 이어집니다. 따라서 필요한 만큼만 페이지 폴트가 일어날 수 있어야 하며, 그렇기에 위의 LRU나 Modified Page Count와 같은 여러 알고리즘이 연구되거나 사용되고 있습니다.
'운영체제 OStudy' 카테고리의 다른 글
| [OStudy] 9주차 - 병행성(1) (0) | 2025.11.06 |
|---|---|
| [OStudy] 7주차 - 메모리 가상화(3) (0) | 2025.10.21 |
| [OStudy] 6주차 - 메모리 가상화(2) (0) | 2025.10.13 |
| [OStudy] 5주차 - 메모리 가상화(1) (0) | 2025.09.29 |
| [OStudy] 4주차 - 스케줄링(2) (1) | 2025.09.22 |