운영체제 기본/Linux의 파일 메모리 매핑/첫 접근과 페이지 폴트: 두 판 사이의 차이
파일 폴트의 데이터 공급을 페이지 캐시 문서에 연결 |
용어 통일: 시스템 콜, 컨텍스트 스위치와 첫 등장 영문 병기 |
||
| 59번째 줄: | 59번째 줄: | ||
== read와 다른 점, 여전히 남는 조건 == | == read와 다른 점, 여전히 남는 조건 == | ||
일반적인 buffered <code>read()</code>는 시스템 | 일반적인 buffered <code>read()</code>는 시스템 콜(System Call)이 요청한 파일 데이터를 사용자 버퍼로 복사해 전달한다. 반면 이 <code>mmap()</code> 경로는 파일 데이터가 있는 페이지를 사용자 가상 주소에 연결하고 CPU load로 읽게 한다. 별도 버퍼 복사를 피하더라도 저장장치 입출력이 없거나 모든 복사가 사라지거나 언제나 더 빠르지는 않다. 폴트와 페이지 테이블 관리 비용도 접근 패턴에 좌우된다. | ||
매핑 시작 파일 오프셋은 페이지 크기에 맞게 정렬하고, 읽을 바이트가 매핑 길이와 파일 데이터 범위 안에 있는지 확인한다. <code>mmap()</code> 성공은 파일이 이후에도 같은 길이라는 보장이 아니다. 다른 실행 흐름이 파일을 줄여 해당 페이지가 파일 끝 밖으로 밀려나면, VMA 범위 안의 주소라도 <code>SIGBUS</code>가 발생할 수 있다. 이는 단순한 VMA 부재와 다르다.<ref name="filemap" /> | 매핑 시작 파일 오프셋은 페이지 크기에 맞게 정렬하고, 읽을 바이트가 매핑 길이와 파일 데이터 범위 안에 있는지 확인한다. <code>mmap()</code> 성공은 파일이 이후에도 같은 길이라는 보장이 아니다. 다른 실행 흐름이 파일을 줄여 해당 페이지가 파일 끝 밖으로 밀려나면, VMA 범위 안의 주소라도 <code>SIGBUS</code>가 발생할 수 있다. 이는 단순한 VMA 부재와 다르다.<ref name="filemap" /> | ||
2026년 10월 31일 (토) 08:32 판
파일 메모리 매핑 목차 · 이전 글 mmap과 주소 공간
매핑된 주소를 처음 읽는 순간
앞 글에서 mmap()은 파일의 일부를 프로세스 주소 공간에 연결하는 VMA를 만들었다. 여기서는 Linux v6.12의 x86-64, 전통적인 IDT 예외 진입, 일반 ext4 파일의 buffered 경로를 기준으로 그 주소를 읽는다. 매핑은 PROT_READ와 MAP_PRIVATE로 만들었고 MAP_POPULATE와 DAX는 사용하지 않는다. 아래 흐름은 보통 크기 페이지의 PTE가 아직 없는 경우다.
/* p는 성공한 mmap()의 반환값이며 i는 매핑과 파일 범위 안의 오프셋이다. */ const unsigned char *bytes = p; unsigned char value = bytes[i];
load 명령은 read()를 호출하지 않는다. CPU는 가상 주소 bytes + i를 읽는다. VMA는 커널이 관리하는 영역 설명이고, PTE는 CPU의 주소 변환에 쓰이는 페이지 테이블 항목이다. 따라서 VMA가 존재한다는 사실과 그 주소를 지금 변환할 수 있다는 사실은 다르다. 파일 내용이 이미 메모리에 있더라도 이 프로세스의 PTE는 없을 수 있다.
앞선 다른 주소의 폴트가 주변 PTE까지 설치했다면 첫 load도 바로 성공한다. 반대로 당장 사용할 PTE가 없으면, 정상적인 매핑 안의 읽기라도 페이지 폴트가 발생한다. 이는 프로그램 오류와 다르다.
CPU 예외에서 VMA 검사까지
present PTE가 없으면 x86-64 CPU는 페이지 폴트 예외 #PF를 발생시킨다. 예외 벡터는 14이고 fault 주소는 CR2에 남는다. IDT에서 본 진입 장치를 사용하지만, 이 사건은 장치가 비동기적으로 보낸 완료 인터럽트가 아니라 지금 실행한 명령 때문에 발생한 동기 예외다.
load bytes[i] → #PF, vector 14 → asm_exc_page_fault → exc_page_fault → handle_page_fault → do_user_addr_fault
v6.12의 IDT는 해당 예외를 asm_exc_page_fault로 연결하며, C 쪽 exc_page_fault는 전통적인 진입 경로에서 CR2를 읽고 handle_page_fault()를 호출한다. 사용자 주소에 대한 처리는 do_user_addr_fault()로 이어진다.[1][2]
커널은 먼저 fault 주소를 포함하는 VMA를 찾아 접근 권한을 확인한다. v6.12에는 lock_vma_under_rcu()를 이용한 빠른 경로가 있고, 사용할 수 없거나 재시도가 필요하면 lock_mm_and_find_vma() 경로로 넘어간다. 매핑이 없거나 허용되지 않은 접근이라면 정상적인 파일 읽기 복구를 진행하지 않고 신호 처리로 이어질 수 있다. 유효한 읽기라면 handle_mm_fault()로 실제 주소 변환 복구를 요청한다.[2]
파일 읽기 폴트로 분기하기
handle_mm_fault()는 공통 검사와 회계 처리를 감싸며, 여기서 추적하는 일반 경로는 __handle_mm_fault()로 들어간다. 커널은 페이지 테이블 계층을 따라간다. PTE가 비어 있다면 다음 분기가 중요하다.
handle_mm_fault → __handle_mm_fault → handle_pte_fault → do_pte_missing → do_fault → do_read_fault
do_pte_missing()는 익명 메모리와 파일 매핑을 구분한다. 이 예제는 파일 매핑이므로 do_fault()로 가며, 쓰기 fault가 아니므로 do_read_fault()를 선택한다. MAP_PRIVATE라고 해서 읽는 순간 파일 전체를 사본으로 만드는 것은 아니다. 지금은 쓰기 시 복사가 아닌 읽기 경로다.[3]
여기에는 중요한 갈림길이 있다. do_read_fault()는 조건이 맞으면 먼저 do_fault_around()를 시도한다. ext4의 map_pages 콜백은 filemap_map_pages()이며, 이미 준비된 파일 페이지를 주변 주소까지 매핑할 수 있다. 여기서 해결되면 filemap_fault()는 호출하지 않는다. 모든 파일 폴트가 다음 경로를 거치지는 않는다.[3][4]
파일 데이터 준비와 PTE 설치
fault-around로 해결하지 못한 일반 경로에서는 __do_fault()가 vma->vm_ops->fault(vmf)를 호출한다. 앞 글에서 ext4가 등록한 이 함수 포인터의 대상이 filemap_fault()다. 가상 메모리와 파일 데이터 준비의 연결점이다.[3][4]
파일 데이터가 이미 준비되어 있을 수도 있고, 저장장치에서 읽어 올 필요가 있을 수도 있다. 필요한 작업을 기다리는 동안 현재 태스크가 잠들 수 있다는 점은 입출력 대기에 연결된다. 잠금을 놓았다가 다시 얻어 fault 처리를 재시도할 수도 있다. 파일 데이터는 페이지 캐시를 통해 공급된다.[5]
정상적으로 파일 페이지를 얻은 경로에서는 finish_fault()가 페이지 테이블 잠금 아래 상태를 다시 확인하고 set_pte_range()를 통해 PTE를 설치한다. PTE는 해당 페이지와 보호 속성을 연결한다. 예외 처리가 성공적으로 끝나 사용자 실행으로 돌아오면 중단됐던 load 명령이 다시 실행된다. 이번에는 주소 변환이 가능하므로 그 바이트를 읽는다.[3]
read와 다른 점, 여전히 남는 조건
일반적인 buffered read()는 시스템 콜(System Call)이 요청한 파일 데이터를 사용자 버퍼로 복사해 전달한다. 반면 이 mmap() 경로는 파일 데이터가 있는 페이지를 사용자 가상 주소에 연결하고 CPU load로 읽게 한다. 별도 버퍼 복사를 피하더라도 저장장치 입출력이 없거나 모든 복사가 사라지거나 언제나 더 빠르지는 않다. 폴트와 페이지 테이블 관리 비용도 접근 패턴에 좌우된다.
매핑 시작 파일 오프셋은 페이지 크기에 맞게 정렬하고, 읽을 바이트가 매핑 길이와 파일 데이터 범위 안에 있는지 확인한다. mmap() 성공은 파일이 이후에도 같은 길이라는 보장이 아니다. 다른 실행 흐름이 파일을 줄여 해당 페이지가 파일 끝 밖으로 밀려나면, VMA 범위 안의 주소라도 SIGBUS가 발생할 수 있다. 이는 단순한 VMA 부재와 다르다.[5]
성공한 매핑은 파일 디스크립터를 close()한 뒤에도 유지된다. 그러나 munmap() 뒤의 포인터는 더 이상 그 매핑을 가리키지 않는다. 주소가 재사용될 수도 있으므로 이후 접근이 반드시 신호로 실패하지도 않는다.
참고 문헌
- ↑ Linux v6.12 arch/x86/kernel/idt.c, 페이지 폴트 IDT 항목.
- ↑ 2.0 2.1 Linux v6.12 arch/x86/mm/fault.c, exc_page_fault, handle_page_fault, do_user_addr_fault.
- ↑ 3.0 3.1 3.2 3.3 Linux v6.12 mm/memory.c, handle_mm_fault, __handle_mm_fault, handle_pte_fault, do_pte_missing, do_fault, do_read_fault, finish_fault, set_pte_range.
- ↑ 4.0 4.1 Linux v6.12 fs/ext4/file.c, ext4_file_vm_ops와 ext4_file_mmap.
- ↑ 5.0 5.1 Linux v6.12 mm/filemap.c, filemap_fault와 filemap_map_pages.