Main Memory
Main Memory
운영체제에서 메인 메모리는 CPU가 직접 접근할 수 있는 메모리이다. 프로그램이 실행될 때 해당 프로그램은 메모리에 복사되어 적재되며, 이를 프로세스라고 한다. CPU는 프로그램 카운터가 지시하는 대로 연산을 수행한 후, 결과 데이터를 메인 메모리에 저장하거나 필요한 데이터를 메모리에서 가져온다.
기본적인 메모리 구성은 각각의 프로세스에 별도의 메모리 공간이 할당되는 방식이다.
개별적인 메모리 공간을 분리하기 위해, 특정 프로세스만 접근할 수 있는 합법적인 메모리 주소 영역을 설정하고 프로세스가 합법적인 영역만 접근하도록 하는 것이 중요하다. 이를 위해 기본 레지스터와 상한 레지스터가 사용된다.
기본 레지스터(base)는 프로세스가 접근할 수 있는 메모리 시작 주소를 지정하며, 상한 레지스터(base + limit)는 프로세스가 접근할 수 있는 메모리 영역의 크기를 지정한다. 이러한 레지스터를 통해 각 프로세스는 자신에게 할당된 메모리 영역만 접근할 수 있게 된다.
주소의 할당(Address Binding)
프로그램이 실행되기 위해서는 메모리에 적재 된 후 프로세스 컨택스트1 내에 배치되어야 한다. 유저 프로그램은 실행되기 전 여러 단계를 거치며, 이 과정에서 주소는 여러 다른 방식으로 표시될 수 있다.
- 기호 주소(Symbolic Address)
- 원시 프로그램에서는 주소가 일반적으로 기호 형태로 표현됨
- ex) 변수명, 함수명 등
- 재배치 주소(Relocatable Address): 컴파일러는 기호 주소를 재배치 가능한 주소로 변환
- 절대 주소(Absolute Address)
- 링커(Linker)나 로더(Loader)는 재배치 가능한 주소를 메모리 내 절대 주소로 변환하여 바인딩
- ex) 실행 파일이 메모리에 로드될 때의 실제 메모리 주소
이 과정을 통해 프로그램이 메모리에 적재되고 실행할 준비를 갖추게 된다. 이러한 주소 변환 과정은 프로그램이 올바르게 실행되도록 하고, 메모리 자원을 효율적으로 관리할 수 있도록 한다.
주소의 할당(Address Binding)의 구분
메모리 주소 공간에서 명령어와 데이터 바인딩은 바인딩이 이루어지는 시점에 따라 구분한다. 이를 알기 전에 논리 주소와 물리 주소에 대해 알아야 한다. 논리 주소(Logical Address)는 CPU에서 생성한 가상 주소로, 프로그램 코드 내에서 사용되는 주소이다. 물리 주소(Physical Address)는 메모리 장치가 보내는 실제 메모리 주소이다.
컴파일 시간 바인딩
컴파일 시간 바인딩은 프로그램이 컴파일될 때 메모리의 특정 위치에 상주할 것을 가정하고, 그 위치를 기준으로 절대 주소를 생성한다. 이 경우, 컴파일러는 프로그램의 모든 변수와 명령어에 대해 실제 메모리 주소를 할당한다. 따라서 컴파일러가 생성하는 주소는 이미 물리 주소이기 때문에, 논리 주소와 물리 주소가 동일하다. 예를 들어, 프로그램이 항상 메모리의 주소 1000에서 시작한다고 가정하면, 컴파일된 모든 명령어와 데이터는 1000을 기준으로 한 절대 주소를 가진다.
적재 시간 바인딩
적재 시간 바인딩은 프로그램이 컴파일된 후, 실행 파일이 메모리에 로드될 때 물리 주소가 할당되는 방식이다. 이 경우 컴파일러는 재배치 가능한 주소를 생성하지만, 프로그램이 메모리에 로드될 때 이 재배치 가능한 주소가 물리 주소로 변환된다. 적재 시점에서 메모리의 특정 위치에 프로그램을 로드하면서, 모든 재배치 가능한 주소를 물리 주소로 변환하여 고정한다. 따라서 프로그램이 실행을 시작하면, 이때의 논리 주소는 이미 물리 주소로 변환된 상태이므로 논리 주소와 물리 주소가 동일하다.
실행 시간 바인딩
실행 시간 바인딩은 프로그램이 실행되는 동안 언제든지 메모리 내의 다른 위치로 이동할 수 있는 유연성을 제공한다. 실행 시간 바인딩에서는 논리 주소와 물리 주소가 다를 수 있으며, CPU가 생성하는 논리 주소는 실행 시간에 MMU(Memory Management Unit)에 의해 물리 주소로 변환된다. 이 방식은 동적 메모리 할당, 페이지 교환 등과 같은 복잡한 메모리 관리 기법을 지원한다.
메모리 보호(Memory Protection)
메모리는 일반적으로 상주 운영체제용과 유저 프로세스용으로 구분된다. 일반적으로 여러 유저 프로세스가 메모리에 적재되어 있는 것이 바람직하다. 이는 시스템 자원을 효율적으로 사용하고, 프로세스 간 통신 효율성을 높이는 등 여러 장점이 있기 때문이다.
그러나 여러 프로세스가 메모리를 공유하는 경우 메모리 보호가 매우 중요하다. 각 프로세스가 자신에게 할당된 메모리 영역만 접근할 수 있도록 보장해야 하며, 이를 위해 메모리 보호 기법이 사용된다.
운영체제는 재배치 레지스터와 상한 레지스터를 사용하며 메모리 접근을 관리한다. 재배치 레지스터는 가장 작은 물리 주소값을 저장하고, 상한 레지스터는 논리 주소의 범위값을 저장한다. 각 논리 주소는 상한 레지스터에 의해 지정된 범위 내에 존재해야 하며, MMU(Memory Management Unit)는 동적으로 논리 주소에 재배치 레지스터 값을 더하여 물리 주소로 변환한다. 이를 통해 프로세스는 자신이 허가된 메모리 영역만 접근할 수 있게 된다.
메모리 할당(Memory Allocation)
효율적인 메모리 관리를 위해서는 메모리 할당 방법도 중요하다. 메모리 할당에는 다중 파티션 방식, 가변 분할, 동적 메모리 할당 문제 등이 있다.
다중 파티션 방식
다중 파티션 방식은 다음과 같다.
- 메모리를 고정된 크기의 여러 파티션으로 나눈다.
- 각 파티션은 정확히 하나의 프로세스를 포함할 수 있다.
- 파티션이 비어 있으면 입력 큐에서 프로세스가 선택되어 비어있는 파티션에 로드된다.
- 프로세스가 종료되면 파티션을 다른 프로세스에서 사용할 수 있게 된다.
멀티프로그래밍의 정도는 파티션 수에 의해 결정
다중 파티션 방식은 구현이 간단하고 관리가 용이하지만, 파티션 크기와 프로세스 크기가 일치하지 않는 경우 메모리 낭비가 발생할 수 있다.
가변 분할 방식
가변 분할 방식은 메모리를 고정된 크기가 아닌, 프로세스가 필요한 만큼 크기로 분할하여 할당하는 방식이다. 이 방식은 메모리의 사용 효율성을 높일 수 있지만, 분할과 병합 과정에서 외부 단편화가 발생할 수 있다.
동적 메모리 할당 문제
동적 메모리 할당에서는 시간이 지남에 따라 메모리 할당과 해제가 반복되면서 단편화 문제가 발생할 수 있다. 단편화 문제는 메모리 공간이 여러 작은 조각으로 나뉘어, 충분한 총 메모리 공간이 있어도 큰 프로세스를 위한 연속된 메모리 블록을 찾기 어려운 상황을 초래한다.
동적 메모리 할당 문제를 해결하기 위해 다양한 할당 전략이 사용된다. 주요 할당 전략에는 최초 적합(first-fit), 최적 적합(best-fit), 최악 적합(worst-fit)이 있다.
- 최초 적합(first-fit)
- 첫 번째 사용 가능한 가용 공간 선택
- 집합의 시작에서부터 검색하거나, 지난번 검색이 끝났던 곳에서 시작
- 충분히 큰 가용 공간을 찾았을 때 검색 종료
- 장점: 메모리 검색 속도 빠름
- 단점: 단편화 문제
- 최적 적합(best-fit)
- 사용 가능한 공간들 중에서 가장 작은 공간 선택
- 장점: 메모리를 효율적으로 사용 가능
- 단점: 리스트가 크기 순으로 되어 있지 않다면 전체 리스트를 검색
- 최악 적합(worst-fit)
- 가장 큰 가용 공간 선택
- 단점
- 자유 공간이 크기 순으로 정렬되어 있지 않으면 전체 리스트를 검색
- 메모리 낭비
단편화(Fragmentation)
단편화(Fragmentation)는 사용 가능한 메모리 공간이 작은 조각들로 분산되어 있어, 이를 효과적으로 사용하지 못하는 현상이다. 단편화는 내부 단편화와 외부 단편화가 있다.
내부 단편화(Internal Fragmentation)
내부 단편화(Internal Fragmentation)는 할당된 메모리 블록 내부에 사용되지 않는 공간이 발생하는 현상이다. 예를 들어, 프로세스가 1025바이트의 메모리를 요청했지만 시스템이 메모리 블록을 2048바이트 단위로만 할당하여 나머지 1023바이트는 사용되지 않고 낭비되는 경우이다. 이는 메모리 할당 시 요청된 크기에 가장 잘 맞는 블록 크기를 선택할 수 있다면 해결할 수 있지만, 많은 오버헤드가 발생하는 문제점이 있다.
외부 단편화(External Fragmentation)
외부 단편화(External Fragmentation)는 할당되지 않은 메모리 공간이 여러 작은 조각으로 나뉘어져 있어, 충분한 크기의 메모리 요청을 만족시키기에 충분한 연속된 공간이 존재하지 않은 경우이다. 예를 들어, 1000바이트의 메모리가 필요한데 시스템에는 500바이트짜리 두 개의 조각만이 남아 있어 이 요청을 만족시킬 수 없는 경우이다. 이는 페이징(Paging) 기법으로 해결할 수 있다.
페이징(Paging)
페이징(Paging)은 외부 단편화를 방지하는 프로세스의 물리 주소 공간이 연속적이지 않도록 하는 메모리 관리 체계이다. 페이징의 원리는 물리 메모리를 프레임이라고 하는 고정된 크기의 블록으로 나눈다. 논리 메모리를 페이지라고 하는 동일한 크기의 블록으로 나눈다. CPU에 의해 생성된 모든 주소는 페이지 번호(p)와 페이지 오프셋(d) 두 부분으로 나눠, 페이지 테이블을 참조하여 프레임에 할당한다.
이러한 구조 덕분에 각 프로세스는 물리적 메모리가 연속적이지 않아도 연속적인 논리적 주소 공간을 가질 수 있으며, 이는 메모리 사용의 유연성을 크게 향상시킨다.
페이지 크기
페이지 크기는 시스템마다 다를 수 있으며, 일반적으로 4KB에서 1GB 사이의 2의 거듭제곱으로 설정된다. 논리 주소 공간 사이즈가 $2^m$이고, 페이지 사이즈가 $2^n$바이트일 때, 논리 주소의 상위 $m-n$비트는 페이지 번호를 지정하고, 하위 $n$비트는 페이지 오프셋을 지정한다.
위 사진을 보면 $n = 2$이고 $m = 4$일 때, 논리 주소 공간 크기는 $2^4$이고 페이지 크기가 $2^2$이다.
논리 주소가 0일 때 페이지는 0이고 오프셋은 0이다. 즉, 페이지 테이블을 참조하여 물리 주소는 $5 \times 4$로 20이다. 논리 주소가 13일 때 페이지는 3이고 오프셋은 1이다. 즉, 물리 주소는 $2 \times 4 + 1$로 9이다.
페이징은 외부 단편화를 방지할 수 있지만 내부 단편화 문제가 존재한다. 즉, 프로세스가 페이지 경계와 일치하지 않는 크기의 메모리를 요구한다면 마지막 페이지 프레임은 전부 할당되지 않는 문제가 있다.
TLB(Translation Lookaside Buffer)
대부분의 컴퓨터는 페이지 테이블을 메인 메모리에 저장하고, 페이지 테이블 기준 레지스터(RTBR)를 사용하여 페이지 테이블을 가리킨다. 하지만 페이지 테이블이 메모리에 저장되어 있으면 메모리 접근 시간이 길어질 수 있다. 이를 해결하기 위해 TLB(Translation Lookaside Buffer)가 있다.
TLB(Translation Lookaside Buffer)는 CPU 내의 작은 고속 캐시 메모리로, 자주 참조되는 페이지 테이블의 일부를 저장하여 주소 변환 속도를 높이는 역할을 한다. TLB는 key와 value로 구성되어 있고, 페이지 번호를 찾으면 프레임 번호를 즉시 사용하여 메모리 접근이 가능하다. 만약 페이지 번호가 TLB에 없다면 메모리에 있는 페이지 테이블을 참조하여 새로운 페이지 번호와 프레임 번호를 TLB에 생성한다. 이 과정을 통해 TLB는 자주 참조되는 페이지 테이블 항목을 만들어 주소 변환 속도를 크게 높인다.
보호(Protection)
메모리 보호(Protection)는 각 프레임과 관련된 보호 비트에 의해 수행된다. 보호 비트는 다음과 같다.
- 페이지 읽기 또는 읽기-쓰기 전용 비트: 보호 비트를 검사하여 읽기 전용 페이지에 쓰기를 시도하면 위반을 검출
- 유효-무효 비트
- ‘유효’로 설정되면 관련된 페이지가 프로세스의 합법적인 페이지임을 나타냄
- ‘무효’로 설정되면 그 페이지는 프로세스의 논리 주소 공간에 속하지 않는다는 것을 나타냄
Ref
[1] Operating System Concepts(Silberschatz, Galvin and Gagne)
Footnote
프로세스 컨택스트: 프로그램이 실행되기 위해 필요한 모든 정보를 포함하는 것으로, 프로그램이 올바르게 실행될 수 있도록 준비된 환경이다. ↩︎