메모리는 값 저장용 배열이며, CPU는 주소 값으로 특정 위치에 접근합니다. 하나의 프로세스는 메모리를 모두 사용할 수 있지만, 두 개 이상의 프로세스에서는 메모리 자원 분배가 중요합니다.
프로그램은 원래 디스크에 저장되어 있습니다. 프로그램이 실행되기 위한 단계는 다음과 같습니다.
프로그램이 이렇게 메모리로 올라올 때 메모리의 어느 부분을 사용할 지를 결정을 해 주어야 하는데 이것이 바로 주소 할당에 관한 문제입니다.
즉 메모리 접근을 위한 물리적인 주소를 결정해주는 것입니다.
1. Compile Time Binding (컴파일 시간 바인딩)
2. Load Time Binding (로드 시간 바인딩)
3. Execution Time Binding (실행 시간 바인딩)
논리적 주소 : CPU가 생성하는 주소. 가상 주소(virtual address)라고도 불립니다.
물리적 주소 : 메모리가 취급하는 주소
두 주소는 컴파일 시간 바인딩과 적재 시간 바인딩에서는 서로 같은 값을 가집니다.
하지만 실행 시간 바인딩 기법에서는 서로 다른 값을 가지게 됩니다. 실행 시간에 이 논리적 주소를 물리적 주소로 바꿔주는 것이 바로 MMU입니다.
MMU(메모리 관리기, Memory Management Unit)
실행 시간에 논리적 주소를 물리적 주소로 바꿔주는 하드웨어 장치입니다.
가장 단순한 MMU 기법을 예로 들자면, 재배치 레지스터(Relocation Register)에 있는 값 만큼을 CPU에서 받은 논리적 주소에 더해줘서 최종적인 물리적인 주소를 만들게 됩니다.
이 방법은 실행 시간 바인딩 기법 중 하나이며, 대부분의 운영 체제에서 사용됩니다.
Continuous Memory Allocation
이 시스템에서는 두 개의 레지스터가 필요합니다.
따라서 할당된 메모리의 범위 내의 값을 가지는 논리적 주소 값들은 리미트 레지스터를 통과해 재배치 레지스터의 값과 합쳐져서 물리적 주소를 만들어냅니다.
다른 물리주소를 만들기 위해선 당연히 리미트 레지스터와 재배치 레지스터가 가지고 있는 값은 각각의 프로세스마다 달라져야 합니다.
메모리는 비연속적인 물리적 주소 공간을 할당해 줄 수도 있습니다. 그리고 이런 비연속적인 할당을 통해 external fragmentation을 예방할 수 있습니다.
페이징(Paging)은 메모리를 일정한 크기의 블록으로 분할하여 논리적 주소와 물리적 주소를 매핑하는 기법입니다.
페이징에서 사용되는 개념
페이징 과정
이런 방식을 사용하면 각각의 프로세스들은 물리 메모리에서 굳이 연속된 파티션을 할당 받을 필요가 없습니다. 물리 메모리의 어떤 프레임이든 비어있다면 사용할 수 있기 때문에 external fragmentation은 사라지게 되고, 메모리 사용의 효율을 크게 늘릴 수 있습니다.
페이지의 사이즈
운영체제는 이러한 페이지 테이블을 사용하기 위해 여러 방법들을 지원합니다
첫 번쨰 방법
두 번째 방법
세번째 방법
두 번째 방법의 단점을 해결하기 위해 TLB(Translation Look-aside Buffer)라는 것을 사용합니다.
TLB
데이터를 찾아야 할 때는 먼저 TLB를 확인합니다. TLB에 내가 원하는 페이지 번호가 있으면 해당 프레임 번호를 가져와서 메모리의 페이지 테이블을 조회합니다.
이 임시 저장소가 효율적으로 작동하려면 최대한 자주 사용하는 페이지들이 TLB에 저장되어 있는 것이 좋습니다.
Context Switch가 일어나면 원칙적으로는 TLB의 모든 정보를 지워버려야 합니다.
모든 프로세스는 자기 자신의 페이지 테이블과 논리적 주소 공간을 가지고 있기 때문에, 프로세스 간에 페이지의 번호가 같은 경우도 충분히 있을 수 있습니다. 그래서 CPU 내에 다른 프로세스의 TLB가 남아있다면 다른 프로세스의 메모리 공간에 잘못 접근하는 사태가 벌어질 수 있기 때문입니다. 앞에서도 말했지만, 다른 프로세스의 메모리 공간에 접근하는 것은 금지되어 있습니다.
하지만 Context Switch가 일어날 때 마다 모든 TLB를 지우고 새로운 정보를 입력하는 것은 너무 비효율적이라고 생각할 수 있습니다.
그래서 여기서는 ASID(Address-Space IDentifiers)라는 것을 사용합니다. TLB에 key와 value 이 외에 ASID라는 정보를 추가로 제공하는 것인데 이 ASID는 해당 페이지가 어떤 프로세스의 페이지인지에 대한 정보를 담고 있습니다. 다시 말해, 페이지를 검색할 때 우선 ASID를 살펴보고 지금 CPU에서 실행 중인 프로세스의 페이지들만 고려하여 검색을 하는 것입니다.
페이지 테이블을 만들 때, 메모리 보호를 위해서 각 페이지들에 추가적인 비트들을 부여하여 사용할 수 있습니다.
Read-write/Read-only bit
Read-write/Read-only를 표현하는 비트입니다. 당연히 Read-write 모드일 때만 데이터를 수정할 수 있으며 Read-only 모드에서는 읽을 수만 있습니다.
Valid-invalid bit
이 비트가 valid로 설정되어 있으면, 해당 페이지가 프로세스의 논리 주소 공간 안에 있는 합법적인 페이지라는 것을 나타냅니다. 운영체제는 이 비트를 통해서 해당 페이지에 접근을 허용할 것인지 말지를 결정합니다.
예를 들면 14비트의 논리적 주소 공간을 갖는 시스템은 0~16383의 주소를 사용할 수 있습니다. 그리고 이 중에 프로그램이 0~10468까지의 주소만 사용할 수 있다고 가정하면, 2KB짜리 페이지를 0번부터 5번까지 6개 사용할 수 있습니다. 이렇게 되면 페이지 테이블에 0~5번의 페이지에는 valid, 그리고 이 외의 페이지들에는 invalid로 설정해 줄 수 있습니다.
테이블 길이 레지스터(PTLR, Page Table Length Register)
또한 몇몇 시스템에서 사용하는 방법으로, 페이지 테이블의 크기를 저장해 둡니다.
말 그대로 페이지 테이블의 사이즈를 저장하는 레지스터로써, 프로세스가 제시한 주소가 유효한 범위 내에 있는 지를 확인하는 용도로 사용됩니다.
페이징의 큰 장점 중 하나가 바로 코드를 쉽게 공유할 수 있다는 것입니다. 프로세스들은 코드 페이지들이 만약 재진입 가능 코드(Reentrant code, or Pure code)라면 해당 코드 페이지들을 공유할 수 있습니다.
재진입 가능 코드
수행되는 동안 절대 변하지 않는 코드로써(프로그램 내의 어떤 명령어도 프로그램 내의 다른 명령어를 위한 변수들을 수정시키지 않는다는 뜻) 프로그램들이 언제든 변수가 수정되지 않았다는 확신을 가지고 코드 내에 진입할 수 있도록 만들어 줍니다.
이렇게 재진입 가능 코드의 경우에는 코드를 물리 메모리에 올려놓으면 여러 프로세스들이 이 코드를 공유하여 사용할 수 있습니다. (라이브러리와 비슷한 느낌)
배경
현재 사용되는 많은 컴퓨터들은 굉장히 큰 주소 공간을 가집니다. 주소 공간이 커진다는 말은 페이지 테이블도 함께 커질 수 밖에 없다는 것을 뜻합니다. 32비트 논리 주소 공간을 사용할 때, 페이지 사이즈가 4KB라면, 페이지 테이블은 100만 개의 항목을 담을 수 있어야 합니다. 이러한 문제점을 극복하기 위해 나온 방법이 바로 계층적 페이징입니다.
페이지 테이블을 위한 페이지 테이블을 만든다고 생각할 수 있습니다.
논리주소 변환
이러한 경우에 논리주소도 변환이 되어야 합니다.
그래서 페이지 번호가 들어가야 할 자리에 Outer Page Table에서 원하는 페이지를 찾기 위한 주소와, 두 번째 페이지 테이블에서 찾아야 할 주소를 모두 넣어줍니다.
Outer Page Table 그 자체의 주소는 CPU 내의 한 레지스터에 저장해둡니다.
메모리의 용량에 따라서 더 많은 단계를 거칠 수 있는데, 너무 많은 계층을 거치게 되면 최종적인 페이지에 도착할 때 까지 참조해야 할 페이지 테이블이 너무 많아져서 시간이 오래 걸릴 수 있습니다.
주소 공간이 32비트 보다 커지게 되면 해시 페이지 테이블을 많이 사용합니다.
논리적 주소에서 페이지 번호를 해시 함수를 거쳐 만든 해시 값으로 해시 테이블에 접근을 하여 프레임 번호를 찾는 것입니다. 해시 테이블에서 각각의 항목들은 연결 리스트를 가지고 있어서, 자신에게 해시되는 원소들을 쭉 연결시키게 됩니다.
그래서 페이지 테이블을 찾고자하면 우선 페이지 번호를 해시 함수를 돌려서 해시 값을 얻어내고, 이 해시 값으로 해시 테이블에 접근을 하게 됩니다. 그럼 해당 해시 값에 연결 리스트로 구현된 여러 페이지 정보들이 존재하고, 그중에 자신의 페이지 번호와 맞는 정보를 찾아서 프레임 번호를 얻어낼 수 있습니다.(아래 그림 참고)
너무 많은 프로세스가 존재하거나 프로세스가 계속 늘어나게 되면 굉장히 큰 공간이 페이지 테이블로 사용됩니다.
이를 막기 위해 물리 메모리와 같은 사이즈의 큰 하나의 페이지 테이블을 사용하여 모든 프로세스의 페이지 테이블 정보를 한 번에 구현하는 것입니다.
물리 메모리와 페이지 테이블의 크기가 같기 때문에 하나의 프레임 당 하나의 페이지 테이블 항목이 매핑됩니다.
모든 프로세스의 페이지 테이블이 하나에 모여있기 때문에 각각의 항목에 프로세스의 아이디를 추가로 표시해 주어야 합니다.
물론 이렇게 페이지 테이블이 너무 커지게 되면 원하는 페이지를 검색할 때 너무 느려지고, 프로세스 아이디를 이용해 테이블을 만들기 때문에 공유 메모리를 사용하기는 어렵다는 단점이 있습니다.
이 문제를 해결하기 위해 해싱 방법을 접목하여 사용할 수도 있습니다.