본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/Linux/메모리 매핑/파일 매핑과 COW

~ldmsys

파일 매핑(File-backed Mapping)은 파일의 범위를 프로세스의 가상 주소에 연결한다. 일반 ext4 파일의 buffered 경로를 기준으로, 읽을 때 페이지 캐시가 어떻게 연결되는지부터 살펴본다. 이어서 같은 파일의 쓰기가 MAP_PRIVATE와 MAP_SHARED에서 어떻게 달라지는지 비교한다. 기준은 Linux v6.12다.

파일 매핑과 첫 접근

다음 코드는 미리 존재하는 비어 있지 않은 파일 /home/ldmsys/a.txt의 첫 바이트를 읽는다. 실행 중 다른 프로세스가 파일을 줄이거나 바꾸지 않는다고 가정한다.

#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void) {
    int fd = open("/home/ldmsys/a.txt", 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()은 주소를 반환하고, 실제 바이트 읽기는 ((const unsigned char *)p)[0]에서 발생한다. 그 사이 fd를 닫아도 매핑이 파일 객체의 참조를 유지하므로 접근할 수 있다. 길이 1은 요청한 바이트 범위이며, 커널은 매핑을 페이지 단위로 관리한다.

ext4의 폴트 처리기

mmap → ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap
  → mmap_region → __mmap_region → mmap_file → call_mmap
  → file->f_op->mmap → ext4_file_mmap

ext4_file_mmap()은 DAX가 아닌 경로에서 VMA의 vm_ops를 ext4_file_vm_ops로 정한다. 이 표의 .fault는 filemap_fault, .map_pages는 filemap_map_pages, .page_mkwrite는 ext4_page_mkwrite다. 파일 전체를 즉시 읽는 대신, 이후 읽기·쓰기를 처리할 함수를 등록한다.[1]

읽기 폴트와 페이지 캐시

PTE가 없는 주소를 읽으면 공통 페이지 폴트 경로를 지나 다음 분기로 들어간다.

handle_mm_fault → __handle_mm_fault → handle_pte_fault
  → do_pte_missing → do_fault → do_read_fault
  → __do_fault → filemap_fault
  → 파일 페이지 준비 → finish_fault → PTE 설치

filemap_fault()는 해당 파일 오프셋의 페이지를 페이지 캐시에서 찾고, 필요하면 파일시스템을 통해 데이터를 읽는다. 입출력을 기다리는 동안 태스크가 잠들거나, 잠금을 놓았다가 처리를 재시도할 수 있다. 페이지가 준비되면 finish_fault()가 PTE를 연결하고, 사용자 모드의 load가 다시 실행된다.[2][3]

페이지가 이미 준비되어 있다면 do_fault_around()와 filemap_map_pages()가 주변 페이지까지 연결할 수 있다. 그 경로에서 해결되면 filemap_fault()를 호출하지 않는다. 첫 접근이 항상 저장장치 읽기를 일으키는 것도, 매 페이지마다 반드시 폴트가 하나씩 발생하는 것도 아니다.[2][3]

MAP_PRIVATE와 쓰기 시 복사

앞의 코드는 PROT_READ만 요청했으므로 쓰기는 허용되지 않는다. COW를 보려면 PROT_READ | PROT_WRITE와 MAP_PRIVATE로 매핑한다. 읽기 가능한 일반 파일을 private writable로 매핑하는 것은 파일 자체에 쓰는 것과 다르며, Linux는 이 경우 fd가 반드시 쓰기용으로 열려 있을 것을 요구하지 않는다. 반면 shared writable 매핑은 파일의 쓰기 권한도 필요하다.[4]

private 매핑도 읽기만 할 때는 파일의 페이지 캐시 페이지를 다른 매핑과 함께 사용할 수 있다. 처음부터 파일 전체 사본을 만들지 않는다. 대신 쓰기를 허용한 VMA라도 원본 파일 페이지를 가리키는 PTE에는 바로 쓰기 권한을 주지 않는다. 그 상태에서 store가 실행되면 보호 폴트가 발생한다.

읽기 단계
  프로세스 A의 private PTE ── R ──┐
                                ├─ 파일의 페이지 캐시
  프로세스 B의 매핑        ───────┘

A의 private 쓰기 후
  프로세스 A의 PTE ── RW ── 익명 사본 [A의 변경]
  프로세스 B의 매핑 ─────── 파일의 페이지 캐시 [원본]

커널은 원본 페이지의 내용을 익명 페이지로 복사하고, 쓰기를 요청한 프로세스의 PTE를 새 페이지로 바꾼다. 재시도한 store는 이 사본을 바꾸며 파일의 페이지 캐시는 바꾸지 않는다. VMA는 여전히 파일에 연결되어 있어도, 그 안의 수정된 페이지는 익명 페이지일 수 있다. COW는 VMA 전체가 아니라 실제로 쓰는 페이지 단위로 진행된다.[3]

최초 쓰기와 쓰기 보호 폴트

읽기 전에 처음부터 쓰는 경우와, 이미 읽기용 PTE가 있는 경우는 진입 함수가 다르다.

쓰기 직전 상태 주요 처리 경로 결과
PTE가 아직 없음 do_fault → do_cow_fault → 파일 fault 처리기 → finish_fault 파일 내용을 얻어 익명 사본을 만들고 PTE 설치
파일 페이지의 읽기 전용 PTE가 있음 do_wp_page → wp_page_copy 기존 파일 페이지에서 익명 사본을 만들고 PTE 교체

이미 private 익명 사본을 갖고 있고 그 PTE가 쓰기 가능하면, 이후 쓰기는 폴트 없이 진행한다. 그 사본도 fork() 뒤에는 익명 페이지의 COW 대상이 될 수 있다.[3]

MAP_PRIVATE는 파일의 일관된 스냅샷을 보장하는 기능은 아니다. 매핑 뒤 다른 경로에서 파일이 변경되었을 때 아직 private 사본으로 갈라지지 않은 부분에서 무엇이 보일지에 의존해서는 안 된다. 여기의 핵심 보장은 이 매핑을 통한 private 쓰기가 원래 파일에 반영되지 않는다는 것이다.[5]

MAP_SHARED와 변경 반영

MAP_SHARED에서 허용된 쓰기는 같은 파일 데이터를 공유하는 쪽에 적용된다. 파일 페이지를 프로세스 전용 익명 사본으로 갈라놓으면 shared의 의미를 잃는다.

PTE가 없는 첫 쓰기는 do_shared_fault()로 들어간다. 파일 페이지를 확보하고, 파일시스템의 page_mkwrite 콜백이 있으면 쓰기 준비를 요청한다. ext4에서는 ext4_page_mkwrite()가 필요한 블록과 파일 상태를 준비한다. 이어 PTE와 dirty 상태를 다루어, 변경된 페이지를 나중에 저장장치에 기록할 수 있게 한다. 이미 읽기 전용 PTE가 있는 shared 쓰기는 do_wp_page()의 shared 경로로 이어진다.[3][1][6]

메모리에서 변경이 보이는 시점과 디스크에 기록이 끝나는 시점은 다르다. dirty 페이지는 writeback 대상이며, 프로그램이 기록 완료를 명시적으로 기다려야 하면 msync(..., MS_SYNC) 등의 인터페이스와 오류 처리를 사용한다. MAP_SHARED 자체는 여러 실행 흐름의 동기화나 원자성을 보장하지 않는다.[5][7]

범위와 비용

일반 파일의 시작 오프셋은 페이지 크기에 맞춰야 한다. 주소가 VMA 안에 있더라도 파일이 잘려 해당 페이지가 파일 끝 밖으로 나가면 SIGBUS가 발생할 수 있다. 매핑 성공은 파일의 이후 크기까지 고정하는 약속이 아니다.[2]

buffered read()는 페이지 캐시의 데이터를 사용자 버퍼로 복사한다. 파일 mmap()은 그 페이지를 사용자 주소에 연결해 load/store로 접근하게 한다. 읽기 복사를 피할 수 있지만 폴트와 페이지 테이블 관리 비용이 들고, private 쓰기에는 COW 비용이 추가된다. 따라서 데이터 크기와 접근 패턴에 따라 유불리가 달라진다.

참고 문헌

  1. ↑ 1.0 1.1 Linux v6.12, fs/ext4/file.c, ext4_file_operations, ext4_file_mmap, ext4_file_vm_ops.
  2. ↑ 2.0 2.1 2.2 Linux v6.12, mm/filemap.c, filemap_fault와 filemap_map_pages.
  3. ↑ 3.0 3.1 3.2 3.3 3.4 Linux v6.12, mm/memory.c, do_read_fault, do_cow_fault, do_shared_fault, do_wp_page, finish_fault.
  4. ↑ Linux v6.12, mm/mmap.c, do_mmap의 MAP_PRIVATE·MAP_SHARED 및 파일 접근 모드 검사.
  5. ↑ 5.0 5.1 Linux man-pages, mmap(2), MAP_PRIVATE와 MAP_SHARED.
  6. ↑ Linux v6.12, fs/ext4/inode.c, ext4_page_mkwrite.
  7. ↑ Linux man-pages, msync(2), MS_SYNC.