Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.
지금까지는 물리 메모리에 딱 들어맞는, 비현실적으로 작은 물리 공간만을 가정했다. 여태까지는 실행 중인 프로세스의 모든 물리 공간이 메모리에 들어맞을 것이라 가정했지만, 지금부터는 동시에 실행되는 많은 주소 공간들을 사용할 수 있게 이 가정을 완화해보고자 한다.
이를 위해서는 추가적인 메모리 계층이 필요하다. 지금까지는 모든 페이지들이 물리 메모리에 위치해있었다. 하지만 이제부터 OS는 지금은 크게 필요하지는 않은 주소 공간의 부분들을 어딘가에 치워 놓음으로써 더 큰 주소 공간을 사용하는 것처럼 만들어야 한다. 일반적으로 그런 곳은 메모리보다 더 큰 용량을 가지고, 더 느리며, 현대 시스템에서 그 역할은 보통 하드 디스크 드라이브가 맡는다.
어떻게 OS는 더 크고 느린 장치를 이용해 투명하고도 큰 가상 주소 공간의 환상을 제공할 수 있을까?
왜 한 프로세스가 그렇게 큰 주소 공간을 사용할 수 있게 해야할까? 정답은 그렇게 하는 게 편하기 때문이다. 큰 주소 공간이 있으면 프로그램의 자료 구조를 위한 충분한 공간이 메모리에 있는지를 걱정할 필요없이, 자연스럽게 프로그램을 작성하고 원하는 만큼 메모리를 할당하면 된다. 스왑 공간을 추가하는 것은 또한, OS가 여러 동시에 실행되는 프로세스들을 위한 큰 가상 공간에 대한 환상을 제공할 수 있게 하기 위함이기도 하다.
우리가 해야할 첫 번째 일은 디스크의 일부 공간을 페이지가 이동할 수 있도록 예약해놓는 것이다. OS에서는 보통 그런 공간을 스왑 공간(swap space)라 부르는데, 우리가 메모리의 페이지를 그 공간으로 내보내고(swap out), 그 공간으로부터 페이지들을 가져오기(swap in) 때문이다. OS가 페이지 크기의 단위로, 스왑 공간을 읽고 쓸 수 있다고 가정하자. 이렇게 하려면 OS는 해당 페이지의 디스크 주소를 기억해야 한다.
스왑 공간의 크기도 중요하다. 왜냐하면 스왑 공간의 크기는 궁극적으로 특정 시간에 시스템에서 사용될 수 있는 메모리 페이지의 최대 수를 결정하기 때문이다. 지금은 간단하게 그 크기가 아주 크다고만 가정하자.
위 예시에서는 4 페이지의 물리 메모리, 8 페이지의 스왑 공간을 사용한다. 여기서 세 프로세스들은 물리 메모리 공간을 공유하고 있는데, 이 프로세스들 각각은 자신의 유효 페이지의 일부만을 메모리에 가지고 있고, 나머지는 디스크의 스왑 공간에 두고 있다. 네 번째 프로세스 Proc3은 현재 실행되고 있지 않으며, 자신의 모든 페이지들을 디스크에 두고 있다.
다만 꼭 스왑 공간만이 스왑을 위해 쓰일 수 있는 유일한 장소인 것은 아니다. 예를 들어 바이너리 프로그램을 실행한다고 해보자. 이 바이너리 파일은 코드 페이지는 처음에는 디스크에 있다가, 프로그램이 실행되면 메모리에 올라간다. 만약 시스템이 어떤 이유로 물리적 메모리에 공간을 만들어야 하게 되면, 시스템은 그냥 해당 코드 페이지의 메모리를 재사용해도 괜찮다. 나중에 디스크에 있는 바이너리 파일을 다시 메모리로 불러들이면 된다는 것만 알면 말이다.
디스크에 공간이 생겼으니, 페이지를 디스크로 스왑 인/아웃하기 위한 장치가 필요하다. 간단히 하드웨어로 관리되는 TLB 시스템을 사용한다고 가정한다.
메모리 참조가 일어나면 어떤 일이 일어나는지 떠올려보자. 실행 중인 프로세스는 가상 메모리 참조를 만들고, 하드웨어는 이를 물리 주소로 변환한 후 원하는 데이터를 메모리로부터 가져온다. 하드웨어는 우선 가상 주소로부터 VPN을 추출하고, 이에 해당하는 엔트리가 TLB에 있는지를 확인한다. 만약 TLB에 있다면(TLB hit) 이에 맞는 물리 주소를 만들고 데이터를 메모리로부터 가져온다.
만약 TLB 미스가 일어나면, 하드웨어는 메모리 내의 페이지 테이블을 찾아 해당 VPN을 인덱스로 하는 PTE를 찾는다. 만약 페이지가 유효하고 물리 메모리에 있다면, 하드웨어는 PFN을 PTE로부터 추출하고, 이를 TLB에 캐싱한 후 명령어를 재실행한다. 이번에는 TLB 히트가 일어난다.
그런데 만약 페이지가 디스크로 스왑되도록 하고 싶다면 추가적인 장치를 이용해야한다. 하드웨어가 PTE를 살펴보면, 페이지가 물리 메모리에 있지 않다는 것을 알 수 있다. 이는 PTE의 present bit을 통해서 알 수 있는데, 만약 present bit가 1이면 해당 페이지가 물리 메모리에 있다는 것이고, 0이라면 페이지가 물리 메모리가 아니라 디스크 어딘가에 있다는 것을 의미한다.
물리 메모리에 있지 않은 페이지에 접근하려고하는 것을 페이지 폴트(page fault)라고 부른다. 페이지 폴트가 일어나면 OS는 페이지 폴트 핸들러를 실행함으로써 페이지 폴트를 처리해야 한다.
TLB 미스와 관련해서 두 종류의 시스템이 있었던 것을 떠올려보자. 바로 하드웨어로 관리되는 TLB와 소프트웨어로 관리되는 TLB였다. 두 종류의 시스템에서 모두, 페이지가 현재 물리 메모리에 없는 경우 OS가 페이지 폴트를 처리하는 역할을 맡게 된다. 사실상 모든 시스템들이 페이지 폴트를 소프트웨어로(즉 OS에게 맡겨) 처리한다.
만약 페이지가 메모리에 올라와있지 않아 페이지 폴트가 일어났다면, OS는 해당 페이지를 디스크로부터 메모리로 스왑 인해야 한다. OS는 원하는 페이지를 어디에서 찾을지 어떻게 알 수 있을까? 이와 관련한 정보들은 보통 페이지 테이블에 저장된다. OS는 PTE의 비트들을 이용해 PFN을 찾던 것처럼 해당 페이지의 디스크 주소를 찾는다. OS는 페이지 폴트를 받았을 때, PTE에서 주소를 찾고,
디스크로부터 해당 페이지를 가져오도록 요청한다.
디스크 I/O 작업이 끝나면 OS는 페이지 테이블에 해당 페이지가 메모리에 있다고 표시하고, PTE의 PFN 필드에 새롭게 가져온 페이지의 메모리 내 주소를 갱신한 후, 명령을 재실행한다. 이때는 TLB 미스가 일어난다. 해당 페이지 테이블이 메모리에는 올라와있지만 TLB에는 캐싱되지 않았기 때문이다. TLB 미스가 일어나면 OS는 해당 변환을 TLB에 업데이트하고 명령을 재실행한다. 이번에는 TLB에서 변환을 찾을 수 있고, 원하던 데이터나 명령을 변환된 물리 주소로부터 가져올 수 있게 된다.
I/O 작업이 실행 중일 때 프로세스는 BLOCKED 상태에 있다는 유념하자. 따라서 OS는 페이지 폴트가 처리되고 있을 때 다른 대기 중인 프로세스를 실행한다. I/O 작업에는 비용이 많이 들기 때문에 이 오버랩은 하드웨어를 멀티프로그램 시스템에서 하드웨어를 효과적으로 사용하는 방법이다.
위와 같은 과정에서 우리는 메모리에 스왑 인 해오기에 충분한 공간이 있다고 가정했다. 그런데 메모리가 이때 메모리가 이미 꽉 차있다면 어떨까? OS는 하나 이상의 페이지들을 쫓아내고 새로운 페이지가 들어올 공간을 확보해야 한다. 이렇게 쫓아낼, 혹은 교체할 페이지를 찾는 과정을 페이지 교체 정책(page-replacement policy)이라 부른다.
페이지 교체 정책은 잘못 만들면 프로그램을 메모리가 아닌 디스크의 속도로 실행되게 한다. 현대의 기술로 따지자면 프로그램이 만 배~십만 배는 느리게 돌아갈 수 있다는 것이다. 이와 관련한 자세한 내용은 다음 장에서 소개될 것이다.
아래는 하드웨어의 페이지 폴트 제어 흐름이다.
VPN = (VirtualAddress & VPN_MASK) >> SHIFT
(Success, TlbEntry) = TLB_Lookup(VPN)
if (Success == True) // TLB Hit
if (CanAccess(TlbEntry.ProtectBits) == True)
Offset = VirtualAddress & OFFSET_MASK
PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
Register = AccessMemory(PhysAddr)
else
RaiseException(PROTECTION_FAULT)
else // TLB Miss
PTEAddr = PTBR + (VPN * sizeof(PTE))
PTE = AccessMemory(PTEAddr)
if (PTE.Valid == False)
RaiseException(SEGMENTATION_FAULT)
else
if (CanAccess(PTE.ProtectBits) == False)
RaiseException(PROTECTION_FAULT)
else if (PTE.Present == True)
TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
RetryInstruction()
else if (PTE.Present == False)
RaiseException(PAGE_FAULT)
TLB 미스가 일어났을 때 고려해야할 세 가지 케이스가 있다.
PFN = FindFreePhysicalPage()
if (PFN == -1) // no free page found
PFN = EvictPage() // replacement algorithm
DiskRead(PTE.DiskAddr, PFN) // sleep (wait for I/O)
PTE.present = True // update page table:
PTE.PFN = PFN // (present/translation)
RetryInstruction() // retry instruction
위는 소프트웨어의 제어 흐름이다. OS는 폴트를 발생시킨 페이지를 위한 물리 프레임을 찾아야 한다. 만약 그럴 공간이 없다면 메모리에 해당 페이지를 위한 공간을 만들 때까지 대기한다. 물리 프레임이 들어오면 핸들러는 스왑 공간으로부터 페이지를 불러들이는 I/O 요청을 만든다. 이 작업이 끝나면 OS는 페이지 테이블을 업데이트하고 명령을 재실행한다. 재실행 됐을 때에는 TLB 미스가 일어나고, 다시 해당 명령을 재실행하면 TLB 히트가 일어나서 하드웨어는 원하던 데이터에 접근할 수 있게 된다.
지금까지 OS는 메모리가 완전히 가득 찰 때까지 기다리고, 오직 그럴 때만 다른 페이지를 위한 공간을 확보하기 위해 페이지를 교체한다고 가정했다. 하지만 이건 현실과는 다르다. OS는 미리 조금의 메모리를 가용 상태로 유지한다.
이를 위해 대부분의 운영체제는 high watermark(HW), low watermark(LW) 를 가지고 있다. 이들은 언제 메모리로부터 페이지를 쫓아낼지를 결정하기 위해 사용된다. 만약 OS가 LW보다 적은 페이지만 사용할 수 있다는 걸 알게 되면, 메모리를 해제하는 데 쓰이는 백그라운드 스레드가 실행된다. 이 백그라운드 스레드는 스왑 데몬(swap daemon), 혹은 페이지 데몬(page daemon)이라 불림.이 스레드는 HW만큼의 페이지가 사용 가능해질 때까지 페이지들을 쫓아낸다.
이렇게 여러 개의 교체를 한 번에 수행하면 성능 최적화가 가능해진다. 많은 시스템들은 여러 페이지들을 하나로 묶고 디스크의 스왑 파티션에 씀으로써 디스크의 효율성을 높인다. 이러한 클러스터링은 디스크의 탐색시간과 회전 지연시간을 줄임으로써 성능을 높인다.
백그라운드 페이징 스레드를 사용하려면 위 소프트웨어 제어 흐름이 조금 바뀌어야 한다. 위에서는 교체를 직접 진행했지만, 이제는 가용 페이지가 있는지의 여부만 간단히 체크하고, 만약 해당 페이지가 없다면 백그라운드 페이징 스레드에 프리 페이지가 필요하다는 걸 알린다. 스레드가 페이지들을 해제하면, 원래 사용하던 스레드를 깨우고 원하던 페이지를 불러들여 작업을 진행한다.
시스템의 물리 메모리보다 크게 메모리를 사용하는 방법에 대해 알아봤다.
페이지 테이블에서는 페이지가 현재 메모리에 있는지 아닌지를 표시하기 위해 present bit을 사용했다. 만약에 해당 페이지가 현재 물리 메모리에 올라와 있지 않으면 OS는 페이지 폴트 핸들러를 실행해, 디스크로부터 원하는 페이지를 물리 메모리로 불러옴으로써 페이지 폴트를 처리한다.
이 일련의 행동들이 프로세스에게는 투명하게 일어난다는 점을 잊지 마자. 프로세스의 입장에서는 그저 자신의 고유한, 연속적인 가상 메모리에 접근할 뿐이지만, 사실 페이지는 물리 메모리의 임의의 장소에 위치해있고, 심지어는 메모리에 있지 않기도 하다.