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

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

~ldmsys
둘러보기와 상하단 이전 다음 문서 틀 통일
잔글 Linux 각론 경로 변경에 따른 본문·목차·틀 링크 갱신
 
(같은 사용자의 중간 판 3개는 보이지 않습니다)
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/메모리 매핑/페이지 폴트 처리|페이지 폴트 처리]]
}}
}}
{{운영체제 기본 둘러보기}}
{{운영체제 기본 둘러보기}}

2026년 11월 1일 (일) 02:22 기준 최신판

mmap()은 호출한 프로세스에 가상 주소 구간을 설정한다. 파일을 읽기 위한 별도 명령이라기보다, 이 주소를 읽거나 쓸 때 어떤 대상을 어떻게 접근할 것인지를 정하는 인터페이스다. Linux v6.12의 MMU를 사용하는 네이티브 x86-64와 RISC-V RV64를 기준으로 한다.

mmap 인자와 매핑 대상

void *p = mmap(addr, length, prot, flags, fd, offset);

addr가 NULL이면 커널이 주소를 선택한다. length는 매핑할 길이이고, prot은 읽기·쓰기·실행 등 허용할 접근을 지정한다. flags는 공유 여부와 익명 매핑 여부 등을 정한다. 반환값은 물리 주소가 아니라 이 프로세스가 접근할 가상 주소이며, 실패하면 MAP_FAILED다.

대상 대표 인자 주소가 가리킬 대상
익명 메모리 MAP_PRIVATE | MAP_ANONYMOUS, fd = -1, offset = 0 처음에는 0으로 보이며, 쓰기에 필요한 RAM을 마련하는 영역
일반 파일 MAP_PRIVATE 또는 MAP_SHARED, 열린 파일의 fd 파일 오프셋에 대응하는 데이터
MMIO 장치 드라이버가 허용한 flags와 장치 fd 드라이버가 공개한 장치의 물리 주소 영역

MAP_PRIVATE와 MAP_SHARED는 읽기·쓰기 권한과 별개다. PROT_WRITE는 쓰기를 허용할지를 정하고, private/shared는 허용된 쓰기의 결과를 어떻게 취급할지 정한다. 일반 파일의 private 쓰기는 그 프로세스의 사본에 적용되며, shared 쓰기는 공유하는 파일 데이터에 적용된다. 익명·장치 매핑의 구체적인 동작은 뒤 장에서 구분한다.

VMA와 페이지 테이블

VMA(Virtual Memory Area)는 연속된 가상 주소 범위의 정책을 담는 커널 자료구조다. 시작·끝 주소, 접근 권한, 공유 여부, 연결된 파일과 처리 함수 등을 기록한다. 반면 페이지 테이블 엔트리(PTE)는 CPU가 개별 가상 페이지를 실제 대상으로 변환하는 데 쓰는 항목이다.

프로세스의 가상 주소 구간
  VMA: 이 구간의 의미, 권한, 대상, 처리 방법
    |
    +-- PTE 없음: 접근하면 페이지 폴트로 준비
    +-- PTE 있음: 지정된 RAM 또는 장치 주소로 변환

익명 메모리나 일반 파일의 보통 매핑에서는 mmap()이 VMA를 설정하고, 실제 페이지 준비와 PTE 설치는 접근할 때로 미룬다. 따라서 mmap() 성공이 모든 RAM 할당이나 파일 읽기의 완료를 뜻하지 않는다. 장치 드라이버는 매핑을 만드는 중에 PTE를 직접 설치할 수도 있다. 미리 페이지를 채우는 옵션이나 큰 페이지의 세부 경로 대신, 이후 장에서는 기본 페이지 단위의 일반적인 경로를 따라간다.

mmap 시스템 콜과 주소 공간

mmap()의 인자와 사용자 인터페이스는 두 아키텍처에서 같다. 그러나 사용자 공간에서 커널로 넘어가는 명령, 인자 레지스터, 커널 진입 코드는 다르다. 먼저 갈라지는 부분을 보고, 공통 메모리 관리 코드에서 다시 합친다. 여기서 설명하는 시스템 콜 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()에 전달한다. 오프셋은 이 경계에서 한 번 페이지 단위로 바뀐다.[7]

공통 경로와 대상별 연결

mmap
  +-- 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
  → mmap_region → __mmap_region
       +-- private anonymous: 익명 VMA 설정
       +-- 일반 파일: 파일시스템의 mmap 콜백
       +-- 장치 파일: 장치 드라이버의 mmap 콜백

ksys_mmap_pgoff()는 MAP_ANONYMOUS가 아니면 fget()으로 fd를 열린 struct file 객체에 연결한다. 익명 매핑은 사용자 파일을 찾지 않는다. vm_mmap_pgoff()가 주소 공간의 쓰기 잠금을 잡고, do_mmap()은 길이와 오프셋, 접근 조건을 검사하며 사용할 주소를 구한다.[8][9]

__mmap_region()은 VMA를 구성하고 대상별 처리를 연결한다. private anonymous 매핑에는 vma_set_anonymous()를 사용한다. 파일이 있으면 mmap_file()과 call_mmap()을 통해 file->f_op->mmap을 호출한다. 같은 파일 연산 표라도 일반 파일에서는 파일시스템의 함수, 장치 파일에서는 그 장치 드라이버의 함수로 이어진다. 커널이 VMA를 관리하는 틀은 공통이지만, 데이터의 공급 방식까지 같은 것은 아니다.[8][10][11]

파일 매핑의 vm_pgoff는 VMA 시작점에 대응하는 페이지 단위 파일 오프셋이다. 일반 파일에서는 구간 안 주소의 파일 위치가 다음처럼 정해진다.

파일 위치 = (vm_pgoff × PAGE_SIZE) + (접근 주소 - vm_start)

장치 파일의 오프셋 의미는 드라이버의 인터페이스에 달려 있다. 예를 들어 MMIO 장의 UIO는 오프셋을 장치의 매핑 영역 번호를 고르는 데 사용한다. 또한 인접한 호환 VMA와 합칠 수 있으므로 mmap() 호출 하나가 항상 VMA 하나로 남는 것은 아니다.[8]

매핑의 수명

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

주소 구간을 확보했다고 해서 이후 모든 접근의 성공이 보장되지는 않는다. 익명 페이지를 준비할 메모리가 부족할 수 있고, 매핑한 파일이 줄어들 수도 있으며, 장치에는 장치별 수명 조건이 있다. 다음 장에서는 유효한 접근을 CPU 예외와 커널이 어떻게 구별하고 완성하는지 살펴본다.

참고 문헌