이전까지 파일을 읽거나 쓰거나 등의 파일 관련 작업을 하기 위해서는 시스템 콜을 사용해야 했다. 시스템 콜을 사용하면 cpu를 사용하게 되고 이는 비용으로 연결된다. 그래서 운영체제에서는 mmap 콜을 이용하여 가상 메모리를 파일에 매핑시킨다. 만약 매핑되어있는 가상 메모리에 접근하면 바로 파일에 접근하여 작업을 수행 할 수 있다. mmap은 interprocess communication에도 사용된다 한다. 주소만 공유하면 공유가능해서 인것 같다. 이러한 방식을 사용하면 메모리에 파일을 전부 올릴 필요도 없고 시스템 콜을 사용하지 않으므로 속도가 빠르다. 단 파일을 페이지 단위로 매핑하기 때문에 크기가 매우 큰 파일이거나 파일을 읽고 쓰는 오버헤드를 줄인 파일들은 오히려 느릴 수도 있다.(ex 데이터 베이스) 핀토스에서 이를 구현하기 위해 file.c를 수정하며 mmap 과 unmmap 시스템 콜을 만들어 주어야 한다. 그리고 spt를 추가하고 페이지 폴트를 수정했기 때문에 기존에 사용하던 시스템 콜 보조 함수들을 수정해야 한다.

/* syscall.c */
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
#ifdef VM
thread_current()->rsp_stack = f->rsp;
#endif
switch (f->R.rax)
{
...
case SYS_READ:
check_valid_buffer(f->R.rsi,f->R.rdx,f->rsp,1);
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
check_valid_buffer(f->R.rsi,f->R.rdx,f->rsp,0);
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
...
case SYS_MMAP:
f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8);
break;
case SYS_MUNMAP:
munmap(f->R.rdi);
break;
...
}
우선 stack 확장을 위해 인터럽트 프레임에 있는 스택 포인터를 스레드 구조체의 rsp_stack 변수에 저장한다. 그리고 버퍼를 사용하는 시스템 콜에서 버퍼의 유효성을 확인한다. 그리고 기존의 check_address를 spt를 사용하는 방법으로 수정한다.
/* syscall.c */
struct page * check_address(const uint64_t *uaddr){
struct thread *cur = thread_current();
/* 유저 영역이 아니면 강제 종료 */
if(is_kernel_vaddr(uaddr)){
exit(-1);
}
/* spt에 존재하는지 확인 */
return spt_find_page(&cur->spt,uaddr);
}
void check_valid_buffer(void* buffer, unsigned size, void* rsp, bool to_write) {
for (int i = 0; i < size; i++) {
/* 버퍼가 spt에 존재 하는지 확인 */
struct page* page = check_address(buffer + i);
/* 버퍼가 spt에 없으면 강제 종료 */
if(page == NULL)
exit(-1);
/* 버파가 읽기 전용인데 쓰려고 한다면 강제 종료 */
if(to_write == true && page->writable == false)
exit(-1);
}
}
struct file *process_get_file(int fd) {
struct thread *curr = thread_current();
struct file* fd_file = curr->fdTable[fd];
if(fd_file)
return fd_file;
else
return NULL;
}
mmap 과 munmap 콜에서 주소나 파일 식별자를 확인한 다음 do_mmap과 do_munmap을 호출한다.
/* syscall.c */
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) {
if (offset % PGSIZE != 0) {
return NULL;
}
/* addr은 페이지 시작 주소이며 유저 영역 안에 있어야 한다.*/
if (pg_round_down(addr) != addr || is_kernel_vaddr(addr) || addr == NULL || (long long)length <= 0)
return NULL;
/* stdin, stdout은 제외한다.*/
if (fd == 0 || fd == 1)
exit(-1);
/* 기존에 매핑되어 있는 페이지면 안된다. */
if (spt_find_page(&thread_current()->spt, addr))
return NULL;
struct file *target = process_get_file(fd);
/* 존재하는 파일을 매핑해야 한다.*/
if (target == NULL)
return NULL;
/* do_mmap 호출한다. */
void * ret = do_mmap(addr, length, writable, target, offset);
return ret;
}
void munmap (void *addr) {
do_munmap(addr);
}
do_mmap과 do_munmap이 동작하는 과정을 살펴보면 다음과 같다.
/* vm/file.c */
void *
do_mmap (void *addr, size_t length, int writable,
struct file *file, off_t offset) {
struct file *mfile = file_reopen(file);
void * ori_addr = addr;
size_t read_bytes = length > file_length(file) ? file_length(file) : length;
size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;
while (read_bytes > 0 || zero_bytes > 0) {
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
struct container *container = (struct container*)malloc(sizeof(struct container));
container->file = mfile;
container->offset = offset;
container->page_read_bytes = page_read_bytes;
if (!vm_alloc_page_with_initializer (VM_FILE, addr, writable, lazy_load_segment, container)) {
return NULL;
}
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
addr += PGSIZE;
offset += page_read_bytes;
}
return ori_addr;
}
void
do_munmap (void *addr) {
while (true) {
struct page* page = spt_find_page(&thread_current()->spt, addr);
if (page == NULL)
break;
struct container * aux = (struct container *) page->uninit.aux;
// dirty(사용되었던) bit 체크
if(pml4_is_dirty(thread_current()->pml4, page->va)) {
file_write_at(aux->file, addr, aux->page_read_bytes, aux->offset);
pml4_set_dirty (thread_current()->pml4, page->va, 0);
}
pml4_clear_page(thread_current()->pml4, page->va);
addr += PGSIZE;
}
}
do_mmap은 load_segment와 함수가 동일하다. 생각해보면 파일 주소를 가상 메모리 주소와 매핑하는 것이기 때문에 동작이 동일하다. 다만 do_munmap은 spt를 순회하며 만약 파일 내용을 수정했다면 디스크에도 해당 내용을 수정하며 pml4의 내용을 초기화 하며 dirty bit를 다시 0으로 초기화 시킨다. 다른 프로세스에서 해당 페이지를 다시 사용할 것이기 때문이다.