brunch

You can make anything
by writing

C.S.Lewis

by zwoo Dec 07. 2022

[정글]열한째주. 운영체제가 메모리를 관리하는 방법

페이지 테이블과 lazy loading

이번에 만들어볼 것은 가상메모리 시스템의 효율을 극대화해주는 lazy loading 이다.


사용자는 무한한 메모리공간을 가졌다고 착각합니다. 운영체제는 그 환상을 지켜주어야 합니다.


사용자는 실제로 가진 RAM보다 훨씬 더 많은 프로그램을 사용한다. 이것이 가능한 이유는 가상메모리라는 추상적인 공간 덕분이다. 물리메모리는 *8GiB일지라도, 사용자는 그보다 더 크게 사용할 수 있다.

이게 무슨 말이냐면, 가령 메모리를 1GiB씩 사용하는 프로그램을 동시에 10개 실행시키더라도 10개 모두 문제 없이 실행된다는 의미이다. 운영체제는 각각의 프로그램을 프로세스(핀토스 프로젝트 기준으로는 스레드)로 관리하고, 우선순위에 따라 조금씩 번갈아가면서 작업을 수행하는데, 각각의 프로세스에서 사용하는 데이터들을 따로따로 기록해놨다가, 그때 그때 활성화된 프로세스가 사용할 데이터들만 반드시 필요한 것으로 판단하고 실제 물리 메모리에 올리기 때문이다.


이를 좀더 추상화해보면 다음과 같다. 프로세스에서 사용하는 데이터는 페이지 라는 4KB크기의 공간이고 프로세스가 사용하는 페이지의 목록은 페이지 테이블에 기록된다. 페이지테이블은 말 그대로 테이블이며 테이블 한칸 한칸의 인덱스를 페이지 테이블 엔트리(PTE)라고 한다. PTE는 주소 덩어리다. 다음 그림을 살펴보자.


페이지 테이블

https://en.wikipedia.org/wiki/Page_table

일단 0부터 31까지의 숫자가 있다. 이것이 주소가 저장되는 영역이다.


CPU는 컴퓨터의 모든 명령을 책임지는 주체로서, 메모리(RAM) 공간에 있는 데이터들을 가지고 와서 연산을 수행한다. 그런데 CPU의 처리속도에 비해 데이터를 가지고 오는 시간이 많이 느리기 때문에 불필요한 지연이 발생하므로  CPU 내부에 임시 기억장치인 레지스터를 두고, 메모리에서 가져온 데이터를 임시로 저장했다가 연산할 때는 이 데이터를 참조한다. 이 레지스터의 용량은 32bit 또는 64bit 이다. 컴퓨터가 사용하는 주소는 레지스터의 크기에 맞게 최대크기가 정해지고 그에 따라 자료형도 정해진다. RAM이 아무리 크더라도 레지스터의 크기보다 큰 주소값을 갖는 데이터는 레지스터에 온전히 저장할 수가 없기 때문에 레지스터 크기에 맞춰야 한다.


다시 그림을 보면, 32비트의 주소공간이 4개의 구역으로 구분되어 각각 어딘가를 가리키고 있다. 이것은 사람의 주소체계에 비유하면 이해하기 쉽다. '서울시 강남구 테헤란로 152' 라는 주소는 각각 시 테이블, 구 테이블, 도로명 테이블, 건물 번호 테이블의 특정 위치(엔트리)를 가리킨다. PTE도 마찬가지로 몇 개의 테이블의 엔트리로 구성되며, 이것을 멀티 레벨 페이지 테이블이라고 부른다. 레벨 개수는 꼭 4개로 고정된 것은 아니고 아키텍처마다 다르다. 물론 싱글 페이지 테이블도 존재한다. 싱글 페이지 테이블과 멀티 레벨 페이지 테이블을 단순하게 구분하자면 물리 메모리 공간을 얼마나 효율적으로 사용하는가에 차이가 있다. 멀티 레벨 페이지 테이블은 주소가 자세하게 적혀있는 만큼 물리 메모리 공간을 꼭 연속적으로 활용할 필요가 없어서 유연하고 효율적이다. 데이터의 크기와 물리 메모리 공간 크기가 딱 맞지 않고 애매하게 남거나 부족한 현상을 단편화라고 하는데, 멀티 레벨 페이지 테이블은 비교적 *단편화 문제가 적다.


Lazy Loading

그렇다면 현재 스레드에서 사용하는 데이터들은 모두 동시에 올라와 있어야만 할까? 꼭 그렇지는 않다. 데이터는 사용자가 필요로 하는 순간에만 사용자의 눈앞에 나타나면 된다. 속도만 보장된다면, 현재 스레드의 페이지 테이블에 기록되어있는 데이터라고 해도 사용자가 요청하기 전까지 데이터가 물리 공간에 올라와있지 않아도 상관 없다.


사용자가 특정 페이지에 접근을 요청하면 CPU는 CPU내부의 MMU라는 메모리 관리장치에게 가져오라고 시킨다. MMU는 번역기를 가지고 있어서 받은 가상 주소를 페이지테이블 상의 물리메모리주소와 비교해가면서 물리 메모리 공간 중 어디에 있는 데이터인지를 탐색하는데, 찾지 못하면 MMU는 CPU에 'page fault' 인터럽트를 걸고 폴트 처리기를 동작시켜서 대응을 시작한다. 이때 주소 자체가 유효하지 않다면 비정상 접근으로 판단되어 프로세스가 그대로 종료되고, 페이지 테이블 상으로 유효한 주소인데 다만 아직 물리 메모리공간에만 없거나, 파일데이터라서 DISK를 읽어와야 하는 경우에는 각각 적절하게 처리된 후 익셉션이 해제된다.


Lazy Loading은 페이지폴트를 염두에 두고 구현하는 기능이다. 프로세스가 처음 실행될 때는 우선 모든 데이터들을 프로세스가 가진 페이지 테이블에 로드해두고, 해당 주소가 참조되어 page fault가 발생하면 물리메모리 혹은 DISK를 참조해서 데이터를 가지고 오도록 하는 것이다. 핀토스 프로젝트에서는 페이지를 로드할 때 미리 구조체 내부에 어떤 타입의 페이지이며 어떤 초기화 함수를 호출해야 하는지 저장해두고 page fault 발생시 페이지 정보가 변경되도록 하였으며, fault를 핸들링하는 함수에서 페이지 테이블을 조회 후 물리 공간을 요청하도록 하였다.


메모리는 더 효율적으로 관리되어야 한다

앞으로 구현해야 할 것은 더울 효율적인 메모리 관리, 바로 stack growth와 swap in / out 이다.


가상 메모리 개념에서 커널 영역은 물리 메모리공간과 1대1로 매핑되어있고 항상 올라가 있지만, 그 아래 유저 메모리 영역은 물리 메모리 공간에 매핑되지 않고 활성화된 프로세스가 실행되면 각각의 데이터에게 가상의 주소만을 부여하고 커널 영역의 유저 풀에 매핑해주어서 커널의 관리 하에 물리 메모리를 오고 갈 수 있게 된다. 유저 메모리 레이아웃은 효율적인 관리를 위해 다음과 같이 구획이 되어있다.


여기서 힙영역은 늘어나는 속성을 갖지만 스택영역은 기본적으로 고정 크기를 갖는다. 메모리 상에 여유가 있는데 스택공간이 고정되었다고 해서 더이상 사용할 수 없도록 막는 것보다는, 유연하게 늘려줄 수 있다면 좋을 것이다. stack growth 가 필요한 이유이다.


또한 추가적으로 디스크 공간 상에 SWAP 공간을 만들어두고 물리 메모리가 부족하면 기존 페이지들 중에서 가장 덜 중요하다고 판단할 만한 페이지를 잠시 SWAP 공간으로 치워두고 요청받은 메모리 공간을 내어주도록 할 수도 있다. swap in / out을 구현하기 전까지는 페이지 테이블을 통해 물리 메모리에 한번 올라간 페이지는 프로세스가 종료되기 전까지는 계속 물리 메모리에 존재하도록 해야 했지만, 이제 필요에 따라 임시 공간에 치워두고, 다시 가져올 수 있게 되는 것이다.


남아있는 의문

이번 과제를 하면서 가장 의문이었던 것은, 실제 운영체제가 페이지테이블을 어떻게 관리하는가 하는 것이었다. 핀토스는 싱글프로세스-싱글스레드로 동작하는데 실제 운영체제는 동시에 여러 프로세스가 실행될 수 있고, 무엇보다 프로세스 간 유기적인 데이터 교환도 지원해줄 텐데 각각의 스레드에 페이지 테이블을 개별로 할당할 것 같지는 않다는 생각이 든다.


또한 유저스레드와 커널스레드라는 용어의 구분도 여전히 명확하게 되지 않고 있다. 내게는 모든 것이 커널스레드로 보인다. '라이브러리에 의해 만들어지는 스레드' 라는 건 도대체 뭘까?




Photo by Alexander Andrews on Unsplash


GiB (기비바이트) :

킬로, 메가, 기가바이트라는 단위는 10진법 단위이고 10^3 씩 증가한다. 이진법 체계를 따르는 컴퓨터에서는 이를 정확하게 맞추기가 어려워서 10^3과 유사하게 2^10씩 증가하는 크기단위를 킬로, 메가, 기가바이트라고 부른다. 이진법 체계의 크기 단위를 엄밀하게 표현하기 위해 나온 단위가 키비(KiB), 메비(MiB), 기비(GiB)바이트이다.



단편화

단편화는 기억 장치의 빈 공간 또는 자료가 여러 개의 조각으로 나뉘는 현상을 말한다. 이 현상은 기억장치의 사용 가능한 공간을 줄이거나, 읽기와 쓰기의 수행속도를 늦추는 문제점을 야기한다. 사용자가 필요로 하는 데이터 공간을 나누는 방식 중에는 세그멘테이션과 페이징이 대표적인데, 세그멘테이션 방식에서 단편화가 특히 더 심하게 발생한다. 왜냐하면 페이징 방식에서 각각의 공간을 균일한 크기(예를 들어 4KB)로 할당해주는 것과 달리 세그멘테이션에서는 고정되지 않은 크기로 할당하기 때문이다. 특정 공간을 크기가 들쭉날쭉하게 자르면 당연히 남는 공간을 예측하고 활용하기가 더 어렵다. 페이징 방식에서는 단편화문제가 비교적 덜 발생하고, 특히 연속적인 할당을 해야하는 싱글페이지테이블에 비해 멀티레벨 페이지테이블 방식에서 단편화 문제가 크게 개선된다.


내부 단편화

기억 장치가 의도된 바 없이 할당될 때 일어난다. 공간이 낭비된다. "내부"라는 용어는 필요 없는 기억 자료가 할당된 영역 안에 있지만 쓰이지 않는 것을 말한다.

운영체제의 메모리 관리기법 중 페이징을 이용할 때 발생할 수 있다. 일정 크기의 페이지에 프로세스 할당시, 프로세스의 크기가 페이지보다 작을 경우 내부 단편화가 발생한다.


외부 단편화

여유 공간이 여러 조각으로 나뉘는 현상을 말한다. 프로그램이 다양한 크기의 기억 장소의 남은 영역을 할당하고 할당을 해제할 때 일어나며, 할당 알고리즘이 약화된다. 비록 남은 기억 공간을 사용할 수 있지만, 조각이 너무 작게 나 있어서 응용 프로그램의 성능을 뒷받쳐 주지 못하기 때문에 효과적으로 사용하지 못하게 된다. "외부"라는 용어는 사용할 수 없는 기억 장소가 할당된 영역 밖에 있다는 것을 뜻한다.


매거진의 이전글 [정글]열째주. 제가 OS를 왜 만들어야 하죠?(2)
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari