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

운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간: 두 판 사이의 차이

~ldmsys
BIOS GRUB 부팅 경로 보강과 각론 형식 통일
RV64 파일 오프셋 단위 변환 설명 명확화
 
42번째 줄: 42번째 줄:
RV64의 네이티브 ABI는 <code>ecall</code> 명령을 사용한다. 시스템 콜 번호는 <code>a7</code>에, 같은 여섯 인자는 <code>a0</code>부터 <code>a5</code>에 놓인다. <code>ecall</code>은 사용자 모드의 환경 호출 예외를 일으킨다. CPU는 <code>stvec</code>에 등록된 트랩 진입점으로 이동하며, Linux의 <code>handle_exception</code>과 예외 분기 표를 거쳐 <code>do_trap_ecall_u()</code>로 전달된다. 이 함수는 <code>regs-&gt;a7</code>의 번호로 시스템 콜을 분기한다. <code>ecall</code> 다음 명령으로 돌아가도록 저장된 <code>epc</code>를 4 증가시키는 것도 여기서 한다.<ref name="mmap-rv-entry">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/entry.S?h=v6.12 Linux v6.12, arch/riscv/kernel/entry.S: handle_exception, excp_vect_table].</ref><ref name="mmap-rv-traps">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/traps.c?h=v6.12 Linux v6.12, arch/riscv/kernel/traps.c: do_trap_ecall_u].</ref><ref name="mmap-rv-abi">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/syscall.h?h=v6.12 Linux v6.12, arch/riscv/include/asm/syscall.h: syscall_get_arguments].</ref>
RV64의 네이티브 ABI는 <code>ecall</code> 명령을 사용한다. 시스템 콜 번호는 <code>a7</code>에, 같은 여섯 인자는 <code>a0</code>부터 <code>a5</code>에 놓인다. <code>ecall</code>은 사용자 모드의 환경 호출 예외를 일으킨다. CPU는 <code>stvec</code>에 등록된 트랩 진입점으로 이동하며, Linux의 <code>handle_exception</code>과 예외 분기 표를 거쳐 <code>do_trap_ecall_u()</code>로 전달된다. 이 함수는 <code>regs-&gt;a7</code>의 번호로 시스템 콜을 분기한다. <code>ecall</code> 다음 명령으로 돌아가도록 저장된 <code>epc</code>를 4 증가시키는 것도 여기서 한다.<ref name="mmap-rv-entry">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/entry.S?h=v6.12 Linux v6.12, arch/riscv/kernel/entry.S: handle_exception, excp_vect_table].</ref><ref name="mmap-rv-traps">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/traps.c?h=v6.12 Linux v6.12, arch/riscv/kernel/traps.c: do_trap_ecall_u].</ref><ref name="mmap-rv-abi">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/syscall.h?h=v6.12 Linux v6.12, arch/riscv/include/asm/syscall.h: syscall_get_arguments].</ref>


RV64의 <code>SYSCALL_DEFINE6(mmap, ...)</code>은 <code>riscv_sys_mmap(..., offset, 0)</code>을 호출한다. 이 도우미도 바이트 단위 오프셋의 정렬을 검사하고 페이지 단위로 바꾸어 <code>ksys_mmap_pgoff()</code>에 전달한다. 이 예제의 오프셋 0은 두 RV64 변환 단계에서도 0이다.<ref name="mmap-rv">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/sys_riscv.c?h=v6.12 Linux v6.12, arch/riscv/kernel/sys_riscv.c: mmap, riscv_sys_mmap, mmap2].</ref>
RV64의 <code>SYSCALL_DEFINE6(mmap, ...)</code>은 <code>riscv_sys_mmap(..., offset, 0)</code>을 호출한다. 이 도우미도 바이트 단위 오프셋의 정렬을 검사하고 페이지 단위로 바꾸어 <code>ksys_mmap_pgoff()</code>에 전달한다. 이 예제의 오프셋 0은 페이지 단위로 변환한 뒤에도 0이다.<ref name="mmap-rv">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/sys_riscv.c?h=v6.12 Linux v6.12, arch/riscv/kernel/sys_riscv.c: mmap, riscv_sys_mmap, mmap2].</ref>


=== 공통 경로: 파일 객체와 VMA ===
=== 공통 경로: 파일 객체와 VMA ===

2026년 10월 31일 (토) 09:28 기준 최신판

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

기준은 Linux v6.12, x86-64와 RISC-V RV64, 일반 ext4 파일, 읽기 전용 MAP_PRIVATE다. 두 아키텍처의 네이티브 64비트 사용자 ABI와 MMU를 사용하는 Linux를 대표 사례로 삼는다. RISC-V는 커널이 S-mode에서 실행되는 구성을 기준으로 한다. 페이지 캐시를 사용하는 일반적인 파일 매핑이며, 파일 오프셋은 0이고 페이지를 미리 채우는 옵션은 지정하지 않는다.

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

다음 프로그램은 미리 존재하는 비어 있지 않은 일반 파일 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만 요청하므로 이 주소를 통한 쓰기를 허용하지 않는다. 파일 수정과 쓰기 시 복사에 관한 이야기는 이 읽기 경로에서 제외한다.

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

mmap(NULL, 1, PROT_READ, MAP_PRIVATE, fd, 0)이라는 C 호출의 의미는 두 아키텍처에서 같다. 그러나 사용자 공간에서 커널로 넘어가는 명령, 인자 레지스터, 커널 진입 코드는 다르다. 먼저 갈라지는 부분을 보고, 공통 메모리 관리 코드에서 다시 합친다. 여기서 설명하는 시스템 콜 ABI는 일반 C 함수 호출 규약과 구별한다.

x86-64: syscall과 x86 mmap 진입점

네이티브 x86-64 ABI는 syscall 명령을 사용한다. 시스템 콜 번호는 rax에, 여섯 인자 addr, len, prot, flags, fd, offset은 순서대로 rdi, rsi, rdx, r10, r8, r9에 놓인다. mmap 번호는 9다. 이 진입은 뒤 장의 페이지 폴트처럼 IDT를 통하는 경로가 아니다.[1][2]

시스템 콜 디스패치를 거쳐 arch/x86/kernel/sys_x86_64.c의 SYSCALL_DEFINE6(mmap, ...) 본문에 도달한다. 이 코드는 바이트 단위 오프셋의 페이지 정렬을 검사하고, off >> PAGE_SHIFT로 페이지 단위 오프셋을 만들어 ksys_mmap_pgoff()를 호출한다.[3]

RISC-V RV64: ecall과 RISC-V mmap 진입점

RV64의 네이티브 ABI는 ecall 명령을 사용한다. 시스템 콜 번호는 a7에, 같은 여섯 인자는 a0부터 a5에 놓인다. ecall은 사용자 모드의 환경 호출 예외를 일으킨다. CPU는 stvec에 등록된 트랩 진입점으로 이동하며, Linux의 handle_exception과 예외 분기 표를 거쳐 do_trap_ecall_u()로 전달된다. 이 함수는 regs->a7의 번호로 시스템 콜을 분기한다. ecall 다음 명령으로 돌아가도록 저장된 epc를 4 증가시키는 것도 여기서 한다.[4][5][6]

RV64의 SYSCALL_DEFINE6(mmap, ...)은 riscv_sys_mmap(..., offset, 0)을 호출한다. 이 도우미도 바이트 단위 오프셋의 정렬을 검사하고 페이지 단위로 바꾸어 ksys_mmap_pgoff()에 전달한다. 이 예제의 오프셋 0은 페이지 단위로 변환한 뒤에도 0이다.[7]

공통 경로: 파일 객체와 VMA

mmap(addr, len, prot, flags, fd, byte_offset)
   |
   +-- x86-64: syscall -> entry_SYSCALL_64 -> x86 mmap
   |
   +-- RV64: ecall -> handle_exception -> do_trap_ecall_u
                 -> RV64 mmap -> riscv_sys_mmap
   |
   v
ksys_mmap_pgoff -> vm_mmap_pgoff -> do_mmap
   |
   v
mmap_region -> __mmap_region

이 그림의 디스패치에는 아키텍처별 시스템 콜 래퍼가 포함된다. 이후 함수가 공통 코드라는 것은 주소 배치와 보호 비트까지 두 CPU에서 같다는 뜻이 아니다. 공통 정책 코드도 주소 선택과 페이지 보호 속성 처리에서는 아키텍처별 구현을 사용한다.

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

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

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

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

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

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가 들어 있다. 지금 이 함수들로 파일 전체를 읽는 것이 아니라, 뒤에서 접근을 해결할 때 사용할 함수들을 연결해 두는 것이다.[10][11][12]


반환 뒤에도 남는 것

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

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

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

참고 문헌