-
728x90
구현을 열심히 해 보았지만 능력의 한계로 진행이 더뎌졌다. 그래서 빠르게 팀원의 도움과 여러 블로그의 참조를 통해서 내가 어떤 잘못을 했는지 파악하는 방향으로 나아가고자 했다.
나의 구현과 팀원 혹은 블로그들의 구현 상 달랐던 점을 비교하고 각 함수가 무엇을 하는지 살펴보겠다.
일단 구현 사항들은 다음과 같다.
vm.c
- supplemental_page_table_entry()
- vm_claim_page()
- vm_evict_frame()
- vm_get_victim()
- spt_insert_page()
- vm_alloc_with_initializer()
- vm_do_claim_page()
- vm_try_handle_fault()
- vm_get_frame()
- spt_find_page()
vm.h
- struct lazy aux
- struct page
- struct frame
process.c
- lazy_load_segment()
- load_segment()
- setup_stack()
여기서 문제가 되었었던 함수들을 이제 하나씩 살펴 보겠다.
vm_do_claim_page()
- 기능
페이지를 요청하고 mmu를 세팅한다고 주석에 나와있다.
인자로 들어오는 page와 frame을 연결시키는 작업을 수행하고 가상 주소와 물리 주소 간의 매핑을 페이지 테이블에 추가한다. - 함수 설명
- 먼저, 사용가능한 프레임을 가져온다.
vm_get_frame()은 User pool에서 물리 메모리에 사용 가능한 프레임을 palloc_get_page를 통해 프레임에 할당한 후 그 프레임을 반환하는 함수이다. - 다음으로 frame과 page를 연결한다.
여기서 중요한 것은 frame의 kva나 page의 va를 직접 연결하는 것이 아니다.
가상 주소(va)나 커널 가상 주소(kva)를 직접 저장하는 것은 해당 주소값에만 초점을 맞춘 연결을 의미한다. 반면, 구조체 자체를 서로 가리키게 해야 프레임을 통해 페이지 접근이 가능해진다. - mmu를 세팅한다.
pml4_get_page()는 스레드의 pml4테이블에서 인자로 들어온 page의 가상주소(page->va)와 연결된 kva를 가져온다.
vm_do_claim_page()안에서 호출되는 pml4_get_page()에서는 page의 가상주소에 연결되어 있는 kva가 존재하면 안된다(새로운 페이지 이므로) 따라서 pml4_get_page()를 validation으로 활용한다.
validation을 통과하고 난 후엔 pml4_set_page로 통해 현재 스레드의 pml4 테이블에 가상주소끼리의 연결을 수행한다.
이 때 페이지를 세팅하는 과정에서 인자로 들어왔던 page->va를 page_round_down()을 사용하여 주소를 페이지 단위로 정확하게 관리하기 위하여 반내림(혹은 올림) 해준다. - 실제로 페이지를 메모리에 적재한다.
swap_in()을 호출하여 디스크에서 메모리로 적재한다.
- 먼저, 사용가능한 프레임을 가져온다.
- 디버깅
- pml4_get_page()는 인자에 page->va 즉, 페이지의 가상 주소를 넣었어야 했는데, 페이지 자체의 주소를 넣어 validation을 하고 있었다.
- pg_round_down()도 수행하고 있지 않았다.
vm_try_handle_fault()
- 기능
- page fault event를 처리한다.
- 매개변수
- struct intr_frame *f:
인터럽트 발생 시의 CPU 레지스터 상태를 포함하는 구조체의 포인터. - void *addr:
페이지 폴트가 발생한 가상 주소입니다. 이 함수는 addr이 가리키는 주소에서 문제를 해결하려고 시도한다. - bool user:
폴트가 사용자 모드에서 발생했는지를 나타내는 플래그.
메모리 접근 권한과 관련된 처리를 결정하는 데 사용할 수 있다. - bool write:
페이지 폴트가 쓰기 연산 중에 발생했는지를 나타내는 플래그입니다.
이는 페이지의 writable 멤버와 관련하여 사용된다. - bool not_present:
해당 가상 주소에 대한 페이지가 현재 메모리에 존재하지 않음을 나타내는 플래그입니다.
대부분의 페이지 폴트 처리는 이 플래그가 true일 때 수행됩니다.
- struct intr_frame *f:
- 함수 설명
- NULL 주소 검증
addr이 NULL인 경우, 이는 유효하지 않은 메모리 접근 시도로 간주되어 false를 반환해야 한다. - 커널 주소 공간 접근 검증
사용자 프로그램이 커널 주소 공간에 접근하려고 시도하는 경우 즉, (is_kernel_vaddr(addr)가 true인 경우) 바로 return false 해야 한다. 왜냐하면 현재로선 page fault가 일어나는 것은 유저공간에서만 가능하기 때문이다. - 페이지 존재 여부 및 쓰기 권한 검증:
not_present가 true인 경우, 즉, 페이지가 메모리에 존재하지 않을 때, spt_find_page()를 통해 주어진 addr에 해당하는 페이지를 보충 spt에서 찾는다.
페이지가 없거나, 쓰기 권한이 없는 페이지에 쓰기를 시도하는 경우, 오류로 간주하여 false를 반환한다. - 페이지 클레임 처리
위의 검증을 모두 통과한 경우, vm_do_claim_page(page)를 호출하여 페이지 폴트를 해결한다.
즉, 검증을 다 이겨냈다는 말은 의도된 페이지 폴트라는 말이다.
- NULL 주소 검증
- 디버깅
- 조건문의 분기가 잘못되어 있었다.
vm_get_frame()
- 기능
새로운 프레임을 유저 풀로부터 할당받는다. 만약 사용 가능한 프레임이 없다면, 기존의 프레임을 evict하여 그 공간을 반환하여 항상 유효한 프레임을 return한다. - 매개변수
아무것도 받지 않는다. 특정 페이지와 연결된 페이지를 주거나 하는 것도 아니고, 그저 유효한 프레임을 return한다. - 함수 설명
- 프레임 구조체 할당
malloc을 사용하여 struct frame 타입의 새로운 인스턴스에 대한 메모리를 동적으로 할당받는다.
새로운 프레임이 생겼고, 이제 정보를 채워 넣어야 한다. - 물리적 페이지 할당
palloc_get_page(PAL_USER)를 호출하여 사용자 모드에서 사용할 수 있는 물리적 페이지(프레임)를 할당받는다.
PAL_USER 플래그는 커널의 유저 풀에서 페이지를 가져온다 즉, 사용자 모드용 페이지를 요청하는 것을 나타낸다.
frame->kva는 할당받은 물리적 메모리의 커널 가상 주소이다.
이 주소는 이후 pml4 페이지 테이블에 가상 페이지를 물리적 메모리에 매핑하여 실제 물리 메모리에 접근할 수 있다. - 페이지 할당 실패 시 처리
frame->kva가 NULL인 경우, 즉 물리적 메모리가 부족하여 페이지 할당에 실패한 경우, 기존에 할당된 프레임 중 하나를 회수하는 과정(페이지 쫓아내기, 페이지 교체 알고리즘을 통한 eviction)을 진행한다.
vm_evict_frame() 함수는 사용중인 프레임 중 하나를 선정하여 그 프레임을 사용할 수 있도록 만든다.
회수된(쫓겨난) 프레임의 page 포인터를 NULL로 설정하여, 그 프레임이 더 이상 특정 가상 페이지와 연결되어 있지 않음을 나타낸다.
- 프레임 구조체 할당
- 디버깅
- 사용할 프레임을 malloc으로 동적으로 할당받고 있지 않았다. 프로세스와 스레드는 동적으로 생성되고 종료되기 때문에 각 프로세스나 스레드가 메모리를 요구할 때마다, 필요한 프레임 구조체를 동적으로 할당함으로써, 시스템은 이러한 동적 환경을 관리할 수 있다.
- list_push_back()에 frame 자체가 아닌 frame의 fram_elem을 넣어줘야 리스트에서 사용이 가능하다.
spt_find_page()
- 기능
spt(supplemental page table)에서 주어진 가상 주소에 해당하는 page 구조체를 찾는다. - 매개변수
- supplemental_page_table *spt
spt는 스레드 별로 하나씩 갖고 있다. 주로 thread_current()->spt와 같은 인자를 받을 것이다. - void *va
spt에서 찾아야 할 va이다. 이 va는 페이지 자체의 주소가 아니라, page->va를 말한다.
이 va를 통해 우리가 원하는 page 구조체를 찾을 수 있다.
- supplemental_page_table *spt
- 함수 설명
- 메모리 할당 및 주소 정렬
먼저, 새로운 struct page 인스턴스에 대한 메모리를 동적으로 할당받는다.
이를 return하여 계속해서 수정하거나 사용할 것이기 때문이다.
할당받은 page 구조체의 가상 주소(va) 필드를 설정하기 전에, pg_round_down(va)를 사용하여 주어진 가상 주소를 해당 주소가 속한 페이지의 시작 주소로 반올림한다.
이는 페이지 기반 메모리 관리에서 한 페이지의 모든 주소가 페이지의 시작 주소로 표현되어야 하기 때문이다. - 해시 테이블에서의 검색
보충 페이지 테이블은 해시 테이블을 사용하여 각 가상 페이지에 대한 정보를 효율적으로 저장하고 검색한다.
hash_find 함수를 호출하여, 준비된 page 구조체의 hash_elem을 기반으로 해시 테이블 내에서 해당 요소를 찾는다. - 검색 결과 반환
element가 NULL이 아닌 경우, 즉 해시 테이블에서 해당 가상 주소에 대응하는 요소를 찾은 경우, hash_entry 매크로를 사용하여 해당 hash_elem을 포함하는 struct page 구조체로 변환하여 반환한다.
만약 element가 NULL인 경우, 즉 주어진 가상 주소에 대응하는 페이지 정보가 보충 페이지 테이블에 없는 경우, NULL을 반환한다.
- 메모리 할당 및 주소 정렬
- 디버깅
- 우리가 찾은 페이지를 malloc()을 해주어야 할 이유를 찾지 못하고 있었는데, page를 return하여 spt_find_page()를 호출한 함수에서 이 page를 수정하여 사용할 수 있기 때문인 것 같다.
- 페이지의 시작 주소를 나타내고, 정렬하기 위한 pg_round_down()을 하지 않고 있었다.
setup_stack()
- 기능
간단하게는 프로세스의 스택을 설정하는 역할을 한다.
vm 구현 이전에는 물리 메모리를 직접 할당하고 가상 주소에 매핑했다.
가상 메모리 시스템을 사용하는 버전에서는, 페이지를 먼저 가상 주소 공간에 할당하고 필요시 물리 메모리에 매핑하는 과정을 거친다. - 매개변수
- struct intr_frame *if_
현재 스레드의 인터럽트 프레임을 주로 받아올 것이고, 이를 통해 rsp에 접근하여 스택의 값을 조정한다.
- struct intr_frame *if_
- 함수 설명
- 스택의 맨 밑 주소 계산
stack_bottom은 사용자 스택의 맨 밑 주소를 계산한다.
여기서 USER_STACK은 스택의 최상위 주소(통상적으로 가상 메모리의 높은 주소 쪽에 위치)를 나타내며, PGSIZE (페이지 크기)를 빼주어 스택의 시작 주소를 결정한다.
이 주소는 스택의 실제 메모리 매핑을 시작할 위치이다. - 스택 페이지 할당:
- vm_alloc_page(VM_ANON, stack_bottom, 1) 호출을 통해, stack_bottom에 해당하는 가상 주소에 페이지를 할당한다.
이 단계에서는 메모리의 가상 주소에 페이지를 할당하되, 실제 물리 메모리와는 아직 매핑되지 않는다.
- vm_alloc_page(VM_ANON, stack_bottom, 1) 호출을 통해, stack_bottom에 해당하는 가상 주소에 페이지를 할당한다.
- 스택 페이지 매핑(claim)
- vm_claim_page(stack_bottom)을 호출하여 앞서 할당한 가상 페이지를 실제 물리 메모리에 매핑한다.
이 과정에서 필요한 물리 메모리가 할당되고, 스택 페이지가 실제로 사용 가능한 상태가 된다.
- vm_claim_page(stack_bottom)을 호출하여 앞서 할당한 가상 페이지를 실제 물리 메모리에 매핑한다.
- 스택 포인터 설정
- 스택 페이지 매핑에 성공하면, 인터럽트 프레임의 스택 포인터(if_->rsp)를 USER_STACK으로 설정한다.
이는 프로세스의 스택 포인터를 사용자 스택의 최상단으로 초기화하는 것을 의미합니다.
스택 포인터가 이 위치에 설정됨으로써, 프로세스는 스택을 사용하여 함수 호출, 지역 변수 저장 등의 작업을 수행할 수 있게 됩니다.
- 스택 페이지 매핑에 성공하면, 인터럽트 프레임의 스택 포인터(if_->rsp)를 USER_STACK으로 설정한다.
- 성공 여부 반환
- 위의 모든 단계가 성공적으로 수행되면, success 변수를 true로 설정하여 함수가 성공적으로 스택을 설정했음을 나타낸다. 만약 중간에 어떤 단계에서라도 실패한다면, success는 false를 유지하며, 이는 스택 설정이 정상적으로 완료되지 않았음을 의미한다.
- 스택의 맨 밑 주소 계산
- 디버깅
- 스택을 setup하는 과정에서 page를 palloc해주고 있었다. vm_alloc_page()를 통해 새로운 페이지를 stack_bottom의 위치에 설정해주어야 하는 것이 setup_stack()의 기능이었다.
구현을 다 하고, commit을 하는 과정에서 간격때문인지 무엇 때문인지는 모르겠으나, 실제로 로직이 수정되지 않은 부분도 수정된 것으로 포함되었다.
나중에 겪을 귀찮은 conflict들을 위해 기존으로 되돌리는 과정에서, supplemental_page_table_kill의 수정도 되돌리는 바람에 무엇이 틀렸는지 찾는 과정을 다시 한 번 겪어야 했다.문제는 supplemental_page_table_kill에 있었다.
hash_destroy()가 아닌 hash_clear()을 통해 hash를 청소해야 한다.
여기까지 했을 때 56개가 통과되었고, fork부분을 구현하기 위해 다음 함수를 수정하였다.
supplemental_page_table_copy
중요한 것은 페이지를 선언하고 그냥 바로 dst_page->va = src_page->va처럼 할당하면 안된다는 것이다.
이러한 직접적인 할당은 항상 패닉이 일어났다.
따라서 따로 변수를 만들어 담아 두고, 최종적으로 validation을 다 거치고 난 이후에 dst_page에 값들을 넣어주어야 한다.
- 기능
한 spt를 다른 spt로 복사하는 함수이다. 주로 프로세스의 주소 공간을 복제하는 데 사용된다. - 매개변수
- struct supplemental_page_table *dst
복사한 데이터를 넣을 spt - struct supplemental_page_table *src
복사할 데이터가 담긴 spt
- struct supplemental_page_table *dst
- 함수 설명
- 해시 테이블 반복자(iterator) 초기화
src 보충 페이지 테이블의 해시 테이블에 대한 반복자(i)를 초기화한다.
hash_first 함수를 사용하여 반복자를 초기 해시 테이블 엔트리로 설정한다. - 해시 테이블 순회
while (hash_next (&i)) 루프를 통해 src spt의 모든 페이지를 순회한다.
각 페이지는 hash_entry 함수를 사용하여 현재 hash_elem에서 struct page 타입으로 변환된다. - 페이지 타입 및 속성 복사
각 소스 페이지(src_page)의 타입(src_type)과 쓰기 가능 여부(src_writable)를 확인한다.
페이지 타입 확인에 src_page->operations->type 접근 방식이 사용되는 이유는, 페이지의 동작(operations)과 관련된 정보를 직접 조회하여 현재 페이지의 상태를 반영하기 때문이다. - VM_UNINIT 타입 페이지 처리
소스 페이지가 초기화되지 않은 상태(VM_UNINIT)인 경우, 해당 페이지를 초기화하는 데 필요한 정보(src_init, src_aux)를 사용하여 동일한 가상 주소에 페이지를 할당하고 초기화 함수를 설정한다. - 기타 페이지 타입 처리
VM_UNINIT 이외의 페이지 타입에 대해, 소스 페이지와 동일한 타입과 속성으로 대상 보충 페이지 테이블에 페이지를 할당한다.
vm_alloc_page 함수를 사용하여 할당을 시도하고, 이후 vm_claim_page를 호출하여 물리 메모리에 페이지를 실제로 매핑한다. - 메모리 내용 복사
페이지 할당과 매핑이 성공한 후, memcpy 함수를 사용하여 소스 페이지의 물리 메모리 내용을 대상 페이지의 물리 메모리로 복사한다. 물리메모리에 접근하는 것이기 때문에 복사 과정에서 페이지 프레임의 커널 가상 주소(kva)를 사용한다.
할당, 매핑, 복사가 전부 다르다는 것을 유의해야 한다. - 성공 여부 반환
모든 페이지의 복사가 성공적으로 완료되면 true를 반환하여, 보충 페이지 테이블 복사 과정이 성공적으로 완료되었음을 나타낸다. 만약 어느 단계에서든 실패하면, 즉시 false를 반환한다.
- 해시 테이블 반복자(iterator) 초기화
- 디버깅
- 일단, 소스 페이지의 주소를 목적지 페이지로 직접 할당하려하고 있었다. 또한 메모리를 할당하는 것 만으로는 복사가 되는 것이 아니라 메모리 자체를 복사해야 진짜 복사가 이뤄진다는 사실을 생각하지 못하고 있었다.
- 더불어 page_get_type()를 잘못 사용하고 있었다.
복사를 통해서 uninit page 또한 복사가 되어야 하는데, page_get_type은 초기화 이후의 page type을 반환한다. 따라서 uninit page의 정보는 복사되지 않았다.
'Projects > Krafton_Jungle_4' 카테고리의 다른 글
[React] 실력다지기 프로젝트 - 프론트엔드 (2) 2024.04.20 [PintOS] Project 3 - Virtual Memory, Stack Growth (1) 2024.04.03 [PintOS] Project 3 - Virtual Memory, Anonymous page (1) (0) 2024.03.27 [PintOS] Project 3 - Virtual Memory git book, Introduction (2) 2024.03.23 [PintOS] Project 2 - System Call (3) (0) 2024.03.20