18. 페이징 소개 :
OS가 대부분의 공간 관리 문제를 해결하고자 할 때 두 가지 중에 하나의 방식을 사용한다. 첫 번째는 어떤 작업을 가변적인 사이즈의 조각들로 잘라내는 것이다.(가상 메모리에서 세그멘테이션과 같이) 근데 이 방법은 단점이 있는데, 공간을 서로 다른 사이즈의 청크들로 채우다 보니, 공간이 점차 단편화되고, 공간 할당이 점점 더 힘들어진다.
두 번째 접근방식은 페이징인데, 공간을 고정 사이즈 조각으로 나누는 것이다. 가상 메모리에선 이를 페이징이라고 부른다. 힙, 스택처럼 가변적인 논리적 세그먼트로 나누는 것이 아니고, 페이지라 불리는 고정 사이즈 유닛으로 나눈다. 사실 OS는 물리 메모리를 페이지 프레임이라고 부르는 고정 사이즈 슬롯(말 그대로 페이지를 위한 틀이다. 붕어빵의 틀처럼)의 어레이로 바라본다. 각 페이지 프레임은 단일 가상-메모리 페이지를 포함한다.
요점 :
어떻게 페이지로 메모리를 가상화할까? - 세그멘테이션이 가진 문제들을 페이지 테크닉은 어떻게 피하는가? 어떻게 최소한의 시공간의 오버헤드를 가지면서 이러한 테크닉을 잘 수행할까?
간단한 예와 오버뷰 :
직관적인 이해를 위해 전체 64 bytes 크기에 4개의 16-byte 페이지가 있다 하자(페이지 번호는 0, 1, 2, 3). 물론 실제 메모리는 훨씬 크다.(흔히 32 비트 짜리면 4-GB의 주소 공간, 만약 64비트로 주소를 매핑한다면 2^64 = 16-Exabyte의 공간까지 처리할 수 있다.)
그리고 이를 고정 사이즈 슬롯인 8개의 페이지 프레임을 가진 128-byte짜리 물리 메모리에 올린다고 하자. 이러면 우선 flexibility의 이점을 가진다. 효율적으로 주소 공간을 추상화한다. 추상화라는 게 뭔 소리냐면, 프로세스가 주소 공간을 어떻게 사용하든 무관하게 페이지 접근을 할 수 있다는 뜻이다.(물론 여기서 가변적으로 변하는 힙이나 스택은 잠깐 놔두고 생각하자) 다음은 페이징 여유가 있는 free-space 관리의 simplicity를 가진다. 즉, OS가 위 64-byte 주소 공간을 갖고 싶은데, 그저 4개의 빈 페이지를 고르기만 하면 된다. 아마도 OS는 free list를 어딘가 들고 있을 것이다. 그냥 그 리스트 중에 4개를 가져오면 된다. 그림처럼 편하게 대충 잡고 주소 공간을 물리 메모리에 할당했다.
주소 공간의 각 가상 페이지가 실제 물리 메모리에 어디에 있는지 적어두기 위해 OS는 페이지 테이블이라 불리는 프로세스별 데이터 구조체를 들고 있다. 페이지 테이블의 주역할은 주소 공간의 각 가상 페이지를 위한 주소 변환 정보를 저장해두는 것인데, 그래서 우리가 각 페이지가 물리 메모리 안에 어디에 있는지 알 수 있다. 대략 페이지 테이블엔 이런 식으로 4개의 주소 변환 정보가 적혀 있을 것이다. (가상 페이지 0 -> 물리 프레임 3 )
페이지 테이블이 프로세스별 구조체라는 것을 기억하는 게 중요하다.(물론 대부분의 페이지 테이블은 프로세스별 구조체긴 하다. inverted page table만 예외다.) 만약 다른 프로세스가 위 메모리에서 또 run 하고 싶다면, 그를 위한 별도의 페이지 테이블이 필요하다. 방금 말했다시피 프로세스별 구조체이기 때문이다.
이제 우리가 방금 메모리에 올린 64바이트짜리 프로세스가 메모리 접근을 수행한다고 하자. 가상 주소로부터 레지스터로 데이터의 load가 발생하고 있다.(명령어 FETCH는 이전에 이미 했다고 치고) 이 프로세스가 생성한 가상 주소를 번역하려면, 먼저 이를 가상 페이지 번호(VPN)와 페이지 오프셋으로 나눈다. 예를 들어, 프로세스의 가상 주소 공간이 64-byte니까 6비트가 필요하다. 그런데 페이지 사이즈가 16-byte니까 윗 2비트는 VPN으로 주고(0, 1, 2, 3번 페이지), 나머지 뒷단 4비트는 오프셋으로 준다.(0~15의 페이지 내 오프셋)
프로세스가 가상 주소를 던져주면, OS와 하드웨어가 함께 이를 유의미한 물리 주소로 번역한다. 예를 들어, 가상 주소 "010101"로부터 Load가 발생했다고 생각하자. 즉, 이 가상 주소는 가상 페이지의 1번 중 5번째 바이트를 의미한다. 이제 페이지 테이블을 보고 가상 페이지 1번이 물리 프레임 번호(PFN) 또는 물리 페이지 번호(PPN) 7번이라는 것을 찾는다. 이때 물리 메모리는 프레임이 8개라 프레임을 구분하기 위한 비트는 3개고 7번 물리 페이지는 111로 번역된다. 따라서 우리는 "1110101"이라는 물리 주소를 찾을 수 있다.
이제 기본 오버뷰는 습득했고, 몇 가지가 궁금해지지 않았는가? 이런 페이지 테이블은 어디 저장하는지? 페이지 테이블의 typical 한내용은 무엇이고, 테이블은 얼마나 큰지? 페이징은 시스템을 느리게 할까?
먼저 어디에 페이지 표가 저장될까?부터 알아보자. 페이지 테이블은 이전에 공부했던 세그먼트 표나 base/bound pair 보다 훨씬 끔찍하게 커질지도 모른다. 예를 들어서 전형적인 32-비트 주소 공간에 4KB 사이즈 페이지들을 표현한다고 생각해보자. 그러면 주소 공간은 20비트의 가상 페이지 번호(VPN)와 12비트의 페이지 오프셋으로 나눌 것이다.(12비트로 4KB의 용량 표현이 가능하기 때문에) 그러면 페이지 번호는 20비트니까 대략 100만 개 페이지까지 관리 가능하다는 뜻이다.
페이지 테이블 엔트리에는 주소 번역 정보와 기타 유용한 정보를 기록할 텐데 엔트리 당 4바이트가 요구된다고 가정하자. 그러면 페이지 테이블당 4MB의 용량이 필요하다는 뜻이다! 컴퓨터가 100개의 프로세스를 올려두고 있는 상황이라면, 겨우 주소 변환 하나 하자고 400MB의 메모리를 운영체제가 요구해야 하는 상황이다!(다시 말하지만 페이지 테이블은 프로세스별 구조체이다.)
현대 기준으로 생각해봐도, 많은 컴퓨팅 머신들은 대략 수~수십 기가바이트급의 메모리를 갖추고 있다. 그중에서 방금처럼 수백 메가의 메모리를 주소 변환을 위해 따로 쓰고 있다는 것은 다소 부담스럽게 들린다. 만약에 64-비트 주소 공간 체계가 되면 상황은 더욱 끔찍해질 것이다.
일단 지금은 페이지 테이블이 OS가 관리하는 물리 메모리에 올라가 있다고 가정하자. 이후에 우리는 대부분의 OS 메모리가 가상화되어 페이지 테이블이 OS 가상 메모리 안에 저장될 수 있음을 보일 것이다.(심지어 스토리지 쪽에 스왑 되어 있다.)
다음으로, 페이지 테이블 안에는 실제로 무엇을 적는가? 에 대해 알아보자. 계속 말해왔듯이, 페이지 테이블이란 그저 가상 페이지를 물리 프레임에 매핑하는 구조체이다. 그래서 가장 간단한 폼은 linear page table로, 그저 OS가 가상 페이지 번호를 어레이에 인덱싱 해두고 우리가 찾고 싶은 물리 프레임 번호를 찾기 위해 페이지 테이블 엔트리를 뒤지는 구조다. 다음에 우리는 페이징 할 때 발생하는 몇 가지 문제들을 해결할 수 있는 좀 더 복잡한 구조에 대해서도 알아볼 것이다.
각 PTE(페이지 테이블 엔트리) 당 valid bit가 있는데, 이는 흔히 특정 변환이 valid 한 지 여부를 알려준다. 예를 들어서, 프로그램이 돌기 시작하면, 한쪽 끝에서는 코드, 데이터를 지나 힙 영역이 동적 할당되기 시작한다. 반대 끝에는 컴파일 타임에 할당된 스택이 쌓여 있다. 이 둘 사이에 모든 사용되지 않은 공간은 invalid로 마킹해 둘 것이다. 만약에 프로세스가 이런 메모리에 접근을 시도하면 트랩이 발생되어 OS가 개입해서 프로세스를 종료시켜 버릴 것이다.
그러니까 valid bit는 이런 희소한 주소 공간 구조에 중요한 개념이다. 사용되지 않은 페이지들을 invalid로 표시함으로써, 아직 사용되지 않은 주소 공간을 전부 할당할 필요가 없으며 엄청난 메모리 양을 아낄 수 있는 효과가 있다.
페이지 테이블은 이와 유사하게 protection 비트를 가질 수도 있다. 해당 페이지에 읽기, 쓰기, 실행 권한을 구분하는 것이다. 마찬가지로 허용되지 않은 페이지의 접근은 트랩을 발생시켜 OS가 출동해서 해당 프로세스를 종료시켜 버린다.
Present 비트는 현재 페이지가 물리 메모리에 있는지 아니면 스토리지에 있는지(즉, 이 페이지가 swapped out 되었는지) 알려준다. 추후에 물리 메모리보다 큰 주소 공간을 서포트하기 위해 주소 공간을 스토리지로 어떻게 스왑 하는지 공부해볼 것이다. Swapping은 거의 사용되지 않는 페이지들을 스토리지에 옮겨둠으로써 물리 메모리의 제약으로부터 벗어나게 해 준다.
Dirty 비트는 메모리로 가져온 이후로 해당 페이지가 수정되었는지 알려준다. Reference 비트(또는 Accessed 비트)는 이 페이지가 그동안 접근되었는지의 여부를 알려준다. 이는 어느 페이지가 인기가 있는지(참조가 자주 되었다는 뜻이겠죠?), 따라서 메모리에 계속 있어야 하는지 여부를 결정할 때 유용한데, 이러한 정보는 페이지 교체 시에 중요하다.
x86 아키텍처의 PTE(페이지 테이블 엔트리) 구조는 낮은 비트 순으로 설명하자면, 먼저 위에서 설명한 present 비트(P)가 있고, 읽기 전용인지 쓰기가 가능한지 구분하는 R/W 비트, 유저 모드에서도 페이지 접근이 가능한지 또는 관리자 모드에서만 접근이 가능한지 결정하는 U/S 비트가 있다. PWT, PCD, PAT, G 비트 등은 어떻게 하드웨어 캐싱을 하는지를 결정한다. A와 D는 각각 둘 다 위에서 이미 설명한 Accessed, Dirty 비트다. 마지막으로, 페이지 프레임 번호(PFN)가 있다.
문제점 :
페이징은 메모리를 많이 잡아먹는 데다가, 심지어 컴퓨팅을 매우 느리게 만들 수도 있다. 메모리에 있는 페이지 테이블이 차지한 용량이 너무 커질 수도 있다는(현대 메모리 용량을 생각해도 무시할 수 없는 수준) 우려를 앞서 이야기했다. 그런데 심지어, 페이지 테이블이 컴퓨팅 수행을 느리게까지 할 수 있다는 문제가 있다. 간단한 다음 명령어를 보자.
movl 010101, % eax
참고로, eax란 Extended Accumulator Register로, 주로 산술 논리 연산의 반환 값을 적어두는 레지스터이다. 자, 아까 했던 것을 그대로 이용해서, 해당 명령어를 Fetch 해야 한다. 우리가 알고 싶은 물리 메모리를 찾기 위해서, 가상 주소(21)를 보고 메모리 안에 있는 페이지 테이블을 뒤져서 실제 물리 주소(117)로 번역해 찾아온다. 그러고 나서야 우리는 찾아온 물리 주소로 다시 가서 우리가 원하는 데이터를 Load 해올 수 있다.
그런데 여기서 궁금한 점. 명령어를 처리하기 위해 필요한 데이터가 물리 메모리에 어디 있는지 찾아오려고 페이지 테이블로 가서 가상 주소를 물리 주소로 번역한 건데, 그럼 페이지 테이블은 어떻게 찾아간 것일까? 우리가 원하는 PTE가 메모리 상에 어디 있는지는 누가 알려주는 걸까? 사실 page-table base register가 페이지 테이블의 시작 위치에 해당하는 물리 주소를 들고 있다. 이제 그 레지스터를 보고 페이지 테이블 시작 주소를 찾은 뒤, 그 값에다 VPN * sizeof(PTE)를 더해주면 우리가 가고 싶은 PTE 주소를 구할 수 있다. 아래 코드를 같이 보자.
VPN = (VirtualAddress & VPN_MASK) >> SHIFT
PTEAddr = PageTableBaseRegister + (VPN * sizeof(PTE))
VPN_MASK는 따로 어려운 개념은 전혀 아니고, 그냥 가상 주소 중에 앞에 VPN에 해당하는 비트가 1이고, 나머지는 0인 값이어서, 비트 AND를 취하고 Shift를 취하면 가상 페이지 번호를 정수 값으로 받아낼 수 있다. 여기서 마스크 값은 이진수로 "110000"에 해당한다. 그리고 PTE 사이즈를 위 설명한 그림처럼 32 비트라고 한다면, page-table base register에서 현재 프로세스의 페이지 테이블 시작 주소를 읽어온 뒤 그 값에다 32비트 * VPN만큼 더해주면 된다. 우리의 예제에서는 가상 주소가 "010101"이므로, 우리가 가고 싶은 곳은 페이지 0, 1, 2, 3중 두 번째인 페이지 1이다. 페이지 테이블이 가상 페이지 번호 순서대로 인덱싱 되어 있다고 치면, 페이지 테이블 시작 주소에서 32비트 * 1 = 32비트만큼 더한 만큼의 메모리 위치로 가면 맞을 것이다.
일단 이 물리 주소(우리가 찾고 싶은 페이지 테이블 엔트리가 메모리에 어디에 있는지 알려주는 주소)를 찾으면, 하드웨어는 메모리로부터 PTE를 Fetch 해올 수 있다. 그리고 거기에서 물리 프레임 번호를 추출한 뒤 가상 주소의 오프셋 비트와 concatenate 해서(가상 주소나 물리 주소나 오프셋 값은 똑같다.) 우리가 원래 알고 싶었던 실제 물리 주소를 찾아낸다. 드디어, 하드웨어는 원래 원했던 데이터를 메모리로부터 인출해서 eax에 담아 둔다. 이제야 프로그램이 값 하나 로딩을 성공한 셈이다!
요약하자면 다음과 같다. 가상 주소로부터 가상 페이지 번호 추출 -> 해당 페이지 테이블 엔트리의 실제 주소 찾기 -> PTE Fetch(메모리 접근) -> 접근해서 이제 이 페이지 써도 되는지 확인받기, Valid 한가? 아니라면 하드웨어는 세그멘테이션 폴트 예외를 발생시킨다. 통과했다면 다음으로 Access 가능한가? 아니라면 프로텍션 폴트 예외를 발생시킨다. 해당 예외 처리들은 OS가 출동해서 해당 프로세스를 종료시킨다. 둘 다 통과했다면 이제 해당 페이지 엔트리에서 원래 알고 싶었던 물리 프레임 번호를 찾아온다. -> 원래 들고 있던 가상 주소의 오프셋과 결합해서 물리 메모리 주소를 완성한다. -> 원래 하고 싶었던 명령어의 데이터 인출(메모리 접근)
그러니까 매번 메모리 참조를 할 때마다 페이징은 추가 메모리 참조를 요구한다. 일이 많아지게 만드는 셈이다. 이러한 추가 작업 때문에 프로세스는 두배 또는 그 이상 느려질 수 있다. 하드웨어와 소프트웨어의 디자인을 신중하게 고려하지 않는다면 페이지 테이블은 시스템을 아주 귀찮게(!) 만들 수 있다.
지나가는 글: 데이터 구조체 - 페이지 테이블
현대 운영체제 메모리 관리 시스템에서 페이지 테이블은 가장 중요한 구조체 중 하나라고 볼 수 있을 겁니다. 일반적으로 페이지 테이블엔 가상 주소에서 물리 주소로의 번역 정보를 저장하고 있기 때문에, 프로세스 상의 각 페이지가 실제 물리 메모리에 어디에 존재하는지 시스템에게 알려줍니다. 시스템에서 각각의 프로세스들은 이런 번역들이 꼭 필요하기 때문에, 페이지 테이블은 프로세스별로 존재하게 되지요. 정확한 페이지 테이블의 구조는 하드웨어 시스템의 종류에 따라 결정되나, 운영체제에 의해 좀 더 유연하게 관리될 수도 있습니다.
마치기 전에, 페이징을 사용할 때 일어나는 메모리 접근 결과를 살펴보기 위해 쉬운 예제를 하나 추적해보자.
int array [1000];
...
for (i = 0; i < 1000; i++)
array [i] = 0;
다음과 같은 C언어 파일을 컴파일하고 실행한다고 생각해보자.
prompt> gcc -o array array.c -Wall -O
prompt>./array
물론, 제대로 공부하고 싶다면 어셈블리 언어로 봐야 한다. 여기서는 내가 친절히 어셈블리 어로 번역해서 보여주겠다.
1024 movl $0x0, (% edi,% eax,4)
1028 incl % eax
1032 cmpl $0x03e8,%eax
1036 jne 0x1024
당신이 x86 ISA를 좀 안다면, 매우 쉽게 이해할 수 있을 것이다. 첫 번째 명령어의 의미는 0(zero value)을 해당 어레이 위치의 가상 메모리 주소로 옮겨오라는 뜻이다. 그 주소는 % edi + % eax * 4를 통해 구할 수 있다. 참고로, eax는 위에서 이미 설명한 Extended Accumulator Register이고, edi는 Extended Destination Index Register로 주로 메모리로 가야 하는 목적지의 주소를 적는 용도로 쓰인다. 쉽게 눈치챘겠지만, edi에는 array [] 배열의 시작 주소를 가리키고, eax는 array [] 배열 내의 몇 번째 값인지, 즉 인덱스 값을 가리키고 있다. 4를 곱하는 이유는 해당 배열의 자료형이 integer 배열이기 때문에 각 사이즈가 4바이트이기 때문이다.
두 번째 명령어는 인덱스 값을 증가시킨다. 세 번째 명령어는 10진수로 1000에 해당하는 값과 인덱스를 비교한다. 만약 두 값이 같지 않다면, 네 번째 명령어가 묘사하는 것과 같이 프로그램 카운터를 1024로 되돌린다.
이 명령어 시퀀스가 만들어내는 메모리 접근이 어디에서 발생하는지 이해하려면(가상이든 물리 차원이든) 코드 스니펫과 배열이 가상 메모리 중 어디에서 발견되는지, 페이지 테이블의 내용과 그 위치는 어디에 있는지 몇 가지 가정이 필요하다.
예를 들어, 가상 주소 공간을 64KB로 잡자.(물론 현실적으로 매우 작은 크기다.) 페이지 크기는 1KB로 잡자. 이제 우리가 알아야 할 것은 페이지 테이블의 내용과, 그것의 물리 메모리 위치다. 자, 현재 우리는 array-based linear page table을 가지고 있고 이것은 물리 주소 1KB에(1024번지에) 위치한다고 치자.
사실 이 예제에서 우리가 매핑에 대해 신경 써야 할 가상 페이지는 정말 몇 개 밖에 안된다. 우선 해당 코드에서 활성화된 가상 페이지를 생각해보자, 페이지 사이즈가 1KB이기 때문에, 가상 주소 1024는 가상 주소 공간의 두 번째 페이지의 시작점이라고 볼 수 있다.(두 번째 페이지의 VPN는 1이다. 물론 VPN=0은 첫 번째 페이지 번호가 될 것이다.) 이제 이것을 물리 프레임 4번에 매핑한다고 해보자.(즉, VPN 1 -> PFN 4)
다음으로 array []에 대해 생각해보자. integer 1000개짜리 배열이니까 총사이즈는 4000 bytes고, 계산하기 쉽게, 가상 주소로 40000번지부터 43999번지까지 바이트 단위로 주소 공간을 차지하고 있다고 치자.(즉, array [0] = 40000~40003, array [1] = 40004 ~ 40007 이런 식) 엄밀하게 말하면 39번 가상 프레임은 39936번지에서 시작하니까 39번 가상 프레임에 오프셋 64부터 시작한다고 말해야 정확하다. 이를 대략적으로 다음과 같이 매핑했다고 볼 수 있겠다. VPN 39 -> PFN 7, VPN 40 -> PFN 8, VPN 41 -> PFN 9, VPN 42 -> PFN 10, 다시 한번 말하지만, 40000번지는 39번 프레임의 오프셋 64에 해당하는 주소다.(39*1024 + 64 = 40000이기 때문, 따라서 물리 프레임도 이에 맞춰 7*1024 + 64 = 7232번부터 4000개 정보를 차곡차곡 저장해둔 셈이다. 뒤에 나오는 그림에서 물리 주소를 따지기 위해서 중요하니 확실히 이해하고 넘어가자.)
자, 이 정도면 이제 프로그램의 메모리 참조를 추적할 준비가 되었다고 할 수 있겠다. 이 프로그램이 실행되면, 각 명령어 fetch는 두 번씩 메모리 참조를 필요로 한다. 명령어가 물리 프레임 안에 어디 있는지를 찾기 위해 페이지 테이블에서 한 번, 그리고 CPU가 이제 진짜 수행하고 싶은 명령어 자체를 처리하기 위해 fetch 할 때 다시 한번 필요한 것이다. 추가적으로, mov 종류의 명령어들은 외부 메모리 참조가 또 필요한데, 이는 배열의 가상 주소를 올바른 물리 주소로 번역하기 위해 또 다른 페이지 테이블 접근이 추가되고, 그 이후에 그 배열 접근을 할 수 있는 것이다.
아래 그림은 예제 프로그램의 5회 루프 동안 발생한 메모리 접근을 트레이스 한 것이다.
가장 아래 그래프는 명령어 메모리 참조를 보여주는데, 왼쪽은 가상 주소, 오른쪽은 물리 주소를 나타낸다. 가상 주소 입장에서 1024번지에서 mov 발생, mov 특성상 추가 메모리 접근이 필요해서 시간이 약간 걸리는 모습이다. 그리고 1028번지에서 inc, 1032번지에서 cmp, 1036번지에서 jne -> 1024번지로 점프를 반복하고 있다. 물리 메모리는 아까 위에서 물리 프레임 4번으로 갔다고 했으니까 물리 주소 4096번지 -> 4100번지 -> 4104번지 -> 4108번지 -> 다시 4096번지로 루프를 도는 모습이다.
중간에 위치한 그래프는 배열에 접근하는 모습이다. 가상 메모리는 처음 40000번지에 접근하고, 다음 루프마다 4만큼 더해서 40004, 40008, 40012, 40016 순으로 접근한다. 물리 메모리는 물리 프레임 7번의 오프셋 64에 해당하는 7232번지부터 시작해서, 7236, 7240, 7244, 7248 순으로 접근한다.
가장 위에 그려진 그래프는 명령어 페이지 테이블의 물리 메모리 접근만을 표시한 것인데, 페이지 테이블의 시작 주소는 물리 메모리에서 1024번지에 해당하고, PTE당 32비트, 즉 4바이트로 이루어져 있으니까 VPN 1을 읽는다는 것은 물리 메모리 1028번지를 읽는다는 것이다. 1028번지를 읽고 -> PFN 4로 번역해주는 것이다. 그다음은 mov 자체가 필요로 하는 추가 참조인데, array [0]에 접근해야 하기 때문에 페이지 테이블의 39번째 엔트리를 보고 array가 어디 있는지 찾아가야 한다. 명령어 페이지 테이블의 39번째 엔트리 주소는 1024 + 39*4 = 1180번지로 가서, PFN 7번으로 번역해서 array를 찾아갈 것이다. 즉, mov 명령어는 페이지 테이블을 두 번 본다. 뒤의 명령어들의 처리의 경우 마찬가지로, 페이지 테이블의 1번 엔트리(두 번째 엔트리)인 1028번을 보고 -> PFN 4로 번역해서 명령어들을 다시 읽으러 가는 것이다.(물론 명령어들의 오프셋은 다 다르다.)
자, 그림에서도 보이겠지만 4개의 명령어로 이루어진 루프 한 번당 메모리 접근을 10번 한다. 그래프 위에서 아래 방향으로, 명령어 페이지 테이블을 보기 위해 5번(mov 2번, 나머지 1번씩), array가 물리 메모리에 실제 위치한 곳으로 찾아가기 위해 1번, 실제 명령어 프로그램이 적힌 물리 주소로 fetch를 위해 4번이다.
요약하자면, 이번 시간에 우리는 메모리 가상화의 애로사항을 해결하기 위해 페이징이라는 개념을 소개했다. 페이징은 이전에 공부했던 세그멘테이션과 비교해서 몇 가지 우수한 점이 있다.
첫째, 메모리를 고정 사이즈의 유닛으로 잘라내기 때문에 외부 단편화(external fragmentation)를 일으키지 않는다.(대신에 세그멘테이션과 반대로 내부 단편화를 일으키지만,,, 이것에 관해서는 나중에 이야기하자.) 외부 단편화란 총 빈 공간은 분명히 충분한데 할당된 조각조각들이 자리를 차지하고 있어서 프로그램이나 기타 필요한 데이터를 메모리에 실을 수 없는 현상을 말한다.
둘째, 페이징은 꽤나 유연해서, 희소한 가상 주소 공간을 효율적으로 사용할 수 있게 해 준다. 흔히 많은 교육 자료에서 쓰이는 표현을 빌리자면, 프로그램 입장에서 실제 물리 메모리보다 훨씬 큰 메모리로 보게 해주는 효과가 있다.
그러나, 페이징 구현을 섬세하게 신경 쓰지 않는다면 머신을 느리게 만들 수도 있다.(페이지 테이블을 보기 위해 추가적인 메모리 접근을 많이 요구하니까) 또한 메모리를 많이 낭비시킬 수도 있다.(원래였더라면 애플리케이션 데이터를 더 올릴 수 있었을 자리를 페이지 테이블들이 잔뜩 차지하게 된다면,,,) 그러니까 우리 아키텍처 연구자들에게 있어 중요한 것은 페이징 시스템을 그냥 동작 구현만 할게 아니라, '잘' 동작할 수 있도록 좀 더 신경 쓰고 고민할 필요가 있는 것이다.
그러니까 다음 두 챕터에서는 어떻게 그렇게 할 수 있을지에 관해 같이 고민해볼 것이다.
출처 : REMZI H. ARPACI-DUSSEAU, ANDREA C. ARPACI-DUSSEAU, UNIVERSITY OF WISCONSIN–MADISON, "OPERATING SYSTEMS THREE EASY PIECES", Arpaci-Dusseau Books.
'OS' 카테고리의 다른 글
[운영체제] 20. Paging: Smaller Tables (0) | 2022.01.25 |
---|---|
[운영체제] 19. TLB란 (0) | 2022.01.24 |