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

운영체제 기본/Linux의 메모리 매핑/익명 메모리 매핑

~ldmsys

익명 매핑(Anonymous Mapping)은 사용자 파일의 내용을 가져오는 대신, 프로그램이 사용할 메모리 구간을 만든다. Linux v6.12의 MAP_PRIVATE | MAP_ANONYMOUS를 먼저 살펴본다. 새 구간은 0으로 초기화된 것처럼 보이지만, 그 길이만큼의 물리 RAM을 mmap() 시점에 모두 할당하는 것은 아니다.

익명 매핑과 지연 할당

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

int main(void) {
    long n = sysconf(_SC_PAGESIZE);
    if (n <= 0) return 1;
    unsigned char *p = mmap(NULL, (size_t)n, PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (p == MAP_FAILED) return 1;
    printf("%u\n", (unsigned)p[0]);
    p[0] = 65;
    printf("%u\n", (unsigned)p[0]);
    return munmap(p, (size_t)n) == -1;
}

출력은 0과 65다. 파일 디스크립터는 -1, 오프셋은 0이며, MAP_ANONYMOUS가 파일을 사용하지 않는 매핑임을 나타낸다. 여기서는 작은 새 매핑의 기본 페이지 경로를 따라간다. 큰 페이지, 미리 페이지를 채우는 옵션, 사용자 공간 폴트 처리기는 사용하지 않는다.

공통 설정 경로의 __mmap_region()은 이 구간에 vma_set_anonymous()를 적용한다. VMA는 읽기와 쓰기를 허용하지만, 처음에는 해당 주소의 PTE가 없을 수 있다. 이처럼 주소 공간의 확보와 물리 페이지의 준비를 분리하면, 프로그램이 예약만 하고 쓰지 않는 구간에 RAM을 낭비하지 않을 수 있다.[1]

첫 읽기와 0 페이지

첫 읽기에서 PTE가 없으면 앞 장의 아키텍처별 페이지 폴트 진입을 거쳐 다음 공통 경로로 들어온다.

읽기 → 페이지 폴트 → handle_mm_fault
  → handle_pte_fault → do_pte_missing → do_anonymous_page
  → 읽기 전용 0 페이지를 가리키는 PTE
  → 같은 읽기 명령 재시도 → 0

do_anonymous_page()는 쓰기 폴트가 아니고 해당 주소 공간에서 0 페이지를 허용하면, 이미 0으로 채워진 공용 페이지를 읽기 전용으로 연결한다. 여러 주소가 같은 0 페이지를 읽어도 관찰하는 값이 모두 0이므로 문제가 없다. 이때 각 주소를 위해 전용 데이터 페이지를 하나씩 만들 필요가 없다.[2]

VMA: 읽기·쓰기 허용

가상 페이지 A ── 읽기 전용 PTE ──┐
                               ├─ 공용 0 페이지
가상 페이지 B ── 읽기 전용 PTE ──┘

VMA가 쓰기를 허용해도 PTE는 읽기 전용이다. CPU가 직접 공용 0 페이지를 바꾸지 못하게 하고, 실제 쓰기를 커널이 한 번 받아 처리하기 위해서다. 주소 범위의 정책과 현재 페이지 테이블의 권한이 다른 이유가 여기서 드러난다.

첫 쓰기와 전용 페이지

0 페이지를 읽은 뒤 p[0] = 65를 실행하면, CPU는 읽기 전용 PTE에 대한 쓰기로 페이지 폴트를 일으킨다. VMA는 쓰기를 허용하므로 커널은 잘못된 접근으로 종료하지 않고, 그 주소에 전용 익명 페이지를 마련한다. do_wp_page()에서 wp_page_copy()로 이어지는 경로는 원본이 0 페이지인 경우 0으로 채운 새 페이지를 사용할 수 있다. 새 PTE를 쓰기 가능하게 연결한 뒤 같은 store를 재시도하면 65가 저장된다.[2]

쓰기 전: p ── 읽기 전용 ── 공용 0 페이지
쓰기 후: p ── 읽기·쓰기 ── 전용 익명 페이지 [65, 0, 0, ...]
                           공용 0 페이지는 그대로 유지

처음부터 읽지 않고 쓰면 경로가 조금 다르다. PTE가 없는 쓰기 폴트는 do_anonymous_page() 안에서 전용 페이지를 할당하고 초기화한다. 0 페이지를 먼저 연결했다가 다시 폴트를 일으킬 필요는 없다. 어느 경우든 새로 쓰는 바이트 외의 내용은 0으로 보여야 한다.[2]

fork와 쓰기 시 복사

fork() 뒤 부모와 자식은 서로 독립된 주소 공간을 갖는다. 그러나 이미 준비된 private 익명 페이지를 처음부터 전부 복사할 필요는 없다. Linux는 COW(Copy-on-Write, 쓰기 시 복사)를 적용할 페이지의 부모·자식 PTE를 읽기 전용으로 만들고, 당장은 같은 물리 페이지를 가리키게 할 수 있다.[3][4]

fork 직후                          자식의 쓰기 후
부모 PTE ── R ──┐                 부모 PTE ── R  ── 원래 페이지
                ├─ 원래 페이지
자식 PTE ── R ──┘                 자식 PTE ── RW ── 복사한 페이지

자식이 쓰면 보호 폴트가 발생한다. 커널은 VMA가 허용한 private 쓰기임을 확인하고, 원본 내용을 새 익명 페이지에 복사한 뒤 자식의 PTE만 교체한다. 부모는 원래 내용을 계속 본다. 이후 더는 다른 매핑과 공유하지 않아 페이지를 독점적으로 재사용할 수 있는 경우에는, 복사 없이 쓰기 권한을 회복하는 경로도 있다. 따라서 COW는 모든 쓰기마다 복사한다는 뜻이 아니라, 독립된 내용을 보장하는 데 필요한 시점까지 복사를 미루는 방식이다.[2]

복사를 미루면 fork() 직후 execve()로 주소 공간을 바꾸는 자식이나, 대부분 읽기만 하는 프로세스에 유리하다. 대신 첫 쓰기의 폴트, 필요한 페이지 복사, PTE 갱신 비용이 생긴다. 두 프로세스가 나중에 모두 쓰면 결국 각각의 물리 페이지가 필요하다.

공유 익명 매핑

MAP_SHARED | MAP_ANONYMOUS는 이름 있는 사용자 파일 없이 공유할 메모리 구간을 만든다. 예를 들어 매핑을 만든 뒤 fork()한 부모와 자식은 같은 구간의 변경을 공유한다. private 매핑처럼 쓰기 때마다 서로 독립된 사본으로 갈라지면 이 목적을 달성할 수 없다.

Linux는 이 경우 shmem_zero_setup()으로 내부 shmem 객체를 연결한다. 따라서 API상 익명 매핑이라는 말과, 커널 내부의 모든 익명 매핑이 do_anonymous_page()로 처리된다는 말은 다르다. 앞 절의 공용 0 페이지와 private COW 설명은 private anonymous 경로에 해당한다.[1][5]

공유 메모리가 생겼다고 스레드·프로세스 간 실행 순서까지 보장되지는 않는다. 읽기와 쓰기를 협력시키는 락·원자 연산 등의 문제는 동시성에서 다룬다.

참고 문헌

  1. ↑ 1.0 1.1 Linux v6.12, mm/mmap.c, __mmap_region의 익명 매핑 분기.
  2. ↑ 2.0 2.1 2.2 2.3 Linux v6.12, mm/memory.c, do_anonymous_page, do_wp_page, wp_page_copy.
  3. ↑ Linux v6.12, kernel/fork.c, dup_mmap과 copy_page_range 호출.
  4. ↑ Linux v6.12, mm/memory.c, copy_page_range, copy_present_ptes, __copy_present_ptes, do_wp_page.
  5. ↑ Linux v6.12, mm/shmem.c, shmem_zero_setup과 shmem_fault.