운영체제(구현) - 핀토스 - Virtual memory - Memory Mapped Files

연도·2024년 6월 12일
0

운영체제 이론&구현

목록 보기
17/19
post-thumbnail

파일을 기반으로 매핑하는 페이지. 즉, 이 페이지에 담기는 내용은 실제 디스크에 존재하는 파일의 데이터가 담긴다.

왜 매핑을 해야 하나요?

  • 파일을 메모리처럼 접근할 수 있게 되어서 파일 입출력의 효율성을 향상시킬 수 있다. 이렇게 파일과 매핑된 메모리 영역은 가상 주소 공간에서 접근 가능하며, 해당 영역에 대한 읽기 or 쓰기 작업 가능.
  • 쓰기 작업으로 인해 파일의 내용이 변경되면 매핑이 해제 될 때 디스크의 실제 파일에 해당 변경 사항이 반영된다

void syscall_handler(struct intr_frame *f) - 수정

// 3-4 mmf 추가
	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;
	}

Mapping

mmap - 매핑 x일 경우 리턴 + do_mmap 호출

매핑을 하면 안되는 경우 8가지

  1. addr x
  2. addr가 page-aligned되지 않은 경우
  3. offset이 page-aligned되지 않은 경우
  4. addr가 user 영역이 아닌 경우
  5. addr + length가 user 영역이 아닌 경우 (addr부터 파일의 내용을 매핑해나가면 마지막 주소는 (addr + length)가 되는데 이 주소도 user 영역 안에 존재해야 함.
  6. addr에 할당된 페이지가 이미 존재하는 경우
  7. fd에 해당하는 파일 x
  8. file의 길이가 0 or 0보다 작은 경우

코드

void *mmap(void *addr, size_t length, int writable, int fd, off_t offset)
{
	/* 매핑이 이루어질 수 없는 경우(8가지)  */
    if (!addr || addr != pg_round_down(addr)) // addr이 없거나 addr이 page-aligend 되지 않은 경우
        return NULL;

    if (offset != pg_round_down(offset)) // offset이 page-aligned되지 않은 경우
        return NULL;

    if (!is_user_vaddr(addr) || !is_user_vaddr(addr + length)) // addr or addr + length 가 > 유저 영역이 아닌 경우
        return NULL;

    if (spt_find_page(&thread_current()->spt, addr)) // addr에 할당된 페이지가 이미 존재하는 경우
        return NULL;

    struct file *f = process_get_file(fd);
    if (f == NULL) // fd에 해당하는 파일이 없는 경우
        return NULL;

    if (file_length(f) == 0 || (int)length <= 0) // file의 길이가 0 or 0보다 작은 경우
        return NULL;

	// 위의 경우를 제외하고, do_mmap() 호출하여 파일과 가상 주소간의 매핑 진행. 

    return do_mmap(addr, length, writable, f, offset); // 파일이 매핑된 가상 주소 반환
}

do_mmap

해야할 것

  • lazy loading으로 할당

fil-backed page도 anonymous page와 같이 lazy_loading 이용

page가 초기화될 때 파일에서 내용을 읽어올 수 있도록 lazy_load_segment 와 로딩할 때 필요한 데이터를 페이지에 저장

  • 파일에 대한 독립적 참조

유닉스 규칙

매핑은 매핑을 해제하는 munmap 이 호출 or 프로세스가 종료될 때까지 유효하다(파일을 닫거나 제거해도 매핑은 해제x)

  • 여러 페이지에 매핑되는 파일 처리

length가 한 페이지 사이즈를 넘어갈 경우에는 한 파일의 내용이 여러 페이지에 걸쳐서 매핑된다.

매핑 진행 시 addr(페이지 가상 주소) + PGSIZE 에 위치하는 다음 페이지에 이어서 매핑하면 된다.

매핑 해제 시 몇 번째 페이지까지 해제해야 하는지 알아야 함.

그렇기 때문에 총 몇 페이지를 사용해서 매핑이 이루어져야 하는지 page 구조체에 함께 저장.

코드

void *
do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset)
{
	// 1. 파일 재개방('f'에 저장. 이는 원본 파일의 상태 유지하기 위함.)
	struct file *f = file_reopen(file);

	// 2. 초기 변수 설정 
	void *start_addr = addr; // 매핑 성공 시 파일이 매핑된 가상 주소 반환하는데 사용.

	// 3. 총 페이지 수 계산(PGSIZE보다 작으면 1페이지, 그렇지 않으면 필요한 페이지 수 계산)
	int total_page_count = length <= PGSIZE ? 1 : length % PGSIZE ? length / PGSIZE + 1
																  : length / PGSIZE; // 이 매핑을 위해 사용한 총 페이지 수

	// 4 읽기 및 초기화 바이트 계산
	size_t read_bytes = file_length(f) < length ? file_length(f) : length;
	size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;

	// 5. 페이지 정렬 확인
	ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
	ASSERT(pg_ofs(addr) == 0); // upage가 페이지 정렬되어 있는지 확인.
	ASSERT(offset % PGSIZE == 0); // ofs가 페이지 정렬되어 있는지 확인  

	// 6. 매핑할 페이지 **생성**
	while (read_bytes > 0 || zero_bytes > 0) // 두 값이 모두 0이 될 때까지 루프를 돌며 페이지 할당.
	{
		/*
		6-1
		이 페이지를 채우는 방법 계산
		파일에서 PAGE_READ_BYTES 바이트를 읽고
		최종 PAGE_ZERO_BYTES 바이트를 0으로 채운다.
		*/
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		// 6-2 구조체 **초기화**
		struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
		lazy_load_arg->file = f;
		lazy_load_arg->ofs = offset;
		lazy_load_arg->read_bytes = page_read_bytes;
		lazy_load_arg->zero_bytes = page_zero_bytes;

		// 6.3 페이지 초기화(페이지를 대기 상태로 생성, 초기화 함수로 lazy_load_segment사용)
		if(!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, lazy_load_arg))
		{
			return NULL;
		}

		// 6.5 다음 페이지로 이동
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		addr += PGSIZE;
		offset += page_read_bytes;
	}

Unmapping

해야할 것

  • 매핑을 끊을 시, 수정사항이 존재한다면 수정된 사항을 반영해야 하므로 파일에 대한 정보 페이지 가지고 있어야 함.
  • 수정사항을 변경한 뒤, 프로세스의 가상 페이지 목록에서 페이지를 제거해서 매핑 해제한다.

file_backed_initializer

파일로부터 백업된 페이지 초기화.

해야할 것

  • 수정사항을 파일에 다시 기록하기 위해서는 매핑을 해제하는 시점에 매핑된 파일의 정보를 알아야 하기 때문에 이를 위해 file-back-page가 초기화될 때 호출되는 file_backed_initializer 에서 파일에 대한 정보를 page 구조체에 추가

코드

bool
file_backed_initializer (struct page *page, enum vm_type type, void *kva) {
	/* Set up the handler */
	// 1. 핸들러 설정
	page->operations = &file_ops;

	// 2. 파일 페이지 구조체 설정
	struct file_page *file_page = &page->file;

	// 3. lazy_load_arg 구조체 설정
	// 페이지가 초기화되지 않은 상태에서 전달된 추가 인자('aux'를 'lazy_load_arg' 구조체로 변환)
	struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)page->uninit.aux;

	// 4. 파일 페이지 정보 초기화
	file_page->file = lazy_load_arg->file;
	file_page->ofs = lazy_load_arg->ofs;
	file_page->read_bytes = lazy_load_arg->read_bytes;
	file_page->zero_bytes = lazy_load_arg->zero_bytes;
} 

file_backed_destroy

파일로 백업된 페이지 파괴. 페이지를 메모리에서 해제 and 페이지가 수정된 경우 해당 내용을 파일에 다시 기록

해야할 것

  • 프로세스가 종료될 때 마찬가지로 매핑이 해제되어야 하므로, 수정사항을 파일에 반영하고 가상 페이지 목록에서 페이지 제거.

코드

static void
file_backed_destroy (struct page *page)
{
	// 1. 'file_page' 구조체 가져오기
	struct file_page *file_page UNUSED = &page->file;

	// 2. 페이지가 수정되었는지 확인
	if (pml4_is_dirty(thread_current()->pml4, page->va))
	{
		// 3. 페이지가 수정('dirty 상태인 경우')된 경우 파일에 쓰기
		file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->ofs);
		pml4_set_dirty(thread_current()->pml4, page->va, 0); // 페이지의 'dirty' 상태 해제
	}

	// 4. 페이지를 페이지 테이블에서 제거
	pml4_clear_page(thread_current()->pml4, page->va);
}

munmap → do_munmap을 부른다

do_munmap

매핑된 파일의 메모리 영역을 해제

코드

void
do_munmap (void *addr)
{
	// 1. spt 및 페이지 찾기.
	struct supplemental_page_table *spt = &thread_current()->spt;
	struct page *p = spt_find_page(spt, addr);

	// 2. 매핑된 페이지 수 가져오기
	int count = p->mapped_page_count;

	// 3. 매핑 해제 루프
	for (int i = 0; i < count; i++)
	{	
		// 존재할 경우
		if(p)
		{
			destroy(p);
		}

		addr += PGSIZE;
		p = spt_find_page(spt, addr); // spt에서 새로운 주소('addr')에 해당하는 다음 페이지 찾기
	}
}

supplemental_page_table_copy

자식 프로세스가 부모 프로세스의 실행 컨텍스트를 상속하는 과정에서 SPT를 복사할 때, file_backed_page는 자식 페이지가 부모의 프레임과 매핑되도록 분기를 추가

코드

/* 
		자식 프로세스가 부모 프로세스의 실행 컨텍스트를 상속하는 과정에서 SPT를 복사할 때,
		file_backed_page는 자식 페이지가 부모의 프레임과 매핑되도록 분기 추가.
		 */
		// 2. type이 file이면
		if(type == VM_FILE)
		{
			struct lazy_load_arg *file_aux = malloc(sizeof(struct lazy_load_arg));
			file_aux->file = src_page->file.file;
			file_aux->ofs = src_page->file.ofs;
			file_aux->read_bytes = src_page->file.read_bytes;
			file_aux->zero_bytes = src_page->file.zero_bytes;
			
			if(!vm_alloc_page_with_initializer(type, upage, writable, NULL, file_aux))
			{
				return false;
			}

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

			file_backed_initializer(file_page, type, NULL);
			file_page->frame = src_page->frame;
			pml4_set_page(thread_current()->pml4, file_page->va, src_page->frame->kva, src_page->writable);
			continue;
		}

0개의 댓글