정글사관학교 개발일지/운영체제-PintOS

PintOS Project 3 - Virtual Memory (4) Memory Mapped Files (정글사관학교 81일차 TIL)

Woonys 2022. 1. 22. 20:46
반응형

0. Memory Mapped files 개념 정리

이전 파트에서 다뤘던 anonymous page는 특정 파일에 매핑되지 않은 페이지를 다뤘다면, 이번에는 특정 파일과 매핑된(==file-backed) 페이지를 다룬다. 따라서 memory-mapped file은 곧 file-backed page와 연결된 파일을 뜻한다.

 

이 페이지 안의 내용은 이미 디스크에 존재하는 파일의 데이터를 복제하기 때문에 page fault가 발생했을 때 즉시 디스크->프레임에 할당된다. 메모리가 해제되거나 swap-out되면 메모리 내 변경 사항은 파일로 반영된다.

 

깃북 설명 링크는 여기.

여기서는 mmap과 munmap 함수를 구현한다.

 

mmap은 이전에 malloc 공부할 때 잠시 다루기도 했는데, 그때는 단순히 페이지 크기로 메모리를 할당해주는 함수라고만 간단히 이해하고 넘어갔다. 여기서 좀 더 자세히 다뤄보자.

 

gitbook에서는 mmap을 이렇게 설명한다.

 

void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
파일 디스크립터 fd로 오픈한 파일을 offset byte 위치에서부터 시작해 length 바이트 크기만큼 읽어들여 addr에 위치한 프로세스 가상 주소 공간에 매핑한다. 전체 파일은 페이지 단위로 나뉘어 연속적인 가상 주소 페이지에 매핑된다.

즉, mmap()은 메모리를 페이지 단위로 할당받는 시스템 콜이다. mmap()에 대한 보다 자세한 설명은 linux manual의 설명을 참조하자.

 

참조: mmap과 malloc 차이

 

만약 읽어들이는 파일 길이(offset부터 length만큼 읽어오니)가 PGSIZE로 나눠 떨어지지 않는다면 남는 데이터가 발생할 것이다. 이 남는 데이터를 마지막 페이지에 매핑하면 페이지에 빈 공간(아래에서 파란색 영역)이 남는데, 여기에는 0으로 초기화해줄 필요가 있다. 왜냐? 이전에 이 페이지 영역에서 작업했던 기록이 남아있을 수 있는데, 이를 현재 프로세스의 페이지가 접근해서 읽어들이면 보안 상의 문제가 발생하기 때문이다. 따라서 여기는 반드시 0으로 초기화해주고 추후 파일을 작업하는 과정에서 수정 내역이 발생하면 메모리에서 디스크로 파일을 업데이트해줄 텐데, 이때 파란색 영역은 버려진다.

 

 

1. mmap() 시스템 콜 관련 함수 구현

 

mmap() 구현

 

mmap()부터 구현해보자. 인자로 받은 내용을 검사하고 적합한 접근이면 do_mmap()을 호출한다. 실패하는 경우 역시 존재하는데,

 

  • 파일의 시작점(offset)이 page-align되지 않았을 때
    • 이는 빠른 접근을 위한 제약사항이다. spt_find_page()를 이용해 가상 주소에 접근하면 그 주소가 포함된 페이지만 리턴으로 받는다. OS는 단일 주소 위치가 아닌 가상 메모리 내 페이지 단위로 접근해 정보를 읽어들인다. 파일 역시 페이지 단위로 load/save되는 것을 위의 mmap에 대한 설명을 통해 알 수 있다. 그러니 시작점이 page-align(파일 시작점이 PGSIZE의 배수와 일치해야 페이지의 제일 윗부분부터 채워질테니)되지 않으면 페이지 단위로 접근하지 못하는 상황이 발생(페이지 윗 부분이 떠버림)
  • 가상 유저 page 시작 주소가 page-align되어있지 않을 때
  • 매핑하려는 페이지가 이미 존재하는 페이지와 겹칠 때(==SPT에 존재하는 페이지일 때)
  • 페이지를 만들 시작 주소 addr이 NULL이거나 파일 길이가 0일 때
  • 콘솔 입출력과 연관된 파일 디스크립터 값(0: STDIN, 1:STDOUT)일 때

위를 반영해 예외 케이스가 뜨면 NULL을 반환하고 그렇지 않으면 제공되는 함수인 do_mmap()을 실행후 결괏값을 반환한다.

 

void* mmap(void *addr, size_t length, int writable, int fd, off_t offset) {
	
	/* failure case 1: 파일 시작점이 page-align되어 있는지 체크 */
	if (offset % PGSIZE != 0) {
        return NULL;
    }

	/* failure case 2: 해당 주소의 시작점이 page-align되어 있는지 & user 영역인지 & 주소값이 null인지 & length가 0이하인지*/
    if (pg_round_down(addr) != addr || is_kernel_vaddr(addr) || addr == NULL || (long long)length <= 0)
        return NULL;
	/* failure case 3: 콘솔 입출력에 해당하는지(STDIN/STDOUT) */
    if (fd == 0 || fd == 1)
        exit(-1);
    
    // vm_overlap
    if (spt_find_page(&thread_current()->spt, addr))
        return NULL;

    //struct file *target = process_get_file(fd);
	struct file *target = find_file_by_fd(fd);

    if (target == NULL)
        return NULL;


    return do_mmap(addr, length, writable, target, offset);
}

 

do_mmap() 구현

 

mmap()이 파일에 가상 페이지 매핑을 해줘도 적합한지를 체크해주는 함수였다면, do_mmap()은 실질적으로 가상 페이지를 할당해주는 함수이다. 인자로 받은 addr부터 시작하는 연속적인 유저 가상 메모리 공간에 페이지를 생성해 file의 offset부터 length에 해당하는 크기만큼 파일의 정보를 각 페이지마다 저장한다. 프로세스가 이 페이지에 접근해서 page fault가 뜨면 물리 프레임과 매핑(이때 claim을 사용한다)해 디스크에서 파일 데이터를 프레임에 복사한다.

 

이전에 다뤘던 load_segment()와 유사한 로직이다. 차이점은

1) load_segment()가 파일의 정보를 담은 uninit 타입 페이지를 만들 때 파일 타입을 VM_FILE로 선언해주는 것과

2) 매핑이 끝난 후 연속된 유저 페이지 나열의 첫 주소를 리턴한다는 점

이 있다.

 

load_segment()

더보기
static bool load_segment (struct file *file, off_t ofs, uint8_t *upage,
		uint32_t read_bytes, uint32_t zero_bytes, bool writable) 
{
	ASSERT ((read_bytes + zero_bytes) % PGSIZE == 0);
	ASSERT (pg_ofs (upage) == 0);
	ASSERT (ofs % PGSIZE == 0);

	/* upage 주소부터 1페이지 단위씩 UNINIT 페이지를 만들어 프로세스의 spt에 넣는다(vm_alloc_page_with_initializer).
	   이 때 각 페이지의 타입에 맞게 initializer도 맞춰준다. */
	while (read_bytes > 0 || zero_bytes > 0) {
		/* 1 Page보다 같거나 작은 메모리를 한 단위로 해서 읽어 온다.
		   페이지보다 작은 메모리를 읽어올때 (페이지 - 메모리) 공간을 0으로 만들 것이다. */
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		/* 새 UNINIT 페이지를 만들어서 현재 프로세스의 spt에 넣는다. 
		   페이지에 해당하는 파일의 정보들을 container 구조체에 담아서 AUX로 넘겨준다.
		   타입에 맞게 initializer를 설정해준다. */
		struct container *container = (struct container *)malloc(sizeof(struct container));
		container->file = file;
		container->page_read_bytes = page_read_bytes;
		container->offset = ofs;

		if (!vm_alloc_page_with_initializer (VM_ANON, upage, 
				writable, lazy_load_segment, container))
			return false;
		// page fault가 호출되면 페이지가 타입별로 초기화되고 lazy_load_segment()가 실행된다. 

		/* Advance. */
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes;
	}
	return true;
}

 

/* Do the mmap */
void *
do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset) {
			struct file *mfile = file_reopen(file);

  void * start_addr = addr; // 시작 주소

  
  /* 주어진 파일 길이와 length를 비교해서 length보다 file 크기가 작으면 파일 통으로 싣고 파일 길이가 더 크면 주어진 length만큼만 load*/
  size_t read_bytes = length > file_length(file) ? file_length(file) : length;
  size_t zero_bytes = PGSIZE - read_bytes % PGSIZE; // 마지막 페이지에 들어갈 자투리 바이트

	/* 파일을 페이지 단위로 잘라 해당 파일의 정보들을 container 구조체에 저장한다.
	   FILE-BACKED 타입의 UINIT 페이지를 만들어 lazy_load_segment()를 vm_init으로 넣는다. */
	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;
		// 여기서는 페이지 할당을 FILE-BACKED로 해줘야 하니 아래 VM_FILE로 넣어준다.
		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 start_addr;

}

 

이렇게 만들어진 페이지를 유저 프로세스가 접근하여 page fault가 일어나면 해당 페이지를 초기화(file-backed) 및 claim하여 물리 프레임과 서로 연결시켜주고, lazy_load_segment()가 호출되어 디스크에 있는 파일을 해당 페이지와 매핑되어 있는 물리 프레임에 복사하게 된다.

 

2. munmap() 시스템 콜 관련 함수 구현

munmap() 함수는 우리가 지우고 싶은 주소 addr 로부터 연속적인 유저 가상 페이지의 변경 사항을 디스크 파일에 업데이트한 뒤, 매핑 정보를 지운다. 여기서 중요한 점은 페이지를 지우는 게 아니라 present bit을 0으로 만들어준다는 점이다. 따라서 munmap() 함수는 정확히는 지정된 주소 범위 addr에 대한 매핑을 해제하는 함수라고 봐야겠다.

 

이때 인자로 들어오는 주소 addr은 아직 매핑이 해제되지 않은 같은 프로세스에서 mmap 호출에 의해 반환된 가상 주소여야 한다. 뭔 말이냐, 매핑을 해제하려는 페이지는 mmap()으로 할당된 가상 페이지여야 하며, 이 페이지는 mmap()으로 할당할 당시의 유저 프로세스와 동일한 프로세스여야 한다는 뜻. 다른 프로세스가 갑자기 와서 자기가 mmap()으로 페이지 할당해주지도 않았는데 munmap()을 하면 안되지 않겠나.

 

Dirty Bit

Dirty bit은 해당 페이지가 변경되었는지 여부를 저장하는 비트이다. 페이지가 변경될 때마다 이 비트는 1이 되고, 디스크에 변경 내용을 기록하고 나면 해당 페이지의 dirty bit는 다시 0으로 초기화해야 한다. 즉, 변경하는 동안에 dirty bit가 1이 된다.

 

페이지 교체 정책에서 dirty bit이 1인 페이지는 디스크에 접근해 업데이트한 뒤 매핑을 해제해줘야 하기 때문에 비용이 비싸다. (디스크 한 번 갔다 와야 하니까). 따라서 페이지 교체 알고리즘은 dirty page 대신 변경되지 않은 상태인 깨끗한(?) 페이지를 내보내는 것을 선호한다. 이 경우는 그냥 swap out만 해주면 되니까.

 

Present Bit

해당 페이지가 물리 메모리에 매핑되어 있는지 아니면 swap out되었는지를 가리킨다. Swap in/out에서 더 다룰 것. present bit이 1이면 해당 페이지가 물리 메모리 어딘가에 매핑되어 있다는 말이며 0이면 디스크에 내려가(swap out)있다는 말이다. 이렇게 present bit이 0인, 물리 메모리에 존재하지 않는 페이지에 접근하는 과정이 file-backed page에서의 page fault이다.

 

munmap() 구현

 

간단하다. do_munmap()을 호출한다. 사실상 wrapper 함수.

 

@/userprog/syscall.c

void munmap(void *addr){
	do_munmap(addr);
}

 

do_munmap() 구현

 

memory unmapping을 실행한다. 즉, 페이지에 연결되어 있는 물리 프레임과의 연결을 끊어준다. 유저 가상 메모리의 시작 주소 addr부터 연속으로 나열된 페이지 모두를 매핑 해제한다.

 

이때 페이지의 Dirty bit이 1인 페이지는 매핑 해제 전에 변경 사항을 디스크 파일에 업데이트해줘야 한다. 이를 위해 페이지의 container 구조체에서 연결된 파일에 대한 정보를 가져온다.

 

file_backed_swap_out()과 동일한 방식.

 

void do_munmap (void *addr) {

/* addr부터 연속된 모든 페이지 변경 사항을 업데이트하고 매핑 정보를 지운다.
	가상 페이지가 free되는 것이 아님. present bit을 0으로 만드는 것! */
    
    while (true) {
    struct page* page = spt_find_page(&thread_current()->spt, addr);
    
    if (page == NULL) {
    	return NULL;
    }
    
    struct container *container = (struct container *)page->uninit.aux;
    
    /* 수정된 페이지(dirty bit == 1)는 파일에 업데이트해놓는다. 이후에 dirty bit을 0으로 만든다. */
    if (pml4_is_dirty(thread_current()->pml4, page->va)){
    	file_wtire_at(container->file, addr, container->page_read_bytes, container->offset);
        pml4_set_dirty(thread_current()->pml4, page->va, 0);
    }
    
    /* present bit을 0으로 만든다. */
    pml4_clear_page(thread_current()->pml4, page->va);
    addr += PGSIZE;
    }
}

 

do_munmap()과 관련된 함수를 살펴보자.

 

pml4_is_dirty()

더보기

페이지의 dirty bit이 1이면 true를, 0이면 false를 리턴한다.

bool pml4_is_dirty (uint64_t *pml4, const void *vpage) {
	uint64_t *pte = pml4e_walk (pml4, (uint64_t) vpage, false);
	return pte != NULL && (*pte & PTE_D) != 0;
}

file_write_at()

더보기

물리 프레임에 변경된 데이터를 다시 디스크 파일에 업데이트해주는 함수. buffer에 있는 데이터를 size만큼, file의 file_ofs부터 써준다.

off_t file_write_at (struct file *file, const void *buffer, off_t size,
		off_t file_ofs) {
	return inode_write_at (file->inode, buffer, size, file_ofs);
}

pml4_set_dirty()

더보기

인자로 받은 dirty의 값이 1이면 page의 dirty bit을 1로, 0이면 0으로 변경해준다.

void
pml4_set_dirty (uint64_t *pml4, const void *vpage, bool dirty) {
	uint64_t *pte = pml4e_walk (pml4, (uint64_t) vpage, false);
	if (pte) {
		if (dirty)
			*pte |= PTE_D;
		else
			*pte &= ~(uint32_t) PTE_D;

		// 뭐하는 놈일까
		if (rcr3 () == vtop (pml4))   // CR3 : Page Directory Base Register
			invlpg ((uint64_t) vpage);  // INVLPG : Invalidate TLB Entries
	}
}

pml4_clear_page()

더보기

페이지의 present bit 값을 0으로 만들어주는 함수

void
pml4_clear_page (uint64_t *pml4, void *upage) {
	uint64_t *pte;
	ASSERT (pg_ofs (upage) == 0);
	ASSERT (is_user_vaddr (upage));

	pte = pml4e_walk (pml4, (uint64_t) upage, false);

	if (pte != NULL && (*pte & PTE_P) != 0) {
		*pte &= ~PTE_P;   // Present bit(0x1) mask off
		if (rcr3 () == vtop (pml4))   // CR3 : Page Directory Base Register
			invlpg ((uint64_t) upage);  // INVLPG : Invalidate TLB Entries
	}
}

 

 

supplemental_page_table_kill() 수정

 

현재 spt_kill()함수는 spt 테이블 내 hash table을 돌면서 싹다 제거하는 방식이다. 이제 file_backed page일 경우 munmap을 실행해주는 코드를 추가한다.

 

/* Free the resource hold by the supplemental page table */
void
supplemental_page_table_kill (struct supplemental_page_table *spt UNUSED) {
	/* TODO: Destroy all the supplemental_page_table hold by thread and
	 * TODO: writeback all the modified contents to the storage. */

	struct hash_iterator i;
	
	hash_first (&i, &spt->spt_hash);
	while (hash_next (&i)) {
		struct page *page = hash_entry (hash_cur (&i), struct page, hash_elem);

		if (page->operations->type == VM_FILE) {
			//do_munmap(page->va); pjt 4
		}
		destroy(page);
	}
	hash_destroy(&spt->spt_hash, spt_destructor);
}

 

수정 전 spt_kill()

더보기

 

void
supplemental_page_table_kill (struct supplemental_page_table *spt UNUSED) {
	/* TODO: Destroy all the supplemental_page_table hold by thread and
	 * TODO: writeback all the modified contents to the storage. */

	struct hash_iterator i;
	
	hash_first (&i, &spt->spt_hash);
	while (hash_next (&i)) {
		struct page *page = hash_entry (hash_cur (&i), struct page, hash_elem);
		destroy(page);
	}
	hash_destroy(&spt->spt_hash, spt_destructor);
}

 

이외에 메모리 매핑 내용은 공부를 좀 더 해서 살을 붙여야 할 듯 하다.

반응형