가상 메모리와 메모리 할당
가상 메모리(Virtual memory)란?
컴퓨터의 주 기억 장치인 RAM의 크기는 상대적으로 작다. 하지만 프로세스의 크기는 컴퓨터의 성능의 발전과 함께 점점 커지고 있다. 그렇다면 우리가 물리 메모리의 크기보다 큰 프로세스를 작동시키기 위해서는 어떻게 해야 할까?
그를 위한 것이 가상 메모리(Virtual memory)이다. 가상 메모리 시스템에서 모든 프로세스는 각자 메모리 주소 0에서부터 시작하는 커다란 가상의 메모리 공간을 가진다. 이는 프로세스가 연속성이 보장되는 논리 주소(Logical Address)를 가지게 함으로써 프로세스가 실제 물리 메모리의 주소를 신경쓰지 않고 작동할 수 있도록 한다.
이러한 가상 메모리는 실제 물리 메모리와 보조 저장 장치의 스왑(Swap) 영역을 같이 사용하기에 가능하다. 지금 실행해야 할 부분만 물리 메모리에 저장하고 그 이외의 프로세스는 보조 저장 장치의 스왑 영역에 저장하는 것이다.
동적 주소 변환(Dynamic Address Translation)
가상 메모리의 논리 주소는 실제 물리 메모리의 물리 주소와는 다르므로 동적 주소 변환(Dynamic Address Translation)을 거쳐서 물리 주소로 변환해 주어야 한다. 이를 통해 프로세스는 가상 메모리를 물리 메모리처럼 사용할 수 있다. 주로 MMU(Memory Management Unit) 등의 하드웨어의 도움을 받아 이루어진다.
가상 메모리의 분할
물리 메모리의 주소 하나하나를 모두 MMU가 관리하고 변환해주는 것은 오버헤드가 매우 클 것이다. 따라서 MMU는 물리 메모리를 일정 단위로 쪼개서 관리하게 된다.
만약 가상 메모리를 사용하지 않고 모든 프로그램을 물리 메모리에 연속적으로 저장한다고 가정해보자. 이러한 방식을 연속 메모리 할당 이라고 하는데 연속 메모리 할당에는 두 가지 분할 방식, 고정 분할과 동적 분할이 존재한다.
고정 분할은 물리 메모리를 특정한 크기의 파티션으로 나누어 관리한다. 동적 분할은 프로세스의 크기에 딱 맞는 크기의 메모리를 할당한다.
만약 가상 메모리를 사용하게 된다면 더 이상 연속적으로 물리 메모리에 저장할 필요가 없기에 비연속 메모리 할당을 하게 되는데, 비연속 메모리 할당은 가변 분할인 세그멘테이션(Segmentation) 과 고정 분할인 페이징(Paging)으로 나뉘게 된다.
메모리 단편화(Memory Fragmentation)
메모리 단편화는 메모리 내에 사용할 수 없는 빈 공간이 생겨 버려지는 것을 뜻한다. 메모리 단편화는 외부 단편화(External Fragmentation)과 내부 단편화(Internal Fragmentation)으로 나뉘는데, 간단하게 예시를 들며 하나씩 알아보자.
위에서 설명했던 메모리의 연속 메모리 할당, 그 중에서 고정 분할을 살펴보자. 우리는 고정 분할 방식을 사용하여 메모리를 64바이트의 블록으로 나누어 관리하도록 했다고 가정해보자.
만약 이 상태에서 65바이트 크기의 프로세스를 메모리에 저장한다면 어떻게 될까? 우리는 메모리를 블록 단위로 관리하고 65바이트는 1 블록보다 크므로 2 블록을 할당해주어야 한다. 그러면 63바이트의 메모리가 버려지게 되고 이를 내부 단편화라고 한다. 내부 단편화는 프로세스의 크기보다 더 큰 크기의 메모리를 할당하여 메모리가 버려지는 것을 뜻한다.
이번엔 외부 단편화를 살펴보자. 위의 내부 단편화 문제를 해결하기 위해 연속 메모리 할당 중 동적 분할을 사용한다고 가정해보자. 16바이트 크기의 메모리가 있고 8바이트의 프로세스 A와 8바이트의 프로세스 B가 현재 메모리를 점유 중이다. 이 때 프로세스 A가 종료되고 7바이트 크기의 프로세스 C가 A가 있던 자리를 점유한다고 가정하면, 1바이트의 사용하기 어려운 크기의 작은 메모리가 버려지게 된다.
이를 외부 단편화라고 한다. 외부 단편화는 메모리를 점유하고 해제되는 과정에서 생기는 여러 작은 빈 메모리 파편을 뜻한다. 이런 외부 단편화가 심화되면 빈 메모리 공간이 충분한데도 여기저기 쪼개져 나뉘어져있어 새 프로세스를 할당해주지 못하는 상황이 생길 수도 있다.
페이징(Paging) 기법
페이징은 가상 메모리를 사용하는 비연속 메모리 할당의 일종으로 물리 메모리를 일정한 크기의 프레임(Frame)으로 나누고, 프로세스를 프레임과 동일한 크기의 페이지(Page)로 나누고 번호를 매겨 메모리를 관리한다.
각 프로세스는 페이지가 어느 프레임에 저장되어 있는지를 가리키는 페이지 테이블(Page Table)을 가지기 때문에 연속성을 유지한 가상 메모리를 사용하면서도 실제 물리 메모리는 비연속적으로 사용할 수 있다.
페이지 테이블은 물리 메모리의 OS 영역에 저장되고, 물리 메모리를 차지하므로 페이지 테이블의 크기를 적절하게 조절하는 것 또한 중요하다. 페이지 하나의 크기를 너무 작게 잡으면 페이지 테이블의 크기가 기하급수적으로 늘어날 것이다. 또한 페이지의 크기는 고정적이므로 위에서 설명한 내부 단편화가 일어날 수 있다.
이렇게 페이지 테이블 전체를 물리 메모리에 저장해두고 사용하는 것을 직접 매핑(Direct Mapping)이라고 한다.
하지만 직접 매핑에는 몇 가지 문제가 있다. 첫째로는 모든 페이지 테이블을 물리 메모리에 저장해야 한다. 이는 페이지 크기에 따라 상당한 용량을 차지한다. 두번째로는 프로세스가 작동할 때마다 페이지 테이블에 1번, 주소를 구한 뒤 실제 메모리에 1번, 메모리에 2번씩 접근해야 한다는 문제가 있다. 이를 해결하기 위한 연관 매핑(Associative Mapping)이 있다.
연관 매핑은 CPU와 CPU 캐시 사이에 존재하는 하드웨어 캐시인 TLB(Translation Look-aside Buffer)의 도움을 받는 방식이다. 페이지 테이블을 물리 메모리가 아닌 보조 저장장치의 스왑 영역에서 관리하고, 페이지 테이블의 자주 쓰는 일부만 TLB에 올려서 대부분의 작업은 페이지 테이블에 접근하지 않고 TLB에 접근하여 수행할 수 있게 된다. 연관 매핑의 작동 방식은 다음과 같다.
1) 가상 주소가 주어지면 먼저 TLB에 접근한다.
2) TLB에 원하는 가상 주소가 있다면 바로 물리 주소로 변환한다.(TLB Hit)
3) 만약 TLB에 원하는 가상 주소가 없다면(TLB Miss), 스왑 장치의 페이지 테이블에 접근한다.
4) 페이지 테이블에서 원하는 가상 주소가 valid하다면(물리 메모리에 적재된 페이지라면), 물리 주소로 변환 후 TLB를 갱신해준다.
5) 만약 가상 주소가 valid 하지 않다면, 페이지 폴트(Page Fault)가 일어난다.
연관 매핑은 메모리에 페이지 테이블을 적재하지 않아 공간을 아낄 수 있지만, TLB Miss가 빈번하게 일어날 경우 오버헤드가 크다는 단점이 있다.
페이지 폴트(Page Fault)
필요로 하는 페이지가 물리 메모리에 적재되어 있지 않을 때에는 보조 저장장치의 스왑 영역에서 필요로 하는 페이지를 찾아 물리 메모리에 적재해야 한다. 이를 페이지 폴트라고 한다. 페이지 폴트가 과도하게 일어나 작업이 거의 수행되지 않는 것을 스레싱(Thrashing)이라고 한다.
페이지 폴트가 일어났을 때 마침 비어있는 프레임이 있다면 필요로 하는 페이지를 빈 프레임에 적재하면 되겠지만, 빈 프레임이 없다면 이미 페이지가 적재된 프레임 하나를 비우고 페이지를 적재해야 한다. 이 때 어느 프레임을 비울지를 선택하는 방법을 페이지 교체 알고리즘(Page Replacement Algorithm)이라고 한다.
페이지 교체 알고리즘(Page Replacement Algorithm)
페이지 교체 알고리즘엔 몇 가지가 있다.
1) FIFO(First In, First Out) : 가장 먼저 메모리에 적재된 페이지를 교체한다.
2) OPT(Optimal) : 앞으로 사용되지 않을 시간이 가장 긴 페이지를 교체한다. 언제 사용될지 예측이 불가능하기 때문에 현실적으로 불가능하다.
3) LRU(Least Recently Used) : 가장 오래 전에 사용했던 페이지를 교체한다. OPT를 현실적으로 개량한 버전이며 가장 많이 사용된다.
4) LFU(Least Frequently Used) : 가장 적게 사용한 페이지를 교체한다.
세그멘테이션(Segmentation)
세그멘테이션은 프로세스를 논리적 단위로 나눈다. 대표적으로 프로세스를 code, data, stack, heap으로 나눈것이 세그멘테이션의 일종이다. 이러한 세그멘테이션은 크기가 서로 다 다르기 때문에 외부 파편화 문제에 취약하다.
세그멘테이션의 작동은 페이징 기법과 대단히 유사하다. 프로세스들은 각각 MMU에서 세그멘테이션 테이블을 가지고, 이를 통해 가상 주소를 물리 메모리의 실제 주소로 변환하여 사용한다.
세그멘테이션 테이블은 페이지 테이블과 달리 프레임 번호를 가리키지 않고 Base와 Limit 값을 가진다.
세그멘테이션의 크기가 모두 다르기에 이러한 방식을 사용하는데, 논리 주소 (4,300)은 세그멘세이션 4번의 base+300으로 번역된다. 즉 4700+300이 되어 물리 메모리의 5000번에 접근하게 된다. 하지만 만약 논리 주소 (2,800) 이 들어오면 5100으로 전환되고 이는 2번 세그멘테이션의 base+limit인 4700보다 큰 값, 세그멘테이션의 범위를 벗어난 값이므로 segment violation이라는 예외가 발생한다.
세그멘테이션은 페이징과 비교해서 보호와 공유에 있어서 더 효율적이다. 이는 세그멘테이션이 논리적 단위로 프로세스를 나누기 때문인데, 권한을 의미하는 r,w,x 비트를 테이블에 부여할 때에도 세그멘테이션은 기능적 단위로 나뉘기 때문에 권한의 분리가 명쾌하다. 페이징은 code, data 등의 영역이 혼재될 수 있기 때문에 권한이 애매모호할 수가 있다.
공유 또한 같은 프로세스가 동시에 여러 개 실행된다고 가정할 때 세그멘테이션은 프로세스끼리 공유하는 code 영역을 공유할 수 있지만, 페이징은 다른 영역까지도 공유할 가능성이 크다.
지금까지 가상메모리의 개념, 그리고 메모리의 할당과 분할, 페이징과 세그멘테이션에 대해 알아보았다.
처음에는 한눈에 볼 수 있도록 한 포스팅으로 끝내는 것이 좋겠다고 생각했지만 이렇게까지 길어지면 역시나 나누는게 좋지 않았을까 하는 생각이 든다.
원래 운영체제는 여기까지 보고 JAVA나 네트워크 쪽으로 넘어갈까 했지만 다음 포스팅에서 딱 파일 시스템까지만 알아보고 넘어가도록 하겠다.