이번 포스팅에서는 MMAP/MUNMAP 기능을 구현할 때 필요한 함수들을 알아보도록 하자 !
void *mmap(void *addr, size_t length, int writable, int fd, off_t offset)유효성 검증
open된 file 가져오기
do_mmap() 호출
void *mmap(void *addr, size_t length, int writable, int fd, off_t offset)
{
if (pg_ofs(addr) != 0 || (uint64_t)addr <= 0 || is_kernel_vaddr(addr) || pg_ofs(offset) != 0)
return NULL;
if (is_kernel_vaddr((uint64_t)addr + length) || (uint64_t)addr + length <= 0)
return NULL;
struct file *file = process_get_file(fd);
if (file == NULL || length == 0)
return NULL;
return do_mmap(addr, length, writable, file, offset);
}
void sys_munmap(void *addr)void munmap(void *addr)
{
check_address(addr);
do_munmap(addr);
}
do_mmap()파일을 페이지 단위로 쪼개고
각 페이지를 lazy 방식으로 등록
struct load_info에 필요한 파일 정보 저장 (file, offset, read_bytes, zero_bytes)
등록 시 vm_alloc_page_with_initializer(VM_FILE, ...) 사용
첫 번째 페이지에 mapped_page_count 저장 (→ do_munmap()에서 반복용)
/* mmap() 시스템 콜 구현: 파일을 메모리에 매핑하고 lazy load 등록
*
* [역할]
* - 파일의 offset부터 length만큼의 데이터를 주소 addr부터 가상 메모리에 매핑
* - 실제 데이터는 lazy_load() 함수로 지연 로딩됨 (페이지 폴트 발생 시 load)
* - 페이지 단위로 매핑하고 이미 존재하는 주소일 경우 실패
*
* @param addr : 매핑할 시작 가상 주소 (유저 스택 영역과 겹치면 안 됨)
* @param length : 매핑할 총 크기 (바이트 단위)
* @param writable : 해당 매핑된 메모리 영역이 쓰기 가능한지 여부
* @param file : 매핑할 파일의 포인터
* @param offset : 파일 내에서 시작할 위치
* @return 성공 시 addr, 실패 시 NULL
*/
void *do_mmap(void *addr, size_t length, int writable,
struct file *file, off_t offset)
{
// 매핑할 총 페이지 수 계산 (page-aligned가 아니면 올림 처리)
int cnt_page = length % PGSIZE ? length / PGSIZE + 1 : length / PGSIZE;
size_t length_ = length;
// 파일의 길이보다 offset이 크면 유효하지 않음
if (file_length(file) < offset)
return NULL;
// (1) 매핑하려는 주소 공간에 이미 페이지가 존재하면 실패
for (int i = 0; i < cnt_page; i++) {
if (spt_find_page(&thread_current()->spt, addr + i * PGSIZE) != NULL)
return NULL;
}
// (2) 파일 시스템 락을 필요 시 획득 (동기화)
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// (3) 각 페이지에 대해 lazy loading 방식으로 매핑 등록
for (int i = 0; i < cnt_page; i++) {
// 파일을 reopen해서 각 페이지마다 독립적인 참조 확보
struct file *file_ = file_reopen(file);
// 파일 로딩 정보 전달용 구조체 생성
struct file_load *aux = malloc(sizeof(struct file_load));
aux->file_length = length; // 전체 길이 (munmap 대비)
aux->file = file_;
aux->ofs = offset + i * PGSIZE; // 이 페이지가 읽을 파일 offset
// 이번 페이지에서 읽어야 할 바이트 계산
if (length_ >= PGSIZE) {
aux->read_bytes = PGSIZE;
length_ -= PGSIZE;
} else {
aux->read_bytes = length_;
}
// 나머지는 zero-fill 영역
aux->zero_bytes = PGSIZE - aux->read_bytes;
// lazy loader를 지정하여 VM_FILE 타입으로 페이지 할당
vm_alloc_page_with_initializer(VM_FILE, addr + i * PGSIZE, writable, lazy_load, aux);
}
// (4) 락 해제
if (!is_lock_held)
lock_release(&filesys_lock);
return addr;
}
do_munmap()시작 주소 기준으로 mapped_page_count만큼 순회
각 페이지를 찾아서:
dirty면 write-back
destroy(page) 호출 (→ file_backed_destroy()로 연결)
/* munmap() 시스템 콜 구현: 주어진 addr부터 시작하는 매핑 해제
*
* [역할]
* - addr에서 시작된 파일 매핑을 해제 (mmap으로 등록된 영역)
* - dirty 상태인 경우 파일에 write-back 수행
* - 프레임의 참조 수 감소 및 페이지 테이블에서 제거
*
* @param addr : 매핑 해제할 시작 주소 (반드시 페이지 정렬된 주소)
*/
void do_munmap(void *addr)
{
size_t length;
int cnt_page;
// (1) addr는 반드시 페이지 기준이어야 함 (0x...000 형태)
if (pg_ofs(addr) != 0)
return;
// (2) 해당 주소의 페이지를 보조 페이지 테이블에서 찾음
struct page *page = spt_find_page(&thread_current()->spt, addr);
if (page == NULL)
return;
// (3) mmap된 총 길이와 해제할 페이지 수 계산
length = page->file.file_length;
cnt_page = length % PGSIZE ? length / PGSIZE + 1 : length / PGSIZE;
// (4) 파일 시스템 락을 필요 시 획득
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// (5) 페이지 하나씩 순회하며 해제
for (int i = 0; i < cnt_page; i++)
{
// 현재 해제 대상 페이지
page = spt_find_page(&thread_current()->spt, addr + i * PGSIZE);
// dirty이면 현재 메모리 내용을 파일에 기록
if (pml4_is_dirty(thread_current()->pml4, page->va))
{
file_write_at(page->file.file, page->va, page->file.read_bytes, page->file.ofs);
}
// 프레임 참조 수 감소
page->frame->cnt_page -= 1;
// 파일 핸들 닫기
file_close(page->file.file);
// 보조 페이지 테이블에서 제거
hash_delete(&thread_current()->spt.spt_hash, &page->page_elem);
// 페이지 테이블에서도 해당 매핑 제거
pml4_clear_page(thread_current()->pml4, page->va);
}
// (6) 락을 획득했으면 해제
if (!is_lock_held)
lock_release(&filesys_lock);
}
lazy_load()
file_load를 참조하여
file_read로 필요한 바이트 읽기
나머지는 memset()으로 0 초기화
/* lazy loading을 위한 페이지 초기화 함수
*
* [역할]
* - 초기화 시 등록된 파일 정보를 이용하여
* 파일로부터 데이터를 읽고, 나머지 공간은 0으로 채운다.
* - 페이지 폴트가 발생했을 때 최초로 호출되며,
* 해당 페이지의 프레임에 실제 내용을 로딩한다.
*/
static bool lazy_load(struct page *page, void *aux_)
{
// 1. 인자로 전달된 보조 정보 구조체 파싱
struct file_load *aux = (struct file_load *)aux_;
struct file *file = aux->file;
off_t ofs = aux->ofs; // 파일에서 읽기 시작할 오프셋
uint32_t read_bytes = aux->read_bytes; // 읽어야 할 바이트 수
uint32_t zero_bytes = aux->zero_bytes; // 남은 공간을 0으로 채울 바이트 수
free(aux); // aux는 더 이상 필요 없으므로 해제
// 2. 파일 시스템 락 획득 (다중 스레드 환경에서의 race condition 방지)
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// 3. 파일 위치를 오프셋으로 이동
file_seek(file, ofs);
// 4. 파일에서 실제 내용 읽기
read_bytes = file_read(file, page->frame->kva, read_bytes);
// 5. 락 해제
if (!is_lock_held)
lock_release(&filesys_lock);
// 6. 남은 영역을 0으로 초기화 (zero-fill)
memset(page->frame->kva + read_bytes, 0, zero_bytes);
return true;
}
file_backed_initializer()page->operations에 file_ops 설정
page->file에 load_info 복사
bool file_backed_initializer(struct page *page, enum vm_type type, void *kva)
{
// 이 페이지는 이제 file-backed 페이지가 되었음을 등록
page->operations = &file_ops;
// lazy_load_segment 등에서 넘겨준 파일 관련 정보(aux) 해석
struct file_load *aux = page->uninit.aux;
// file_page 구조체에 파일 관련 메타데이터 설정
struct file_page *file_page = &page->file;
file_page->file = aux->file; // 매핑된 파일
file_page->ofs = aux->ofs; // 읽기 시작할 오프셋
file_page->read_bytes = aux->read_bytes; // 실제 읽을 바이트 수
file_page->zero_bytes = aux->zero_bytes; // 나머지 0으로 채울 바이트 수
file_page->file_length = aux->file_length; // 전체 매핑 길이
// 페이지의 pml4 등록 (현재 스레드와 연결됨)
page->pml4 = thread_current()->pml4;
// 프레임의 page_list에 연결 → 해당 프레임이 여러 페이지와 연결될 수 있음 (mmap 공유)
list_push_back(&page->frame->page_list, &page->out_elem);
return true;
}
file_backed_swap_in()file_read_at()으로 내용 복원/* 파일 기반 페이지(file-backed page)를 swap-in 하는 함수
*
* [역할]
* - 해당 페이지가 물리 메모리에서 제거되어 swap-out된 이후,
* 다시 파일에서 내용을 읽어와 복구
* - file-backed 페이지들은 여러 VA에서 mmap으로 공유될 수 있으므로
* 연결된 다른 페이지들도 함께 프레임에 매핑 복원
*
* @param page 복원할 대상 페이지
* @param kva 페이지가 매핑될 커널 주소 (frame->kva와 동일)
* @return true always (읽기 실패 등은 별도로 처리하지 않음)
*/
static bool file_backed_swap_in(struct page *page, void *kva)
{
struct file_page *file_page = &page->file;
struct list *file_list = file_page->file_list; // 이 프레임을 공유했던 페이지 목록
struct frame *frame = page->frame;
// 파일 시스템 보호를 위해 락 획득
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// 실제 파일로부터 데이터를 읽어와 frame에 로드
file_read_at(file_page->file,
page->frame->kva,
file_page->read_bytes,
file_page->ofs);
if (!is_lock_held)
lock_release(&filesys_lock);
// 공유된 다른 가상 페이지들도 이 프레임에 다시 연결
while (!list_empty(file_list))
{
struct page *in_page = list_entry(list_pop_front(file_list), struct page, out_elem);
// 다시 frame에 연결 (page_list에 복원)
list_push_back(&frame->page_list, &in_page->out_elem);
// 해당 페이지를 MMU에 다시 매핑 (VA → frame->kva)
pml4_set_page(in_page->pml4, in_page->va, frame->kva, in_page->writable);
}
// file_list는 재사용하지 않으므로 해제
free(file_list);
return true;
}
file_backed_swap_out()file_write_at()으로 저장/* 파일 기반 페이지(file-backed page)를 swap-out 하는 함수
*
* [역할]
* - 프레임에 존재하던 file-backed 페이지들을 모두 제거
* - dirty 상태인 경우: 파일에 변경 내용 write-back
* - 이후 프레임과 페이지 간 연결 해제
* - 해당 프레임과 연결된 모든 페이지는 file_list에 저장됨 (swap-in용)
*
* @param page 현재 swap-out을 유도한 대표 페이지
* @return true 항상 true 반환
*/
static bool file_backed_swap_out(struct page *page)
{
struct file_page *file_page = &page->file;
// 1. 해당 프레임을 공유하는 모든 페이지를 모아둘 리스트 생성
struct list *file_list = malloc(sizeof(struct list));
list_init(file_list);
file_page->file_list = file_list;
struct frame *frame = page->frame;
// 2. 파일 시스템 동기화를 위한 락 획득
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// 3. 해당 프레임을 공유 중인 모든 페이지 처리
while (!list_empty(&frame->page_list))
{
struct page *out_page = list_entry(list_pop_front(&frame->page_list), struct page, out_elem);
// dirty 페이지인 경우만 파일에 write-back 수행
if (pml4_is_dirty(out_page->pml4, out_page->va))
{
file_write_at(out_page->file.file,
out_page->frame->kva,
out_page->file.read_bytes,
out_page->file.ofs);
}
// 공유 리스트(file_list)에 추가 (swap-in 시 복원용)
list_push_back(file_list, &out_page->out_elem);
// 해당 가상 주소와 물리 주소의 매핑 해제
pml4_clear_page(out_page->pml4, out_page->va);
}
// 4. 락 반환
if (!is_lock_held)
lock_release(&filesys_lock);
return true;
}
file_backed_destroy()pml4_clear_page()/* 파일 기반 페이지(file-backed page) 제거 함수
*
* [역할]
* - 해당 페이지가 dirty한 경우 파일에 write-back
* - 파일 close
* - 프레임의 참조 수 감소
* - 필요한 경우 MMU 매핑 해제
*
* 이 함수는 vm_dealloc_page() → destroy() → file_backed_destroy() 순으로 호출된다.
*/
static void file_backed_destroy(struct page *page)
{
struct file_page *file_page = &page->file;
// 1. 프레임 참조 카운트 감소 (공유된 프레임이므로 한 페이지 제거 시마다 감소)
page->frame->cnt_page -= 1;
// 2. 파일 동기화를 위한 락 획득 (중첩 락 방지)
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// 3. 페이지가 dirty 상태라면 파일에 write-back
if (pml4_is_dirty(thread_current()->pml4, page->va))
{
file_write_at(page->file.file,
page->frame->kva,
page->file.read_bytes,
page->file.ofs);
}
// 4. 해당 페이지가 참조하던 파일 닫기 (ref count 감소)
file_close(page->file.file);
// 5. 락 해제
if (!is_lock_held)
lock_release(&filesys_lock);
// 6. 아직 다른 페이지들이 같은 프레임을 공유 중이면 현재 페이지만 매핑 해제
if (page->frame->cnt_page > 0)
pml4_clear_page(thread_current()->pml4, page->va);
}
file_load)struct file_load {
struct file *file;
off_t ofs;
int file_length;
int read_bytes;
int zero_bytes;
};
→ lazy_load_segment, initializer 등에서 사용
sys_mmap()
└─ do_mmap()
├─ 페이지 쪼개기
├─ load_info 준비
├─ vm_alloc_page_with_initializer()
└─ file_backed_initializer()
└─ 첫 페이지에 mapped_page_count 저장
유저가 접근 → page fault 발생
└─ lazy_load()
├─ file_read_at() 호출
└─ memset() 0 패딩
sys_munmap()
└─ do_munmap()
├─ mapped_page_count만큼 순회
├─ dirty 여부 확인
├─ file_write_at()
└─ destroy(page)
mmap-* 계열 (직접 관련)| 테스트 이름 | 설명 |
|---|---|
mmap-read | mmap된 페이지에서 read 동작이 잘 되는지 |
mmap-write | mmap된 페이지에 write한 결과가 반영되는지 |
mmap-close | 파일 닫은 후에도 mmap 페이지에 접근 가능한지 |
mmap-unmap | 명시적으로 unmap 했을 때 자원이 정리되는지 |
mmap-overlap | 여러 영역 mmap 시 겹치는 영역 처리 |
mmap-twice | 같은 파일을 두 번 mmap 해도 문제 없는지 |
mmap-ro | read-only로 mmap한 페이지를 write하면 실패해야 함 |
mmap-exit | mmap한 상태에서 프로세스가 종료할 때 write-back 되는지 |
mmap-shuffle | mmap 영역이 메모리 할당 순서를 변경해도 잘 동작하는지 |
mmap-clean | dirty하지 않은 페이지는 write-back 하지 않아야 함 |
mmap-inherit | fork 후 자식이 mmap 영역을 상속받는지 |
mmap-bad-fd, mmap-bad-fd2, mmap-bad-fd3 | 잘못된 파일 디스크립터로 mmap 시도 시 처리 |
mmap-null | addr가 NULL인 경우 자동 배치되는지 |
mmap-over-code, mmap-over-data, mmap-over-stk | 이미 존재하는 영역 위에 mmap 시도 시 거절되는지 |
mmap-remove | 파일 삭제 후 mmap 영역 사용 가능 여부 |
mmap-zero | mmap 길이가 0일 경우 거절하는지 |
mmap-zero-len | 실제로 읽을 바이트가 0일 경우 대응 |
mmap-off | 파일 오프셋이 적용되는지 |
mmap-bad-off | 정렬되지 않은 offset일 경우 거절하는지 |
mmap-kernel | 커널 주소로 mmap 시도 시 거절되는지 |
lazy-* (lazy loading)
lazy-file ✅ → mmap된 페이지에 실제 접근 시 lazy_load_segment가 호출되는지 확인
lazy-anon ✅ → mmap은 아니지만, 비슷한 방식의 lazy loading 확인용
page-*, merge-*
page-merge-mm ❌ → mmap 페이지들이 page merging 시 잘 처리되는지
page-merge-stk, page-merge-seq, page-merge-par 등은 연관은 있지만 mmap 없이도 통과 가능
swap-*
swap-file, swap-anon, swap-iter ❌ → mmap된 페이지가 스왑 영역에 나갔다 다시 돌아올 때도 정상 동작해야 함 (최종 보완 단계)mmap/munmap 관련 함수들은 아직 구현중에 있는데, 기초 개념이 너무 안잡혀있는듯한 느낌이 들어서 정리를 해보고
다시 들여다보려고 한다.
마지막 프로젝트라 붕 뜨는 느낌이 있지만 끝까지 잘 마무리 해야겠다.