이번 포스팅에서는 메모리 관리에 대해서 정리해 보고자 한다. 참고로 내용이 많으므로 두 번에 나누어서 정리한다.
컴퓨터가 널리 보급되면서, 범용 컴퓨터 시스템의 목적은 CPU의 활용률을 극대화 하는 것으로 나아갔다. 사용자들에게 빠른 응답을 제공하기 위해서 보다 많은 프로그램을 메모리에 올려서 실행(multi-programming) 시키게 되었고, 여러 프로그램을 동시에 실행시키기 위한 스케줄링 기법이 등장하게 되었다.
이와 같이 여러 프로그램이 동시에 메모리에 적재되어 실행되면서, 메모리를 공유할 필요가 생겼다. 컴퓨터의 메모리는 한정되었는데, 실행하는 프로그램이 많아지면 메모리 요구량이 증가했기 때문이다.
컴퓨터에서 작동하는 응용 프로그램은 프로그래밍 언어로 만들며, 보통은 컴파일러를 사용하여 작성된 프로그램을 실행 가능한 코드로 변경한다. 이 때 언어 번역 프로그램을 사용하는데, 언어 번역 프로그램은 고급 언어(C, Java 등)로 작성한 소스코드를 컴퓨터가 실행할 수 있는 기계어로 번역하는 프로그램이다. 대표적인 언어 번역 프로그램은 컴파일러(Compiler)와 인터프리터(Interpretor)가 있다.
컴파일러 : 소스코드를 컴퓨터가 실행할 수 있는 기계어로 번역한 후 한꺼번에 실행한다. C, Java 등이 해당됨
인터프리터 : 소스코드를 한 행씩 번역하여 실행한다. 자바스크립트, 베이직 등이 해당됨
컴파일러와 인터프리터의 차이를 살펴보면 컴파일러에서는 변수를 미리 선언해야 하며 실행 전에 코드 최적화 작업이 일어나지만, 인터프리터는 한줄씩 실행되기 때문에 변수를 미리 선언할 필요가 없고, 코드 최적화 과정이 일어나지 않는다. 자바의 경우는 .java 파일을 javac(자바 컴파일러)가 .class 파일로 컴파일하고 그 이후에 자바 인터프리터가 기계어로 바꾸는 작업을 JVM에서 수행한다. 즉, 자바는 컴파일러와 인터프리터가 둘 다 있는 것이다. 컴파일러가 인터프리터보다 평균적으로 실행 시간도 빠르고, 보안적으로도 유리하지만 그럼에도 인터프리터를 같이 사용하는 이유는 인터프리터가 플랫폼에 종속되지 않으며, 인터프리터가 바이러스나 악성 프로그램으로부터 보호해 주는 역할도 할 수 있기 때문이다.
주소 공간(Address Space), 물리 주소(Physical Address)와 가상 주소(Virtual Address)
CPU의 비트는 한 번에 다룰 수 있는 최대 크기를 의미한다. 32bit CPU는 한 번에 다룰 수 있는 데이터의 최대 크기가 32bit이고, 64bit CPU는 한 번에 다룰 수 있는 데이터의 최대 크기가 64bit이다. CPU의 비트는 메모리 주소 공간의 크기와도 연관이 있다.
주소 공간(address space)은 프로세스에서 참조할 수 있는 주소들의 범위(집합)을 의미한다. 프로세스와 1대1의 관계를 가지며, 사용자 쓰레드는 주소공간을 공유한다. 주소공간의 크기는 CPU의 주소 버스(address bus)에 의존하는데 주소 버스가 32bit 인 경우 2^32개의 서로 다른 주소에 대해서 식별자를 만들 수가 있다. 즉, 0부터 2^32 - 1까지의 주소 범위를 addressing 할 수 있다는 의미이다. 32bit CPU는 메모리를 최대 2^32 B = 4GB까지 사용할 수 있다.
32bit CPU든 64bit CPU든 컴퓨터에는 메모리가 설치되며 각 메모리 주소 공간이 있다. 이렇게 설치된 메모리의 주소 공간을 물리 주소 공간(physical address space)이라고 한다. 물리 주소 공간은 하드웨어 입장에서 바라본 주소 공간으로 컴퓨터마다 그 크기가 다르다. 이와 반대로 사용자 입장에서 바라본 주소 공간은 논리 주소 공간(logical address space)이라고 한다.
컴퓨터의 메인 메모리를 접근할 때 사용되는 주소를 물리 주소(physical address)라고 한다. 이는 메모리 관리자 입장에서 바라본 주소이다. 메모리에서 어떤 값을 읽어내기에는 편리할 수 있으나, 운영체제가 업그레이드 되거나 하면 주소 범위가 바뀔 수 있기 때문에 사용자에게는 불편할 수도 있다. 따라서 사용자 프로세스의 입장에서 사용하는 주소를 따로 정의해 주게 되는데, 이를 가상 주소(logical address or virtual address)라고 한다. 프로세스의 관점에서 사용하는 주소이며, 물리 주소와 관계없이 항상 0번지에서 시작한다.
주소 연결(Address Binding)
초창기 컴퓨터는 물리 주소를 compile time에 생성했다. 프로세스가 물리 메모리에서 실행되는 주소를 컴파일 타임에 알아서 절대 코드를 생성한다. 만약 시작 주소의 위치가 바뀔 경우에는 다시 컴파일을 해야 한다. 이러한 방법은 다양한 프로그램이 실행됨에 따라 컴파일 타임에 물리 주소를 정하기가 어려워지면서 한계에 부딪혔다. 1개의 프로그램이 실행되는 경우는 문제가 없지만, 멀티 프로그래밍인 경우 컴파일 타임 시에 결정된 주소는 다른 프로그램과 같이 메모리에 적재하기가 어렵다. 따라서 논리 주소의 필요성이 생기기 시작했다.
컴파일 타임(compile time)에서는 컴파일러가 symbol 테이블을 만들고 주소는 symbol 테이블에 상대적인 주소로 이루어진다. 컴파일 된 오브젝트 파일은 주소 0부터 시작하게 된다. 링크 타임(link time, linkage editor에서 소요되는 시간)에서는 오브젝트 파일들과 시스템에서 제공하는 라이브러리들을 묶어서 symbol 테이블에 의존적이 아닌 주소를 만들어 낸다(address resolution). 링크의 결과로 하나의 executable 파일이 만들어진 주소는 0부터 시작한다.
로더 타임(link time)에서는 프로그램의 실행을 위해 loader는 executable을 메모리로 로드한다. 주소 공간 전체가 메모리에 올라간다면, 로드 시에 물리 주소에 대한 바인딩이 일어난다. 프로그램은 relocatable 주소로 되어 있기 때문에 base register를 통해서 물리 주소로 바꾸어 실행하게 된다. 만일 프로그램의 시작 주소를 바꾸려면, 다시 로드를 해야 한다. 실행 타임(execution time)은 프로세스가 실행될 때 물리 주소가 바뀌는 경우, 물리 주소에 대한 binding은 프로세스가 실행될 때 일어난다. 페이징이나 스와핑을 통해서 프로세스가 올려지는 메모리의 물리 주소는 바뀔 수 있다. 이러한 형태의 주소 결정 방법을 사용하기 위해서 MMU와 같은 특별한 하드웨어가 필요하게 된다.
가상 메모리(Virtual Memory)
초기 CPU에서 주소를 변환하는 방법은 CPU에서 물리 상대 주소를 사용하였기 때문에, 프로그램 내 instruction들의 주소를 시작 주소(base address) 부터의 상대적인 offset으로 표현하였다. 시작 주소가 결정되면 시작 주소 + 상대 주소의 합으로 절대 주소를 생성하였다. 이후 CPU에서 논리 주소(가상 주소)를 사용하는 경우에는 CPU와 Memory 사이에서 주소 Translation이 일어나게 된다. 이 Translation을 수행하는 장치를 MMU(Memory Management Unit)라고 한다. MMU는 CPU로부터 받은 가상 주소를 물리 주소로 번역하여 메모리에 보내는 역할을 한다.
여기서 가상 메모리(virtual memory)의 개념이 등장한다. 가상 메모리는 실제 존재하지는 않지만 사용자에게 메모리로서의 역할을 하는 메모리를 의미한다. 기본 아이디어는 프로세스가 수행되기 위해서 프로그램의 모든 부분이 물리 메모리에 있을 필요는 없다는 생각에서 시작되었다. 현재 실행되고 있는 code/data/stack 부분만이 (전체가 아니라) 물리 메모리에 있으면 프로세스는 실행이 가능하다. 예를 들어 1GB의 물리 메모리가 있는데 이 정도 크기이면 금방 다 쓰게 된다. 이럴 때 훨씬 큰 가상 메모리가 그때그때 필요한 메모리를 제공해 주어 일관된 프로세스가 진행되게 할 수 있다.
32bit CPU의 최대 메모리 크기는 4GB이다. 이 시스템에서 각각 4GB의 주소 공간을 차지하는 3개의 프로세스를 동시에 실행하려면 운영체제를 포함해서 적어도 12GB의 메모리가 필요하다. 이 경우 가상 메모리 시스템에서는 물리 메모리의 내용 중 일부를 하드디스크의 일부 공간, 즉 스왑 영역으로 옮긴다. 스왑 영역은 하드디스크에 존재하지만, 메모리 관리자가 관리하는 영역으로서 메모리의 일부이며, 가상 메모리의 구성 요소 중 하나이다. 메모리 관리자는 물리 메모리의 부족한 부분을 스왑 영역으로 보충한다. 즉, 물리 메모리가 꽉 찼을 때 일부 프로세스를 스왑 영역으로 보내고(스왑 아웃), 몇 개의 프로세스가 작업을 마치면 스왑 영역에 있는 프로세스를 메모리로 가져온다(스왑 인).
가상 메모리 시스템에서 가변 분할 방식을 이용한 메모리 관리 기법은 세그멘테이션, 고정 분할 방식을 이용한 메모리 관리 기법은 페이징이라고 한다. 세그멘테이션 기법은 가변 분할 방식의 단점인 외부 단편화 등의 문제 때문에 잘 사용되지 않으며, 페이징 기법은 페이지 관리에 어려움이 있다. 그래서 두 기법의 단점을 보완한 세그멘테이션-페이징 혼용 기법을 주로 사용한다.
메모리를 관리할 때는 논리 주소와 물리 주소를 매핑하는 매핑 테이블을 작성하여 관리를 한다. 가상 메모리 시스템에서 가상 주소는 실제로 물리 주소나 스왑 영역 중 한 곳에 위치하며, 메모리 관리자는 가상 주소와 물리 주소를 일대일 매핑 테이블로 관리한다.
페이징(Paging)
페이징 기법은 주소 공간을 동일한 크기인 페이지로 나누어 관리하는 기법이다. 보통 한 페이지의 크기는 4KB로 나누어 사용한다. 프레임과 페이지라는 단위를 사용하는데 차이는 다음과 같다. 각각의 프레임 크가와 페이지 크기는 같다.
-
프레임(Frame) : 물리 메모리를 고정된 크기로 나누었을 때, 하나의 블록
-
페이지(Page) : 가상 메모리를 고정된 크기로 나누었을 때, 하나의 블록
페이지가 하나의 프레임을 할당 받으면, 물리 메모리에 위치하게 된다. 프레임을 할당받지 못한 페이지들은 외부 저장장치(Backing store)에 저장된다. Backing store도 페이지, 프레임과 같은 크기로 나누어져 있다. CPU가 관리하는 모든 주소는 두 부분으로 나뉜다.
-
페이지 번호(Page number) : 각 프로세스가 가진 페이지에 각각 부여된 번호 (ex. 1번 프로세스는 0부터 63번까지의 페이지를 가지고 있음)
-
페이지 주소(Page offset) : 각 페이지의 내부 주소를 가리킴 (ex. 1번 프로세스 12번 페이지의 34번째 데이터)
페이지 테이블(Page table)은 각 프로세스의 페이지 정보를 저장하는 공간이며, 프로세스마다 하나의 페이지 테이블을 가진다. 해당 페이지에 할당된 물리 메모리(프레임)의 시작 주소를 가지고 있으며, 이 시작 주소와 페이지 주소를 결합하여 원하는 데이터가 있는 물리 메모리의 주소를 알 수가 있다.
위의 그림을 보면 가상 메모리의 page 0은 page table에서 물리 메모리의 frame 1과 매핑이 됨을 알 수 있다. 참고로 문맥 변환(context switching)을 할 때 마다 페이지 테이블에서 참조하는 주소는 달라진다. 이러한 방법으로 가상 주소가 물리 주소로 변환이 됨을 알 수 있다.
위의 그림은 페이지 테이블을 이용한 주소 변환 과정을 조금 더 자세하게 표현하였다. CPU에서 가상 주소(논리 주소)를 p 페이지의 d 오프셋(p 페이지의 d번째 주소)으로 정의를 하면 페이지 테이블에서 p 페이지 d 오프셋이 f 페이지 d 오프셋(f 페이지의 d번째 주소, d는 변하지 않음)으로 매핑되어 변환됨을 알 수 있다. d가 변하지 않는 이유는 페이지와 프레임의 크기를 똑같이 나누었기 때문이다. 결국 변환된 물리 주소를 가지고 물리 메모리에서 해당 데이터를 참조할 수 있다.
페이지 테이블에서 각각의 한 줄을 페이지 테이블 엔트리(PTE, Page Table Entry)라고 부른다. PTE의 각 필드는 Page base address(해당 페이지에 할당된 프레임의 시작 주소), Flag bits 등으로 구성된다.
참고자료
- 고려대학교 유혁 교수님 운영체제(COSE341) 수업 자료
- <Operating System Concepts> 9th ed. by A. Silberschatz
- <쉽게 배우는 운영체제> 조성호 저, 한빛아카데미
- https://m.blog.naver.com/ehcibear314/221228200531
'Computer Sci. > Operating System' 카테고리의 다른 글
OS #9. 메모리 관리(Memory Management) Part2 (0) | 2020.04.28 |
---|---|
OS #7. 동기화(Synchronization) Part2 (0) | 2020.04.11 |
OS #6. 동기화(Synchronization) Part1 (1) | 2020.04.01 |
OS #5. 쓰레드 (Thread) (0) | 2020.03.24 |
OS #4. CPU 스케줄링 (CPU Scheduling) (0) | 2020.03.20 |