운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간

관리자 (토론 | 기여)님의 2026년 10월 31일 (토) 08:18 판 (mmap의 파일 연산과 공통 페이지 캐시 설명 연결)

파일 메모리 매핑 목차 · 다음: 첫 접근과 페이지 폴트

기준은 Linux v6.12, x86-64, 일반 ext4 파일, 읽기 전용 MAP_PRIVATE다. 파일 오프셋은 0이며 페이지 캐시를 우회하는 DAX(Direct Access) 매핑이나 페이지를 미리 채우는 MAP_POPULATE 옵션, 잠긴 매핑은 사용하지 않는다.

주소를 받는 순간과 내용을 읽는 순간

다음 프로그램은 미리 존재하는 비어 있지 않은 일반 파일 sample.bin의 첫 바이트를 숫자로 출력한다. 실행 중 다른 프로세스가 파일을 줄이거나 내용을 바꾸지 않는다고 가정한다. 첫 바이트만 필요하므로 길이는 1이다. Linux는 매핑 범위를 페이지 단위로 관리하지만 프로그램은 그중 첫 바이트만 읽는다.

#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void) {
    int fd = open("sample.bin", O_RDONLY);
    if (fd == -1) return 1;
    void *p = mmap(NULL, 1, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (p == MAP_FAILED) return 1;
    printf("%u\n", (unsigned)((const unsigned char *)p)[0]);
    return munmap(p, 1) == -1;
}

mmap()의 성공은 파일 내용을 읽는 데 성공했다는 뜻이 아니다. 커널이 해당 프로세스의 가상 주소 범위를 파일에 연결하는 설정을 받아들였다는 뜻이다. 반환값은 파일 내용 자체나 물리 주소가 아니라 프로세스가 나중에 접근할 가상 주소다. 실제 바이트 읽기는 printf()에 넘길 값을 계산하는 ((const unsigned char *)p)[0]에서 발생한다. 두 동작 사이에는 close(fd)까지 들어갈 수 있다.

MAP_PRIVATE라는 이름도 이 시점에 파일 전체의 사본을 만든다는 뜻은 아니다. 이 예제는 PROT_READ만 요청하므로 이 주소를 통한 쓰기를 허용하지 않는다. 파일 수정과 쓰기 시 복사에 관한 이야기는 이 읽기 경로에서 제외한다.

시스템 호출은 파일과 주소 공간을 연결한다

사용자 라이브러리를 거쳐 x86-64의 mmap 시스템 호출에 들어가면, 커널은 먼저 바이트 단위 파일 오프셋이 페이지 경계에 맞는지 확인한다. 이후 off >> PAGE_SHIFT로 페이지 단위 오프셋을 만들어 ksys_mmap_pgoff()를 부른다. 예제에서는 둘 다 0이다.[1]

ksys_mmap_pgoff()는 파일 디스크립터를 fget()으로 커널의 struct file 객체에 연결한다. 이어 vm_mmap_pgoff()가 보안 검사를 거치고 프로세스 주소 공간의 쓰기 잠금을 잡아 do_mmap()을 호출한다. 파일 디스크립터 번호만 저장해 두는 것이 아니라, 실제 열린 파일 객체를 바탕으로 매핑을 구성하는 것이다.[2][3]

do_mmap()은 길이를 페이지 단위로 올림하고, 사용 가능한 가상 주소 범위를 구하며, 파일의 접근 모드와 요청한 매핑 조건을 검사한다. 그 결과를 mmap_region()에 넘기면 내부의 __mmap_region()이 주소 공간에 매핑을 구성한다. 이 과정에서 등장하는 VMA(Virtual Memory Area)는 프로세스의 연속된 가상 주소 구간에 적용할 규칙을 담은 커널 자료구조다. VMA는 바이트 내용이나 페이지 테이블 자체가 아니다.[2]

새 VMA를 만드는 경로에서는 시작·끝 주소, 파일 내 시작 위치인 vm_pgoff, 읽기 등의 접근 조건, 파일 객체와 연결할 참조를 설정한다. 예제의 시작 주소에서 0바이트 떨어진 위치는 파일의 0바이트에 대응한다. 일반적으로 구간 내 주소의 파일 위치는 VMA의 파일 시작 오프셋에 구간 시작으로부터의 거리를 더해서 정해진다. 호환되는 이웃 VMA와 합칠 수 있으므로, mmap() 한 번이 언제나 별도 VMA 하나로 남는 것은 아니다.[2][4]

파일 시스템은 나중의 접근을 처리할 함수를 등록한다

새 파일 매핑을 설정하는 호출 흐름은 다음과 같다. 화살표는 이 예제 조건에서의 호출 관계이며, 모든 파일 시스템에 같은 끝부분이 적용되는 것은 아니다.

mmap 시스템 호출 → ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap
→ mmap_region → __mmap_region → mmap_file → call_mmap
→ file->f_op->mmap → ext4_file_mmap

mmap_file()은 call_mmap()을 통해 파일 연산 표의 mmap 함수를 호출한다. ext4의 파일 연산 표에는 ext4_file_mmap이 등록되어 있다. DAX가 아닌 경로에서 이 함수는 VMA의 vm_ops를 ext4_file_vm_ops로 지정한다. 이 표에는 .fault = filemap_fault, .map_pages = filemap_map_pages가 들어 있다. 지금 이 함수들로 파일 전체를 읽는 것이 아니라, 뒤에서 접근을 해결할 때 사용할 함수들을 연결해 두는 것이다.[4][5][6]


반환 뒤에도 남는 것

매핑을 만드는 코드는 vma->vm_file = get_file(file)로 파일 객체의 참조를 확보한다. 따라서 예제에서 디스크립터를 닫더라도 살아 있는 매핑이 파일 객체를 유지한다. close()는 munmap()의 대체물이 아니다.[2]

반대로, 성공 반환만으로 매핑 범위의 모든 페이지 테이블 엔트리(PTE)에 실제 메모리 연결이 만들어졌다고 생각해서는 안 된다. 이 예제에는 미리 접근을 채우도록 요청하는 옵션이 없고, do_mmap()의 populate 결과도 0으로 남는다. 준비된 것은 주소 구간의 의미와 접근을 처리할 경로다. 파일 데이터가 이미 메모리에 있는지와 이 프로세스가 그 데이터를 가리킬 PTE를 갖는지는 별개의 질문이다.[2][3]

CPU가 매핑된 첫 바이트를 읽다가 필요한 주소 변환을 찾지 못하면 페이지 폴트가 발생한다. 이것이 다음 장의 출발점이며, IDT를 통한 예외 진입과 VMA에 등록한 처리 경로가 이어진다. 파일 데이터의 공급에는 페이지 캐시가 관여한다. ext4의 파일 연산과 데이터 공급 경로는 ext4의 파일 읽기로 연결된다.

소스