이전 포스트에서는 프로젝트 3의 Memory Management부분에 대해 구현을 했습니다. 마지막 부분에 해당하는 vm_alloc_page_with_initializer라는 함수를 시작으로 나머지 Anonymous Page를 구현 시작해 보도록 하겠습니다.
✔️ Anonymous Page란?
- Anonymous 페이지는 파일 기반 페이지와 달리 이름이 지정된 파일 소스가 없기 때문에 Anonymous page라고 부릅니다.
- Anonymous 페이지는 스택 및 힙과 같은 실행 파일에 사용됩니다.
- Anonymous 페이지는 프로그램이 실행되는 동안 필요한 메모리 공간을 동적으로 할당하여 프로그램의 요구에 맞게 메모리를 사용할 수 있습니다.
- Anonymous 페이지는 가상 메모리 공간에서 실제 메모리로 데이터를 옮기거나 실제 메모리에서 가상 메모리로 데이터를 가져오는 데 사용됩니다.
구현
1️⃣ bool vm_alloc_page_with_initializer (enum vm_type type, void *va, bool writable, vm_initializer *init, void *aux)
- 주어진 타입의 초기화되지 않은 페이지를 생성합니다.
- uninit 페이지의 swap_in 핸들러는 유형에 따라 페이지를 자동으로 초기화하고, 주어진 AUX로 INIT를 호출합니다.
- 페이지 구조가 완성되면 프로세스의 보조 페이지 테이블에 페이지를 삽입합니다.
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;
// upage가 이미 사용 중인지 확인합니다.
if (spt_find_page (spt, upage) == NULL) {
// 새로운 페이지 구조체를 동적으로 할당합니다.
struct page *page = malloc(sizeof(struct page));
if (page == NULL)
goto err;
// 가상 메모리 타입에 따라 초기화자 함수 포인터를 설정합니다.
int ty = VM_TYPE (type);
bool (*initializer)(struct page *, enum vm_type, void *);
switch(ty){
case VM_ANON:
initializer = anon_initializer;
break;
case VM_FILE:
initializer = file_backed_initializer;
break;
}
// uninit_new 함수를 호출하여 페이지를 초기화합니다.
uninit_new(page, upage, init, type, aux, initializer);
// 페이지를 보조 페이지 테이블에 삽입합니다.
if (!spt_insert_page(spt, page)) {
free(page);
goto err;
}
return true;
}
err:
return false;
}
2️⃣ 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
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) {
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
struct file_loader *file_loader = malloc(sizeof(struct file_loader));
file_loader->page_read_bytes = page_read_bytes; // 페이지에 실제로 읽어들일(read_bytes) 바이트 수
file_loader->page_zero_bytes = page_zero_bytes; // 페이지에 0으로 초기화할 바이트 수
file_loader->ofs = ofs; // 파일 오프셋(ofs)
file_loader->file = file; // 파일 포인터(file)
// 익명 페이지(VM_ANON)를 할당하기 위해 vm_alloc_page_with_initializer 함수 호출
// 할당된 페이지에는 지연 로딩(lazy loading)을 위해 lazy_load_segment 함수를 호출하여 파일 로더(file_loader)를 전달함
if (!vm_alloc_page_with_initializer (VM_ANON, upage,
writable, lazy_load_segment, file_loader)){
free(file_loader); // 페이지 할당에 실패한 경우 할당된 자원을 해제
return false; // 세그먼트 로딩 실패를 나타내는 false 반환
}
read_bytes -= page_read_bytes; // 읽어들인 바이트 수 갱신
zero_bytes -= page_zero_bytes; // 초기화할 바이트 수 갱신
upage += PGSIZE; // 페이지 주소(upage) 갱신
ofs += page_read_bytes; // 오프셋(ofs) 갱신
}
return true; // 세그먼트 로딩 성공을 나타내는 true 반환
}
아래의 lazy_load_segment함수를 구현하면 알겠지만 load_segment를 하던 중에 vm_alloc_page_with_initializer함수를 실행하게 되면서 aux값을 뭘 넘겨줘야 할지에 대해 고민을 많이 했었는데, 결국 파일에 대한 내용을 보내줘야 하니 파일의 정보를 담을 구조체하나를 만들어서 구조체를 넘겨주는 식으로 구현을 했습니다.
이때 aux를 구조체로 넘겨주고나서 구현이 다 끝났는지 알고 테스트 체크를 할 때 ofs을 갱신 안 해준 상태로 테스트를 하니 테스트가 다 fail 뜨는 것을 확인할 수 있었습니다. 이 부분을 찾으려고 주소를 다 printf로 찍어보는 과정을 통해 겨우 찾아낼 수 있었습니다.
이와 관련된 Trouble shooting은 주간 발표 때 정리해 둔 github discussion링크를 통해 확인할 수 있습니다.(링크는 포스트 맨 아래)
3️⃣ static bool lazy_load_segment (struct page *page, void *aux)
- 이 함수는 실행 파일의 페이지에 대한 이니셜라이저이며 페이지 오류가 발생할 때 호출됩니다.
- 페이지 구조체와 aux를(file_loader) 인자로 받습니다.
- 이 정보를 사용하여 세그먼트를 읽을 파일을 찾아서 결국 세그먼트를 메모리로 읽어야 합니다.
static bool
lazy_load_segment (struct page *page, void *aux) {
struct file_loader *file_loader = (struct file_loader*)aux;
struct file *file = file_loader->file;
off_t ofs = file_loader->ofs;
uint8_t *upage = page->va;
uint32_t page_read_bytes = file_loader->page_read_bytes;
uint32_t page_zero_bytes = file_loader->page_zero_bytes;
file_seek(file, ofs); // 파일을 오프셋(ofs) 위치로 이동
if (file_read(file, page->frame->kva, page_read_bytes) != (int)page_read_bytes) { // 파일에서 페이지로 데이터 읽기
palloc_free_page(page->frame->kva); // 페이지 할당이 실패한 경우 할당된 자원을 해제
return false;
}
memset(page->frame->kva + page_read_bytes, 0, page_zero_bytes); // 읽어들인 바이트 이후의 메모리를 0으로 초기화
memcpy(page->va, page->frame->kva, PGSIZE); // 페이지의 가상 주소에 로드된 데이터 복사
return true;
}
4️⃣ static bool setup_stack(struct intr_frame *if_)
- 로드 시점에 명령줄 인수를 사용하여 할당하고 초기화할 수 있으므로 결함이 발생할 때까지 기다릴 필요가 없습니다.
- 스택을 식별하는 방법을 제공해야 할 수도 있습니다.
- vm_ 유형에 있는 보조 마커(예: VM_MARKER_0)를 사용하여 페이지를 표시할 수 있습니다.
static bool setup_stack(struct intr_frame *if_) {
bool success = false;
void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);
// 페이지를 할당하여 스택의 최하단에 해당하는 메모리 영역을 확보
// 페이지 타입은 익명 페이지(VM_ANON)와 표시 페이지(VM_MARKER_0)를 OR 연산하여 사용
// 스택 페이지는 쓰기 가능(writable)한 속성을 갖도록 설정
if (!vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, writable)) {
return false;
}
// 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 스택 페이지를 등록
// 스택 페이지를 주소 공간에 실제로 차지한 것으로 표시
if (!vm_claim_page(stack_bottom)) {
vm_dealloc_page(stack_bottom);
return false;
}
if_->rsp = USER_STACK; // 스택 포인터(if_->rsp)를 USER_STACK으로 설정하여 스택 시작 지점을 가리킴
success = true;
return success;
}
5️⃣ bool supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src)
- supplemental page table src에서 dst로 복사합니다.
- 자식이 부모의 실행 컨텍스트를 상속해야 할 때 사용됩니다(예: 포크()). src의 보충 페이지 테이블에 있는 각 페이지를 반복하고 dst의 보충 페이지 테이블에 있는 항목의 정확한 복사본을 만듭니다.
- uninit 페이지를 할당하고 즉시 클레임해야 합니다.
bool supplemental_page_table_copy(struct supplemental_page_table *dst, struct supplemental_page_table *src) {
struct hash_iterator i;
struct hash *src_hash = &src->pages;
struct hash *dst_hash = &dst->pages;
hash_first(&i, src_hash);
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;
if (type == VM_UNINIT) { // 초기화되지 않은 페이지(VM_UNINIT)인 경우
struct uninit_page *uninit_page = &src_page->uninit;
struct file_loader* file_loader = (struct file_loader*)uninit_page->aux;
// 새로운 파일 로더(new_file_loader)를 할당하고 기존의 파일 로더 정보를 복사
struct file_loader* new_file_loader = malloc(sizeof(struct file_loader));
memcpy(new_file_loader, uninit_page->aux, sizeof(struct file_loader));
new_file_loader->file = file_duplicate(file_loader->file); // 파일을 복제하여 새로운 파일 포인터를 생성
// 초기화할 페이지에 신규 파일 로더를 이용하여 초기화할 페이지 할당
vm_alloc_page_with_initializer(uninit_page->type, src_page->va, true, uninit_page->init, new_file_loader);
vm_claim_page(src_page->va); // 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 페이지 등록
} else { // 초기화된 페이지인 경우
vm_alloc_page(src_page->operations->type, src_page->va, true); // 페이지 할당
vm_claim_page(src_page->va); // 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 페이지 등록
memcpy(src_page->va, src_page->frame->kva, PGSIZE); // 페이지의 가상 주소에 초기화된 데이터 복사
}
}
return true;
}
6️⃣ void supplemental_page_table_kill (struct supplemental_page_table *spt)
- 보조 페이지 테이블이 보유하고 있던 모든 리소스를 해제합니다.
- 이 함수는 프로세스가 종료될 때 호출됩니다.
- 페이지 항목을 반복하고 테이블의 페이지에 대해 destroy(page)를 호출해야 합니다.
- 이 함수에서 실제 페이지 테이블(pml4)과 물리적 메모리(팔로잉된 메모리)는 걱정할 필요가 없습니다.
void supplemental_page_table_kill(struct supplemental_page_table *spt) {
// 보조 페이지 테이블의 모든 엔트리를 제거하고, 관련 자원을 해제하는 함수입니다.
// hash_action_destroy 콜백 함수를 사용하여 각 엔트리의 자원을 해제
hash_clear(&spt->pages, hash_action_destroy);
}
void hash_action_destroy(struct hash_elem* hash_elem_, void *aux) {
// 해시 테이블 엔트리에 대해 수행되는 콜백 함수입니다.
// 엔트리에 대한 자원을 해제하고, 페이지를 제거합니다.
struct page* page = hash_entry(hash_elem_, struct page, hash_elem);
if (page != NULL) {
destroy(page); // 페이지에 할당된 자원을 해제하는 함수 호출
free(page); // 페이지 자체의 동적 메모리 해제
}
}
위에서 보조 페이지 테이블을 제거하는 함수인 supplemental_page_table_kill을 구현할 때 hash_clear라는 함수를 사용해서 bucket에 있는 모든 엔트리를 제거합니다. hash_clear를 할 때 콜백 함수로 hash_action_destroy라는 함수를 만들어서 사용합니다.
hash_action_destroy는 요구사항에 맞게 destroy(page)를 해서 페이지에 할당된 자원을 해제하고 마지막으로 페이지의 동적 메모리 할당된 것을 해제하기 위해 free(page)를 사용합니다.
위의 함수들을 다 구현하면 아래 사진과 같이 프로젝트 2에서 구현한 대부분의 테스트를 통과한 것을 확인할 수 있습니다.
이번 Anonymous Page를 구현하면서 정리한 개념과 관련 트러블 슈팅에 대해서 적어놓은 github discussion 주소입니다.
관련 내용과 구현된 코드를 확인해볼 수 있습니다.
https://github.com/Blue-club/pintos2_Team3/discussions/8