WIL - PintOS:Project 3 / VM - Memory Mapped Files

박상우·2024년 6월 29일
0

📝 TIL

목록 보기
42/45
post-thumbnail

⚽️ Main Goal

  • Memory Mapped Files는 File Backed Mapping를 의미
  • page fault 발생시 물리적 프레임이 할당되고 파일이 메모리로 복사
  • Memory Mapped Filed이 unmapped 또는 Swapped Out 되면 모든 변경 사항이 반영

Memory Mapped File는 파일을 기반으로 매핑하는 페이지이다.

페이지 내부에는 실제 파일 데이터가 담겨있다.

mmap()의 작동과정

  • mmap() 실행 후, 가상 메모리 주소에 file 주소 매핑 ( 페이지 테이블 pml4에 매핑 )
    • 매핑만 되어있고, 로드는 X (Lazy Load 적용)
  • 메모리에 접근할 때, 파일 데이터를 복사하여 물리 메모리에 업로드
  • Read( 읽을 때 ) - 가상 메모리에 매핑되어 있는 물리 메모리 데이터를 읽음
  • Write( 쓸 때 ) - 물리 메모리 데이터를 수정, 가상 메모리의 수정 여부를 판단하는 flag(dirty)를 수정
  • 프로세스가 종료되거나, exit() 시스템 콜이 호출되면 메모리를 해제한다.
    • 해제시 파일 수정 여부를 확인하고, 수정이 되었다면 해당 부분은 원본 파일에 반영한다.

⚙️ 구현하기

mmap and munmap System Call

☑️ mmap

void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
  • fd로 열린 파일의 오프셋부터 length 만큼을 가상 주소 공간 addr위치에 매핑
  • addr에서 시작하는 연속적인 가상 페이지에 매핑
  • stick out → 만약 파일의 길이가 페이지 크기(PGSIZE)의 배수가 아닌 경우, 마지막 페이지의 일부 바이트가 파일 끝을 넘어서게 됩니다. 이 부분은 페이지 폴트가 발생했을 때 0으로 설정하고, 페이지를 다시 디스크에 쓸 때는 무시됩니다.
  • 성공시 매핑된 가상 주소를 반환, 실패하면 NULL을 반환
  • 매핑 실패 케이스 예외처리
    • fd로 열린 파일의 크기가 0인 경우
    • length가 0인 경우
    • addr이 page_aligned 되지 않은 경우
    • 기존 매핑된 페이지 집힙과 겹치는 경우
    • addr이 0 인경우 → Linux에서는 addr이 NULL인 경우 적절한 주소를 찾아서 매핑하지만 PintOS에서는 addr에서 매핑을 시도하기 때문에 addr이 0인 경우 실패, 가상 페이지 0은 매핑되지 않는다.
    • fd == 0 or fd == 1인 경우
  • lazy load로 할당
    • vm_alloc_page_with_initializer
    • vm_alloc_page
// syscall.c

...
switch (syscall_num)
{
	...

	**case SYS_MMAP: // system call에 MMAP 함수 매핑
		f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8);
		break;**
}
// syscall.c
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) {
	// - length가 0인 경우, addr이 0인 경우, fd == 0 or fd == 1인 경우
	if ( length <= 0 || addr == 0 || fd == 0 || fd == 1 )
		return NULL;

	// - addr이 page_aligned 되지 않은 경우
	if (pg_ofs(addr) != 0)
		return NULL;

	// - addr이 커널영역의 주소라면 (시작 주소와, 끝 주소를 모두 검사)
	if (!is_user_vaddr(addr) || !is_user_vaddr(addr + length - 1))
		return NULL;

	// - fd로 열린 파일의 크기가 0인 경우
	struct file* f = process_get_file(fd);
  if ( f == NULL || file_length(f) == 0 )
		return NULL;

	// - 기존 매핑된 페이지 집힙과 겹치는 경우
	struct thread *curr = thread_current();
	void *end_addr = addr + length;
	for (void *va = addr; va < end_addr; va += PGSIZE) {
		struct page p;
		struct hash_elem *e;

		p.va = va;
		e = hash_find(&curr -> spt, &p.elem);

		if (e != NULL)
			return NULL;
	}

	return do_mmap(addr, length, writable, f, offset);
}
// file.c
/* Do the mmap */
void * do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset) {

 	// 동일한 매핑에 대해서 서로다른 프로세스의 결과물이 서로에게 영향을 주면 바람직하지 않다.
	// 사용할 때 `file_reopen`을 통해서 새로운 파일 디스크립터를 통해 파일을 생성하면 다른 매핑에 영향을 주지않고 독립적인 파일 핸들링이 가능해진다.
	struct file *reopen_f = file_reopen(file);

	off_t fl = file_length(reopen_f);

	size_t read_bytes = fl < length ? fl : length ; //매핑할 총 바이트 수로 초기화
	size_t zero_bytes = PGSIZE - read_bytes % PGSIZE; // 마지막에 0으로 채울 바이트의 크기

	int page_count = length <= PGSIZE ? 1 : length % PGSIZE ? length / PGSIZE + 1 // 연속적으로 매핑된 페이지의 총 개수
                                                          : length / PGSIZE;    // 모든 페이지에 일괄적으로 동일한 페이지 개수를 전달하기 위해 미리 계산해서 넘겨줌

	void *start_addr = addr; // 시작 주소 리턴을 위해 저장

	ASSERT((read_bytes + zero_bytes) % PGSIZE  == 0)
	ASSERT(pg_ofs(addr) == 0)
	ASSERT(offset % PGSIZE == 0)

	// 총 길이 read_bytes(length)만큼 page 단위로 반복하면서 가상 페이지에 매핑 
	while ( read_bytes > 0 || zero_bytes > 0 ) {
		
		// 최대 페이지 크기 만큼, 읽을 파일 데이터의 크기를 결정
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		// 읽은 데이터의 크기와 결합하여 페이지 단위를 맞출 0값의 크기를 계산
		size_t page_zero_bytes = PGSIZE - page_read_bytes;
	
		// page initialize를 위한 인자 전달
		struct aux_struct *aux = (struct aux_struct *)malloc(sizeof(struct aux_struct)); // aux( lazy_load_segment를 위한 데이터 )를 저장할 공간을 동적할당.
		if (aux == NULL)
			return false;
		
		// 파일 관련 데이터를 aux 인자에 적재
		aux -> file = reopen_f;
		aux -> offset = offset;
		aux -> read_bytes = page_read_bytes;
		aux -> zero_bytes = page_zero_bytes;
		
		// File Backed Page를 Lazy Loading을 활용하여 초기화
		if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, aux))
			return NULL;
		
		// 파일에 할당한 페이지를 연쇄적으로 해제하기 위해 분리한 파일의 개수를 각 파일 구조체에 저장
		struct page * p = spt_find_page(&thread_current() -> spt, start_addr);
		p -> page_count = page_count;
		
		
		// 페이지에 적재할 다음 파일 데이터로 이동
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		offset += page_read_bytes;
		addr += PGSIZE;
	}

	return start_addr;
}

여기서 왜 mmap과 do_mmap을 구분해두었을까?

  • 각 함수의 구조를 통해서 책임을 분리하기 위해서 사용한다. mmap 함수 내부에서는 입력값의 유효성, 초기설정을 처리하는 부분이고 실제 우리가 생각하는 파일 데이터를 실제 메모리와 매핑하는 작업은 do_mmap에서 수행한다.

mmap을 통해 파일 데이터들은 연속된 페이지에 저장된다. 이후 munmap에서도 동일하게 연속된 페이지를 삭제해주어야하기 때문에, 페이지 구조체 내부에 연속된 페이지 개수를 의미하는 어떠한 추가적인 멤버변수가 필요하다.

// vm.h
struct page {
	...
	int page_count;
	...
}

추가적으로 여기서 lazy_load_segment()함수를 사용하기 위해서 기존 static 함수였던 lazy_load_segment

를 static하지 않은 함수로 변경하고 헤더파일에 선언하여 import 가능하게 한다.

// process.h
bool lazy_load_segment(struct page *page, void *aux);

// process.c
bool lazy_load_segment(struct page *page, void *aux) {
	...
}

☑️ munmap

void munmap (void *addr);
  • 지정된 주소 범위 addr에 대한 매핑을 해제
    • 연속적으로 할당된 페이지에 대해서 반대로 연속적으로 해제
// syscall.c

...
switch (syscall_num)
{
	...
	case SYS_MUNMAP:  **// system call에 MMAP 함수 매핑**
		munmap(f->R.rdi);
		break;
}
// syscall.c
void munmap(void *addr) { 
	if (addr == NULL || is_user_vaddr(addr))
		return NULL;

	do_munmap(addr);
}
// file.c
/* Do the munmap */
void do_munmap (void *addr) { // - addr는 매핑 해제되지 않은 프로세서의 mmap에 대한 이전 호출에서 반환된 가상 주소
	uintptr_t cur_addr = addr;
	struct thread *curr = thread_current();
	struct page *p = spt_find_page(&curr -> spt, cur_addr); // 매핑 해제할 페이지 조회

	// 매핑된 페이지를 페이지 개수만큼 순회
	// addr의 경우 항상 매핑된 File Backe Page의 첫 페이지 주소를 가리키고 있기 떄문에 페이지 수만큼 반복문을 실행
	for (int c = 0; c < p -> page_count; c++) {
		// 페이지 제거
		if (p)
			destroy(p);

		// 다음 페이지 확인
		cur_addr += PGSIZE;
		p = spt_find_page(&curr -> spt, cur_addr);
	}

}

☑️ 관련 추가 구현

File Backed Page를 핸들링하기 위해서 추가적인 file_page의 멤버 변수와 이를 초기화 하는 initializer 함수의 추가 개선이 필요하다.

File Backed Page가 초기화 하기 위해

  • struct file* file; → 대상 파일 데이터
  • off_t offset; → 파일을 읽을 위치
  • size_t read_bytes; → offset으로 부터 읽을 데이터의 크기
  • size_t zero_bytes; → 페이지 정렬을 위해 채울 0 데이터 크기
    ( 사용하는 곳에서 PGSIZE - read_bytes 계산값으로 대체 가능)

를 file_page 구조체의 멤버 변수로 추가한다.

// file.h
struct file_page {
	struct file* file;
	off_t offset;
	size_t read_bytes;
	size_t zero_bytes;
};

File Backed Page를 초기화하는 함수를 구성하는 file_backed_initializer 함수를 수정한다. 인자로 받은 aux를 현재 페이지에 할당된 파일 데이터로 초기화 인자들을 복사한다.

// file.c
/* Initialize the file backed page */
bool
file_backed_initializer (struct page *page, enum vm_type type, void *kva) {
	/* Set up the handler */
	page->operations = &file_ops;

	struct file_page *file_page = &page->file;

	struct aux_struct *aux = (struct aux*)page -> uninit.aux;
	file_page -> file = aux -> file;
	file_page -> offset = aux -> offset;
	file_page -> read_bytes = aux -> read_bytes;
	file_page -> zero_bytes = aux -> zero_bytes;

	return true;
}

munmap 과정에서 실행될 file_backed_destroy 함수를 수정한다.

File Backe Page가 메모리에서 해제될 때, pml4의 dirty 인자를 확인하여 수정되었다면 원본 파일에 수정내용을 반영하고 pml4 매핑 해제, 메모리에서 삭제한다.

/* Destory the file backed page. PAGE will be freed by the caller. */
static void
file_backed_destroy (struct page *page) {
	struct thread *curr = thread_current();

	struct file_page *file_page UNUSED = &page->file;

	if (pml4_is_dirty(curr -> pml4, page -> va)) { // 파일이 dirty한 상태이면 수정된 부분 기록

		lock_acquire(&filesys_lock);

		file_write_at(file_page -> file, page -> va, file_page -> read_bytes, file_page -> offset); // 수정된 파일 페이지 만큼 파일 부분 수정
		pml4_set_dirty(curr -> pml4, page -> va, 0); // 페이지의 dirty 인자 수정

		lock_release(&filesys_lock);
	}

	pml4_clear_page(curr -> pml4, page -> va); // 페이지 제거
}

File Backed Page를 대상으로 fork하는 경우 타입에 맞게 자식 페이지를 생성할 수 있도록 페이지의 초기화 인자(aux)를 복사하고, initializer함수를 직접 호출하여 페이지를 생성한다. 이후 직접 매핑까지 완료한다.

// vm.c
/* Copy supplemental page table from src to dst */
bool supplemental_page_table_copy (struct supplemental_page_table *dst UNUSED,
	struct supplemental_page_table *src UNUSED)
{
	...
	else if ( type == VM_FILE ) {
			// 자식 페이지 초기화를 위한 부모 페이지의 정보 복사
			struct aux_struct *aux = (struct aux*)malloc(sizeof(struct aux_struct));
			aux -> file = curr_page -> file.file;
			aux -> offset = curr_page -> file.offset;
			aux -> read_bytes = curr_page -> file.read_bytes;
			aux -> zero_bytes = curr_page -> file.zero_bytes;
			
			// 자식 파일 페이지 생성을 위한 초기화를 위한 객체 생성
			if(!vm_alloc_page_with_initializer(VM_FILE, upage, writable, NULL, aux))
				return false;

			struct page *file_page = spt_find_page(dst, upage);

			// file-backed page 초기화
			file_backed_initializer(file_page, type, NULL); 

			// 기존 부모 프로세스가 사용하는 프레임과 연결 -> 두 프로세스가 동일한 파일을 매핑하는 경우 물리 프레임을 공유
			file_page -> frame = curr_page -> frame; 
			pml4_set_page(thread_current() -> pml4, file_page -> va, curr_page -> frame -> kva); 

			continue;
		}
	...
}

🎉 결과

pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
pass tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
pass tests/vm/pt-write-code2
pass tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
**FAIL tests/vm/page-merge-par
FAIL tests/vm/page-merge-stk
FAIL tests/vm/page-merge-mm**
pass tests/vm/page-shuffle
pass tests/vm/mmap-read
pass tests/vm/mmap-close
pass tests/vm/mmap-unmap
pass tests/vm/mmap-overlap
pass tests/vm/mmap-twice
**FAIL tests/vm/mmap-write**
pass tests/vm/mmap-ro
pass tests/vm/mmap-exit
pass tests/vm/mmap-shuffle
pass tests/vm/mmap-bad-fd
pass tests/vm/mmap-clean
pass tests/vm/mmap-inherit
pass tests/vm/mmap-misalign
pass tests/vm/mmap-null
pass tests/vm/mmap-over-code
pass tests/vm/mmap-over-data
pass tests/vm/mmap-over-stk
pass tests/vm/mmap-remove
pass tests/vm/mmap-zero
pass tests/vm/mmap-bad-fd2
pass tests/vm/mmap-bad-fd3
pass tests/vm/mmap-zero-len
**FAIL tests/vm/mmap-off
FAIL tests/vm/mmap-bad-off
pass** tests/vm/mmap-kernel
pass tests/vm/lazy-file
pass tests/vm/lazy-anon
**FAIL tests/vm/swap-file
FAIL tests/vm/swap-anon
FAIL tests/vm/swap-iter
FAIL tests/vm/swap-fork**
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
**FAIL tests/filesys/base/syn-read**
pass tests/filesys/base/syn-remove
**FAIL tests/filesys/base/syn-write**
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
**FAIL tests/vm/cow/cow-simple**
13 of 141 tests failed.
profile
나도 잘하고 싶다..!

0개의 댓글