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

잔글 관리자님이 운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간 문서를 운영체제 기본/Linux의 메모리 매핑/mmap과 주소 공간 문서로 이동했습니다: 공통 mmap 구조와 매핑 유형별 설명으로 재구성
잔글 Linux 각론 경로 변경에 따른 본문·목차·틀 링크 갱신
 
(같은 사용자의 중간 판 2개는 보이지 않습니다)
1번째 줄: 1번째 줄:
{{이전 다음 문서
{{이전 다음 문서
|목차=[[운영체제 기본/Linux의 파일 메모리 매핑|파일 메모리 매핑 목차]]
|이전=처음 문서
|다음=다음 문서 →<br />[[운영체제 기본/Linux의 파일 메모리 매핑/첫 접근과 페이지 폴트|첫 접근과 페이지 폴트]]
|목차=[[운영체제 기본/Linux/메모리 매핑|메모리 매핑 목차]]
|다음=다음 문서 →<br>[[운영체제 기본/Linux/메모리 매핑/페이지 폴트 처리|페이지 폴트 처리]]
}}
}}


기준은 '''Linux v6.12, x86-64와 RISC-V RV64, 일반 ext4 파일, 읽기 전용 <code>MAP_PRIVATE</code>'''다. 두 아키텍처의 네이티브 64비트 사용자 ABI와 MMU를 사용하는 Linux를 대표 사례로 삼는다. RISC-V는 커널이 S-mode에서 실행되는 구성을 기준으로 한다. 페이지 캐시를 사용하는 일반적인 파일 매핑이며, 파일 오프셋은 0이고 페이지를 미리 채우는 옵션은 지정하지 않는다.
<code>mmap()</code>은 호출한 프로세스에 가상 주소 구간을 설정한다. 파일을 읽기 위한 별도 명령이라기보다, <strong>이 주소를 읽거나 쓸 때 어떤 대상을 어떻게 접근할 것인지</strong>를 정하는 인터페이스다. Linux v6.12의 MMU를 사용하는 네이티브 x86-64와 RISC-V RV64를 기준으로 한다.


== 주소 매핑과 데이터 접근 ==
== mmap 인자와 매핑 대상 ==
다음 프로그램은 미리 존재하는 비어 있지 않은 일반 파일 <code>sample.bin</code>의 첫 바이트를 숫자로 출력한다. 실행 중 다른 프로세스가 파일을 줄이거나 내용을 바꾸지 않는다고 가정한다. 첫 바이트만 필요하므로 길이는 1이다. Linux는 매핑 범위를 페이지 단위로 관리하지만 프로그램은 그중 첫 바이트만 읽는다.
<pre>
void *p = mmap(addr, length, prot, flags, fd, offset);
</pre>
 
<code>addr</code>가 NULL이면 커널이 주소를 선택한다. <code>length</code>는 매핑할 길이이고, <code>prot</code>은 읽기·쓰기·실행 등 허용할 접근을 지정한다. <code>flags</code>는 공유 여부와 익명 매핑 여부 등을 정한다. 반환값은 물리 주소가 아니라 이 프로세스가 접근할 <strong>가상 주소</strong>이며, 실패하면 <code>MAP_FAILED</code>다.
 
{| class="wikitable"
! 대상 !! 대표 인자 !! 주소가 가리킬 대상
|-
| 익명 메모리 || MAP_PRIVATE &#124; MAP_ANONYMOUS, fd = -1, offset = 0 || 처음에는 0으로 보이며, 쓰기에 필요한 RAM을 마련하는 영역
|-
| 일반 파일 || MAP_PRIVATE 또는 MAP_SHARED, 열린 파일의 fd || 파일 오프셋에 대응하는 데이터
|-
| MMIO 장치 || 드라이버가 허용한 flags와 장치 fd || 드라이버가 공개한 장치의 물리 주소 영역
|}
 
<code>MAP_PRIVATE</code>와 <code>MAP_SHARED</code>는 읽기·쓰기 권한과 별개다. <code>PROT_WRITE</code>는 쓰기를 허용할지를 정하고, private/shared는 허용된 쓰기의 결과를 어떻게 취급할지 정한다. 일반 파일의 private 쓰기는 그 프로세스의 사본에 적용되며, shared 쓰기는 공유하는 파일 데이터에 적용된다. 익명·장치 매핑의 구체적인 동작은 뒤 장에서 구분한다.
 
== VMA와 페이지 테이블 ==
<strong>VMA(Virtual Memory Area)</strong>는 연속된 가상 주소 범위의 정책을 담는 커널 자료구조다. 시작·끝 주소, 접근 권한, 공유 여부, 연결된 파일과 처리 함수 등을 기록한다. 반면 <strong>페이지 테이블 엔트리(PTE)</strong>는 CPU가 개별 가상 페이지를 실제 대상으로 변환하는 데 쓰는 항목이다.


<pre>
<pre>
#include &lt;fcntl.h&gt;
프로세스의 가상 주소 구간
#include &lt;stdio.h&gt;
  VMA: 이 구간의 의미, 권한, 대상, 처리 방법
#include &lt;sys/mman.h&gt;
     |
#include &lt;unistd.h&gt;
     +-- PTE 없음: 접근하면 페이지 폴트로 준비
int main(void) {
     +-- PTE 있음: 지정된 RAM 또는 장치 주소로 변환
    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;
}
</pre>
</pre>


<code>mmap()</code>의 성공은 파일 내용을 읽는 데 성공했다는 뜻이 아니다. 커널이 해당 프로세스의 가상 주소 범위를 파일에 연결하는 설정을 받아들였다는 뜻이다. 반환값은 파일 내용 자체나 물리 주소가 아니라 '''프로세스가 나중에 접근할 가상 주소'''다. 실제 바이트 읽기는 <code>printf()</code>에 넘길 값을 계산하는 <code>((const unsigned char *)p)[0]</code>에서 발생한다. 두 동작 사이에는 <code>close(fd)</code>까지 들어갈 수 있다.
익명 메모리나 일반 파일의 보통 매핑에서는 <code>mmap()</code>이 VMA를 설정하고, 실제 페이지 준비와 PTE 설치는 접근할 때로 미룬다. 따라서 <code>mmap()</code> 성공이 모든 RAM 할당이나 파일 읽기의 완료를 뜻하지 않는다. 장치 드라이버는 매핑을 만드는 중에 PTE를 직접 설치할 수도 있다. 미리 페이지를 채우는 옵션이나 큰 페이지의 세부 경로 대신, 이후 장에서는 기본 페이지 단위의 일반적인 경로를 따라간다.
 
<code>MAP_PRIVATE</code>라는 이름도 이 시점에 파일 전체의 사본을 만든다는 뜻은 아니다. 이 예제는 <code>PROT_READ</code>만 요청하므로 이 주소를 통한 쓰기를 허용하지 않는다. 파일 수정과 쓰기 시 복사에 관한 이야기는 이 읽기 경로에서 제외한다.


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


=== x86-64: syscall과 x86 mmap 진입점 ===
=== x86-64: syscall과 x86 mmap 진입점 ===


네이티브 x86-64 ABI는 <code>syscall</code> 명령을 사용한다. 시스템 콜 번호는 <code>rax</code>에, 여섯 인자 <code>addr, len, prot, flags, fd, offset</code>은 순서대로 <code>rdi, rsi, rdx, r10, r8, r9</code>에 놓인다. <code>mmap</code> 번호는 9다. 이 진입은 뒤 장의 페이지 폴트처럼 IDT를 통하는 경로가 아니다.<ref name="mmap-x86-entry">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S?h=v6.12 Linux v6.12, arch/x86/entry/entry_64.S: entry_SYSCALL_64와 ABI 주석].</ref><ref name="mmap-x86-table">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/syscalls/syscall_64.tbl?h=v6.12 Linux v6.12, arch/x86/entry/syscalls/syscall_64.tbl: mmap].</ref>
네이티브 x86-64 ABI는 <code>syscall</code> 명령을 사용한다. 시스템 콜 번호는 <code>rax</code>에, 여섯 인자 <code>addr, len, prot, flags, fd, offset</code>은 순서대로 <code>rdi, rsi, rdx, r10, r8, r9</code>에 놓인다. <code>mmap</code> 번호는 9다. 이 진입은 [[운영체제 기본/Linux/메모리 매핑/페이지 폴트 처리|페이지 폴트]]처럼 IDT를 통하는 경로가 아니다.<ref name="mmap-x86-entry">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S?h=v6.12 Linux v6.12, arch/x86/entry/entry_64.S: entry_SYSCALL_64와 ABI 주석].</ref><ref name="mmap-x86-table">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/syscalls/syscall_64.tbl?h=v6.12 Linux v6.12, arch/x86/entry/syscalls/syscall_64.tbl: mmap].</ref>


시스템 콜 디스패치를 거쳐 <code>arch/x86/kernel/sys_x86_64.c</code>의 <code>SYSCALL_DEFINE6(mmap, ...)</code> 본문에 도달한다. 이 코드는 바이트 단위 오프셋의 페이지 정렬을 검사하고, <code>off &gt;&gt; PAGE_SHIFT</code>로 페이지 단위 오프셋을 만들어 <code>ksys_mmap_pgoff()</code>를 호출한다.<ref name="mmap-x86">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/sys_x86_64.c?h=v6.12 Linux v6.12, arch/x86/kernel/sys_x86_64.c: SYSCALL_DEFINE6(mmap)].</ref>
시스템 콜 디스패치를 거쳐 <code>arch/x86/kernel/sys_x86_64.c</code>의 <code>SYSCALL_DEFINE6(mmap, ...)</code> 본문에 도달한다. 이 코드는 바이트 단위 오프셋의 페이지 정렬을 검사하고, <code>off &gt;&gt; PAGE_SHIFT</code>로 페이지 단위 오프셋을 만들어 <code>ksys_mmap_pgoff()</code>를 호출한다.<ref name="mmap-x86">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/sys_x86_64.c?h=v6.12 Linux v6.12, arch/x86/kernel/sys_x86_64.c: SYSCALL_DEFINE6(mmap)].</ref>
42번째 줄: 52번째 줄:
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은 페이지 단위로 변환한 뒤에도 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>에 전달한다. 오프셋은 이 경계에서 한 번 페이지 단위로 바뀐다.<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 ===


=== 공통 경로와 대상별 연결 ===
<pre>
<pre>
mmap(addr, len, prot, flags, fd, byte_offset)
mmap
  |
  +-- x86-64: syscall → entry_SYSCALL_64 → x86 mmap
  +-- x86-64: syscall -> entry_SYSCALL_64 -> x86 mmap
  +-- RV64: ecall → handle_exception → do_trap_ecall_u
  |
                                  → RV64 mmap → riscv_sys_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
ksys_mmap_pgoff -> vm_mmap_pgoff -> do_mmap
      +-- private anonymous: 익명 VMA 설정
  |
      +-- 일반 파일: 파일시스템의 mmap 콜백
  v
      +-- 장치 파일: 장치 드라이버의 mmap 콜백
mmap_region -> __mmap_region
</pre>
</pre>


이 그림의 디스패치에는 아키텍처별 시스템 콜 래퍼가 포함된다. 이후 함수가 공통 코드라는 것은 주소 배치와 보호 비트까지 두 CPU에서 같다는 뜻이 아니다. 공통 정책 코드도 주소 선택과 페이지 보호 속성 처리에서는 아키텍처별 구현을 사용한다.
<code>ksys_mmap_pgoff()</code>는 <code>MAP_ANONYMOUS</code>가 아니면 <code>fget()</code>으로 fd를 열린 <code>struct file</code> 객체에 연결한다. 익명 매핑은 사용자 파일을 찾지 않는다. <code>vm_mmap_pgoff()</code>가 주소 공간의 쓰기 잠금을 잡고, <code>do_mmap()</code>은 길이와 오프셋, 접근 조건을 검사하며 사용할 주소를 구한다.<ref name="mmap">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/mmap.c?h=v6.12 Linux v6.12, mm/mmap.c], ksys_mmap_pgoff, do_mmap, __mmap_region.</ref><ref name="util">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/util.c?h=v6.12 Linux v6.12, mm/util.c], vm_mmap_pgoff.</ref>
 
<code>ksys_mmap_pgoff()</code>는 파일 디스크립터를 <code>fget()</code>으로 커널의 <code>struct file</code> 객체에 연결한다. 이어 <code>vm_mmap_pgoff()</code>가 프로세스 주소 공간의 쓰기 잠금을 잡아 <code>do_mmap()</code>을 호출한다. 파일 디스크립터 번호만 저장해 두는 것이 아니라, 실제 열린 파일 객체를 바탕으로 매핑을 구성하는 것이다.<ref name="mmap-mm">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/mmap.c?h=v6.12 Linux v6.12, mm/mmap.c: ksys_mmap_pgoff, do_mmap, mmap_region, __mmap_region].</ref><ref name="mmap-util">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/util.c?h=v6.12 Linux v6.12, mm/util.c: vm_mmap_pgoff].</ref>
 
<code>do_mmap()</code>은 길이를 페이지 단위로 올림하고, 사용 가능한 가상 주소 범위를 구하며, 파일의 접근 모드와 요청한 매핑 조건을 검사한다. 그 결과를 <code>mmap_region()</code>에 넘기면 내부의 <code>__mmap_region()</code>이 주소 공간에 매핑을 구성한다. 이 과정에서 등장하는 '''VMA(Virtual Memory Area)'''는 프로세스의 연속된 가상 주소 구간에 적용할 규칙을 담은 커널 자료구조다. VMA는 바이트 내용이나 페이지 테이블 자체가 아니다.<ref name="mmap-mm"/>


새 VMA를 만드는 경로에서는 시작·끝 주소, 파일 내 시작 위치인 <code>vm_pgoff</code>, 읽기 등의 접근 조건, 파일 객체와 연결할 참조를 설정한다. 예제의 시작 주소에서 0바이트 떨어진 위치는 파일의 0바이트에 대응한다. 일반적으로 구간 내 주소의 파일 위치는 VMA의 파일 시작 오프셋에 구간 시작으로부터의 거리를 더해서 정해진다. 호환되는 이웃 VMA와 합칠 수 있으므로, <code>mmap()</code> 한 번이 언제나 별도 VMA 하나로 남는 것은 아니다.<ref name="mmap-mm"/><ref name="mmap-internal">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/internal.h?h=v6.12 Linux v6.12, mm/internal.h: vma_set_range, mmap_file].</ref>
<code>__mmap_region()</code>은 VMA를 구성하고 대상별 처리를 연결한다. private anonymous 매핑에는 <code>vma_set_anonymous()</code>를 사용한다. 파일이 있으면 <code>mmap_file()</code>과 <code>call_mmap()</code>을 통해 <code>file-&gt;f_op-&gt;mmap</code>을 호출한다. 같은 파일 연산 표라도 일반 파일에서는 파일시스템의 함수, 장치 파일에서는 그 장치 드라이버의 함수로 이어진다. 커널이 VMA를 관리하는 틀은 공통이지만, 데이터의 공급 방식까지 같은 것은 아니다.<ref name="mmap"/><ref name="internal">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/internal.h?h=v6.12 Linux v6.12, mm/internal.h], mmap_file.</ref><ref name="fs">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fs.h?h=v6.12 Linux v6.12, include/linux/fs.h], call_mmap.</ref>


== 파일시스템의 페이지 폴트 처리기 등록 ==
파일 매핑의 <code>vm_pgoff</code>는 VMA 시작점에 대응하는 페이지 단위 파일 오프셋이다. 일반 파일에서는 구간 안 주소의 파일 위치가 다음처럼 정해진다.
ext4 파일 매핑을 설정하는 호출 흐름은 다음과 같다.


<pre>
<pre>
mmap 시스템 콜 → ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap
파일 위치 = (vm_pgoff × PAGE_SIZE) + (접근 주소 - vm_start)
→ mmap_region → __mmap_region → mmap_file → call_mmap
→ file->f_op->mmap → ext4_file_mmap
</pre>
</pre>


<code>mmap_file()</code>은 <code>call_mmap()</code>을 통해 파일 연산 표의 <code>mmap</code> 함수를 호출한다. ext4의 파일 연산 표에는 <code>ext4_file_mmap</code>이 등록되어 있다. DAX가 아닌 경로에서 이 함수는 VMA의 <code>vm_ops</code>를 <code>ext4_file_vm_ops</code>로 지정한다. 이 표에는 <code>.fault = filemap_fault</code>, <code>.map_pages = filemap_map_pages</code>가 들어 있다. 지금 이 함수들로 파일 전체를 읽는 것이 아니라, '''뒤에서 접근을 해결할 때 사용할 함수들을 연결해 두는 것'''이다.<ref name="mmap-internal"/><ref name="mmap-fs">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fs.h?h=v6.12 Linux v6.12, include/linux/fs.h: call_mmap].</ref><ref name="mmap-ext4">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/ext4/file.c?h=v6.12 Linux v6.12, fs/ext4/file.c: ext4_file_operations, ext4_file_mmap, ext4_file_vm_ops].</ref>
장치 파일의 오프셋 의미는 드라이버의 인터페이스에 달려 있다. 예를 들어 MMIO 장의 UIO는 오프셋을 장치의 매핑 영역 번호를 고르는 데 사용한다. 또한 인접한 호환 VMA와 합칠 수 있으므로 <code>mmap()</code> 호출 하나가 항상 VMA 하나로 남는 것은 아니다.<ref name="mmap"/>


== 매핑의 수명 ==
파일 매핑은 파일 객체에 대한 참조를 보유한다. 매핑 성공 뒤 fd를 <code>close()</code>하더라도 매핑은 남는다. <code>munmap()</code>은 주소 구간의 매핑을 해제하는 별도 작업이다. 해제한 포인터를 다시 사용하면 안 되며, 같은 주소가 나중에 재사용될 수 있으므로 잘못된 접근이 항상 신호로 드러나지도 않는다.<ref name="mmap"/>


 
주소 구간을 확보했다고 해서 이후 모든 접근의 성공이 보장되지는 않는다. 익명 페이지를 준비할 메모리가 부족할 수 있고, 매핑한 파일이 줄어들 수도 있으며, 장치에는 장치별 수명 조건이 있다. 다음 장에서는 유효한 접근을 CPU 예외와 커널이 어떻게 구별하고 완성하는지 살펴본다.
== mmap 반환 후의 매핑 상태 ==
매핑을 만드는 코드는 <code>vma->vm_file = get_file(file)</code>로 파일 객체의 참조를 확보한다. 따라서 예제에서 디스크립터를 닫더라도 살아 있는 매핑이 파일 객체를 유지한다. <code>close()</code>는 <code>munmap()</code>의 대체물이 아니다.<ref name="mmap-mm"/>
 
반대로, 성공 반환만으로 매핑 범위의 모든 페이지 테이블 엔트리(PTE)에 실제 메모리 연결이 만들어졌다고 생각해서는 안 된다. 이 예제에는 미리 접근을 채우도록 요청하는 옵션이 없고, <code>do_mmap()</code>의 <code>populate</code> 결과도 0으로 남는다. 준비된 것은 주소 구간의 의미와 접근을 처리할 경로다. 파일 데이터가 이미 메모리에 있는지와 이 프로세스가 그 데이터를 가리킬 PTE를 갖는지는 별개의 질문이다.<ref name="mmap-mm"/><ref name="mmap-util"/>
 
CPU가 매핑된 첫 바이트를 읽다가 필요한 주소 변환을 찾지 못하면 페이지 폴트가 발생한다. 이것이 [[운영체제 기본/Linux의 파일 메모리 매핑/첫 접근과 페이지 폴트|다음 장]]의 출발점이며, x86-64의 [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|IDT]] 진입과 RISC-V의 트랩 진입을 각각 거쳐 VMA에 등록한 공통 처리 경로로 이어진다. 파일 데이터의 공급에는 [[운영체제 기본/Linux의 파일시스템/페이지 캐시|페이지 캐시]]가 관여한다. ext4의 파일 연산과 데이터 공급 경로는 [[운영체제 기본/Linux의 파일시스템/ext4의 파일 읽기|ext4의 파일 읽기]]로 연결된다.


== 참고 문헌 ==
== 참고 문헌 ==
93번째 줄: 90번째 줄:


{{이전 다음 문서
{{이전 다음 문서
|목차=[[운영체제 기본/Linux의 파일 메모리 매핑|파일 메모리 매핑 목차]]
|이전=처음 문서
|다음=다음 문서 →<br />[[운영체제 기본/Linux의 파일 메모리 매핑/첫 접근과 페이지 폴트|첫 접근과 페이지 폴트]]
|목차=[[운영체제 기본/Linux/메모리 매핑|메모리 매핑 목차]]
|다음=다음 문서 →<br>[[운영체제 기본/Linux/메모리 매핑/페이지 폴트 처리|페이지 폴트 처리]]
}}
}}
{{운영체제 기본 둘러보기}}
{{운영체제 기본 둘러보기}}