Music for reading(spotify)

My Little Pony's trixie say 'real programmer use butterfly'

메모리 관리 기법

OS가 해야 하는 일 중 정말 중요한 일들이 몇 개가 있다.1 그 중 하나는 메모리 관리 (Memeory Management) 인데, 이는 운영 모드에 깊이 관련이 되어 있다.

자세한 내용에 들어가기에 앞서 메모리 관리에서 중요한 내용 중 하나인 페이징 (Paging)세그멘테이션 (Segmentation) 에 대해서 짚고 넘어가자.

페이징과 세그멘테이션

Paging and segmentation comparison chart
(그림 1. Paging and segmentation comparison chart)

이 문서를 정리하면서 많은 자료를 찾아봤는데, 그 중 몇몇 블로그 글들은 ‘페이징과 세그멘테이션은 가상 메모리에서 사용하는 기법이다. 가상 메모리는 메모리를 하드에 저장하는 기법이다.’ 라는 표현을 사용한 것을 찾아내었다. 기본적으로 이는 스왑 메모리가상 메모리 (혹은 선형 어드레스) 에 대한 지식이 미흡한 상태로 작성한 글이라는 생각이 들었다. 아마 윈도우에서 ‘가상 메모리’라는 이름으로 스왑 메모리를 사용하기 때문이 아닐까 생각한다.

이 글에서는 이 부분을 확실하게 하고 넘어가겠다. 가상 메모리 는 스왑 메모리와 물리 메모리의 영역을 전부 합쳐 가상의 메모리 영역으로 지정하여, 가상의 주소를 부여하고 사용하는 방법이다. Virtual Memory, Linear Memory 등으로 불리우며, 이 메모리의 주소는 Virtual Address, Linear Address 등으로 불리운다.

amd64와 IA-32e 아키텍처 공식 문서에서는 Virtual Memory, Linear Address 라는 표현을 혼용하여 사용하고 있기 때문에, 이 문서에서도 혼용하여 사용할 것이다.

페이징

페이징은 메모리 관리 방법 중 하나로써, 프로세스가 연속되지 않은 방식 으로 메모리에 저장되게 하는 방법이다. 프로세스를 연속되지 않은 방식으로 저장할 경우 외부 파편화 2 문제를 해결할 수 있지만, 내부 파편화 3 문제는 해결할 수 없다.

페이징을 구현하기 위해서는 물리/논리 메모리 공간 이 같은 크기의 사이즈 블럭으로 분리되어 있어야 한다. 이 고정된 사이즈로 나뉘어진 블럭을 프레임 (Frame) 이라 부르고, 이 프레임들이 모인 논리 메모리를 페이지 (Page) 라 한다.

세그멘테이션

세그멘테이션도 또한 메모리 관리 방법 중 하나로써, 프로세스를 서로 다른 크기의 논리적 단위인 세그먼트 (Segment) 로 분할한다. 세그멘테이션을 사용하면 내부 파편화 문제는 해결할 수 있지만, 외부 파편화 문제는 해결할 수 없다.

세그멘테이션을 구현하기 위해서는 논리 메모리와 물리 메모리가 같은 크기로 구성되어 있을 필요는 없다.

각 모드에 따른 메모리 관리 방식

리얼 모드

리얼 모드는 최대 1MB까지 주소 공간을 사용하며 세그멘테이션만을 지원한다. 리얼 모드의 세그먼트 크기는 64KB로 고정이며, 세그먼트의 시작 어드레스는 세그먼트 레지스터에 직접 설정한다. 세그먼테이션에서 세그먼트의 시작 어드레스는 코드나 데이터 등에 접근 할 때 기준 어드레스로 사용된다.

Six segments
(그림 2. 세그먼트 레지스터와 물리 주소의 관계)

리얼 모드에서는 페이징이 존재하지 않기 때문에, 물리 주소로 변환하는 과정이 간단하다. 세그먼테이션을 거쳐서 나온 주소가 곧 물리 주소가 되기 때문이다. 리얼 모드의 세그먼테이션은 세그먼트 레지스터의 값에 범용 레지스터의 오프셋 값을 더하는 것으로 동작한다. 세그먼트 레지스터의 값에 16(2^4)을 곱한 값을 세그먼트의 기준 주소로 사용하고, GPR의 오프셋 값을 더하면 약 1MB까지의 메모리를 사용할 수 있다.

보호 모드

보호 모드는 리얼 모드와는 다르게 세그먼테이션과 페이징을 모두 지원한다. 보호 모드의 세그먼테이션은 리얼 모드의 세그먼테이션보다 훨씬 많은 기능을 제공하고 있고, 세그먼트 레지스터에 세그먼트의 기준 주소를 직접 설정하는 것이 아니라, 디스크립터4 자료구조의 오프셋을 설정하는 방식이다. 보호 모드부터는 세그먼트 레지스터라는 이름이 아닌, 세그먼트 디스크립터를 가리킨다(선택한다)는 의미에서 세그먼트 셀렉터 (Segment Selector) 로 바뀐다.

세그먼트 디스크립터는 각각의 세그먼트들에 대해 정의하고, 보호하고, 격리시키는 역할을 한다.

Legacy Generic Segment Descriptor
(그림 3. 범용 세그먼트 디스크립터 - 레거시 모드)

[그림 3] 을 참조하면, 각각에 필드에 대해 이름이 붙여져 있는 것을 확인할 수 있다. 여기서는 모든 필드에 대해서 보지는 않고, 당장 필요한 몇 가지 필드에 대해서만 탐색할 예정이다.

그럼 이 쯤에서 의문이 들 수 있다. ‘세그먼트 디스크립터는 그럼 어디에 저장되어 있나요?’ 답변을 하자면, 실제 메모리에 저장되어 있다. 바로 Global Descriptor Table (GDT) 라는 곳에 모여 있는데, GDT는 연속된 디스크립터의 집합이며, 최대 8192개6 의 디스크립터를 저장할 수 있는 공간이다. 하지만 이 GDT 또한 메모리에 저장되어 있는 영역이기 때문에, GDT의 위치를 CPU가 알기 위해서 존재하는 것이 바로 Global Descriptor Table Registers (GDTRs) 이다. 이 GDTR은 16비트의 GDT 크기 필드와 32비트 기준 주소 필드로 구성된 자료구조의 물리 주소를 넘겨받는다. 프로세서는 GDTR에 이 값을 저장했다가 세그먼트 셀렉터를 참조하여 GDT의 위치를 찾는 데 사용한다.

보호 모드에서 세그먼테이션 방법으로 저장 된 주소를 찾는 방법은 리얼 모드와 마찬가지로 세그먼트 레지스터의 기준 주소에 GPR을 더해 구한다. 이렇게 구한 값은 선형 주소(가상 주소)이며, 프로세서는 실제 메모리에 접근할 때 이 선형 주소를 기반으로 물리 ㅠ주소를 계산합니다. 보호 모드는 리얼 모드와 달리 GPR의 크기가 32비트이다. 따라서 32비트로 접근할 수 있는 범위는 0~21,47,48,3647 (2^32-1) 까지 접근이 가능하고, 굳이 세그먼트의 크기를 제한할 이유가 없어졌다. 그렇기 때문에 보호 모드부터는 세그먼트의 크기를 지정할 수 있다. 세그먼트 크기는 해당 세그먼트의 어드레스에 접근할 때 참조하며, 기준 주소에 더해질 값(범용 레지스터의 값 혹은 CPU 명령어로 들어오는 값)이 세그먼트의 크기(세그먼트 디스크립터에 정의된 해당 세그먼트의 크기)를 넘을 수 없다. 만약 이 범위를 넘게 된다면 CPU는 세그멘테이션 폴트 예외를 발생한다.

Segmented Memory Model
(그림 4. Segmented Memory Model)

리얼 모드와 달리, 디스크립터 테이블을 거쳐 나온 선형 주소는 실제 메모리와 무조건 일치하지 않는다. 만약 페이징 기능을 사용하고 있다면 해당 메모리는 선형 주소가 될 것이고, 사용하지 않는다면 해당 메모리는 실제 메모리가 된다.

Segmentation and Paging
(그림 5. Segmentation and Paging)

페이징은 물리 메모리를 페이지 (Page) 라고 불리는 일정한 크기로 나누고, 선형 주소와 물리 주소를 나눠 놓은 페이지로 연결하는 방식을 뜻한다. 페이징을 사용하면 물리 메모리 크기보다 더 큰 영역의 선형 주소도 물리 페이지만 연결하면 사용이 가능하다는 장점이 있어, 공유 메모리 처리, 독립된 주소 공간 보장, 공유 라이브러리 등의 기능을 지원해 줄 수 있다는 장점이 있다.

보호 모드에서 페이징은 페이지 크기에 따라 몇 가지 페이징 방식으로 나뉜다.

각각의 페이징 방식은 나뉘는 단계, 플래그의 상태 등에 따라 구분되지만, 여기서는 기본 개념 이해만 하고 넘어가기 위해 가장 기본이 되는 4-Kb Non-PAE Page Translation을 기준으로 설명하겠다. (이하 4kb Non-PAE)

4kb Non-PAE는 32bit 선형 주소를 페이지-디렉터리 오프셋, 페이지-테이블 오프셋, 페이지-오프셋 세 부분으로 나누며, 물리 메모리를 4KB로 나누어 관리하는 방법이다. 선형 주소의 페이지-디렉터리 오프셋과 페이지-테이블 오프셋은 단순히 페이지의 테이블과 물리 주소의 오프셋을 찾기 위한 부분이며, 실제 페이지 테이블 정보와 페이지 디렉터리 정보는 각각 페이지-디렉터리 테이블, 페이지-테이블에 저장되어 있다. 이는 GDT와 마찬가지로 메모리 상에 존재하는 자료구조일 뿐이며, 이를 이용하기 위해 CR3 레지스터가 존재한다. CR3 레지스터는 페이지 디렉터리의 시작 주소를 가리키며 페이지 디렉터리 엔트리의 위치 계산에 이용된다.

페이지 디렉터리 엔트리와 페이지 테이블 엔트리는 크기가 모두 4바이트이다. 페이지 크기가 최소 4KB이므로 모든 페이지는 20비트 안에 표현이 가능하다. 따라서 페이지 (디렉터리/테이블) 엔트리는 상위 12~31비트까지를 페이지 기준 주소를 나타내고, 하위 0~11비트까지를 속성 필드로 사용한다.

4-Kbyte PDE/PTE Non-PAE
(그림 6. 4-Kbyte Non-PAE Page Translation)

속성 필드 중에 유심히 봐야 할 필드는 U/S(User/Supervisor) 필드이다. U/S 필드는 해당 페이지에 접근할 수 있는 권한을 나타낸다. 앞서 설명했던 DPL을 저장하는 필드로써, 이를 이용하면 메모리 모델을 단순하게 유지하면서 커널 영역과 유저 영역을 구분하는 것이 가능하다.

선형 주소는 최상위 비트부터 페이지-디렉터리 오프셋 10비트, 페이지-테이블 오프셋 10비트, 페이지 오프셋 12비트로 구분된다. 디렉터리와 테이블의 오프셋이 10비트이므로 페이지 디렉터리와 페이지 테이블의 총 엔트리 수는 1024개이다(2^10) 선형 주소의 마지막에 있는 페이지 오프셋은 12비트이므로 최댓값은 4KB(2^12)이며, 페이지의 크기가 4KB이므로 4KB내에서의 오프셋을 나타내기 위함이다. 페이지 디렉터리 엔트리와 페이지 테이블 엔트리 값은 각자 다음에 위치하는 페이지 테이블의 시작 주소, 페이지의 시작 주소를 나타내며 이 값에 선형 주소의 오프셋을 더해 물리 주소를 구한다. [그림 7.]을 보자.

4-Kbyte Non-PAE Page Translation - Legacy mode
(그림 7. 4-Kbyte Non-PAE Page Translation)

[그림 7.]을 바탕으로 선형 주소로부터 물리 주소를 계산하는 법을 알아본다.

  1. CR3 레지스터에 설정된 어드레스를 통해 페이지 디렉터리 테이블의 시작 주소를 찾는다.
  2. 페이지 디렉터리의 시작 주소로부터 선형 주소의 페이지 디렉터리 오프셋을 이용하여 해당 디렉터리 엔트리를 찾는다. (PDE)
  3. 페이지 테이블의 시작 주소로부터 선형 주소의 페이지 테이블 오프셋을 이용하여 해당 페이지 테이블 엔트리를 찾는다. (PTE)
  4. 페이지의 시작 주소에 선형 주소의 페이지 오프셋 값을 더해 실제 물리 주소로 변환한다.

롱 모드

[추가예정]

마치며

힘들다. 이해하는 거랑 적는 거랑은 또 다르다는 걸 느꼈다. 내일은 실제로 OS를 만들기 시작하는 부분이다. 화이팅!

레퍼런스

노트

  1. 사실상 지원하지 않는 OS가 없다고 할 정도로. 

  2. 메모리가 덩어리 채 할당되고 해제되는 과정에서 빈 부분이 발생하게 되는데, 이 공간이 많아져서 실제 메모리 공간은 충분하지만 더 이상 메모리를 할당할 수 없는 상황. 

  3. 프로세스가 필요한 양보다 메모리가 추가로 할당되어서 낭비되는 상황, 프레임의 갯수는 정수개로 할당이 되기 때문에 생기는 문제. 

  4. 메모리 영역의 정보를 저장하는 자료구조로써, 여러 종류가 있다. 

  5. 낮을 수록 높은 권한을 가진다. 

  6. 32비트 기준 

  7. Physical-address extensions