memory mapping?
파일을 프로세스의 메모리에 매핑하는 것.
프로세스에 전달할 데이터를 직접 프로세스의 가상 주소 공간으로 매핑하는 것
->read
나write
함수를 사용하지 않고도 프로그램 내부에서 정의한 변수를 사용해서 파일에서 데이터를 읽고 쓸 수 있게 된다.
일반적으로mmap
시스템 콜을 사용한다.장점?
read, write 시스템 콜 호출 오버헤드를 줄일 수 있다.
여러 프로세스가 동일한 파일을 매핑하게 되면 물리 메모리를 공유해서 메모리 사용량을 줄일 수 있다.
파일의 내용을 메모리처럼 직접 접근할 수 있다.
그렇다면 단점은?
- page fault overhead: 처음 접근할 때 페이지 폴트가 발생해서 디스크에서 메모리로 데이터를 로드하는 오버헤드가 발생할 수 있다.
- 매핑된 파일의 크기가 크면 프로세스의 VM 공간을 많이 차지할 것
현재 핀토스는 mmap
, munmap
함수가 구현되어 있지 않다!
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
orvm_alloc_page
함수를 사용해서 페이지 객체를 만들어야 한다.- 파일의 길이가 PGSIZE의 배수가 아닌 경우 → 마지막 페이지에 남는 공간은 0으로 초기화되어야 함 - 페이지가 디스크로 다시 쓰여질 때 이 공간은 버려져야 한다.
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
함수를 호출해서 실제 매핑 작업을 수행한다. -> 파일의 특정 부분을 메모리 주소에 매핑
메모리 매핑 -> 파일의 내용을 특정 메모리 주소에 매핑
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
: 한 페이지의 크기 (바이트의 수)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
페이지 할당
void munmap (void *addr){
do_munmap(addr);
}
프로세스의 가상 페이지 목록에서 페이지를 제거해서 매핑 해제
/* 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 하나가 터지는 상황이라 완벽한 정답 코드는 아님에 주의하시길