메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

리눅스 커널 프로그래밍(1) - 메모리 모델

한빛미디어

|

2007-01-29

|

by HANBIT

16,096

제공 : 한빛 네트워크
출처 : IT EXPERT, 리눅스 커널 프로그래밍 Chapter 8

운영체제의 핵심 기능 중에 하나는 메모리를 효율적으로 관리하는 것입니다. 메모리 관리 역시 운영체제에 따라 발전해왔습니다. 여기서는 메모리 모델이 등장하게 된 배경을 살펴보고, 메모리를 관리하는 다양한 방법을 살펴봅니다.

메모리 모델의 배경

운영체제는 가능하면 메모리를 많이 확보해야 하며, 동시에 여러 프로그램이 실행되는 경우에 각자의 영역을 침범하지 않게 프로그램에 할당된 공간을 보호해야 합니다. 또한, 메모리도 하드디스크와 마찬가지로 프로그램의 실행과 종료가 빈번하면 작은 영역으로 잘게 나눠지게 됩니다. 총 여유 메모리는1M지만, 100k의 크기로 10곳에 나눠져 있다면, 100k 이상의 크기를 할당할 수 없기 때문에 100k 이상의 프로그램을 실행할 수 없게 됩니다. 즉, 어떤 방법을 사용하면 이런 문제들을 가장 효율적으로 해결할 수 있을까? 이런 문제들에 대한 해결 방법을 간단히 메모리 모델이라 부릅니다. 메모리 모델의 발전은CPU의 발전과 함께 했습니다.

1980년대에 처음 등장했던 코모도어64(Commodore64)같은 8비트 PC들은 4k에서 256k 정도의 메모리를 가졌습니다. 8086과 함께 등장했던 16비트 PC들은 512k나 640k의 메모리를 가졌습니다. 이런 시스템들은 플로피 디스켓을 사용해서 OS를 부팅했으며, 한 번에 하나의 프로그램만 실행할 수 있었습니다. 이때만 해도 한 번에 여러 프로그램을 실행하는 멀티 태스킹이란 무엇인가라는 주제로 얘기를 나누며, 정말 가능한가? 시스템이 많이 느려지지 않나? 라고 생각했던 시대였습니다. 정말 그리운 시대죠?

이런 환경에서 실행하는 프로그램들은 정확하게 시스템이 가진 메모리 크기만 사용할 수 있었습니다. 게임이나 로터스 같은 DB 관리 프로그램들을 실행하기 위해 필요한 메모리 크기를 확보하기 위해 노력했던 시대였습니다. 인텔 CPU는 메모리를 세그먼트 단위로 관리했는데, 한 번에 최대로 할당할 수 있는 크기가 세그먼트 하나, 즉 64k를 넘을 수 없었습니다. 때문에, 이런 제약사항을 뛰어넘기 위해 다양한 기법들이 쓰였으며, 메모리 관리에 대한 책임은 개발자에게 있었습니다.

8086의 특이한 점은 16비트 CPU지만 데이터를 주고 받는 통로는 20비트였습니다. 즉, 최대 1M까지의 공간에 접근할 수 있었습니다. 216은 65536, 즉 64k라는 작은 공간이었고, 이 공간을 세그먼트라 불렀습니다. 세그먼트 단위로 옮기면서 메모리를 관리했었습니다.

80286은 8086의 다음 세대로 가상의 8086을 지원할 수 있는 기능을 가진 CPU입니다. 실제로는 그 사이에 80186이 있지만 이것을 구경한 사람은 거의 없습니다. AT 컴퓨터라 불리던 80286은 인텔계열에서 최초로 보호 모드(Protected Mode)와 실제 모드(Real Mode)를 지원하는 CPU였습니다. 한 번에 하나의 프로그램만 실행할 수 있는 모드를 실제 모드라 부르며, 한 번에 여러 개의 프로그램을 실행할 수 있는 모드를 보호 모드라 합니다. 이 때부터 응용프로그램이 사용하는 메모리 영역의 보호 수준을 결정하는 기능이 추가되었기 때문에 보호 모드라 부릅니다. 사용할 수 있는 최대 메모리도 8086의 1M에서 16M로 확장되었습니다. 그럼에도 불구하고 80286은 8086과의 호환을 위해 실제 모드를 지원하며, 특별한 명령을 통해 보호 모드로 변경됩니다. 리눅스나 윈도우 시스템은 모두 PC에 전원이 들어오면 실제 모드로 시작하며, 지정된 명령들을 통해 보호 모드로 진입합니다.

80386은 인텔 계열에서 최초로 등장한 32비트 시스템이며, 보호 모드에서 완전한 멀티 태스킹을 지원, OS의 커널 영역과 사용자 영역의 분리를 위한 특권 레벨, 가상 메모리, 스왑 등의 지원을 포함합니다. 오늘날 메모리 관리에서 핵심이 되고 있는 페이징 기능도 80386부터 지원을 시작했습니다. 메모리 모델도 실제 모드, 보호 모드, 가상 모드의 세 가지를 지원합니다. 80286의 보호 모드는 16M까지 지원했지만, 80386부터 최대 4G까지 지원하게 됩니다. 가상 모드는 보호 모드에서 가상의 8086 머신을 실행할 수 있는 모드입니다.

80386부터 32비트 운영체제를 이용하기 시작했으며, 한 번에 할당할 수 있는 메모리 크기도 더 이상 64k가 아니라 자신이 원하는 크기만큼 할당할 수 있게 되었습니다. 더욱이, 가상 메모리의 지원에 힘입어 실제 물리 메모리보다 더 많은 메모리를 할당하는 것도 가능하게 되었습니다.

도스가 목표로 했던 시스템은 최초에는 8비트였고, 그 이후에는 16비트인 8086 CPU였습니다. 이 때의 CPU는(나중에 설명하겠지만) 세그먼트 방식만 지원했으며, 도스도 커널이 위치할 영역을 지정하는 것 외에는 특별한 메모리 관리를 하지 않았습니다. 개발자들은 스스로 메모리 관리를 해야했고, 볼랜드(Borland) 사의 개발 도구들은 개발하려는 프로그램의 용도에 맞는 메모리 관리를 제공하기도 했습니다. 이때는 개발자가 잘못 작성한 프로그램이 메모리의 다른 영역을 덮어쓰는 것이 가능했고, 하드웨어의 주소만 알면 디바이스를 직접 제어할 수 있었습니다.

80386 이후에 등장한 운영체제들은 32비트 보호 모드로 동작했으며, 각 프로그램들은 자신의 가상 주소 영역을 갖게 되었으며, A 프로그램이 B 프로그램의 영역에 접근하는 것은 금지되어 있었습니다. 오직, 커널 만이 A와 B를 모두 접근할 수 있습니다. 예를 들어서, 1M 메모리를 가진 시스템이 있을 때, 동시에 2개의 프로그램을 실행하려면 프로그램에 어떻게 메모리를 할당할 것인가하는 문제가 발생합니다. 프로그램 하나당 정해진 크기만큼 할당하는 방법을 생각할 수 있지만, 이는 정해진 크기 이상의 메모리가 필요한 프로그램을 실행할 수 없게 됩니다. 사용할 수 있는 메모리 영역을 표로 만들고, 프로그램이 실행될 때 요청한 크기만큼 메모리를 동적으로 할당하는 방법이 있습니다. 이 방법을 사용하는 경우의 문제는 다음과 같습니다. 1M 메모리를 가진 시스템에서 600k가 필요한 A가 실행되고, 300k가 필요한 B가 실행됩니다. 이때 A는 실행을 종료하면 여유 공간의 크기가 600k, 100k 두 부분이 됩니다. 여유 공간의 합은 700k이지만, 700k가 필요한 프로그램에게 메모리를 할당할 수 없기 때문에 실행할 수 없습니다. 이와 같은 문제를 외부 단편화(External Defragmentation)라 합니다.

또 다른 문제는 1M 메모리를 가진 시스템에서 A 프로그램이 메모리를 300k 사용하고, B 프로그램이 메모리를 500k 사용하고 있는데, A 프로그램에서 필요한 메모리 크기가 커져서 600k까지 사용하게 되었습니다. 이런 경우에 프로그램 A와 B가 서로 충돌해서 더 이상 프로그램을 실행할 수 없게 됩니다. 프로그램을 작성하면서 계산기 프로그램이 내 프로그램의 메모리 영역을 침범하면 안 되는데라고 고민하면서 프로그램을 작성하지 않을 것입니다. 이런 문제들을 해결하기 위해 제공된 것이 가상 메모리입니다. 즉, 운영체제는 메모리 관리와 관련된 문제들을 해결하기 위해 다양한 방법들을 제공하며, 가상 메모리와 물리 메모리를 일관되게 관리하는 것을 목표로 합니다.

메모리 모델의 종류

메모리를 관리하는 방법은 세그먼트(Segment) 기법과 페이징(Paging) 기법이 있습니다. 우리가 흔히 보는 책은 페이지를 매기는 방법이 두 가지가 있습니다. 예를 들어, 1,000페이지로 된 책이 있을 때, 페이지를 1부터 1,000번까지 순서대로 매겨놓은 책이 있는가 하면, 책을 각 장 별로 나누고, 각 장에서 몇 번째 페이지라고 표기하는 방법이 있습니다. “2-20”으로 표기하면 “2장에서 20번째 페이지”를 의미합니다. 첫 번째 방법과 같이 선형적으로 일관되게 페이지를 매기는 방법을 페이징이라 하고, 두 번째 방법과 같이 각 장별로 책을 나누고, 각 장에서 몇 번째 페이지로 나누어 관리하는 방법을 세그먼트 기법이라고 합니다. 여기서, 장은 세그먼트에 해당하며, 해당 장의 몇 번째 페이지는 옵셋(Offset)에 해당합니다.

[여기서 잠깐]
옵셋(Offset)은 ‘어셉’에 가깝지만 외래어로는 ‘오프셋’ 또는 ‘옵셋’으로 표현하고 있습니다. 메모리(Memory)는 ‘메므리’에 가까우며, 모델(Model)은 ‘마들’에 가깝습니다. 지하철역 이름인 ‘마들’과 발음이 같군요. 한국에 사는 원어민들이 자주 혼동하는 발음에는 ‘스떼이크’가 있습니다. 스테이크(Steak)가 장소를 뜻하는 ‘숙대입구’와 발음이 거의 비슷하게 들려서 ‘스테이크에서 만나자’라고 말하면 이해하지 못하는 경우가 많다고 합니다.



세그먼트 모델은 각 메모리 공간을 용도에 따라 나누는 방법을 사용합니다. CPU 상에서 실행되는 명령어들을 담고 있는 영역을 코드 세그먼트(CS, Code Segment), 실행에 필요한 데이터들을 담고 있는 영역을 데이터 세그먼트(DS, Data Segment), 프로그램을 실행하면서 임시로 사용하는 저장 공간인 스택 세그먼트(SS, Stack Segment), 추가적인 데이터들을 담아놓을 수 있는 임시 세그먼트(ES, Extra Segment) 등이 있습니다. int, char 등으로 선언되는 데이터들은 데이터 세그먼트에 저장하고, 함수와 같이 실행할 코드들은 코드 세그먼트에 두며, 함수 호출시의 인자를 저장하는 등의 용도로 스택 세그먼트를 사용합니다.

세그먼트 모델은 항상 “세그먼트 선택자(Segment Selector) + 옵셋”으로 메모리 위치를 지정합니다. 이와 같은 방법을 사용하면 각 메모리 공간을 용도에 따라 나눠 쓰게 되며, 다른 영역과 분리할 수 있기 때문에 안정성을 향상시킬 수 있습니다. 세그먼트 모델은 16비트 실제 모드와 32비트 가상 모드에서 16비트 옵셋을 가지며, 한 번에 64k의 메모리에 접근할 수 있습니다. 보호 모드에서는 논리주소를 16비트 세그먼트 선택자 + 32비트 옵셋으로 접근하기 때문에 4G까지 접근할 수 있습니다. 16비트 세그먼트 선택자는 각 메모리 공간의 용도, 접근 권한, 범위 등을 정의하고 있습니다. 80386 이후 펜티엄에 이르기까지 32비트 시스템은 모두 4G의 가상 공간을 갖게 되었지만 세그먼트 모델을 사용해서 영역을 나누기 때문에 완전한 선형 메모리 모델(Flat Memory)은 아닙니다. 펜티엄D 이후로 등장한 EM64T나 IA64 같은 64비트 아키텍처에서는 세그먼트를 사용하지 않으며, CS, DS, ES, SS 세그먼트를 0으로 설정하며, 선형 메모리 모델을 사용합니다.

페이징 기법은 전체 메모리를 선형 공간(Flat Memory)으로 보고, 각 영역을 페이지 단위로 나누어 관리하는 방법을 의미합니다. 리눅스는 다양한 아키텍처에서 일관되게 메모리를 관리하기 위해 페이징 기법을 주로 사용하고 있지만, i386 CPU에서는 세그먼트 기법을 반드시 사용해야 하기 때문에 i386 환경에서는 세그먼트 기법과 페이징 기법을 함께 사용하고 있습니다. 리눅스 커널의 경우 i386 환경에서 내부적으로 세그먼트 기법을 사용하지만 프로그램이 이를 알아채지 못하게 하며, 프로그램들은 선형 공간에 있다고 생각합니다. 이와 같은 환경을 암시적인 세그먼트(implied segments)라 부릅니다.

세그먼트 기법은 3가지가 있으며, 기본 모델은 운영체제와 응용프로그램 영역을 구분하지 않고, 코드 세그먼트와 데이터 세그먼트만 구분하여 사용하는 방법입니다. 보호 모델은 운영체제와 응용프로그램 영역을 나누고, 각 영역별로 코드 세그먼트와 데이터 세그먼트를 나누는 기법입니다. 이 방법은 커널 공간과 사용자 공간의 세그먼트 한계를 지정하며, 운영체제와 응용프로그램을 분리하며, 프로세스간 보호 기능을 제공하기 때문에 이 방법이 널리 쓰입니다. 멀티 세그먼트 모델은 코드 세그먼트, 데이터 세그먼트뿐만 아니라 각 프로세스에 대해서도 세그먼트 기법을 도입하는 것으로 이 경우에는 각 프로세스 영역에 대한 접근도 운영체제가 할 수 없고, CPU를 통해서 접근해야 합니다.

리눅스 커널은 세그먼트 기법 중에 보호 모델을 사용하기 때문에 각 영역별로 코드 세그먼트와 데이터 세그먼트를 정의하고 있습니다. 커널 영역에 대한 코드 세그먼트, 데이터 세그먼트, 사용자 영역에 대한 코드 세그먼트, 데이터 세그먼트를 정의하고 있습니다(4개). 이들 정의는 include/asm-i386/segment.h에서 확인할 수 있습니다.



[여기서 잠깐]
커널 2.4는 세그먼트 정의를 각 비트 결과를 합한 16진수로 하고 있지만, 커널 2.6에서는 각 비트별로 나누어서 상수로 정의하고 있습니다.

각 세그먼트는 세그먼트 디스크립터(Segment Descriptor)에 따라 각 비트별로 값을 기술하게 되어 있으며, 이 값들의 결과를 16진수로 적으면 0x10, 0x18, 0x23, 0x2B와 같습니다. 커널 세그먼트는 디스크립터 권한 레벨(DPL)을 0으로 설정하며, 사용자 세그먼트는 디스크립터 권한 레벨(DPL)을 3으로 설정합니다.

[여기서 잠깐]
세그먼트 디스크립터에 대한 자세한 내용은 “Intel Architecture Software Developer’s Manual, Volume 3 for P4”를 참고하기 바랍니다.

i386 환경이나 다양한 CPU 아키텍처를 지원하는 현대 운영체제는 정확하게 세그먼트와 페이징으로 나누어 관리하기 보다는 이 두 가지를 적절하게 혼합된 형태를 사용합니다. 운영체제에서 메모리를 관리하는 방법은 운영체제 개발자 마음대로 정할 수 있지만 실제로 CPU와 데이터를 주고받을 때는 해당 CPU에 맞춰서 데이터를 주고받아야 합니다. 따라서, 메모리와 관련된 코드는 해당 CPU에 종속적인 코드일 수밖에 없으며, 어셈블리어로 작성됩니다. 리눅스 커널은 다양한 CPU 아키텍처를 지원하는데, 이를 위해 공통된 인터페이스는 C 언어로 작성하고, 해당 CPU 아키텍처에서 구현해야 하는 내용만 어셈블리로 작성하고 있습니다. 따라서, 해당 CPU 아키텍처의 구조나 어셈블리를 잘 모르더라도 커널에서 제공하는, C 언어로 작성된 커널 API를 사용해서 메모리를 조작할 수 있습니다.
TAG :
댓글 입력
자료실