이번 장에서는 메모리 관리의 개념과 그 기법들에 대해 다룬다.
디스크에 저장된 프로그램은 실행되기 전에 반드시 메모리에 적재되어 프로세스가 되어야 CPU가 실행할 수 있다. 이때 하드디스크에 위치한 프로세스들이 메모리로 올라가기 위해 대기하는 큐를 input queue라고 한다. 즉 inqut queue를 통해 메모리로 올라온다.
프로그램은 실행되기 전에 아래와 같은 여러 단계를 거친다.

1. 소스 코드 작성
2. 컴파일 (object module 생성) 단계
3. 링킹 (load module 생성) 단계
4. 로더(loader)를 통해 main memory에 자리를 잡고 실행됨
이 흐름이 중요한 이유는 주소 바인딩(Address Binding)이 이 과정의 어느 시점에 일어나느냐에 따라 방식이 달라지기 때문이다.
Address Binding이란 프로세스의 명령어와 데이터를 메모리에 매핑하기 위해, 프로그램이 사용하는 논리적 주소를 실제 메인 메모리의 물리적 주소와 연결하는 작업이다.
아래의 3가지 시점에 바인딩 작업이 이뤄질 수 있다.
Compile Time Binding: 사전에 프로그램이 메모리의 몇번지에 적재될지 정보를 미리 알 때 수행되며, 컴파일 시점에 바인딩 된다.
Load Time Binding: 사전에 적재 위치를 모를 때, 컴파일러는 일단 재배치 코드(relocatable code)를 생성하고 실제 메모리에 로딩되는 시점에 주소 바인딩이 이뤄진다.
Execution Time Binding: 프로세스 실행 중 프로세스가 메모리의 다른 위치로 이동할 수 있는 환경에서 사용된다. 이 경우 주소 변환이 실행시간에 계속 일어나야하므로 하드웨어 지원(base/limit register, MMU)이 필요하며, 최종적인 바인딩은 실제 명령어가 실행되는 막판 시점까지 지연된다.
바인딩 시점에 따라 logical address와 physical address가 같을 수도, 다를 수도 있다.
Compile-time / Load-time binding에서는 둘이 같다. 프로그램이 메모리에 올라갈 위치가 미리 고정되거나 load 시점에 확정되므로, CPU가 내보내는 주소가 곧 실제 메모리 주소가 된다.
반면 Execution-time binding에서는 두 주소가 서로 다르다. 메모리에 한번 올라온 프로세스라도 실행 도중 다른 메모리 구역으로 위치를 이동할 수 있기 때문에, 최종 physical address는 실행되는 순간에 계속 바뀔 수 있기 때문이다.
MMU(Memory-Management Unit)는 논리 주소를 물리 주소로 변환해주는 하드웨어 장치다.
relocation register + logical address = physical address
즉 MMU 내부에 있는 relocation register의 값과 logical addr의 값을 더한 값이 최종 physical addr의 값이 된다.

relocation register = 14000
logical address = 346
physical address = 14000 + 346 = 14346
이때 사용자 프로그램(프로세스)은 오직 logical address만 다루며, 본인의 데이터가 진짜 메모리의 몇번지에 저장돼있는지는 절대 알지 못한다.
Dynamic relocation은 메모리에 적재돼 실행 중인 프로세스의 위치가 도중에 위 그림처럼 동적으로 바뀔 수 있도록 지원하는 기능이다.
CPU가 임의로 발생시키는 논리적 주소는 프로그램 내부의 상대적 위치일뿐 실제 주소가 아니다. 이 논리적 주소를 실제 메모리 위치로 매핑하기 위해, 해당 프로세스에게 할당된 실제 물리적 메모리의 시작 위치를 재배치 레지스터가 갖고있는 것이다.
즉 실행 도중 프로세스의 메모리 위치가 바뀌어도 상관 없는 이유는, 재배치 레지스터의 값만 새롭게 이동한 위치의 값으로 업데이트해주면 되기 때문이다.
이러한 dynamic relocation 방식은 메모리 관리를 효율적으로 하기 위해 프로세스들을 한쪽으로 밀어버리는 Compaction 작업에서도 중요하게 쓰인다. 메모리 곳곳에 흩어진 작은 빈 조각들을 모아 큰 여유공간을 만들기 위해 프로세스들을 이동시킬때, dynamic reloaction이 있기에 위치 이동이 가능한 셈이다.
프로그램의 특정 루틴(코드)이 실제로 호출돼 사용되기 전까지는 메모리에 적재하지 않는 기법이다.
즉 기존의 static loading은 프로그램이 실행될때 코드 전체를 한꺼번에 메모리에 올려버리는 반면 dynamic loading은 프로그램 내부의 특정 루틴(함수나 서브프로그램)이 호출되기 전까지는 메모리에 적재하지 않고 디스크에 놔두고, 실행 중에 해당 루틴이 실제로 필요한 순간, 이미 메모리에 올라와있는지 확인하고 없다면 그때서야 load한다.
호출되지 않는 코드는 메모리에 올라가지 않으므로 메모리 공간을 훨씬 효율적으로 사용할 수 있으며, 발생빈도는 낮지만 처리하기 위해 매우 방대한 양의 코드가 필요한 경우에 유용하게 쓰인다.(ex:에러 처리 루틴)
프로그램에서 사용하는 시스템 라이브러리 함수 등의 linking 작업을 처음부터 수행하지 않고, 프로그램이 실제로 실행되는 시점(Execution time)까지 최대한 지연시키는 기술이다.
기존 static linking은 자주 쓰는 라이브러리들을 초기에 모조리 연결(linking)시켜 메모리에 올려놓는 반면 dynamic linking은 라이브러리를 연결시키지 않고 놔뒀다가 프로그램이 실제로 실행될 때 연결한다.
이때 stub라는 일종의 메모 쪽지 역할을 하는 개념을 이용하는데, 사용하려는 라이브러리 함수가 메모리의 몇번지에 위치해있는지에 대한 주소 정보만을 stub에 기록해둔다.
프로그램이 쭉 실행되다가, 마침내 해당 함수가 실제로 필요한 실행시점(Execution time)이 되면, stub에 적혀있던 주소값을 보고 메모리에 접근하고, 거기 있는 해당 라이브러리 함수의 코드를 가져와 실행시킨다.
즉 이 기법이 정상적으로 작동하려면 호출하려는 대상 라이브러리 함수들이 이미 메모리 위에 올라와있어야하고, 메모리 위에 올라와있는지 아닌지를 체크해주는건 운영체제의 몫이다.
Swapping은 한정된 메인 메모리 공간의 한계를 극복하기 위한 방법으로, 프로세스를 통째로 메모리와 하드디스크 사이에서 교환하는 메모리 관리 기법이다.

swapping 소요 시간은 메모리와 디스크 사이에서 전송되는 데이터와 프로그램의 양에 비례한다.
연속 메모리 할당 방식은 프로세스가 메모리에 적재될 때 한 덩어리로 연속적으로 붙어있는 빈 공간(파티션)을 통째로 할당해주는 기법이다.
single-partition allocation방식에서 메모리는 보통 두 영역으로 나뉜다.
낮은 주소 영역: 운영체제
높은 주소 영역: 사용자 프로세스
프로세스가 자기 영역 밖을 침범하지 못하도록 두 레지스터로 보호한다.
CPU가 특정 메모리 주소에 접근을 시도할 때, 시스템은 이 두 레지스터를 이용해 해당 접근이 정당한지 검사해 메모리를 보호한다.
base register의 값 + limit register의 값보다 작은지 확인한다.multiple-partition allocation방식에서는 메모리의 사용자 영역을 여러 개의 조각으로 쪼개서 사용한다.
프로세스가 종료되면 빈 메모리 블록인 hole이 생긴다. 위 그림 속 회색 부분이 hole이다. 새 프로세스가 메모리에 들어오려고 하면, OS는 흩어져있는 빈 공간들을 탐색하다가 그 프로세스를 수용할 수 있을만큼 충분히 큰 hole을 찾아 거기에 집어넣는다.
이때 운영체제는 항상 아래 두가지 상태를 테이블에 기록, 관리해야한다.
1. 현재 어느 프로세스들이 메모리 어디를 차지하고있는지
2. 현재 메모리 어디어디가 비어있고, 그 크기는 얼마인지
그럼 흩어져 있는 hole들 중에 뭘 내줘야할까?
이걸 결정하는 방식은 아래 3가지가 있다.
연속 할당 방식은 메모리 공간이 작은 조각으로 나눠져 메모리가 낭비되는 단편화 문제가 발생한다.
단편화 종류로는 외부 단편화와 내부 단편화가 있다.
여기저기 흩어진 빈 hole들을 모두 합치면 요청한 크기를 충분히 만족하지만, 연속된 한 덩어리가 아니기 때문에 정작 프로세스에게 메모리를 할당해주지 못하는 상태다.
이는 Compaction으로 해결할 수 있다.
Compaction이란 외부 단편화 문제를 해결하기 위한 메모리 공간 압축 기법이다.
compaction이란 메모리 곳곳에 여기 찔끔 저기 찔끔 작게 흩어져있는 빈 hole들을 모아 하나의 큰 빈 hole로 만들기 위해, 흩어져 있는 프로세스들을 메모리의 한쪽 방향으로 쫙 밀어버리는 작업이다.
예를 들어 프로세스의 위아래로 작은 hole들이 있을때 이 프로세스를 한쪽으로 밀어붙이면 반대쪽에 흩어져있던 공간들이 합쳐져 더 큰 공간이 만들어지게 되며, 이를 통해 크기가 큰 새로운 프로세스도 수용할 수 있게 된다.
이때 앞서 살펴본 dynamic reloaction 기술이 뒷받침되어야만 compaction이 가능하다. 실행 중인 프로세스의 물리적 메모리 위치가 도중에 바뀌더라도 MMU의 지원을 통해 올바른 주소를 찾아갈 수 있기 때문이다.
I/O 문제
compaction을 수행할때 심각한 입출력 문제가 발생할 수 있다. 만약 어떤 프로세스가 디스크 읽기 등의 I/O 작업을 요청해 둔 상태에서 컴팩션이 일어나 메모리의 다른 위치로 밀려났다고 가정해보자. I/O 작업이 완료돼 그 결과를 메모리에 쓰려고 할때, 프로세스는 이미 다른 곳으로 이동했음에도 불구하고 과거의 위치에 덮어써버리는 치명적 오류가 발생할 수 있다.
이러한 주소 꼬임 문제를 방지하기 위해, 모든 I/O 작업은 메모리 내에 미리 지정된 운영체제 버퍼 영역으로만 수행하도록 제한한다. 이렇게 규칙을 정해두면 프로세스가 컴팩션으로 인해 메모리 내 어디로 밀려가있든 상관없이 I/O 결과물은 항상 고정된 OS버퍼 영역에 안전하게 기록되므로 데이터가 엉뚱한 곳에 쓰이는 문제를 원천적으로 차단할 수 있다.
프로세스에게 할당된 메모리 공간이 요청한 크기보다 약간 더 커서, 그 차이만큼의 공간이 쓰이지 못하고 낭비되는 상태다.
가령 100K를 요청한 프로세스에게 110K짜리 hole을 줬을때, 남는 10K를 새로운 빈 hole로 관리하는 오버헤드를 피하고자 110K를 통째로 넘겨주며 발생한다.
Compaction — 흩어진 hole을 하나로 모으는 방법이다. dynamic relocation이 가능해야 하며, I/O 작업 중인 프로세스는 고정시키거나 운영체제 버퍼를 통해 I/O를 수행해야 한다.
이 외부 단편화 문제를 해결하기 위해 Paging이 등장한다.
프로세스의 논리 주소 공간이 물리 메모리에서 연속적일 필요가 없도록 만드는 방식이다. 즉 연속 메모리 할당은 프로세스가 통째로 묶여서 메모리에 들어갔다면, 페이징은 프로세스를 잘게 쪼개서 메모리 여기저기에 흩뿌린다. 이 덕분에 연속 할당의 고질병이었던 외부 단편화가 완벽히 해결된다.
프로세스가 특정 크기(page n개)의 메모리를 요구하면, OS는 메모리 곳곳에 흩어져있는 frame들을 찾아 n개만큼 할당해준다.
이때 프로세스의 페이지들이 메모리 여기저기 흩어져있기 때문에, "논리적 페이지 몇번이 실제 메모리의 몇번 프레임에 들어있는지"를 기록해두는 page table이 필요하며, 각 프로세스는 자기 자신만의 독립적인 page table을 가진다.
하지만 이 방식은 외부 단편화 문제는 해결하지만 마지막 page가 frame을 다 채우지 못하면 내부 단편화가 생길 수 있다는 한계가 있다.
paging 기법에서 CPU가 발생시키는 논리적 주소는 page number와 page offset이라는 두가지 파트로 나뉘어 구성된다.
page number를 통해 page table에서 frame number를 찾고나면, 앞서 구한 frame number 뒤에 page offset 값을 아무런 변환 없이 그대로 갖다붙여서 최종적인 물리적 주소를 완성한다.
Page table은 메인 메모리에 저장된다. 아래의 두 레지스터로 관리한다.
PTBR (Page-Table Base Register) : page table 시작 주소
PRLR (Page-Table Length Register) : page table 크기
page table 자체도 메인 메모리 위에 존재한다. 따라서 CPU가 데이터를 읽거나 쓰려면 page table을 읽기 위한 접근 1번, 실제 데이터를 읽거나 쓰기 위한 접근 2번 이렇게 총 2번씩 메모리에 접근해야 하는 비효율성이 생긴다.
이러한 문제를 해결하기 위해 일종의 고속 캐시 메모리인 TLB(=Associative memory)을 사용한다.
주소 변환을 빠르게 하기 위한 특수 고속 캐시다 메모리다. 병렬 탐색(Parallel)이 가능해 찾고자 하는 page number를 한꺼번에 쫙 뿌려서 frame number를 순식간에 찾아낸다.

TLB Hit : page number가 TLB에 있음 → 바로 frame number 획득
TLB Miss : page number가 TLB에 없음 → page table 접근 후 TLB 갱신
TLB를 활용했을 때 메모리 접근 속도가 얼마나 빨라지는지 수치적으로 계산해보자.
hit time = 1 + ε
miss time = 2 + ε
EAT = (1 + ε)α + (2 + ε)(1 - α) = 2 + ε - α
α = hit ratio, ε = TLB 검색 시간
→ α가 1에 가깝고 ε가 작으면 EAT ≈ 1
즉 TLB가 없었다면 모든 메모리 접근마다 page table과 실제 데이터 확인을 위해 무조건 메모리르 2번씩 접근해야했지만, TLB를 활용하면 전체 접근 시간을 메모리를 거의 1번만 접근하는 수준으로 크게 단축시킬 수 있다는 것을 증명한 것이다.
paging 방식에서의 memory protection은 page table의 각 행에 valid/invalid bit를 붙인다.
예를 들어 어떤 프로세스가 0번부터 5번까지 단 6개의 페이지만을 필요로 한다면, page table에서 이 6개의 페이지에만 v표시를 해두고, 나머지 테이블에는 모두 i 표시를 한다.
만약 프로세스가 이 i로 표시된 영역에 접근을 시도하면, 시스템은 프로세스의 정상적인 영역을 벗어난 불법적인 접근으로 간주해 즉각 차단하게 된다.
page table은 각 프로세스마다 존재하며 main memory 위에 올라가있어서 프로세스 수가 많아지면 메모리 상에서 차지하는 공간이 매우 커지는 문제가 발생한다. 따라서 이를 관리하기 위해 아래와 같은 다양한 page table 구조가 등장한다.
Hierarchical paging은 하나의 커다란 page table을 여러 개의 계층으로 쪼개는 방식으로, 마치 책의 목차에서 장과 절을 찾아가는 것과 같은 원리이다.
대표적인 방식으로 two-level paging이 있는데, 이의 구조는 아래와 같다.

위 예시는 32bit의 주소, 4KB의 page size일때의 예시이며, 4KB는 2^12바이트이므로 page 안에서의 세부위치를 나타내는데(offset) 12비트가 필요하다. 32bit-12bit=20bit이므로 page number는 20비트이다. 이때 이 20비트의 page number는 약 100만개(2^20)의 항목을 의미하므로 이를 통째로 관리하면 table이 너무 커진다. 따라서 이 20비트의 page number를 다시 p1(10비트),p2(10비트)로 나눈것이다.
전체 page number를 두 부분(p1,p2)으로 나누고, 앞의 10비트인 p1값을 이용해 outer page table을 검색한다. 검색한 결과로 찾아낸 두번째 단계의 page table(inner page table)에서 다시 나머지 10비트인 p2값을 이용해 검색해 최종 목적지인 frame number를 찾아낸다. 마지막으로 처음에 빼뒀던 12비트의 displacement값을 이 frame number 뒤에 그대로 붙여주면 실제 메모리의 물리적 주소가 완성된다.
프로세스가 논리주소 공간 전체를 다 쓰지 않고 일부분만 사용해 inner page table은 메모리에 생성하지 않아도 된다. 따라서 거대한 단일 page table을 통째로 유지하는 것보다 메모리 공간을 크게 절약할 수 있다.
outer page table 접근 1번, inner page table 접근 1번, 실제 데이터 접근 1번 이렇게 총 3번 메모리에 접근해야하므로 주소 변환에 걸리는 시간이 더 길어진다.
주소 공간이 커지면 hierarchical paging 방식 사용 시 level이 4-5단계 이상으로 깊어질 수 있다. 이는 메모리를 여러 번 참조함을 의미하므로 성능이 저하될 것이다.
Hashed paging 방식은 이러한 문제를 해결하기 위한 방식으로, 주소 주소 크기가 32비트 이상인 넓은 논리적 주소 공간에서 많이 사용된다. virtual page number를 해시 함수의 입력값으로 넣어 계산된 결과값을 바탕으로 table을 검색한다.
해시 함수: 어떤 값을 넣으면 인덱스로 바꿔주는 함수

주소 변환 과정은 아래와 같다.
각 항목에는 특정 page number와 frame number가 쌍으로 저장되며, 전체 table을 검색할 필요 없이 해시함수의 결과 위치만 따라가면 한번 또는 몇번의 탐색만으로 원하는 frame number를 찾을 수 있어 효율적이다.
서로 다른 page number가 동일한 결과를 내는 hash collision 문제가 자주 발생해 chain이 너무 길어지면 연결 리스트를 처음부터 끝까지 순차적으로 뒤져야 하므로 탐색 성능이 떨어진다.
프로세스마다 독립적인 page table을 가진 기존의 paging 방식과 반대로, 시스템 전체에 실제 물리 메모리의 frame 개수만큼만 엔트리를 가지는 단 하나의 page table만 유지하는 방식이다.
주소 변환 과정은 아래와 같다.
프로세스가 늘어나도 page table을 추가로 생성할 필요가 없으므로 page table 관리에 들어가는 메모리 공간을 절약할 수 있다.
특정 page가 어느 frame에 있는지 찾으려면 page table을 맨 처음부터 하나씩 전부 검색해야하므로, 주소를 매핑하는데 걸리는 시간이 길어질 수 있다.
Paging에서 여러 프로세스가 동일한 코드나 데이터를 함께 사용할 수 있도록, 서로 다른 프로세스의 page table 항목이 물리 메모리의 동일한 frame을 가리키게 하는 기법니다.
공유 가능한 코드는 보통 read-only (reentrant) code 이며, shared code는 모든 프로세스의 logical address space에서 같은 위치에 나타나야 한다.
예를 들어 한글 프로그램 자체는 내용이 변하지 않는 읽기 전용 코드이므로 여러 프로세스 사이에서 공유될 수 있다. 이를 구현하기 위해 각 프로세스(P1,P2,P3)의 page table에서 해당 editor program에 해당하는 항목들이 모두 동일한 물리적 frame number(ex:3번,4번,6번 frame)를 가리키도록 동일하게 설정한다. 이렇게 하면 세 프로세스가 물리적 메모리 어딘가에 단 하나만 깔려있는 editor program을 똑같이 참조해 공유할 수 있다.
Segmentation은 메모리를 고정된 크기(page)로 자르는 paging과 달리, 사용자의 관점을 반영해 논리적인 단위(segment)로 나눠 메모리에 할당하는 방식이다.
각 segment는 그 역할에 따라 크기가 다를 수 있으며, 하나의 segment 안에서는 물리적 메모리 공간을 연속적으로 통째로 할당받지만, 한 프로그램을 이루는 여러 segment들은 메모리 상에서 서로 뿔뿔이 흩어져서 위치할 수 있다.
즉 앞서 학습한 contiguous allocation과 같은 방식으로 할당된다.

Paging : 운영체제 관점, 고정 크기
Segmentation: 사용자 관점, 논리적 단위
Segmentation 시스템에서 CPU가 생성하는 논리 주소는 다음 두 가지 정보로 구성된다.
Segment table의 각 항목은 page table과 달리 아래 두 가지 정보를 가진다.
segment table이 메모리 어디에 있는지를 먼저 알아야하는데, 이때 아래의 두 레지스터를 통해 알 수 있다.
STBR (Segment-Table Base Register) : segment table 시작 위치
STLR (Segment-Table Length Register) : segment 개수

메모리 접근 요청이 발생하면 가장 먼저 주소 안의 segment number를 확인한다. 이 번호가 프로세스의 전체 segment 개수 정보가 담긴 STLR의 값보다 크다면 잘못된 접근이므로 오류가 발생한다.
STBR가 가리키는 segment table의 위치로 찾아가, segment 번호에 해당하는 항목을 뒤진다.
table에서 가져온 Limit 값과 논리주소의 displacement를 비교해 만약 displacement값이 limit보다 크다면 해당 segment의 범위를 벗어난 것이므로 에러가 발생한다.
범위를 벗어나지 않는 정상적인 접근이라면, table에 적힌 base값에 displacement 값을 더해 최종 물리주소를 완성한다.
논리적 보호 및 공유에 유리한 segmentation의 장점과 external fragmentation을 해결하는 paging의 장점을 모두 취하기 위해 두 방식을 결합한 기법이다. 대표적으로 MULTICS 시스템에서 사용된다.
순수 segmentation 방식에서는 segment table의 base가 물리 메모리의 시작 주소를 가리켰으나, 이 방식에서는 segment가 page로 쪼개져있기 때문에, segment table의 base는 해당 segment를 관리하기 위한 page table의 시작주소를 가리킨다.