메모리 계층 구조
https://camuscoding.tistory.com/8
[운영체제]1-2 운영체제의 태동
지난 포스팅에서 운영체제가 무엇인지 알기 위해 운영체제의 개념에 대해 알아보았다. 그럼 이 운영체제는 어떻게 생겨나게 된 것일까? 1. 고정 프로그램 컴퓨터 시대 - 1940 년대이 당시 컴
camuscoding.tistory.com
운영체제에 대해 공부하며 '메모리'를 꾸준히 언급해왔고, 메모리 없이 운영체제를 설명하는 것은 불가능할 것이다. 폰노이만의 내장형 프로그래밍 개념부터 지금까지 메모리는 CPU와 함께 컴퓨터를 이루는 필수적인 부품으로 자리잡았다.
이제부터 그 메모리가 무엇인지에 대해 알아보도록 하겠다.
메모리란 CPU가 실행할 프로그램 코드와 데이터를 저장하는 물리 장치를 의미한다. CPU가 실행할 프로그램의 코드와 데이터를 적재해두고, 실행 중 발생하는 데이터를 저장한다.
메모리의 종류에는 CPU의 레지스터, 캐시 메모리, 메인 메모리, 보조 기억장치 등이 있는데, 일반적으로 메인 메모리(RAM)을 메모리라고 부른다.
메모리 장비들은 읽기/쓰기의 속도와 용량에 따라 계층 구조를 이루는데 이를 메모리 계층구조라고 한다.

이렇게 메모리 계층 구조가 생기게 된 이유에는 기술의 발전에 있다.
CPU와 메인 메모리가 발전하면서 CPU의 속도를 메인 메모리가 따라가지 못하게 되었고, 이 간극을 줄이기 위해선 메모리 엑세스 시간을 단축 시켜야 했다.
이때 생겨난 것이 캐시메모리인데 CPU가 현재 실행중인 프로세스의 코드와 데이터를 당장 실행할 일부를 메인 메모리에서 가져와두어 메모리 엑세스 시간을 줄여주었다.
처음에는 off-chip 캐시메모리를 사용했지만 CPU안에 캐시를 두는 on-chip 캐시 메모리가 등장해 더 빠르게 메모리 엑세스가 가능해졌다.
즉 메모리 계층 구조의 목적은 CPU의 메모리 엑스스 시간을 줄이기 위함이다.
메인 메모리는 현재 실행중인 모든 프로세스의 코드, 데이터, 읽고 쓰는 여러 파일들 운영체제의 커널 코드와 데이터를 적재한다.
프로그램의 실행은 운영체제가 보조 기억장치에서 실행할 파일을 메인 메모리에 적재하는 과정에서 시작된다. 이 메인 메모리에서 L3 캐시메모리로, 그리고 L1/L2 캐시 메모리로, 마지막으로 CPU 레지스터에 데이터가 적재되며 프로그램이 CPU에 의해 실행된다.
* 참고로 캐시메모리는 옵션이기 때문에 컴퓨터 사양에 따라 없을 수도 있을 수도 있다.
메모리 계층 구조 간 적재 과정을 더 자세히 살펴보겠다.
Cache <-> RAM
- CPU가 현재 프로세스나 스레드의 실행을 중단하고 다른 프로세스의 스레드를 실행하게 되면 실행하는 프로세스/ 스레드의 명령이나 데이터를 캐시에서 찾을 수 없는 상황이 벌어지는데, 이를 Cache Miss라고 한다. 이러면 다시 RAM에서 캐시로 새로 실행할 프로세스의 명령/데이터 등을 적재 해야한다. 하지만 캐시에 있는 데이터 중 값이 수정되어 변경된 데이터는 다시 메모리에 변경 내용을 기록해야 하기 때문에 프로세스나 스레드의 컨텍스트 스위칭은 보이지 않는 추가적인 오버헤드를 유발하는 것이다.
RAM <-> SSD(보조 기억장치)
- 프로세스가 새로운 메모리 할당을 요구하거나 새로운 프로세스를 실행시키기 위해 메모리가 필요할 때 운영체제는 RAM의 빈 영역을 할당해준다. 그런데 RAM이 다 사용중이거나 빈 영역이 일정 크기 이하여서 메인 메모리의 영역을 할당해주지 못하는 상황이 발생하게 될 것이다. 이럴 때는 메인 메모리에 적재된 코드나 데이터의 일부분을 하드디스크나 SSD 에 저장하고 메인 메모리의 공간을 확보하기도 한다. 이렇게 메인 메모리의 영역을 보조 기억장치까지 확장하는 기법을 '가상 메모리'라고 한다. 이에 대해서는 이후에 더 자세히 알아볼 것이다.
그런데 이렇게 메모리 계층화시키는 것이 정말 메모리 엑세스 시간을 줄여줄까? 효율적일까?
그렇다. 그 이유는 프로그램 실행의 특성인 '참조의 지역성' 때문인데 코드, 데이터, 자우너 등이 아주 짧은 시간 내에 다시 사용된다는 것이다.
메모리 관리
메모리는 컴퓨터의 핵심 공유자원이다. 프로그램이 실행되는 공간이기 때문에 한정적이지만 실행되기 위해선 필수적으로 필요한 공간이기 때문이다. 메모리는 운영체제에 의해 관리되는데 그 이유는 다음과 같다.
1. 메모리는 여러 프로세스에 의해 공유되는 공유자원이다. 프로세스 별로 할당된 영역과 비어있는 메모리 영역을 운영체제가 관리하며 여러 프로세스가 메모리를 나눠서 사용하도록 관리해야 한다.
2. 메모리를 보호하기 위함이다. 프로세스에게 할당한 공간을 다른 프로세스가 접근하지 못하도록 막고, 사용자 모드에서 커널 공간에 접근하지 못하도록 막아 충돌하지 않도록 한다.
3. 메모리 용량의 한계를 극복하기 위함이다. 설치된 메모리는 크기가 한정되어 있지만 설치된 메모리보다 더 큰 메모리를 요구하는 프로세스가 존재하거나, 용량은 적지만 다수의 프로세스를 실행하는 경우가 발생할 수 있는데, 이럴 때 가상 메모리같은 메모리 관리 정책을 운영체제가 실행하여 메모리 용량의 한계를 극복해준다.
4. 메모리 사용의 효율성을 위해서이다. 정해진 양의 메모리 안에서 다수의 프로세스를 효과적으로 실행하기 위해 운영체제가 메모리를 관리한다.
이를 정리하면
메모리를 할당 - 여러 프로세스가 메모리를 나누어 사용하도록 한다
메모리를 보호 - 프로세스가 다른 프로세스의 영역이나 운영체제의 영역을 침범하지 않게 한다
을 위해 운영체제가 메모리를 관리한다고 생각하면 된다.
메모리 주소
메모리에 어떻게 접근할 수 있을까?
메모리는 오직 '주소'를 이요해서만 접근할 수 있는데 주소에는 물리주소와 논리/가상 주소가 있다.
우선 간단히 설명하면
물리주소란 실제 메모리(하드웨어 주소)를 의미하는데, 물리 메모리에 매겨진 주소를 말한다. 이는 컴퓨터를 설계하고 제작하는 시점에서 물리 메모리에 매겨지는 하드웨어적인 주소이며 0번지부터 시작한다.
논리/가상주소란 프로그램 내에서 사용되는 주소를 의미하는데, 프로세스 내에서 코드나 데이터(변수나 동적할당)의 주소이다.
더 자세히 알아보자
물리 메모리는 컴퓨터 시스템 내부에 뻗쳐 있는 주소 버스에 연결되어 있다. CPU 칩에서 발생된 이진 신호의 물리 주소가 주소 버스에 실려 물리 메모리에 전달된다.

논리주소는 프로세스가 적재된 물리메모리와 관계 없이 컴파일러에 의해 프로세스 내에서 매겨진 주소이다. 즉 프로세스의 시작점이 논리주소 0번지라는 것이다. 코드나 변수의 상대 주소는 이 0번지의 상대주소이다.
논리주소는 프로세스나 사용자에게 2가지 착각을 하게한다.
1) 모든 메모리를 독점하고 있다는 착각
2) 프로세스의 코드/변수들이 0번지부터 연속적으로 메모리에 적재되어 있다는 착각
이것이 논리주소가 가상주소라고 불리는 이유이다.
따라서 CPU가 프로세스를 실행시키는 동안 다루는 모든 주소는 '논리주소'이다. 그렇기 때문에 프로세스내에 적재된 코드와 변수의 논리주소는 항상 동일하게 된다.
위 그림을 보면서 논리주소가 물리주소로 변환되는 과정을 이해해보자.
CPU가 메모리에 엑세스 하려면 당연히 논리주소가 물리주소로 바뀌어야하고, 물리 주소가 물리 메모리에 전달되어야 한다.
논리주소를 물리주소로 바꾸는 역할을 MMU(Memory Management Unit)이 수행하는데, MMU는 프로세스의 논리주소와 물리 주소의 매핑 정보를 담은 매핑 테이블을 참고해 논리주솔를 물리 주소로 바꾼다.
이 덕분에 컴파일러는 사용자가 작성한 프로그램을 논리주소로 컴파일하기만 하면된다, 몇번 물리 주소에 적재될지는 컴파일러 시점에서는 알 수 없고 알 필요도 없는 것이다.
프로그램이 실행을 시작할 때, 운영체제는 프로세스를 생성하고 실행파일로부터 코드와 데이터 등을 물리 메모리에 적재한 후 프로세스 별로 코드와 데이터가 적재된 물리 주소의 매핑 테이블을 만들어둔다.
MMU는 이 매핑 테이블을 이요해 CPU로부터 출력된 논리주소를 물리주소로 변환한다.
매핑 테이블 덕분에 컴파일러는 프로그램을 논리주소로 컴파일 하는데 있어 부담이 없고, 운영체제 또한 프로세스의 코드/데이터를 물리 메모리 빈 곳에 할당/적재할 때 부담이 없다.
위 그림에서는 CPU가 mov ax, 4라는 명령을 시행하는데, 여기서 4는 논리주소 4번지를 의미한다. MMU는 매핑 테이블을 참고해 물리주소로 변환시키는데 해당 프로세스의 논리주소 4번지는 물리주소는 8896번지이다. 물리 메모리에 서 물리 메모리 8892 번지를 찾는데 이곳이 실행할 프로세스의 논리주소 0번지이고, 8896으로 가 논리주소 4번지를 찾게 된다. 그리고 ax에 4번지의 값을 저장한다.
메모리 할당
운영체제가 새 메모리를 실행시키거나 실행중인 프로세스가 메모리를 필요로 할 때 프로세스에 물리 메모리를 할당한다.
그리고 이는 전적으로 운영체제 커널에 의해 이루어진다.
메모리를 할당하는 기법은 다양한데

연속 메모리 할당은 각 프로세스에게 메모리 한 덩어리(연속된 공간)을 할당하는 기법이다.
이는 메모리 전체를 파티션이라는 고정 크리로 나누고 프로세스마다 1개의 파티션을 할당하는 고정 크기 할당 방법과
파티션을 미리 나누지 않고 프로세스의 크기나 요청에 따라 파티션의 크기를 정해 프로세스에게 할당해주는 가변 크기 할당 방법으로 나뉜다.
하지만 연속 메모리 할당은 메모리 전체에 비어 있는 작은 공간(hole)이 존재하게 되고, 이것들이 모이면 작지 않은 메모리 낭비가 발생하게 된다.
이런 연속 메모리의 비유연한 메모리 할당 정책을 극복하고자 분할 메모리 할당 기법이 등장했다.
분할 메모리 할당은 필요한 메모리를 여러 덩어리로 나눠 분산 할당하는 기법이다.
이 또한 가변 크기할당(세그먼트)와 고정크기 할당(페이징)으로 나뉘는데,
가변 크기 할당은 프로세스에게 크기가 다른 여러개의 덩어리 메모리를 할당하는 것이다. 즉 프로세스를 여러개의 논리적인 덩어리로 분할한다. 이 각 덩어리를 segement(프로세스내에서 하나의 단위로 다룰 수 있는 블록)이라고 한다. 이 프로세스를 구성하는 세그먼트를 물리 메모리에 분산할당한다. 이때 보통 프로세스를 코드, 데이터, 스택, 힙 4개의 세그먼트로 분할한다. 각각의 세그먼트들은 크기가 서로도 다르기 때문에 프로세스의 실행 /종료를 반복하게 되면 메모리에 저재된 세그먼트들 사이에 빈 공간이 생기게 된다. 이들 중 일부분은 너무 작아서 다시 사용하지 못하게 되고 외부 단편화라고 불리는 메모리 낭비가 발생한다.
이런 문제를 해결하기 위해 다시 고정크기 할당(페이징)이 생겨났는데, 이는 프로세스에 동일한 크기의 덩어리를 여러개 할당하는 것이다.
즉 프로세스를 논리적 단위가 아니라 논리적 주소 0번지부터 page라 부르는 고정 크기로 분할하고, 물리 메모리 역시 page와 동일한 크기로 나눈다(이를 frame이라고 부른다). 그리고 프로세스의 각 페이지를 물리메모리의 프레임이 하나씩 분산 할당해주는 것이다.
연속 메모리 할당
연속 메모리 할당에 대해 더 자세히 알아보겠다. 연속 메모리 할당은 앞서 말했듯 프로세스 1개에 연속된 메모리 블록을 할당해주는 것인데, 초기 운영체제에서 주로 사용했지만 지금은 잘 사용하지 않는다.
MS-DOS와 같은 단일 사용자 단일 프로세스 시스템에서는 실행중인 프로세스가 전체 메모리를 하나의 연속된 저장공간으로 사용할 수 있었다.
고정 크기 할당은 메모리를 처음부터 파티션이라는 고정크기로 나누고, 프로세스에 1개의 파티션을 할당해주는 것인데, 예를 들어 4KB, 8KB, 16KB 등 몇가지 고정크기 파티션을 만들어두고 프로세스 크기에 적합한 파티션을 할당해주는 것이다.
대표적으로 IBM의 IBM OS/360 MFT가 이 방식을 사용했는데, 메모리를 n개의 동일한 크기의 파티션으로 나누었다. 그러면 동시에 실행시킬 수 있는 프로세스의 개수는 n개로 제한되고, 이를 초과하면 대기큐에서 차례를 기다린다.
프로세스의 크기가 파티션보다 작으면 메모리가 낭비되고, 크다면 실행이 불가능하다는 문제가 있었다. 따라서 실행시킬 프로세스의 크기를 사전에 계산해 파티션 크기를 정해둬야 했다.
가변 크기 할당은 고정 크기 할당에서 생기는 내부 단편화 문제를 해겨하고자 했는데, 프로세스 크기가 서로 다르기 때문에 파티션을 처음부터 나누지 않고 각 프로세스에 동일한 크기의 메모리를 할당해주겠다는 것이었다. 이또한 외부 단편화 문제는 발생했는데
단편화란 프로세스에서 할당할 수 없는 작은 크기의 조각 메모리들이 생기는 현상을 의미한다. 단편화 문제는 사실 어떤 메모리 할당 정책도 피할 수 없기 때문에 이를 줄이는 것이 최선이다.
단편화는 생기는 위치에 따라서 내부 단편화(고정크기 파티션)과 외부 단편화(가변 크기 파티션)로 나눌 수 있다. 내부 단편화란 프로세스에게 할당된 메모리 영역 내에 활용할 수 없는 조각 메모리(hole)이 생기는 상황을 말하고, 외부 단편화는 할당된 메모리 사이에 사용할 수 없는 hole이 생기는 상황을 말한다. 크기가 다른 파티션이 할당되고 반환되면서 파티션 사이에 사용할 수 없는 작은 메모리 공간이 생기게 된다. 이럴 때는 파티션을 이동시켜 홀을 없애는 메모리 압축 기법이 사용되기도 한다.
연속 메모리 할당을 구현하기 위해선 하드웨어와 운영체제의 지원이 필수적이다.
하드웨어는 논리주소를 물리주소로 변환하고 프로세슥 다른 프로세스 메모리에 엑세스하지 못하도록한다.
이를 위해 현재 프로세스에게 할당된 물리 메모리 시작 주소를 기록하는 base레지스터, 현재 프로세스에 할당된 물리 메모리의 크기를 기록하는 limit레지스터, 현재 엑세스하는 메모리의 논리주소를 기록하는 주소 레지스터가 사용된다.
그리고 앞서 알아보았듯이 논리주소와 물리주소의 변환은 MMU가 담당하는데, 논리주소를 물리주소로 바꾸기 전에 다른 프로세스를 보호하기 위해 논리주소를 limit 레지스터와 비교해 할당된 메모리 범위를 넘으면 시스템 오류를 내보내 프로세스를 강제 종료시킨다.
운영체제는 프로세스별로 할당된 물리 메로리의 시작주소와 크기 정보를 저장한다. 그리고 비어있는 메모리 영역을 관리하고 프로세스마다 base, limit 레지스터 값을 적재해둔다.
운영체제가 프로세스를 처음 생성시키거나 프로세스 실행 중 메모리 요청시 공간을 할당해줘야하는데 이를 홀 선택 알고리즘 혹은 동적 메모리 할당이라고 한다.
고정 크기 할당은 비어 있는 파티션 중 하나를 할당해주면 되기에 간단하지만 가변 크기 할당은 프로세스마다 시작주소와 크기를 구성하고 이들을 홀리스트로 관리한다,
그리고 first fit, best fit, worst fit 알고림으로 할당시키는데,
first fit은 처음 만나는 프로세스보드 큰 파티션을 바로 할당시키는 것이다. 속도는 빠르지만 메모리 낭비가 심하다.
best fit은 가능한 가장 작은 홀을 할당시켜준다.
worst fit은 가증한 가장 큰 홀을 할당시켜준다.
연속 메모리 할당의 장점은 알고리즘이 간단해 구현이 용이하고, 논리 -> 물리 주소의 변환 과정이 간단해 CPU가 메모리에 엑세스하는 속도가 빠르다는 것이다. 하지만 메모리 할당의 유연성이 부족해 메모리 압축 과정이 요구되기도 한다.
세그멘테이션 메모리 관리
프로세스를 구성하는 논리 블록을 세그먼트라고 한다. 프로세스를 논리 세그먼트들로 나누고 각 논리 세그먼트를 한 덩어리의 물리 메모리(물리 세그먼트)에 할당한다,
코드 세그먼트는 프로그래매 전체에 걸쳐 작성된 모든 코드를 한데 모은 것이고, 데이터 세그먼트는 프로그램 전체에 걸쳐 작성된 전역변수와 정적 변수를 모은 것이며, 스택 세그먼트는 함수가 호출될 때 지역변수와 매개변수, 리턴 값을 저장하는 메모리 공간이다. 그리고 힙 세그먼트는 프로세스가 실행 중에 동적으로 할당받는 메모리 영역을 의미한다.
세그멘테이션 기법은 컴파일러와 링커, 로더의 도움이 필요하다.
컴파일러와 링커는 응용프로그램과 라이브러리의 코드를 한 곳으로 모아 코드 세그먼트를 구성한다. 그리고 전역변수와 정적 변수를 모아 데이터 세그먼트를 구성하고 실행 파일을 생성한다. 로더는 프로그램 실행 시 실행 파일 내 구성된 각 논리 세그먼트에 대해 물리 메모링세ㅓ 동일한 크기로 물리 세그먼트를 할당해준다. 논리 세그먼트를 물리 세그먼트에 적재하고, 운영체제는 프로세스 실행 시 필욯나 크기의 스탟 세그먼트와 동적 할당을 위한 힙 세그먼트를 물리 메모리에 할당한다.
논리세그먼트와 물리 세그먼트의 매핑과정에는 세그먼트 테이블이 쓰인다. 세그먼트 기법을 사용하는 운영체제는 프로세스의 각 논리 세그먼트가 할당된 물리 메모리의 위치를 관리하기 위해 세그먼트 테이블을 사용하기 때문이다.
세그먼트 테이블은 시스템 전체에 1개를 두고 관리하는데, 세그먼트들의 시작물리주소와 세그먼트의 크기로 구성되어 있다. 현재 실행중인 모든 프로세스에 대해 논리 세그먼트당 하나의 항목이 저장된다.
세그먼트 테이블의 구현에는 하드웨어와 운영체제, 컴파일러,링크,로더의 도움이 필요하다.
하드웨어는 논리주소를 구성하는데 세그먼테이션의 논리주소를 [세그먼트 번호, offset(세그먼트 내의 상대 주소)]로 관리한다. 예를 들어
mov ds, 2;
mov ax, [ds:100]
은 2번 세그먼트 내 100번지를 읽어 ax 레지스터에 저장하라는 의미이다.
CPU는 세그먼트 테이블의 시작 주소를 가리키는 레지스터가 필요하고, MMU는 메모리 보호와 주소 변환을 수행해준다.
운영체제는 현재 할당된 물리 세그먼트들의 리스트와 빈메모리 리스트를 만들고 관리한다. 그리고 세그먼트 테이블을 생성, 관리, 유지하며 프로세스가 생성될 때마다 논리 세그먼트를 적재할 물리 세그먼트를 빈 메모리 리스트에서 찾아 할당해준다. 또 프로세스가 종료될 때 마다 할당된 물리 메모리를 반환하는 기능을 수행한다.
사용자 프로그램은 컴파일러에 의해 사전에 정의된 세그먼트들을 분할하고 링킹되어야 한다. 또 기계 명령에 들어가는 메모리 주소 역시 [세그먼트 번호, offset]형태로 컴파일 되어 있어야 하며, 로더 역시 실행 파일에 만들어진 논리 세그먼트들을 인지하고 이들을 물리 메모리의 빈 영역을 할당받아 적재개 세그먼트 테이블을 갱신한다.
지금까지 메모리가 무엇이고, 메모리 계층 구조는 왜 생겨났는지 그리고 운영체제에 의해 메모리는 어떻게 관리되고 있는지 알아보았다. 메모리에 관해서는 앞으로 2챕터 정도 더 자세하게 알아볼 것이다.
'운영체제' 카테고리의 다른 글
| [운영체제] 9. 페이징 메모리 관리 (2) | 2025.06.11 |
|---|---|
| [운영체제] 7. 교착상태 (0) | 2025.03.18 |
| [운영체제] 6. 스레드 동기화 (0) | 2025.03.12 |
| [운영체제] 5. CPU 스케줄링 (0) | 2025.02.12 |
| [운영체제] 4. 스레드와 멀티 스레딩 (2) | 2025.01.27 |