-
PintOS Project3 - Virtual Memory 2편정글 크래프톤 5기 회고 및 정리/PintOS 2024. 6. 4. 19:33
3. Stack Growth
구현1
bool vm_try_handle_fault (struct intr_frame *f, void *addr, bool user, bool write, bool not_present);- 이 함수는 userprog/excpetion.c 의 page_fault() 에서 페이지 폴트 예외를 처리하는 동안 호출된다.
- 페이지 폴트가 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) { ... if (not_present) { void *rsp = f->rsp; if (!user) rsp = thread_current()->rsp_stack; if ((USER_STACK - (1 << 20) <= rsp - 8 && rsp - 8 == addr && addr <= USER_STACK) || (USER_STACK - (1 << 20) <= rsp && rsp <= addr && addr <= USER_STACK)) vm_stack_growth(addr); ... } return false; }void *rsp = f->rsp; - user access의 경우 rsp는 유저 stack을 가리킨다. if (!user) rsp = thread_current()->rsp; - kernel access인 경우 thread에서 rsp를 가져와야 한다. vm_stack_growth(addr); - 스택 확장으로 처리할 수 있는 폴트인 경우, vm_stack_growth를 호출한다.
구현2
void vm_stack_growth (void *addr);- 대부분의 운영체제는 스택 크기에 절대적인 제한을 둔다. ( 이 프로젝트에서는 스택 크기를 최대 1MB로 제한해야 한다. )
- addr에 하나 이상의 이명 페이지를 할당하여 스택 크기를 늘리는 함수
static void vm_stack_growth (void *addr UNUSED) { vm_alloc_page(VM_ANON | VM_MARKER_0, pg_round_down(addr), 1); }설명 - 스택 크기를 증가시키기 위해 anon page를 하나 할당하여 주어진 주소(addr)를 유효주소로 해준다. pg_round_down - 페이지의 크기만큼 stack을 늘려줘야하기 때문에 주소(addr)와 제일 가까운 해시의 시작점을 찾아준다.
- PGSIZE 만큼 늘리기 위해 사용!* pt-grow-stk-sc 테스트가 fail 뜬다. 밑의 트러블 슈팅에서 좀 더 자세히 보겠다.
4. Memory Mapped Files
- 익명 페이지와 달리, 메모리 매핑된 페이지는 파일 기반 매핑이다.
- 페이지의 내용은 기존 파일의 데이터를 반영한다.
- 페이지 폴트가 발생하면, 물리적 프레임이 즉시 할당되고 파일에서 메모리로 내용이 복사된다.
- 메모리 매핑된 페이지가 언매핑되거나 스왑 아웃될 때, 내용의 변경 사항이 파일에 반영된다.
구현1 (Complete)
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);- fd로 열린 파일을 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑한다.
- 파일 전체가 addr에서 시작하여 연속적인 가상 페이지에 매핑된다.
- 만약 파일 길이가 페이지 크기(PGSIZE)의 배수가 아니라면, 마지막으로 매핑된 페이지의 일부 바이트가 파일의 끝을 넘어 "튀어" 나온다.
· 이 바이트들은 페이지가 페이징될 때 0으로 설정하고
· 페이지가 디스크에 쓰여질 때는 버린다.
- 이 함수가 성공하면 파일이 매핑된 가상 주소를 반환한다.
- 실패하면 파일을 매핑할 유효한 주소가 아닌 NULL을 반환해야 한다.- mmap 호출은 fd로 열린 파일의 길이가 0 바이트 경우 실패할 수 있다.
- addr이 페이지 정렬되어 있지 않았거나 매핑된 페이지 범위가 기존에 매핑된 페이지 집합(스택 또는 실행 파일 로드 시 매핑된 페이지 포함)과 겹치는 경우는 반드시 실패해야 한다.
- Linux에서 addr이 NULL인 경우 커널이 매핑을 생성할 적절한 주소를 찾는다.
- 간단히 하기 위해, 주어진 addr에서 mmap을 시도할 수 있다.
- 따라서, addr이 0인 경우 반드시 실패해야 한다.
- 이는 일부 PintOS 코드가 가상 페이지 0이 매핑되지 않았다고 가정하기 때문이다.
- 또한, length가 0인 경우에도 mmap은 실패해야 한다.
- 마지막으로, 콘솔 입력 및 출력을 나타내는 파일 디스크립터는 매핑할 수 없다.- 메모리 맵핑된 페이지는 익명 페이지와 마찬가지로 지연 할당 방식으로 할당되어야 한다.
- 페이지 객체를 만들기 위해 vm_alloc_page_with_initializer 또는 vm_alloc_page를 사용할 수 있다.// syscall.c void syscall_handler (struct intr_frame *f UNUSED) { uint64_t system_call_num = f->R.rax; switch (system_call_num) { ... case SYS_MMAP: f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8); break; case SYS_MUNMAP: munmap(f->R.rdi); break; ... } } void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) { if (addr == NULL || || addr != pg_round_down(addr)) return NULL; if (is_kernel_vaddr(addr) || is_kernel_vaddr(addr + length)) return NULL; if (offset != pg_round_down(offset)) return NULL; if (spt_find_page(&thread_current()->spt, addr)) return NULL; if (fd == 0 || fd == 1) return NULL; struct file *found_file = find_file_by_fd(fd); if (found_file == NULL) return NULL; if (file_length(found_file) == 0 || (int)length <= 0) return NULL; return do_mmap(addr, length, writable, f, offset); } void munmap (void *addr) { do_munmap(addr); }if (addr == NULL || || addr != pg_round_down(addr))
return NULL;- addr 이 0(NULL) 일 경우 반드시 실패
- 파일 전체가 addr에서 시작하여 연속적인 가상 페이지에 매핑된다.if (is_kernel_vaddr(addr) || is_kernel_vaddr(addr + length))
return NULL;- 페이지를 매핑해야 하므로 유저 영역의 주소이어야 한다.
- 페이지의 일부 바이트가 파일의 끝을 넘어 튀어나오면 안된다.if (offset != pg_round_down(offset))
return NULL;- fd로 열린 파일은 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑한다. if (spt_find_page(&thread_current()->spt, addr))
return NULL;- 매핑된 페이지 범위가 기존에 매핑된 페이지 집합과 겹치는 경우 반드시 실패해야 한다. if (fd == 0 || fd == 1) return NULL; - 콘솔 입력 및 출력을 나타내는 파일 디스크립터는 매핑할 수 없다. if (found_file == NULL) return NULL;
if (file_length(found_file) == 0 || (int)length <= 0)
return NULL;- fd로 열린 파일의 길이가 0 바이트인 경우 실패할 수 있다.
- length가 0인 경우에도 mmap은 실패해야 한다.
void * do_mmap (void *addr, size_t length, int writable, struct file *file, off_t offset) { struct file *reopened_file = file_reopen(file); void *start_addr = addr; int total_page_count = length % PGSIZE ? length / PGSIZE + 1 : length / PGSIZE; size_t read_bytes = file_length(reopened_file) < length ? file_length(reopened_file) : 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 container *container = (struct container *)malloc(sizeof(struct container)); container->file = reopened_file; container->offset = ofs; container->read_bytes = page_read_bytes; if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, container)) return NULL; struct page *p = spt_find_page(&thread_current()->spt, start_addr); p->mapped_page_count = total_page_count; read_bytes -= page_read_bytes; zero_bytes -= page_zero_bytes; addr += PGSIZE; offset += page_read_bytes; } return start_addr; }- fd로 열린 파일을 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑하는 것이 목표이기에 load_segment의 로직과 매우 흡사하다.
- 다른 부분만 알아보도록 하겠다.file_reopen(file); - 이 파일에 대한 새로운 파일 디스크립터를 얻기에 다른 매핑에 영향을 주거나 영향을 받지 않는 독립적인 매핑을 가질 수 있다. start_addr - 이 함수가 성공하면 파일이 매핑된 가상 주소를 반환한다. total_page_count & mapped_page_count - total_page_count : 이 매핑을 위해 사용한 총 페이지 수
- mapped_page_count : 매핑된 페이지 갯수, 해제 시 사용한다.
구현2 (Complete)
void munmap (void *addr);- 지정된 주소 범위 addr에 대한 맵핑을 해제한다.
- 이 주소는 동일한 프로세스에서 이전에 mmap 호출에 의해 반환된 가상 주소여야 하며, 아직 해제되지 않은 상태여야 한다.- 프로세스가 종료될 때, exit을 통하든 다른 방법으로든, 모든 맵핑은 암묵적으로 해제된다.
- 맵핑이 암묵적으로 또는 명시적으로 해제될 때, 프로세스에 의해 작성된 모든 페이지는 파일로 다시 쓰여지며, 작성되지 않은 페이지는 그렇지 않아야 한다.
- 그런 다음 페이지들은 프로세스의 가상 페이지 목록에서 제거된다.
- 파일을 닫거나 제거해도 그 파일의 맵핑이 해제되지는 않는다.
- 맵핑은 생성된 후 munmap이 호출되거나 프로세스가 종료될 때까지 유효하며, 이는 Unix 관례를 따른다.
- 자세한 내용은 Open File 제거하기를 참조하시오.
· 각 맵핑에 대해 파일의 별도이고 독립적인 참조를 얻기 위해서는 file_reopen 함수를 사용해야 한다.
- 두개 이상의 프로세스가 동일한 파일을 맵핑하는 경우, 일관된 데이터를 보는 것이 요구되지 않는다.
- Unix는 두 맵핑이 동일한 물리 페이지를 공유하도록 하며, mmap 시스템 호출은 페이지가 공유도히는지 아니면 개인적인지(즉, 쓰기 시 복사)를 지정할 수 있는 인자도 가지고 있다.void do_munmap (void *addr) { struct supplemental_page_table *spt = &thread_current()->spt; struct page *p = spt_find_page(spt, addr); int count = p->mapped_page_count; for (int i = 0; i < count; i++) { if (p) destroy(p); addr += PGSIZE; p = spt_find_page(spt, addr); } }
구현3 (No touch)
void vm_file_init (void);- 파일 기반 페이지 서브시스템을 초기화한다.
- 이 함수에서는 파일 기반 페이지와 관련된 모든 것을 설정할 수 있다.
구현4 (No touch)
bool file_backed_initializer (struct page *page, enum vm_type type, void *kva);- 파일 기반 페이지를 초기화한다.
- 이 함수는 먼저 page->operations에 파일 기반 페이지를 위한 핸들러를 설정한다.
- 메모리를 지원하는 파일과 같이 페이지 구조체에 일부 정보를 업데이트하고 싶을 수 있다.
구현5 (Complete)
static void file_backed_destroy (struct page *page);- 파일 기반 페이지를 관련 파일을 닫음으로써 파괴한다.
- 내용이 변경되었다면, 변경 사항을 파일에 다시 써줘야 한다.
- 이 함수에서 페이지 구조체를 해제할 필요는 없다.
· file-backed_destroy의 호출자가 처리해야 한다.
static void file_backed_destroy (struct page *page) { // page struct를 해제할 필요는 없습니다. (file_backed_destroy의 호출자가 해야 함) struct file_page *file_page UNUSED = &page->file; if (pml4_is_dirty(thread_current()->pml4, page->va)) { file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->ofs); pml4_set_dirty(thread_current()->pml4, page->va, 0); } pml4_clear_page(thread_current()->pml4, page->va); }pml4_is_dirty - 가상 페이지 vpage에 대한 pml4의 PTE(Page Table Entry, 페이지 테이블 항목)가 더티(dirty)인 경우, 즉 PTE가 설치된 이후 페이지가 수정되었다면 true를 반환한다.
- pml4에 vpage에 대한 PTE가 없는 경우 false를 반환한다.file_write_at 1. buffer로부터 size바이트를 file에 쓴다.
2. 파일 내의 오프셋 file_ofs부터 시작한다.
3. 실제로 쓰인 바이트 수를 반환하며, 이는 파일의 끝에 도달하면 size보다 적을 수 있다.
- (보통 이런 경우 파일을 확장하지만, 파일 확장은 아직 구현되지 않았습니다.)
4. 파일의 현재 위치는 영향을 받지 않는다.pml4_set_dirty - pml4에서 가상 페이지 vpage에 대한 PTE의 더티 비트를 0로 설정한다. pml4_clear_page - 사용자 가상 페이지 upage를 페이지 디렉토리 PD에서 "존재하지 않음(not present)"으로 표시한다.
- 이후 해당 페이지에 대한 접근은 오류(fault)를 발생시킬 것이다.
- 페이지 테이블 항목의 다른 비트는 보존된다. upage는 매핑될 필요가 없다.
ETC. pintOS 트러블 슈팅
1. Sync 관련 (project2)
- 먼저 우리가 구현한 lock_acquire와 lock_release를 재검토할 필요가 있다.
- 나는 위의 두 함수가 깔끔하게 구현이 안되어있어서 그런지 계속 lock_release 부분에서 에러가 발생했다.
- 그래서 semaphore를 이용해서 lock처럼 사용해서 이 문제를 해결했다.
· open, read, write, process_exec의 load 수행 시 동기화를 제어해줘야하기에 이 4부분에 semaphore를 걸어줬다.
- 그래도 lock 관련 함수는 수정해야 한다.
· file 관련 함수들 안에서 lock 코드를 사용하기 때문에 lock 관련 코드들을 정상 작동하게 만들어야 한다.
· 아니면 file_read 함수에서 lock_release 에러가 계속 발생하더라...2. pt-write-code2
- 테스트 코드를 읽어보면 쉽게 해결할 수 있다. int read (int fd, void *buffer, unsigned size) { ... // project3 : pt-write-code2 test struct page *page = spt_find_page(&thread_current()->spt, buffer); if (page && !page->writable) { sema_up(&filesys_sema); exit(-1); } ... }3. pt-grow-stk-sc

- gitbook의 해당 파트를 읽어보자!
- 페이지 폴트에 의존하는 겨애우 커널에서 페이지 폴트가 발생하는 다른 경우를 처리해야 한다.
- 프로세서는 예외로 인해 사용자 모드에서 커널 모드로 전환될 때만 스택 포인터를 저장하므로 전달된 값을 읽으면 사용자는 스택 포인터가 아닌 정의되지 않은 값이 생성된다.
- 사용자 모드에서 커널 모드로 처음 전환할 때 rsp를 저장하는 것과 다른 방법을 준비해야 한다.// syscall.c void syscall_handler (struct intr_frame *f UNUSED) { thread_current()->rsp_stack = f->rsp; ... }// vm.c bool vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED, bool user UNUSED, bool write UNUSED, bool not_present UNUSED) { ... if (not_present) { void *rsp = f->rsp; if (!user) rsp = thread_current()->rsp_stack; ... }4. page-merge-...
- 해당 테스트는 복잡하지만 한번 읽어보자!
· create - open - write - fork의 순으로 실행되는 것을 볼 수 있는데 create의 동기화도 필요하다.
* 그래도 page-merge-mm 안될 경우도 있다... 이유는 좀 더 찾아보겠다.bool create (const char *file, unsigned initial_size) { sema_down(&filesys_sema); check_address(file); bool result = filesys_create(file, initial_size); sema_up(&filesys_sema); return result; }'정글 크래프톤 5기 회고 및 정리 > PintOS' 카테고리의 다른 글
PintOS Project3 - Virtual Memory 3편 (미완) (0) 2024.06.06 PintOS Project3 - Virtual Memory 1편 (0) 2024.06.04