가상 메모리, 주소 공간과 주소 변환 방법 [1 / 4]

junto·2024년 3월 16일
0

operating system

목록 보기
7/13
post-thumbnail

운영체제는 사용하기 쉽고, 하나의 CPU에서 여러 프로그램이 안전하게 실행할 수 있는 메모리 개념을 만들어야 한다. 운영체제가 어떻게 메모리 가상화를 지원하고 필요한 하드웨어 지원이 무엇인지 알아본다.

1. 주소 공간

1) 프로세스의 주소 공간

  • 하나의 프로세스는 코드 영역, 데이터 영역, 스택 영역, 힙 영역을 가진다. 코드 영역은 실행할 프로그램의 코드(명령어)가 저장되는 영역을 말하며, 데이터 영역에는 정적 변수가 저장된다.
  • 확장되거나 축소될 수 있는 메모리 영역이 존재하는데, 스택과 힙 영역이다. 스택 영역은 지역 변수, 함수 이름, 함수 인자와 반환 값 등을 저장하며 힙 영역은 프로그램 실행 시에 동적으로 할당되는 메모리를 위해 사용된다.
  • 주소 공간이 어떻게 이루어져 있는지 확인하기 위해 C언어로 된 간단한 프로그램을 실행한다.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
	int x = 5;
	int y = 10;
	static int staticNum = 10;
	printf("stack: main location: %p\n", (void *)main);
	printf("data address: %p\n", &staticNum);
	printf("heap address: %p\n", (void *)malloc(1));
	printf("heap address: %p\n", (void *)malloc(1));
	printf("x stack address: %p\n", &x);
	printf("y stack address: %p\n", &y);
}
  • 출력 결과는 아래와 같다. 주소 공간을 그림으로 표현하면 다음과 같다.
stack: main location: 0x1001c7e44
data address: 0x1001cc000
heap address: 0x6000004bc020
heap address: 0x6000004bc030
x stack address: 0x16fc3b3cc
y stack address: 0x16fc3b3c8

현대 운영체제는 메모리 가상화를 지원한다. 먼저 이를 확인하기 위해 위의 프로그램에 간단한 명령어를 추가한다.

#include <unistd.h>
int num = 0;
while (1) {
	printf("heap address: %p\n", (void *)malloc(1));
    sleep(1);
}
  • 쉘을 통해 해당 프로그램을 여러 개 실행시킨다.
./a.out &; ./a.out &; ./a.out &;
  • 출력 결과를 보면 여러 프로세스가 각자의 독립된 주소 공간에서 실행되고 있음을 확인할 수 있다.

2) 논리 주소와 물리적 주소

  • 물리적 주소(Physical address)는 메모리가 실제 올라가는 위치를 말한다.
  • 심볼릭 주소(Symbolic address)는 변수를 사용하고, 함수를 호출할 때 변수와 함수 이름으로 접근하는 것처럼 프로그래머는 심볼릭 주소를 사용한다. 심볼릭 주소는 컴파일할 때 논리 주소로 변경된다.
  • 논리 주소(Logical address)는 프로세스마다 독립적으로 가지는 주소 공간을 말한다. CPU가 보는 주소는 논리 주소이다. 이는 메모리 가상화를 지원하기 위해서다. 아래 살펴볼 동적 주소 변환을 위해 프로그램은 실행 중에 물리주소가 변경될 수 있다. 하지만 기존 논리 주소를 유지하면 어떤 물리 주소에서든 명령어를 실행하는 데 문제 없다.

프로그램이 실행이 되려면 메모리에 올라가야 한다. 이때, 가상 주소가 실제 물리 주소로 변환하는 방식에 따라 구분할 수 있다.

2. 주소 변환 (Address Binding)

1) Compiletime binding

  • 물리적 메모리 주소가 컴파일 시 정해진다. 물리적 메모리 주소를 변경하기 위해선 다시 컴파일 해야 한다.

2) Loadtime binding

  • 로더(Loader)라 불리는 소프트웨어가 실행하고자 하는 실행 파일의 모든 주소를 물리 메모리 주소로 변경한다.
  • 잘못된 주소를 생성하여 다른 프로세스나 운영체제 메모리에 접근할 수 있어 보안상 취약한 문제가 있다. 또한 한 번 배치되면 추후 주소 공간을 재배치하는 것이 어렵다. 1), 2) 방식으로 프로그램이 실행되기 전에 모든 주소가 결정되는 방식을 정적 재배치(static relocation)이라고 한다.

3) Runtime binding

  • 프로그램이 실행된 상태에서도 프로세스의 물리 주소를 변경할 수 있다. 이렇게 동적으로 주소가 결정되는 방식을 동적 재배치(dynamic relocation)이라고 한다.
  • 하드웨어 지원이 필요하다. 논리 주소를 가상 주소로 변환시켜 주는 하드웨어를 메모리 관리 장치(Memory Management Unit, MMU)라고 한다.

주소 변환 동작 방식

  • 각 CPU마다 두 개의 하드웨어 레지스터가 필요하다. 하나는 베이스 레지스터(base register)이고, 다른 하나는 리밋 레지스터(limit register)이다.
  • 하드웨어는 베이스 레지스터에 가상 주소를 더하여 물리 주소를 생성한다.
  • 위의 그림에서 논리 주소 346에 베이스 레지스터 14000를 더해 물리 주소 14346을 결정한다.
physicalAddress = virtualAddress + baseRegister;
  • 하드웨어는 가상 주소가 limit register를 초과하거나 음수인 경우 트랩이 발생하여 운영체제 예외 핸들러가 실행된다.

3. 운영체제의 지원

주소 변환은 운영체제의 개입 없이 하드웨어만으로 처리된다는 사실을 알 수 있었다. Runtime Binding을 제대로 지원하기 위해선 다음과 같은 운영체제 역할이 필요하다.

1) 빈 공간 제공

  • 새로운 프로세스가 생성되면 운영체제는 필요한 공간을 할당하기 위해 흔히 빈 공간 리스트 자료 구조를 검색하여 제공한다.

2) 자원 회수

  • 프로세스가 정상적으로 종료되거나 또는 잘못된 행동으로 비정상 종료될 때 프로세스가 사용하던 메모리를 회수하여 다른 프로세스나 운영체제가 사용할 수 있도록 한다.

3) 문맥 교환(Context Switch)

  • 운영체제는 프로세스 전환 시 베이스와 리밋 레지스터 쌍을 저장하고 복원한다. 이는 프로세스 관리 정보인 PCB(Process Control Block)에 저장된다.

4) 예외 핸들러 제공

  • 프로세스가 바운드 밖의 메모리에 접근하려는 경우 하드웨어는 예외를 발생시켜 운영체제 핸들러가 실행될 수 있도록 한다.

현재 Runtime binding(동적 재배치)은 어떤 문제가 있을까?

  • 2개의 하드웨어 레지스터를 가정하므로 사용하지 않는 스택과 힙 사이의 공간이 낭비(내부 단편화)되고 있다. 그림으로 표현하면 아래와 같다.

4. 메모리 관련 용어

1. Dynamic Loading

  • 프로세스 전체를 메모리에 미리 다 올리는 것이 아니라 실행될 때 메모리에 올리는 기법을 말한다.
  • 메모리 이용률이 향상되며, 가끔씩 사용되는 오류 처리에 유용하다.
  • 운영체제의 특별한 지원 없이 프로그램 자체에서 구현 가능하다. (OS는 라이브러리를 통해 지원 가능)

    요구페이징(Demand Paging), 필요한 부분만 메모리를 올리는 기법과 헷갈릴 수 있다. 동적 로딩(Dynamic Loading)은 해당 기법이 나오기 전에 프로그래머가 직접 동적 로딩 방식으로 코드를 작성한 것을 말한다. 지금은 혼용해서 쓰기도 한다.

2. Overlay

  • 메모리에 프로세스의 부분 중 실제 필요한 정보만을 올린다. 프로세스의 크기가 메모리보다 클 때 유용하다. 운영체제의 지원없이 사용자에 의해 구현된다. 초창키 컴퓨터에선 프로그램 하나를 메모리에 올리는 것도 힘들었기 때문에 프로그래머가 수작업으로 구현한 것이다.

3. Linking

링킹(linking)이란 여러 군데 존재하던 컴파일된 파일들을 하나로 묶어 실행 파일로 만드는 작업이다. 내가 작성하지 않은 라이브러리도 링킹되어 실행 파일이 된다.

1) Static Linking

  • 라이브러리가 프로그램의 실행 파일 코드에 포함된다.
  • 실행 파일의 크기가 커지며, 동일한 라이브러리를 각각의 프로세스가 메모리에 올리므로 메모리 낭비가 발생할 수 있다.

2) Dynamic Linking

  • 라이브러리가 실행 시 연결된다. Linking을 실행 시간까지 미루는 기법을 말한다. 라이브러리 호출 부분에 라이브러리 루틴의 위치를 찾기 위해 stub이라는 작은 코드를 둔다.
  • 라이브러리가 이미 메모리에 있으면 그 루틴의 주소로 가고 없으면 디스크에서 읽어온다.
  • 운영체제 지원이 필요하다.

참고 자료

  • 2014 이화여대 반효경 운영체제 강의
  • 운영체제, 아주 쉬운 세 가지 이야기
profile
꾸준하게

0개의 댓글