가상 메모리란?
가상 메모리(Virtual Memory)는 실제 물리적인 메모리보다 큰 용량을 가지는 논리적인 메모리 공간입니다. 프로세스가 실행될 때 운영체제는 해당 프로세스에게 필요한 메모리 공간을 할당합니다. 이 때 물리적인 메모리 공간이 부족하면 가상 메모리를 사용하여 프로세스가 필요로 하는 메모리 공간을 보충합니다.
가상 메모리는 하드디스크의 일부 공간을 메모리처럼 사용하므로 실제 메모리보다 저렴하게 구현할 수 있습니다. 또한 가상 메모리는 여러 프로세스 간에 공유될 수 있습니다. 각 프로세스는 자신만의 가상 주소 공간을 갖고, 이 가상 주소를 실제 물리적인 주소로 매핑하여 사용합니다.
가상 메모리는 물리적인 메모리보다 큰 용량을 가질 수 있지만, 실제로는 하드디스크에 접근해야 하므로 속도가 느리다는 단점이 있습니다. 또한 가상 메모리를 사용하면 메모리 접근이 물리적인 메모리와 달리 하드디스크에 접근해야 하므로 오버헤드가 발생합니다.
연속 메모리 할당
프로세스에 연속적인 메모리 공간을 할당하는 방식입니다.
스와핑
현재 실행 되지 않는 메모리를 임시로 보조기억장치 일부 영역으로 내쫒고, 그 빈 공간에 또 다른 프로세스를 적재하여 실행하는 방식입니다.
- 스왑영역: 쫒겨나는 보조기억장치의 일부
- 스왑아웃: 현재 실행되지 않는 프로세스가 메모리에서 스왑 영역으로 옮겨지는 것
- 스왑인: 스왑 영역에 있던 프로세스가 다시 메모리로 옮겨오는 것 ( 옮겨지기 전 물리주소와 다른 주소에 적재될 수 있습니다 )
메모리할당
- 최초 적합 (First-Fit): 메모리 공간에서 가장 처음으로 요청한 크기 이상의 공간을 할당합니다. 이 알고리즘은 할당 후 남은 공간을 이용할 수 있지만, 큰 공간을 할당하려는 요청에 대해 일부 메모리 낭비가 발생할 수 있습니다.
- 최적 적합 (Best-Fit): 메모리 공간에서 요청한 크기에 가장 근접한 크기의 공간을 할당합니다. 이 알고리즘은 작은 공간을 할당하려는 요청에 유리하지만, 남은 공간의 분산으로 인해 내부 단편화가 발생할 수 있습니다.
- 최악 적합 (Worst-Fit): 메모리 공간에서 가장 큰 공간을 할당합니다. 이 알고리즘은 큰 공간을 할당하려는 요청에 유리하지만, 내부 단편화가 많이 발생할 수 있습니다.
외부단편화
외부 단편화(External Fragmentation)는 메모리 할당이 반복되면서, 사용되지 않는 작은 메모리 조각들이 메모리 공간에 흩어져서 생기는 현상을 말합니다. 이로 인해, 메모리 공간의 일부가 사용될 수 없게 되는데, 이를 해결하기 위해서는 사용하지 않는 메모리 조각들을 모아서 연속적인 큰 공간으로 만들어야 합니다.
외부 단편화는 동적 메모리 할당에서 발생하는 문제로, 프로그램이 실행될 때, 동적으로 할당한 메모리를 사용합니다. 하지만, 이러한 할당이 반복되면서 메모리 공간이 여러 조각으로 나뉘어져서 생기는 문제입니다. 예를 들어, 프로그램이 실행되면서 크기가 20바이트인 메모리가 할당되고, 그 다음에는 10바이트, 30바이트 등의 메모리가 할당된다면, 사용되지 않는 20바이트의 메모리 조각들이 발생하게 됩니다.
외부 단편화를 해결하기 위해서는, 사용되지 않는 메모리 조각들을 찾아서 이들을 하나로 모으는 작업이 필요합니다. 이를 위해서는 메모리 관리 기법으로는 가상 메모리, 페이징, 세그먼트 등이 사용됩니다.
내부단편화
내부 단편화(Internal Fragmentation)는 메모리 할당 시 발생하는 현상으로, 프로세스가 할당받은 메모리보다 실제로 필요한 메모리가 작을 때 발생합니다. 즉, 할당받은 메모리 중 일부가 사용되지 않고 남아있는 경우를 말합니다.
메모리 할당 시 고정 크기의 블록으로 할당하는 경우 내부 단편화가 발생할 수 있습니다. 예를 들어, 10KB 크기의 메모리 블록을 할당하는 경우, 프로세스가 7KB의 메모리를 필요로 할 경우에는 10KB 크기의 메모리 블록이 할당되어 3KB의 메모리 공간이 남게 됩니다. 이 경우 3KB 크기의 남은 공간이 내부 단편화가 됩니다.
내부 단편화는 메모리 사용을 비효율적으로 만들어, 전반적인 시스템 성능에 영향을 미칠 수 있습니다. 따라서 메모리 할당 시에는 가능한 한 프로세스가 실제로 필요로 하는 만큼의 메모리를 할당하여 내부 단편화를 최소화하는 것이 좋습니다. 동적 메모리 할당에서는 메모리 풀링 등의 기법을 사용하여 내부 단편화를 최소화하는 방법이 있습니다.
메모리 풀링
메모리 풀링은 미리 일정 크기의 메모리 블록들을 할당하여 풀(Pool)에 저장해두고, 프로그램에서 메모리를 요청할 때마다 풀에서 사용 가능한 블록을 할당하고, 사용이 끝난 블록은 풀에 반환하여 재사용할 수 있도록 합니다.
메모리 풀링은 매번 동적으로 메모리를 할당하고 해제하는 것보다 효율적인 메모리 관리를 할 수 있습니다. 특히, 많은 수의 작은 크기의 객체를 할당하고 해제하는 경우에 효과적입니다. 이는 객체가 빈번하게 생성되고 삭제되는 상황에서 매번 할당 및 해제를 수행하는 것보다, 객체 풀을 미리 할당해두고 객체를 재사용하는 것이 더욱 효율적이기 때문입니다.
하지만 메모리 풀링을 사용할 때에도 내부 단편화 문제가 발생할 수 있으므로, 메모리 블록의 크기를 적절히 설정하는 것이 중요합니다. 또한, 메모리 풀링을 사용하면서도 메모리 누수 문제에 주의해야 합니다. 풀에서 할당한 메모리 블록이 해제되지 않는 경우에는 메모리 누수가 발생할 수 있으므로, 프로그램에서 메모리 블록을 할당할 때와 해제할 때 신중하게 관리해야 합니다.
*** iOS에서는... ***
ARC(Automatic Reference Counting) 기법을 사용하여 객체의 메모리를 자동으로 관리합니다. ARC는 객체가 필요하지 않을 때 자동으로 메모리를 해제하여 내부 단편화 문제를 해결합니다. 따라서 iOS에서는 메모리 풀링 기법을 직접적으로 사용하지 않습니다.
하지만 iOS에서도 메모리 누수 문제가 발생할 수 있습니다. 메모리 누수는 객체가 더 이상 사용되지 않음에도 메모리에서 해제되지 않는 현상을 의미합니다. 이러한 문제는 주로 Strong Reference Cycle(강한 참조 순환)이 발생하는 경우에 발생합니다. Strong Reference Cycle은 두 개 이상의 객체가 서로 강한 참조를 가지고 있어, 객체가 더 이상 필요하지 않아도 메모리에서 해제되지 않는 현상을 말합니다.
따라서 iOS에서는 메모리 누수 문제를 방지하기 위해 weak reference(약한 참조)와 unowned reference(미소유 참조)를 사용합니다. 이를 통해 객체 간의 Strong Reference Cycle을 방지하고, 객체가 더 이상 필요하지 않을 때 자동으로 메모리를 해제할 수 있도록 합니다. 또한 iOS에서는 Instrumentation 도구를 제공하여 메모리 누수를 검사하고, 문제를 해결할 수 있도록 도와줍니다.
세그먼트
세그먼트(Segment)는 프로그램 실행 시 주소 공간을 독립적인 논리적 단위인 세그먼트 단위로 분할하여 사용하는 메모리 관리 기법입니다. 프로그램의 코드, 데이터, 스택 등을 각각의 세그먼트로 분할하여, 독립적으로 메모리를 할당하고 접근할 수 있습니다.
세그먼트는 세그먼트 번호와 세그먼트 내부의 오프셋(offset)을 조합하여 실제 물리적인 주소를 계산하며, 이를 통해 주소 변환을 수행합니다. 세그먼트는 코드와 데이터 등의 영역을 분리하여 보안과 코드 재사용성 등을 높일 수 있습니다.
세그먼트는 보통 가변 길이의 세그먼트와 고정 길이의 세그먼트로 나뉘며, 가변 길이의 세그먼트는 세그먼트가 크기를 변경할 수 있어서 외부 단편화 문제가 발생할 가능성이 있습니다. 이러한 문제를 해결하기 위해서는 가변 길이 세그먼트의 경우, 내부적으로 페이지를 사용하여 관리할 수 있습니다.
페이징을 통한 가상 메모리 관리
페이징
페이징은 물리적인 메모리(RAM)를 일정한 크기의 페이지(Page)로 분할하고, 이를 가상 메모리와 연결시켜 사용하는 방식입니다. 각 페이지는 고정된 크기로 분할되어 있으며, 가상 메모리 주소 공간에서는 페이지 단위로 할당됩니다.
이렇게 분할된 페이지들은 물리적인 메모리에 저장되지 않을 수도 있고, 필요할 때마다 디스크로 스왑되어 저장될 수 있습니다. 이를 통해 가상 메모리의 용량을 늘리고, 더 많은 프로그램을 실행할 수 있게 됩니다.
페이지 테이블
페이지 테이블은 가상 메모리 주소를 물리적인 메모리 주소로 변환하는 역할을 합니다. 페이지 테이블은 가상 메모리의 페이지 번호와 해당 페이지가 물리적인 메모리의 어느 위치에 저장되어 있는지를 매핑하는 정보를 담고(페이지 테이블 레지스터) 있습니다. 이를 통해 CPU가 가상 메모리 주소를 참조할 때, 페이지 테이블을 참조하여 물리적인 메모리 주소로 변환하고, 해당 위치의 데이터에 접근할 수 있습니다.
페이지 테이블의 크기는 가상 메모리의 크기에 따라 결정되며, 대부분의 운영체제에서는 페이지 테이블을 계층적인 구조로 구성하여 메모리 절약을 위한 최적화를 수행합니다. 또한 페이지 테이블의 일부는 캐시 메모리에 저장될 수 있어서 페이지 테이블 접근 속도를 높일 수 있습니다.
페이징에서의 주소 변환
페이징에서의 주소 변환은 가상 주소와 물리 주소 간의 변환을 뜻합니다.
주소 변환은 다음과 같은 과정을 거칩니다.
- 가상 주소(virtual address)를 페이지 번호(page number)와 페이지 내부의 오프셋(offset)으로 분리합니다.
- 페이지 테이블(Page Table)에서 페이지 번호에 해당하는 페이지 프레임(Frame)의 물리 주소(physical address)를 찾습니다.
- 페이지 프레임의 물리 주소와 페이지 내부의 오프셋을 조합하여 실제 물리적인 주소를 계산합니다.
페이지 테이블은 각 프로세스마다 하나씩 존재하며, 프로세스의 가상 메모리 공간을 페이지 단위로 나눈 페이지들의 물리적인 주소를 기록합니다. 페이지 테이블은 페이지 번호를 인덱스로 사용하여 페이지 프레임의 물리적인 주소를 저장하므로, 페이지 번호와 페이지 프레임의 물리적인 주소를 연결해주는 역할을 합니다.
페이징을 사용하면 가상 메모리 공간을 물리적 메모리보다 크게 사용할 수 있으며, 프로세스간의 메모리 보호도 쉽게 구현할 수 있습니다. 하지만, 페이지 테이블을 사용하여 주소 변환을 수행하므로, 오버헤드가 발생할 수 있습니다. 또한, 페이지 크기를 적절하게 설정하지 않으면 내부 단편화 문제가 발생할 수 있습니다.
오버헤드
오버헤드(Overhead)는 어떤 작업을 수행하기 위해 추가로 필요한 작업이나 자원을 말합니다. 즉, 필요한 작업을 수행하기 위해 불필요한 작업이나 자원을 사용하는 것을 의미합니다. 예를 들어, 어떤 프로그램이 실행될 때 추가로 메모리를 할당하는 작업을 수행한다면, 이 작업을 위해 필요한 메모리 공간이 오버헤드가 될 수 있습니다.
페이지 테이블 엔트리
페이지 테이블 엔트리(Page Table Entry, PTE)는 가상 주소와 물리 주소 간의 매핑 정보를 담고 있는 페이지 테이블(Page Table)의 하나의 항목입니다. 페이지 테이블은 가상 주소 공간을 물리 주소 공간에 매핑하는 데 사용됩니다.
페이지 테이블 엔트리는 보통 다음과 같은 정보를 포함합니다.
- 유효 비트(Valid bit): 해당 페이지가 유효한지 여부를 나타내는 비트입니다. 유효 비트가 0인 경우 해당 페이지는 물리 메모리에 존재하지 않는 것으로 간주됩니다.
- 물리 주소(Physical Address 혹은 프레임번호): 해당 가상 주소에 대응하는 물리 주소입니다. 유효 비트가 1인 경우에만 유효한 값입니다.
- 접근 권한(Bits for access permission 혹은 보호비트): 해당 페이지에 대한 접근 권한 정보입니다. 일반적으로 읽기, 쓰기, 실행 등의 권한 정보가 포함됩니다.
- 참조 비트(reference bit): CPU가 접근한 적이 있는지 여부를 나타냅니다.
- 수정비트(modified bit 혹은 더티비트): 해당 페이지에 데이터를 쓴 적이 있는지 없는지 수정 여부를 알려줍니다.
페이지 테이블 엔트리는 CPU가 가상 주소를 물리 주소로 변환하는 데 중요한 역할을 합니다. CPU는 가상 주소를 이용하여 페이지 테이블에서 해당 페이지 테이블 엔트리를 찾아내고, 해당 엔트리의 정보를 이용하여 물리 주소를 계산합니다. 이 과정에서 페이지 테이블을 탐색하는 비용이 발생하게 되는데, 이를 최소화하기 위해 TLB(Translation Lookaside Buffer)라는 캐시 메모리를 사용하기도 합니다.
페이징의 이점
- 내부 단편화 해결: 고정 크기의 메모리 블록을 사용하는 연속 메모리 할당 방식에서 발생하는 내부 단편화(internal fragmentation)를 해결할 수 있습니다. 페이지 단위로 분할하여 할당하면, 메모리 공간을 더욱 효율적으로 사용할 수 있습니다.
- 외부 단편화 해결: 외부 단편화(external fragmentation) 문제도 해결할 수 있습니다. 페이지 단위로 분할하여 할당하면, 메모리 공간을 연속적으로 사용할 필요가 없으므로, 일정 크기 이상의 프로세스를 처리할 때도 효율적으로 메모리 공간을 할당할 수 있습니다.
- 공유 가능: 페이징은 페이지 단위로 메모리를 분할하므로, 페이지를 공유(shared)할 수 있습니다. 예를 들어, 여러 개의 프로세스가 같은 라이브러리 함수를 사용할 때, 페이지를 공유하여 메모리 사용량을 줄일 수 있습니다.
- 가상 메모리: 페이징은 가상 메모리(virtual memory)를 구현할 수 있는 기초가 됩니다. 가상 메모리는 물리 메모리보다 큰 메모리 공간을 사용할 수 있게 해주는 기술로, 프로세스가 더 많은 메모리를 사용할 수 있도록 합니다.
- 프로세스 보호: 각 페이지마다 접근 권한 정보를 기록하여 프로세스 간의 충돌을 방지할 수 있습니다. 프로세스가 다른 프로세스의 페이지를 읽거나 쓰는 것을 방지하면서, 안전하게 메모리를 사용할 수 있습니다.
- 디스크 I/O 최적화: 페이징은 디스크 I/O를 최적화할 수 있는 기초가 됩니다. 페이지 단위로 디스크에 저장된 데이터를 읽고 쓰기 때문에, 필요한 페이지만 디스크에서 읽어오면 되므로, 불필요한 디스크 I/O를 줄일 수 있습니다.
쓰기 시 복사 COW(Copy-on-Write)
COW는 페이지를 복사할 때, 새로운 페이지를 할당하지 않고, 원본 페이지를 공유하다가 내용이 변경될 때에만 새로운 페이지를 할당하는 방식입니다. 이를 통해 메모리 사용량을 줄이고, 복사 작업을 더 효율적으로 처리할 수 있습니다.
계층적 페이징( 다단계 페이지 테이블)
계층적 페이징(Hierarchical Paging)은 페이징 기법의 한 종류로, 페이지 테이블을 계층적으로 구성하여 메모리 접근 시간을 줄이는 기법입니다.
일반적인 페이징 기법에서는 모든 페이지 번호를 메모리에 저장하여 접근합니다. 이 때, 페이지 테이블의 크기가 커지면, 메모리에 대한 접근 시간이 증가할 수 있습니다. 이를 해결하기 위해 계층적 페이징은 페이지 테이블을 여러 계층으로 나누어 구성합니다.
예를 들어, 32비트 시스템에서 4KB 페이지를 사용한다면, 각 페이지는 12비트의 오프셋(offset)을 가집니다. 이를 제외한 20비트는 페이지 번호(page number)로 사용됩니다. 계층적 페이징에서는, 이 20비트를 2개의 그룹으로 나누어 페이지 테이블을 구성합니다. 첫 번째 그룹의 크기는 페이지 번호의 상위 n비트이고, 두 번째 그룹의 크기는 하위 (20-n)비트입니다. 첫 번째 그룹은 페이지 디렉토리(page directory)라는 새로운 페이지 테이블을 구성하고, 각 항목은 페이지 테이블의 시작 주소를 가집니다. 두 번째 그룹은 각 페이지 테이블의 인덱스(index) 역할을 합니다. 이렇게 페이지 테이블을 계층적으로 구성하면, 각 페이지 테이블의 크기가 작아져 메모리 접근 시간이 줄어들게 됩니다.
계층적 페이징의 장점은, 큰 페이지 테이블을 메모리에 로드하지 않고도 작은 페이지 테이블에 접근할 수 있다는 것입니다. 이는 메모리 접근 시간을 단축시켜 전체 시스템의 성능을 향상시킵니다. 또한, 페이지 테이블의 크기를 동적으로 조정할 수 있어, 메모리 공간의 효율성도 높일 수 있습니다. 하지만 계층적 페이징은 페이지 테이블의 구조가 복잡해지므로, 페이지 테이블의 갱신 및 접근 시에 오버헤드가 발생할 수 있습니다.
페이지 교체와 프레임 할당
요구페이징
요구페이징(Demand Paging)은 가상 메모리 관리 기법 중 하나로, 프로세스가 실행될 때 미리 전체 프로세스의 모든 페이지를 메모리에 로딩하는 대신, 해당 페이지가 실제로 필요할 때만 필요한 페이지를 메모리에 로딩하는 방식입니다.
요구페이징은 메모리 사용량을 줄이고, 프로세스의 실행 시간을 단축시키는 장점이 있습니다. 그러나 페이지 폴트(Page Fault)가 발생할 경우 해당 페이지를 메모리에 로딩해야 하기 때문에, 페이지 폴트가 발생하는 경우 시스템 성능에 영향을 미칠 수 있습니다.
페이지 폴트는 페이지 테이블에서 해당 페이지가 현재 메모리에 없음을 나타내는 비트를 설정하고, 페이지 폴트 인터럽트가 발생하여 해당 페이지를 디스크에서 가져와 메모리에 로딩합니다. 이 과정에서 디스크 I/O가 발생하므로, 페이지 폴트가 빈번하게 발생하면 성능 저하가 발생할 수 있습니다.
요구페이징을 최적화하기 위해서는 적절한 페이지 교체 알고리즘과 페이지 프레임 할당 알고리즘이 필요합니다. 페이지 교체 알고리즘은 페이지 폴트가 발생할 때 어떤 페이지를 교체할지 결정하는 알고리즘으로, 페이지 폴트 발생 횟수를 최소화하면서 메모리 사용 효율성을 높이는 것이 목표입니다. 페이지 프레임 할당 알고리즘은 페이지를 어떤 프레임에 할당할지 결정하는 알고리즘으로, 물리 메모리의 크기와 프레임의 크기 등을 고려하여 최적의 프레임 할당 방식을 찾는 것이 목표입니다.
요구페이징은 가상 메모리를 효율적으로 관리하기 위한 중요한 기법 중 하나이며, 운영체제에서 메모리 관리를 수행하는 데에 핵심적인 역할을 합니다.
페이지 교체 알고리즘
페이지 교체 알고리즘은 메모리에서 사용 중인 페이지들 중에서 교체할 페이지를 선택하는 방법입니다. 일반적으로 페이지 부재가 발생하여 새로운 페이지를 적재할 때 메모리 내에 여유 공간이 없을 경우 페이지 교체 알고리즘이 동작합니다. 대표적인 페이지 교체 알고리즘으로는 다음과 같은 것들이 있습니다.
- FIFO (First In First Out): 가장 먼저 메모리에 적재된 페이지를 교체하는 방식입니다.
- LRU (Least Recently Used): 가장 오랫동안 사용되지 않은 페이지를 교체하는 방식입니다.
- LFU (Least Frequently Used): 가장 적게 사용된 페이지를 교체하는 방식입니다.
- Optimal: 앞으로 가장 오랫동안 사용하지 않을 페이지를 먼저 교체하는 방식입니다. 최적의 교체 알고리즘이지만 구현이 어렵고, 실제로 사용하기는 어렵습니다.
이 외에도 다양한 페이지 교체 알고리즘이 존재합니다. 각각의 알고리즘은 장단점이 있으며, 사용되는 시스템의 특성에 따라 적절한 알고리즘을 선택하여 사용하게 됩니다.
페이지 폴트
페이지 폴트(page fault)는 컴퓨터가 프로그램을 실행할 때, 요청한 페이지(page)가 현재 물리적으로 메모리에 적재되어 있지 않아서, 해당 페이지를 찾기 위해 하드 디스크에서 가져와야 하는 상황을 말합니다. 이는 성능 저하의 원인이 될 수 있으므로, 운영 체제는 페이지 폴트가 발생하는 경우 적절하게 처리하여 성능을 최적화합니다. 예를 들어, 페이지 폴트가 발생하면 운영 체제는 하드 디스크에서 해당 페이지를 가져와 물리 메모리에 적재하여 이후에 다시 요청할 때 빠르게 접근할 수 있도록 합니다.
스래싱과 프레임 할당
스래싱(Thrashing)은 운영체제가 메모리 부족 상태에서 페이지 교체 알고리즘을 수행하면서, 페이지 교체가 지나치게 빈번하게 발생하여 CPU 시간 대부분을 페이지 교체에 소비하게 되는 현상을 말합니다. 스래싱이 발생하면 시스템의 전체 성능이 저하되며, 애플리케이션의 응답 시간이 길어지는 등의 문제가 발생할 수 있습니다.
프레임 할당(Frame Allocation)은 메모리 관리 기법 중 하나로, 프로세스가 실행되는 동안 필요한 메모리 공간을 할당하는 것을 말합니다. 운영체제는 프로세스가 필요로 하는 메모리 공간을 필요한 만큼 할당하며, 이를 프레임(Frame)이라고 합니다. 프로세스가 실행되는 동안 필요한 메모리 공간이 더 많아질 경우, 추가적인 프레임을 할당하게 되며, 메모리 부족 상태에서는 프레임 할당을 조절하여 스래싱을 방지하려고 합니다.
스래싱과 프레임 할당은 밀접한 관련이 있습니다. 스래싱이 발생하면 시스템은 프로세스에 필요한 충분한 프레임을 할당하지 못하게 되며, 반대로 프레임 할당이 부족하면 스래싱이 발생할 가능성이 높아집니다. 따라서 운영체제는 스래싱을 방지하고 성능을 최적화하기 위해 적절한 프레임 할당 방식을 선택하고 관리해야 합니다.