-
[Jungle] Week10 Pintos_Project3_Virtual Memory: gitbook 해설Pintos 2023. 6. 13. 13:30
Introduction
Project3: Virtual Memory
이제 핀토스의 내부 작동 방식에 어느 정도 익숙해졌을 것입니다. OS는 적절한 동기화를 통해 여러 스레드의 실행을 적절히 처리할 수 있으며, 여러 사용자 프로그램을 한 번에 로드할 수 있습니다. 그러나 실행할 수 있는 프로그램의 수와 크기는 컴퓨터의 주 메모리 크기에 의해 제한됩니다. 이 과제에서는 무한한 메모리라는 환상을 구축하여 이러한 제한을 제거합니다.
지난 과제 위에 이 과제를 구축하게 됩니다. 프로젝트 2의 테스트 프로그램은 프로젝트 3에서도 작동해야 합니다. 프로젝트 2 제출물의 버그는 프로젝트 3에서도 동일한 문제를 일으킬 가능성이 높으므로, 프로젝트 3 작업을 시작하기 전에 프로젝트 2 제출물의 버그를 수정해야 합니다.
프로젝트 3의 경우, 여러분의 편의를 위해 단계별 지침을 제공합니다.Background
Source Files
이 프로젝트의 vm 디렉토리에서 작업합니다. 메이크파일을 업데이트하여 -DVM 설정을 켭니다. 저희는 방대한 양의 템플릿 코드를 제공합니다. 반드시 주어진 템플릿을 따라야 합니다. 즉, 주어진 템플릿에 기반하지 않은 코드를 제출하면 0점을 받습니다. 또한 "변경하지 마세요"라고 표시된 템플릿은 절대로 변경해서는 안 됩니다. 여기에서는 수정할 각 템플릿 파일에 대한 몇 가지 세부 정보를 제공합니다.
- include/vm/vm.h, vm/vm.c
가상 메모리에 대한 일반적인 인터페이스를 제공합니다. 헤더 파일에서 가상 메모리 시스템이 지원해야 하는 다양한 vm_ 유형(VM_UNINIT, VM_ANON, VM_FILE, VM_PAGE_CACHE)에 대한 정의와 설명을 확인할 수 있습니다(지금은 프로젝트 4용이므로 VM_PAGE_CACHE는 무시하세요). 또한 여기에 보조 페이지 테이블을 구현할 것입니다(아래 참조).
- include/vm/uninit.h, vm/uninit.c
초기화되지 않은 페이지에 대한 연산을 제공합니다(vm_type = VM_UNINIT). 현재 설계에서는 모든 페이지가 처음에 초기화되지 않은 페이지로 설정된 다음 익명 페이지 또는 파일 백업 페이지로 변환됩니다.
- include/vm/anon.h, vm/anon.c
익명 페이지에 대한 작업을 제공합니다(vm_type = VM_ANON).
- include/vm/file.h, vm/file.c
파일 지원 페이지에 대한 연산을 제공합니다(vm_-type = VM_FILE).
- include/vm/inspect.h, vm/inspect.c
채점을 위한 메모리 검사 연산을 포함합니다. 이 파일을 변경하지 마십시오.
이 프로젝트에 작성하는 대부분의 코드는 vm 디렉터리에 있는 파일과 이전 프로젝트에서 도입한 파일에 있습니다. 다음과 같은 몇 가지 파일만 처음 접하게 될 것입니다:
- include/devices/block.h, devices/block.c
블록 장치에 대한 섹터 기반 읽기 및 쓰기 액세스를 제공합니다. 이 인터페이스를 사용하여 스왑 파티션을 블록 장치로 액세스할 수 있습니다.Memory Terminology
먼저 메모리와 스토리지에 대한 몇 가지 용어를 소개합니다. 이 용어 중 일부는 프로젝트 2(가상 메모리 레이아웃 참조)에서 익숙한 것이지만 대부분은 새로운 용어입니다.
Pages
가상 페이지라고도 하는 페이지는 길이가 4,096바이트(페이지 크기)인 가상 메모리의 연속적인 영역입니다. 페이지는 페이지 정렬, 즉 페이지 크기로 균등하게 나눌 수 있는 가상 주소에서 시작해야 합니다. 따라서 64비트 가상 주소의 마지막 12비트는 페이지 오프셋(또는 그냥 오프셋)입니다. 상위 비트는 페이지 테이블의 인덱스를 나타내는 데 사용되며 곧 도입될 예정입니다. 64비트 시스템에서는 4단계 페이지 테이블을 사용하므로 가상 주소는 다음과 같이 표시됩니다:63 48 47 39 38 30 29 21 20 12 11 0 +-------------+----------------+----------------+----------------+-------------+------------+ | Sign Extend | Page-Map | Page-Directory | Page-directory | Page-Table | Page | | | Level-4 Offset | Pointer | Offset | Offset | Offset | +-------------+----------------+----------------+----------------+-------------+------------+ | | | | | | +------- 9 ------+------- 9 ------+------- 9 ------+----- 9 -----+---- 12 ----+ Virtual Address
각 프로세스에는 독립적인 사용자(가상) 페이지 세트가 있으며, 이는 가상 주소 KERN_BASE(0x8004000000) 아래에 있는 페이지입니다. 반면 커널(가상) 페이지 세트는 전역이므로 실행 중인 스레드나 프로세스에 관계없이 동일한 위치에 유지됩니다. 커널은 사용자 페이지와 커널 페이지 모두에 액세스할 수 있지만 사용자 프로세스는 자신의 사용자 페이지에만 액세스할 수 있습니다. 자세한 내용은 가상 메모리 레이아웃을 참조하세요.
핀토스는 가상 주소 작업에 유용한 몇 가지 함수를 제공합니다. 자세한 내용은 가상 주소 섹션을 참조하세요.Frames
물리적 프레임 또는 페이지 프레임이라고도 하는 프레임은 물리적 메모리의 연속적인 영역입니다. 페이지와 마찬가지로 프레임도 페이지 크기와 페이지 정렬을 유지해야 합니다. 따라서 64비트 물리적 주소는 다음과 같이 프레임 번호와 프레임 오프셋(또는 그냥 오프셋)으로 나눌 수 있습니다:
12 11 0 +-----------------------+-----------+ | Frame Number | Offset | +-----------------------+-----------+ Physical Address
x86-64는 물리적 주소의 메모리에 직접 액세스할 수 있는 방법을 제공하지 않습니다. 핀토스는 커널 가상 메모리를 물리적 메모리에 직접 매핑하여 이 문제를 해결합니다. 커널 가상 메모리의 첫 번째 페이지는 물리적 메모리의 첫 번째 프레임에, 두 번째 페이지는 두 번째 프레임에 매핑하는 식으로 말이죠. 따라서 커널 가상 메모리를 통해 프레임에 액세스할 수 있습니다.
핀토스는 물리적 주소와 커널 가상 주소 간 변환을 위한 함수를 제공합니다. 자세한 내용은 가상 주소를 참조하십시오.Page Tables
페이지 테이블은 CPU가 가상 주소를 물리적 주소, 즉 페이지에서 프레임으로 변환하는 데 사용하는 데이터 구조입니다. 페이지 테이블 형식은 x86-64 아키텍처에 의해 결정됩니다. 핀토스는 스레드/mmu.c에서 페이지 테이블 관리 코드를 제공합니다.
아래 다이어그램은 페이지와 프레임 간의 관계를 보여줍니다. 왼쪽의 가상 주소는 페이지 번호와 오프셋으로 구성됩니다. 페이지 테이블은 페이지 번호를 프레임 번호로 변환하고, 이 프레임 번호는 수정되지 않은 오프셋과 결합되어 오른쪽의 물리적 주소를 얻습니다.+----------+ .--------------->|Page Table|-----------. / +----------+ | | 12 11 0 V 12 11 0 +---------+----+ +---------+----+ | Page Nr | Ofs| |Frame Nr | Ofs| +---------+----+ +---------+----+ Virt Addr | Phys Addr ^ \_______________________________________/
Swap Slots
스왑 슬롯은 스왑 파티션에 있는 디스크 공간의 페이지 크기 영역입니다. 슬롯 배치를 결정하는 하드웨어 제한이 프레임보다 유연하지만, 스왑 슬롯은 페이지 정렬을 해야 하므로 단점이 없습니다.
Resource Management Overview
다음 데이터 구조를 설계/구현해야 합니다:
Supplemental page table (추가 페이지 테이블)
페이지 테이블을 보완하여 페이지 오류 처리를 활성화합니다. 아래의 보충 페이지 테이블 관리를 참조하십시오.
Frame table (프레임 테이블)
물리적 프레임의 퇴출 정책을 효율적으로 구현할 수 있습니다. 아래 프레임 테이블 관리하기를 참조하세요.
Swap table (스왑 테이블)
스왑 슬롯의 사용량을 추적합니다. 아래 스왑 테이블 관리하기를 참조하세요.
완전히 다른 세 개의 데이터 구조를 구현할 필요는 없습니다. 관련 리소스를 전체 또는 부분적으로 통합 데이터 구조로 병합하는 것이 편리할 수 있습니다.
각 데이터 구조에 대해 각 요소에 어떤 정보가 포함되어야 하는지 결정해야 합니다. 또한 데이터 구조의 범위를 로컬(프로세스별) 또는 전역(시스템 전체에 적용)으로 결정하고 해당 범위 내에서 필요한 인스턴스 수를 결정해야 합니다.
설계를 단순화하기 위해 이러한 데이터 구조를 페이지링할 수 없는 메모리(예: calloc 또는 malloc으로 할당된 메모리)에 저장할 수 있습니다. 즉, 데이터 구조 내의 포인터가 유효하게 유지된다는 것을 확신할 수 있습니다.
Choices of implementation - 구현 선택(성능 관점)
구현할 수 있는 방법으로는 배열, 리스트, 비트맵, 해시 테이블 등이 있습니다. 배열은 종종 가장 간단한 접근 방식이지만, 빈도가 낮은 배열은 메모리를 낭비합니다. 목록도 간단하지만 특정 위치를 찾기 위해 긴 목록을 탐색하면 시간이 낭비됩니다. 배열과 목록 모두 크기를 조정할 수 있지만 목록이 중간에 삽입 및 삭제를 더 효율적으로 지원합니다.
핀토스는 lib/kernel/bitmap.c와 include/lib/kernel/bitmap.h에 비트맵 데이터 구조를 포함하고 있습니다. 비트맵은 비트의 배열로, 각 비트는 참 또는 거짓일 수 있습니다. 비트맵은 일반적으로 (동일한) 리소스 집합의 사용량을 추적하는 데 사용됩니다. 리소스 n이 사용 중이면 비트맵의 비트 n은 참입니다. 핀토스 비트맵은 크기가 고정되어 있지만 크기 조정을 지원하도록 구현을 확장할 수 있습니다.
핀토스에는 해시 테이블 데이터 구조도 포함되어 있습니다(해시 테이블 참조). 핀토스 해시 테이블은 다양한 테이블 크기에 걸쳐 삽입 및 삭제를 효율적으로 지원합니다.
더 복잡한 데이터 구조는 더 나은 성능이나 기타 이점을 제공할 수 있지만, 구현을 불필요하게 복잡하게 만들 수도 있습니다. 따라서 설계의 일부로 고급 데이터 구조(예: 균형 잡힌 이진 트리)를 구현하는 것은 권장하지 않습니다.Managing the Supplemental Page Table
The supplemental page table는 각 페이지에 대한 추가 데이터로 페이지 표를 보완합니다. 페이지 테이블의 형식에 따른 제한 때문에 필요합니다. 이러한 데이터 구조는 종종 "페이지 테이블"이라고도 불리며, 혼동을 줄이기 위해 "보충"이라는 단어를 추가합니다.
보충 페이지 테이블은 최소한 두 가지 용도로 사용됩니다. 가장 중요한 것은 페이지 오류가 발생하면 커널이 보충 페이지 테이블에서 오류가 발생한 가상 페이지를 조회하여 거기에 어떤 데이터가 있어야 하는지 알아내는 것입니다. 둘째, 커널은 프로세스가 종료될 때 보충 페이지 테이블을 참조하여 어떤 리소스를 해제할지 결정합니다.
Organization of Supplemental Page Table보충 페이지 테이블은 원하는 대로 구성할 수 있습니다. 구성에는 세그먼트 또는 페이지 측면에서 최소한 두 가지 기본 접근 방식이 있습니다. 여기서 세그먼트는 연속된 페이지 그룹, 즉 실행 파일 또는 메모리 매핑 파일이 포함된 메모리 영역을 의미합니다.
선택적으로 페이지 테이블 자체를 사용하여 추가 페이지 테이블의 구성원을 추적할 수 있습니다. 이렇게 하려면 threads/mmu.c에서 핀토스 페이지 테이블 구현을 수정해야 합니다. 이 접근 방식은 고급 학생에게만 권장합니다.
Handling page fault
보조 페이지 테이블의 가장 중요한 사용자는 page fault handler(페이지 오류 처리기)입니다. 프로젝트 2에서 페이지 오류는 항상 커널 또는 사용자 프로그램의 버그를 나타냈습니다. 프로젝트 3에서는 더 이상 그렇지 않습니다. 이제 페이지 오류는 파일 또는 스왑 슬롯에서 페이지를 가져와야 함을 나타낼 수 있습니다. 이러한 경우를 처리하려면 보다 정교한 페이지 오류 처리기를 구현해야 합니다. userprog/exception.c의 page_fault()인 페이지 결함 처리기는 vm/vm.c의 페이지 결함 처리기인 vm_try_handle_fault()를 호출합니다. 페이지 결함 처리기는 대략적으로 다음과 같은 작업을 수행해야 합니다:
1. 보충 페이지 테이블에서 오류가 발생한 페이지를 찾습니다. 메모리 참조가 유효한 경우, 추가 페이지 테이블 항목을 사용하여 파일 시스템이나 스왑 슬롯에 있거나 단순히 0 페이지일 수 있는 페이지에 있는 데이터를 찾습니다. 공유(즉, 쓰기 시 복사)를 구현하는 경우 페이지의 데이터가 이미 페이지 프레임에 있지만 페이지 테이블에는 없을 수도 있습니다. 추가 페이지 테이블에 사용자 프로세스가 액세스하려는 주소에 데이터가 없거나 페이지가 커널 가상 메모리 내에 있는 경우 또는 액세스가 읽기 전용 페이지에 쓰려는 시도인 경우 액세스가 유효하지 않은 것으로 표시됩니다. 잘못된 액세스는 프로세스를 종료하여 모든 리소스를 해제합니다.
2. 페이지를 저장할 프레임을 가져옵니다. 공유를 구현하는 경우 필요한 데이터가 이미 프레임에 있을 수 있으며, 이 경우 해당 프레임을 찾을 수 있어야 합니다.
3. 파일 시스템이나 스왑에서 데이터를 읽거나 제로화하는 등의 방법으로 데이터를 프레임으로 가져옵니다. 공유를 구현하는 경우 필요한 페이지가 이미 프레임에 있을 수 있으며, 이 경우 이 단계에서는 아무런 작업이 필요하지 않습니다.
4. 결함이 있는 가상 주소의 페이지 테이블 항목을 실제 페이지로 가리킵니다. threads/mmu.c의 함수를 사용할 수 있습니다.Managing the Frame Table
프레임 테이블에는 각 프레임에 대해 하나의 항목이 포함됩니다. 프레임 테이블의 각 항목에는 현재 사용 중인 페이지에 대한 포인터(있는 경우)와 사용자가 선택한 기타 데이터가 포함됩니다. 프레임 테이블을 사용하면 빈 프레임이 없을 때 퇴거할 페이지를 선택함으로써 핀토에서 퇴거 정책을 효율적으로 구현할 수 있습니다.
사용자 페이지에 사용되는 프레임은 palloc_get_page(PAL_USER)를 호출하여 '사용자 풀'에서 가져와야 합니다. 일부 테스트 케이스가 예기치 않게 실패할 수 있는 "커널 풀"에서 할당하지 않으려면 PAL_USER를 사용해야 합니다. 프레임 테이블 구현의 일부로 palloc.c를 수정하는 경우 두 풀 간의 구분을 유지해야 합니다.
프레임 테이블에서 가장 중요한 작업은 사용되지 않는 프레임을 얻는 것입니다. 프레임이 비어 있으면 이 작업은 쉽습니다. 빈 프레임이 없는 경우 프레임에서 일부 페이지를 제거하여 프레임을 비워야 합니다.
스왑 슬롯을 할당하지 않고는 프레임을 퇴거시킬 수 없는데 스왑 슬롯이 가득 차면 커널이 패닉 상태에 빠집니다. 실제 OS는 이러한 상황을 복구하거나 방지하기 위해 다양한 정책을 적용하지만 이러한 정책은 이 프로젝트의 범위를 벗어납니다.
퇴거 프로세스(The process of eviction)는 대략 다음 단계로 구성됩니다:
1. 페이지 교체 알고리즘을 사용하여 퇴거할 프레임을 선택합니다. 아래에 설명된 페이지 표의 '액세스된' 및 '더티' 비트가 유용하게 사용될 것입니다.
2. 프레임을 참조하는 모든 페이지 테이블에서 해당 프레임에 대한 참조를 제거합니다. 공유를 구현하지 않는 한, 주어진 시간에 하나의 페이지만 프레임을 참조해야 합니다.
3. 필요한 경우 파일 시스템에 페이지를 쓰거나 스왑합니다. 그러면 퇴거된 프레임이 다른 페이지를 저장하는 데 사용될 수 있습니다.Accessed and Dirty Bits
x86-64 하드웨어는 각 페이지의 페이지 테이블 항목(PTE)에 있는 한 쌍의 비트를 통해 페이지 교체 알고리즘을 구현하는 데 약간의 지원을 제공합니다. 페이지에 대한 읽기 또는 쓰기 시 CPU는 페이지의 PTE에서 액세스된 비트를 1로 설정하고 쓰기 시 더티 비트를 1로 설정합니다. CPU는 이러한 비트를 0으로 재설정하지 않지만 OS는 그렇게 할 수 있습니다.
에일리어스, 즉 동일한 프레임을 참조하는 두 개(또는 그 이상의) 페이지를 알고 있어야 합니다. 앨리어싱된 프레임에 액세스하면 액세스된 비트와 더티 비트는 하나의 페이지 테이블 항목(액세스에 사용된 페이지에 대한 항목)에서만 업데이트됩니다. 다른 앨리어스에 대한 액세스 및 더티 비트는 업데이트되지 않습니다.
핀토스에서는 모든 사용자 가상 페이지가 커널 가상 페이지에 별칭이 지정됩니다. 이러한 별칭을 어떻게든 관리해야 합니다. 예를 들어, 코드에서 두 주소 모두에 대해 액세스된 비트와 더티 비트를 확인하고 업데이트할 수 있습니다. 또는 커널이 사용자 가상 주소를 통해서만 사용자 데이터에 액세스하여 문제를 피할 수도 있습니다.
다른 별칭은 공유를 구현하거나 코드에 버그가 있는 경우에만 발생해야 합니다.
액세스 및 더티 비트와 함께 작동하는 함수에 대한 자세한 내용은 페이지 테이블 액세스 및 더티 비트 섹션을 참조하세요.Managing the Swap Table
스왑 테이블은 사용 중인 스왑 슬롯과 사용 가능한 스왑 슬롯을 추적합니다. 프레임에서 스왑 파티션으로 페이지를 내보내기 위해 사용하지 않는 스왑 슬롯을 선택할 수 있어야 합니다. 페이지를 다시 읽거나 페이지가 스왑된 프로세스가 종료될 때 스왑 슬롯을 해제할 수 있어야 합니다.
vm/build 디렉터리에서 pintos-mkdisk swap.dsk --swap-size=n 명령을 사용하여 n-MB 스왑 파티션이 포함된 swap.dsk라는 이름의 디스크를 생성합니다. 그 후 핀토스를 실행하면 swap.dsk가 자동으로 추가 디스크로 첨부됩니다. 또는 --swap-size=n을 사용하여 한 번 실행할 때 임시 n-MB 스왑 디스크를 사용하도록 핀토에 지시할 수 있습니다.
스왑 슬롯은 실제로 퇴거에 필요한 경우에만 느리게 할당해야 합니다. 실행 파일에서 데이터 페이지를 읽고 프로세스 시작 시 즉시 스왑에 쓰는 것은 느리지 않습니다. 특정 페이지를 저장하기 위해 스왑 슬롯을 예약해서는 안 됩니다.
스왑 슬롯의 내용을 프레임으로 다시 읽을 때 스왑 슬롯을 해제합니다.Managing Memory Mapped Files
파일 시스템은 읽기 및 쓰기 시스템 호출을 통해 가장 일반적으로 액세스합니다. 두 번째 인터페이스는 mmap 시스템 호출을 사용하여 파일을 가상 페이지에 '매핑'하는 것입니다. 그러면 프로그램은 파일 데이터에 직접 메모리 명령을 사용할 수 있습니다. 파일 foo의 길이가 0x1000바이트(4KB, 즉 한 페이지)라고 가정해 보겠습니다. foo가 주소 0x5000에서 시작하는 메모리에 매핑되어 있으면 0x5000. .0x5fff 위치에 액세스하는 모든 메모리는 foo의 해당 바이트에 액세스하게 됩니다.
다음은 mmap을 사용하여 콘솔에 파일을 인쇄하는 프로그램입니다. 이 프로그램은 명령줄에 지정된 파일을 열고 가상 주소 0x10000000에 매핑한 다음, 매핑된 데이터를 콘솔에 쓰고(fd 1), 파일 매핑을 해제합니다.#include <stdio.h> #include <syscall.h> int main (int argc UNUSED, char *argv[]) { void *data = (void *) 0x10000000; /* Address at which to map. */ int fd = open (argv[1]); /* Open file. */ void *map = mmap (data, filesize (fd), 0, fd, 0); /* Map file. */ write (1, data, filesize (fd)); /* Write file to console. */ munmap (map); /* Unmap file (optional). */ return 0; }
제출물은 메모리 매핑된 파일에서 어떤 메모리를 사용하는지 추적할 수 있어야 합니다. 이는 매핑된 영역에서 페이지 오류를 적절히 처리하고 매핑된 파일이 프로세스 내에서 다른 세그먼트와 겹치지 않도록 하기 위해 필요합니다.
Memory Management
가상 메모리 시스템을 지원하려면 가상 페이지와 물리적 프레임을 효과적으로 관리해야 합니다. 즉, 어떤 (가상 또는 물리적) 메모리 영역이 어떤 목적으로, 누가, 어떤 용도로 사용되고 있는지 등을 추적해야 합니다. 먼저 보충 페이지 테이블을 다룬 다음 물리적 프레임을 다룰 것입니다. 이해를 돕기 위해 가상 페이지에는 '페이지'라는 용어를, 물리적 페이지에는 '프레임'이라는 용어를 사용한다는 점에 유의하세요.
Page Structure and Operations
struct page
include/vm/vm.h에 정의된 page는 가상 메모리에 있는 페이지를 나타내는 구조입니다. 페이지에 대해 알아야 하는 모든 필수 데이터를 저장합니다. 현재 템플릿에서 구조는 다음과 같습니다:
struct page { const struct page_operations *operations; void *va; /* Address in terms of user space */ struct frame *frame; /* Back reference for frame */ union { struct uninit_page uninit; struct anon_page anon; struct file_page file; #ifdef EFILESYS struct page_cache page_cache; #endif }; };
페이지 연산(아래 참조), 가상 주소 및 물리적 프레임이 있습니다. 또한 유니온 필드도 있습니다. 유니온은 메모리 영역에 서로 다른 유형의 데이터를 저장할 수 있는 특수 데이터 유형입니다. 유니온에는 여러 멤버가 있지만 한 번에 하나의 멤버만 값을 포함할 수 있습니다. 즉, 시스템에서 페이지는 uninit_page, anon_page, file_page 또는 page_cache가 될 수 있습니다. 예를 들어 페이지가 익명 페이지인 경우(익명 페이지 참조) 페이지 구조체에는 익명 페이지에 필요한 모든 정보를 포함하는 필드 구조체 anon_page anon이 멤버 중 하나로 포함됩니다.
Page Operations
위에서 설명한 대로 include/vm/vm.h에 정의된 대로 페이지의 위치는 VM_UNINIT, VM_ANON 또는 VM_FILE이 될 수 있습니다. 페이지에 대해 스왑인, 스왑아웃, 페이지 소멸 등 여러 가지 작업을 수행할 수 있습니다. 각 페이지 유형에 따라 이러한 작업에 필요한 단계와 작업이 다릅니다. 즉, VM_ANON 페이지와 VM_FILE 페이지에 대해 서로 다른 소멸 함수를 호출해야 합니다. 한 가지 방법은 각 함수에 스위치 케이스 구문을 사용하여 각 케이스를 처리하는 것입니다. 이를 처리하기 위해 객체 지향 프로그래밍의 "클래스 상속" 개념을 도입합니다. 실제로 C 프로그래밍 언어에는 "클래스"나 "상속"이라는 개념이 없으며, Linux와 같은 실제 운영 체제 코드에서 유사한 방식으로 함수 포인터를 활용하여 개념을 구현합니다.
함수 포인터는 지금까지 배운 다른 포인터와 마찬가지로 메모리 내의 함수 또는 실행 코드를 가리키는 포인터입니다. 함수 포인터는 런타임 값에 따라 특정 함수를 호출하여 검사 없이 실행할 수 있는 간단한 방법을 제공하기 때문에 유용합니다. 이 경우 코드 수준에서 단순히 destroy(page)를 호출하는 것만으로도 충분하며, 컴파일러는 올바른 함수 포인터를 호출하여 페이지 유형에 따라 적절한 파괴 루틴을 선택합니다.
페이지 오퍼레이션을 위한 구조체 page_operations는 include/vm/vm.h에 정의되어 있습니다. 이 구조를 3개의 함수 포인터를 포함하는 함수 테이블로 생각하면 됩니다.struct page_operations { bool (*swap_in) (struct page *, void *); bool (*swap_out) (struct page *); void (*destroy) (struct page *); enum vm_type type; };
이제 페이지_운영 구조를 어디에서 찾을 수 있는지 살펴봅시다. include/vm/vm.h의 페이지 구조체 페이지를 살펴보면 operations라는 필드가 있는 것을 볼 수 있습니다. 이제 vm/file.c로 이동하면 함수 프로토타입 앞에 선언된 page_operations 구조체 file_ops를 볼 수 있습니다. 이것은 파일 백업 페이지에 대한 함수 포인터 표입니다. .destroy 필드에는 페이지를 삭제하는 함수이며 같은 파일에 정의된 file_backed_destroy 값이 있습니다.
함수 포인터 인터페이스를 사용하여 file_backed_destroy가 어떻게 호출되는지 이해해 보겠습니다. vm_dealloc_page(페이지)(vm/vm.c에서)가 호출되고 이 페이지가 파일 백업 페이지(VM_FILE)라고 가정합니다. 함수 내부에서는 destroy(페이지)를 호출합니다. destroy(페이지)는 다음과 같이 include/vm/vm.h에 매크로로 정의되어 있습니다:#define destroy(page) if ((page)->operations->destroy) (page)->operations->destroy (page)
이것은 destroy 함수를 호출하면 실제로 페이지 구조에서 검색된 파괴 함수인 (페이지)->작동->파괴(페이지)를 호출한다는 것을 알려줍니다. 이 페이지는 VM_FILE 페이지이므로 .destroy 필드는 file_backed_destory를 가리킵니다. 결과적으로 파일 백업 페이지에 대한 소멸 루틴이 수행됩니다.
Implement Supplemental Page Table
이 시점에서 Pintos에는 메모리의 가상 및 물리적 매핑을 관리하기 위한 페이지 테이블(pml4)이 있습니다. 그러나 이것만으로는 충분하지 않습니다. 이전 섹션에서 설명한 대로 페이지 오류 및 리소스 관리를 처리하기 위해 각 페이지에 대한 추가 정보를 저장할 보조 페이지 테이블도 필요합니다. 따라서 프로젝트 3의 첫 번째 과제로 보충 페이지 테이블에 대한 몇 가지 기본 기능을 구현하는 것이 좋습니다.
Implement supplemental page table management functions in vm/vm.c.
먼저 Pintos에서 supplemental page table를 어떻게 디자인할지 결정해야 합니다. 나만의 supplemental page table을 디자인한 후 디자인과 관련하여 아래 세 가지 기능을 구현합니다.
void supplemental_page_table_init (struct supplemental_page_table *spt);
Initializes the supplemental page table. You may choose the data structure to use for the supplemental page table. The function is called when a new process starts (in initd of userprog/process.c) and when a process is being forked (in __do_fork of userprog/process.c).
추가 페이지 표를 초기화합니다. 추가 페이지 테이블에 사용할 데이터 구조를 선택할 수 있습니다. 이 함수는 새 프로세스가 시작될 때(userprog/process.c의 initd에서), 프로세스가 포크될 때(userprog/process.c의 __do_fork에서) 호출됩니다.
struct page *spt_find_page (struct supplemental_page_table *spt, void *va);
Find struct page that corresponds to va from the given supplemental page table. If fail, return NULL.
주어진 보충 페이지 테이블에서 va에 해당하는 구조체 페이지를 찾습니다. 실패하면 NULL을 반환합니다.
bool spt_insert_page (struct supplemental_page_table *spt, struct page *page);
Insert struct page into the given supplemental page table. This function should checks that the virtual address does not exist in the given supplemental page table.
주어진 추가 페이지 테이블에 구조체 페이지를 삽입합니다. 이 함수는 주어진 추가 페이지 테이블에 가상 주소가 없는지 확인해야 합니다.
Frame Management
이제부터는 모든 페이지가 메모리가 생성될 당시의 메타데이터만 보관하는 것이 아닙니다. 따라서 물리 메모리를 관리하기 위해서는 다른 체계가 필요합니다. include/vm/vm.h에는 물리적 메모리를 나타내는 struct frame(구조체 프레임)이 존재합니다. 현재 구조체는 다음과 같습니다:
/* The representation of "frame" */ struct frame { void *kva; struct page *page; };
커널 가상 주소인 kva와 페이지 구조인 페이지의 두 필드만 있습니다. 프레임 관리 인터페이스를 구현할 때 멤버를 더 추가할 수 있습니다.
Implement vm_get_frame, vm_claim_page and vm_do_claim_page in vm/vm.c.
static struct frame *vm_get_frame (void);
Gets a new physical page from the user pool by calling palloc_get_page. When successfully got a page from the user pool, also allocates a frame, initialize its members, and returns it. After you implement vm_get_frame, you have to allocate all user space pages (PALLOC_USER) through this function. You don't need to handle swap out for now in case of page allocation failure. Just mark those case with PANIC ("todo") for now.
palloc_get_page를 호출하여 사용자 풀에서 새 물리적 페이지를 가져옵니다. 사용자 풀에서 페이지를 성공적으로 가져온 경우 프레임을 할당하고 멤버를 초기화한 후 반환합니다. vm_get_frame을 구현한 후에는 이 함수를 통해 모든 사용자 공간 페이지(PALLOC_USER)를 할당해야 합니다. 페이지 할당에 실패할 경우 지금은 스왑 아웃을 처리할 필요가 없습니다. 당분간은 PANIC("할 일")으로 표시하면 됩니다.
bool vm_do_claim_page (struct page *page);
Claims, meaning allocate a physical frame, a page. You first get a frame by calling vm_get_frame (which is already done for you in the template). Then, you need to set up the MMU. In other words, add the mapping from the virtual address to the physical address in the page table. The return value should indicate whether the operation was successful or not.
클레임, 즉 물리적 프레임, 페이지를 할당합니다. 먼저 템플릿에서 이미 완료된 vm_get_frame을 호출하여 프레임을 가져옵니다. 그런 다음 MMU를 설정해야 합니다. 즉, 가상 주소에서 페이지 테이블의 실제 주소로 매핑을 추가합니다. 반환 값은 작업이 성공했는지 여부를 나타내야 합니다.
bool vm_claim_page (void *va);
Claims the page to allocate va. You will first need to get a page and then calls vm_do_claim_page with the page.
VA를 할당할 페이지를 청구합니다. 먼저 페이지를 가져온 다음 해당 페이지로 vm_do_claim_page를 호출해야 합니다.
Anonymous Page
이 프로젝트의 이 부분에서는 익명 페이지라는 디스크 기반이 아닌 이미지를 구현합니다.
익명 매핑에는 백업 파일이나 장치가 없습니다. 익명 페이지는 파일 기반 페이지와 달리 이름이 지정된 파일 소스가 없기 때문에 익명입니다. 익명 페이지는 스택 및 힙과 같은 실행 파일에 사용됩니다.
익명 페이지를 설명하는 구조체는 include/vm/anon.h에 anon_page가 있습니다. 현재는 비어 있지만 구현할 때 필요한 정보나 익명 페이지의 상태를 저장하기 위해 멤버를 추가할 수 있습니다. 또한 페이지의 일반 정보를 포함하는 include/vm/page.h의 구조체 페이지를 참조하세요. 익명 페이지의 경우 페이지 구조체에 구조체 anon_page anon이 포함되어 있습니다.Page Initialization with Lazy Loading
지연 로딩은 메모리가 필요한 시점까지 메모리 로딩을 지연시키는 설계입니다. 페이지가 할당되어 해당 페이지에 해당하는 페이지 구조가 있지만 전용 물리적 프레임이 없고 페이지의 실제 콘텐츠가 아직 로드되지 않은 상태입니다. 콘텐츠는 실제로 필요한 시점에만 로드되며, 이는 페이지 오류로 표시됩니다.
페이지 유형이 세 가지이므로 초기화 루틴은 각 페이지마다 다릅니다. 아래 섹션에서 다시 설명하겠지만 여기서는 페이지 초기화 흐름에 대한 개략적인 보기를 제공합니다. 먼저, 커널이 새 페이지 요청을 수신하면 vm_alloc_page_with_initializer가 호출됩니다. 이 이니셜라이저는 페이지 구조를 할당하고 페이지 유형에 따라 적절한 이니셜라이저를 설정하여 새 페이지를 초기화한 후 사용자 프로그램에 제어권을 다시 반환합니다. 사용자 프로그램이 실행되는 동안 프로그램이 소유하고 있다고 생각하지만 아직 페이지에 콘텐츠가 없는 페이지에 액세스하려고 시도하기 때문에 페이지 오류가 발생합니다. 오류 처리 절차 중에 uninit_initialize가 호출되어 앞서 설정한 이니셜라이저를 호출합니다. 이 이니셜라이저는 익명 페이지의 경우 anon_initializer, 파일 지원 페이지의 경우 file_backed_initializer가 됩니다.
페이지는 초기화->(page_fault->lazy-load->스왑인>스왑아웃->...)->파괴의 수명 주기를 가질 수 있습니다. 라이프사이클의 각 전환마다 필요한 절차는 페이지 타입(또는 VM_TYPE)에 따라 다르며, 이전 단락에서는 초기화에 대한 예제였습니다. 이 프로젝트에서는 각 페이지 유형별로 이러한 전환 프로세스를 구현합니다.Lazy Loading for Executable
지연 로딩에서는 프로세스가 실행을 시작할 때 즉시 필요한 메모리 부분만 주 메모리에 로드합니다. 이렇게 하면 모든 바이너리 이미지를 한 번에 메모리에 로드하는 에지 로딩에 비해 오버헤드를 줄일 수 있습니다.
지연 로딩을 지원하기 위해 include/vm/vm.h에 VM_UNINIT이라는 페이지 유형을 도입했습니다. 모든 페이지는 처음에 VM_UNINIT 페이지로 생성됩니다. 초기화되지 않은 페이지에 대한 페이지 구조체(include/vm/uninit.h의 구조체 uninit_page)도 제공합니다. 초기화되지 않은 페이지를 생성, 초기화 및 소멸하는 함수는 include/vm/uninit.c에서 찾을 수 있으며, 이 함수는 나중에 완성해야 합니다.
페이지 오류 발생 시 페이지 오류 처리기(userprog/exception.c의 page_fault)는 먼저 유효한 페이지 오류인지 확인하는 vm/vm.c의 vm_try_handle_fault로 제어를 전송합니다. 여기서 유효한 오류란 유효하지 않은 페이지에 액세스하는 오류를 의미합니다. 가짜 오류인 경우 페이지에 일부 콘텐츠를 로드하고 사용자 프로그램에 제어권을 반환합니다.
가짜 페이지 오류에는 지연 로드, 스왑아웃 페이지, 쓰기 보호 페이지의 세 가지 경우가 있습니다(쓰기 시 복사(추가) 참조). 지금은 첫 번째 경우인 지연 로드된 페이지에 대해서만 살펴보겠습니다. 지연 로딩에 대한 페이지 오류인 경우 커널은 이전에 vm_alloc_page_with_initializer에서 설정한 초기화기 중 하나를 호출하여 세그먼트를 지연 로드합니다. userprog/process.c에서 lazy_load_segment를 구현해야 합니다.Implement vm_alloc_page_with_initializer(). You should fetch an appropriate initializer according to the passed vm_type and call uninit_new with it.
bool vm_alloc_page_with_initializer (enum vm_type type, void *va, bool writable, vm_initializer *init, void *aux);
주어진 타입의 초기화되지 않은 페이지를 생성합니다. uninit 페이지의 swap_in 핸들러는 유형에 따라 페이지를 자동으로 초기화하고, 주어진 AUX로 INIT를 호출합니다. 페이지 구조가 완성되면 프로세스의 보조 페이지 테이블에 페이지를 삽입합니다. vm.h에 정의된 VM_TYPE 매크로를 사용하면 편리합니다.
페이지 결함 처리기는 호출 체인을 따라가다가 최종적으로 swap_in을 호출할 때 uninit_intialize에 도달합니다. 우리는 이에 대한 완전한 구현을 제공합니다. 하지만 설계에 따라 uninit_initialize를 수정해야 할 수도 있습니다.
static bool uninit_initialize (struct page *page, void *kva);
첫 번째 오류 발생 시 페이지를 초기화합니다. 템플릿 코드는 먼저 vm_initializer와 aux를 가져와 함수 포인터를 통해 해당 페이지_초기화자를 호출합니다. 디자인에 따라 함수를 수정해야 할 수도 있습니다.
필요에 따라 vm/anon.c에서 vm_anon_init 및 anon_initializer를 수정할 수 있습니다.
void vm_anon_init (void);
익명 페이지 하위 시스템에 대해 초기화합니다. 이 함수에서는 익명 페이지와 관련된 모든 것을 설정할 수 있습니다.
bool anon_initializer (struct page *page,enum vm_type type, void *kva);
이 함수는 먼저 page-> operations에서 익명 페이지에 대한 핸들러를 설정합니다. 현재 빈 구조체인 anon_page의 일부 정보를 업데이트해야 할 수도 있습니다. 이 함수는 익명 페이지(예: VM_ANON)의 이니셜라이저로 사용됩니다.
Implement load_segment and lazy_load_segment in userprog/process.c. Implement segment loading from executables. All of these pages should be loaded lazily, that is, only as the kernel intercepts page faults for them.
프로그램 로더의 핵심인 userprog/process.c의 load_segment에 있는 루프를 수정해야 합니다. 루프가 돌아갈 때마다 vm_alloc_page_with_initializer를 호출하여 보류 중인 페이지 객체를 생성합니다. 페이지 오류가 발생하면 파일에서 세그먼트가 실제로 로드되는 시점입니다.
static bool load_segment (struct file *file, off_t ofs, uint8_t *upage, uint32_t read_bytes, uint32_t zero_bytes, bool writable);
현재 코드는 파일에서 읽을 바이트 수와 메인 루프 내에서 0으로 채울 바이트 수를 계산합니다. 그런 다음 vm_alloc_page_with_initializer를 호출하여 보류 중인 객체를 생성합니다. vm_alloc_page_with_initializer에 제공할 보조 값을 보조 인수로 설정해야 합니다. 바이너리 로딩에 필요한 정보를 포함하는 구조를 생성할 수 있습니다.
static bool lazy_load_segment (struct page *page, void *aux);
load_segment에서 vm_alloc_page_with_initializer의 네 번째 인수로 lazy_load_segment가 제공되는 것을 보셨을 것입니다. 이 함수는 실행 파일의 페이지에 대한 이니셜라이저이며 페이지 오류가 발생할 때 호출됩니다. 이 함수는 페이지 구조체와 aux를 인자로 받습니다. aux는 load_segment에서 설정한 정보입니다. 이 정보를 사용하여 세그먼트를 읽을 파일을 찾아서 결국 세그먼트를 메모리로 읽어야 합니다.
You should adjust the setup_stack in userprog/process.c to fit stack allocation into the new memory management system.
첫 번째 스택 페이지는 느리게 할당할 필요가 없습니다. 로드 시점에 명령줄 인수를 사용하여 할당하고 초기화할 수 있으므로 결함이 발생할 때까지 기다릴 필요가 없습니다. 스택을 식별하는 방법을 제공해야 할 수도 있습니다. vm/vm.h의 vm_ 유형에 있는 보조 마커(예: VM_MARKER_0)를 사용하여 페이지를 표시할 수 있습니다.
마지막으로, vm_try_handle_fault 함수를 수정하여 오류 주소에 해당하는 페이지 구조를 spt_find_page를 통해 보조 페이지 테이블을 참조하여 해결합니다.
모든 요구 사항을 구현한 후 포크를 제외한 프로젝트 2의 모든 테스트를 통과해야 합니다.Supplemental Page Table - Revisit
이제 복사 및 정리 작업을 지원하기 위해 추가 페이지 테이블 인터페이스를 다시 살펴봅니다. 이러한 작업은 프로세스를 생성(보다 구체적으로 하위 프로세스 생성)하거나 삭제할 때 필요합니다. 자세한 내용은 아래에 자세히 설명되어 있습니다. 이 시점에서 보충 페이지 테이블을 다시 살펴보는 이유는 위에서 구현한 초기화 함수 중 일부를 사용하고 싶을 수 있기 때문입니다.
Implement supplemental_page_table_copy and supplemental_page_table_kill in vm/vm.c.
bool supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src);
추가 페이지 테이블을 src에서 dst로 복사합니다. 자식이 부모의 실행 컨텍스트를 상속해야 할 때 사용됩니다(예: 포크()). src의 보충 페이지 테이블에 있는 각 페이지를 반복하고 dst의 보충 페이지 테이블에 있는 항목의 정확한 복사본을 만듭니다. 유니트 페이지를 할당하고 즉시 클레임해야 합니다.
void supplemental_page_table_kill (struct supplemental_page_table *spt);
보조 페이지 테이블이 보유하고 있던 모든 리소스를 해제합니다. 이 함수는 프로세스가 종료될 때 호출됩니다(userprog/process.c의 process_exit()). 페이지 항목을 반복하고 테이블의 페이지에 대해 destroy(page)를 호출해야 합니다. 이 함수에서 실제 페이지 테이블(pml4)과 물리적 메모리(팔로잉된 메모리)는 걱정할 필요가 없습니다. 호출자는 보조 페이지 테이블이 정리된 후 이를 정리하기 때문입니다.
Page Cleanup
Implement uninit_destroy in vm/uninit.c and anon_destroy in vm/anon.c. This is handler for destroy operation on uninitialized page. Even though uninitialized pages are transmuted to the other page objects, there still can be uninit page when the process exits.
static void uninit_destroy (struct page *page);
페이지 구조체가 보유하던 리소스를 해제합니다. 페이지의 VM 유형을 확인하고 그에 따라 처리하는 것이 좋습니다.
지금은 익명 페이지만 처리할 수 있습니다. 나중에 이 기능을 다시 방문하여 파일로 백업된 페이지를 정리할 수 있습니다.
static void anon_destroy (struct page *page);
익명 페이지가 보유하고 있던 리소스를 해제합니다. 페이지 구조체를 명시적으로 해제할 필요는 없으며 호출자가 해제해야 합니다.
이제 프로젝트 2의 모든 테스트가 통과되어야 합니다.
Stack Growth
프로젝트 2에서 스택은 USER_STACK에서 시작하는 단일 페이지였으며, 프로그램 실행은 이 크기로 제한되었습니다. 이제 스택이 현재 크기 이상으로 커지면 필요에 따라 추가 페이지를 할당합니다.
스택 액세스로 '보이는' 경우에만 추가 페이지를 할당합니다. 스택 액세스와 다른 액세스를 구분할 수 있는 휴리스틱을 고안합니다.
사용자 프로그램이 스택 포인터 아래의 스택에 쓰면 버그가 발생하는데, 일반적인 실제 OS는 스택의 데이터를 수정하는 '신호'를 전달하기 위해 언제든지 프로세스를 중단할 수 있기 때문입니다. 그러나 x86-64 PUSH 명령어는 스택 포인터를 조정하기 전에 액세스 권한을 확인하므로 스택 포인터 8바이트 아래에서 페이지 오류가 발생할 수 있습니다.
사용자 프로그램의 스택 포인터의 현재 값을 얻을 수 있어야 합니다. 시스템 호출 또는 사용자 프로그램에 의해 생성된 페이지 결함 내에서 각각 syscall_handler() 또는 page_fault()로 전달된 구조체 intr_frame의 rsp 멤버에서 해당 값을 검색할 수 있습니다. 유효하지 않은 메모리 액세스를 감지하기 위해 페이지 결함에 의존하는 경우 커널에서 페이지 결함이 발생하는 다른 경우를 처리해야 합니다. 프로세서는 예외로 인해 사용자 모드에서 커널 모드로 전환될 때만 스택 포인터를 저장하므로, page_fault()로 전달된 구조체 intr_frame에서 rsp를 읽으면 사용자 스택 포인터가 아닌 정의되지 않은 값이 반환됩니다. 사용자 모드에서 커널 모드로 처음 전환할 때 구조체 스레드에 rsp를 저장하는 등 다른 방법을 마련해야 합니다.Implement stack growth functionalities. To implement this, you first modify vm_try_handle_fault in vm/vm.c to identify the stack growth. After identifying the stack growth, you should make a call to vm_stack_growth in vm/vm.c to grow the stack. Implement the vm_stack_growth.
bool vm_try_handle_fault (struct intr_frame *f, void *addr, bool user, bool write, bool not_present);
이 함수는 페이지 오류 예외를 처리하는 동안 userprog/exception.c의 page_fault에서 호출됩니다. 이 함수에서는 페이지 오류가 스택 증가에 유효한 경우인지 여부를 확인해야 합니다. 스택 증가로 오류를 처리할 수 있음을 확인했다면 오류가 발생한 주소로 vm_stack_growth를 호출합니다.
void vm_stack_growth (void *addr);
추가 주소가 더 이상 결함이 있는 주소가 되지 않도록 하나 이상의 익명 페이지를 할당하여 스택 크기를 늘립니다. 할당을 처리할 때 addr을 PGSIZE로 반올림해야 합니다.
대부분의 OS는 스택 크기에 절대적인 제한을 두고 있습니다. 일부 OS에서는 사용자가 제한을 조정할 수 있습니다(예: 많은 Unix 시스템에서 ulimit 명령). 많은 GNU/Linux 시스템에서 기본 제한은 8MB입니다. 이 프로젝트의 경우 스택 크기를 최대 1MB로 제한해야 합니다.
이제 모든 스택 증가 테스트 케이스가 통과되어야 합니다.
Memory Mapped Files
이 섹션에서는 메모리 매핑 페이지를 구현합니다. 익명 페이지와 달리 메모리 매핑 페이지는 파일 백업 매핑입니다. 페이지의 콘텐츠는 일부 기존 파일의 데이터를 미러링합니다. 페이지 오류가 발생하면 즉시 물리적 프레임이 할당되고 콘텐츠가 파일에서 메모리로 복사됩니다. 메모리 매핑된 페이지가 매핑 해제되거나 교체되면 콘텐츠의 모든 변경 사항이 파일에 반영됩니다.
mmap and munmap System Call
Implement mmap and munmap, which are the two system calls for memory mapped files. Your VM system must load pages lazily in mmap regions and use the mmaped file itself as a backing store for the mapping. You should implement and use do_mmap and do_munmap defined in vm/file.c to implement these two system calls.
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
오프셋 바이트에서 시작하여 fd로 열린 파일을 프로세스의 가상 주소 공간인 addr에 길이 바이트로 매핑합니다. 전체 파일은 addr에서 시작하는 연속적인 가상 페이지로 매핑됩니다. 파일 길이가 PGSIZE의 배수가 아닌 경우 최종 매핑된 페이지의 일부 바이트가 파일 끝을 넘어 '튀어나오게' 됩니다. 페이지에 오류가 발생하면 이 바이트를 0으로 설정하고 페이지가 디스크에 다시 기록될 때 이 바이트를 삭제합니다. 성공하면 이 함수는 파일이 매핑된 가상 주소를 반환합니다. 실패하면 파일을 매핑할 수 있는 유효한 주소가 아닌 NULL을 반환해야 합니다.
fd로 열린 파일의 길이가 0바이트인 경우 mmap 호출이 실패할 수 있습니다. addr이 페이지 정렬되지 않았거나 매핑된 페이지 범위가 스택 또는 실행 파일 로드 시 매핑된 페이지를 포함하여 매핑된 페이지의 기존 세트와 겹치는 경우 실패해야 합니다. Linux에서는 addr이 NULL이면 커널이 매핑을 생성할 적절한 주소를 찾습니다. 간단하게 하기 위해, 주어진 주소로 매핑을 시도하면 됩니다. 따라서 일부 핀토스 코드는 가상 페이지 0이 매핑되지 않은 것으로 가정하므로 addr이 0이면 실패해야 합니다. 길이가 0이면 mmap도 실패해야 합니다. 마지막으로 콘솔 입력 및 출력을 나타내는 파일 기술자는 매핑할 수 없습니다.
메모리 매핑된 페이지도 익명 페이지와 마찬가지로 지연 방식으로 할당해야 합니다. vm_alloc_page_with_initializer 또는 vm_alloc_page를 사용하여 페이지 객체를 만들 수 있습니다.void munmap (void *addr);
지정된 주소 범위 addr에 대한 매핑을 언매핑하며, 이 주소는 아직 매핑이 언매핑되지 않은 동일한 프로세스에서 이전에 mmap을 호출하여 반환한 가상 주소여야 합니다.
프로세스가 종료되거나 다른 방법으로 종료될 때 모든 매핑은 암시적으로 매핑 해제됩니다. 매핑이 암시적이든 명시적이든 매핑이 해제되면 프로세스가 쓴 모든 페이지가 파일에 다시 쓰여지며, 쓰지 않은 페이지는 다시 쓰여서는 안 됩니다. 그러면 해당 페이지는 프로세스의 가상 페이지 목록에서 제거됩니다.
파일을 닫거나 제거해도 해당 매핑은 해제되지 않습니다. 일단 생성된 매핑은 유닉스 규칙에 따라 munmap이 호출되거나 프로세스가 종료될 때까지 유효합니다. 자세한 내용은 열린 파일 제거하기를 참조하세요. 각 매핑에 대해 파일에 대한 별도의 독립적인 참조를 얻으려면 file_reopen 함수를 사용해야 합니다.
두 개 이상의 프로세스가 동일한 파일을 매핑하는 경우 일관된 데이터를 볼 필요는 없습니다. 유닉스에서는 두 매핑이 동일한 물리적 페이지를 공유하도록 하여 이 문제를 처리하며, mmap 시스템 호출에는 클라이언트가 페이지가 공유인지 비공개인지(즉, 쓰기 시 복사)를 지정할 수 있는 인수도 있습니다.
필요에 따라 vm/vm.c에서 vm_file_init 및 vm_file_initializer를 수정할 수 있습니다.void vm_file_init (void);
파일 백업 페이지 하위 시스템을 초기화합니다. 이 기능에서는 파일 백업 페이지와 관련된 모든 것을 설정할 수 있습니다.
bool file_backed_initializer (struct page *page, enum vm_type type, void *kva);
파일 지원 페이지를 초기화합니다. 이 함수는 먼저 page-> operations에서 파일 백업 페이지에 대한 핸들러를 설정합니다. 메모리를 백업하는 파일과 같은 페이지 구조체의 일부 정보를 업데이트할 수 있습니다.
static void file_backed_destroy (struct page *page);
관련 파일을 닫아 파일 백업 페이지를 삭제합니다. 콘텐츠가 더러워진 경우 변경 내용을 파일에 다시 써야 합니다. 이 함수에서 페이지 구조체를 해제할 필요는 없습니다. 이 작업은 file_backed_destroy의 호출자가 처리해야 합니다.
Swap In/Out
메모리 스왑은 물리적 메모리 사용량을 최대화하기 위한 메모리 회수 기술입니다. 주 메모리의 프레임이 할당되면 시스템은 사용자 프로그램의 메모리 할당 요청을 더 이상 처리할 수 없습니다. 한 가지 해결책은 현재 사용되지 않는 메모리 프레임을 디스크에 스왑아웃하는 것입니다. 이렇게 하면 일부 메모리 리소스를 확보하여 다른 애플리케이션에서 사용할 수 있습니다.
스왑은 운영 체제에서 수행됩니다. 시스템이 메모리가 부족하다는 것을 감지하고 메모리 할당 요청을 받으면 디스크 스왑을 위해 퇴거할 페이지를 선택합니다. 그런 다음 메모리 프레임의 정확한 상태가 디스크에 복사됩니다. 프로세스가 스왑아웃된 페이지에 액세스하려고 하면 OS는 정확한 콘텐츠를 메모리로 다시 가져와서 페이지를 복구합니다.
퇴거를 위해 선택된 페이지는 익명 페이지 또는 파일 백업 페이지일 수 있습니다. 이 섹션에서는 각 경우를 처리합니다.
모든 스와핑 연산은 명시적으로 호출되지 않고 함수 포인터로 호출됩니다. 이들은 각 페이지의 초기화기에 대한 연산으로 등록될 struct page_operations file_ops의 멤버입니다.Anonymous Page
Modify vm_anon_init and anon_initializer in vm/anon.c.
익명 페이지에는 백업 스토리지가 없습니다. 익명 페이지의 스왑을 지원하기 위해 스왑 디스크라는 임시 백업 스토리지를 제공합니다. 익명 페이지에 대한 스왑을 구현할 때 스왑 디스크를 활용하게 됩니다.
void vm_anon_init (void);
이 기능에서는 스왑 디스크를 설정해야 합니다. 또한 스왑 디스크에서 사용 가능한 영역과 사용 중인 영역을 관리하기 위한 데이터 구조가 필요합니다. 스왑 영역은 PGSIZE(4096바이트) 단위로 관리됩니다.
bool anon_initializer (struct page *page, enum vm_type type, void *kva);
익명 페이지의 이니셜라이저입니다. 스와핑을 지원하려면 익명 페이지에 몇 가지 정보를 추가해야 합니다.
이제 익명 페이지에 대한 스왑을 지원하기 위해 vm/anon.c에 anon_swap_in 및 anon_swap_out을 구현합니다. 페이지를 스왑 인하려면 페이지를 스왑 아웃해야 하므로, anon_swap_in을 구현하기 전에 anon_swap_out을 구현하는 것이 좋습니다. 데이터 내용을 스왑 디스크로 이동한 후 안전하게 메모리로 다시 가져와야 합니다.
static bool anon_swap_in (struct page *page, void *kva);
디스크에서 메모리로 데이터 내용을 읽어 스왑 디스크에서 익명 페이지로 스왑합니다. 데이터의 위치는 페이지가 교체될 때 스왑 디스크에 저장되어 있어야 하는 페이지 구조체입니다. 스왑 테이블을 업데이트하는 것을 잊지 마세요(스왑 테이블 관리 참조).
static bool anon_swap_out (struct page *page);
메모리에서 디스크로 내용을 복사하여 익명 페이지를 스왑 디스크로 교체합니다. 먼저 스왑 테이블을 사용하여 디스크에서 사용 가능한 스왑 슬롯을 찾은 다음 데이터 페이지를 해당 슬롯에 복사합니다. 데이터의 위치는 페이지 구조에 저장되어야 합니다. 디스크에 더 이상 여유 슬롯이 없으면 커널을 패닉시킬 수 있습니다.
File-Mapped Page
파일 백업 페이지의 콘텐츠는 파일에서 가져오기 때문에 매핑된 파일을 백업 저장소로 사용해야 합니다. 즉, 파일 백업 페이지를 내보내면 매핑된 파일에 다시 기록됩니다. vm/file.c에서 file_backed_swap_in, file_backed_swap_out을 구현합니다. 설계에 따라 file_backed_init 및 file_initializer를 수정할 수 있습니다.
static bool file_backed_swap_in (struct page *page, void *kva);
파일에서 내용을 읽어와서 kva에서 페이지를 교체합니다. 파일 시스템과 동기화해야 합니다.
static bool file_backed_swap_out (struct page *page);
파일에 내용을 다시 써서 페이지를 바꿉니다. 먼저 페이지가 더럽지 않은지 확인하는 것이 좋습니다. 페이지가 더럽지 않다면 파일의 내용을 수정할 필요가 없습니다. 페이지를 교체한 후에는 해당 페이지의 더티 비트를 꺼야 한다는 점을 잊지 마세요.
Copy-on-write (Extra)
Implement copy-on-write mechanism in Pintos.
카피 온 쓰기는 물리적 페이지의 동일한 인스턴스를 사용하여 더 빠르게 복제 작업을 수행할 수 있는 리소스 관리 기법입니다. 일부 리소스를 여러 프로세스에서 사용하는 경우 일반적으로 각 프로세스는 충돌이 발생하지 않도록 리소스의 자체 복사본을 보유해야 합니다. 그러나 리소스를 수정하지 않고 읽기만 하는 경우에는 물리적 메모리에 여러 개의 복사본을 보유할 필요가 없습니다.
예를 들어 포크를 통해 새 프로세스가 생성되었다고 가정해 보겠습니다. 자식은 데이터를 가상 주소 공간에 복제하여 부모의 리소스를 상속해야 합니다. 일반적으로 가상 메모리에 콘텐츠를 추가하려면 물리적 페이지를 할당하고, 프레임에 데이터를 쓰고, 페이지 테이블에 가상->물리적 매핑을 추가해야 합니다. 이러한 단계는 시간이 많이 소요될 수 있습니다.
하지만 카피 온 쓰기 기술을 사용하면 리소스의 새 복사본에 대해 새로운 물리적 페이지를 할당하지 않습니다. 이는 기술적으로 콘텐츠가 이미 물리적 메모리에 존재하기 때문입니다. 따라서 자식 프로세스의 페이지 테이블에만 가상->물리적 매핑을 추가하고, 가상 주소는 이제 자식의 메모리 공간에 있습니다. 그러면 부모와 자식은 동일한 물리적 페이지에서 동일한 데이터에 액세스하게 됩니다. 하지만 여전히 별도의 가상 주소 공간을 통해 격리되어 있으며, 동일한 프레임을 참조하고 있다는 것은 OS만 알 수 있습니다. 프로세스 중 하나가 공유 리소스의 콘텐츠를 수정하려고 시도할 때만 새로운 물리적 페이지에 별도의 복사본을 생성합니다. 따라서 실제 복사 작업은 첫 번째 쓰기 작업으로 연기됩니다.
즉, OS는 복사본에 대한 쓰기 페이지에서 쓰기 시도를 감지할 수 있어야 합니다. 이러한 요구를 충족하기 위해 OS는 "쓰기 보호" 메커니즘을 사용합니다. 간단한 아이디어입니다: 쓰기 액세스 시 페이지 오류를 발생시키면 됩니다. 이는 메모리 관리 시스템의 지원으로 쉽게 구현할 수 있으며, 쓰기 방지 페이지를 아예 쓸 수 없는 것으로 표시하기만 하면 됩니다.
포크에 대해서만 복사-온-쓰기를 구현하면 됩니다. 자식 프로세스가 부모 프로세스로부터 리소스를 상속받으면 자식이 리소스를 수정하려고 시도할 때까지 동일한 물리적 데이터를 참조할 수 있습니다. 쓰기 방지된 모든 페이지는 퇴출 후보입니다.
여기에서는 복사본에 대한 기본적인 테스트 사례만 제공합니다. 가능한 모든 경우를 고려해야 합니다(작은 힌트를 드리자면, 파일 백업 페이지의 공유를 구현해야 합니다). 이 추가 프로젝트의 채점은 숨겨진 테스트 케이스도 함께 채점됩니다.
FAQ
Do we need a working Project 2 to implement Project 3?
네.
How do we resume a process after we have handled a page fault?
page_fault()에서 반환하면 현재 사용자 프로세스가 다시 시작됩니다. 그런 다음 인스트럭션 포인터가 가리키는 인스트럭션을 다시 시도합니다. 사용자 프로세스가 스택 포인터 위에서 오류가 발생하는 이유는 무엇인가요? 스택 증가 테스트에서 사용자 프로그램이 사용자 프로그램의 현재 스택 포인터보다 위에 있는 주소에서 오류가 발생하는 것을 확인할 수 있습니다.
Does the virtual memory system need to support data segment growth?
아니요. 데이터 세그먼트의 크기는 링커에 의해 결정됩니다. 핀토스에는 아직 동적 할당 기능이 없습니다(메모리 매핑 파일을 사용하여 사용자 수준에서 '가짜' 할당은 가능하지만). 데이터 세그먼트 증가를 지원한다고 해서 잘 설계된 시스템에 복잡성이 추가되는 것은 거의 없습니다.
Why should I use PAL_USER for allocating page frames?
palloc_get_page()에 PAL_USER를 전달하면 메인 커널 풀 대신 사용자 풀에서 메모리를 할당하게 됩니다. 사용자 풀의 페이지가 부족하면 사용자 프로그램만 페이징되지만 커널 풀의 페이지가 부족하면 많은 커널 함수가 메모리를 가져와야 하므로 많은 오류가 발생합니다. 원하는 경우 palloc_get_page() 위에 다른 얼로케이터를 추가할 수 있지만, 기본 메커니즘으로 사용해야 합니다. 또한 -ul 커널 명령줄 옵션을 사용하여 사용자 풀의 크기를 제한할 수 있으므로 다양한 사용자 메모리 크기로 VM 구현을 쉽게 테스트할 수 있습니다.
'Pintos' 카테고리의 다른 글