[Jungle] Week10. Pintos Project 3. Memory Mapped Files

somi·2024년 6월 13일
0

[Krafton Jungle]

목록 보기
67/68

memory mapping?

파일을 프로세스의 메모리에 매핑하는 것.
프로세스에 전달할 데이터를 직접 프로세스의 가상 주소 공간으로 매핑하는 것
-> readwrite함수를 사용하지 않고도 프로그램 내부에서 정의한 변수를 사용해서 파일에서 데이터를 읽고 쓸 수 있게 된다.
일반적으로 mmap 시스템 콜을 사용한다.

장점?

  • read, write 시스템 콜 호출 오버헤드를 줄일 수 있다.

  • 여러 프로세스가 동일한 파일을 매핑하게 되면 물리 메모리를 공유해서 메모리 사용량을 줄일 수 있다.

  • 파일의 내용을 메모리처럼 직접 접근할 수 있다.

그렇다면 단점은?

  • page fault overhead: 처음 접근할 때 페이지 폴트가 발생해서 디스크에서 메모리로 데이터를 로드하는 오버헤드가 발생할 수 있다.
  • 매핑된 파일의 크기가 크면 프로세스의 VM 공간을 많이 차지할 것

현재 핀토스는 mmap, munmap 함수가 구현되어 있지 않다!

  • lazy load에 의해 파일 데이터를 메모리로 로드하는 mmap함수 구현
  • 그리고 파일 매핑을 제거하는 munmap함수 구현해보자

memory mapped files?

  • anonymous page와 달리
    프로세스의 주소 공간에 파일을 매핑하여 파일의 내용을 메모리처럼 접근할 수 있게끔
    -> 파일 입출력 시의 디스크 I/O를 줄이고 성능을 향상시킬 수 있음!


mmap 함수 구현 시 주의해야 하는 사항들

  • addr이 page-aligned 되었는지
    : 메모리 매핑은 페이지 단위로 이루어지기 때문에 매핑하려는 가상 주소가 페이지 경계에 맞춰져 있어야 함!
  • 매핑하려는 페이지가 이미 존재하는 페이지와 겹칠 때(SPT에 이미 존재하는 페이지일 때)
  • fd로 열린 파일의 길이가 0바이트면 mmap 호출은 실패해야 함
    : 매핑할 데이터가 없으니 불필요
  • 주소 addr이 0인 경우 - NULL 포인터는 유효한 메모리 주소가 아님
  • length = 0 인 경우 - mmap 실패해야 함
  • 콘솔 입력, 출력을 나타내는 파일 디스크립터 (0: STDIN, 1:STDOUT)는 매핑을 할 수 없다.
    : 파일 시스템과 직접 관련된 데이터가 아님!
  • lazy allocation - 메모리 매핑된 페이지는 anonymous 페이지처럼 Lazy-loading 방식으로 할당되어야 한다.⇒ vm_alloc_page_with_initializer or vm_alloc_page 함수를 사용해서 페이지 객체를 만들어야 한다.
  • 파일의 길이가 PGSIZE의 배수가 아닌 경우 → 마지막 페이지에 남는 공간은 0으로 초기화되어야 함 - 페이지가 디스크로 다시 쓰여질 때 이 공간은 버려져야 한다.

mmap


void *mmap (void *addr, size_t length, int writable, int fd, off_t offset){

	//offset != pg_round_down(offset) 
	//offset이 페이지의 경계에 정확하게 맞춰지지 않았을 경우
	if (offset % PGSIZE != 0) {
		return NULL;
	}

	//매핑하려는 페이지가 이미 spt에 존재하는 경우
	if (spt_find_page(&thread_current()->spt, addr)){
		return NULL;
	}

	if (!addr || is_kernel_vaddr(addr) || !is_user_vaddr(addr + length) || addr != pg_round_down(addr)){
		return NULL;
	}

	struct file *f = process_get_file(fd);

	if (f == NULL){
		return NULL;
	}

	//fd로 열린 파일의 길이가 0바이트인 경우
	if (file_length(f) == 0 || (int)length <= 0) {
		return NULL;
	}

	if (fd == 0 || fd == 1){
		return NULL;
	}

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

트러블 슈팅

아무 생각없이 if (file_length(f) == 0 || (int)length <= 0 || f == NULL)이렇게 하나의 조건문으로 file이 NULL인지와 file의 길이를 비교하게 예외를 처리하려고 했었다.
하지만 file이 NULL일 때 file_length를 체크하게 되면
file이 NULL인지 확인하는 ASSERT 구문이 포함되어 있어
NULL 포인터 참조로 오류가 발생할 수 있다.
그러니 file이 NULL인지를 먼저 확인하게끔 수정해주니 mmap-bad-fd 관련 테스트를 통과할 수 있었다.

모든 조건을 만족하면 do_mmap 함수를 호출해서 실제 매핑 작업을 수행한다. -> 파일의 특정 부분을 메모리 주소에 매핑


do_mmap

메모리 매핑 -> 파일의 내용을 특정 메모리 주소에 매핑

void *
do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset) {
	
	lock_acquire(&filesys_lock);
	/*file_reopen을 통해 동일한 파일에 대해 다른 주소를 가지는 파일 구조체 생성
	 -> mmap하는 동안 외부에서 해당 파일을 close하는 불상사를 막기 위해서*/
	struct file *f = file_reopen(file); 

	void *start_addr = addr; //매핑을 시작할 주소
	
	/*총 페이지의 수 
	- length : 총 데이터의 길이 (바이트 수)
	- PGSIZE : 한 페이지의 크기 (바이트의 수)
	- 데이터의 길이가 한 페이지의 크기보다 작으면 총 페이지 수는 1 
								  크면 데이터를 페이지 크기로 나눴을 때 정확히 나눠 떨어지는지에 따라 다르게 계산 
	*/
	int total_page_count = length <= PGSIZE ? 1 : length % PGSIZE ? length / PGSIZE + 1
                                                                  : length / PGSIZE; 

	/* read_bytes: 파일에서 읽어와야 할 실제 데이터의 크기 
	- file_length(f) : 파일 f의 총 길이(바이트 수)
	- length: 읽어와야 하는 데이터의 목표 길이
	- file_length(f) < length : 파일의 길이만큼만 읽을 수 있다.
	- file_length(f) > length : 목표한 길이만큼 데이터를 읽어올 수 있다. 
	*/
	size_t read_bytes  = file_length(f) < length ? file_length(f) : length;
	
	//read_bytes가 PGSIZE 크기의 배수가 아닐 경우
	//마지막 페이지는 일부만 데이터로 채워지고 나머지는 0으로 채워져야 함 
	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 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;

		//addr에 file-backed 페이지 할당
		//lazy_load_segment 함수의 인자로는 lazy_load_arg를
		if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, lazy_load_arg))
			return NULL; //할당 실패시 return NULL
		
		struct page *p = spt_find_page(&thread_current()->spt, start_addr);
		p->mapped_page_count = total_page_count;


		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		addr += PGSIZE;
		offset += page_read_bytes;
	}
	lock_release(&filesys_lock);
	return start_addr;
}

총 페이지의 수

  • length : 총 데이터의 길이 (바이트 수)
  • PGSIZE : 한 페이지의 크기 (바이트의 수)
  • 데이터의 길이가 한 페이지의 크기보다 작으면 총 페이지 수는 1
    - 보다 크면 데이터를 페이지 크기로 나눴을 때 정확히 나눠 떨어지는지에 따라 다르게 계산

int total_page_count = length <= PGSIZE ? 1 : length % PGSIZE ? length / PGSIZE + 1 : length / PGSIZE;

  • file_reopen을 통해 새로운 fd 생성한다. -> 매핑 중에 다른 프로세스에서 파일을 닫는 것 방지하기 위해
    => 동일한 파일을 여러 프로세스가 매핑할 수도 있기 때문에 독립적인 매핑을 가지기 위해
  • 매핑할 총 페이지의 수를 계산 - length와 PGSIZE를 비교하여 계산하고
  • 파일에서 읽어야 하는 read_bytes, zero_bytes 계산한다.
  • lazy_load_arg 구조체에 lazy_load에 필요한 정보를 전달해주고
    vm_alloc_page_with_initializer 함수를 사용해서 lazy loading을 위한 인자 전달 및 VM_FILE 페이지 할당

unmap


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

프로세스의 가상 페이지 목록에서 페이지를 제거해서 매핑 해제

do_munmap

/* Do the munmap */
//매핑된 메모리 해제
void
do_munmap (void *addr) {
	struct supplemental_page_table *spt = &thread_current()->spt;
	struct page *p = spt_find_page(spt, addr);
	int cnt = p->mapped_page_count;
	// lock_acquire(&filesys_lock);

	//매핑된 페이지의 수만큼 반복
	for (int i = 0; i <cnt; i++){	
		if (p) {
			destroy(p);
		}
		addr += PGSIZE;
		p = spt_find_page(spt, addr);
	}
	// lock_release(&filesys_lock);
}

addr에서 시작하는 매핑된 메모리 블록 해제
-> 각 페이지를 찾아서 destroy함수를 통해서 페이지를 없앤다.


근데 지금 page-merge 테스트 케이스를 통과하고 싶어서 lock을 엄청나게 걸다가
swap-fork, swap-iter, page-parallel, merge 하나가 터지는 상황이라 완벽한 정답 코드는 아님에 주의하시길

profile
📝 It's been waiting for you

0개의 댓글