초기 시스템은 물리 메모리에 하나의 실행 중인 프로세스가 존재하고 특별한 가상화는 거의 존재하지 않았다. 하지만,
시간이 흐르면서 멀티 프로그래밍(Multi-Programming) 시대가 도래
하였다. 여러 프로세스가 실행 준비 상태에 있고 OS는 프로세스들을 전환하면서 CPU의 활용률을 극대화시켰다.
멀티 프로그래밍과 시분할(Time-Sharing) 시스템을 도입하면서 단순히 메모리에 다수의 프로그램 코드, 데이터를 적재하는 것 뿐만 아니라, 메모리의 할당과 해제, 프로세스 간의 전환 성능, 보호(Protection)가 중요해졌다. 아무튼 시분할 시스템의 문제는 너무 느리게(특히 메모리가 커질수록) 동작하고, 레지스터 상태를 저장하고 복원하는 것은 빠르지만 메모리의 내용 전체를 디스크에 저장하는 것은 매우 느리다는 점이다. 이 부분에서 프로세스 전환 시 프로세스를 메모리에 그대로 유지하면서, 운영체제가 시분할 시스템을 효율적으로 구현할 수 있게 할 필요성
이 있었다.
간단하게는 프로세스가 메모리에 접근할 수 있는 주소의 범위
를 뜻한다. 주소 공간을 통해 OS는 프로세스마다 고유한 메모리 공간을 할당하고 관리한다. 주소 공간은 두 가지 형태로 나눌 수 있는데...
여기서 주의할 점은, 실제 프로그램이 물리 주소 0에서 16KB 사이에 존재하는 것은 아니라는 것
이다. 실제로는 임의의 물리 주소에 탑재된다. 프로세스가 실행될 때, OS는 해당 프로세스에 가상 주소 공간을 할당한다. 이 주소 공간은 물리 메모리에서 다른 위치에 매핑될 수 있다.
그림에서 출력된 주소 값들은 모두 가상 주소
인 것이다.
예를 들어 C 프로그램이 실행된다고 한다면, 두 가지 유형의 메모리 공간이 할당된다. 첫 번째는
Stack(스택)으로 실행 시간에 자동으로 할당하고 해제
한다. 가령, func() 이라는 함수 안에서 x라 불리는 정수를 위한 공간을 선언하면 func() 함수가 호출될 때 스택에 공간을 확보한다. 함수에서 리턴하면 컴파일러가 메모리를 반환한다. 따라서 함수 리턴 이후에도 유지되어야 하는 정보는 스택에 저장하지 않는 것이 좋다. 오랫동안 값이 유지되어야 하는 변수를 위해서는Heap(힙)이라는 메모리가 필요하다. 프로그래머가 직접 할당과 해제를 해야 한다.
void func() {
int *x = (int *) malloc(sizeof(int));
...
}
예를 들어, 이 코드에서 주의 사항이 있다. 첫 번째로 한 행에 스택과 힙 할당이 모두 발생한다. 컴파일러가 포인터 변수의 선언 (int *x)을 만나면 정수 포인터를 위한 공간을 할당해야 한다는 것을 안다. 프로그램이 malloc( ) 을 호출하여 정수를 위한 공간을 힙으로부터 요구한다. malloc( ) 은 그 정수의 시작 주소를 알려준다(실패하면 NULL을 반환). 이 반환된 주소가 스택에 저장되어 프로그램에 의해 사용된다.
이제 공간을 해제하는 것, 더 이상 사용되지 않는 힙 메모리를 해제하기 위해 free() 함수
를 호출한다. 만약 malloc( ) 함수로 메모리를 할당하고 free( ) 함수로 해제하지 않는다면, OS가 그 메모리 공간이 현재 사용 중인줄 알고 추후에 그 공간이 필요할 때 할당을 안 해줄 수도 있다.
이런 malloc(), free() 함수는 시스템 콜이 아니라 라이브러리를 호출하는 것
이다. 주소 공간에 관련된 시스템 콜은 brk() 또는 sbrk()
이다. brk() 은 Program break(Data segment의 끝 주소)의 위치를 변경할 때 사용된다.
예를 들어, brk(addr) 이라고 한다면, addr가 새로운 Program break 주소라는 뜻이다.
또한, 공간을 늘리고 싶을 때 sbrk()를 사용
하면 된다. 예를 들어, sbrk(inc)은 현재 Program break에서 inc 바이트 만큼 늘려 Program break를 갱신한다는 것이고, sbrk(0)
의 의미는 현재 위치, 즉 Data segment의 끝 위치를 얻어온다는 뜻이다.
1950년대 Time-Sharing 시스템에 도입된 재배치 방법으로 CPU가 제공하는 레지스터인
Base와 Limit(=bounds)
를 활용하여 주소 변환을 수행하는 과정을 의미한다.
이 base와 limit 레지스터를 이용하여 원하는 위치에 주소 공간을 배치할 수 있게 한다. 배치와 동시에 프로세스가 오직 자신의 주소 공간에만 접근한다는 것을 보장한다. 프로그램 시작 시, OS가 프로그램이 탑재될 물리 메모리 위치를 결정하고 base 레지스터를 그 주소로 지정한다. 그 주소 값을 유저 프로세스가 생성하는 모든 메모리 주소에 합산한 결과를 물리 메모리 주소로 전송한다.
즉, 가상 주소는 다음 식에 의해서 물리 주소를 계산한다.
물리 주소 = 가상 주소 + 재배치 레지스터(base)
위 그림처럼 프로세스가 346번지에 엑세스할 때, 실은 메인 메모리의 14346번지(=물리 주소)에 액세스하게 되는 것이다. 만약 가상 주소가 250,000 번지로 주어진다면 존재하지 않는 물리 주소를 부르는 것이기 때문에 유효하지 않다. 주어진 Logical memory의 메모리 범위 안이라면 유효한 것이다.
사용자 모드에서 수행되는 프로그램이 OS의 메모리 공간이나 다른 사용자 프로그램의 메모리 공간에 접근하면 OS는 치명적인 오류(Trap)로 간주한다.