티스토리 뷰
주소(Address)란 ?
주소는 서로 다른 위치를 구분하는 식별자를 말합니다. 실생활에서 우리가 쓰는 주소의 개념과 동일합니다. 주소는 식별자로서 중복될 수 없으며 하나의 대상을 고유하게 표현할 수 있습니다.
그러면 메모리 주소는 무엇일까요? 메모리 공간 내의 서로 다른 위치를 가리키기 위한 식별자를 말하는 거겠죠?
아차, 여기서 우리가 다루는 메모리 공간은 가상 메모리 공간을 의미합니다. 물리적인 메모리에 대해서 서술할 때는 "물리 메모리"라고 접두사를 붙이겠습니다.
컴퓨터는 이러한 메모리 주소를 2진수로 표현하고, 컴퓨터 시스템이 채택한 주소 체계가 32비트 또는 64비트냐에 따라 메모리 주소의 표현 범위가 달라지게 됩니다. 이러한 메모리 주소의 단위는 어떻게 될까요? 컴퓨터는 메모리 주소를 바이트 단위로 관리합니다.
그러면 여기서 궁금한 점이 생기시겠죠. 32비트 주소 체계일 때와 64비트 주소체계일 때 메모리 공간의 크기가 어떻게 될까요 !?
32비트 기준으로 2^32 byte = 2^2 * 2^30 = 4 GB 만큼이 되겠네요. 그러면 64비트는 !? 직접 계산을 해보시길 바랍니다.
주소 바인딩(Address Binding)
프로그램이 실행되기 위해서는 프로그램이 물리 메모리에 적재되어야 합니다. 그렇게 되면 해당 프로세스를 위한 독립적인 주소 공간이 생성이 되는 거죠.
이때의 주소 공간에 할당되는 주소를 논리적 주소 또는 가상 메모리 주소라고 부릅니다. 저희는 앞으로 가상 메모리 주소라고 부르겠습니다.
신기하게도 CPU는 자기와 동일한 물리 하드웨어인 물리 메모리 주소를 사용하지 않고 프로세스마다 독립적으로 가지는 주소 공간 내의 가상 메모리 주소를 사용합니다. 여기서 물리 메모리 주소란 물리 메모리 주소 공간에서의 주소를 의미합니다.
앞서 말했듯이 CPU는 프로세스의 가상 메모리 주소를 사용하는데, 프로그램은 현재 물리 메모리에 적재되어 있습니다. 그래서 필연적으로 서로 다른 두 공간 사이에 주소 변환이 필요하게 되고, 그 과정을 주소 바인딩이라고 합니다.
이러한 주소 바인딩 기법에는 대표적으로 컴파일 타임 바인딩, 로드 타임 바인딩, 런타임 바인딩 이 있습니다. 이러한 바인딩 기법에 대해서 알아보도록 하겠습니다.
컴파일 타임 바인딩(Compile-time Binding)
컴파일 타임 바인딩은 말 그대로 컴파일 타임에 주소 바인딩을 수행하는 기법을 말합니다. 따라서 컴파일 시점에 프로그램이 물리 메모리 주소 공간 중 어디에 할당되어질지 결정이 되어야 합니다. 이를 절대코드를 생성하는 바인딩이라고도 합니다.
어, 그러면 적재하고 싶은 물리 메모리 주소 공간을 바꾸고 싶으면 어떡하지라는 생각이 들 수도 있습니다. 해결 방법은 여러분이 생각하는 그 방법이 맞습니다. 매번 컴파일을 다시 해야 합니다. 이는 매우 비효율적이며 컴파일 타임의 단점입니다.
로드 타임 바인딩(Load-time Binding)
로드 타임 바인딩은 프로그램이 실행될 때 주소 바인딩을 수행하는 기법을 말합니다. 이때 주소 바인딩 작업은 로더의 책임이며, 한번 실행되어서 발생된 주소 바인딩에 의해 결정된 프로그램들이 올라간 물리 메모리 주소 들은 절대 변하지 않습니다.
여기서 로더는 프로그램을 물리 메모리에 적재시키는 프로그램을 말합니다.
이 방식을 사용하기 위해 전제조건이 필요한데, 로드 타임 바인딩 기법을 사용하기 위해서는 해당 프로그램을 컴파일하는 컴파일러가 재배치 가능 코드를 생성하는 방식을 지원하여야 합니다.
런타임 바인딩(Run-time Binding)
런타임 바인딩은 프로그램이 실행된 이후에도 프로그램이 올라간 물리 메모리 주소들이 로드 타임 바인딩처럼 고정되는 것이 아니라 변경될 수 있는 바인딩 기법을 말합니다.
매핑된 물리 메모리 주소들이 변경될 수 있기 때문에, 변경되어도 물리 메모리 공간 어디로 옮겨졌는지를 알아야 하는데 문제가 발생합니다. 따라서 이것을 해결하기 위해서는 별도의 하드웨어적인 소자를 두어 처리합니다.
위와 같은 역할을 해주는 하드웨어를 MMU(Memory Management Unit) 라고 하며 가상 메모리 주소를 물리 메모리 주소로 변환하여 주는 하드웨어 장치입니다.
MMU의 역할을 간단하게 살펴보자면, MMU는 프로그램이 물리 메모리에 적재된 시작점을 가리키는 기준 레지스터와 프로그램의 메모리 주소 공간의 크기를 담고 있는 한계 레지스터를 이용하여 프로세스가 독립적으로 가지고 있는 가상 메모리 공간상의 가상 메모리 주소를 계산하게 됩니다.
그림으로 간단하게 살펴보자면 다음과 같습니다.
여기서 기준 레지스터와 한계 레지스터를 사용하는 이유는 주소 바인딩과 관련되어 있다기보다는 메모리 보안과 관련이 있습니다.
물리 메모리에는 여러 프로그램이 동시에 올라와 있게 되고 각 프로그램은 서로의 프로그램에게 영향을 주어서는 안됩니다.
하지만 의도적이든 그렇지 않든 프로그램 내의 코드에서 자신의 프로그램의 주소 공간을 벗어나는 접근을 시도하였고 만약 물리 메모리 상 내 프로그램 주소 공간 다음에 다른 프로그램이 존재한다면 해당 영역에 접근하게 되는 것이므로 이는 크나큰 이슈입니다.
따라 그런 경우는 트랩을 발생시켜 해당 프로그램을 종료하는 등의 조치를 취합니다. 위와 같은 이유로 프로그램 시작 기준점과, 프로그램의 크기(한계)를 통하여 프로그램에게 주어진 영역 내의 접근만을 허용하도록 만들어주는 것이 기준 레지스터와 한계 레지스터의 역할입니다.
용어 정리
이러한 메모리 관련된 기법들을 공부하다보면 자주 나오는 용어들이 생기게되는데, 용어를 모르면 문맥 파악이 힘들기때문에 몇가지 용어들을 짚고 넘어가도록 하겠습니다.
동적 로딩(Dynamic Loading)
동적 로딩이란 여러 프로그램이 동시에 물리 메모리에 적재하여 수행하는 방법인 다중 프로그래밍에서 물리 메모리의 사용 효율성을 높이기 위해 고안된 기법입니다.
앞에서 살펴볼 때 프로그램이 물리 메모리에 적재된다고 한 것은 프로그램의 전체가 연속적으로 물리 메모리에 올라가는 것을 전제로 하였습니다. 하지만 그렇게 될 경우 당장 필요하지 않는 프로그램의 영역까지 올리게 되는 것이고 이는 어떻게 보면 메모리 측면으로는 비효율적일 수 있습니다.
따라 프로그램을 물리 메모리에 적재할 때 전부 적재하는 것이 아니라 필요한 그 부분만을 메모리에 적재하는 동적 로딩을 기법을 사용하면 효율적으로 단일 프로그램 측면뿐만 아니라 다중 프로그램 측면에서도 효율성을 높일 수 있습니다.
이러한 기법은 운영체제의 특별한 지원 없이 프로그램 자체에서 구현할 수 있다는 특징이 있고 또한 운영체제가 라이브러리 통해 지원할 수도 있습니다.
동적 연결(Dynamic Linking)
동적 연결을 알아보기 전에 연결이라는 것이 어떠한 의미를 가지는지를 알아보겠습니다.
연결이란 우리가 작성한 소스 코드를 컴파일하여 생성된 목적 파일들과 이미 컴파일되어 있는 라이브러리 파일들을 묶어 실행파일을 생성하는 일련의 과정을 말합니다. 여기서 주의 깊게 봐야 되는 것은 묶어서 생성입니다.
앞서 우리가 동적이라는 개념을 도입한 이유는 필요한 그 순간에 적재하여 사용하여 메모리 등의 효율을 높이기 위해서입니다. 그런데 일반적인 연결의 경우는 사용하기 전에 무거운 기타 다른 파일들까지 묶어서 실행파일로 생성되기 때문에 이는 어떻게 보면 비효율적인 부분이 있습니다.
그러면 동적 연결이라는 것이 어떤 과정인지 왜 사용하게 되는지도 알 수 있게 되었습니다.
동적 연결이란 컴파일을 통해 생성된 위와 같은 목적 파일과 라이브러리 파일들을 묶는 과정을 프로그램의 실행 시점까지 지연(Lazy) 시키는 기법입니다. 즉 라이브러리를 필요할 때 연결된다는 것입니다.
어떻게 연결되는지 세부과정은 앞서 동적 연결을 왜 도입했고, 그 효과가 무엇인지에 비해 의미가 없다고 생각하여 따로 적진 않으려고 합니다. 궁금하시다면 찾아보시는 걸 추천드리며 다만 이러한 동적 연결 기법은 운영체제의 지원이 필요합니다.
중첩(Overlay)
중첩이란 프로스의 주소 공간을 분할하여 실제 필요한 부분만을 물리 메모리에 적재하는 기법을 말합니다. 이렇게 보면 앞서 살펴본 동적 로딩과 무엇이 다르지라고 당연히 생각이 들 수 있습니다.
이는 간단합니다. 목적은 동일하지만 사용하게 된 이유가 다릅니다.
동적 로딩의 경우는 더욱 효과적인 다중 프로그래밍을 위해서이고, 중첩의 경우는 단일 프로세스 환경에서 물리 메모리 영역보다 더 큰 프로세스를 올리기 위하여 어쩔 수 없기 때문에 고안되었습니다.
그리고 동적 로딩의 경우는 운영체제가 라이브러리를 통해 지원할 수 있지만 중첩의 경우는 오로지 해당 프로그램을 작성하는 프로그래머가 하나하나 구현하여야 했습니다.
스와핑(Swapping)
스와핑이란 물리 메모리에 올라온 프로세스의 주소 공간 전체를 디스크의 스왑 영역으로 일시적으로 내려보내는 것을 말하며 이때 디스크의 스왑 영역을 백킹 스토어(Backing Store)라고도 합니다. 이는 디스크 내에 파일 시스템과는 별도로 존재하는 영역입니다.
이때 스왑 영역으로 일시적으로 내려보내는 것을 스왑-아웃, 스왑 영역으로 내려져있던 프로세스의 주소 공간을 다시 물리 메모리에 올리는 것을 스왑-인이라고 합니다.
프로그램 전체를 내려보내는 것과 올리는 것을 행위를 스왑이라고 하는 것이지 나중에 살펴볼 페이징 시스템에서 페이지 단위로 내려보내는 것과 올리는 것을 스왑이라고 하지 않습니다.
이러한 스왑 과정을 그림으로 살펴보면 다음과 같습니다.
위 그림에서 눈치챘을 수도 있는데, 스왑-아웃이 일어난 프로세스를 다시 스왑-인하는 과정에서 물리 메모리 공간 중 어디에 다시 올려야 하는지에 대해서 의문을 품을 수 있습니다. 그 점이 스왑 과정에서 일어나는 문제입니다.
앞서 살펴본 컴파일 타임 바인딩과 로드 타임 바인딩을 사용한다면 원래 존재하던 물리 메모리 주소에 그대로 스왑-인 하여야 할 것입니다. 왜 그렇지라고 생각한다면 앞에서 설명했던 각 바인딩에 대해서 다시 보고 생각해보시길 바랍니다.
두 바인딩 기법과는 다르게 런타임 바인딩에서는 스왑-인을 할 때 원래 존재했던 영역이 아닌 물리 메모리 영역 중 빈 곳에 자유롭게 적재하면 된 다는 것을 알 수 있을겁니다.
이러한 스와핑을 알아봤는데, 이러한 스와핑이 언제 일어나는지, 왜 필요한지에 대한 궁금증이 남았을 거라고 생각합니다.
그것은 CPU 스케줄링과 관련 있으며 그 중 중기 스케줄러를 공부해보면 알 수 있게 될 것입니다.
마무리
메모리 관리는 알아야 하는 부분이 많은 파트입니다. 따라서 해당 글에 모두 담기가 어려워 2편으로 나누어 작성하고자 합니다. 다음 편에서 포스팅하려는 내용은 이렇게 물리 메모리에 어떻게 적재, 즉 할당되는지에 대해서 알아보고자 합니다.
미리 알아보면 좋은 내용은 다음과 같습니다.
- 물리 메모리 할당 방식
- 페이징 기법
- 세그멘테이션 기법
- 페이지드 세그멘테이션 기법 (페이징 기법 + 세그멘테이션 기법)
- Total
- Today
- Yesterday
- 우선순위큐
- 해쉬
- 스택
- 알고리즘
- Java
- dsu
- dfs
- 연결리스트
- 정렬
- set
- Uber
- 구현
- 쓰레드
- dp
- sql
- 카카오
- 회고
- 코딩인터뷰
- 탐욕법
- 프로그래머스
- BFS
- 문자열
- 스트림
- 오늘의집
- 코드 스니펫
- kotlin
- TDD
- 비트연산
- JPA
- k8s
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |