지금까지는 각 프로세스의 전체 주소 공간을 메모리에 올려 사용해왔다. 그러나 우리가 사용하는 주소 공간은 스택과 힙 사이에 많은 "빈" 공간이 있음을 알아봤다. Figure 16.1에서 볼 수 있듯이, 이 빈 공간은 프로세스에서 사용되지 않지만, 전체 주소 공간을 물리적 메모리의 다른 위치로 이동시킬 때 여전히 물리적 메모리를 차지하고 있다.
이렇게 전체 주소 공간을 가상화하기 위해 베이스와 바운드 레지스터 쌍을 사용하는 것은 낭비적이다. 또한, 전체 주소 공간이 메모리에 맞지 않을 때 프로그램을 실행하는 것이 어려워진다. 따라서 베이스와 바운드 방식은 우리가 원하는 만큼 유연하지 않다. 이러한 문제를 해결하기 위해서는 어떻게 해야 할까? 4GB 크기의 32비트 주소 공간과 같이 큰 주소 공간을 지원하면서도 스택과 힙 사이의 많은 빈 공간을 효율적으로 활용할 수 있는 방법을 찾아야 한다.
1. Segmentation: Generalized Base/Bounds
이전에는 각 프로세스의 전체 주소 공간을 메모리에 넣었다. 그러나 base와 bounds register를 사용하면 OS가 프로세스를 물리적 메모리의 다른 위치로 쉽게 재배치할 수 있다.
하지만 우리의 주소 공간에는 스택과 힙 사이에 큰 "free" 공간이 있다는 것에 주목해야 한다. 이 공간은 사용되지 않더라도 전체 주소 공간을 물리적 메모리의 다른 위치에 재배치하면 물리적 메모리를 차지한다. 따라서 가상 메모리를 사용하는 것은 비효율적이다.
이 문제를 해결하기 위해 segmentation이라는 개념이 탄생했다. Segmentation은 주소 공간의 각 논리적 세그먼트마다 하나의 base와 bounds register 쌍을 가질 수 있도록 하는 것이다.
세그먼트는 특정 길이의 연속적인 주소 공간이며, 우리가 가진 전형적인 주소 공간에서는 세 개의 논리적으로 다른 세그먼트가 있다: 코드, 스택 및 힙. 이러한 segmentation을 사용하면 각 세그먼트를 물리적 메모리의 다른 위치에 놓을 수 있으며, 사용하지 않는 가상 주소 공간으로 물리적 메모리를 채우지 않을 수 있다.
Segmentation을 지원하는 하드웨어 구조는 각 세그먼트에 대해 하나의 base와 bounds register 쌍으로 구성된다. 이제 각 세그먼트가 독립적으로 물리적 메모리에 배치될 수 있으므로 대규모 주소 공간도 처리할 수 있다.
이를 위해 가상 주소를 물리적 주소로 변환할 때 각 세그먼트의 base 값을 더하는 방법을 사용한다. 단, 스택이나 힙 세그먼트의 경우 더해지는 값을 먼저 offset으로 추출한 후에 더해야 한다.
이러한 방식으로 프로그램에서 유효하지 않은 주소에 대한 접근 시도가 있으면 OS가 해당 프로세스를 종료시키는 등의 예외 처리를 해주어야 한다.
2. Which Segment Are We Referring To?
지금까지 우리는 주소 공간의 중요한 구성 요소 중 하나인 스택을 놓치고 있었다. 그러나 위의 다이어그램에서 스택은 물리적 주소 28KB로 재배치되었으며, 중요한 차이점이 하나 있다: 스택은 뒤로(즉, 낮은 주소 쪽으로) 자란다. 물리적 메모리에서는 28KB1에서 "시작"하여 26KB까지 뒤로 자라며, 이는 가상 주소 16KB에서 14KB에 해당한다. 따라서 변환 방식이 조금 다르다.
먼저, 우리는 조금 더 많은 하드웨어 지원이 필요하다. 하나의 base와 bounds 값 대신, 하드웨어는 세그먼트가 어느 방향으로 자라는지도 알아야 한다. 예를 들어, 세그먼트가 양의 방향으로 자라면 1로 설정되는 비트가 있을 수 있고, 음의 방향으로 자라면 0이 될 수 있다. 이러한 수정된 하드웨어 구성은 Figure 16.4에서 볼 수 있다.
스택 가상 주소를 물리적 주소로 변환하는 방법은 다음과 같다. 예를 들어, 가상 주소 15KB에 액세스하려고 한다면, 이는 물리적 주소 27KB에 매핑되어야 한다. 이진 형식으로 표현된 우리의 가상 주소는 11 1100 0000 0000 (16진수 0x3C00)이다. 하드웨어는 상위 두 비트 (11)를 세그먼트를 지정하는 데 사용하지만, 남은 3KB의 offset이 남아 있다. 올바른 음의 offset을 얻으려면, 최대 세그먼트 크기를 3KB에서 빼야 한다. 이 예에서 세그먼트 크기는 4KB이므로 올바른 음의 offset은 3KB에서 4KB를 뺀 것으로 -1KB이다. 따라서, 우리는 음의 offset(-1KB)을 base(28KB)에 더하여 올바른 물리적 주소인 27KB에 도달할 수 있다. bounds check는 음의 offset의 절대값이 해당 세그먼트의 현재 크기 이하인지 확인하여 계산할 수 있다(이 예에서는 2KB).
3. What About The Stack?
지금까지는 주소 공간의 중요한 부분인 스택에 대해 놓치고 있었다. 그림에서 스택은 물리적 주소 28KB로 이동되었지만, 한 가지 중요한 차이가 있다. 스택은 거꾸로 성장한다(즉, 낮은 주소 쪽으로). 물리적 메모리에서는 28KB1에서 시작하여 가상 주소 16KB에서 14KB까지 대응한다. 이러한 경우 번역 방법이 다르게 진행되어야 한다.
우선 약간의 하드웨어 지원이 필요하다. 단순히 베이스와 바운드 값뿐만 아니라 하드웨어는 세그먼트가 어느 방향으로 성장하는지도 알아야 한다(예를 들어, 세그먼트가 양의 방향으로 성장하면 1로 설정되는 비트 등). 우리는 하드웨어가 추적하는 것을 그림 16.4에서 볼 수 있다.
하드웨어는 세그먼트가 음의 방향으로 성장할 수 있다는 것을 이해하므로 가상 주소를 약간 다르게 번역해야 한다. 예를 들어 스택 가상 주소를 살펴보고 이를 프로세스를 이해하기 위해 번역해보자.
이 예제에서는 가상 주소 15KB에 접근하려고 하는데, 이는 물리적 주소 27KB에 매핑되어야 한다. 따라서 이진 형태의 가상 주소는 다음과 같다: 11 1100 0000 0000 (16진수 0x3C00). 하드웨어는 상위 두 비트(11)를 세그먼트로 지정하지만, 그런 다음 3KB의 오프셋이 남는다. 올바른 음의 오프셋을 얻으려면 최대 세그먼트 크기를 3KB에서 빼야 한다. 이 경우 세그먼트의 크기는 4KB가 될 수 있으므로, 올바른 음의 오프셋은 3KB에서 4KB를 뺀 값인 -1KB가 된다. 그런 다음 음의 오프셋(-1KB)을 베이스(28KB)에 더하여 올바른 물리적 주소 27KB에 도달할 수 있다. 바운드 체크는 절대값이 세그먼트의 현재 크기(이 경우 2KB)보다 작거나 같은지 확인하여 계산할 수 있다.
4. Support for Sharing
세그먼테이션을 지원하면서, 시스템 디자이너들은 하드웨어 지원을 추가로 더해서 새로운 유형의 효율성을 얻을 수 있다는 것을 깨달았다. 특히, 코드 공유는 여전히 많이 사용되고 있다. 공유를 지원하기 위해서는 하드웨어의 보호 비트를 사용해야 한다. 기본적인 지원은 각 세그먼트 당 몇 개의 비트를 추가하여 해당 세그먼트에 대해 프로그램이 읽거나 쓰거나 또는 실행할 수 있는지를 나타낸다. 코드 세그먼트를 읽기 전용으로 설정하면, 동일한 코드가 여러 프로세스에서 공유될 수 있고, 각 프로세스는 자신의 개인적인 메모리에 접근하고 있는 것처럼 보이지만, OS는 수정할 수 없는 메모리를 비밀리에 공유함으로써 격리를 보호한다. 보호 비트와 함께, 이전에 설명한 하드웨어 알고리즘도 변경되어야 한다. 경계 내에 있는지 확인하는 것 외에도, 하드웨어는 특정 액세스가 허용되는지 확인해야 한다. 사용자 프로세스가 읽기 전용 세그먼트에 쓰거나, 실행할 수 없는 세그먼트에서 실행하려고 하면, 하드웨어는 예외를 발생시켜 문제를 처리할 수 있게 한다.
5. Fine-grained vs. Coarse-grained Segmentation
이전까지 우리는 몇 개의 세그먼트(코드, 스택, 힙)만을 다룬 경우를 대부분 살펴봤다. 이렇게 세분화된 경우를 '굵은-세분화(coarse-grained)'라고 생각할 수 있다. 이러한 굵은-세분화 방식은 주소 공간을 상대적으로 큰 덩어리로 잘라냈다. 그러나 일부 초기 시스템(Multics 등)은 주소 공간을 더 세밀하게 세분화한 '미세-세분화(fine-grained)' 방식을 지원하였다. 많은 세그먼트를 지원하기 위해서는 보다 더 많은 하드웨어 지원이 필요하며, 이 경우 일반적으로 메모리에 저장된 세그먼트 테이블을 사용한다.
이러한 세그먼트 테이블은 매우 많은 수의 세그먼트를 생성할 수 있도록 지원하며, 우리가 지금까지 다룬 것보다 더 유연한 방식으로 세그먼트를 사용할 수 있도록 한다. 예를 들어, 초기 기계인 버러스 B5000은 수천 개의 세그먼트를 지원하며, 컴파일러가 코드와 데이터를 분리된 세그먼트로 나누어 OS와 하드웨어가 지원하도록 기대했다. 당시의 사고 방식은 미세-세분화된 세그먼트를 가지면, OS가 사용 중인 세그먼트와 사용하지 않는 세그먼트를 더 잘 파악할 수 있고, 이를 통해 주 메모리를 더 효과적으로 활용할 수 있다는 것이다.
6. OS Support
세그먼테이션은 메모리 관리 방법 중 하나로, 가상 주소 공간을 논리적인 단위로 분할하여 물리적 메모리에 할당하는 방식이다. 이를 통해 더 많은 프로세스를 물리 메모리에 올릴 수 있고, 프로세스마다 다양한 크기의 가상 주소 공간을 지원할 수 있다. 그러나 세그먼테이션은 몇 가지 문제점을 야기한다. 첫째, 문맥 전환 시 세그먼트 레지스터를 저장하고 복원해야 한다. 둘째, 세그먼트 크기가 동적으로 변할 때 운영 체제가 이를 관리해야 한다. 세그먼트가 커질 때는 메모리 할당 라이브러리가 시스템 호출을 수행하고 운영 체제가 적절한 메모리를 할당한다. 하지만 외부 메모리 단편화 문제가 발생할 수 있다. 물리 메모리는 자유공간이 떨어져 세그먼트를 할당하거나 크기를 늘리기 어려울 수 있다. 이 문제를 해결하기 위해 메모리 압축을 수행하거나 프리 리스트 관리 알고리즘을 사용한다. 하지만 어떤 알고리즘을 사용하더라도 외부 메모리 단편화 문제는 해결되지 않으며, 최소화하는 것이 중요하다.