VM.. 아직 구현을 시작하지도 않았고 과제 설명서를 읽으면서 흐름을 따라가는 중인데 하... 진짜 엄청 어렵다
다음주에 제대로 구현을 완성할지 확신이 서지 않는다
일단 정리한 흐름을 포스팅 해두겠다
process.c를 자세히 보면 거의 끝 줄에서 #ifndef VM으로 둘러싸인 코드가 있을 것이다
VM을 시작하기 전에 사용하는 코드인데, 요구 페이징 기법을 사용하지 않고 바로 물리 메모리를 할당받아 사용하고 있다.
VM이 활성화되면 else 문의 코드가 활성화되어 기존의 코드들이 사용되지 않는다. else 문 내의 함수는 다음 3개이다.
static bool
lazy_load_segment(struct page *page, void *aux);
static bool
load_segment(struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable);
static bool
setup_stack(struct intr_frame *if_);
load_segment와 setup_stack은 기존에도 사용하던 함수들이다. 이 함수들이 대체되는 것이다. 이 함수들이 어떻게 대체되나를 보기전에, uninit 페이지가 무엇인지 알아보자
요구 페이징 기법은 가상 페이지만 할당해놓고, 이에 해당하는 물리 페이지를 미리 적재해두지 않고 해당 가상 페이지에 접근해서 페이지 폴트가 발생하면 그제서야 물리 페이지를 할당해주는 기법이다.
이 기법에서 가상 주소는 있지만, 해당하는 물리 주소에는 실제 데이터는 없는 페이지를 uninit, 미초기화 페이지라고 한다
그럼 페이지 폴트가 발생하면 어떻게 알맞은 물리 페이지를 적재해주는걸까? 어딘가에 그 정보를 저장해둬야 하는거 아닐까? 그 정보를 저장해두는 것이 page 구조체이고, page 구조체를 관리하는 테이블이 SPT라고 이전에 포스팅했다.
https://gooch123.tistory.com/24#1
[pintOS] frame와 page, SPT와 프레임 테이블
vm을 미리 예습하면서 헷갈렸던 개념들이 여럿 있어서 정리해둔다먼저, 우리의 핀토스는 qemu라는 에뮬레이터 위에서 작동한다따라서 작동할 때 사용할 최대 메모리 크기를 고정하고, 파일 기반
gooch123.tistory.com
페이지 폴트가 발생하면, SPT를 뒤져서 해당 가상 주소에 해당하는 가상 페이지가 존재하는지 확인하고, 존재한다면 그 페이지의 메타데이터를 담은 page 구조체를 가져와서 물리 프레임을 메모리에 적재하고 매핑해준다.
그럼 우리의 pintos 코드에서는 어떻게 이런 흐름을 가져가는걸까?
우리의 유저 실행 파일은 프로세스에 load되는 과정에서 load_segement 함수를 사용했다. vm에서는 이 load_segment가 바뀐다. 어떻게 바뀌는지 한번 보자
흐름 따라가기
load_segment
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)
{
/* 이 페이지를 어떻게 채울지 계산합니다.
* FILE에서 PAGE_READ_BYTES 바이트를 읽고
* 남은 PAGE_ZERO_BYTES 바이트는 0으로 초기화합니다. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE; // 4KB까지만 읽어라
size_t page_zero_bytes = PGSIZE - page_read_bytes;
// 0 패딩 사이즈는 4KB - read_byte
/* TODO: Set up aux to pass information to the lazy_load_segment. */
void *aux = NULL; // 전달해야할 인자
if (!vm_alloc_page_with_initializer(VM_ANON, upage,
writable, lazy_load_segment, aux))
return false;
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
}
return true;
}
인자가 총 다섯개 있다. 확인해보자
먼저 가상 페이지에 담을 파일 포인터(file)와 파일 오프셋(ofs), 그리고 해당 가상 페이지를 적재할 가상 주소(upage)가 있다
그리고 해당 페이지에서 실제 읽어야할 바이트(read_bytes), 그리고 0으로 패딩해줘야 할 바이트(zero_byte)가 있다. 왜 0으로 패딩이 필요하냐면 페이지 단위로 적재해야 하기에 항상 PGSIZE(4KB)를 맞춰줘야 하는데 파일을 여러 페이지로 나누었을 때 마지막 페이지는 4KB보다 작은 크기일 수 있기에 0으로 패딩해주는 것이다
load_segment를 계속 보면 while 루프를 통해 가상 메모리에 할당해줘야할 남은 파일 크기가 페이지 크기보다 크면은 계속 루프를 돌며 vm_alloc_page_with_initializer 를 호출하는 것을 볼 수 있다.
그럼 vm_alloc_page_with_initializer 는 무슨 역할을 하는 함수일까?
vm_alloc_page_with_initializer
vm.c의 이 함수에는 다음과 같은 주석이 있다
대기 중인 페이지 객체라 함은 바로 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: VM 타입에 따라 페이지를 생성하고, 초기화 함수를 가져온 뒤,
* TODO: uninit_new를 호출하여 "uninit" 페이지 구조체를 생성하세요.
* TODO: uninit_new 호출 후에는 필요한 필드를 수정해야 합니다. */
/* TODO: 생성한 페이지를 spt에 삽입하세요. */
}
err:
return false;
}
지금은 뼈대 코드만 존재하고 있다. SPT에서 해당 가상 주소를 가지는 페이지 구조체가 있는지 확인하고, 없다면 다음 로직을 수행해야 한다
VM 타입에 따라 페이지를 생성하라고 하는데, VM 타입에는 uninit 페이지 이외에 file_backed_page와 anon_page가 있다
file_backed_page는 파일을 기반으로 생성된 파일, 예를 들어 file_open을 통해 가져온 페이지를 말하고, anon_page는 익명 페이지 즉, 지속적으로 파일과 연결될 필요가 없는 페이지를 말한다. 이것도 예를 들면 스택 영역이나 힙 영역에 할당되는 페이지나, 딱 한번만 파일에서 읽히고 연결관계가 없어지는 elf 섹션 페이지를 말하는 것이다. 이들은 런타임 중에 CPU가 필요에 따라 할당받는 페이지일 뿐이지, 파일에 고정되어있는 페이지 영역이 아니다
어쨌든 여기서 uninit_new를 사용하면 미초기화 페이지를 만들 수 있다고 한다. 이 함수도 따라가 보면 좋겠지만 흐름을 따라갈 때 너무 구체적으로 파고들면 시간만 오래 걸리고 이해하기도 어렵다. 여기서 uninit_new를 통해 미초기화 페이지를 만들고 이 미초기화 페이지에 매핑되어야 할 물리 프레임의 정보를 저장하고 이를 SPT에 등록해둔다고 추상화하자
그러면 이제 SPT에 등록된 미초기화 페이지들은 어떻게 사용되는걸까?
페이지 폴트가 발생하면 그제서야 초기화해주고, 그 프레임을 가져온다고 했다. 따라가보자
page_fault
이 함수는 exception.c 내에 존재하는 페이지 폴트 발생시 호출되는 함수이다
#ifdef VM
/* For project 3 and later. */
if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
return;
#endif
이 함수 내에서는 VM 프로젝트에서만 작동하는 코드가 있다. 그럼 저 함수를 또 따라가보자
vm_try_handle_fault
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;
struct page *page = NULL;
/* TODO: Validate the fault */
/* TODO: Your code goes here */
return vm_do_claim_page(page);
}
주석을 보니 이 함수에서 해당 페이지 폴트가 어떤 페이지 폴트인지 확인해야하는 것 같다. 예를 들어 이게 미초기화된 페이지에 접근해서 생긴 페이지 폴트인지, 아니면 정말로 아무것도 없는 주소에 접근해서 발생한 페이지 폴트인지 !!
사실 더해서 추가 스택 영역에 접근해서 할당받아야하는 페이지 폴트도 있긴 한데, 이것은 나중에 다뤄보자
아마 로직을 생각해보면 SPT에서 메타데이터 페이지를 찾아서 그 페이지를 vm_do_claim_page에 넘겨주면 되는 듯 하다. 만약 SPT에 없다면 진짜 페이지 폴트라 false 반환하겠지
vm_do_claim_page
static bool
vm_do_claim_page(struct page *page)
{
struct frame *frame = vm_get_frame();
/* Set links */
frame->page = page;
page->frame = frame;
/* TODO: Insert page table entry to map page's VA to frame's PA. */
return swap_in(page, frame->kva);
}
여기서는 vm_get_frame을 통해 실제 물리 프레임을 할당받고, frame과 page를 서로 일대일 매핑시켜주고 있다. TODO로는 해당 가상 주소를 물리 주소에 연결시켜주라고 하는데, 이는 mmu.c의 pml4_set_page 함수가 하는 역할이니 지금은 깊게 찾아보지 않겠다. 나중에 구현기에서 다~ 다루겠다.
그런데 뜬금없이 swap_in이 나왔다!! 이건 대체 뭐지? 이건 매크로인데, 이것도 깊게 들어가면 한참 걸리니까 가볍게 가자
page 구조체 다시보기
struct page {
const struct page_operations *operations;
void *va; /* Address in terms of user space */
struct frame *frame; /* Back reference for frame */
/* Your implementation */
/* 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
};
};
여기의 page_operation 구조체를 따라가보자
struct page_operations {
bool (*swap_in) (struct page *, void *);
bool (*swap_out) (struct page *);
void (*destroy) (struct page *);
enum vm_type type;
};
#define swap_in(page, v) (page)->operations->swap_in ((page), v)
#define swap_out(page) (page)->operations->swap_out (page)
#define destroy(page) \
if ((page)->operations->destroy) (page)->operations->destroy (page)
위 swap_in 매크로는 page 구조체 내의 page_operation 구조체 내의 swap_in을 실행시켜주는거라고 보면 되겠다
그럼 swap_in에는 무엇이 있을까?
여기서 우리는 C로 객체지향을 유사하게 구현할 수 있다는 것을 알고 가야한다
어떻게 가능하냐고? 그건 다음 포스팅에서...
아까 페이지의 종류에는 uninit, anon, file이 있다고 했다. 각각의 페이지마다 호출되는 함수가 다르도록 만들 수 있다고 머리에 박아두자!! 엥 이거 완전 다형성아니냐
uninit 페이지에서 swap_in을 사용하면 어떤 함수가 불릴까?
static const struct page_operations uninit_ops = {
.swap_in = uninit_initialize,
.swap_out = NULL,
.destroy = uninit_destroy,
.type = VM_UNINIT,
};
문법이 이해 안되더라도 그냥 넘어가자
이 구현체를 갖고 있는 page의 swap_in 호출은 uninit_initialize 함수를 호출하게 된다고 이해하면 된다
void
uninit_new (struct page *page, void *va, vm_initializer *init,
enum vm_type type, void *aux,
bool (*initializer)(struct page *, enum vm_type, void *)) {
ASSERT (page != NULL);
*page = (struct page) {
.operations = &uninit_ops,
...
}
여기서 미초기화 페이지로 사용될 page 구조체의 operation에 위 operation 구조체를 넣어준다. uninit.c를 보면 더 이해가 잘 될 것이다. 하여튼 미초기화 페이지에서 swap_in을 하게 되면 uninit_initialize 를 호출하게 되는데, 이는 무슨 함수인가?
static bool
uninit_initialize (struct page *page, void *kva) {
struct uninit_page *uninit = &page->uninit;
/* Fetch first, page_initialize may overwrite the values */
vm_initializer *init = uninit->init;
void *aux = uninit->aux;
/* TODO: You may need to fix this function. */
return uninit->page_initializer (page, uninit->type, kva) &&
(init ? init (page, aux) : true);
}
미초기화 페이지 필드의 init 함수와 page_initialize 함수를 같이 호출해서 둘 다 성공해서 true를 반환하게 된다.
아니 page_initialize는 대체 뭐고 init는 또 뭐야??
천천히 살펴보자... page_initialize는 이 미초기화 페이지가 초기화될때 어떤 페이지 유형이 될지 정하는 초기화 함수다. 그러니까 이 미초기화 페이지에 접근해서 페이지 폴트가 발생하면, 이 페이지는 실제로 file_backed 유형 또는 anon 유형이 되야 할 터다.
그럼 이러한 초기화 함수는 언제 넣어주는거지??
이 부분은 uninit_new에서 인자로 받은 VM_TYPE에 따라 다른 초기화 함수를 넣어준다고 이해하고 넘어가자
그니까 여기서 다른 페이지 유형으로 초기화된다는 거구나~~~??? 하고 넘어가자
init 함수는 또 어디서 넣어주는 건가??
아까 vm_alloc_page_with_initializer에서 uninit_new 함수를 호출해서 페이지를 생성하라고 했다
vm_alloc_page_with_initializer는 load_segment에서 호출되었고, 이 때의 코드를 다시 유심히 봐야한다
if (!vm_alloc_page_with_initializer(VM_ANON, upage, writable, lazy_load_segment, aux))
어? 자세히 보니 init 인자로 lazy_load_segment라는 함수 포인터를 인자로 주네?
그럼 미초기화 페이지에 접근해서 페이지 폴트가 발생하면 lazy_load_segment 함수를 호출하는거겠네?
그럼 여기로.. 아이고 힘들다 헥헥 또 따라가보자
static bool
lazy_load_segment(struct page *page, void *aux)
{
/* TODO: Load the segment from the file */
/* TODO: 이 함수는 해당 VA(가상 주소)에서 첫 페이지 폴트가 발생할 때 호출됩니다. */
/* TODO: 이 함수를 호출할 때 VA는 사용할 수 있습니다. */
}
이제 마지막 종착지다. 아까 프레임 자체는 vm_do_claim_page에서 할당받았다. 하지만 그 프레임은 빈 공간이었다. 그 프레임을 여기서 채워주면 될 것이다 !!
어떻게 채워주냐고? AUX에서 매핑 정보를 담아두면 될 것 같다. 예를 들면 파일 포인터, 파일 오프셋 등등..
정말 긴 흐름이었다. 사실 뭉뚱그려서 설명한 부분도 있고, 내가 아직 제대로 이해하지 못하는 부분이 있을 수도 있다. 이는 계속 공부하면서 계속 업데이트하겠다...
'크래프톤 정글' 카테고리의 다른 글
| [pintOS] stack_growth 구현기 (0) | 2025.06.03 |
|---|---|
| [pintOS] uninit 페이지 구현기 (0) | 2025.06.03 |
| [pintOS] frame와 page, SPT와 프레임 테이블 (0) | 2025.05.26 |
| [pintOS] dup2 구현기 (0) | 2025.05.24 |
| [pintOS] 과제 2 rox 테스트 구현 (0) | 2025.05.21 |