Paging

또다른 메모리 관리 기법에는 Paging(페이징)이 있는데, 베이스/바운드나 세그맨테이션과는 다르게 페이징은 고정된 크기로 주소공간을 나눈다.

이렇게 나뉜 고정된 크기의 한 단위를 페이지라고 하며, 가상메모리의 주소공간에서는 이를 페이지라고 부르고 물리메모리에서는 프레임이라고 부른다.

물론 프레임 역시 페이지와 동일한 크기로 물리메모리들을 나눠놓은 단위이다.

예를 들어서 64바이트의 아주 작은 주소공간이 있다고 할 때, 이를 16바이트씩 고정된 크기로 4개 페이지로 나누는 경우를 생각해보자.

전체 주소공간인 64바이트를 어드레싱하기 위해서는 주소에 6비트가 필요한데, 이중에서 특정한 페이지를 인덱싱하기 위해서는 2개 비트만 있으면 된다(4개 페이지).

따라서 주소에서 앞의 두 비트는 4개 페이지를 가리키기 위해 사용하고, 나머지 4개 비트는 각 페이지의 크기가 되는데, 이런 개념은 세그맨테이션과 비슷하다.

21번지라는 가상주소에 접근하는 경우, 21에 특정한 오프셋 마스크와의 비트연산을 통해 오프셋을 가져올 수 있는데, 이 오프셋 값은 가상메모리와 물리메모리에서 동일하다.

대신 가상메모리의 페이지 번호만 물리메모리의 프레임으로 변환해줌으로써 주소 변환이 일어나게 된다.

페이징 기법에서 실제 물리메모리로 주소를 변환하기 위한 정보는, 가상주소에서의 특정 페이지가 물리메모리의 어떤 프레임인지에 대한 정보가 되는 것이다.

예를 들어 물리메모리의 3번 프레임에 내 프로세스의 0번 페이지가 매핑되어있으면, MMU의 주소 변환 과정에서 이런 정보를 활용해서 Virtual Page Number(VPN)가 어떤 Physical Frame Number(PFN)로 바뀌는지만 찾아내는 것이다.

근데 이제 가상주소는 6비트(VPN 2비트, 오프셋 4비트)인데, 물리메모리가 총 256바이트라고 한다면 이보다 크기 때문에 더 많은 비트들을 필요로 한다.

이 때 페이지의 크기는 가상메모리든 물리메모리든 상관 없이 모두 16바이트씩으로 같아야 하기 때문에, 물리메모리에는 총 16개의 프레임이 존재하게 되며, PFN에는 4개 비트를 사용한다.


페이징 기법에서 이런 정보들은 페이지 테이블로 관리하는데, 테이블의 각 원소는 PTE(Page Table Entry)라고 부른다.

가상메모리의 특정 페이지를 가지고 물리메모리의 어떤 프레임인지 알기 위해서는 간단히 생각하면 키-값쌍의 형식으로 저장할 수도 있겠지만, 페이지 테이블이라는 이름에 걸맞게 이런 키들을 정렬함으로써 배열 형식으로 저장이 가능하다.

즉 가상 주소공간에서의 페이지 순서대로 몇번째 프레임과 매핑되어있는지 저장해놓으면, VPN을 인덱스로 하여 테이블에서 랜덤액세스로 PFN를 가져올 수 있게 된다.

이때 VPN을 인덱스로 하는 이러한 페이지 테이블에, 단순히 PFN만 담지 않고 다른 여러가지 플래그들도 포함하는데, 이들을 통틀어서 하나의 PTE라고 하는 것이다.

추가적인 정보들은 control bit라고 부르며, 해당 페이지와 관련돼서 수행되는 작업들에 대해 어떻게 처리할지에 관한 값들이 표시되어있다(자세한 내용은 나중에).

그렇다면 MMU가 이런 정보들을 보고 알맞게 주소변환을 해주기 위해서는, 기존에는 베이스/바운드 레지스터만을 추가적으로 두어(혹은 각 세그먼트 별로) 활용했지만, 페이징 기법의 경우 일반적으로 페이지가 굉장히 많은 편이다.

그래서 모든 페이지들의 PTE들을 위한 공간을 따로 마련하지 않고, 대신 현재 실행중인 프로세스의 페이지테이블의 시작주소(PTBR)만을 기억하고 있는다(물론 문맥교환시에 PTBR들도 같이 스왑해줌).

그렇게 메모리 어딘가에 저장되어있는 페이지테이블에서, 자기가 원하는 엔트리 PTE를 찾아낼 수 있기 때문에 MMU가 이를 활용해 물리메모리로 변환해줄 수 있다.


아래는 MMU가 가상 주소를 받아 물리 주소로 변환해주는 과정이다.

뭔가 소프트웨어적으로 동작하는 코드같아 보이지만, 실제로는 하드웨어적으로 발생하는 작업들을 수도코드 같은 형태로 표현한 것임에 주의해야 한다.

VPN = (VirtualAddress & VPN_MASK) >> SHIFT

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 {
    offset = VirtualAddress & OFFSET_MASK
    PhysicalAddr = (PTE.PFN << PFN_SHIFT)
    Register = AccessMemory(PhysicalAddr)
}

우선 세그맨테이션에서와 비슷하게, VPN 부분을 얻어내기 위해 마스킹을 통해 가상주소에서 페이지 넘버를 가져온다.

그리고 메모리에 존재하는 해당 프로세스의 페이지 테이블에서, 원하는 페이지 넘버(VPN)만큼 인덱싱한 PTE의 주소를 가져오고, AccessMemory를 통해 그 값들을 읽어온다.

페이징 역시 세그맨테이션과 비슷하게, 가상주소에서 사용하지 않는 페이지를 굳이 물리메모리에 할당해놓지 않는다.

그러나 페이지 테이블에는 모든 페이지들을 담아놓기 때문에, 사용하지 않는다는 정보를 PTE 에 표시해놓는데(PTE.Valid - 컨트롤 비트들 중 하나), 만약 사용하지 않는 페이지에 접근하는 경우 폴트를 발생시키는 것이다.

마찬가지로 프로텍션비트를 확인해서도, 읽기나 쓰기 등의 권한이 없는데 수행하려고 하는 경우에도 예외를 발생시켜서 OS가 처리할 수 있도록 한다.

이래저래 검사를 다 통과한 유효한 PTE일 경우에는, 가상주소에서 오프셋을 읽어온 뒤 페이지와 매핑된 프레임넘버와 합쳐줘서 실제 물리주소로 바꿔주는 과정이라고 볼 수 있다.


페이징 기법은 메모리 관리 측면에서 좀 더 이점을 갖는다.

커널이 free-list를 관리할 때, 세그맨테이션을 사용하는 경우 크기가 가변이라 관리하기가 복잡한 반면, 페이징은 고정된 크기를 사용해서 편리해진다.

페이지들의 경계가 딱딱 붙어있고 고정된 크기로 메모리에 차있기 때문에 그렇고, 따라서 external fragmentation 역시 발생하지 않는다.

대신 각 페이지 내부적으로는 사용하지 않는 공간들이 있을 수 있으며, MMU가 주소 변환을 위해 메모리에 매번 액세스하기 때문에 조금 느리다는 단점이 있다.