이 글은 핀토스 가상메모리 메모리 매핑, 스와핑에 관한 글입니다.
Memory Mapped File
Swap In/Out
파일 입출력 방법에는 크게 두 가지가 있다.
오늘날 많은 컴퓨팅 아키텍처들이 memory-mmapped I/O를 지원하는데, mmap은 디스크 파일을 메모리처럼 다이렉트로 읽고 쓸 수 있는 장점을 제공한다.
mmap은 메모리 페이지에 디스크 블록을 매핑시켜버린다.
read, write()처럼 buffer에 쓰고 그것을 메모리-디스크간 복사할 필요 없이, mmap은 처음에 파일이 들어갈 가상주소만 매핑해두면 거기를 자기 메모리처럼 읽고 쓸 수 있다.
즉, 그 위치의 가상주소를 읽으면 '파일 내용'이 읽혀진다.
디멘드 페이징으로 파일 데이터를 메모리로 로드하는 함수
mmap이 다이렉트로 파일에 접근할 수 있다고 생각하면 만사형통 같지만, 실제로 그렇지는 않다.
핀토스에서 구현할 mmap() 은 VM_FILE 타입으로 변환될 가상페이지(현재는 uninit)만 만들어놓고, 이것이 물리 프레임과 실제 연결되는 타이밍은 page fault로 do_claim()이 호출될 때다.
do_mmap()
: addr, file, offset 등 나중에 파일을 로드할 때 필요한 정보들을 받아서 load_aux
구조체에 저장.vm_alloc_page_with_initialize()
: uninit_new로 VM_FILE 타입에 맞게 초기화, SPT 해시테이블에 삽입do_mmap
void *
do_mmap (void *addr, size_t length, int writable,
struct file *file, off_t offset) {
//obtain a separate and independent reference to the file for each of its mappings.
// 얼마나 읽을 것인가: 주어진 파일 길이와 length 비교: 읽고자 하는 length와 file 사이즈 중 작은 것
struct file *file_ = file_reopen(file);
size_t read_bytes = length > file_length(file) ? file_length(file) : length;
size_t zero_bytes = PGSIZE - (read_bytes % PGSIZE);
off_t offset_ = offset ;
// 시작 주소 : 페이지 확장시 리턴 주소값 변경 방지
void *start_addr = addr;
if (file_ == NULL || read_bytes == 0) return NULL;
/* 가상 페이지 매핑 */
while (read_bytes > 0 || zero_bytes > 0) { // 둘 다 0이 될 때까지 반복.
// 파일을 페이지 단위로 잘라 load_aux에 저장
size_t page_read_bytes = read_bytes > PGSIZE ? PGSIZE : read_bytes; // 마지막에만 read_bytes
size_t page_zero_bytes = PGSIZE - page_read_bytes; // 중간에는 0, 마지막에 padding
struct load_aux *load_aux = (struct load_aux *)malloc(sizeof(struct load_aux));
load_aux->file = file_;
load_aux->offset = offset_;
load_aux->page_read_bytes = page_read_bytes;
// 대기중인 오브젝트 생성 - 초기화되지 않은 주어진 타입의 페이지 생성. 나중에 UNINIT을 FILE-backed로.
if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, load_aux)) {
return NULL;
}
// 다음 페이지로 이동
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes; // 마지막엔 0
addr += PGSIZE;
offset_ += page_read_bytes;
}
// 매핑 시작 주소 반환
return start_addr;
}
mmap보다 중요한 건 munmap이다.
보통 메모리를 해제하는 free, clear, cleanup과 같은 이름을 지닌 함수들과 달리,
munmap은 매핑된 주소의 변경된 내용들을 디스크 파일에 반영해준다.
즉, 애초에 mmap을 호출한 이유가 파일에 쓰기 위함이었다면 munmap을 해주지 않으면 말짱도루묵인 셈이다.
ANON 페이지와 달리, memory-mapped page는 FILE 기반 페이지다.
디스크와 상호작용이 중요할 수밖에 없으며,
do_munmap()
은 process_exit()
에서 호출되어 SPT의 모든 vm_entry 페이지들을 돌면서 그들의 변경내용을 디스크에 반영한 후 프로세스를 종료해야 한다.
file_write_at()
은 struct page의 uninit.aux 필드에서 받은 정보로 올바른 파일 위치를 찾아가 변경내용을 써준다.
do_munmap
void
do_munmap (void *addr) {
struct thread *t = thread_current();
// traverse
while (true) {
struct page *page = spt_find_page(&t->spt, addr);
if (page == NULL)
return;
// 파일 변경사항을 반영할 수 있도록 aux를 받아옴
struct load_aux *load_aux = (struct load_aux *)page->uninit.aux;
if (pml4_is_dirty(t->pml4, page->va)) {
// Writes SIZE bytes from BUFFER into FILE
file_write_at(load_aux->file, addr, load_aux->page_read_bytes, load_aux->offset);
pml4_set_dirty(t->pml4, page->va, 0);
}
addr += PGSIZE;
}
}
process_exit
void process_exit (void) {
struct thread *curr = thread_current ();
// Close all Opened file by for loop
for (int i=0; i<FDCOUNT_LIMIT; i++)
close(i);
palloc_free_multiple(curr->fd_table, FDT_PAGES);
file_close(curr->running);
/* munmap - file-backed 경우 매핑 해제 */
struct supplemental_page_table *spt = &curr->spt;
if (!hash_empty(&spt->spt_hash)) {
struct hash_iterator iter;
hash_first(&iter, &spt->spt_hash);
while (hash_next(&iter)) {
struct page *page = hash_entry(hash_cur(&iter), struct page, hash_elem);
if (page->operations->type == VM_FILE) {
do_munmap(page->va);
}
}
}
sema_up(&curr->wait_sema); // unblock parent
sema_down(&curr->free_sema);
process_cleanup ();
}
테스트 케이스를 보면 mmap()이 어떤 식으로 사용되는지 알수 있다.
'mmap-exit.c'는 디스크 파일을 가상주소에 매핑하고
매핑 해제 후 내용이 제대로 반영되었는지 확인하는 테스트다.
ACTUAL(0x10000000)
에 매핑memcpy(dst, src)
로 char 타입 변수 sample을 ACTUAL에 덮어씀(copy) process_exit()
에서 do_munmap()
으로 해당 주소의 변경내용을 디스크 파일에 반영 check_file()
로 'sample.txt' 파일과 sample 텍스트 비교mmap-exit.c
void
test_main (void)
{
pid_t child;
/* Make child write file. */
quiet = true;
child = fork("child-mm-wrt");
if (child == 0) { // 자식 스레드만 실행
CHECK ((child = exec ("child-mm-wrt")) != -1, "exec \"child-mm-wrt\"");
} else { // 부모 스레드만 실행
CHECK (wait (child) == 0, "wait for child (should return 0)");
quiet = false;
/* Check file contents. */
check_file ("sample.txt", sample, sizeof sample);
}
}
child-mm-wrt.c
#define ACTUAL ((void *) 0x10000000)
void
test_main (void)
{
CHECK (create ("sample.txt", sizeof sample), "create \"sample.txt\"");
CHECK ((handle = open ("sample.txt")) > 1, "open \"sample.txt\"");
CHECK (mmap (ACTUAL, sizeof sample, 1, handle, 0) != MAP_FAILED, "mmap \"sample.txt\"");
memcpy (ACTUAL, sample, sizeof sample);
}
테스트 결과 출력
(mmap-exit) begin
(child-mm-wrt) begin
(child-mm-wrt) create "sample.txt"
(child-mm-wrt) open "sample.txt"
(child-mm-wrt) mmap "sample.txt"
(child-mm-wrt) end
(mmap-exit) open "sample.txt" for verification
(mmap-exit) verified contents of "sample.txt"
(mmap-exit) close "sample.txt"
(mmap-exit) end
우리가 스왑하고자 하는 것은 물리 페이지다.
프로세스가 커널에 요청하여 할당받은 물리 프레임을 리스트로 관리하기 위해 frame_table
연결리스트 구조체를 사용하였다.
frame_table
은 LRU_list라 볼 수 있으며, 여기서 교체할 페이지를 선택하게 된다.
스왑의 목적은 메모리 가동률을 높이는 것.
자주 쓰는 메모리는 남겨두고 안쓰는 메모리 구역은 디스크로 보내
제한된 메모리 자원으로 최대한의 효용을 내는 것
주기억장치(RAM) <-> 보조기억장치(disk)간 스왑 인/아웃이
핀토스에서는 frame_talbe(메모리)과 swap_disk(디스크)로 구현하게 되어있으며
그 흐름은 다음과 같이 도식화할 수 있음
vm_get_frame()
에서 물리 프레임을 요청할 때 frame_table
에 추가vm_evict_frame()
으로 교체할 페이지를 선정, swap_out()
으로 디스크로 보냄pml4_clear_page()
로 해당 페이지를 'not present'
로 표시
vm_get_frame
static struct frame *
vm_get_frame (void) {
struct frame *frame = NULL;
frame = (struct frame *)malloc(sizeof(struct frame));
/* palloc() and get frame */
frame->kva = palloc_get_page(PAL_USER); // RAM user pool -> kernel va로 1 page 할당
frame->page = NULL; // @ for debug
ASSERT (frame != NULL);
ASSERT (frame->page == NULL);
/* If there is no available page, evict the page and return it */
if (frame->kva == NULL) {
frame = vm_evict_frame();
frame->page = NULL;
return frame;
}
list_push_back(&frame_table, &frame->frame_elem);
return frame;
}
vm_evict_frame
static struct frame *
vm_evict_frame (void) {
struct frame *victim UNUSED = vm_get_victim ();
/* swap out the victim and return the evicted frame. */
swap_out(victim->page);
return victim;
}
메모리가 가득찼을 때 운영체제가 선택한 메모리 페이지를 디스크의 스왑 영역으로 옮기는 함수
anon 스왑아웃 루틴
anon_swap_out
static bool
anon_swap_out (struct page *page) {
struct anon_page *anon_page = &page->anon;
int page_no = bitmap_scan(swap_table, 0, 1, false);
if (page_no == BITMAP_ERROR) return NULL;
for (int i = 0; i < SECTORS_PER_PAGE; ++i) {
// Convert swap slot index to writing sector number
disk_write(swap_disk, page_no * SECTORS_PER_PAGE + i,
page->va + DISK_SECTOR_SIZE * i);
}
bitmap_set(swap_table, page_no, true);
pml4_clear_page(&thread_current()->pml4, page->va, 0);
anon_page->swap_index = page_no;
return true;
}
anon_page->swap_index = page_no
)page_no * SECTORS_PER_PAGE + i
로 스왑디스크 내 섹터 넘버를 계산anon_swap_in
static bool
anon_swap_in (struct page *page, void *kva) {
struct anon_page *anon_page = &page->anon;
int page_no = anon_page->swap_index;
if (anon_page->swap_index == INVALID_SLOT_IDX) return false;
if (bitmap_test(swap_table, page_no) == false) return false;
for (int i = 0; i < SECTORS_PER_PAGE; ++i) {
disk_read(swap_disk, page_no * SECTORS_PER_PAGE + i, kva + DISK_SECTOR_SIZE * i);
}
bitmap_set(swap_table, page_no, false);
return true;
}
비트맵은 array of bits로, 각 비트는 true or false를 나타냄
Swap disk의 경우 bit = 0: 비어있음 / 1 : 차있음 표시
bitmap_scan()
size_t
bitmap_scan (const struct bitmap *b, size_t start, size_t cnt, bool val){
ASSERT (start <= b->bit_cnt) // start: 비트맵 몇 번째부터?
if (cnt <= b->bit_cnt) {
size_t last = b->bit_cnt - cnt;
size_t i;
for (i=start; i<=last, i++)
if (!bitmap_contains (b, i, cnt, !value))
return i;
}
return BITMAP_ERROR;
}
/* (참고) 비트맵 구조체 */
struct bitmap {
size_t bit_cnt; // number of bits
elem_type *bits; // 0 or 1
}
page_no
는 스왑 인으로 Swap disk에서 읽어들일 때 중요하게 사용된다.
스왑디스크에서 DISK_SECTOR_SIZE 만큼 읽을 때,
가상주소의 최하위 12비트를 >>
로 없애면 page number(인덱스)를 구할 수 있다.
한편 page_round_down(va)
은 가장 가까운 페이지 경계 주소를 리턴한다.
/* Page offset (bits 0:12). */
#define PGSHIFT 0 /* Index of first offset bit. */
#define PGBITS 12 /* Number of offset bits. */
#define PGSIZE (1 << PGBITS) /* Bytes in a page. */
#define PGMASK BITMASK(PGSHIFT, PGBITS) /* Page offset bits (0:12). */
/* Page number (the most significant bits) */
#define pg_no(va) ((uint64_t) (va) >> PGBITS)
/* Round down to nearest page boundary. */
#define pg_round_down(va) (void *) ((uint64_t) (va) & ~PGMASK)
우리는 frame_table에서 교체할 물리 프레임을 찾고 있지만,
필요한 것은 page table entry(PTE)에 저장된 accessed bit다.
어떻게 주어진 frame만 가지고 어떻게 accessed bit를 알 수 있을까?
이는 frame 구조체 멤버인 page->va로 pml4 페이지 테이블을 순회하며 해당 페이지 엔트리를 찾음으로써 가능하다.
vm_get_victim()
static struct frame *
vm_get_victim (void) {
/* Get the struct frame, that will be evicted. */
struct frame *victim = NULL;
struct thread *curr = thread_current();
struct list_elem *e = start;
/* initialize start to first element of the frame_table */
if (!start) {
start = list_begin(&frame_table);
}
for (start = e; start != list_end(&frame_table); start = list_next(start)) {
victim = list_entry(start, struct frame, frame_elem);
if (pml4_is_accessed(curr->pml4, victim->page->va)) {
pml4_set_accessed(curr->pml4, victim->page->va, 0);
}
else // refer bit is 0
return victim;
}
/* If no victim is found from start to end, search from beginning to start */
for (start = list_begin(&frame_table); start != e; start = list_next(start)) {
victim = list_entry(start, struct frame, frame_elem);
if (pml4_is_accessed(curr->pml4, victim->page->va)) {
pml4_set_accessed(curr->pml4, victim->page->va, 0);
}
else
return victim;
}
return victim; // frame을 다 돌아도 refer bit 0이 안나오면 현재 페이지를 evict
}