2013. 10. 3. 14:49

x86 아키텍쳐 메모리 관리 스킴의 이해 I

이번 시간에는 x86 아키텍쳐의 메모리 관리 스킴에 대해서 알아 보도록 합시다.

 

사실, 이 주제를 포스팅하는 것에 대해 고민하였습니다. 이 주제로 검색하면 굉장히 잘 정리된 블로그나 기술 문서들이 많기 때문입니다. 특히, 운영체제 개발 관련 서적에서는 이와 관련된 내용이 체계적으로 잘 정리가 되어 있습니다. 그리고 그것을 구현한 코드도 함께 볼 수 있구요. 이미 보편적으로 오래전에 파급된 이 주제를 제가 이제와서 언급하는게 적절한지는 잘 모르겠군요. 제가 x86 Architecture에 대해 공부한지는 꽤 되었습니다. 예전에 차라리 정리를 해놓았으면 하는 후회가 남네요. 뒤늦게라도 다시 정리를 해보는 것도 리마인드 차원에서 나쁘지는 않을 것 같군요. 사실 저는 개발하는 것보다 이론 공부를 더 좋아하는 성격인지라, 순전히 이론만으로 이 주제를 잘 설명할지는 의문이긴 합니다만, 이번 시간에 한 번 정리를 해보도록 하겠습니다.

 

당연히 이 주제는 너무나도 유명한 아래의 인텔 메뉴얼을 참고하였습니다.

Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A:
System Programming Guide, Part 1

 

x86 아키텍쳐에서 메모리 관리 기법은 크게 세그먼테이션과 페이징으로 분류할 수 있습니다. 이 두가지 메모리 관리 기법을 언급드리기 앞서, 우리가 선행해야 할 것은 과거의 메모리 관리 스킴입니다. 과거 메모리 관리 스킴으로 시작하여 현재 어떻게 변천해 왔는지 알아보는 것은 충분히 의미가 있습니다.

 

우선, 세그먼테이션(Segmentation)를 간단히 정의해도록 합시다. 세그먼트는 우리말로 풀어보면 사전적 의미는 '조각'이고, 세그먼테이션은 '분할'로 나오네요. 굳이 억지로 번역하면 '조각 나누기' 정도로 해석할 수 있겠네요. 그럼 필요한 이유가 뭘까요? 저의 생각으로는 적당히 세 가지 정도를 이유로 들 수 있을 것 같은데요. 첫 번째는 코드와 데이터처럼, 서로 다른 성격을 지닌 메모리 영역을 나누어 관리하기 위함입니다. 당연한 얘기가 되겠지만 프로그램을 동작중에 데이터를 코드 영역에다가 쓰기 행위는 제한되어야 할 것이고, 또 다른 예로는 프로그램 특성상 오직 읽어야 하는 영역임에도 불구하고 이를 접근하여 쓰기를 시도하는 행위도 읽기 전용 메모리 속성으로 구분되어야 할 것입니다. 이와 같이, 세그먼테이션을 통해 각 세그먼트의 보호 속성을 지정하여 관리할 수 있습니다. 두 번째는 필요에 따라서 특정 메모리 영역을 공유할 수 있습니다. 동일한 프로그램을 두 번 실행되었다고 하더라도 프로그램 코드를 두 번 메모리에 로딩할 필요없이 특정 코드 세그먼트 영역에 한 번만 로드하여 공유하고 데이터 영역만 분리하여 효율적인 메모리 관리를 할 수 있습니다. 이것은 사실 멀티 태스킹 환경에서 그러한 지원은 당연하다고 볼 수 있지요. 세 번째 이유는 특정 메모리 영역의 접근이 용이하다는 점입니다. 단순히 물리 메모리를 하나로 코드와 데이터 등의 영역을 관리한다면 유저 입장에서는 특정 영역에 접근할 때, 예를 들어 코드는 0xFFFF, 메모리는 0x1000와 같은 절대 위치를 항상 참조해야 하기 때문에 번거로움이 있습니다. 그래서 각 세그먼트 영역마다 베이스 주소와 한계 주소과 같은 정보를 CPU에게 미리 알려 준 상태라면 유저(프로그래머) 관점에서는 어떤 영역이든 오프셋 0을 기준으로 하여 접근하면 되니 메모리 관리에 신경쓰지 않아도 될 것입니다. 다시 요약하면, 속성이 다른 영역의 메모리를 적절히 나누어 관리하기 위한 기법으로 정의할 수 있습니다. 그렇다면 이번에는 페이징에 대한 정의를 찾아봐야 할 것 같습니다만, 앞서 페이징이 필요한 이유부터 알아봅시다.

   우선 세그먼테이션을 기반으로 하여 가상 메모리가 아닌 순수 물리 메모리상에서 여러 개의 프로그램이 동작하는 멀티 태스킹 환경이라고 가정하겠습니다. 그림①은 프로그램 크기가 다른 프로세스 A, B, C, D, E가 차례대로 물리 메모리 영역에 로딩된 상황입니다. 순서대로 실행하여 메모리에 로딩한 상황 자체는 특별한 문제가 없습니다. 그리고 그림②에서는 프로세스 B와 D가 실행이 끝나고 언로딩된 상황을 보여주고 있습니다. 여기서 문제는 다시 새로운 프로세스 F를 실행하기 위해 로딩하려는 시점입니다. 그림에서 보시다시피, ⓐ와 ⓑ 영역에 Process F를 연속된 공간으로 로딩하기엔 공간이 부족합니다. 이와 같이, ⓐ와 ⓑ 영역을 메모리에서 외부 단편화(External Fragmentation)가 발생하였다고 표현합니다. 외부 단편화의 범위는 프로세스가 가지고 있는 코드와 데이터 영역 모두가 됩니다. 이런 외부 단편화로 인해, 프로그램 로딩을 위한 적절한 공간을 할당받을 수 없다면 기존에 로딩한 프로그램들을 재배치하고 조각으로 나누어진 영역들을 하나로 합치는 등의 워스트 케이스가 발생하면 그 버든은 아주 클 수 밖에 없지요. 바꿔 얘기하면, 외부 단편화 문제는 세그먼테이션에서 발생하는 문제라고 말씀드릴 수 있습니다. 외부 단편화 문제를 해결하기 위해, 인텔 x86에서는 페이징 기법을 제공합니다. 프로그램을 실행할 때 코드와 데이터를 통째로 로딩하는 것이 아니라, 모든 영역을 페이지 단위(IA-32에서는 기본 4K)로 나누어 메모리를 관리할 수 있습니다.

위의 왼쪽 그림을 보면, 물리 메모리에서 각 프로세스의 코드와 데이터 일부가 페이지 단위로 로드되어 있습니다. 중간 그림에서 프로세스 D의 실행이 종료되고 페이지 아웃된 후에 새로운 프로세스 E 코드 일부가 물리 메모리에 로드된 것을 보여주고 있습니다. 페이지 단위의 메모리 공간 확보만 보장된다면 프로그램 영역 할당은 쉽게 이루어질 수 있습니다. 즉, 페이징 기법으로 외부 단편화 문제를 극복할 수 있습니다. 그리고 여기서 아직 가상 메모리 개념을 자세히 설명드리지 않았습니다만, 사실 페이징 기법은 가상 메모리 스킴과 밀접한 관련이 있습니다. 위의 그림에서는 단지 실행에 필요한 프로세스 코드와 데이터 일부가 로드된 것을 보여준다는 점을 유념하시기 바랍니다. 일단, 오늘은 여기까지만 정리하고 다음편에서 이 주제에 대해서 계속 설명드리겠습니다. 좋은 하루 보내세요.

 

 

Written by Simhyeon, Choe