이제 지연 로딩의 바탕이 되는 uninit, 미초기화 페이지를 구현해보자
미초기화 페이지가 뭔지 아직도 모른다면 https://gooch123.tistory.com/25 여기를 참고하자
미초기화 페이지를 위해 구현해야할 함수는 다음과 같다
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 lazy_load_segment (struct page *page, void *aux);
bool vm_alloc_page_with_initializer (enum vm_type type, void *va,
bool writable, vm_initializer *init, void *aux);
static bool uninit_initialize (struct page *page, void *kva);
void vm_anon_init (void);
bool anon_initializer (struct page *page, enum vm_type type, void *kva);
하나씩 어떻게 구현되었는지 알아보자
함수 구현하기
함수 구현 이전에, 위 함수들을 구현하면 어느 영역의 어떤 페이지들이 미초기화 페이지로 만들어지는지 확인해보자

위 그림은 pintOS 메모리 구조를 나타낸 것이다. pintOS는 x86-64 위에서 작동하는 OS이기에 호스트의 물리 주소를 알 수가 없다
대신 커널 가상 주소를 물리 주소와 일대일 매핑시켜 사용한다고 한다. 그러니까 그림 중간의 굵은 선 (kern_base)은 사실 모두 물리 주소처럼 동작한다는 것이다. kern_base 밑의 영역은 이제 유저 프로세스의 영역, 즉 가상 메모리 영역이다.
pintOS에서는 유저 프로그램에게 동적 할당이 제공되지 않기에 heap 영역은 없다.
load_segment
이 함수는 메모리 구조에서 가상 메모리 영약의 코드 세그먼트나 bss 같은 세그먼트 영역을 미초기화 페이지로 예약해주는 함수이다.
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. */
struct lazy_load_info *aux = malloc(sizeof(struct lazy_load_info)); // 전달해야할 인자
if (aux == NULL)
return false;
aux->file = file_reopen(file);
aux->offset = ofs;
aux->readbyte = page_read_bytes;
aux->zerobyte = page_zero_bytes;
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;
ofs += page_read_bytes;
upage += PGSIZE;
}
return true;
}
인자로 총 6개를 받는다
- file : 예약할 데이터를 가진 파일
- ofs : 파일의 어느 오프셋부터 시작할지
- upage : 가상 페이지의 주소
- read_byte : 파일에서 읽어올 바이트 크기
- zero_byte : padding 바이트 크기
- writable : 해당 페이지가 쓰기 권한이 있는지
while 루프를 돌면서 읽어야 할 영역이 크다면, 여러 개의 페이지를 만들도록 하고 있다. 여기서 특히 잘 봐야할 점은 aux에 lazy_load_info를 만들어서 넘겨준다는 것
lazy_load_info는 이 미초기화 페이지에 처음으로 접근해서 페이지 폴트가 발생했을 때 어디에서 어떻게 얼마만큼 데이터를 가져와야 하는지에 대한 메타데이터를 저장해두는 구조체다. lazy_load_segment에서는 이 구조체를 사용하여 로드를 완료할 것이다.
struct lazy_load_info
{
struct file *file;
off_t offset;
size_t readbyte;
size_t zerobyte;
};
사실 load_segment에서 받는 인자들은 바로 로드에 사용되는 것이 아니라 지연 로딩을 위한 메타데이터일 뿐이라는 것 !!
이거 완전 씽크빅이네
vm_alloc_page_with_initializer
우리는 미초기화 페이지를 생성할 때 반드시 이 함수를 사용해서 생성해야 한다.
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;
/* 이미 해당 page가 SPT에 존재하는지 확인합니다 */
if (spt_find_page(spt, upage) == NULL)
{
/* TODO: VM 타입에 따라 페이지를 생성하고, 초기화 함수를 가져온 뒤,
* TODO: uninit_new를 호출하여 "uninit" 페이지 구조체를 생성하세요.
* TODO: uninit_new 호출 후에는 필요한 필드를 수정해야 합니다. */
bool (*page_initializer)(struct page *, enum vm_type, void *kva);
struct page *page = malloc(sizeof(struct page));
if (page == NULL)
{
goto err;
}
switch (VM_TYPE(type))
{
case VM_ANON:
page_initializer = anon_initializer;
break;
case VM_MMAP:
case VM_FILE:
page_initializer = file_backed_initializer;
break;
default:
free(page);
goto err;
break;
}
uninit_new(page, upage, init, type, aux, page_initializer);
page->writable = writable;
/* TODO: 생성한 페이지를 spt에 삽입하세요. */
if (!spt_insert_page(spt, page))
{
// 실패 시 메모리 누수 방지 위해 free
free(page);
// 실패 했으니까 에러로 가야겠지?
goto err;
}
return true;
}
err:
return false;
}
여기서는 인자로 5개를 받는다
- type : 미초기화 페이지가 초기화될 때 어느 페이지로 초기화될지 타입을 정한다
- upage : 어느 가상 주소 영역에 이 페이지가 할당될지 정한다
- writable : 이 페이지가 쓰기가 가능한지 나타낸다
- init : 지연 로딩시에 실제 데이터를 적재할 함수
- aux : 페이지가 초기화 될 때 필요한 메타데이터나 다른 부가 정보를 넘겨받는다
먼저 if 문으로 해당 가상 영역(upage)에 이미 할당된 페이지가 있는지 확인한다. 왜? 이미 있는 영역에 덮어씌워지면 안되니까
여기서 우린 page_initializer도 만들어야 한다. page_initializer는 이 페이지가 지연 로딩될 때 어떤 페이지로 초기화될지에 대한 로직을 담은 함수 포인터이다.
그럼 page_initializer와 인자로 받은 vm_initializer가 무엇이 다른가?
page_initializer는 단지 이 미초기화 페이지가 익명 페이지나 파일 페이지로 초기화되도록 해주는 것 뿐이고, 이 페이지에 매핑된 물리 주소에 실제 물리 데이터를 적재해주는 것은 vm_initializer가 해야한다.
switch 문에서 page를 동적 할당해주고, 넘겨받은 타입에 따라 page_initializer에 필요한 초기화 함수를 담아준다
그리고 uninit_new를 통해 실제 미초기화 페이지를 만들어주는데, 이 함수는 절대 수정하지 말라고 주석에 적혀있으니 딱히 건드리지는 말자.
그리고 page의 writable을 설정해준다. 여기서 위치가 중요한데, uninit_new 보다 먼저 page의 writable을 설정하면 uninit_new를 하면서 writable이 쓰레기 값이 되어버린다. uninit_new가 내부적으로 page의 값 자체를 덮어씌워주는 함수라서 그런 것 같다.
그 다음, 만들어진 미초기화 페이지를 spt_insert_page를 통해 SPT에 삽입한다.
lazy_load_segment
이 함수는 load_segment에서 vm_alloc_page_with_initializer를 호출할 때 인자로 넘겨주었던 함수이다.
미초기화 페이지에 맨 처음 접근해서 페이지 폴트가 발생했을 때 여러 단계를 거쳐 이 lazy_load_segment가 호출되게 된다.
어느 과정을 거쳐 호출되는지 모르겠다고? 그럼 여기를 보자 https://gooch123.tistory.com/25#4
bool lazy_load_segment(struct page *page, void *aux)
{
/* TODO: Load the segment from the file */
/* TODO: 이 함수는 해당 VA(가상 주소)에서 첫 페이지 폴트가 발생할 때 호출됩니다. */
/* TODO: 이 함수를 호출할 때 VA는 사용할 수 있습니다. */
struct lazy_load_info *lazy_info = (struct lazy_load_info *)aux;
struct file *read_file = lazy_info->file;
off_t my_read_byte = file_read_at(read_file, page->frame->kva, lazy_info->readbyte, lazy_info->offset);
if (my_read_byte != (off_t)lazy_info->readbyte)
{
return false;
}
memset(page->frame->kva + lazy_info->readbyte, 0, lazy_info->zerobyte);
free(lazy_info);
return true;
}
여러 단계를 거쳐 넘겨받은 aux를 통해 파일에서 데이터를 읽어 매핑된 물리 주소에 write 해야 한다.
bogus 폴트로 인해 물리 프레임 공간을 할당받았으므로 프레임의 kva가 이 페이지에 매핑된 물리 주소이다.
file_read_at을 통해 실제 파일에서 데이터를 읽어 넘겨받은 물리 주소에 write 할 수 있다. 그리고 실제로 읽은 바이트 크기를 반환하는데 이를 통해 실패하지 않고 읽었는지 검사할수 있다.
그리고 마지막으로 padding 값이 있을 수 있으니 memset으로 0으로 만들어주자.
그리고 남은건 vm_anon_init, uninit_initialize, anon_initializer인데, 이 부분은 지금 딱히 추가하거나 수정할 부분이 없으니 넘어가자 !!
아 그리고 한가지 더
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 = spt_find_page(spt, addr);
if (page == NULL)
return false; // 찐 폴트
ASSERT(page->operations != NULL && page->operations->swap_in != NULL);
return vm_do_claim_page(page);
}
이 함수는 exception.c의 page_fault 에서 호출되는 함수이다. 여기서 spt_find_page를 통해 해당 페이지가 예약되어있는지 확인하고 vm_do_claim_page로 초기화를 진행해주자
포스팅 끝 포스팅 재미없엉 핀토스 코딩이 더 재밌엉
'크래프톤 정글' 카테고리의 다른 글
| [pintOS] vm 트러블슈팅 (1) | 2025.06.04 |
|---|---|
| [pintOS] stack_growth 구현기 (0) | 2025.06.03 |
| [pintOS] uninit 페이지 초기화 흐름 (0) | 2025.05.26 |
| [pintOS] frame와 page, SPT와 프레임 테이블 (0) | 2025.05.26 |
| [pintOS] dup2 구현기 (0) | 2025.05.24 |