컴퓨터(Computer Science)/운영체제(Operation System)

Memory Management, 메모리 관리 기본 개념 [운영체제]

게임이 더 좋아 2020. 6. 3. 11:57
반응형
728x170

 

 

메모리에 대해서 알아볼 것이다.

 

CPU는 PC(프로그램 카운터)가 지시하는 대로 메모리로부터 다음 수행할 명령어를 가져온다.

그 명령어는 추가적인 데이터가 필요하거나, 데이터를 메모리로 내보낼 수 있다.

 

명령어 실행은 메모리로부터 명령어를 가져오고, 해독하고

메모리에서 피연산자를 가져오고 그래서 실행한 후, 결과를 메모리에 저장한다.

 

메인 메모리와 각 처리 코어에 내장된 레지스터들은 CPU가 직접 접근할 수 있는 유일한 저장장치이다.

Instruction들은 메모리 주소만을 인수로 취하고, 디스크의 주소를 인수로 취하지 않는다.

** Disk에 있다면 CPU가 쓸 수 없다는 말이다.

 

++일반적으로 CPU 코어에 내장된 레지스터들은 일반적으로 CPU clock의 cycle에 맞춰서 접근한다. 

메모리 버스를 통해 전송되는 메인 메모리의 경우 접근을 하기 위해서는 CPU clock이 많이 필요하고 이렇게 되면 필요한 데이터가 없어서 명령어를 수행하지 못하는 , stall 상태가 될 수 있다.

 

다시 말하면 메인 메모리는 주소를 기반으로 접근을 허용하는데

 

실제로 메모리의 주소는 따로 있지만

OS에서는 다르게 동작한다 바로 논리적 주소, Logical Address를 만들어낸다.

 **CPU도 Logical Address로 동작을 한다.(실제 주소를 refer해서 동작하는 것은 아니다)

뒤에서 나오겠지만 컴파일된 Instruction을 바탕으로 논리적 주소에 대해 연산을 수행하면 MMU가 해당하는 물리적주소에 매핑을 해주어 CPU가 동작하게 도와준다.

 

 

**Symbolic Address는 사용자가 알고 있는 주소를 말한다. (프로그래머가 짠 코드)

컴파일이 될 때 그제서야 바뀌게 된다.

 

논리적 주소가 물리적 주소가 되는 것을 Address Translation이라고도 하는데 여기선 바인딩이라고도 한다.

 

 


 

 

그렇다면 바인딩 또는 Translation이 언제 이루어지느냐?

 

운영체제가 프로세스를 실제로 물리 메모리에 어떻게 적재하냐부터 알아보자

 아래 그림같이 한다.

 

 

그림을 보면

3가지 시점이 있다.

Compile, Load, Execution

 

 

 

크게 3가지 시점으로 볼 수 있다.

 

1. 컴파일 시에 결정됨

만일 프로세스가 메모리 내에 들어갈 위치를 컴파일 시간에 미리 알 수 있으면 컴파일러는 절대코드를 생성할 수 있다. 예를 들면 사용자 프로세스가 R번지로부터 시작한다는 것을 미리 알면 컴파일러는 번역할 코드를 그 위치에서부터 시작한다. 

2. 프로그램이 시작될 때 (Load)

만일 프로세스가 메모리 내 어디로 올라오게 될지를 컴파일 시점에 모르면, 컴파일러는 일단 이진 코드를 재배치 가능코드로 만들어야 한다. 이 경우에 심볼과 진짜 번지수와의 바인딩은 프로그램이 메인 메모리로 실제로 적재되는 시간에 이루어지게 된다. 이렇게 만들어진 재배치 가능 코드는 시작 주소가 변경되면 아무 때나 사용자 코드를 다시 적재하기만 하면 된다.

 

3. 프로그램이 실행 중 (Runtime)

만약 프로세스가 실행하는 중간에 메모리 내의 한 세그먼트로부터 다른 세그먼트로 옮겨질 수 있다면, 바인딩이 실행 시간까지 허용되었다고 말할 수 있다.

//특별한 하드웨어만 이용 가능하다.

 

 

아래 예시를 보면서 이해해보자

맨 왼쪽이 사람이 짠 코드이다.

어떻게 물리적 주소가 되는지 알아보자

 

 

우리가 변수 A에 저장한 것이라고 알아들었지만

컴파일 되면 해당 A는 주소로 바뀌어 연산이 된다.

실제 실행은 컴파일 단계에서 이루어지지 않고 

물리적 메모리에 적재된 후에 가능하다.

 

그래서 위 3가지 방법을 알아보면

Complie time binding 에서는 compile 시점과 같은 것을 볼 수 있다.

즉, 컴파일 시점에 물리적 주소도 결정되는 것이다. (논리적 주소 = 물리적 주소)

**하지만 현재에는 비효율적이라 쓰지 않는다. (병렬 실행 시스템 하에서 순서대로 적재 시 파편화 발생)

 

Load time binding 은 컴파일 당시 논리적 주소와 조금 바뀐 것을 알 수 있다.  

 

Run time binding  도 마찬가지다. 실행 도중에 주소가 바뀌기도 한다.

**하지만 그럴 때면 CPU가 binding을 점검해야한다. MMU가 지원해준다.

 

 


 

그렇다면 해당 binding, tranlation은 누가 하는가?

 

OS가 하지 않는다. 
특정 device를 고안하여 이용한다.

그것이 바로 MMU이다 Memory Management Unit으로 실제로 논리적 주소를 물리적 주소에 매핑해주는 device다.

**MMU가 매핑해주는 덕분에 우리는 물리적 메모리의 주소를 알아야 할 필요가 없어진다.

 

 

 

시스템이 올바르게 동작하기 위해서는 OS 영역을 보호해야 할 뿐만 아니라 사용자 프로그램 사이도 보호해야 한다.

// 운영체제가 CPU와 메모리 간의 접근 중에 개입하게 되면 성능 하락이 된다.

 

즉, 프로세스가 독립된 메모리 공간을 가지도록 보장해야 한다.

개별적인 프로세스별 메모리 공간은 서로를 보호하고 병행 실행을 위해 여러 프로세스가 메모리에 적재되게 하는 것이 필수적이다.

 

다시 말해서 논리적 주소가 물리적 주소로 매핑될 때 에러가 생기지 않도록 공간을 보장해야한다는 말이다.

아래 그림을 보자

 

개별적인 메모리 공간을 분리하기 위해서 기준을 정해놔야 한다.

 

 

그림을 보면 base(기준), limit(상한)이 존재한다.

**메모리 공간의 보호는 CPU 하드웨어가 사용자 모드에서 만들어진 모든 주소와 레지스터를 비교함으로써 이루어진다.

 

 

그림으로 어떻게 되는지 보자면

 

 

CPU가 컴파일된 instruction을 읽으며 논리적 주소에 접근해야한다면 

논리적 주소를 받아 MMU 가 계산해서 물리적주소에 매핑해준다.

 

**여기서 relocation register에는 해당 프로세스의 시작 위치(주소)가 담겨있다. 

즉, 해당 시작 위치에서 CPU가 실행 중인 위치를 더하게 되면 실제 물리적 주소에서의 위치와 같아지는 것이다.

 

**limit register는 해당 프로그램의 크기를 담고 있어서 해당 프로그램이 아닌 주소에 접근하는 것을 막는다.

예를 들어 CPU가 4000 논리적 주소를 실행한다면 해당 limit register에 의해 trap이 걸린다.

다시 말해서 사용자 모드에서 수행되는 프로그램이 운영체제의 메모리 공간이나 다른 사용자 프로그램의 메모리 공간에 접근하면 운영체제는 치명적인 오류로 간주하고 트랩(trap)을 발생시킨다.

 

**그렇지만 커널 모드에서 수행되는 운영체제는 운영체제 메모리 영역과 사용자 메모리 영역의 접근에 어떠한 제약도 받지 않는다.

 

 

MMU의 매핑 과정에 대해 더 자세히보자면

 

 

limit register부터 체크한 다음 해당 relocation register와 연산을 하는 것이다.

 

**위와 같은 방식은 한 번에 모든 프로그램을 메모리에 적재할 때나 가능한데

현재 그렇게 하는 방법은 쓰이지 않고 있다. 

사용하는 부분만 적재해서 사용하는 방법을 채택하고 있다.

 

 


 

메모리에서 빠질 수 없는 용어들이 몇가지 있다.

바로 아래 4가지 용어인데 차례대로 알아보자

 

 

 


 

Dynamic Loading 이란 동적 적재를 말한다.

 

 

Runtime에서 loading이 일어난다는 뜻이다.

해당 Memory가 쓰이지 않는데도 적재되어 있으면 메모리 낭비기 때문에 좋지 않다.

EX) 프로그램 특정상황에만 사용되고 말 것들

때문에 동적 적재라는 것을 쓰는 것이다.

굳이 쓰이지 않으면 올리지 않는다. 쓰일 때만 올린다. 이런느낌이다.

 

**여기서는 프로그래머가 직접 하는 부분이다. (라이브러리 이용)

OS에서 페이징을 조절하여 fault나서 replace하는 것과는 조금 다르다.

하지만 용어가 겹쳐서 쓰여서 구분을 굳이 해야하는가..? 라는 의문이 있다.

 


 

Dynamic Loading과 조금 구분해야하는 용어 중 Overlay라는 것이 있다.

 

 

Overlay와 거의 개념이 같지만

조금 다르다.

굳이 알 필요는 없지만 지식이라면 지식이니까..

작은 공간의 메모리를 가지는 컴퓨터의 초창기 시절

메모리에 모든 부분을 적재할 수 없는 상황으로 인해

프로그래머가 직접 어떻게 메모리에 적재를 할 지 결정하는 것이다.

 

 


다음 용어는 Swapping이다.

스와핑이라고 한다.

 

 

프로세스를 Backing store,  Secondary storage같은 곳으로 쫓아낸다고 보면 되는데

 

메모리에 너무 많은 프로그램이 올라와 있어서 degree of multiprogramming이 높아질 때

시스템의 성능의 저하가 된다. 때문에 스와핑을 통해 정도를 낮추고 성능을 개선한다.

이 때 메모리에서 내려가는 프로세스는 CPU 우선순위가 낮은 프로세스가 될 것이다.

 

 

해당 프로세스가 쫓겨났다가 다시 올 때

Compile time binding 이나 Load time binding일 때에는

해당 메모리 주소가 정해져있으므로 프로세스가 다시 실행되기 위해선 쫓겨난 위치에 다시 적재가 되어야 한다.

하지만 그렇게 활용하기에는 쉽지 않기 때문에

Runtime binding이 될 때 스와핑이 효율적으로 동작할 수 있다.

 


 

다음은 Dynamic Linking이다.

동적으로 연결한다는 의미인데 Dynamic Loading이라고도 쓴다.

굳이 Linking과 Loading을 구분하자면 할 수 있지만 해야하는 이유가 없다.

**동적 적재에서는 로딩(loading)이 실행 시까지 미루어졌었지만 동적 연결에서는 연결(linking)이 실행 시기까지 미루어지는 것이다.

 

일반적으로 프로세스가 실행되기 위해 그 프로세스 전체가 미리 메모리에 올라와 있어야 했다.

**그렇다면 프로세스의 크기가 메모리의 크기보다 커서는 안된다.

 

하지만 전체를 올려놓는 것은 실제로 memory utilization의 저하를 가져왔다.

그래서 고안해낸 방법이다.

 

 

static linking을 보면서 비교해보자면

static은 라이브러리 같은 것들이 실행 파일에 포함시켜져서 만들어지는 반면에

Dynamic은 실행 시에 연결되기 때문에 static 보다 메모리 면에서 이득이 있다.

해당 위치를 찾기 위한 코드만 두면 알아서 찾아서 실행이 된다.

 

다시 말하면

동적 적재에서 각 루틴은 실제 호출되기 전까지는 메모리에 올라오지 않고 재배치 가능한 상태로 디스크에서 대기하고 있다.

먼저 main 프로그램이 메모리에 올라와 실행된다. 

이 루틴이 다른 루틴을 호출하게 되면 호출된 루틴이 이미 메모리에 적재됐는지를 조사한다.

만약 적재되어 있지 않다면 재배치 가능 연결 적재기(relocatable linking loader)가 불려서 요구된 루틴을 메모리로 가져오고 이러한 변화를 테이블에 기록한다. 

그후 CPU 제어는 중단되었던 루틴으로 보내진다.

** 루틴이 필요한 경우에만 적재되어 효율적이다. 

 

(.dll 이라는 확장자를 많이 봤으면 익숙할 것이다.)

DLL이란 사용자 프로그램이 실행될 때, 사용자 프로그램에 연결되는 시스템 라이브러리이다.

이 기능이 없으면 시스템의 각 프로그램은 실행 가능 이미지에 해당 언어 라이브러리의 사본을 포함해야 한다.

//위 요구는 실행 가능 이미지의 크기를 증가시킬 뿐만 아니라 메인 메모리를 낭비할 수 있다.

 

DLL의 장점 또 하나는 라이브러리를 여러 프로세스 간에 공유할 수 있어 메인 메모리에 DLL 인스턴스가 하나만 있을 수 있다는 것이다.

++ 그래서 DLL을 공유 라이브러리라고도 부른다.

 

 

 

 

728x90
반응형
그리드형