본문 바로가기

운영체제/반효경 교수님 - 운영체제 강의

운영체제 6강 - 메모리 관리

현재 포스팅은 반효경 교수님의 운영체제 강의 + 추가적인 내용을 바탕으로 정리된 글입니다 :)

1. 주소 바인딩

프로그램이 실행되기 위해 물리적 메모리에 로드되는 과정에서, 그 프로세스를 위한 가상주소 공간(or 논리적 주소 공간)이 생깁니다.

앞선 포스팅에서 살펴봤듯이 이러한 주소 공간은 코드,데이터,스택,힙 등으로 구성되어 있으며, 프로세스는 이러한 가상 주소 공간 내에서만 동작하며, 다른 프로세스의 메모리 공간에 직접 접근할 수 없습니다.

 

이때 논리적 주소는 이러한 가상 주소 공간에서 사용되는 주소를 말하며 0번지 부터 시작됩니다. 물리적 주소물리적 메모리에 실제로 올라가는 위치를 말합니다.

 

앞서 말씀드렸듯이, 프로세스가 실행되기 위해서는 해당 프로그램이 물리적 메모리에 적재되어야 합니다. 또한 CPU가 기계어 명령을 수행하기 위해 논리적 주소를 통해 메모리를 참조하게 되면 해당 논리적 주소가 물리적 메모리의 어느 위치에 매핑되는지 확인해야 합니다.

이때 프로세스의 논리적 주소를 물리적 메모리 주소로 연결시켜주는 작업주소 바인딩이라고 합니다!

 

주소 바인딩 방식은 프로그램이 적재되는 물리적 메모리의 주소가 결정되는 시기에 따라 세가지로 분류할 수 있습니다.

우선 물리적 메모리 주소가 프로그램을 컴파일 할때 결정되는 주소 바인딩 방식컴파일 타임 바인딩 이라 합니다. 이러한 바인딩 방식은 프로그램이 올라가 있는 물리적 메모리의 위치를 변경하고 싶다면 컴파일을 다시해야 하므로, 현대 시분할 시스템에서 잘 사용하지 않는 방식입니다.

 

프로그램의 실행이 시작될때 물리적 메모리 주소가 결정되는 주소 바인딩 방식을 로드 타임 바인딩이라 하고, 프로그램이 실행을 시작한 후에도 그 프로그램이 위치한 물리적 메모리상 주소가 변경될 수 있는 바인딩 방식을 실행시간 바인딩(runtime binding)이라 합니다.

 

실행 시간 바인딩 방식에서는 CPU가 주소를 참조할때마다 해당 데이터가 물리적 메모리의 어느 위치에 존재하는지, 주소 매핑 테이블을 이용해 바인딩을 점검해야 합니다. 또한 다른 바인딩 방식과는 달리 실행 시간 바인딩 방식이 가능하기 위해, 기준 레지스터와 한계 레지스터 뿐만 아니라 MMU(Memory Management Unit)라는 하드웨어적인 지원이 뒷받침되어야 합니다.

 

이러한 MMU는 논리적 주소를 물리적 주소로 매핑해주는 하드웨어 장치로, 논리적 주소 값에 기준 레지스터의 값을 더해 물리적 주소값을 얻어냅니다. 예를 들어 CPU가 논리적 주소 123번지에 있는 내용을 요청한다면, 기준 레지스터에 저장된 23000이라는 값에 이 주소를 더해 23123번지에 있는 내용을 참조하게 되는 것이죠 :)

이러한 방식으로 물리적 주소로 매핑하려면, 프로그램의 주소 공간이 메모리의 한 장소에 연속적으로 적재 되는 경우만 가능하겠죠???

 

프로세스는 자기 자신만의 고유한 주소 공간을 가지므로, 동일한 주소값(논리적 주소)이라 하더라도 각 프로세스마다 서로 다른 내용을 담고 있습니다. 즉 CPU가 논리적 주소 100번지를 참조한다 했을 때, 이는 현재 CPU에서 수행되고 있는 프로세스가 무엇인지에 따라 이 100번지가 가리키는 내용은 상이해 지는 것이죠. 이를 위해 MMU 기법에서는 Context Switching으로 CPU에서 수행중인 프로세스가 바뀔 때마다 기준 레지스터 값을 그 프로세스에 해당 되는 값으로 재설정합니다.

 

 

하지만 물리적 메모리 안에 여러 프로세스가 동시에 올라가 있는 다중 프로그래밍 환경에서는 어떨까요??

 

이때는 MMU 방식을 사용해(논리적 주소 + 기준 레지스터 값) 주소 변환을 했을 경우, 결과가 해당 프로세스의 주소 공간을 벗어날 수 있습니다. 따라서 보안적으로 문제가 생길수 있습니다.(ex: 다른 프로그램 영역 침범, 메모리 영역 변경)

운영체제는 이러한 상황을 방지하기 위해 한계 레지스터라는 또 하나의 레지스터를 사용합니다.

 

이러한 한계 레지스터현재 CPU에서 수행중인 프로세스의 논리적 주소의 최댓값을 담고있습니다. 따라서 CPU가 메모리 참조 요청을 했을 때 그 주소가 한계 레지스터 값보다 큰지 먼저 체크함으로서 물리적 보안을 유지하게 됩니다.

 

일련의 과정을 정리해보자면, 먼저 CPU가 요청한 프로세스의 논리적 주소 값이 한계 레지스터 내에 저장된 그 프로세스의 크기보다 작은지 확인합니다. 작다면 논리적 주소값에 기준 레지스터 값을 더해 물리적 주소를 구한 다음 해당 물리적 메모리 위치에 접근하도록 허락합니다. 

반면 논리적 주소 값이 한계 레지스터 값보다 크다면, 다른 프로세스의 주소 영역에 접근하려는 시도이므로 트랩을 발생시켜 해당 프로세스를 강제 종료시킵니다.


2. 메모리 관리와 관련된 용어

동적 로딩

동적 로딩은 여러 프로그램이 동시에 메모리에 올라가서 수행되는 다중 프로그래밍 환경에서 메모리 사용의 효율성을 높이기 위한 방법 중 하나로, 프로세스가 시작될 때, 그 프로세스의 주소 공간 전체를 메모리에 올려 놓는 것이 아니라, 해당 부분이 불릴때 그 부분만을 메모리에 적재하는 방식입니다.

 

예를들어 거의 사용될일 없는 오류 처리 코드가 있다고 가정해봅시다. 이러한 코드가 메모리에 항상 올라와 있다면 메모리 측면에서 효율적이지 않겠죠??

따라서 이러한 부분이 필요할때만 적재함으로서 메모리 이용의 효율성을 증가하는 것입니다!

 

동적 연결

연결(linking)이란 프로그래머가 작성한 소스 코드를 컴파일 하여 생성된 목적 파일(Object File)과 이미 컴파일된 라이브러리 파일을 묶어 하나의 실행파일을 생성하는 과정을 말합니다.

 

동적 연결이란 컴파일을 통해 생성된 목적파일과 라이브러리 파일 사이의 연결을 프로그램의 실행 시점까지 지연시키는 기법을 뜻합니다.

 

동적 연결과 대비되는 정적 연결의 경우엔 프로그래머가 작성한 코드와 라이브러리 코드가 모두 합쳐저 실행파일이 생성됩니다. 따라서 실행 파일 크기가 상대적으로 크며, 동일한 라이브러리 코드 이더라도 각 프로세스가 개별적으로 메모리에 적재해야하므로 물리적 메모리가 낭비되는 단점을 가집니다.

 

반면 동적 연결에서는 실행 파일에 라이브러리 코드가 포함되지 않으며, 프로그램 실행 중 라이브러리 함수가 호출될때, 라이브러리에 대한 연결이 이루어집니다.

이때 실행 파일의 라이브러리 호출 부분에 스텁(stub)이라는 작은 코드를 두고, 라이브러리 호출 시 스텁을 통해 해당 라이브러리가 메모리에 이미 존재하는지 살펴보고, 존재한다면 주소의 메모리 위치에서 직접 참조를 하고, 그렇지 않은 경우엔 디스크에서 동적 라이브러리 파일을 찾아 메모리에 적재한 후 수행하게 되는 것입니다.

 

즉 동적 연결에서는 다수의 프로그램이 공통적으로 사용하는 라이브러리를 메모리에 한번만 적재하므로 메모리 사용의 효율성을 높일 수 있습니다.

 

중첩(Overlays)

중첩이란 프로세스의 주소 공간을 분할 해 실제 필요한 부분만 적재하는 기법입니다. 이러한 중첩은 동적 로딩과 개념적으로 유사하지만, 사용하는 이유에서 차이를 보입니다.

 

동적 로딩은 메모리에 더 많은 프로세스를 동시에 올려놓고 실행하기 위한 용도로 사용하는 반면, 중첩은 단일 프로세스만을 메모리에 올려 놓는 환경에서 메모리 용량 보다 큰 프로세스를 실행하기 위한 어쩔 수 없는 선택이였습니다

 

스와핑(Swapping)

스와핑이란 메모리에 올라온 프로세스의 주소 공간 전체를 디스크의 스왑 영역에 일시적으로 내려놓는 것을 말합니다.

이러한 스왑 영역은 다수 사용자 프로세스를 담을 수 있을 만큼 충분히 큰 저장공간이어야 하고, 어느 정도의 속도가 보장되어야 합니다.

 

스와핑이라는 개념은 프로세스가 종료되어 그 주소 공간을 디스크로 내쫓는 것이 아니라, 특정한 이유로 수행 중인 프로세스의 주소 공간을 일시적으로 메모리에서 디스크로 내려놓는 것을 의미합니다. 이러한 스와핑은 너무 많은 프로그램이 메모리에 동시에 올라와 프로세스 당 할당되는 메모리의 양이 적어져 성능이 떨어지는 경우를 막기 위해, 메모리의 프로세스 수를 조절합니다.

 

일반적으로 스와핑에서 중기 스케줄러가 스왑 아웃을 시킬 프로세스를 선정합니다. 선정 후 해당 프로세스에 대해 현재 메모리에 올라가 있는 주소 공간의 내용을 통째로 디스크 스왑 영역에 스왑 아웃 시키게 됩니다.


3. 물리적 메모리 할당 방식

프로세스를 메모리에 올리는 방식에 따라 연속할당불연속할당으로 나누어 볼 수 있습니다.

 

연속 할당 방식은 각각의 프로세스를  물리적 메모리의 연속적인 공간에 올리는 방식으로, 물리적 메모리를 다수의 분할로 나누어 하나의 분할에 하나의 프로세스가 적재 되도록 합니다. 이때 분할을 관리하는 방식에 따라 고정 분할가변 분할로 나뉩니다.

 

고정 분할 방식물리적 메모리를 정해진 개수 만큼의 영구적인 분할로 나누어 두고, 각 분할에 하나의 프로세스를 적재하는 방식입니다.

이러한 고정 분할 방식은 동시에 메모리에 올릴 수 있는 프로그램 수가 고정되어 있고, 수행 가능한 프로그램의 최대 크기 또한 제한된다는 점에서 가변분할 방식에 비해 융통성이 떨어집니다. 또한 외부 조각과 내부 조각이 발생할 수 있습니다.

 

외부 조각이란 프로그램의 크기보다 분할의 크기가 작은 경우 해당 분할이 비어 있는데도 불구하고 프로그램을 적재하지 못하기 때문에 발생하는 메모리 공간을 의미합니다. 이는 특정 프로그램에 배당된 공간이 아니기에, 만약 외부 조각보다 작은 크기의 프로그램이 도착하면 외부 조각에 적재시킬 수 있습니다.

 

내부 조각은 프로그램의 크기보다 분할의 크기가 큰 경우 해당 분할에 프로그램을 적재하고 남는 메모리 공간을 뜻합니다. 이는 특정 프로그램에 이미 배당된 공간으로 볼 수 있으므로, 내부 조각에 수용할 수 있는 충분히 작은 크기의 프로그램이 있다해도 공간을 활용할 수 없어 메모리가 낭비됩니다.

 

가변 분할 방식은 고정분할과 달리 메모리에 적재되는 프로그램의 크기에 따라 분할의 크기, 개수가 동적으로 변하는 방식을 말합니다.

이러한 가변 분할 방식은 분할의 크기를 프로그램의 크기보다 일부러 크게 할당하지 않기 때문에 내부 조각은 발생하지 않으나, 이미 메모리에 존재하는 프로그램이 종료될 경우 중간에 빈 공간이 발생하게 되며, 이 공간이 새롭게 시작되는 프로그램의 크기보다 작을 경우 외부 조각이 발생할 수 있습니다.

 

가변 분할 방식에서는 프로세스를 메모리에 올릴때 물리적 메모리 내 가용 공간 중 어떤 위치에 올릴 것인지를 다루어야 하는데, 이를 동적 메모리 할당 문제라고 부릅니다.

이러한 동적 메모리 할당 문제를 해결하는 방법으로는 최초적합(first-fit), 최적적합(best-fit), 최악 적합(worst-fit)이 있습니다.

 

최초적합은 메모리를 처음부터 탐색하며, 프로세스를 할당하기에 충분한 크기의 첫번째 빈 공간에 할당하는 방식입니다. 이는 가용 공간을 모두 탐색하는 법이 아니므로 시간적인 측면에서 효율적입니다.

 

최적적합은 프로세스를 할당하기에 가장 적합한 크기의 빈 공간에 할당하는 방식입니다. 즉 모든 빈 공간을 검사하고, 프로세스에 가장 잘 맞는 공간을 선택합니다. 이 경우 모든 가용공간을 탐색해야 하므로 시간적 오버헤드가 발생하고, 다수의 매우 작은 가용 공간들이 생성될 수 있지만, 공간적인 측면에서 효율적입니다.

 

최악적합은 프로세스를 할당하기에 가장 큰 빈 공간에 할당하는 방식입니다. 이 또한 최적적합처럼 모든 가용 공간 리스트를 탐색해야 하는 오버헤드가 발생하고, 상대적으로 더 큰 프로그램을 담을 수 있는 가용공간을 빨리 소진한다는 문제점이 있습니다.

 

가변분할 방식에서 발생하는 외부 조각 문제를 해결하기 위한 방법으로 컴팩션(compaction)이라는 것이 있습니다.

컴팩션은 물리적 메모리 중에서 프로세스에 의해 사용 중인 메모리 영역을 한쪽으로 몰고 가용 공간들을 다른 한쪽으로 모아 하나의 큰 가용 공간을 만드는 방법입니다. 

이는 현재 수행중인 프로세스의 메모리상의 위치를 상당 부분 이동시켜야 하기에 오버헤드가 큰 작업입니다. 따라서 중간에 일부 가용 공간이 발생하더라도 가급적 적은 수의 컴팩션을 하는 것이 중요한데 이는 이론적으로도 복잡할 뿐 아니라, 실행시간 바인딩 방식이 지원되는 환경에서만 수행될 수 있습니다.

 

불연속 할당 기법이란 하나의 프로세스가 물리적 메모리의 여러 위치에 분산되어 올라갈 수 있는 메모리 할당 기법을 말합니다.

이는 하나의 프로그램을 분할하는 기준에 따라 동일한 크기로 나누어 메모리에 올리는 페이징 기법, 크기가 일정하지 않지만 의미 단위로 나누어 올리는 세그먼트 기법, 그리고 세그멘테이션 내에서 동일한 크기의 페이지로 나누어 메모리에 올리는 페이지드 세그멘테이션 기법이 있습니다.


4. 페이징 기법

페이징 기법이란 프로세스의 주소 공간을 동일한 크기의 페이지 단위로 나누어 물리적 메모리의 서로 다른 위치에 페이지들을 저장하는 방식을 말합니다. 페이징 기법에서는 각 프로세스의 주소 공간 전체를 물리적 메모리에 한꺼번에 올릴 필요가 없으며, 일부는 백킹 스토어(주 메모리와 함께 사용되는 보조 저장장치 의미 ex:하드)에, 일부는 물리적 메모리에 혼재시키는 것이 가능합니다.

 

페이징 기법에서는 물리적 메모리를 페이지와 동일한 크기의 프레임(frame)으로 미리 나누어 둡니다. 이는 메모리에 올리는 단위가 동일한 크기의 페이지 단위이므로, 메모리를 같은 크기로 미리 분할해 두어도 빈 프레임이 있으면 어떤 위치든 사용될 수 있기 때문입니다. 즉 앞서 연속 할당 문제에서 발생했던 동적 메모리 할당 문제가 발생하지 않는다는 장점을 가집니다.

 

하지만 주소 변환 절차가 연속 할당 방식에 비해 다소 복잡합니다.

 

왜냐하면 하나의 프로세스라 하더라도 페이지 단위로 물리적 메모리에 올리는 위치가 상이하므로, 바인딩 작업이 페이지 단위로 이루어져야 하기 때문이죠. 즉 특정 프로세스의 몇번째 페이지가 물리적 메모리의 몇번째 프레임에 들어 있다는 페이지 별 주소 변환 정보를 유지하고 있어야 합니다.

따라서 페이징 기법에서는 모든 프로세스가 각각의 주소 변환을 위한 페이지 테이블을 가지게 되며, 이 테이블은 프로세스가 가질 수 있는 페이지의 개수만큼 주소 변환 엔트리를 가지고 있습니다.

 

페이징 기법에서는 프로세스의 주소 공간과 물리적 메모리가 모든 같은 크기의 페이지 단위로 나뉘기에, 빈 공간은 어디든지 활용할 수 있어 외부 조각 문제가 발생하지 않지만, 프로그램의 크기가 항상 페이지 크기의 배수가 된다는 보장이 없기에 내부 조각이 발생할 수 있습니다.

예를들어 페이지 크기가 4KB이고 프로그램의 크기가 10KB라고 한다면, 이 경우 프로그램은 3개의 페이지로 나누어 지며 각각의 크기는 4KB, 4KB, 2KB로 나뉩니다. 이때 2KB 부분의 남은 2KB 부분은 사용되지 않기에 내부 조각화가 발생하는 것이죠!

 

페이징 기법에서는 CPU가 사용하는 논리적 주소를 페이지 번호페이지 오프셋으로 나누어 주소 변환에 사용합니다.

페이지 번호는 각 페이지별 주소 변환 정보를 담고 있는 페이지 테이블 접근 시 인덱스로 사용되고, 이를 바탕으로 특정 프로세스의 물리적 메모리상 시작 위치를 알 수 있습니다. 페이지 오프셋은 하나의 페이지 내에서의 변위를 알려주며,  기준 주소값에 변위를 더함으로써 요청된 논리적 주소에 대응하는 물리적 주소를 얻을 수 있습니다.

 

이러한 주소 변환 과정에서 우선 페이지 테이블에 접근 하고, 실제 데이터에 접근하는 두번의 메모리 접근이 필요합니다. 이와 같은 페이지 테이블 접근 오버헤드를 줄이고 메모리 접근 속도를 향상시키기 위해 TLB 라고 불리는 하드웨어 캐시가 사용되기도 합니다.

TLB에는 빈번히 참조되는 페이지에 대한 주소 변환 정보만을 담게 됩니다.

 

페이지 테이블의 각 항목에는 주소 변환 정보 뿐 아니라 메모리 보호를 위한 보호 비트(protection bit)유효무효 비트(valid-invalid bit)를 두고있습니다. 여기서 보호비트는 각 페이지에 대한 접근 권한의 내용을 담고 있고, 유효무효 비트는 해당 페이지의 내용이 유효한지에 대한 내용을 담고있습니다.

 

페이지 테이블은 모든 페이지 항목에 대한 정보를 담고 있기에, 메모리 공간의 낭비가 심할 수 있습니다. 이러한 문제를 해결하기 위한 대안으로 역페이징 기법이 사용될 수 있습니다. 이는 논리적 주소에 대해 페이지 테이블을 만드는 것이 아니라, 물리적 주소에 대해 페이지 테이블을 만들기에 각 프로세스마다 페이지 테이블을 두지 않고, 시스템 전체에 페이지 테이블을 하나만 두는 방법을 말합니다.


5. 세그멘테이션(Segmentation)

세그멘테이션이란 프로세스의 주소 공간을 의미 단위의 세그먼트로 나누어 물리적 메모리에 올리는 기법입니다.

 

하나의 프로세스를 구성하는 주소 공간은 일반적으로 코드, 데이터, 스택 등의 의미 있는 단위로 구성되며, 세그먼트는 이와 같은 주소 공간을 기능 단위 또는 의미로 단위로 나눈것을 말합니다. 예를들어 코드,데이터, 스택 등의 기능 단위로 세그먼트를 정의하거나, 함수 하나하나를 각각의 세그먼트로 정의할 수도 있는 것입니다!

 

세그먼테이션 방식은 페이징과 같이 특정 크기 단위로 나눈것이 아니라, 의미를 가진 논리적인 단위로 나눈것이기 때문에 그 크기가 균일하지 않다는 점입니다. 

연속 할당 방식처럼 프로세스의 주소 공간이 통째로 메모리에 적재되는 것이 아니라, 나누어져 각각 메모리에 적재된다는 점에서 페이징과 유사한점도 있습니다.

 

세그멘테이션에서의 주소 변환은 세그먼트 번호오프셋 두가지 정보를 사용하여 이루어집니다. 여기서 세그먼트 번호는 세그먼트 테이블의 인덱스이고, 이 인덱스에 대한 엔트리에는 세그먼트의 시작 주소세그먼트 길이가 저장되어있습니다. 오프셋은 세그먼트 시작 주소로 부터 얼마나 떨어져 있는지 나타내는 상대적인 위치를 가리킵니다.

 

우선 MMU는 세그먼트 번호를 세그먼트 테이블의 인덱스로 사용하여 세그먼트의 시작 주소를 찾습니다. 그리고 오프셋을 시작 주소에 더하여 실제 물리 주소를 찾게 됩니다. 하지만 만약 논리적 주소의 오프셋 값이 세그먼트의 길이를 넘어선다면 이는 잘못된 주소 참조이기에, 시스템은 에외상황을 발생시켜 해당 메모리 위치에 대한 접근을 봉쇄하게 됩니다.

 

페이징 기법과 마찬가지로 세그먼테이션 기법에서도 세그먼트 테이블의 각 항목에 보호비트유효비트를 둡니다. 이때 보호 비트는 각 세그먼트에 대해 읽기/쓰기/실행 등의 궎이 있는지 나타내며, 유효비트는 각 세그먼트의 주소 변환 정보가 유효한지, 즉 해당 세그먼트가 현재 물리적 메모리에 적재되어 있는지를 나타냅니다.

 

세그먼트는 의미 단위로 나누어져 있기 때문에 공유와 보안의 측면에서 페이징 기법에 비해 훨씬 효과적입니다. 예를 들어 페이지 기법에서 동일한 크기로 주소 공간을 나누다 보면 공유하려는 코드와 사유 데이터 영역이 동일 페이지에 공존하는 경우가 발생할 수 있습니다.

반면 세그멘테이션 기법에서는 이러현 현상이 발생하지 않으므로 공유나 보안처럼 의미 있는 단위에 대해 수행하는 업무에서는 페이징 기법보다 세그멘테이션 기법이 장점을 가집니다.

 

한편 세그멘테이션 기법에서는 프로그램을 의미 단위로 나누기 떄문에 세그먼트의 길이가 균일하지 않습니다. 따라서 물리적 메모리 관리에서 외부 조각이 발생하게 되며, 세그먼트를 어느 가용 공간에 할당할 것인지 결정하는 문제가 발생합니다.

이는 앞서 살펴본 연속 할당 메모리 관리의 가변분할 방식에서의 문제와 동일한 범주의 문제라 할 수 있기에, 최초 적합 및 최적 적합과 같은 방식으로 이를 해결할 수 있습니다.


6. 페이지드 세그멘테이션

페이지드 세그멘테이션 방법은 페이징과 세그멘테이션이라는 두가지 메모리 관리 기법을 결합한 것입니다.

 

이 기법은 세그먼테이션 기법과 마찬가지로 프로세스를 여러 개의 세그먼트로 나누지만 이때 세그먼트 들이 임의의 길이를 가질 수 있는 것이 아니라 반드시 동일한 크기 페이지들의 집합으로 구성되어야 합니다. 즉 페이지드 세그먼테이션 기법에서는 하나의 세그먼트 크기를 페이지 크기의 배수가 되도록 함으로써 세그먼테이션 기법에서 발생하는 외부 조각 문제점을 해결하는 동시에 세그먼트 단위로 프로세스 간의 공유나 프로세스 내의 접근권한 보호가 이루어지도록 함으로써 페이징 기법의 약점을 해소합니다.

 

이러한 페이지 세그먼테이션 기법에서는 주소 변환을 위해 외부의 세그먼트 테이블과 내부의 페이지 테이블 총 2개를 이용합니다.


개발자 준비생이 대학 강의를 듣고 정리한 내용입니다

혹시 틀린 내용이 있다면 댓글 부탁드리겠습니다!! 곧바로 수정하겠습니다