Memory

HunkiKim·2022년 8월 28일
0

Memory

메모리 관리 개요

메모리 관리의 복잡성

CPU는 메모리에 있는 내용을 가져오거나 작업 결과를 메모리에 저장하기 위해 메모리 주소 레지스터(MAR)을 사용한다. 메모리 주소 레지스터에 필요한 메모리 주소를 넣으면 데이터를 메모리에서 가져오거나 메모리에 데이터를 옮길 수 있다.

폰노이만 구조의 컴퓨터에서 메모리는 유일한 작업 공간이며, 모든 프로그램은 메모리에 올라와야 실행 가능하다. 운영체제도 프로그램이므로 메모리에 올라와야 실행 가능하다. 따라서 메모리는 사용자 프로세스뿐 아니라 운영체제 프로세스도 공존한다. 예를들어 컴퓨터 전원 버튼을 누르면 운영체제가 메모리에 올라간다. 부팅이 끝나면 여러 응용 프로그램이 메모리에서 작업할 수 있다.

메모리 관리는 운영체제를 비롯해 여러 작업을 동시에 처리할 때 메모리를 어떻게 관리하는가에 관한 문제이다. 이처럼 복잡한 메모리 관리는 메모리 관리 시스템(Memory Management System), MMS가 담당한다.

메모리 관리자

메모리 관리는 메모리 관리자가 담당한다. 메모리 관리자는 정확히 말해 메모리 관리 유닛(Memory Manage Unit, MMU)라는 하드웨어인데 일반적으로 메모리 관리자라고 일컫는다. 메모리 관리자의 작업은 가져오기(fetch), 배치(placement), 재배치(replacement)이다.

fetch

프로세스와 데이터를 메모리로 가져오는 작업이다. 메모리 관리자는 사용자가 요청하면 프로세스와 데이터를 모두 메모리로 가져온다. 그런데 어떤 상황에서는 데이터의 일부만 가져와 실행하기도 한다. 예를 들어 큰 동영상 플레이어를 먼저 가져와 실행하고, 동영상 데이터는 필요할 때마다 수시로 가져와 실행하는 것이다. 메모리 관리자는 요청이 없어도 필요할 것 같은거 예측해서 미리 가져오기도 한다.

placement

가져온 프로세스와 데이터를 메모리의 어떤 부분에 올려놓을지 결정하는 작업이다. 배치 작업 전에 메모리를 어떤 크기로 자를 것인지가 매우 중요하다. 같은 크기로 자르느냐, 실행되는 프로세스의 크기에 맞게 자르느냐에 따라 메모리 관리 복잡성이 달라지기 때문이다. 이렇게 나누어진 메모리의 구역에 따라 프로세스와 데이터를 어떤 위치에 놓을지 결정 하는 것이 바로 배치 작업이다.

재배치 작업

새로운 프로세스를 가져와야 하는데 메모리가 꽉 찼다면 메모리에 있는 프로세스를 하드디스크로 옮겨놓아야 새로운 프로세스를 메모리에 가져올 수 있다. 이처럼 꽉 차 있는 메모리에 새로운 프로세스를 가져오기 위해 오래된 프로세스를 내보내는 작업이 재배치 작업이다.

메모리 관리자는 가져오기, 배치, 재배치 작업 시 다음 과 같은 정책에 따라 메모리를 관리한다.

가져오기 정책

프로세스가 필요로 하는 데이터를 언제 메모리로 가져올지 결정하는 정책이다. 프로세스가 요청할 때 가져오는게 일반적이지만, 미리 가져오는 prefetch도 있다.

배치 정책

가져온 프로세스를 메모리의 어떤 위치에 올려놓을지 정책이다. 메모리를 같은 크기로 자르는 것을 페이징(paging)이라고 하며, 프로세스의 크기에 맞게 자르는 것을 세그멘테이션(segmentation)라고 한다. 배치 정책은 페이징과 세그멘테이션의 장단점을 파악하여 메모리를 효율적으로 관리할 수 있도록 정책을 만드는 거이다. 시스템의 효율을 좌우하는 매우 중요한 기준이다.

재배치 정책

메모리가 꽉 찼을 때 메모리 내에 있는 어떤 프로세스를 내보낼지 결정하는 정책이다. 앞으로 사용하지 않을 프로세스를 찾아서 내보내는 알고리즘을 교체 알고리즘(replacement algorithm)이라고 한다.

메모리 주소

메모리에 접근할 때는 주소를 이용한다. 따라서 메모리와 주소는 매우 밀접한 관계이다. 메모리 주소는 절대 주소와 상대 주소를 나뉘는데 둘의 차이와 주소를 지정하는 방법이 있다.

CPU의 비트는 메모리 주소 공간(address space)의 크기와도 연관이 있다.

32bit : 메모리 주소를 지정하는 레지스터인 메모리 주소 레지스터(MAR)의 크기가 32bit이므로 표현할 수 있는 메모리 주소의 범위가 0~2^32-1, 총개수가 2^32개이다. 이를 16진수로 나타내면 00000000 - FFFFFFFF이며 총크기는 2^32B, 약 4GB이다. 따라서 32bit CPU 컴퓨터는 메모리를 최대 4GB까지 사용할 수 있다.

64bit : 레지스터의 크기, 버스의 대역폭, 한 번에 처리되는 데이터의 최대 크기 등이 32bit CPU의 2배이다. 따라서 32bit CPU보다 처리 속도가 빠르고 사용할 수 있는 메모리도 크다. 32bit CPU는 0~2^32-1번지의 주소 공간을 제공하지만 64bit CPU는 0~2^64-1번지의 주소 공간을 제공한다. 또한 그만큼 64bit CPU의 메모리는 무한대에 가깝게 사용할 수 있다.

구분32bit CPU64bit CPU
주소 범위0~2^32-1번지0~2^64-1번지
총크기4GB무한의 가깝

어쨋든 메모리가 설치되고 메모리 주소 공간이 있다. 이렇게 설치된 주소 공간을 물리 주소 공간(physical address space)이라고 한다. 물리 주소 공간은 하드웨어 입장에서 바라본 주소 공간으로 컴퓨터마다 그 크기가 다르다. 이와 반대로 사용자 입장에서 바라본 주소 공간은 논리 주소 공간이다.

절대 주소와 상대 주소의 개념

예를들어 사용자 프로세스가 메모리의 사용자 영역 400번지에 올라왔다고 가정하자. 컴파일 방식을 사용하는 프로그램은 컴파일 시 변수의 주소를 0번지 부터 배정한다. 컴파일할 당시에는 변수가 메모리의 어느 위치에 올라가는지 알 수 없기 때문에 0번지부터 배정하고 실제로 실행할때 주소를 조정한다. 만약 사용자 프로세스가 메모리의 400번지에 올라간다면 프로세스 내 변수의 각 주소에 400을 더하는데, 이때 400번지는 절대 주소(absolute address)이다. 실제 물리주소(physical address)를 가리키는 절대 주소는 메모리 관리자 입장에서 바라본 주소이다. 즉 메모리 주소 레지스터가 사용하는 주소로, 컴퓨터에 꽂힌 램 메모리의 실제 주소를 말한다.

메모리 관리자는 절대 주소를 사용하지만 사용자 입장에서 절대 주소는 불편하고 위험하다. 절대 주소를 사용하면 매번 운영체제가 영역을 확인해야 한다. 현재 운영체제가 359번지까지 사용하더라도 운영체제가 좋아지면 그 이상의 주소 범위를 사용할 수도 있다. 또한 운영체제 영역의 주소가 사용자에게 노출되면 실수나 고의적인 조작으로 운영체제 영역을 침범할 수도 있다.

사용자 프로세스 입장에서 운영체제 영역은 어ㅈ차피 사용할 수 없는 공간이다. 또한 운영체제의 절대 주소(물리 주소)를 알 필요도 없다. 상대 주소(relative address)는 사용자 영역이 시작되는 번지를 0번지로 변경하여 사용하는 주소 지정 방식이다. 예를들어 운영체제 영역이 끝나는 지점을 사용자 영역이 시작되는 번지로 지정하여 이를 0번지로 변경하여 사용하는 주소 지정 방식이다. 그래서 항상 0번지부터 시작하는데, 이러한 주소 공간을 논리 주소 공간이라고 부른다.

구분절대 주소상대 주소
관점메모리 관리자 입장사용자 프로세스 입장
주소 시작물리 주소 0번지부터 시작물리 주소와 관계없이 항상 0번지부터 시작
주소 공간물리 주소(실제 주소) 공간논리 주소 공간

상대 주소를 절대 주소로 변환하는 과정

메모리 접근 시 절대 주소를 사용하면 특별한 변환 과정 없이 작업을 할 수 있다. 그러나 상대 주소를 사용하면 상대 주소를 실제 메모리 내의 물리 주소, 즉 절대 주소로 변환해야 한다. 이러한 변환 작업은 프로세스가 실행되는 동안 메모리 관리자가 매우 빠르게 처리한다.

  1. 사용자 프로세스가 상대 주소 40번지에 있는 데이터를 요청한다.
  2. CPU는 메모리 관리자에게 40번지에 있는 내용을 가져오라고 명령한다.
  3. 메모리 관리자는 재배치 레지스터를 사용하여 상대 주소 40번지를 절대 주소 400번지로 변환하고 메모리 400번지에 저장된 데이터를 가져온다.

재배치 레지스터는 주소 변환의 기본이 되는 주소값을 가진 레스터로, 메모리에서 사용자 영역의 시작 주소값이 저장된다.

메모리 오버레이

프로그램의 크기가 실제 메모리(물리 메모리)보다 클 때 전체 프로그램을 메모리에 가져오는 대신 적당한 크기로 잘라서 가져오는 기법을 메모리 오버레이(memory overlay)라고 한다. 오버레이는 중첩시키다라는 뜻으로, 하나의 메모리에 여러 프로그램을 겹겹이 쌓아놓고 실행하는 것을 말한다.

메모리 오버레이의 경우 프로그램을 몇 개의 모듈로 나누고 필요할 때마다 모듈을 메모리에서 가져와 사용한다.메모리 오버레이에서 어떤 모듈을 가져오거나 내보낼지는 CPU 레지스터 중 하나인 프로그램 카운터(PC)가 결정한다. 프로그램 카운터는 앞으로 실행할 명령어의 위치를 가리키는 레지스터로, 해당 모듈이 메모리에 없으면 메모리 관리자에게 요청하여 메모리를 가져오게된다.

특징은

  • 한정된 메모리에서 메모리보다 큰 프로그램이 실행 가능하다.
  • 프로그램 전체가 아니라 일부만 메모리에 올라와도 실행이 가능하다. 프로그램은 개념적으로 한 덩어리이지만 일부분만 가지고도 실행할 수 있다.

스왑

메모리 오버레이를 이용하면 메모리보다 큰 프로그램을 실행할 수 있다. 하지만 처리해야할 문제가 남아 있다. 만약 나눠서 사용한다고 하자. 그러면 남은 프로그램은 어디에 둬야할까? 다시 사용할지도 모르고 아직 작업이 끝나지 않았기 떄문에 별도 공간에 보관해야 한다. 이처럼 메모리가 모자라서 쫓겨난 프로세스는 저장장치의 특별한 공간에 모아두는데 이를 스왑 영역(swap area)라고 부른다. 그리고 스왑 영역에서 메모리로 데이터를 가져오는 작업은 스왑 인(swap in), 메모리에서 스왑 영역으로 데이터를 내보내는 작업은 스왑아웃(swap out)라고 한다. 스왑 영역은 메모리 관리자가 관리한다. 절전 모드도 이와 같은 원리이다.

메모리 분할 방식

메모리를 어떤 크기로 나눌 것인가는 메모리 배치 정책에 해당된다. 메모리에 여러 개의 프로세스를 배치하는 방법은 크게 가변 분할 방식(variable-size partitioning)과 고정 분할 방식(fixed-size-partitioning)으로 나뉜다.

가변 분할 방식

  • 가변 분할 방식 : 프로세스의 크기에 따라 메모리를 나누는 것이다. 효율적이지만 메모리 관리자가 메모리를 관리하는 것이 힘들다. 프로세스의 크기에 맞게 메모리가 분할되므로 메모리 영역이 각각다르다. 한 프로세스가 연속된 공간에 배치되기 때문에 연속 메모리 할당(contiguous memory allocation)라고 한다. 외부 단편화를 사용하는데 이는 메모리 할당 및 해제 작업의 반복으로 중간중간 사용하지 않은 메모리가 있는데 다 합치면 충분하지만 떨어져 있어서 사용 못하는 상황이다.

장점

장점으로는 프로세스를 한 덩어리로 처리하여 하나의 프로세스를 연속된 공간에 배치한다.

단점

메모리 관리가 복잡하다.

고정 분할 방식

  • 고정 분할 방식 : 프로세스의 크기와 상관없이 메모리를 같은 크기로 나누는 것이다. 비효율적이지만 관리하기가 편하다. 큰 프로세스가 메모리에 올라오면 여러 조각으로 나뉘어 배치된다. 또한 나뉘어 배치되기 때문에 비연속 메모리 할당(noncontiguous memory allocation)이라고 한다. 또한 내부 단편화가 발생한다. 내부 단편화를 쓰는데 이는 메모리를 할당할 때 프로세스가 필요한 양보다 더 큰 메모리가 할당되어서 프로세스에서 사용하는 메모리 공간이 낭비된다.

장점

메모리관리가 수월하다. 가변 분할 방식의 메모리 통합 같은 부가적인 작업을 할 필요 없다.

단점

메모리가 낭비된다.

마지막으로 컴파일러에 의해 만들어진 주소는 상대 주소라는 것을 잊지 말자!

코드 관점의 Memory

Stack

먼저 java에서의 stack 영역을 살펴보자.

public class Main {
	public static void main(String[] args) {
    	int a = 100;
        a = wow(a);
    }
    
    public static int wow(int num) {
    	int b = num * 4;
        return b;
    }
}

여기서 먼저 스택영역에 쌓이는 순서를 보면 main stack frame에 args,a순으로 쌓인 뒤 wow(a)가 호출되는 순간 wow stack frame이 새로 생겨 다시 그 프레임안에 b를 쌓는다. 그리고 return b를 만나는 순간 stack frame도 사라지며 a = wow(a); 부분에 값이 리턴값으로 변경된다.

Heap

public class Main {
	public static void main(String[] args) {
    	Counter c = new Counter();
    }
}

public class Counter {
	private int state = 0;
    public void increment() { state++;}
    public int get() {return state;}
}

스택과 동일하게 args를 먼저 스택프레임에 만든다. 그리고 new Counter()생성자도 하나의 함수이기 때문에 새로운 스택 프레임을 만든다.

그리고 heap영역에 Counter객체가 생성이 되고 state는 위와 같이 0이다. 그리고 스택 프레임을 제거하고 c라는 이름의 변수에 객체의 주소를 담게된다.

즉 stack에는 변수이름과 주소값, heap영역에 실제 객체를 저장한다.

heap메모리에 접근이 불가능한 객체를 보통 쓰레기 객체라고 한다. 보통 직접 해제를 해주거나 가비지 컬렉터의 도움을 받아야 한다.

파이썬 예제

df wow(num):
	print(num)
a = 1
wow(a)

파이썬은 스택영역에 글로벌 프레임이 존재한다. 그리고 파이썬은 모든거싱 객체이기 떄문에 a는 글로벌 프레임에 1은 heap영역에 저장되며 a는 1을 가리키는 주소값이 들어가게 된다.

0개의 댓글