mmap은 메모리에 파일의 내용을 그대로 옮겨서 매핑한다. 따라서 file_write를 통해 disk I/O가 지속적으로 발생해서 속도가 느려질 걱정이 없다. 우리는 메모리에서 빠르게 내용을 변경하고 mmap 파일이 닫힐 때 그 내용을 파일에 write_back 만 해주면 되는 것이다.
우리는 시스템 콜 mmap과 munmap을 구현해야 한다.
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
void munmap(void *addr);
그럼 당연히 이들을 syscall_handler에 등록해줘야겠지?
void syscall_handler(struct intr_frame *f UNUSED){
...
case SYS_MMAP:
f->R.rax = sys_mmap(arg1, arg2, arg3, arg4, arg5);
break;
case SYS_MUNMAP:
sys_munmap(arg1);
break;
...
}
mmap 파일이 닫힐 때 write back을 해줘야 한다고 한다. write back이 뭘까? 변경사항을 메모리에만 저장해두고 파일이 닫힐 때만 파일에 변경사항을 쓰는 것을 write back이라고 한다. 그럼 우리가 한 페이지의 write back을 위해 저장해야 할 메타데이터는 어떤 것들이 있을까?
struct file_page
struct file_page
{
int mapping_count;
struct file *file;
off_t offset;
size_t read_byte;
size_t zero_byte;
};
아마 이런 것들이 필요하지 않을까? 이 페이지가 어떤 파일에 매핑되었는지, 어느 위치부터 매핑되었는지, 어디까지 읽었고 패딩 크기는 얼마나 되는지 말이다.
그러면 mapping_count는 대체 뭘하는 필드지? mmap을 할 때 파일의 크기가 한 페이지 안에서만 논다는 보장은 전혀 없다. 따라서 여러개의 페이지를 하나의 파일을 위해 매핑해야 할 수도 있다. 이 때 페이지의 순서를 나타내기 위한 count이다. 순서가 왜 필요하냐고? 그건 munmap 에서 자세히 다루겠다.

그럼 여러개의 페이지를 매핑하려면 위의 그림과 같이 매핑하면 되겠지?
아 참고로 보통 낮은 주소에서 높은 주소로 매핑된다고 한다.
mmap
sys_mmap
void *sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset)
이제 본격적으로 mmap을 구현해보자 !! 그 전에 먼저 과제 설명서에 적힌 실패 조건을 잘 살펴보자.

참 많기도 하다. 그리고 과제 설명서에 나와있지 않은, 테스트 케이스에서만 검사하는 실패 조건도 있다 ㅡㅡ
이건 진짜 디버깅 하나하나 해가면서 찾아냈다.
그러면 실패 조건을 먼저 어떻게 검사했는지 알아보자.
/* 파일 입출력에는 mmap이 불가능합니다 */
if (fd < 2)
return MAP_FAILED;
먼저 콘솔 입출력 !! 이건 뭐 당연히 파일이 아니니까 불가능하다
/* 파일의 사이즈가 0이면 mmap이 불가능합니다 */
int filesize = sys_filesize(fd);
if (filesize == 0 || length == 0)
return MAP_FAILED;
그리고 파일의 사이즈가 0이면 이것도 실패다. 사이즈가 0인 파일을 open할 수는 있지만 mmap은 안된다. 왜일까?
아마 내 추측으로는 mmap 자체가 disk I/O 없이 빠르게 내용을 수정한다음 한꺼번에 write back을 하려고 하는 것인데, 0인 파일을 mmap 해봤자 수정할 내용이 없어서 그런거 아닐까 싶다.
아니면 매핑할 데이터 자체가 없어서 그런가?
/* 길이가 비정상적일떄 mmap이 불가능합니다 */
if (length > (uintptr_t)addr)
return MAP_FAILED;
그리고 테스트 중 하나... 무슨 파일 사이즈 1억짜리를 매핑하려 들길래 이를 막아줘야 했다. 비정상적인 파일 길이를 어떻게 체크할까... 했는데, 그냥 주소보다 큰 길이를 매핑하려 들 때 실패하도록 만들었다. 이게 정확한 판단 기준이 될지는 모르겠지만, 테스트는 통과하니 장떙 아닐까?
/* addr이나 offset이 페이지 정렬되어있지 않으면 mmap이 불가능합니다 */
if ((uint64_t)addr == 0 || (uint64_t)addr % PGSIZE != 0 || offset % PGSIZE != 0 || !is_user_vaddr(addr))
return MAP_FAILED;
여기서는 4가지를 검사한다.
- 할당할 주소가 0인가?
- 할당할 주소가 페이지 정렬되어있는가?
- 오프셋이 페이지 정렬되어있는가?
- 할당할 주소가 유저 영역인가?
struct file *target_file = thread_current()->fd_table[fd];
if (target_file == NULL)
return MAP_FAILED;
당연히 인자로 받은 fd에 파일이 없으면 당연히 실패다.
void *start_page = addr;
void *end_page = pg_round_up(addr + length);
/* 해당 페이지 영역에 이미 할당된 페이지가 있으면 mmap이 불가능 합니다 */
for (; end_page > start_page; start_page += PGSIZE)
{
if (spt_find_page(&thread_current()->spt, start_page) != NULL)
return MAP_FAILED;
}
그리고 내가 할당할 영역에 이미 할당되어있는 페이지가 있어도 실패!! 여러 영역에 걸쳐서 매핑해야할 수도 있으니 루프를 돌면서 검사하자.
do_mmap(addr, length, writable, target_file, offset);
return addr;
여기까지 모두 검사하고 나면 드디어 실제 매핑 로직을 호출한다 !! 진짜 검사가 너무 빡세다...
코드 전문은 아래 더보기!!
코드 전문
void *sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset)
{
/* 파일 입출력에는 mmap이 불가능합니다 */
if (fd < 2)
return MAP_FAILED;
/* 파일의 사이즈가 0이면 mmap이 불가능합니다 */
int filesize = sys_filesize(fd);
if (filesize == 0 || length == 0)
return MAP_FAILED;
/* 길이가 비정상적일떄 mmap이 불가능합니다 */
if (length > (uintptr_t)addr)
return MAP_FAILED;
/* addr이나 offset이 페이지 정렬되어있지 않으면 mmap이 불가능합니다 */
if ((uint64_t)addr == 0 || (uint64_t)addr % PGSIZE != 0 || offset % PGSIZE != 0 || !is_user_vaddr(addr))
return MAP_FAILED;
/* 해당 fd에 파일이 없으면 mmap이 불가능합니다 */
struct file *target_file = thread_current()->fd_table[fd];
if (target_file == NULL)
return MAP_FAILED;
void *start_page = addr;
void *end_page = pg_round_up(addr + length);
/* 해당 페이지 영역에 이미 할당된 페이지가 있으면 mmap이 불가능 합니다 */
for (; end_page > start_page; start_page += PGSIZE)
{
if (spt_find_page(&thread_current()->spt, start_page) != NULL)
return MAP_FAILED;
}
do_mmap(addr, length, writable, target_file, offset);
return addr;
}
그러면 이제 실제로 페이지를 매핑해줄 do_mmap을 구현해보자.
do_mmap
void *do_mmap(void *addr, size_t length, int writable,
struct file *file, off_t offset)
여기서 로직을 직접 작성하자 !!
/* 지연 로딩과 스왑 시의 백업 정보 저장 */
off_t file_size = file_length(file);
off_t read_size = file_size - offset;
if (read_size < 0)
read_size = 0;
size_t remain_length = (size_t)read_size;
void *cur_addr = addr;
off_t cur_offset = offset;
struct file *reopen_file = file_reopen(file);
int mapping_count = 0; /* mmap은 여러 페이지에 걸쳐 매핑될 수 있습니다.
munmap 시에 어디까지 해제해줄 것인지 판단할 기준이 됩니다 */
먼저 파일 사이즈를 가져온다. 하지만 이 파일 사이즈가 곧 우리가 읽어야할 길어가 되지는 않는다. 파일의 중간부터 매핑될 수도 있기 때문이다.
여기서 주의할 점은 file_length로 가져온 off_t는 signed int 형이다. 하지만 여기서 루프를 돌면서 남은 길이를 확인할 remain_length는 unsinged 형인 size_t이다. 따라서 타입 캐스팅을 할 때 음수를 unsinged 형으로 캐스팅해버리면 이상하게 큰 수가 나타나버린다.
근데 왜 remain_length를 off_t나 int형이 아닌 size_t로 했냐고?? 글쎄... 아마 이 메타데이터를 넣어줄 구조체의 자료형이 size_t여서 그랬었는데... 지금 보니까 굳이 size_t로 할 필요가 없었던 것 같다. 그냥 int로 만들어주는게 훨 나아보이지만... 이미 작동하는 코드는 건드리지 말라는 성현의 말씀이 있었다.
여기서 나온 변수들 몇몇개를 대강 설명하자면,
- remain_length : 루프를 위해 사용되는 매핑되고 남은 길이
- cur_addr : 매핑할 현재 주소
- cur_offset : 매핑할 파일의 오프셋
- reopen_file : 매핑할 파일. 무조건 reopen으로 다시 열어줘야 한다 !!
while (remain_length > 0)
{
/* 남은 mmap 매핑 길이가 4KB보다 크면 4KB로 맞춥니다 */
size_t allocate_length = remain_length > PGSIZE ? PGSIZE : remain_length;
/* 지연 로딩 시 필요한 정보를 생성합니다 */
struct lazy_load_info *info = make_info(reopen_file, cur_offset, allocate_length);
struct mmap_info *mmap_info = make_mmap_info(info, mapping_count);
void *aux = mmap_info;
/* mmap 또한 지연 로딩이 필요합니다 */
vm_alloc_page_with_initializer(VM_MMAP, cur_addr, writable, lazy_load_segment, aux);
/* remain_length는 unsigned 형입니다. 따라서 음수가 없기에 미리 break를 해줘야 합니다 */
if (remain_length < PGSIZE)
break;
remain_length -= PGSIZE;
cur_addr += PGSIZE;
cur_offset += PGSIZE;
mapping_count++;
}
이제 실제로 mmap 을 미초기화 페이지로 매핑해주자. 한 페이지는 4KB니까 남은 길이가 4KB보다 크면 다운시켜서 4KB로 맞춰주자. 그 다음 메타데이터들을 aux에 저장하기 위해 구조체를 만들어주고, vm_alloc_with_initializer를 통해 미초기화 페이지를 만들어주자. 여기서 lazy_load_segment가 들어가는데, 사실 pintOS 코드의 의도는 메모리 매핑 파일을 위한 함수를 하나 만들어서 거기서 aux에 있는 메타데이터를 통해 초기화를 진행하라는 것 같았지만, 같은 동작을 가진 함수를 한개 더 만들기 싫어서 lazy_load_segment 를 static에서 전역으로 만들고 process.h에 추가해주었다.
마지막으로 다음 루프를 위해 cur_addr과 cur_offset에는 PGSIZE를 더해주고, remain_length에는 PGSIZE만큼 빼주자. 그리고 mapping_count를 증감시켜서 같은 파일 매핑임을 알려주자.
전체 코드와 위에서 사용된 구조체, 다른 함수들은 더보기에 !!
do_mmap 전문
void *do_mmap(void *addr, size_t length, int writable,
struct file *file, off_t offset)
{
/* 지연 로딩과 스왑 시의 백업 정보 저장 */
off_t file_size = file_length(file);
off_t read_size = file_size - offset;
if (read_size < 0)
read_size = 0;
size_t remain_length = (size_t)read_size;
void *cur_addr = addr;
off_t cur_offset = offset;
struct file *reopen_file = file_reopen(file);
int mapping_count = 0; /* mmap은 여러 페이지에 걸쳐 매핑될 수 있습니다.
munmap 시에 어디까지 해제해줄 것인지 판단할 기준이 됩니다 */
while (remain_length > 0)
{
/* 남은 mmap 매핑 길이가 4KB보다 크면 4KB로 맞춥니다 */
size_t allocate_length = remain_length > PGSIZE ? PGSIZE : remain_length;
/* 지연 로딩 시 필요한 정보를 생성합니다 */
struct lazy_load_info *info = make_info(reopen_file, cur_offset, allocate_length);
struct mmap_info *mmap_info = make_mmap_info(info, mapping_count);
void *aux = mmap_info;
/* mmap 또한 지연 로딩이 필요합니다 */
vm_alloc_page_with_initializer(VM_MMAP, cur_addr, writable, lazy_load_segment, aux);
/* remain_length는 unsigned 형입니다. 따라서 음수가 없기에 미리 break를 해줘야 합니다 */
if (remain_length < PGSIZE)
break;
remain_length -= PGSIZE;
cur_addr += PGSIZE;
cur_offset += PGSIZE;
mapping_count++;
}
}
struct mmap_info / struct lazy_load_info
struct lazy_load_info
{
struct file *file;
off_t offset;
size_t readbyte;
size_t zerobyte;
};
struct mmap_info
{
struct lazy_load_info *info;
int mapping_count;
};
make_info / make_mmap_info
struct lazy_load_info *make_info(
struct file *file, off_t offset, size_t read_byte)
{
struct lazy_load_info *info = malloc(sizeof(struct lazy_load_info));
info->file = file;
info->offset = offset;
info->readbyte = read_byte;
info->zerobyte = PGSIZE - read_byte;
return info;
}
struct mmap_info *make_mmap_info(struct lazy_load_info *info, int mapping_count)
{
struct mmap_info *mmap_info = malloc(sizeof(struct mmap_info));
mmap_info->info = info;
mmap_info->mapping_count = mapping_count;
return mmap_info;
}
munmap
이제 매핑된 mmap 페이지를 해제해주는 로직도 필요하다. 처음에 든 의문은 "munmap에서 만약 매핑된 영역의 중간부터 해제하라고 요청이 들어오면 어떻게 해야되는가?" 였다. 중간부터 해제해줘야하나? 아니면 영역의 내부이긴 하니까 영역의 처음부터 다 해제해줘야 하나? 답은 코치님과 테스트 케이스를 통해 알았는데, 무조건 매핑 영역의 처음 주소를 준다고 한다. 그래서 이런 복잡한 로직까지는 신경쓰지 않고 구현하면 된다.
do_mnumap

구현하기 전에 알아둬야 할 것이 있다. 위 그림처럼 매핑이 되어있다고 할 때 0x10000 ~ 0x13000 영역과 0x13000 ~ 0x15000 까지는 서로 다른 영역이다. 하지만 서로 같은 파일을 매핑하고 있고, offset까지도 이쁘게 정렬되어있다 !! 그러면 이 둘이 다른 영역이라는 것을 어떻게 확인할 것인가? 여기서 사용되는게 바로 mapping_count이다. 0x13000 경계에서 mapping_count에서 2에 3이 되는게 아니라 0이 되어버렸다. 이로 인해 이 경계는 서로 다른 mmap 영역이라는 것을 알 수 있다 !! 이거 완전 씽크빅인데?
struct page *target_page = spt_find_page(spt, addr);
if (target_page == NULL)
return;
먼저 해당 영역에 해제할만한 페이지가 있는지부터 확인하자 !!
struct file_page *file_page = &target_page->file;
struct file *target_file = file_page->file;
int target_mmap_count = file_page->mapping_count;
그 다음에 해당 page에 저장된 메타데이터를 통해 첫번째 페이지의 정보를 가져오자. 우리는 이 정보를 통해 다음 주소의 페이지들이 이 첫번째 페이지와 같은 영역인지 확인할 것이다.
addr += PGSIZE; // 다음 mmap_file 찾기
/* while 루프를 통해 연속된 페이지가 같은 mmap 영역인지 확인합니다 */
while (is_my_mmap(addr, target_file, ++target_mmap_count))
{
struct page *remove_page = spt_find_page(spt, addr);
spt_remove_page(spt, remove_page);
addr += PGSIZE;
}
/* 첫번째 주소를 해제합니다 */
spt_remove_page(spt, target_page);
그리고 이제 루프문을 돌면서 조건 함수 is_my_mmap을 통해 같은 매핑 영역인지 확인하고, 같다면 spt_remove_page를 통해 제거해주자.
그리고 루프문이 끝나고 나서야 첫번째 페이지를 없애주자. 루프 이전에 미리 없애면 첫번째 페이지에서 가져온 비교 file이 없어진다.
is_my_mmap
static bool is_my_mmap(void *addr, struct file *mmap_file, int mmap_count)
{
struct supplemental_page_table *spt = &thread_current()->spt;
struct page *find_page = spt_find_page(spt, addr);
if (find_page == NULL)
return false;
struct file *find_file = find_page->file.file;
/* 파일이 NULL이거나 인자로 받은 파일과 다른가? */
/* NULL이거나 다르면 다른 페이지임 !! */
if (find_file == NULL || find_file != mmap_file)
return false;
/**같은 파일이어도 매핑 카운트가 맞지 않으면
* 서로 다른 페이지임 !!
*/
if (find_page->file.mapping_count != mmap_count)
return false;
return true;
}
이건 그냥 검사하는 함수. 루프문에서 전치 증감 연산자를 통해 미리 증가시켜서 mmap_count를 보냈기에 다음 페이지와 비교가 가능하다.
코드 전문은 더보기 !!
void do_munmap(void *addr)
{
struct supplemental_page_table *spt = &thread_current()->spt;
/* SPT에 없으면 해제할 수 없습니다 !! */
/* 항상 mmap 영역의 첫번째 주소를 준다고 합니다 */
struct page *target_page = spt_find_page(spt, addr);
if (target_page == NULL)
return;
struct file_page *file_page = &target_page->file;
struct file *target_file = file_page->file;
int target_mmap_count = file_page->mapping_count;
addr += PGSIZE; // 다음 mmap_file 찾기
/* while 루프를 통해 연속된 페이지가 같은 mmap 영역인지 확인합니다 */
while (is_my_mmap(addr, target_file, ++target_mmap_count))
{
struct page *remove_page = spt_find_page(spt, addr);
spt_remove_page(spt, remove_page);
addr += PGSIZE;
}
/* 첫번째 주소를 해제합니다 */
spt_remove_page(spt, target_page);
}
하지만 여기서 끝이 아니다 !!
우리는 이 페이지가 destroy될 때 write_back을 해주어야만 한다. 하지만 이는 swap-in/out 에서 다룰 것이기 때문에 지금은 코드만 올려두고 설명은 다음 포스팅에서 하겠다.
file_backed_destroy 코드는 더보기에
/* 파일 기반 페이지를 소멸시킵니다. PAGE는 호출자가 해제합니다. */
static void
file_backed_destroy(struct page *page)
{
// file_page는 file-backed 페이지에 대한 메타데이터를 담고 있는 구조체
struct file_page *file_page UNUSED = &page->file;
/** TODO: dirty_bit 확인 후 write_back
* pml4_is_dirty를 사용해서 dirty bit 확인
* write back을 할 때는 aux에 저장된 파일 정보를 사용
* file_write를 사용하면 될 것 같아요
*/
struct thread *curr = thread_current();
// 파일을 스기 가능하게 설정 → read_only로 열렸을 수도 있으므로
file_allow_write(file_page->file);
// 페이지가 dirty 상태 → 메모리 상에서 파일 내용이 수정됨
if (pml4_is_dirty(thread_current()->pml4, page->va))
{
lock_acquire(&filesys_lock);
off_t written = file_write_at(file_page->file, // mmap으로 매핑된 파일 객체
page->frame->kva, // 물리 메모리 상 해당 페이지의 커널 주소
file_page->read_byte, // 실제로 파일에 쓸 바이트 수
file_page->offset); // mmap할 때 저장된 파일 내부의 오프셋 위치
lock_release(&filesys_lock);
ASSERT(written == file_page->read_byte);
// dirty bit를 false로 초기화(더 이상 수정 X)
pml4_set_dirty(curr->pml4, page->va, false);
}
// 해당 페이지가 물리 프레임에 매핑되어 있으면
if (page->frame != NULL && page->frame->ref_cnt < 1)
{
// 물리 페이지를 해제하고, frame 구조체도 동적 메모리 해제
palloc_free_page(page->frame->kva);
free(page->frame);
page->frame = NULL;
}
// 최종적으로 사용자 가상 주소 공간에서 해당 페이지 매핑을 제거
pml4_clear_page(curr->pml4, page->va);
}
'크래프톤 정글' 카테고리의 다른 글
| [pintOS] Copy-on-write 구현기 (4) | 2025.06.07 |
|---|---|
| [pintOS] swap-in/out 구현기 (5) | 2025.06.05 |
| [pintOS] vm 트러블슈팅 (1) | 2025.06.04 |
| [pintOS] stack_growth 구현기 (0) | 2025.06.03 |
| [pintOS] uninit 페이지 구현기 (0) | 2025.06.03 |