메모리 주소 연계 및 동적 적재
1. 주소 연계 (Addressing Binding)
프로그램은 원래 이진 실행 파일 형태로 디스크에 저장된다. 이 프로그램이 실행되기 위해서는 주 메모리로 이동하여 프로세스가 되어야 한다. 사용하는 메모리 관리 기법이 무엇이냐에 따라 프로세스는 실행하는 동안 디스크와 주 메모리 사이를 왕래 할 수 있다.
디스크에서 주 메모리로 들어오기를 기다리고 있는 프로세스들의 집합은 이른바 입력 큐(input queue)를 형성한다. 이 큐에서 하나의 프로세스를 선택해서 메모리로 적재한 후 실행하고 이 프로세스는 실행하는 동안 메모리에서 명령어와 데이터에 접근한다. 언젠가 이 프로세스가 종료되면, 이 프로세스가 사용했던 메모리 공간이 가용 공간이 되며 다른 프로세스를 위해 사용된다.
대부분의 시스템은 사용자 프로세스가 메모리 내 어느 부분으로도 적재될 수 있도록 지원한다. 사용자 프로세스의 주소가 00000번지 부터 시작된다고 해서 이 프로그램이 메모리의 00000번지부터 적재되어야 할 필요는 없다. 이러한 접근 방법은 사용자 프로그램이 사용하는 주소들에게 영향을 준다. 아래의 이미지에서 볼 수 있듯이, 대부분의 경우 사용자 프로그램은 여러 단계를 거쳐 실행되기 때문에 이 단계를 거치는 동안 주소들은 여러 가지 다른 표현 방식을 거치게 된다.
원시 프로그램(source program)에서 주소는 숫자가 아닌 심벌 형태(x:= x + 1과 같은 형태)로 표현된다. 컴파일러는 이 심벌 주소를 재배치 가능 주소(예를 들면 "이 모듈의 첫 번째 바이트로부터 열네 번째 바이트 주소")로 연계시키고, 추후에 연결 편집기(linkage editor)나 적재기(loader)가 재배치 가능 주소를 절대 주소(예를 들면 74014번지)로 연계 시킨다.
전통적으로, 메모리 주소 공간에서 명령어와 데이터의 연계는 그 연계가 이루어지는 시점에 따라 다음과 같이 구분된다.
- 컴파일 시간(compile time) 연계
만일 프로세스가 메모리 내에 들어갈 위치를 컴파일 시간에 미리 알 수 있으면 컴파일러는 절대 코드를 생성할 수 있다. 예를 들면, 사용자 프로세스가 N번지로부터 시작한다는 것을 미리 알 수 있으면 컴파일러는 번역할 코드를 그 위치에서 시작해 나간다. 그러나, 만일 이 위치가 변경되어야 한다면, 이 코드는 다시 컴파일되어야 한다. MS-DOS의 .COM 양식 프로그램은 컴파일 시간에 연계하는 절대 코드의 예이다.
- 적재 시간(load time) 연계
만일 프로세스가 메모리 내 어디로 적재 될지를 컴파일 시점에 알지 못하면 컴파일러는 일단 이진 코드를 재배치 가능 코드로 만들어야 한다. 이 경우에, 심벌과 진짜 번지수와의 연계는 프로그램이 주 메모리로 실제 적재되는 시간에 이루어지게 된다. 이렇게 만들어진 재배치 가능 코드는 시작 주소가 변경되면 아무 때나 사용자 코드를 다시 적재만 하면 된다.
- 실행 시간(execution time) 연계
만약 프로세스가 실행하는 중간에 메모리 내 한 세그먼트로부터 다른 세그먼트로 옮겨질 수 있다면 "연계가 실행 시간까지 허용되었다"라고 한다. 이러한 일이 가능하려면 특별한 하드웨어를 이용해야 한다.
2. 논리 대 물리 주소 공간 (Logical versus Physical Address Space)
CPU가 생성하는 주소를 일반적으로 논리 주소(logical address)라 한다. 반면에, 메모리가 취급하게 되는 주소 (즉, 메모리 주소 레지스터에 주어지는 주소)는 일반적으로 물리 주소(physical address)라고 한다.
앞의 연계 분류에서 언급했던 컴파일과 적재 시 연계 기법의 경우에는 논리 주소와 물리 주소가 같다. 그러나, 실행 시간 바인딩 기법에서는 논리 주소와 물리 주소가 다르다. 이러한 경우에는 논리 주소를 가상 주소(virtual address)라고 한다.
프로그램에 의해 생성된 모든 논리 주소 집합을 논리 주소 공간 (logical address space)이라 하며, 이 논리 주소와 상응하는 모든 물리 주소 집합을 물리 주소 공간(physical address space)이라 한다.
프로그램의 실행 중에는 이와 같은 논리 주소를 물리 주소로 변환해야 하는데, 이 변환(mapping) 작업은 하드웨어 장치인 메모리 관리기(MMU - Memory Management Unit)에 의해 실행된다. 이 메모리 관리기를 통한 다양한 메모리 변환 기법들이 있는데, 그 중에서도 가장 간단한 기법이 바로 재배치 레지스터를 이용하는 기법이다. 재배치 레지스터 기법에 대해서 언급하기 전에, 기준 레지스터에 대한 개념을 알아보도록 하자.
각 프로세스가 실행 될 때, 하나의 프로세스가 다른 프로세스의 주소 공간으로 접근하지 못하게 하기 위해서 "기준 레지스터(base register)"와 "상한(limit)"이라고 불리는 두 개의 레지스터를 사용한다. 기준 레지스터는 가장 작은 합법적인 물리 메모리 주소의 값을 저장하고, 상한 레지스터는 해당 프로세스에게 주어진 영역의 크기를 저장한다. 예를 들어, 만약 기준 레즈스터의 값이 300040이고, 상한 레지스터의 값이 120900이라면, 이 프로세스는 300040 부터 420940까지의 모든 주소를 접근할 수 있다.
메모리 공간의 보호는 CPU 하드웨어가 사용자 모드에서 만들어진 모든 주소와 레지스터를 비교함으로써 이루어진다. 사용자 모드에서 실행되는 프로그램에 의해 운영체제의 메모리 공간이나 다른 사용자 프로그램의 메모리 공간으로 접근이 일어나면, 운영체제는 치명적인 에러로 간주하고 트랩(trap)을 발생시킨다. 아래 이미지가 나타내는 것이 바로 이 메모리 공간 보호 과정이다.
위에서 언급한 기준 레지스터를 재배치 레지스터로서 이용하는 메모리 변환 기법이 바로 재배치 레지스터 기법이다. 이 기법은 기준 레지스터 속에 들어있는 값을 재배치 기준 값으로 사용한다. 즉, 논리 주소가 메모리로 보내질 때마다 사용자 프로세스가 기준 레지스터 속에 들어있는 값과 더하는 것이다. 다음 이미지를 통해서 예를 들어보도록 하자.
위의 경우, 재배치 레지스터의 값은 14000이다. 그리고, CPU로부터 346이라는 논리 주소 값이 MMU로 보내지게 되면, 메모리 관리 장치는 이 346에 재배치 레지스터 값인 14000을 더한다. 그 결과값인 14346이 바로 물리 주소 값이 되는 것이다.
사용자 프로그램은 실제적인 물리 주소를 결코 알 수 없다. 사용자 프로그램은 346번지에 대한 포인터를 생성해서 숫자 346을 가지고 저장, 연산, 다른 주소들과 비교하는 등 모든 일을 할 수 있다. 그러나, 일단 그것이 메모리 주소로 사용될 때에는(간접 적재 및 저장) 기준 레지스터를 기준으로 재배치된다.
이제는 두 가지의 주소, 즉 논리 주소(0에서 max까지의 범위)와 물리 주소(기준값 N을 기준으로 N+0에서 N + max 까지 범위)가 있다는 사실에 유의하여야 한다. 사용자 프로세스는 단지 논리 주소만을 만들어내므로 주소가 0에서 max 위치까지만 있다고 생각할 것이다. 이들 논리 주소는 사용되기 전에 물리 주소로 변환되어야만 한다. 별도의 물리 주소 공간에 연계되는 논리 주소 공간의 개념은 올바른 메모리 관리의 핵심이다.
3. 동적 적재 (Dynamic Loading)
지금까지의 설명에서는 프로세스가 실행되기 위해 그 프로세스 전체가 미리 메모리에 적재되어야 했다. 이 경우, 프로세스의 크기는 메모리의 크기보다 커서는 안 된다. 메모리 공간을 보다 효율적으로 이용하기 위해서는 동적 적재(dynamic loading)를 해야 한다. 동적 적재에서 각 루틴은 실제 호출되기 전까지는 메모리에 적재되지 않고 재배치 가능한 상태로 디스크에서 대기하고 있다.
먼저 main 프로그램이 메모리에 적재되어 실행된다. 이 루틴이 다른 루틴을 호출하게 되면, 호출된 그 루틴이 이미 메모리에 적재되어 있는지를 조사한다. 만약 적재되어 있지 않다면, 재배치 가능 연결 적재기(relocatable linking loader)가 불려져 요구된 루틴을 메모리로 적재하고, 이러한 변경사항을 프로그램의 주소 테이블에 기록해 둔다. 그 후, CPU 제어는 중단 되었던 루틴으로 보내진다.
동적 적재의 장점은 사용되지 않는 루틴들의 경우, 절대로 미리 적재되지 않는다는 것이다. 이러한 구조는 오류 처리 루틴과 같이 아주 간혹 발생하면서도, 많은 양의 코드를 필요로 하는 경우에 특히 유용하다.
동적 적재는 운영체제로부터 특별한 지원을 필요로 하지 않는다. 사용자 자신이 프로그램의 설계를 책임져야 한다. 운영체제는 동적 적재를 구현하는 라이브러리 루틴을 제공하여 프로그래머에게 도움을 줄 수 있다.
자바 언어에서 객체를 만들 때 사용되는 방식이 바로 동적 적재(동적 로딩) 방식이다. 실행 시에 모든 클래스가 적재되지 않아도, 필요한 시점(객체를 만드는 시점)에 메모리로 로딩해서 사용하는 방식이다.
4. 동적 연결 및 공유 라이브러리 (Dynamic Linking & Shared Library)
동적 연결 개념은 동적 적재 개념과 유사하다. 동적 적재에서는 적재가 실행 시까지 미루어지지만, 동적 연결에서는 연결(linking)이 실행 시기까지 미루어지는 것이다. 동적 연결은 주로 시스템 라이브러리에서 사용된다. 만일, 이 방식이 없다면, 모든 프로그램들은 그들의 이진 프로그램 이미지 내에 시스템 라이브러리의 복사본 또는 적어도 참조되는 루틴의 복사본을 가지고 있어야만 한다. 이렇게 라이브러리를 부르는 프로그램마다 그 라이브러리를 한 부씩 가지고 있으면, 디스크와 주 메모리의 낭비가 심해진다.
동적 연결에서는 라이브러리를 호출하는 곳마다 스텁(stub)이 생긴다. 이 스텁은 메모리에 존재하는 라이브러리를 찾는 방법 또는 메모리에 없을 경우 라이브러리를 적재하는 방법을 알려주는 작은 코드 조각이다. 이 스텁이 실행될 때 스텁은 필요한 라이브버리 루틴이 이미 메모리에 존재하는가를 검사한다. 또한, 없을 경우에는 루틴을 메모리로 적재한다. 둘 중의 어느 방식으로 하든 스텁은 라이브러리 루틴의 주소를 알아내게 되고, 자신을 루틴의 주소로 대체하고 루틴을 실행한다. 다음번에 그 부분이 호출되면 이와 같은 동적 연결을 할 필요 없이 직접 라이브러리 루틴을 실행하면 된다. 이러한 구조에서는 printf() 같은 라이브러리를 10개의 프로세스가 사용한다고 하더라도 printf() 라이브러리 코드는 한 개만 적재되어 있으면 된다.
이러한 동적 연결은 라이브러리 루틴을 바꿀 때 특히 유용하다. 라이브러리는 어느 때나 새로운 버전으로 교체될 수 있고, 그렇게 되면 그 라이브러리를 사용하는 모든 프로그램은 자동적으로 새로운 라이브러리를 사용하게 될 것이다. 동적 연결이 없었다면, 새 라이브러리를 이용하기 위해 모든 프로그램은 새로 연결되어야 된다.
프로그램이 다른 라이브러리 버전을 실행하는 것을 방지하기 위해 버전에 대한 정보가 프로그램과 라이브러리 내에 각각 포함되어야 한다. 여러 버전의 라이브러리들이 시스템에 존재할 수도 있기 때문에, 각 프로그램은 라이브러리의 어느 버전을 사용해야 할지를 가려내기 위해 이러한 버전 정보를 이용한다. 소폭의 수정을 가한다면 동일한 버전 번호를 유지할 수 있지만, 대폭적인 수정을 가한다면, 버전 번호를 바꾸어야 한다. 이러한 스스템을 공유 라이브러리(shared library)라고 한다.
동적 적재와는 달리 동적 연결은 일반적으로 운영체제의 도움이 필요하다. 메모리에 있는 프로세스들이 서로로부터 보호된다면 운영체제만이 루틴이 어느 프로세스의 주소 공간에 존재하는지를 검사해 줄 수 있거나, 여러 프로세스들로 하여금 같은 메모리 주소를 공유하도록 해줄 수 있다.
'OS > 메모리 관리' 카테고리의 다른 글
세그멘테이션 (Segmentation) (0) | 2018.10.24 |
---|---|
페이지 테이블의 구조 (Structure of the Page Table) (0) | 2018.10.13 |
페이징 (0) | 2018.10.13 |
연속 메모리 할당 (Contiguous Memory Allocation) (0) | 2018.10.08 |
스와핑(swapping) (0) | 2018.10.07 |