S E P H ' S
[OS] 주기억장치 관리 본문
메모리는 CPU 자원만큼 컴퓨터를 사용하는데 매우 중요한 자원 중 하나이다. 이전에는 운영체제에서 CPU 자원을 관리하는 프로세스 관리에 대해 살펴보았고, 지금부터는 메인 메모리를 관리하는 기능에 대해 살펴볼 것이다. 특히 과거에는 메모리가 매우 비싼 자원이었고 크기 또한 작아 운영체제에서 메모리에 대한 관리가 지금보다 더 중요했었다.
현재는 과거보다 훨씬 큰 메모리를 저렴하게 사용하지만, 지금도 메모리는 부족하다. 메모리가 커진 만큼 프로그램의 크기와 처리하는 데이터의 크기는 그보다 더욱 빠른 속도로 커져왔다. 그러므로 현재에도 여전히 메모리를 최대한 효율적으로 사용하기 위해 여러 방법들이 연구되고 있고, 운영체제 기능에서도 매우 중요한 위치를 차지하고 있다.
1. 메모리에 프로그램 할당하기
메모리는 기본적으로 주소(Adress)와 데이터(Data)로 구성되어 있다.
CPU와 메모리는 양방향으로 위 그림과 같이 주고 받는다. CPU는 주소를 가지고 메인 메모리에 요청하거나 해당 주소에 대한 계산결과를 저장하고 메모리는 요구하는 주소에 저장된 데이터를 CPU에 전달한다.
프로그램을 빌드하는 과정은 소스파일, 목적파일, 실행파일 순서로 생성된다.
- 소스파일(Source file) : 고수준언어 또는 어셈블리어
- 목적파일(Object file) : 컴파일 또는 어셈블 결과
- 실행파일(Executable file) : 링크 결과
위 그림은 프로그램이 만들어지는 과정을 그림으로 표현한 것이다.
- 소스파일은 컴파일러에 의해 컴파일 수행 결과로 목적파일을 생성한다. (어셈블리어는 어셈블러가 어셈블을 수행하여 기계어로 변환한다.) 프로그래밍을 하면서 외부의 라이브러리를 사용할 때가 빈번한데, 컴파일 단계에서는 이를 추가하지 않으므로 목적파일에는 이에 대한 정보가 없다.
- 링크 단계에서 하드디스크에서 프로그래머가 추가한 라이브러리를 찾아 정보를 추가하여, 실행 파일을 만든다. (링크단계는 링커가 수행한다.) 이 프로그램을 실행하면 로더에 의해 메인 메모리에 할당된다.
그리고 생성된 프로그램은 code, data, stack 영역으로 나뉘어져 있다. 단순히 생성된 프로그램에는 code와 data 영역만 존재하며 메모리에 적재될때는 실제로 실행을 해야하므로 stack 영역에 추가되어야 한다.
프로그램을 실제로 메모리에 올리기 위해서는 좀 더 복잡한 과정이 필요하다. 먼저 이 프로그램은 메모리에 몇 번지에 할당될까? 운영체제가 없다면, 프로그래머가 이를 직접 처리해야한다. 하지만 운영체제 덕분에 프로그래머는 이를 신경쓸 필요가 없다. 그래서 프로그래밍을 할 때 주소를 사용하는 경우가 있는데, 프로그램이 메모리에 올라가는 주소를 고려하지 않고 프로그래밍이 가능한 것이다. (고수준 언어에서는 직접 주소를 다루지 않는 경우가 많다.)
또한, 다중 프로그래밍 환경에서는 여러 프로그램이 메모리에 올라가고 내려가고를 반복하기 때문에, 한 프로그램은 고정적인 공간을 사용할 수 없다. 이러한 여러 고려 사항을 해결해주는 것이 전에도 살펴봤던 MMU 이다. 그리고 MMU에는 프로그램이 메모리에 할당될 때마다 다른 주소공간을 사용하기 때문에 재배치 레지스터(Relocation register) 가 별도로 존재한다.
위 그림은 MMU 의 모습이다. 프로그램은 메인 메모리에 해당 주소 사용여부를 생각하지 않고 주소를 사용한다. 만약 해당 프로그램이 사용하는 시작 주소가 0번지라고 할 때, 실제 메인 메모리에서는 할당되는 주소가 유동적이기 때문에 0번지라는 주소를 실제 할당된 주소로 변경해주어야 한다. 이때 재배치 레지스터를 이용한다.
만약 프로그램이 메인 메모리 500번지에 할당되어 재배치 레지스터값이 500으로 설정되었다면, CPU 에서 프로그램의 0번지를 사용할 때 MMU 를 통과하면 재배치 레지스터에 의해 500번지로 변경된다. 그 결과 CPU는 0번지를 사용하는 것으로 알고 있지만, 실제 메모리 내에서는 MMU 에 이해 500번지를 사용하고 있는 것이다.
MMU(Memory Management Unit)의 기능을 살펴보면, 이전에 메모리 보호를 위해 base와 limit 레지스터가 있었다. 이는 CPU에서 주소를 사용하는데 이 주소가 해당 프로그램의 base나 limit 범위를 벗어나면 인터럽트가 발생하여 그 프로그램을 강제로 종료시킨다.
MMU는 이 기능 이외에도 재배치 레지스터를 이용해서 프로그램이 어느 주소를 사용하더라도 실제 메인 메모리에 할당된 주소를 찾아갈 수 있도록 address translation 동작을 수행한다.
MMU 에 의해 위 그림과 같이 주소는 CPU에서 사용하는 논리주소(Logical Address), 메모리가 사용하는 물리주소(Physical Address) 로 나뉜다.
2. 메모리 낭비 방지
운영체제는 메모리를 효율적으로 사용하기 위해 메모리 공간을 낭비하지 않는 것이 중요하다.
2.1 동적 적재 (Dynamic Loading)
동적 적재는 프로그램이 실행하는데 반드시 필요한 루틴/데이터만 적재하는 것이다. 프로그램의 전체 코드에서 모든 루틴이 다 사용되는 것은 아니다. 대표적으로 오류 처리 구문이 그러하다. 오류 처리 구문은 if 문과 같이 오류가 발생할 때만 해당 내부 코드가 실행되는 것을 말한다. 그러므로 동적 적재를 수행하면 프로그램의 실제 메모리에는 이러한 오류 구문을 제외하고 적재한다. 이러한 상태에서 실행하다가 오류가 발생하면 그 때 해당 오류 구문을 찾아 메모리에 올린다.
데이터도 마찬가지이다. 모든 데이터가 반드시 사용되는 것은 아니기 때문에, 특히 배열과 클래스의 경우는 필요한 부분만 메모리에 올려두고, 실행 도중 필요할 때마다 해당 부분을 찾아 메모리에 올려준다.
반대로, 모든 루틴과 데이터를 적재하는 것을 정적 적재(static loading)이라고 한다. 현대 운영체제는 대부분 동적 적재를 사용한다.
2.2 동적 연결 (Dynamic Linking)
동적 연결은 여러 프로그램에 공통으로 사용되는 라이브러리를 중복으로 메모리에 올리는 것이 아닌 하나만 올리도록 하는 것이다.
예를 들어 아래와 같은 코드의 P1, P2 프로세스가 있다고 하자.
// P1
int a = 1;
int b = 2;
printf("%d\n", a + b);
// P2
int a = 1;
int b = 2;
printf("%d\n", a * b);
이 두 소스 파일을 컴파일하면 목적파일이 생성되고, 여기서 사용된 라이브러리를 링크하여 실행파일을 만든 다음 메모리에 적재한다. 두 프로세스가 적재됐을 때, printf()를 사용하는 라이브러리는 메모리에 중복되어서 적재되어있다.
이와 같이 똑같은 라이브러리를 사용하는 프로그램은 흔히 볼 수 있다. 이러한 라이브러리를 하나만 메모리에 올린 후, 이를 사용하는 프로그램이 하나의 메모리에 접근하도록 하면 메모리 낭비를 줄일 수 있다.
동적 연결은 같은 라이브러리가 중복으로 메모리에 올라가는 것을 방지하기 위해 프로그램이 메모리에 적재된 후에 링크(link) 작업을 수행한다. 기존에는 실행 파일이 만들어지기 전에 링크 과정을 수행하였는데, 이를 정적 연결이라고 한다.
위 그림은 예제에서 살펴본 P1, P2 프로세스가 동적 연결을 통해 공통 라이브러리를 연결한 모습이다. 이러한 라이브러리를 Linux에서는 공유 라이브러리(Shared Library), Windows에서는 동적 연결 라이브러리(Dynamic Linking Library, DLL) 라고 부른다.
2.3 Swapping
Swapping은 메모리에 적재되어 있는 프로세스 중에서 오랫동안 사용하지 않은 프로세스를 프로세스 이미지 형태로 만든 후 하드디스크(Backing Store)에 내려보낸다. 메모리에서 Backing Store로 가는 것을 swap-out, 다시 Backing Store 에서 메모리로 가는 것을 swap-in이라고 한다.
여기서 프로세스 이미지는 해당 프로그램이 메모리에 적재된 후 실행되면서 데이터를 추가하거나 변경하는 등의 과정을 거치는데, 현재 데이터의 상태를 프로세스 이미지라고 부른다. 그러므로 이는 단순히, 하드디스크에 존재하는 프로그램(exe파일)과는 전혀 다른 데이터이므로 따로 저장해야한다. 이와 같은 swapping 과정으로 인한 프로세스 이미지를 저장하기 위해 하드디스크 일부분을 분리하여 사용하는데 이를 Backing Store 혹은 Swap Device라고 부른다.
Backing Store의 크기는 대략 메인 메모리 크기 정도로 예상할 수 있다. 메모리의 모든 프로세스가 쫓겨난다고 해도 메인 메모리 크기를 넘지 않기 때문이다. 메인 메모리 크기가 크지 않은 PC나 스마트폰은 하드디스크 일부를 Backing Store로 사용하지만 메모리 크기가 크다면 따로 하드디스크 자체를 Backing Store로 사용하는 경우도 있다.
Swap-out된 프로세스는 다시 swap-in을 할때, 이전의 메모리 주소 공간이 아닌 새로운 주소 공간으로 갈 수도 있다. 이는 해당 프로세스가 backing store에 있는 동안 다른 프로세스가 해당 주소 공간을 사용할 수도 있기 때문이다. 하지만 이는 MMU의 재배치 레지스터로 인해 어디에 적재되었는지 상관없이 정상적으로 실행될 수 있다.
현재는 프로세스의 크기가 커지고, 하드디스크는 메인 메모리보다 속도면에서 매우 느리므로 swapping 동작의 오버헤드는 크다고 볼 수 있다. 하지만 이로 인해 얻는 이득이 더 많으므로 대부분 운영체제는 이를 사용하고 있고, 속도가 중요한 서버 컴퓨터나 슈퍼 컴퓨터는 backing store를 하드디스크가 아닌 좀더 빠른 저장 장치를 사용하기도 한다.
3. 연속 메모리 할당(Contiguous Memory Allocation)
과거에는 메모리에 프로세스가 하나만 올라가는 형태였다. 하지만 현재는 메모리에 여러 프로세스가 할당되는 다중 프로그래밍 환경이다.
부팅 직후에 메모리 상태를 살펴보면 운영체제만 할당되어 있고 비어있는 상태이다. 이러한 빈 공간을 hole이라고 부른다. 즉, 부팅 직후에는 운영체제와 big single hole 만 있는 상태이다. 시간이 지나면서 프로세스가 생성되고 종료됨을 반복하면 여러 곳에 서로 다른 크기의 hole이 존재할 것이다. 이러한 상태를 scatterd holes라고 한다.
위 그림은 부팅 직후 상태에서 시간이 경과하며 프로세스들이 생성, 종료를 반복한 후의 상태이다. 이와 같이 hole 들이 불연속하게 흩어져 있는 상태를 메모리 단편화(Memory Fragmentaion)라고 한다.
메모리 단편화로 인해 여러 곳에 hole 이 흩어져 있는 상태에서 하나의 프로세스가 메모리에 할당되려하면 문제가 발생할 수 있다. 예를 들어 3개의 hole이 있고 각 크기가 50byte, 50byte, 80byte라고 하자. 그런데 할당하려는 프로세스 크기가 150byte이다. 각 hole들을 하나로 합치면 180byte로 이 프로세스를 할당할 수 있는 크기이나 실제로는 나눠져 있으므로 할당되지 못한다. 이러한 현상을 외부 단편화(External fragmentaion)이라고 한다.
연속 메모리 할당 방식
외부 단편화의 해결방법을 보기 전에 연속 메모리 할당 방식을 먼저 살펴보자. 연속 메모리 할당 방식에는 3가지가 있다. First-fit, Best-fit, Worst-fit이 있다.
- First-fit (최초 적합) : 최초 적합은 할당할 프로세스 크기보다 크거나 같은 hole을 탐색한 순서중에서 가장 먼저 찾은 hole에 프로세스를 할당하는 것.
- Best-fit (최적 적합) : 최적 적합은 할당할 프로세스 크기와 hole 크기의 차이가 가장 작은 hole에 프로세스를 할당하는 것(hole 크기는 프로세스 크기보다 반드시 커야함)
- Worst-fit (최악 적합) : 최적 적합과 반대로, 할당할 프로세스 크기와 hole 크기 차이가 가장 큰 hole에 프로세스를 할당하는 것이다.
Hole은 100KB, 500KB, 600KB, 300KB, 200KB 총 5개가 있고 프로세스는 P1, P2, P3, P4 총 4개로 각각 212KB, 417KB, 112KB, 426KB 이다.
- First-fit
- Best-fit
- Worst-fit
각 3가지 방식대로 프로세스를 할당했다. 예제 결과를 보면 최적 적합은 4개 프로세스를 모두 할당할 수 있었지만 나머지 2개의 방법은 마지막 P4를 할당하지 못했다. 모든 hole을 합치면 P4를 할당할 수 있으나, hole은 나눠져 있어 할당할 수 없었다. (외부 단편화)
각 할당 방식의 일반적인 성능을 비교해보면 속도면에서는 최초적합이 가장 빠르다. 메모리 이용률 면에서는 최초적합, 최적적합이 비슷한 성능을 낸다. 하지만 여러 실험을 통해 최적적합을 사용하더라도 외부 단편화로 인해 전체 메모리의 1/3을 낭비한다고 한다. 이는 거의 사용이 불가능한 수준이다.
이를 해결하는 방법 중 하나는 Compaction 이다. compaction은 여러 곳에 흩어진 hole들을 하나로 합치는 것이다. 하지만 hole을 옮기는 오버헤드가 너무 크고 어느 hole을 옮겨야 빠르게 합칠 수 있는지에 대한 최적 알고리즘이 존재하지 않는 큰 단점이 존재한다.
'CS > OS' 카테고리의 다른 글
[OS] 세그멘테이션 (0) | 2023.11.29 |
---|---|
[OS] 페이징 (1) | 2023.11.29 |
[OS] 모니터 (1) | 2023.11.29 |
[OS] 프로세스 동기화 3 (3) | 2023.11.27 |
[OS] 프로세스 동기화 2 (1) | 2023.11.27 |