프로그램이 실행을 위해 메모리에 적재되면(즉, 프로세스가 되면) 해당 프로세스를 위한 독자적인 주소 공간이 생성된다. 이를 논리적 주소 또는 가상 주소(Virtual address)라 하며, 각 프로세스마다 0번부터 주소가 시작된다. 그리고 CPU는 이 논리적 주소에 근거하여 명령을 실행한다.
반면 물리적 주소는 실제 물리적 메모리에 프로세스가 적재되는 영역을 말한다. 물리적 메모리의 낮은 주소 영역에는 OS의 데이터가 적재되고, 높은 주소 영역에 사용자의 프로세스들이 적재된다.
기호 주소(Symbolic address)
변수나 함수와 같이 코드에서 사용하는 상징적인 주소로, 프로그래머의 입장에서 사용하는 주소이다. 단순하게는 변수명 혹은 함수명으로 이해하면 될 것이다.
CPU가 명령을 수행하기 위해서는 논리적 주소를 통해 메모리를 참조하며, 따라서 논리적 주소와 물리적 주소가 서로 매핑되어 있어야 한다. 이때 논리적 주소를 물리적 주소로 연결하는 작업을 주소 바인딩
이라고 한다.
즉, 주소 바인딩은 symbolic address를 logical address를 거쳐 physical address와 연결하는 과정이라고 이해할 수도 있을 것이다.
주소 바인딩은 바인딩이 이루어지는 시기가 언제냐에 따라 세 가지로 분류할 수 있다.
예컨대 프로그램 시작 시 물리적 주소가 500번지부터 비어 있다면 논리적 주소 0번지를 물리적 주소 500번지와 매핑한다.
로더(loader, 사용자 프로그램을 메모리에 적재시키는 프로그램)
가 물리적 메모리 주소를 부여하며, 프로세스의 종료 시까지 물리적 주소가 고정됨주소 매핑 테이블
을 이용해 주소 바인딩을 점검함기준 레지스터
와 한계 레지스터
를 포함하여 MMU
라는 하드웨어적 지원이 필요
- 기준 레지스터(Base register) : 프로세스의 물리적 메모리의 시작 주소를 보유함
- 한계 레지스터(Limit register) : 현재 CPU에서 수행 중인 프로세스의 논리적 주소의 최댓값, 즉 프로세스의 크기를 보유함
- MMU(Memory Management Unit) : 논리적 주소를 물리적 주소로 매핑해주는 하드웨어
CPU가 주소를 참조할 경우 MMU는 기준 레지스터
의 값에 논리적 주소
를 더하여 물리적 주소를 얻는다. 예를 들어 CPU가 논리적 주소 300에 있는 데이터를 요청하였고 기준 레지스터에 저장된 메모리 시작 주소가 2000이라면 MMU는 이 둘을 더하여 물리적 주소 2300에 있는 데이터를 로드하는 것이다.
그런데 논리적 주소값은 프로세스마다 독립적으로 할당되므로 프로세스 A와 프로세스 B에 각각 100번 논리적 주소가 있을 수 있다. 그러나 실제 각 프로세스의 100번 논리적 주소에 매핑되는 물리적 주소는 다를 것이다. 따라서 MMU는 문맥 교환(Context switching)이 일어날 때마다 기준 레지스터의 값도 함께 변경하는 재설정 작업을 진행한다.
한편 프로세스 A의 작업 중 CPU가 요청한 '논리적 주소값 + 기준 레지스터의 값'의 결과가 해당 프로세스의 주소 공간을 넘어 프로세스 B의 영역에 해당되는 경우가 발생할 수도 있다. 이 경우 메모리 보안이 이루어지지 않아 시스템에 문제가 발생할 수 있다. 따라서 이를 방지하기 위해 한계 레지스터
를 사용, CPU가 논리적 주소를 요청할 때마다 한계 레지스터에 저장된 '최대 논리적 주소값'과 비교하여 그보다 작은지를 확인하고, 만약 그 범위를 벗어날 경우 트랩
을 발생시켜 해당 프로세스를 종료시킨다.
앞서 주소 매핑 방식을 설명할 때 전제했던 것은 프로그램의 실행 시, 즉 프로그램이 메모리에 적재되어 프로세스가 될 때 해당 프로그램 전체가 메모리에 적재된다는 것이다. 그런데 이 경우 프로세스의 크기가 메모리의 크기보다 클 수 없다는 한계가 생기며, 이것이 충족되더라도 메모리를 효율적으로 사용하지는 못한다는 단점이 발생한다.
OS는 메모리를 효율적으로 관리하기 위해 여러 가지 메모리 낭비 방지 기술들을 사용하며, 대표적인 방법으로 3가지를 들 수 있다.
프로그램 A와 프로그램 B가 공통으로 print() 함수를 사용한다고 했을 때, 프로그램 전체를 메모리에 올리는 것은 메모리 낭비이다. 서로 중복되는 라이브러리 루틴인 print()는 하나만 적재하고, 프로그램 B가 해당 함수를 요청할 때 적재된 루틴과 연결하는 것이 효율적일 것이다.