Project 3: Virtual Memory
Memory Management
Anonymous Page
Stack Growth
Memory Mapped Files
Swap In/Out
Anonymous Page - Swap In/Out
File-Mapped Page - Swap In/Out
기존 pintos 메모리 문제점
PML4를 가진 기존의 핀토스는 가상메모리와 물리메모리가 바로 맵핑되어 있다.
기존 핀토스 메모리 탑재 과정
이 페이지 테이블에 맵핑된 물리주소는 다른 프로세스와 같은 곳을 가리킬 수 있고 이럴 때 page fault가 된다. 그리고 한번 맵핑되면 물리메모리에서 항상 공간을 차지하기에 효율적인 메모리 관리가 되지 않는다.
목표
Supplemental Page Table
을 추가하고 페이지에 추가적인 정보를 저장하여 효율적인 메모리 관리를 할 수 있게 하는 것이 목표이다.
기존의 페이지 테이블을 보완하고 page fault 발생시 가상페이지를 찾고 물리페이지(frame)을 할 당할 수 있게 처리한다.
프로세스가 종료될 때 Supplemental Page Table 를 참조하여 해제할 리소스를 결정한다.
💡 supplemental page table은 process별로 생성되는 별도의 구조체로써, 동일하게 virtual address와 physical address간 mapping을 지원하지만 struct page와 struct frame 구조체들을 이용하여 기존 page table(pml4 table)이 담지 못하는 정보들을 추가적으로 저장하기 위해 사용됩니다. (ex. evicted page. mmaped page, etc...)
Supplemental Page Table (SPT 구현)
자료 구조 선택
테이블을 구현하기 위해 자료구조를 먼저 선택한다.
추천하는 자료구조 4가지 중에 Hash Table 선택
Hash table관련 자료 https://mangkyu.tistory.com/102
struct supplemental_page_table {
struct hash hash;
};
struct page {
const struct page_operations *operations;
void *va; /* Address in terms of user space */
struct frame *frame; /* Back reference for frame */
/* Your implementation */
struct hash_elem hash_elem;
bool writable;
int mapped_page_cnt;
enum vm_type full_type; // vm_type with markers
/* Per-type data are binded into the union.
* Each function automatically detects the current union */
union {
struct uninit_page uninit;
struct anon_page anon;
struct file_page file;
#ifdef EFILESYS
struct page_cache page_cache;
#endif
};
};
va
키가 되는 가상주소
frame
물리 주소랑 맵핑되는 frame 저장
Union 부분
uninit_page: 제일 처음 만들어진 페이지의 타입
uninit_page 코드
struct uninit_page {
/* Initiate the contets of the page */
vm_initializer *init;
enum vm_type type;
void *aux;
/* Initiate the struct page and maps the pa to the va */
bool (*page_initializer) (struct page *, enum vm_type, void *kva);
};
page fault 시 page_initializer에 저장된 init 함수 호출됨
anon_page: 비 디스크 기반 page
맵핑되는 파일이나 장치가 없어 디스크에 기록되지 않는다.
file_backed_page와 다르게 달리 맵핑된 파일이 없어 익명이라 한다.
file_backed_page: 파일에 기반한 페이지
파일에 기반한 페이지
디스크에서 읽어온다.
구현 코드
spt_find_page : va를 기준으로 hash_table에서 elem을 찾는다.
struct page *
spt_find_page (struct supplemental_page_table *spt UNUSED, void *va UNUSED) {
struct page *page = NULL;
/* TODO: Fill this function. */
page = (struct page *)malloc(sizeof(struct page));
page->va = pg_round_down(va);
//va와 동일한 해시 검색
struct hash_elem *e = hash_find(&spt->hash, &page->hash_elem);
free(page); //사용을 완료한 페이지 메모리 해제하기
if (e == NULL) { // 없을 경우
return NULL;
}
return hash_entry(e, struct page, hash_elem);
}
supplemental_page_table_init: SPT init 함수
void
supplemental_page_table_init (struct supplemental_page_table *spt UNUSED) {
hash_init(&spt->hash, page_hash, page_less, NULL);
}
spt_insert_page
bool
spt_insert_page (struct supplemental_page_table *spt UNUSED, struct page *page UNUSED) {
int succ = false;
/* TODO: Fill this function. */
struct hash_elem *e = hash_insert(&spt->hash, &page->hash_elem);
if(e == NULL) { //성공했을 경우
succ = true;
}
return succ;
}
Anonymous Page
Lazy Loading(Demanding Paging)
Lazy loading은 메모리 로딩이 필요한 시점까지 지연되는 디자인
가상 page만 할당해두고 필요한 page를 요청하면 page fault가 발생하고 해당 page를 type에 맞게 초기화하고 frame과 연결하고 user-program 으로 제어권을 넘긴다.
지연로딩 순서
vm_alloc_page_with_initializer
호출uninit_initialize
를 호출하고 이전에 설정한 initializer를 호출한다.lazy_load_segment
를 호출하여 필요한 데이터를 물리메모리에 올린다.생명주기
initialize
-> (page_fault -> lazy-load -> swap-in -> swap-out-> ...) -> destroy
- Uninitialized Page 구현
vm_alloc_page_with_initializer
인자로 주어진 type으로 초기화되지 않은 page를 만든다.
SPT에 새로 만든 page를 추가하여 준다.
제일 처음 만들어지는 페이지는 uninit page이다.
bool
vm_alloc_page_with_initializer (enum vm_type type, void *upage, bool writable,
vm_initializer *init, void *aux) {
ASSERT (VM_TYPE(type) != VM_UNINIT)
struct supplemental_page_table *spt = &thread_current ()->spt;
/* Check wheter the upage is already occupied or not. */
if (spt_find_page (spt, upage) == NULL) {
/* TODO: Create the page, fetch the initialier according to the VM type,
* TODO: and then create "uninit" page struct by calling uninit_new. You
* TODO: should modify the field after calling the uninit_new. */
struct page * page = (struct page*)malloc(sizeof(struct page)) ;
bool (*initializer)(struct page *, enum vm_type, void *) ;
switch (VM_TYPE(type)) { //타입에 맞는 초기화 함수 지정
case VM_ANON :
initializer = anon_initializer;
break;
case VM_FILE :
initializer = file_backed_initializer;
break;
default :
NOT_REACHED();
break;
}
uninit_new(page, upage, init, type, aux, initializer);
page->writable = writable;
page->full_type = type;
/* TODO: Insert the page into the spt. */
return spt_insert_page(spt, page);
}
err:
return false;
}
- Anonymous Page 구현
ANON Page 타입에 대한 함수를 수정
Anonymous Page에서 중요한 점은 물리메모리와 맵핑될 때 초기화할 때 모든 데이터를 Zeroing 해주어야 한다.
anon_initializer
기존 page안에 있는 union 안의 uninit_page에 대한 데이터를 모두 0으로 만들어준다.
operations을 anon_ops로 변경하고 anon_page에 대한 정보를 바꿔준다.
bool anon_initializer(struct page *page, enum vm_type type, void *kva)
{
/* page struct 안의 Union 영역은 현재 uninit page이다.
ANON page를 초기화해주기 위해 해당 데이터를 모두 0으로 초기화해준다.
Q. 이렇게 하면 Union 영역은 모두 다 0으로 초기화되나? -> 맞다. */
struct uninit_page *uninit = &page->uninit;
memset(uninit, 0, sizeof(struct uninit_page));
/* Set up the handler */
/* 이제 해당 페이지는 ANON이므로 operations도 anon으로 지정한다. */
page->operations = &anon_ops;
struct anon_page *anon_page = &page->anon;
anon_page->swap_index = -1;
return true;
}
이 부분을 구현할때 생각이 잘 안떠오르는 부분 이였다.
union에 대한 값 초기화와 anon_page에 대한 값을 생각 하기 힘들었다.
- load_segment 구현 (수정)
Project 1,2 까지의 pintos는 디스크에서 실행시킬 파일 전체를 물리메모리에 올리고 페이지 테이블에 맵핑해주었다.
이제는 load_segment
를 수정하여 page를 만들고 file에 대한 정보만을 initializer로 넘겨준다. 디스크에 있는 데이터를 물리메모리에 바로 올리지 않는다.
load_segment
vm_entry 구조체를 추가하여 file을 load 할때 필요한 file, offset, read_bytes를 저장하고 initializer를 호출하고 aux 인자로 넘겨준다.
파일을 page 단위로 끊어서 uninit 페이지로 만들고 file 정보를 page에 저장하고 SPT에 추가한다.
static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable) {
ASSERT ((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT (pg_ofs (upage) == 0);
ASSERT (ofs % PGSIZE == 0);
while (read_bytes > 0 || zero_bytes > 0) {
/* Do calculate how to fill this page.
* We will read PAGE_READ_BYTES bytes from FILE
* and zero the final PAGE_ZERO_BYTES bytes. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* TODO: Set up aux to pass information to the lazy_load_segment. */
// void *aux = NULL;
struct vm_entry *vme = (struct vm_entry *)malloc(sizeof(struct vm_entry));
vme->f = file;
vme->offset = ofs;
vme->read_bytes = page_read_bytes;
vme->zero_bytes = page_zero_bytes;
//aux 대신 vme를 넘겨준다.
if (!vm_alloc_page_with_initializer(VM_ANON, upage, writable, lazy_load_segment, vme)) {
return false;
}
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
ofs += page_read_bytes;
}
return true;
}
Q. anon 페이지로 모든 page를 만들고 있는데 file에서 올리는 거면 왜 anon 페이지로 만들어 주지?
lazy_load_segmen() 구현
프로세스가 uninit_page로 처음 접근하여 page_fault가 발생하면 해당 함수가 호출된다.
호출된 page를 frame과 맵핑하고 해당 page에 연결된 물리메모리에 file 정보를 load 해준다.
bool
lazy_load_segment (struct page *page, void *aux) {
/* TODO: Load the segment from the file */
/* TODO: This called when the first page fault occurs on address VA. */
/* TODO: VA is available when calling this function. */
struct vm_entry *vme = (struct vm_entry *)aux;
file_seek(vme->f, vme->offset);
if(file_read(vme->f, page->frame->kva, vme->read_bytes) != (int)(vme->read_bytes)) {
palloc_free_page(page->frame->kva);
return false;
}
memset(page->frame->kva + vme->read_bytes, 0, vme->zero_bytes);
return true;
}
setip_stack() 재구현
기존의 setup_stack()를 SPT가 추가된 상황에 맞게 수정한다.
setup_stack은 page를 할당하고 바로 물리 메모리와 맵핑하여 준다. -> page_fault가 발생전에 물리메모리에 바로 할당
stack_bottom을 page의 va(가상주소 시작위치)로 할당하기 위해 인자로 넘겨준다.
stack_bottom을 추가 할당을 위하여 thread_current에 stack_bottom 저장 변수를 만들고 넣어준다.
bool
setup_stack (struct intr_frame *if_) {
bool success = false;
void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);
/* TODO: Map the stack on stack_bottom and claim the page immediately.
* TODO: If success, set the rsp accordingly.
* TODO: You should mark the page is stack. */
/* TODO: Your code goes here */
/* Map the stack on stack_bottom and claim the page immediately.
You should mark the page is stack. */
if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1)){
// 스택 페이지 할당에 성공한 후, 해당 가상주소로 할당한 페이지를 찾아서 claim
success = vm_claim_page (stack_bottom) ;
if (success){
/* TODO: If success, set the rsp accordingly. */
if_->rsp = USER_STACK;
thread_current()->stack_bottom = stack_bottom;
}
}
return success;
}
Q. 왜 스택은 lazy loading을 하지 않고 바로 물리메모리와 맵핑해 주는 것인가?
setup_stack 이후 파일 실행에 필요한 argument를 바로 stack에 넣어 주어야한다.
stack은 바로 사용하기 때문에 uninit page로 두지 않고 생성 후 바로 frame과 맵핑시켜준다.
/* Set up stack. */
if (!setup_stack (if_))
goto done;
/* Start address. */
if_->rip = ehdr.e_entry;
/* TODO: Your code goes here.
* TODO: Implement argument passing (see project2/argument_passing.html). */
argument_stack(argv, cnt, &if_->rsp);
check_address() 수정
수정 전
void check_address(void *addr)
{
struct thread *cur = thread_current();
/* 주소 addr이 유저 가상 주소가 아니거나 pml4에 없으면 */
if (addr == NULL || !is_user_vaddr(addr) || pml4_get_page(cur->pml4, addr) == NULL)
{
exit(-1);
}
/* addr이 유저 가상 주소이고 동시에 pml4에 있으면 페이지를 리턴해야 한다. */
}
수정 후
void check_address(void *addr)
{
/* 주소 addr이 유저 가상 주소가 아니거나 pml4에 없으면 프로세스 종료 */
if (addr == NULL || !is_user_vaddr(addr))
{
exit(-1);
}
/* 유저 가상 주소면 SPT에서 페이지 찾아서 리턴 */
return spt_find_page(&thread_current()->spt, addr);
}
check_buffer
Supplemental Page Table - Revisit(fork, exec)
SPT 테이블에 대한 copy와 clean up을 위한 수정이 필요함
child를 만들때 copy, process를 종료할 때 destroy가 필요
supplemental_page_table_copy
구현 요구사항
구현 코드
bool
supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src) {
struct hash_iterator i; //해시 테이블 내의 위치
hash_first(&i, &src->hash); //i를 해시의 첫번째 요소를 가리키도록 초기화
while (hash_next(&i)) { // 해시의 다음 요소가 있을 때까지 반복
struct page *src_page = hash_entry(hash_cur(&i), struct page, hash_elem);
enum vm_type type = src_page->operations->type;
void *va = src_page->va;
bool writable = src_page->writable;
if(type == VM_UNINIT) { //초기화되지 않은 페이지인 경우
vm_alloc_page_with_initializer(page_get_type(src_page), va, writable, src_page->uninit.init, src_page->uninit.aux);
}
else if(type == VM_FILE) { //파일 타입일 경우
struct vm_entry *vme = (struct vm_entry *)malloc(sizeof(struct vm_entry));
vme->f = src_page->file.file;
vme->offset = src_page->file.offset;
vme->read_bytes = src_page->file.read_bytes;
vme->zero_bytes = src_page->file.zero_bytes;
if(!vm_alloc_page_with_initializer(type, va,writable, NULL, vme)) {
return false;
}
struct page *page = spt_find_page(dst, va);
file_backed_initializer(page, type, NULL);
page->frame = src_page->frame;
pml4_set_page(thread_current()->pml4, page->va, src_page->frame->kva, src_page->writable);
}
else { //익명 페이지일 경우
if(!vm_alloc_page(type, va, writable)) {
return false;
}
if(!vm_claim_page(va)) {
return false;
}
struct page *dst_page = spt_find_page(dst, va);
memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE);
}
}
return true;
}
supplemental_page_kill
SPT
에 모든 리소를 해제한다.destroy(page)
로 리소스 해제 uninit_destroy
anon_destroy
목표
이제까지의 pintos의 stack 단일 페이지 stack으로 되어있었다.
이러한 스택을 적절한 page fault에서 stack을 추가로 할당하는 작업을 구현하는 것이 목표이다.
Q. 적절한 stack page fault 범위 잡는 범위를 rsp에서 4KB를 설정하였는데 왜 4KB인지 잘 모르겠음
vm_try_handle_fault 수정
addr의 범위가 MAX_STACK 보단 크고 USER_STACK 보단 작을때와 USER_STACK 과 addr이 f->rsp-8 보다 크거나 같다.
위의 범위가 stack growth에 대한 페이지 폴트 일때의 범위이다.
bool
vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
struct supplemental_page_table *spt UNUSED = &thread_current ()->spt;
void *page_addr = pg_round_down(addr); // 페이지 사이즈로 내려서 spt_find 해야 하기 때문
uint64_t MAX_STACK = USER_STACK - (1<<20);
uint64_t addr_v = (uint64_t)addr;
uint64_t rsp = user ? f->rsp : thread_current()->rsp;
if (addr == NULL || is_kernel_vaddr(addr))
return false;
if (!not_present && write)
return false;
/* TODO: Validate the fault */
/* TODO: Your code goes here */
struct page *page = spt_find_page(spt, page_addr);
if (page == NULL) {
if (addr_v > MAX_STACK && addr_v < USER_STACK && addr_v >= rsp - 8) {
vm_stack_growth(page_addr);
page = spt_find_page(spt, page_addr);
} else {
return false ;
}
}
return vm_do_claim_page (page);
}
vm_stack_growth
stack_bottom을 4KB(PGSIZE)만큼 이동한다.
VM_ANON으로 page를 할당하고 vm_claim_page를 호출하여 frame과 맵핑시킨다.
thread_current의 stack_bottom을 갱신한다.
static void
vm_stack_growth (void *addr UNUSED) {
struct supplemental_page_table *spt = &thread_current ()->spt;
while (!spt_find_page (spt, addr)) {
vm_alloc_page (VM_ANON | VM_MARKER_0, addr, true);
vm_claim_page (addr);
addr += PGSIZE;
}
}
목표
이번에는 Anonymous Memory가 아닌 File-Backed Memory, 다시 말해 Memory Mapped Page에 대해 구현해보도록 한다.
File-Backed page
File-Backed page는 파일에 기반한 맵핑을 한다. 안에 내용은 디스크에서 존재하고 있는 파일을 복사한것이다. page fault가 발생하면 바로 물리 프레임을 할당하고 파일의 데이터를 물리 메모리에 복사한다. 이때 I/O를 통해 데이터를 복사하는 것이 아닌 DMA 방식으로 디스크에서 파일을 복사한다.
이 때 유저 가상 페이지를 미리 가상주소 공간에 할당 해주는 것이 mmap이고 페이지와 물리 메모리가 연결된 경우 그 연결을 끊어 주는 것을 mnumap이라 한다.
mmap() 시스템 콜에 의해 맵핑된 가상 메모리는 스택과 힙 사이의 미할당 공간(스택고 힙도 아닌)에 할당된다.
memory mapping page 구현 시에도 lazy loading 방식으로 할당 되어야 한다.
vm_alloc_page로 페이지를 할당하고 해당 페이지에 page fault가 발생하였을 때 frame을 맵핑하고 해당 page에 해당하는 file 데이터를 메인 메모리에 올려준다.
mmap 구현
void *mmap(void *addr, size_t lengthm int writable, int fd, off_t offset);
만약 파일의 size가 PGSIZE보다 크면 마지막 페이지에는 남은 공간 만큼의 빈공간이 생긴다. 이 비어 있는 메모리를 0으로 초기화해주고 다시 디스크에 업데이트할 때는 버린다.
Syscall Mmap
이 함수에서는 다양한 예외 처리를 해준다. 유저가 입력한 값이 정상적인지 확인한다.
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) {
if(!addr || addr != pg_round_down(addr)) { //addr이 존재하지 않거나 정렬되어 있지 않은 경우
return NULL;
}
if (offset != pg_round_down(offset)) { //offset이 정렬되어 있지 않은 경우
return NULL;
}
if (!is_user_vaddr(addr) || !is_user_vaddr(addr + length)) { //사용자 영역에 존재하지 않을 경우
return NULL;
}
if (spt_find_page(&thread_current()->spt, addr)) { //addr에 할당된 페이지가 존재할 경우
return NULL;
}
struct file *f = process_get_file(fd); //fd에 파일이 없을 경우
if (f == NULL) {
return NULL;
}
if (file_length(f) == 0 || (int)length <= 0) { //길이가 0이하일 경우
return NULL;
}
lock_acquire(&filesys_lock);
void *success = do_mmap(addr, length, writable, f, offset);
lock_release(&filesys_lock);
return success;
}
do_mmap
do_mmap은 load_segment와 매우 유사한 면이 있음.
page를 할당할 때 VM_FILE로 하여 할당
이렇게 만들어진 page는 page fault가 발생하면 페이지를 초기화 하고 frame과 연결하고 file_page에 저장된 파일의 정보로 물리메모리에 file 데이터를 복사한다.
void *
do_mmap (void *addr, size_t length, int writable, struct file *file, off_t offset) {
struct file *f = file_reopen(file);
void *start_addr = addr;
int total_page_count;
if(length <= PGSIZE) { //현재 매핑하려는 길이가 페이지 사이즈보다 작을 경우
total_page_count = 1; //한 개의 페이지에 모두 들어간다.
}
else { //하나 이상의 페이지가 필요할 경우
if(length % PGSIZE != 0) { //나누어떨어지지 않을 경우 페이지 1개 더 필요하다.
total_page_count = length / PGSIZE + 1;
}
else {
total_page_count = length / PGSIZE;
}
}
size_t read_bytes = file_length(f) < length ? file_length(f) : length;
size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;
ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT(pg_ofs(addr) == 0);
ASSERT(offset % PGSIZE == 0);
while(read_bytes > 0 || zero_bytes > 0) {
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
struct file_page *file_page = (struct file_page *)malloc(sizeof(struct file_page));
file_page->file = f;
file_page->offset = offset;
file_page->read_bytes = page_read_bytes;
file_page->zero_bytes = page_zero_bytes;
if(!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_seg, file_page)) {
file_close(f);
return NULL;
}
struct page *p = spt_find_page(&thread_current()->spt, start_addr);
p->mapped_page_cnt = total_page_count;
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
addr += PGSIZE;
offset += page_read_bytes;
}
return start_addr;
}
lazy_load_Seg
file_backed_page에서 page fault 발생 시 맵핑된 물리 메모리에 파일 데이터를 복사하는 함수
file_page의 offset을 aux로 받아온 offset으로 옮기고 물리 메모리에 파일에서 read_bytes만큼 데이터를 복사한다.
PGSIZE가 되지 않는 read_bytes인 경우 남은 부분은 0으로 채워준다. memset()
bool lazy_load_seg(struct page *page, void *aux)
{
struct file_page *file_page = (struct file_page *)aux;
page->file = (struct file_page) {
.file = file_page->file,
.offset = file_page->offset,
.read_bytes = file_page->read_bytes,
.zero_bytes = file_page->zero_bytes};
file_seek(file_page->file, file_page->offset);
if (file_read(file_page->file, page->frame->kva, file_page->read_bytes) != (int)(file_page->read_bytes))
{
palloc_free_page(page->frame->kva);
return false;
}
// 3) 다 읽은 지점부터 zero_bytes만큼 0으로 채운다.
memset(page->frame->kva + file_page->read_bytes, 0, file_page->zero_bytes);
return true;
}
munmap(addr)
Dirty Bit
해당 페이지가 수정되었는지를 저장하는 비트이다. 페이지가 변경될 때마다 이 비트는 1이된다.
디스크에 변경 내용을 기록하고 나면 해당 페이지의 더티 비트는 다시 0으로 초기화 된다.
💡 ADDR으로부터 연속된 유저 가상 페이지들의 변경 사항을 디스크의 파일에 업데이트하고 맵핑 정보를 지운다. 페이지를 지우는 것이 아닌 Present Bit을 0으로 만들어준다.
해당 page의 dirty bit가 1이면 수정사항이 있는 것임으로 file에 물리메모리에 있는 데이터를 write하여 준다. 그리고 해당 페이지의 dirty bit을 0으로 만들어준다.
void
do_munmap (void *addr) {
struct supplemental_page_table *spt = &thread_current()->spt;
struct page *p = spt_find_page(spt, addr);
if (p->frame == NULL) {
vm_claim_page(addr);
}
struct file *file = p->file.file;
int count = p->mapped_page_cnt;
for (int i = 0; i < count; i++) {
if(p) {
destroy(p);
}
addr += PGSIZE;
p = spt_find_page(spt, addr);
}
file_close(file);
}
Anonymous Page
ANON의 경우에는 디스크 안에 해당 페이지에 대한 BACKING STORE가 없다. 이 말은, 디스크로 SWAP OUT 되었을 때 이 페이지를 저장할 공간이 디스크 내에 없다는 뜻이다. 따라서 디스크 내에 별도의 공간 SWAP DISK를 만들어 두고, 이 공간에 SWAP OUT된 ANON 페이지를 저장하도록 한다.
vm_anon_init
과 anon-initializer
를 먼저 수정한다.
struct anon_page 리마인드
만약 ANON 페이지가 디스크로 SWAP OUT 되었을 때, 디스크에 저장된 디스크 섹터 구역을 저장해주는 slot
변수가 존재한다.
맨 처음 ANON 페이지를 초기화하면(UNINIT으로부터) 물리 메모리 위에 있으므로 slot
의 값은 -1로 지정해 둔다.
struct anon_page {
uint32_t slot;
};
Swap Table 선언
디스크에서 사용 가능한 swap slot과 사용 불가능한 swap slot을 관리하는 자료구조인 Swap Table을 선언해준다.
스왑 테이블은 가상 메모리에 있는 객체이다.
struct list swap_table; //swap slot 사용 여부를 판단하기 위해 사용
disk_sector_t swap_size = disk_size(swap_disk) / 8;
여기 스왑 테이블의 경우 각각의 비트는 스왑 슬롯 각각과 매칭된다. 스왑 테이블에서 해당 스왑 슬롯에 해당하는 비트가 1이라는 말은 그에 대응되는 페이지가 swap out되어 디스크의 스왑 공간에 임시적으로 저장되었다는 뜻이다.
디스크 섹터는 하드 드라이브의 최소 기억 단위라고 한다. 마치 가상 메모리 공간을 여러 페이지로 나누어 놓은 것처럼, 디스크 위의 물리적인 저장 공간이라는 것이 중요하다. HDD의 경우에는 512 byte의 고정된 크기를 갖는다. 만약 디스크에 파일이 저장된다고 하면, 파일은 여러 디스크 섹터들을 거쳐서 저장된다. 파일의 크기가 2kb라고 하면 4개의 디스크 섹터에 파일이 나뉘어 저장되는 것이다.
가상 페이지의 크기는 4kb이므로, 하나의 페이지를 저장하기 위해 필요한 디스크 섹터의 개수는 8개가 되겠다.
vm_anon_init() 수정
ANON 페이지를 위한 디스크 내 스왑 영역을 생성해주는 함수이다. 디스크 내에 스왑 영역을 만들어주고 이를 관리하는 스왑 테이블도 만들어준다.
swap_size는 스왑 디스크 안에서 만들 수 있는 스왑 슬롯의 개수를 의미한다.
💡 스왑 디스크 공간 내의 총 스왑 슬롯 개수 = 스왑 공간의 크기 / 1페이지당 필요한 섹터의 개수
이다.
만약 스왑 공간이 4096byte라고 한다면 4096 / 8 = 512개의 페이지에 대한 정보를 저장할 수 있는 512개의 스왑 슬롯을 만들 수 있다.
void
vm_anon_init (void) {
/* TODO: Set up the swap_disk. */
swap_disk = disk_get(1, 1);
list_init(&swap_table);
lock_init(&swap_table_lock);
disk_sector_t swap_size = disk_size(swap_disk) / 8;
for (disk_sector_t i = 0; i < swap_size; i++) {
struct slot *slot = (struct slot *)malloc(sizeof(struct slot));
slot->page = NULL;
slot->slot= i;
lock_acquire(&swap_table_lock);
list_push_back(&swap_table, &slot->swap_elem);
lock_release(&swap_table_lock);
}
}
anon_initializer() 수정
slot의 값을 -1로 설정해준다. 페이지 폴트가 떠서 해당 페이지를 물리 메모리로 올리게 되므로, 해당 페이지는 디스크가 아닌 물리 메모리에 있기 때문!
bool
anon_initializer (struct page *page, enum vm_type type, void *kva) {
/* Set up the handler */
page->operations = &anon_ops;
struct anon_page *anon_page = &page->anon;
anon_page->slot = -1;
return true;
}
anon_swap_out(page) 구현
💡 ANON 페이지를 스왑 아웃한다. 디스크에 백업 파일이 없으므로, 디스크 상에 스왑 공간을 만들어 그곳에 페이지를 저장한다.
현재 디스크 섹터와 그에 해당하는 페이지 및 스왑 슬롯은 다음과 같다.
disk sector : 1 2 3 4 5 6 7 8 910111213141516 1718192021222324
each page : | 1 2 3 4 5 6 7 8 | 1 2 3 4 5 6 7 8 | 1 2 3 4 5 6 7 8 |
page_no & swap slot : 1 2 3
가상 메모리의 스왑 테이블에서 비트가 false인 스왑 슬롯을 찾는다. 비트가 false라는 말은 해당 스왑 슬롯에 swap out된 페이지를 할당할 수 있다는 의미이다.
그 후 해당 스왑 슬롯에 해당하는 디스크의 영역에 가상 주소 공간의 데이터를 페이지의 시작 주소부터 디스크 섹터 크기로 잘라서 저장한다.
그 후 해당 스왑 슬롯에 대응되는 스왑 테이블의 비트를 TRUE로 바꿔주고, ANON 페이지의 slot 멤버에 스왑 슬롯 번호를 저장해서 이 페이지가 디스크의 스왑 영역 중 어디에 swap 되었는지를 확인할 수 있도록 한다.
SWAP OUT된 페이지의 Present Bit의 값은 0으로, 프로세스가 해당 페이지의 데이터에 접근하려 하면 페이지 폴트가 뜨게 된다.
static bool
anon_swap_out (struct page *page) {
if(page == NULL) {
return false;
}
struct anon_page *anon_page = &page->anon;
struct slot *slot;
lock_acquire(&swap_table_lock);
for (struct list_elem *e = list_begin(&swap_table); e != list_end(&swap_table); e = list_next(e)) {
slot = list_entry(e, struct slot, swap_elem);
if(slot->page == NULL) {
for (int i = 0; i < 8; i++) {
disk_write(swap_disk, slot->slot * 8 + i, page->va + DISK_SECTOR_SIZE * i);
}
slot->page = page;
anon_page->slot = slot->slot;
page->frame->page = NULL;
page->frame = NULL;
pml4_clear_page(thread_current()->pml4, page->va);
lock_release(&swap_table_lock);
return true;
}
}
lock_release(&swap_table_lock);
PANIC("insufficient swap space");
}
anon_swap_in() 구현
SWAP OUT된 페이지에 저장된 slot 값으로 스왑 슬롯을 찾아 해당 슬롯에 저장된 데이터를 다시 페이지로 원복시킨다.
static bool
anon_swap_in (struct page *page, void *kva) {
struct anon_page *anon_page = &page->anon;
disk_sector_t page_slot = anon_page->slot;
struct slot *slot;
lock_acquire(&swap_table_lock);
for (struct list_elem *e = list_begin(&swap_table); e != list_end(&swap_table); e = list_next(e)) {
slot = list_entry(e, struct slot, swap_elem);
if(slot->slot == page_slot) {
for (int i = 0; i < 8; i++) {
disk_read(swap_disk, page_slot * 8 + i, kva + DISK_SECTOR_SIZE * i);
}
slot->page = NULL;
anon_page->slot = -1;
lock_release(&swap_table_lock);
return true;
}
}
lock_release(&swap_table_lock);
return false;
}
File-Backed Page
Fil-Backed Page의 경우 디스크에 backed file이 있으므로, swap out될 때 해당 파일에 저장되면 된다.
Swap out되면 해당 페이지의 PTE의 Present Bit은 0이 되고, 해당 페이지에 프로세스가 접근하면 페이지 폴트가 일어나고 디스크의 파일 데이터를 다시 물리 메모리에 올리면서 swap in된다.
file_backed_swap_out() 수정
do_munmap()과 거의 유사하다는 것을 알 수 있다. 단지 do_munmap()은 연속된 가상 메모리 공간에 매핑된 페이지들을 모두 swap out해주는 것이고, file_backed_swap_out()은 한 페이지에 대해 매핑을 해제해준다는 것이 다르다.
static bool
file_backed_swap_out (struct page *page) {
struct file_page *file_page UNUSED = &page->file;
struct thread *t = thread_current();
if(page == NULL)
return false;
if(pml4_is_dirty(t->pml4, page->va)) { //dirty bit = 1일 경우 swap out 가능
lock_acquire(&filesys_lock);
file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->offset); //변경사항을 파일에 저장하기
lock_release(&filesys_lock);
pml4_set_dirty(thread_current()->pml4, page->va, 0); //dirty bit = 0
}
page->frame->page = NULL;
page->frame = NULL;
pml4_clear_page(t->pml4, page->va);
return true;
}
file_backed_swap_in() 수정
lazy_load_segment()와 비슷하다는 것을 알 수 있다.
Present Bit이 0으로 세팅되어 있는 File-Backed Page에 프로세스가 접근하게 되면 Supplementary Page Table의 struct page 내에 있는 struct file_page에 접근한다. file_page 구조체의 container 구조체에서 이 가상 페이지와 관련된 디스크의 파일 데이터의 정보를 불러올 수 있다.
이 정보를 토대로 먼저 물리 공간에 프레임을 하나 할당받아 해당 페이지와 매핑해준 다음, 디스크의 파일에서 물리 프레임으로 (vm_do_claim_page()
내 swap_in(page, frame->kva))
데이터를 다시 복사해온다.
static bool
file_backed_swap_in (struct page *page, void *kva) {
struct file_page *file_page UNUSED = &page->file;
size_t page_read_bytes = file_page->read_bytes;
size_t page_zero_bytes = file_page->zero_bytes;
if (page == NULL)
return false;
// 파일의 내용을 페이지에 입력한다
lock_acquire(&filesys_lock);
if(file_read_at(file_page->file, kva, page_read_bytes, file_page->offset) != (int)page_read_bytes){
// 제대로 입력이 안되면 false 반환
lock_release(&filesys_lock);
return false;
}
lock_release(&filesys_lock);
// 나머지 부분을 0으로 입력
memset(kva + page_read_bytes, 0, page_zero_bytes);
return true;
}
Page Replacement Policy
프로세스가 가상 페이지를 통해 물리 메모리에 접근하려 시도할 때, 물리 프레임이 모두 꽉 차 있으면 물리 메모리에서 적당한 하나의 프레임을 골라 디스크로 Swap Out 한다. 이 때 swap out할 적당한 프레임을 고르는 정책을 Page Replacement Policy라고 부른다.
frame_table list 구현 및 수정
앞에서 선언해주었던 struct frame과 frame_table 리스트를 다시 한번 가져와보자. 이 둘의 존재 이유는 물리 프레임들을 관리해주면서 swap out시킬 적당한 프레임을 고르기 편하게 해 주는 데에 있다.
struct frame {
void *kva;
struct page *page;
struct list_elem frame_elem;
};
frame들을 관리하는 frame table 리스트와 해당 리스트에 lock을 걸어주기 위한 frame_table_lock 이라는 변수를 선언해준다.
struct list frame_table; //lru 페이지 교체 기법으로 희생자 선택
struct lock frame_table_lock;
맨 처음 가상 메모리에 대한 서브 시스템들을 초기화해주는 vm_init() 함수에서 프레임 테이블과 frame_table_lock 변수도 초기화해준다.
void
vm_init (void) {
vm_anon_init ();
vm_file_init ();
#ifdef EFILESYS /* For project 4 */
pagecache_init ();
#endif
register_inspect_intr ();
/* DO NOT MODIFY UPPER LINES. */
/* TODO: Your code goes here. */
list_init(&frame_table);
lock_init(&frame_table_lock);
}
vm_get_frame() 수정
이제 페이지를 claim할 때 페이지에 해당하는 물리 프레임을 가져오는 vm_get_frame()을 수정한다.
페이지를 물리 메모리 공간에 palloc하여 할당받는 과정에서 아무 것도 받아오지 못했다면, 물리 프레임에 자리가 없어 가져오지 못한 것으로 판단한다. 그렇담 vm_evict_frame()을 호출하여 프레임 하나의 데이터를 swap out해버리고 해당 프레임과 새 페이지를 매핑해줄 것이다.
이 때 프레임을 할당받은 다음에 물리 프레임을 관리하는 struct frame을 페이지 테이블에 insert시켜 관리한다.
static struct frame *
vm_get_frame (void) {
struct frame *frame = NULL;
/* TODO: Fill this function. */
//사용자 풀에서 페이지를 할당받기 - 할당받은 물리 메모리 주소 반환
void *addr = palloc_get_page(PAL_USER);
if(addr == NULL) {
frame = vm_evict_frame();
frame->page = NULL;
return frame;
}
frame = (struct frame *)malloc(sizeof(struct frame));
frame->kva = addr;
frame->page = NULL;
lock_acquire(&frame_table_lock);
list_push_back(&frame_table, &frame->frame_elem);
lock_release(&frame_table_lock);
ASSERT(frame != NULL);
ASSERT (frame->page == NULL);
return frame;
}
vm_evict_frame() 구현
이 함수는 vm_get_victim() 함수를 호출하여 프레임 테이블에서 적당한 프레임을 찾아낸 다음, 해당 프레임을 swap out 해주는 함수이다.
static struct frame *
vm_evict_frame (void) {
struct frame *victim UNUSED = vm_get_victim ();
/* TODO: swap out the victim and return the evicted frame. */
if(victim->page) {
swap_out(victim->page);
}
return victim;
}
vm_get_victim() 구현
이 함수는 페이지 교체 정책을 실제로 구현하는 함수이다.
static struct frame *
vm_get_victim (void) {
struct frame *victim = NULL;
/* TODO: The policy for eviction is up to you. */
struct thread *curr = thread_current();
lock_acquire(&frame_table_lock);
for (struct list_elem *f = list_begin(&frame_table); f != list_end(&frame_table); f = list_next(f)) {
victim = list_entry(f, struct frame, frame_elem);
if(victim->page == NULL) { //현재 프레임에 페이지가 없으므로 희생자로 선택
lock_release(&frame_table_lock);
return victim;
}
//PTE에 접근했는지 여부 판단 : 즉 최근에 접급한 적이 있으면
if (pml4_is_accessed(curr->pml4, victim->page->va)) {
//접근 비트를 0으로 설정
pml4_set_accessed(curr->pml4, victim->page->va, 0);
}
else { //최근에 접근한 적이 없으면 희생자로 선택
lock_release(&frame_table_lock);
return victim;
}
}
lock_release(&frame_table_lock);
return victim;
}