Virtual Memory 구현하기 Part2: 파일과 페이지 공유 및 교체하기 (Memory Mapped Files, Swap In/Out, Copy On Write)
이번 글에서는 Virtual Memory의 나머지 부분들인 Memory Mapped Files, Swap In/Out, Copy On Write를 마저 구현해 봅니다.
※ 주의: 제 컴퓨터 기준에서는, All Pass가 뜨는 것을 확인했으나, 다른 조원들의 컴퓨터에서 page-merge.* 테스트가 간헐적으로 실패하는 것을 확인했습니다. 따라서 그대로 적용하기보다는, 참고만 하시는 것을 추천드립니다.
사전준비: 전역 Lock 외부 공유 및 활용하기
merge 관련 테스트 통과를 위해서는 process.c
에 있는 load
함수의 filesys_open
전후로 lock을 걸어줘야 하는데요. 이를 위해 기존 syscall.h
에 있었던 전역 lock 구조체인 filesys_lock을 활용합니다.
(1) filesys_lock 외부 선언하기 (syscall.h)
extern struct lock filesys_lock; // 파일 시스템 동기화용 전역 락
(2) syscall 모듈 참조 추가 (process.c)
#include "userprog/syscall.h" // filesys_lock 지원
(3) load 함수의 filesys_open 전후로 lock 처리 추가하기 (process.c)
// filesys_open 이전 코드 생략..
lock_acquire(&filesys_lock); // lock 걸기
file = filesys_open (file_name);
// 이후 코드 생략..
done:
lock_release(&filesys_lock); // lock 풀기
return success;
Memory Mapped Files
(1) file_backed_initializer 구현하기 (file.c)
file_backed_initializer 구현하기
/* 파일 기반 페이지 초기화: lazy load 정보 → file_page에 복사 */
bool
file_backed_initializer(struct page *page, enum vm_type type, void *kva) {
// 해당 페이지의 동작 테이블을 file_ops로 설정
page->operations = &file_ops;
// page 내부 file_page 구조체 설정
struct file_page *file_page = &page->file;
// lazy_load_segment에서 전달된 보조 데이터 추출
struct lazy_load_arg *aux = (struct lazy_load_arg *)page->uninit.aux;
// 파일 매핑 정보 복사
file_page->file = aux->file;
file_page->ofs = aux->ofs;
file_page->read_bytes = aux->read_bytes;
file_page->zero_bytes = aux->zero_bytes;
return true;
}
기존 file_page 구조체에 필드 추가하기 (file.h)
이전에 만든 lazy_load_arg
구조체의 필드들을 그대로 가져와서 사용합니다.
struct file_page {
struct file *file;
off_t ofs;
uint32_t read_bytes;
uint32_t zero_bytes;
};
(2) file_backed_destroy 구현하기 (file.c)
/* 파일 매핑된 페이지 제거 시, 변경 내용 기록 및 자원 해제 */
static void
file_backed_destroy(struct page *page) {
struct file_page *file_page UNUSED = &page->file;
// 페이지가 dirty 상태이면 파일에 변경 내용 반영
if (pml4_is_dirty(thread_current()->pml4, page->va)) {
lock_acquire(&filesys_lock);
file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->ofs);
lock_release(&filesys_lock);
// dirty 비트 초기화
pml4_set_dirty(thread_current()->pml4, page->va, false);
}
// frame이 존재하면 frame 테이블에서 제거 및 정리
if (page->frame) {
list_remove(&page->frame->frame_elem);
page->frame->page = NULL;
page->frame = NULL;
free(page->frame);
}
// 페이지 테이블에서 매핑 제거
pml4_clear_page(thread_current()->pml4, page->va);
}
(3) file_backed_swap_in 구현하기 (file.c)
/* 파일 기반 페이지를 디스크에서 메모리로 swap in */
static bool
file_backed_swap_in(struct page *page, void *kva) {
struct file_page *file_page UNUSED = &page->file;
// 파일에서 데이터 읽어와 물리 메모리(kva)에 적재
lock_acquire(&filesys_lock);
int read = file_read_at(file_page->file, page->frame->kva,
file_page->read_bytes, file_page->ofs);
lock_release(&filesys_lock);
// 남은 영역은 0으로 초기화
memset(page->frame->kva + read, 0, PGSIZE - read);
return true;
}
(4) file_backed_swap_out 구현하기 (file.c)
/* 파일 기반 페이지를 메모리에서 내보내며, 변경된 내용은 파일에 반영 */
static bool
file_backed_swap_out(struct page *page) {
struct file_page *file_page UNUSED = &page->file;
struct frame *frame = page->frame;
// 페이지가 dirty 상태면 파일에 변경 내용 기록
if (pml4_is_dirty(thread_current()->pml4, page->va)) {
lock_acquire(&filesys_lock);
file_write_at(file_page->file, page->frame->kva, file_page->read_bytes, file_page->ofs);
lock_release(&filesys_lock);
pml4_set_dirty(thread_current()->pml4, page->va, false); // dirty 비트 초기화
}
// frame 연결 해제
page->frame->page = NULL;
page->frame = NULL;
// 페이지 테이블에서 매핑 제거
pml4_clear_page(thread_current()->pml4, page->va);
return true;
}
(5) syscall_handler에 SYS_MMAP 케이스 추가하기 (syscall.c)
case SYS_MMAP:
f->R.rax = sys_mmap((void *)arg1, (size_t)arg2, (int)arg3, (int)arg4, (off_t)arg5);
break;
(6) sys_mmap 구현하기 (syscall.c)
sys_mmap 함수 선언 추가하기
/* syscall.c 위쪽에 함수 선언 추가 */
void *sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset);
sys_mmap용 모듈 참조 추가하기
#include "vm/file.h" // do_mmap, do_munmap 지원
sys_mmap 구현하기
/* mmap 시스템 콜: 유저 주소 addr에 파일을 매핑 */
void *
sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset) {
// 유효하지 않은 주소 또는 페이지 정렬이 되지 않은 경우이거나,
// 커널 영역 접근 또는 매핑 범위가 커널을 침범한 경우
if (!addr || pg_round_down(addr) != addr ||
is_kernel_vaddr(addr) || is_kernel_vaddr(addr + length))
return NULL;
// offset이 페이지 정렬이 되지 않은 경우
if (offset != pg_round_down(offset) || offset % PGSIZE != 0)
return NULL;
// 이미 SPT에 등록된 주소인 경우 (중복 매핑 방지)
if (spt_find_page(&thread_current()->spt, addr))
return NULL;
// stdin(0), stdout(1), stderr(2)는 mmap 대상이 아님
if (fd < 3)
return NULL;
// 파일 존재 여부 및 길이 검증
struct file *file = process_get_file(fd);
if (file == NULL || file_length(file) == 0 || (long)length <= 0)
return NULL;
// 실제 매핑 처리 함수 호출
return do_mmap(addr, length, writable, file, offset);
}
(7) do_mmap 구현하기 (file.c)
do_mmap용 모듈 참조 추가하기
#include "userprog/process.h" // lazy_load_segment 지원
#include "threads/vaddr.h" // PGSIZE 지원
#include "userprog/syscall.h" // filesys_lock 지원
do_mmap 구현하기
/* 파일 내용을 addr부터 시작하는 유저 주소에 lazy load 방식으로 매핑 */
void *
do_mmap(void *addr, size_t length, int writable,
struct file *file, off_t offset) {
lock_acquire(&filesys_lock); // 파일 동시 접근 보호
// 원본 파일을 reopen (기존 파일이 닫힐 수 있으므로 별도로 핸들링)
struct file *mfile = file_reopen(file);
void *ori_addr = addr; // 반환용 원래 주소 저장
// 실제 읽을 바이트 계산 (파일 크기를 초과하지 않게)
size_t read_bytes = (length > file_length(mfile)) ? file_length(mfile) : length;
size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;
// 페이지 정렬 및 정합성 검증
ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT(pg_ofs(addr) == 0);
ASSERT(offset % PGSIZE == 0);
struct lazy_load_arg *aux;
while (read_bytes > 0 || zero_bytes > 0) {
// 현재 페이지에 읽을 바이트 및 0으로 채울 바이트 계산
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
// lazy load용 인자 구조체 동적 할당
aux = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
if (!aux)
goto err;
// 인자 값 설정
aux->file = mfile;
aux->ofs = offset;
aux->read_bytes = page_read_bytes;
aux->zero_bytes = page_zero_bytes;
// 초기화 정보 등록 (lazy_load_segment 사용)
if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, aux)) {
goto err;
}
// 다음 페이지 처리 준비
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
addr += PGSIZE;
offset += page_read_bytes;
}
lock_release(&filesys_lock);
return ori_addr; // 성공 시 매핑 시작 주소 반환
err:
free(aux); // 실패 시 자원 해제
lock_release(&filesys_lock);
return NULL;
}
(8) lazy_load_segment 외부 공유하기 (process.h, process.c)
기존 process.c
의 lazy_load_segment
는 static으로 해당 파일 안에서만 사용했지만, file.c
의 do_mmap
이 lazy_load_segment
를 사용함에 따라, 기존에 있던 static을 제거하고 process.h
에 함수 선언을 추가해줍니다.
/* process.c에서 lazy_load_segment의 static을 제거
이후 process.h에 아래 함수 선언 추가 */
bool lazy_load_segment(struct page *page, void *aux);
(9) syscall_handler에 SYS_MUNMAP 케이스 추가하기 (syscall.c)
case SYS_MUNMAP:
sys_munmap((void *)arg1);
break;
(10) sys_munmap 구현하기 (syscall.c)
sys_munmap 함수 선언 추가하기
/* syscall.c 위쪽에 함수 선언 추가 */
void sys_munmap(void *addr);
sys_munmap 구현하기
void sys_munmap(void *addr) {
// 실제 언매핑 함수 호출
do_munmap(addr);
}
(11) do_munmap 구현하기 (file.c)
/* addr부터 시작하는 매핑 영역을 해제 (mmap 해제) */
void
do_munmap(void *addr) {
struct thread *curr = thread_current();
struct page *page;
// addr부터 연속된 매핑된 페이지들을 해제
while ((page = spt_find_page(&curr->spt, addr))) {
if (page)
destroy(page); // 페이지 자원 정리 및 해제
addr += PGSIZE; // 다음 페이지로 이동
}
}
(12) supplemental_page_table_copy 수정하기
페이지 타입이 VM_FILE일 때의 조건문을 추가해줍니다.
/* src SPT의 모든 페이지를 dst SPT로 복사 (fork 시 사용) */
bool
supplemental_page_table_copy(struct supplemental_page_table *dst UNUSED,
struct supplemental_page_table *src UNUSED) {
struct hash_iterator i;
hash_first(&i, &src->spt_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;
void *upage = src_page->va;
bool writable = src_page->writable;
if (type == VM_UNINIT) {
// lazy load 페이지는 초기화 정보만 복사
vm_alloc_page_with_initializer(
src_page->uninit.type,
src_page->va,
src_page->writable,
src_page->uninit.init,
src_page->uninit.aux);
}
else if (type == VM_FILE) {
// file-backed 페이지는 복사된 aux 정보로 등록
struct lazy_load_arg *aux = malloc(sizeof(struct lazy_load_arg));
aux->file = src_page->file.file;
aux->ofs = src_page->file.ofs;
aux->read_bytes = src_page->file.read_bytes;
// dst SPT에 페이지 등록
if (!vm_alloc_page_with_initializer(type, upage, writable, NULL, aux))
return false;
// 초기화 및 매핑 설정 (공유 프레임 사용)
struct page *dst_page = spt_find_page(dst, upage);
file_backed_initializer(dst_page, type, NULL);
dst_page->frame = src_page->frame;
pml4_set_page(thread_current()->pml4, dst_page->va, src_page->frame->kva, src_page->writable);
}
else {
// 이미 초기화된 페이지는 새로 할당하고 데이터 복사
if (vm_alloc_page(type, upage, writable) &&
vm_claim_page(upage)) {
struct page *dst_page = spt_find_page(dst, upage);
memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE);
} else {
return false;
}
}
}
return true;
}
Swap In/Out
(1) Swap Table 인터페이스 추가 (anon.c)
Swap Table용 모듈 참조 추가하기
#include <bitmap.h> // 비트맵 자료구조 지원
#include "threads/vaddr.h" // PGSIZE 지원
#include "threads/mmu.h" // pml4 관련 함수 지원
Swap Table용 매크로 추가하기
#define SECTOR_PER_PAGE (PGSIZE / DISK_SECTOR_SIZE) // 한 페이지당 디스크 섹터 수
Swap Table용 전역 변수 추가하기
static struct bitmap *swap_table; // 스왑 슬롯의 사용 여부를 추적하는 비트맵
static struct lock swap_lock; // swap_table 접근 동기화를 위한 전역 락
기존 anon_page 구조체에 page_no 추가하기 (anon.h)
struct anon_page {
size_t page_no; // 디스크 페이지 번호
};
(2) anon_init 구현하기 (anon.c)
스왑 디스크 영역 할당 및 선언한 전역 변수들을 초기화합니다.
/* 익명 페이지용 스왑 공간 초기화 */
void
vm_anon_init(void) {
// 스왑 디스크 영역 할당 (디스크 1번, 파티션 1)
swap_disk = disk_get(1, 1);
// 스왑 공간 전체 크기만큼 비트맵 생성 (페이지 단위 관리)
swap_table = bitmap_create(disk_size(swap_disk) / SECTOR_PER_PAGE);
// 스왑 공간 접근 동기화를 위한 락 초기화
lock_init(&swap_lock);
}
(3) anon_initializer 구현하기 (anon.c)
/* 익명 페이지 초기화: 초기 상태 설정 및 스왑 미할당 표시 */
bool
anon_initializer(struct page *page, enum vm_type type, void *kva) {
struct uninit_page *uninit = &page->uninit;
// uninit 영역을 깨끗한 상태로 초기화
memset(uninit, 0, sizeof(struct uninit_page));
// 익명 페이지 전용 operations 등록
page->operations = &anon_ops;
// 스왑 공간 미할당 상태로 설정
struct anon_page *anon_page = &page->anon;
anon_page->page_no = BITMAP_ERROR;
return true;
}
(4) anon_swap_in 구현하기 (anon.c)
/* 익명 페이지를 스왑 디스크에서 메모리로 로드 (swap in) */
static bool
anon_swap_in(struct page *page, void *kva) {
struct anon_page *anon_page = &page->anon;
lock_acquire(&swap_lock); // 스왑 영역 동기화
// 스왑 슬롯이 유효하지 않으면 실패
if (anon_page->page_no == BITMAP_ERROR){
lock_release(&swap_lock);
return false;
}
// 해당 스왑 슬롯이 실제로 사용 중인지 확인
if (!bitmap_test(swap_table, anon_page->page_no)){
lock_release(&swap_lock);
return false;
}
// 디스크에서 한 페이지(8 섹터) 단위로 읽어오기
for (size_t i = 0; i < SECTOR_PER_PAGE; i++)
disk_read(swap_disk, (anon_page->page_no * SECTOR_PER_PAGE) + i,
kva + (i * DISK_SECTOR_SIZE));
// 해당 슬롯을 free 상태로 표시
bitmap_set(swap_table, anon_page->page_no, false);
lock_release(&swap_lock);
// 페이지가 더 이상 스왑에 존재하지 않음을 표시
anon_page->page_no = BITMAP_ERROR;
return true;
}
(5) anon_swap_out 구현하기 (anon.c)
/* 익명 페이지를 스왑 디스크로 내보냄 (swap out) */
static bool
anon_swap_out(struct page *page) {
struct anon_page *anon_page = &page->anon;
lock_acquire(&swap_lock); // 스왑 공간 동기화
// 빈 스왑 슬롯 검색 및 사용 표시 (flip)
size_t page_no = bitmap_scan_and_flip(swap_table, 0, 1, false);
// 스왑 공간이 부족한 경우 → 실패
if (page_no == BITMAP_ERROR){
lock_release(&swap_lock);
return false;
}
// 페이지 내용을 8개의 섹터 단위로 디스크에 기록
for (size_t i = 0; i < SECTOR_PER_PAGE; i++)
disk_write(swap_disk, (page_no * SECTOR_PER_PAGE) + i,
page->va + (i * DISK_SECTOR_SIZE));
// 스왑 슬롯 번호 저장
anon_page->page_no = page_no;
// frame 연결 해제 및 페이지 테이블에서 매핑 제거
page->frame->page = NULL;
page->frame = NULL;
pml4_clear_page(thread_current()->pml4, page->va);
lock_release(&swap_lock);
}
(6) anon_destroy 구현하기 (anon.c)
/* 익명 페이지 제거 시, 스왑 슬롯과 frame 자원을 정리 */
static void
anon_destroy(struct page *page) {
struct anon_page *anon_page = &page->anon;
// 스왑 슬롯이 할당되어 있었다면 비트맵에서 해제
if (anon_page->page_no != BITMAP_ERROR)
bitmap_reset(swap_table, anon_page->page_no);
// frame이 존재하면 frame 리스트에서 제거하고 해제
if (page->frame) {
lock_acquire(&swap_lock);
list_remove(&page->frame->frame_elem); // frame 테이블에서 제거
lock_release(&swap_lock);
page->frame->page = NULL;
free(page->frame); // frame 구조체 메모리 해제
page->frame = NULL;
}
}
(7) vm_get_victim 구현하기 (vm.c)
/* 교체할 victim frame을 선택 (2차 기회 방식) */
static struct frame *
vm_get_victim(void) {
struct frame *victim = NULL;
lock_acquire(&frame_lock); // frame 테이블 접근 동기화
// frame_table 순회하며 교체 대상 탐색
for (next = list_begin(&frame_table); next != list_end(&frame_table); next = list_next(next)) {
victim = list_entry(next, struct frame, frame_elem);
// 최근 접근된 페이지라면 accessed 비트만 초기화 (생존 기회 부여)
if (pml4_is_accessed(thread_current()->pml4, victim->page->va)) {
pml4_set_accessed(thread_current()->pml4, victim->page->va, false);
} else {
// 접근되지 않은 페이지라면 victim으로 선정
lock_release(&frame_lock);
return victim;
}
}
// 순회 끝까지 갔을 경우 마지막으로 본 frame 반환 (fallback)
lock_release(&frame_lock);
return victim;
}
전역 탐색 포인터 next 선언하기
/* vm.c 위쪽에 추가 */
struct list_elem *next = NULL; // victim 선정용 전역 포인터
(8) vm_evict_frame 구현하기 (vm.c)
/* victim frame을 선택해 페이지를 스왑 아웃한 후 반환 */
static struct frame *
vm_evict_frame(void) {
struct frame *victim = vm_get_victim(); // 교체할 frame 선택
// frame에 매핑된 페이지가 있으면 스왑 아웃
if (victim->page)
swap_out(victim->page);
return victim; // 빈 frame 반환
}
Copy On Write
(1) supplemental_page_table_copy 수정하기 (vm.c)
else {
// 초기화된 페이지인 경우, dst SPT에 페이지 메타데이터만 등록
if (!vm_alloc_page(type, upage, writable))
return false;
// kva는 새로 생성하지 않고, 기존 frame의 kva를 매핑만 수행
if (!vm_copy_claim_page(dst, upage, src_page->frame->kva, writable))
return false;
}
(2) vm_copy_claim_page 구현하기 (vm.c)
기존 kva를 재사용하고, 새로운 frame 구조체만 할당합니다. 다시말해 물리 메모리 복사 없이 가상 주소만 kva로 매핑합니다.
vm_copy_claim_page 함수 선언하기
/* vm.c 위쪽에 함수 선언 추가 */
static bool vm_copy_claim_page(struct supplemental_page_table *dst, void *va, void *kva, bool writable);
vm_copy_claim_page 함수 구현하기
/* 기존 kva를 사용하여 dst SPT의 va에 매핑만 수행 (frame 할당은 새로) */
static bool
vm_copy_claim_page(struct supplemental_page_table *dst, void *va, void *kva, bool writable) {
// dst SPT에서 va에 해당하는 page 찾기
struct page *page = spt_find_page(dst, va);
if (page == NULL)
return false;
// 새로운 frame 구조체 할당 (물리 메모리는 재사용)
struct frame *frame = (struct frame *)malloc(sizeof(struct frame));
if (!frame)
return false;
// page ↔ frame 연결 설정
page->accessible = writable; // 접근 권한 저장
frame->page = page;
page->frame = frame;
frame->kva = kva; // 기존 kva를 그대로 사용
// 사용자 페이지 테이블에 매핑
if (!pml4_set_page(thread_current()->pml4, page->va, frame->kva, false)) {
free(frame);
return false;
}
// frame_table에 등록
list_push_back(&frame_table, &frame->frame_elem);
// swap-in 동작으로 실제 페이지 내용을 불러옴 (실제 사용 시 보장)
return swap_in(page, frame->kva);
}
기존 page 구조체에 accessible 필드 추가 (vm.h)
/* writable 필드 아래에 추가 */
bool accessible; // 자식 프로세스 페이지 쓰기 가능 여부
(3) vm_try_handle_fault 수정 (vm.c)
존재하는 페이지에 대한 쓰기 접근을 자식 프로세스의 COW 상황으로 보고, write-protect 핸들러로 처리하도록 변경합니다.
/* 존재하는 페이지에 대해 쓰기 접근 시 write-protect 핸들러 호출 */
if (!not_present && write)
return vm_handle_wp(page);
(4) vm_handle_wp 구현하기 (vm.c)
COW 상황에서 호출되며, 기존 물리 메모리를 복제하여 새로운 메모리를 할당하고 가상 주소는 새 kva로 재매핑합니다. 마지막으로 Copy-On-Write의 핵심 처리로써, 쓰기 가능한 전용 복사본을 제공해 공유를 끊습니다.
/* 쓰기 보호(Copy-On-Write) 발생 시, 새 페이지를 할당하고 복사 */
static bool
vm_handle_wp(struct page *page UNUSED) {
// 쓰기 불가능한 페이지라면 처리 불가
if (!page->accessible)
return false;
// 기존 물리 메모리 주소 보관
void *kva = page->frame->kva;
// 새로운 사용자용 물리 페이지 할당
page->frame->kva = palloc_get_page(PAL_USER | PAL_ZERO);
// 할당 실패 시 스왑 아웃 후 재시도
if (page->frame->kva == NULL)
page->frame = vm_evict_frame();
// 기존 내용 복사 (쓰기 시점 복제)
memcpy(page->frame->kva, kva, PGSIZE);
// 페이지 테이블에 새로운 매핑 등록
if (!pml4_set_page(thread_current()->pml4, page->va, page->frame->kva, page->accessible))
return false;
return true;
}
(5) anon_destroy 수정하기 (anon.c)
// anon_destroy의 맨 밑에 추가
pml4_clear_page(thread_current()->pml4, page->va);
기대 테스트 결과
'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글
[Pintos] tests 디렉터리의 모든 txt파일 CRLF 없애기 (0) | 2025.06.09 |
---|---|
[Pintos] Virtual Memory 구현하기 Part1: Lazy Load 방식으로 프로그램 실행하기 (Memory Management, Anonymous Page, Stack Growth) (1) | 2025.06.06 |
[Pintos] OSTEP 기반 Virtual Memory 배경지식 정리: 왜 VM이 필요한가 (0) | 2025.05.31 |
[Pintos] Virtual Memory Layout 정리 (0) | 2025.05.30 |
[Pintos] Virtual Memory 전체적인 큰 그림 그리기 (1) | 2025.05.30 |