[pintOS] 3주차 VM: 진행 과정 기록
CHECK POINT 1
- 상태
- 기존에 통과하던 테스트케이스들이 실패
- 문제가 되는 테스트케이스 예시 :
pintos -v -k -T 60 -m 20 --fs-disk=10 -p tests/userprog/args-none:args-none --swap-disk=4 -- -q -f run args-none
- 개선 요구 사항: Implement Supplemental Page Table
- process 를 새로 생성하거나 fork할 때 실행되는 supplemental page table과 관련된 기능들이 구현되어야 함 (
supplemental_page_table_init()
등)
- 진행 과정
- ((
vm.h
)) supplemental_page_table
생성 및 초기화
- spt의 자료구조를 선택해 반영 (해시 테이블로 선택)
- spt인 hash의 초기 설정은 딱히 안해주어도 되려나?
- ((
vm.c
)) supplemental_page_table_init()
구현
- 입력값으로 들어온 thread의 spt를 initiate
page_hash()
, page_less()
구현 필요
hash_init()
활용
- hash_init()은 malloc을 사용해 bucket에 메모리를 할당하는데, 실패한 경우 처리는 어떻게?
- ((
vm.h
)) struct page
에 hash table을 위한 hash_elem 추가
- ((
vm.c
)) page_hash()
, page_less()
구현
- ((
vm.c
)) spt_find_page()
구현
- spt에서 va를 가진 page를 찾기 위해, 동일한 va를 가진 dummy page를 생성해서 hash_find의 인자로 넘겨줌
- dummy page를 spt_find_page 내에서 지역변수로 정의해도 되겠지?
hash_find()
활용
- ((
vm.c
)) spt_insert_page()
구현
- spt에 page를 추가하며, 추가하기 전에 spt에 동일한 va(virtual address)가 이미 존재하는지 체크해주어야 함
hash_insert()
활용
- ((
vm.c
)) spt_remove_page()
업데이트? 일단 보류
- hash table을 정리하는 역할을 이 함수에서 수행해야 할까?
- hash table에서 추가된 page를 제거하는 타이밍이 언제일까?
- 개별적으로? process가 종료될 때 한 번에?
CHECK POINT 2
- 상태
- check point 1과 동일
- (git book의 가이드에 따라 Frame Management 파트로 분리해 진행)
- 개선 요구 사항: Frame Management
- palloc으로 user 영역에서 page를 할당 받아, frame 구조체의 kva (kernel virtual address)에 연결
- 즉, frame은 물리메모리의 user 영역 내 page를 하나씩 들고 있음
- 이 때, frame 구조체는 malloc으로 kernel 영역에 할당 받음
- 만약 user 영역에 추가 page 할당이 불가능할 경우, 기존의 frame을 재활용
- frame에 연결된 기존 page를 swap out 한 뒤, 해당 frame에 다른 page를 연결
- 그렇기 때문에 malloc으로 할당 받은 frame의 영역을 바로 free하지 않음
- 결과적으로 frame도 필요할 때마다 추가되는 방식임
- (책에서 설명하듯) 물리메모리의 가능한 user 영역들에 frame을 모두 할당해 놓는 것이 아니라, frame과 그 frame에 연결된 page를 만드는 방식임
- 진행 과정
- ((
vm.c
)) frame_table
생성 및 초기화
- Allows efficient implementation of eviction policy of physical frames
- 생성된 frame들을 보관하는 곳
- 자료형은 일단 list로 결정
- 전역 변수? 정적 변수? (아니면 gitbook 가이드대로 malloc?)
vm.c
에서만 접근한다면 정적 변수로 설정
- 주로 swap_in, swap_out할 때 victim을 정할 때 활용됨
vm_init()
에서 list_init()
으로 초기화해주어야 함
- ((
vm.h
)) struct frame
업데이트
- frame table의 자료형에 맞게 frame 구조체 수정
struct list_elem elem;
- ((
vm.c
)) vm_get_frame ()
구현
- palloc_get_page으로 user 영역에서 page 한 개 할당 받기
- palloc에 성공했을 때
- malloc으로 frame 생성 및 할당 받은 page 연결
- frame table에 추가
- 일단 push_back으로 처리하되, 추후 victim 정하는 정책에 맞게 수정
- palloc에 실패했을 때
- ((
vm.h
)) struct page
업데이트
- page의 상태 정보 중 writable 추가
vm_do_claim_page ()
에서 pml4_set_page()
로 pte를 추가할 때 넣어주기 위함
bool writable;
- ((
vm.c
)) vm_do_claim_page ()
구현
- page table(pml4)에 page table entry 추가 (page의 va, frame의 kva 활용)
swap_in()
을 통해 실제 물리 메모리에 올림
pml4_set_page()
활용
- project 2 코드 중
install_page()
참고
- ((
vm.c
)) vm_claim_page ()
구현
- spt에서 매개변수
va
에 해당하는 page를 찾아 page table에 pte 추가
spt_find_page()
, vm_do_claim_page ()
활용
CHECK POINT 3
- 상태
- check point 1,2와 동일
- (git book의 가이드에 따라 Frame Management 파트로 분리해 진행)
- 개선 요구 사항: Page Initialization with Lazy Loading
- 하나의 executable이 새로운 process에 load될 때, load할 대상(?)을 load_segment 함수를 통해 순차적으로 load하게 됨
- project 2까지의 코드에서는 모든 것들을 한꺼번에 physical memory에 load했었지만, 이제는 lazy_load가 되도록 수정해주어야 함
- 진행 과정
- ((
vm.c
))vm_alloc_page_with_initializer()
구현
- 이 함수는 앞서 이야기한
load_segment()
에서 실행되며, 해당 process(thread)의 supplemental page table을 활용해, 각 page가 필요한 시점에 물리메모리에 load 될 수 있도록 (즉 lazy_load)될 수 있도록 처리해주어야 함
malloc()
활용해 page 생성
- 나중에
vm_dealloc_page()
을 통해 free 하게 됨
uninit_new()
활용해 UNINIT 타입으로 page를 초기화
- lazy load 시점에 변경될 타입을 확인 및 전달 시 VM_TYPE macro 활용
- macro 사용 이유? 나중에 VM_MARKER_0 (stack을 의미) 등으로 vm_type에 추가적인 마커를 표시하기도 하기 때문
- 해당 TYPE에 맞게 lazy load 시점에 사용할 initializer 함수도
uninit_new()
에 전달
spt_insert_page()
활용해 현재 thread의 spt에 새로 생성 및 초기화된 page를 추가
- ((
uninit.c
)) uninit_initialize()
체크
- vm_do_claim_page()을 통해 swap_in 될 때, page가 uninit 상태였다면 실행되는 함수
- 인자로 전달된 page를 실제 type으로 다시 초기화하여 kva가 가리키는 물리메모리에 올려놓는 역할 수행
- 이 함수가 실행될 때 page는 이미 thread의 page table(pml4)에 올라간 상태임
- ((
exception.c
)) page_fault()
에서 임시 코드 제거
- project 2에서 추가되었던, page_fault 발생 시 프로세스를 종료하는 코드 제거
- ((
anon.c
)) vm_anon_init()
체크
- ((
anon.c
)) anon_initializer()
체크
- page의 operations 함수들을 anon_ops로 교체
- ((
process.c
)) project2에서의 load_segment()
체크
- load_segment()와 lazy_load_segment() 구현을 위해 project2 때 load_segment에서 수행하던 작업들을 참고
- project2 때는 수행했지만 project3에서는 하지 않고 있는 작업들이 lazy_load_segment에게 이관됨
file_seek(file, ofs)
로 파일에서 읽기 시작할 position을 수정
file_read(file, kpage, page_read_bytes)
로 file에서 page_read_bytes만큼의 데이터를 읽어 kpage(물리메모리)에 저장
file_read()
실패 시 palloc_free_page()
memset()
으로 page의 남은 영역 0으로 변경
install_page()
로 pte를 page table(pml4)에 등록
lazy_load_segment()
에 인자로 전달할 값 묶음
- file: process가 종료되기 전까지는 file이 계속 열려있으니 주소값만 전달해도 되겠지?
- ofs, page_read_bytes, page_zero_bytes
- ((
process.c
)) load_segment()
업데이트
lazy_load_segment()
에서 필요한 정보들을 aux에 담아 vm_alloc_page_with_initializer()
의 마지막 인자로 전달
malloc
을 활용해 load_info
형식으로 aux
를 구성
- 주의!! lazy_load에서 file_seek으로 ofs를 매번 다시 설정해주어야 하며, 이를 위해 ofs를 업데이트 해주어야 함
- ((
process.c
)) lazy_load_segment()
구현
- 위에서 체크했던 내용들을 상황에 맞게 실행
file_seek(file, ofs)
file_read(file, page->frame->kva, page_read_bytes)
file_read()
실패 시 이번에는 vm_dealloc_page
사용
memset()
- lazy load 되는 시점에는 page가 이미 page table에 등록된 상태
aux
의 역할이 끝났으므로 malloc으로 할당되었던 메모리 free
- ((
process.c
)) setup_stack()
구현
- project2에서 하던 작업들
kpage = palloc_get_page (PAL_USER | PAL_ZERO);
로 stack을 위한 영역 할당
- 영역 할당에 성공 시
install_page (((uint8_t *) USER_STACK) - PGSIZE, kpage, true);
로 page table(pml4)에 pte 추가
- 이 때, 가상 주소를 stack의 위치로 설정
if_->rsp = USER_STACK;
로 stack pointer의 위치 수정
- project3에 적용
vm_alloc_page()
로 페이지 할당
- 이 때, type에 marker 표시
- 이 때, upage에 stack의 위치 전달
vm_claim_page()
로 즉시 물리메모리에 배치
- TODO 실패 시 page를 찾아 dealloc 해주어야 함
if_->rsp = USER_STACK;
로 stack pointer의 위치 수정
- ((
vm.c
)) vm_try_handle_fault()
업데이트
- page fault가 kernel 영역에서 발생한 경우 return false
spt_find_page()
활용해 fault를 일으킨 주소값이 supplemental page table에 있는 주소인지 확인
- spt에 있는 경우
vm_do_claim_page(page)
로 물리메모리에 로드
- return 값이 true일 경우
page_fault()
함수가 종료되며, page fault가 해결된 상태(즉 lazy load된 상태)로 다시 user program이 실행됨
CHECK POINT 4
- 상태
- exec 관련 testcase 실패
- fork 관련 testcase 실패
- wait 관련 testcase 실패
- 등등
- 개선 요구 사항: revisit the supplemental page table interface to support copy and clean up operations
- process fork 시 parent의 spt를 child의 spt로 복제하는 함수 생성
- process exit 시 spt를 제거하는 함수 생성 (메모리 초기화 필수)
- 진행 과정
- ((
process.c
)) process_exec()
에서 spt를 다시 초기화
process_cleanup()
과정에서 기존의 spt는 supplemental_page_table_kill()
에 의해 삭제되었으므로, 다시 프로그램을 load하고 실행하기 위해서는 spt를 초기화해주어야 함
- `supplemental_page_table_init(&thread_current()->spt);
- ((
vm.c
)) supplemental_page_table_copy()
구현
- process가 fork되면서
__do_fork_
내에서 실행됨
- 이 때 child process의 spt는
supplemental_page_table_init()
로 초기화된 상태
hash_iterator
, hash_first
, hash_next
, hash_cur
등을 조합해 parent process의 spt 내 page들을 하나씩 복제
- writable 이 false인 경우 공유할 수도 있으려나?? 일단 모두 복제
- 복제 시 page의 type에 맞게 처리
- VM_UNINIT 인 경우
- parent process에서
vm_alloc_page_with_initializer()
로 만들어져 lazy load가 되기를 기다리는 중 (아직 물리메모리에 올라가지 못하고 대기하고 있는 상태)
- child process에서도
vm_alloc_page_with_initializer()
사용하면 되겠다
- type, upage, writable 그대로
- init (lazy_load_segment) 그대로
- aux는 새로 생성해서 넣어주어야 함
- VM_ANON 인 경우
- 이미 uninit 상태를 지나 물리메모리에 올라가 본 page
- setup_stack 비슷하게 처리하면 될 듯
vm_alloc_page(type 그대로, upage 그대로, writable 그대로)
사용해 child process의 spt에 새로운 page를 넣음
spt_find_page()
로 새로운 spt에서의 page를 찾고
vm_do_claim_page()
로 해당 page를 물리메모리에 올려버린 뒤
memcpy()
로 동일한 내용이 되도록 복사
- 만약 parent page가 disk로 swap 되어 있었다면 어떻게 하지?
- memcpy 전에 parent page도 물리메모리에 올려놓도록 조치를 해야할까?
- VM_MARKER_0 인 경우 즉, 스택인 경우를 구분할 필요가 있을까?
- setupstack은 page를 할당하고, if->rsp 를 초기화해주는 역할
- 하지만 stack에 해당하는 page도 memcpy를 통해 복사해주면 되고, rsp는 오히려 초기화하면 안됨
- 스택을 별도로 구분하지 않기로 결정
- VM_FILE 인 경우
- 이미 uninit 상태를 지나 물리메모리에 올라가 본 page
- VM_ANON 일반 상황과 동일하게 처리 가능할 것
- 일단 아무것도 하지 않는 것으로 처리
- ((
anon.c
)) page type으로 스택을 식별할 수 있도록 anon page 수정
struct page_operations stack_ops
추가
anon_initializer()
에서 type 인자를 활용해 구분
- ((
vm.c
)) page_destroy()
구현
hash_destroy()
에 두 번째 인자로 전달할 함수
- hash_elem로 page를 찾아 page를 destroy
- ((
vm.c
)) supplemental_page_table_kill
구현
- 메모리가 할당되었던 부분
hash_init()
에서 hash->bucket에 malloc
vm_alloc_page_with_initializer()
로 생성된 각 page들
hash_destroy()
를 사용해 page_table 내 page들을 모두 free
- 두 번째 인자 desctructor을 통해서
page_table
에 포함된 page들에 접근
free(page)
대신 가이드에 따라 destroy(page)
로 처리 (구현해주어야 함)
hash_destroy
실행 중에 접근 못하도록 lock을 걸어야 하나? 일단 패스
- ((
uninit.c
)) uninit_destroy()
구현
- page가 들고 있는 load_info를 free 시켜주어야 함
- ((
anon.c
)) anon_destroy()
구현
- frame에 할당된 메모리를 해제해주는 것이 맞을까?
- frame에 할당된 메모리를 해제한다면, frame이 들고 있는 물리메모리의 페이지도 함께 free 해주어야 함 (
palloc_free_page
)
- 그 대신, 나중에
vm_get_frame
에서 놀고 있는 frame을 찾도록 할 수도 있을 것 같은데?
- 그렇다면 일단 page를 frame을 지우지 말고, frame 구조체가 가리키는 page를 NULL로 만들어, 죽은 process의 page가 담겨 있던 frame임을 표시해두자.
CHECK POINT 5
-
상태
- project2까지의 testcase 중 read/write 관련된 부분 제외하고 통과
- 본래는 project2까지의 testcase는 모두 통과해야 한다고 하지만, 일단 넘어가자!
-
개선 요구 사항: Stack Growth
- Now, if the stack grows past its current size, we allocate additional pages as necessary.
- 스택에 공간이 부족한 경우에 page fault가 발생하게 되므로, 스택을 키우는 작업을
vm_try_handle_fault()
에서 처리할 수 있음
- 이 때,
vm_try_handle_fault()
에서는 fault난 상황이 스택이 부족했던 것인지, 아니면 정말 엄한 장소에 접근하려 한 것인지 체크를 해야 함
- Allocate additional pages only if they "appear" to be stack accesses. Devise a heuristic that attempts to distinguish stack accesses from other accesses.
- 이를 위해서 page fault 직전의 user stack pointer의 위치를 기억해야 함
- 저장하는 이유?
- page fault를 유발한 주소와 user stack pointer의 위치를 비교하여 page fault가 stack 영역이 부족해 발생한 것인지 판단해주어야 함
- 이 때, if->rsp가 꼭 유저 영역이 아닐 수도 있음. 즉 커널 모드일 때 page fault가 발생했을 수도 있음
- 그런데 if->rsp는 유저 모드에서 커널 모드로 전환될 때만 업데이트되므로, 커널 모드에서 작업이 진행되던 중 page fault가 났을 때는 이미 rsp가 커널 모드에서 변경된 상태일 것
- 그러므로 유저 모드에서 커널 모드로 전환될 때 user stack pointer를 저장해두어야 함
- 구체적으로 보아 저장하는 타이밍은 user program에서 system call 혹은 page fault가 발생했을 때임
- Within a system call or a page fault generated by a user program, you can retrieve it from the rsp member of the struct intr_frame passed to syscall_handler() or page_fault(), respectively.
- 더불어, 스택 영역이 부족한 상황은 메모리에 write하려는 상황일 것임 (단순히 읽는 것이라면 fault도 나지 않았음)
- 또한, 스택 영역이 부족한 상황은 not_present인 상황일 것임 (read only page는 아닐 것)
-
진행 과정
- ((
thread.h
)) stack pointer를 임시로 보관하는 멤버 생성
- ((
syscall.c
)) syscall_handler()
수정
- user program에 의해 syscall이 발생했을 때 user stack pointer의 위치를 저장해 둠
- ((
exception.c
)) page_fault()
수정
- user program에 의해 page fault가 발생했을 때 user stack pointer의 위치를 저장해 둠
- ((
vm.c
)) vm_try_handle_fault()
수정
- stack growth 가 필요한 상황인지 판단해 처리
- 접근하려는 주소는 user 영역이어야 함
- 스택이 부족한 상황이므로 write을 위한 접근
- 스택이 부족한 상황이므로 not_present (read only가 아님)
- 접근하려는 주소가 스택 범위 내에 속해야 함
- stack size는 가이드에 따라 1MB로 제한
- ((
vm.c
)) vm_stack_growth()
구현
setup_stack()
참고
- 스택은 한 번에 여러 page가 커져야 할 수도 있음
CHECK POINT 6
- 상태
- 포인터 관련 testcase에서 read, write 관련된 것 빼고 통과
- 개선 요구 사항: Memory Mapped Files
- 진행 과정
- ((
syscall.c
)) syscall에 mmap, munmap 양식 추가
- mmap은 인자 5개를 받아 주소값 리턴
- munmap은 인자 1개를 받고 리턴값 없음
- ((
file.c
)) mmap된 영역을 누적 관리하는 자료 구조 구현
- mmap된 영역을 munmap 하려면 연이어 할당된 크기 (페이지의 크기)를 알아야 함
- 그냥 간단히 list로 구현
- mmap_list에서 mmap 단위로 관리할 정보를 묶어놓기 위해 mmap_info 구조체 추가
- ((
syscall.c
)) syscall mmap 구현
- 입력값 유효성 체크
- It must fail if addr is not page-aligned or if the range of pages mapped overlaps any existing set of mapped pages, including the stack or pages mapped at executable load time.
- 첫 번째 인자, 즉 가상주소 공간에서 저장할 주소값이 user 영역이어야 함
- 가상주소 공간의 주소값과
offset이 page 단위로 나누어 떨어져야 함
- length가 양의 정수여야 함
- 가상주소 공간에서 mmap할 영역이 비어 있어야 함 (다른 목적으로 이미 차지된 상태여서는 안됨)
- file descriptor table에서 fd로 파일 주소값 가져오기
- file을 reopen해야 할까?
- testcase: mmap-close 처리를 위해 필요!!!
- mmap이 lazy load 방식으로 구현되었기 때문에, mmap이 lazy하게 load되기 전에 file이 close되었을 경우 file을 load하지 못하는 상황이 생김
- 이를 처리하기 위해 새로 연 파일을 넘겨주어야 함
- (load_segment 에서도 동일한 처리가 필요할까??)
- do_mmap 실행
- ((
file.c
)) do_mmap
구현
load_segment()
참고
lazy_load_file()
추가로 구현 (lazy_load_segment
참고)
- read_byte에서 반드시 PGSIZE로 떨어지지 않는다는 점을 처리하는 데서 오래 걸림!
- lazy_load_segment 부분도 수정해주어야 맞을까? 일단 munmap 처리하고 진행해보자
- ((
file.c
)) do_munmap
구현
- mmap에서 리턴한 시작 주소값을 받아 할당된 범위 전체를 삭제해야 함
- 다음 페이지가 mmap으로 연결된 페이지인지 아닌지 어떻게 알 수 있을까?
- mmap된 페이지들을 관리하는 페이지 필요 --> mmap_list 구현 및 활용
- 연결된 페이지들을 하나씩 제거
- addr에서 PGSIZE 씩 더해주며 다음 페이지로 이동하여 처리
- 이 때 spt에서도 page를 제거해주어야 함 (otherwise 에러 발생!)
- vm_dealloc_page 대신 spt_remove_page를 사용!
- spt_remove_page에서는 hash_delete로 spt에서 page 제거 처리
- (lazy_load_segment에서도 spt_remove_page로 수정함)
- dirty page에 대한 처리 필요
- spt_remove_page에서 vm_dealloc_page를 콜하고, vm_dealloc_page에서 destroy(page)를 콜하므로, 관련 기능을 위해 file_backed_destroy을 구현해주어야 함
- ((
file.h
)) (뒤늦은 추가) struct file_page 구현
- munmap 등에서 page가 dirty 상태일 때, 다시 저장해주기 위해 file을 들고 있어야 함
- 파일에 저장할 위치를 파악하기 위해 offset도 들고 있어야 함
- ((
file.c
)) file_backed_destroy
구현
- 해당 page가 dirty 상태인지 pml4_is_dirty 를 활용해 확인
- dirty 상태일 경우, file, offset, 읽어왔던 size를 활용해 file_seek & file_write
- 이 때 궁금증은, write할 때의 size가 요청했던 크기(가령 PGSIZE)인지 아님 실제로 읽어들인 크기인지 여부 (일단은 실제로 읽어들인 크기로 설정)
- 열려 있던 파일을 여기서! 닫아주어야 함
CHECK POINT 7
- 상황
- swap testcase를 처리해야 하는 상황
- 개선 요구 사항
- When the system detects that it has run out of memory but receives a memory allocation request, it chooses a page to evict out to swap disk. Then, the exact state of the memory frame is copied to the disk. When a process tries to access a swapped out page, OS recovers the page by bringing the exact content back to the memory.
- 진행 과정
- ((
vm.c
)) vm_evict_frame 구현
- vm_get_victim이 잘 되었다는 전제 하에
- swap out 처리
- page type에 맞게 처리됨
- 이 때 swap disk가 꽉 찼을 수도 있음
- victim이 된 frame을 비워주어야 함
- 그렇지 않으면 process 간 침범이 발생할 수 있음
- memset 활용
- ((
vm.c
)) clock_lock 추가
- clock 알고리즘 실행 시 thread 간 race가 발생할 수 있어 lock으로 통제
- ((
vm.h
)) struct frame 업데이트
- 현재 page가 할당된 경우, 해당 thread를 저장하도록 멤버 추가
- vm_get_victim에서 접근되었는지 여부를 판단하기 위해, frame에 할당된 thread의 pml4가 필요함
- ((
vm.c
)) vm_get_victim 구현
- clock 알고리즘을 활용해 swap out할 frame을 하나 선택함
- ((
anon.c
)) vm_anon_init 업데이트
- 디스크에서 swap을 위한 영역 받아오기
swap_disk = dist_get(1, 1);
- 스왑 영역에 가용한 페이지 수 계산하기
- swap table 초기화 하기
- 특정 slot이 사용중인지 여부를 체크하는데, bitmap table이 유리 - anon.c에서만 접근할 것이므로 static 으로 설정
- ((
anon.h
)) anon_page에 swap_idx 멤버 추가
- swap out 되었을 때 swap table 상에서의 위치를 저장할 멤버 추가
- ((
anon.c
))
어느 순간에는 해야하는 것들
- ((
syscall.c
)) check_address()
수정
- syscall에서 요청된 주소값의 유효성을 체크
- NULL이 아니어야 함: uaddr == NULL
- user 영역이어야 함: !is_user_vaddr(uaddr)
- 할당된 영역이어야 함: pml4_get_page(curr->pml4, uaddr) == NULL
- lazy_load 방식으로 변경되었기 때문에 세 번째 조건 체크 방식을 수정
- vm_try_handle_fault 부분 참고
- 그냥 page_fault를 띄워버릴 순 없을까??
- exception 상황에서 다시 exception을 때릴 순 없을 것
- 대신 kernel mode에서 접근하려 할 때 page fault가 날 것
마지막 질문들
- 왜 mmap할 때 file에서 offset이 PGSIZE 단위가 되어야 하지?
- testcase 중 mmap-bad-off 에서 offset이 0x1234
- 처음 예상으로는, 파일 크기보다 큰 offset 일 때를 말하는 줄 알았음
- 읽어온 값이 0일 때 처리되는 상황
- 즉, lazy_load가 발생한 이후에 처리되는 줄 알았음
- 그런데 testcase는 요청 시점에 바로 판단을 해버리더라
- 즉, 인자값만 보고 바로 판단하더라
- 그래서 고려한 가능성은 offset도 PGSIZE여야 했다는 것
- 그런데 왜??