- vm_do_claim_page : 프레임이 실질적으로 요구되는 때
- vm_get_frame : 프레임이 없으면 얻어와야해
- vm_evict_frame : 가져온 프레임에 swap_out 하기
- vm_get_victim : Clock Algorithm을 통한 특정한 페이지 얻기
- 객체지향 식 initialize, swap in out, destroy
- swap in : vm_do_claim_page에서 불리는 메모리 올리기 직전의 수행 내용
- swap out : 프레임에 있는 페이지가 잠시 빠져나가게 될 때 해야하는 행위들 집합
- Destroy : 진짜 다썼을 때 해야 할 것들
우리는 기억하고 싶은 것을 평생을 기억하겠다고 결심 했다가도 조금만 지나면 감정만이 남거나, 주관적 왜곡조차 없어진 기억을 겪는다. 망각은 축복이라고 한다. 이러한 축복은 사람만 있으면 된다. 컴퓨터는 까먹으면 안된다. 이와 연관된 메모리 관리 기법을 논해보도록 한다.
2025.06.09 - [구현하기] - PintOS P3 #6 : Stack Growth
PintOS P3 #6 : Stack Growth
Project 2는 4KB로 다 해먹을 수 있지만, 우리는 이제 1MB 까지 즉, 256배에 달하는 크기까지 늘어날 수 있는 운영체제만의 스택 용량을 기대한다. 이를 만드는 과정은 생각보다 간단한데, 이 글을 통
hyeonistic.tistory.com
스택 성장에 대해 논한 이전 글에서 이어진다.
우리는 페이지와 프레임에 대해 간략하게 다루었었다. 페이지는 본연의 물건이고 프레임은 그걸 담는 포장의 개념이다. 즉, 상태를 전환한다기 보다는 프레임은 페이지를 담고있냐 담고있지 않냐의 state만 논하는 것이 제일 합리적이다.
하지만 껍데기라는 의의의 프레임도 어느정도 공간을 차지 하기 때문에 쓸 수 있는 프레임 갯수는 한정되어있다. 즉, 무한히 늘릴 수 없고, 그 와중에 사용하기 위해서는 기존 프레임을 사용하겠다고 뻐팅기는 페이지들 중 제일 활용처가 적은 것을 가져와야한다.
음료 한 잔당 한시간동안 있을 수 있는 카페가 있는데 너무 인기가 많아서 제일 안시킨 사람 쫓아내기 하는 것이다. 이렇게 보니 이 카페는 너무 비인간적이긴 한 것 같다.
vm_do_claim_page : 프레임이 실질적으로 요구되는 때
static bool
vm_do_claim_page (struct page *page) {
ASSERT (page != NULL);
struct frame *frame = vm_get_frame ();
if (frame == NULL)
return false;
(...)
페이지의 실질적인 물리 메모리 적재를 위해 활용 되는 vm_do_claim_page는 실제 시작 전 frame이 필요하다.
기존 글에서는 frame을 무조건 가져온다고 전제했지만 테스트 케이스는 날이 갈수록 프레임을 다쓰고 당연한듯이 더 달라고 한다.
이제 vm_get_frame의 실제 내용을 논해보자.
vm_get_frame : 프레임이 없으면 얻어와야해
static struct frame* vm_get_frame (void) {
struct frame* new_frame = (struct frame *)malloc(sizeof(struct frame));
if(new_frame == NULL) {
PANIC("struct frame에 대한 malloc 실패");
}
void* new_page = palloc_get_page(PAL_USER);
new_frame->kva = new_page;
if (!new_frame->kva){
new_frame = vm_evict_frame();
new_frame->page = NULL;
return new_frame;
}
new_frame->page = NULL;
list_push_back(&g_frame_table, &new_frame->f_elem);
return new_frame;
공간적 여유가 있다면 !new_frame->kva 를 거치지 않고 그대로 프레임 목록에 추가되어서 이 함수가 끝난다.
하지만 우리는 해당 자리에 들어갈 new_page 가 이상이 있는 상황을 전제한다. 그럼으로써 우리는 vm_evict_frame()을 통해 얻어올 프레임과 쫓아낼 페이지를 찾아내자.
vm_evict_frame : 가져온 프레임에 swap_out 하기
static struct frame* vm_evict_frame (void) {
struct frame *victim = vm_get_victim ();
swap_out(victim->page);
return victim;
}
뭘 한다는거지? 사실 여기서 언급되는 swap_out은 엄청 대단한 것인데, 해당 page에 해당되는 고유의 swap_out 함수가 실행된다.
즉, C언어로만 구성된 PintOS에서 타입에 따라 구분 될 수 있는 실행 함수가 있다는 것은 객체지향 형태를 논하기도 했다는 것이다. 이에 대한 구현 기법은 그냥 그런가보다 하고 받아들이기로 하자. 우린 지금 프레임 찾기를 하고 있기 때문이다. TMI는 바로 아래 내용이 있다.
해당 page 고유의 swap_out 함수는 크게 두 가지이다 :
왜냐하면, frame에 올라갔다는것은 type 중 파일이 없는 anon, 파일을 가지고 있는 file 밖에 없다는 것이 된다. 즉, anon으로써의 swap_out, file으로써의 swap_out이 있는 것이다. 복잡하다. 어쨌든 이용 기록이 있었던 만큼 바꾼게 있었다면 저장해야하고, 그러한 작업들이 모두 이루어져야한다는 것이다. 이에 대해서는 다른 글에서 다룰 것이다..
vm_get_victim : Clock Algorithm을 통한 특정한 페이지 얻기
static struct frame * vm_get_victim (void) {
struct frame *victim = NULL;
struct thread *curr = thread_current();
struct list_elem *now = list_begin(&g_frame_table);
lock_acquire(&g_frame_lock);
for (; now != list_end(&g_frame_table); now = list_next(now)) {
victim = list_entry(now, struct frame, f_elem);
if (pml4_is_accessed(curr->pml4, victim->page->va)) {
pml4_set_accessed(curr->pml4, victim->page->va, 0);
} else {
lock_release(&g_frame_lock);
return victim;
}
}
for (; now != list_end(&g_frame_table); now = list_next(now)) {
victim = list_entry(now, struct frame, f_elem);
if (pml4_is_accessed(curr->pml4, victim->page->va)) {
pml4_set_accessed(curr->pml4, victim->page->va, 0);
} else {
lock_release(&g_frame_lock);
return victim;
}
}
lock_release(&g_frame_lock);
ASSERT(now != NULL);
return victim;
}
전체를 한 바퀴 돌면서 is_accessed가 false 인 것을 찾는다. 하나라도 찾으면 해당 프레임을 반환 할 것이고, 하나도 없으면 다음번 순회에서 첫번째로 걸리는 false인 아이템이 걸려 올라올 것이다.
물론 victim->page가 NULL 인경우도 생각해볼 수 있다. 그 부분은 직접 적합한 위치에 조건문을 작성해보길 기대한다.
객체지향 식 initialize, swap in out, destroy
vm_evict_frame에서 우리는 객체지향식의 내용을 다소 엿볼 수 있었다.
페이지 타입에 따른 swap_out 함수 내용의 근본적인 전환이라니 대단하지 않은가?
bool vm_alloc_page_with_initializer (enum vm_type type, void *upage, bool writable,
vm_initializer *init, void *aux) {
struct supplemental_page_table *spt = &thread_current ()->spt; //초기화 SPT에 등록(중복 방지)
if (spt_find_page (spt, upage) == NULL) { //SPT(해시 테이블)에서 VA로 페이지 찾기
// 새 페이지를 콜록
struct page *page = (struct page *)calloc(1, sizeof(struct page));
if (page == NULL)
goto err;
// 필드들 초기화
bool (*page_initializer)(struct page *, enum vm_type, void *);
switch (VM_TYPE(type)) {
case VM_ANON:
page_initializer = anon_initializer;
uninit_new(page, upage, init, type, aux, page_initializer);
break;
case VM_FILE:
page_initializer = file_backed_initializer;
uninit_new(page, upage, init, type, aux, page_initializer);
break;
default:
printf("정의되지 않은 VM_TYPE(type)!\n");
goto err;
}
page->writable = writable;
(......)
}
매개변수로 받아온 type로 인한 분기가 두개 생기고, page_initializer 라는 이런 형태도 있나 싶은 변수 초기화 문장도 보인다. 그리고 얼핏 보기엔 같은 함수 uninit_new를 쓰는 것 같기도 하지만, 이것은 철저하게 완전히 다른 페이지를 생성하게 된다. 각 page_initializer를 가리키는 내용들을 보자.
bool anon_initializer(struct page *page, enum vm_type type, void *kva)
{
page->operations = &anon_ops;
struct anon_page *anon_page = &page->anon;
anon_page->swap_idx = -1;
if (kva != NULL) memset(kva, 0, PGSIZE);
return true;
}
// anon_ops는 이러한 내용이다 :
static const struct page_operations anon_ops = {
.swap_in = anon_swap_in,
.swap_out = anon_swap_out,
.destroy = anon_destroy,
.type = VM_ANON,
};
bool file_backed_initializer (struct page *page, enum vm_type type, void *kva)
{
page->operations = &file_ops;
return true;
}
//file_ops는 이러한 내용이다 :
static const struct page_operations file_ops = {
.swap_in = file_backed_swap_in,
.swap_out = file_backed_swap_out,
.destroy = file_backed_destroy,
.type = VM_FILE,
};
즉 이렇게 사전에 만들어진 내용들이 각 anon.c, file.c에 위치하고, 이것들이 alloc_page 시점에서 적절하게 사용되는 것이다. 그리고 이러한 페이지는 고유의 swap_in, swap_out, destroy를 가지게 된다. 그리고 이것들이 보다 확장되어 만들어지는 것이 지금의 객체지향 언어인 것이다.
두 타입 각각의 함수를 알아보겠다.
swap in : vm_do_claim_page에서 불리는 메모리 올리기 직전의 수행 내용
// anon.c에서 이 세 개가 사전에 있기를 기대한다!! 비트맵 관련 내용이 필요하다.
static struct disk *swap_disk;
/* 스왑 슬롯 비트맵 */
struct bitmap *swap_table;
/* 한 페이지를 몇 개의 섹터로 나누어 저장할지 계산 */
const size_t SECTORS_PER_PAGE = PGSIZE / DISK_SECTOR_SIZE;
static bool anon_swap_in(struct page *page, void *kva) {
struct anon_page *anon_page = &page->anon;
int idx = anon_page->swap_idx;
if (idx < 0) return false;
/* swap 디스크에서 512바이트씩 8번 읽어서 kva에 복사 */
for (int i = 0; i < SECTORS_PER_PAGE; i++) {
disk_read(swap_disk, idx * SECTORS_PER_PAGE + i, kva + DISK_SECTOR_SIZE * i);
}
/* 비트맵에서 해당 슬롯 비우기 (false로) */
bitmap_reset(swap_table, idx);
/* swap_idx 초기화 */
anon_page->swap_idx = -1;
return true;
}
https://hyeonistic.tistory.com/193
PintOS P3 #B : Bitmap
그림에 대한 비트맵을 다루는 것은 아니고, 말그대로 bit들의 집합이다. 넓게 보면 bool의 true/false 배열이라고 간주 할 수도 있다. 즉, 동일한 자원들의 잡힙에서 사용률을 추적하는데 활용 된다.
hyeonistic.tistory.com
비트맵에 대한 글을 참고하면 해당 함수들의 의도를 알 수 있다.
static bool
file_backed_swap_in (struct page *page, void *kva) {
struct file_page *file_page UNUSED = &page->file;
struct file_lazy_aux *aux = (struct file_lazy_aux *) page->uninit.aux;
struct file *file = aux->file;
off_t offset = aux->ofs;
size_t page_read_bytes = aux->read_bytes;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
lock_acquire(&g_filesys_lock);
// reading the contents in from the file = load_segment
// file_read_at을 사용!
if (file_read_at(aux->file, kva, aux->read_bytes, aux->ofs) != (int) aux->read_bytes) {
// palloc_free_page(kva);
// free(aux);
lock_release(&g_filesys_lock);
return false;
}
lock_release(&g_filesys_lock);
memset(kva + page_read_bytes, 0, page_zero_bytes);
return true;
}
파일을 읽어오라길래 load_segment과 거의 유사하게 구현했는데, 이게 빠져도 작동하는 케이스가 있었어서 이게 완벽히 정답이라는 보장은 못하겠다. 어쨌든 우리는 이런식으로 했다는거..
swap out : 프레임에 있는 페이지가 잠시 빠져나가게 될 때 해야하는 행위들 집합
static bool anon_swap_out(struct page *page) {
struct anon_page *anon_page = &page->anon;
/* 비어있는 슬롯을 찾고, 사용 중으로 표시 (race condition 방지) */
size_t idx = bitmap_scan_and_flip(swap_table, 0, 1, false);
if (idx == BITMAP_ERROR) {
PANIC("Swap disk is full!");
return false;
}
/* 디스크에 512바이트씩 8번 기록 (kva → 디스크) */
for (int i = 0; i < SECTORS_PER_PAGE; i++) {
disk_write(swap_disk, idx * SECTORS_PER_PAGE + i, page->frame->kva + DISK_SECTOR_SIZE * i);
}
/* 페이지 ↔ 프레임 연결 끊기 */
page->frame->page = NULL;
page->frame = NULL;
/* 스왑 슬롯 번호 저장 */
anon_page->swap_idx = idx;
/* 페이지 테이블에서 이 페이지의 매핑 제거 (다음 접근 시 page fault 발생) */
pml4_clear_page(thread_current()->pml4, page->va);
return true;
}
static bool file_backed_swap_out (struct page *page)
{
struct file_page *file_page = &page->file;
if (pml4_is_dirty(thread_current()->pml4, page->va))
{
file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->ofs);
pml4_set_dirty(thread_current()->pml4, page->va, 0);
}
/* 페이지 ↔ 프레임 연결 끊기 */
page->frame->page = NULL;
page->frame = NULL;
pml4_clear_page(thread_current()->pml4, page->va);
return true;
}
Destroy : 진짜 다썼을 때 해야 할 것들
static void anon_destroy(struct page *page)
{
struct anon_page *anon_page = &page->anon;
/* 스왑 슬롯이 사용 중이면 비트맵에서 false로 되돌리기 */
if (anon_page->swap_idx >= 0) bitmap_reset(swap_table, anon_page->swap_idx);
/* 페이지 테이블에서 매핑 제거 */
pml4_clear_page(thread_current()->pml4, page->va);
}
static void file_backed_destroy(struct page *page) {
ASSERT(page != NULL);
struct thread *curr = thread_current();
struct file_lazy_aux *aux = (struct file_lazy_aux *) page->uninit.aux;
// 페이지가 dirty ==> 해당 파일에 write back.
if (pml4_is_dirty(curr->pml4, page->va) && page->writable) {
lock_acquire(&g_filesys_lock);
file_write_at(aux->file, page->frame->kva, aux->read_bytes, aux->ofs);
lock_release(&g_filesys_lock);
// Write back 후 dirty bit를 원상 복구.
pml4_set_dirty(curr->pml4, page->va, false);
}
// 유저 페이지 매핑을 클리어
pml4_clear_page(curr->pml4, page->va);
// 프레임이 존재할 경우 free.
if (page->frame != NULL) {
struct frame *f = page->frame;
// 프레임 테이블에서 해당 프레임을 삭제
lock_acquire(&g_frame_lock);
list_remove(&f->f_elem);
lock_release(&g_frame_lock);
// 물리 프레임 및 페이지 free
palloc_free_page(f->kva);
free(f);
page->frame = NULL;
}
// aux 존재할 경우 free
if (aux != NULL) {
free(aux);
page->uninit.aux = NULL;
}
}
'구현하기' 카테고리의 다른 글
PintOS P3 #8 : mmap/munmap (0) | 2025.06.09 |
---|---|
PintOS P3 #B : Bitmap (0) | 2025.06.09 |
PintOS P3 #6 : Stack Growth (0) | 2025.06.09 |
PintOS P3 #5 : SPT를 비롯한 구현 #2 (1) | 2025.06.09 |
PintOS P3 #A : Clock Algorithm (0) | 2025.06.04 |