크래프톤 정글 (컴퓨터 시스템: CSAPP)/9장 가상 메모리

컴퓨터 시스템 : CSAPP 9장 정리 - 9.8 메모리 매핑

고웅 2025. 4. 20. 14:54

9.8 메모리 매핑 (Memory Mapping)

리눅스는 가상 메모리 영역의 초기 내용을 디스크 상의 객체에 연결(mapping)함으로써 초기화한다. 이를 메모리 매핑(memory mapping)이라고 한다. 가상 메모리 영역은 다음 두 종류의 객체 중 하나에 매핑될 수 있다:

  1. 일반 파일
    • 예: 실행 파일, 객체 파일 등.
    • 파일의 일부분을 페이지 단위로 나누어 가상 페이지에 대응시킨다.
    • 요구 페이징(demand paging)을 사용하므로, 해당 페이지에 실제 접근하기 전까지는 메모리에 로드되지 않는다.
    • 영역 크기가 파일보다 크면, 나머지 공간은 0으로 패딩 된다.
  2. 익명 파일(anonymous file)
    • 커널이 생성한, 내용이 모두 0으로 채워진 임시 객체.
    • 이 영역의 페이지에 처음 접근할 때, 커널이 물리 메모리에서 희생 페이지를 선택하고, 그것을 0으로 덮어쓴 후 페이지 테이블을 갱신한다.
    • 이 과정에서는 디스크 I/O가 발생하지 않으며, 해당 페이지들을 demand-zero pages라고 부른다.

한 번 초기화된 가상 페이지는 커널이 관리하는 스왑 파일(swap space)을 통해 디스크와 메모리 간에 교체된다. 이 스왑 공간은 현재 실행 중인 모든 프로세스가 가질 수 있는 총 가상 페이지 수의 상한선 역할을 한다​.


9.8.1 공유 객체 재방문 (Shared Objects Revisited)

메모리 매핑의 기원은 가상 메모리 시스템과 기존 파일 시스템을 통합하면 프로그램과 데이터를 메모리에 효율적으로 로드할 수 있다는 통찰에서 출발한다.

공유 객체의 필요성

  • 모든 프로세스는 독립적인 주소 공간을 가져야 한다. 그러나 대부분의 프로세스는 같은 읽기 전용 코드를 공유한다.
    • 예: bash 셸을 실행하는 모든 프로세스는 동일한 코드 영역을 가진다.
    • 모든 C 프로그램은 printf 등 표준 라이브러리 함수를 사용한다.

이런 코드를 각 프로세스마다 중복 보관하면 메모리 낭비가 심하다.

공유 객체와 사적 객체

  • 공유 객체(shared object)로 매핑된 메모리 영역에 어떤 프로세스가 쓰기를 수행하면, 해당 쓰기는 다른 프로세스에도 보인다. 또한, 디스크 상의 원본에도 반영된다.
  • 사적 객체(private object)로 매핑된 경우에는 쓰기가 다른 프로세스에 보이지 않으며, 디스크에도 반영되지 않는다.

이러한 공유 객체는 메모리 매핑을 통해 효율적으로 관리된다​.


9.8.2 fork 함수 재방문

가상 메모리와 메모리 매핑 개념을 이해한 이후에는, fork 함수가 어떻게 독립적인 가상 주소 공간을 가진 새로운 프로세스를 생성하는지 더 명확히 알 수 있다.

fork 동작 원리

  1. 현재 프로세스가 fork()를 호출하면, 커널은 새로운 프로세스를 위한 자료구조들을 생성하고, 고유한 PID를 할당한다.
  2. 그리고 부모 프로세스의 가상 메모리 정보를 복사한다:
    • mm_struct
    • vm_area_struct (영역 구조체들)
    • 페이지 테이블
  3. 이때 모든 페이지는 읽기 전용(read-only)으로 설정되며, 각 영역은 private copy-on-write로 표시된다.
  4. fork가 반환되면, 자식 프로세스는 부모의 가상 메모리 상태를 정확히 복제한 형태로 실행을 시작한다.

Copy-on-Write (COW) 기법

  • 초기에는 부모와 자식이 동일한 물리 페이지를 공유한다.
  • 둘 중 하나가 해당 페이지에 쓰기(write)를 시도하면, 보호 예외가 발생하고 커널이 새로운 물리 페이지를 만들어 할당한다.
  • 이로 인해 페이지가 실제로 복사되는 시점은 쓰기가 발생한 순간까지 지연된다.
  • 이 방식은 물리 메모리 사용을 최소화하면서도 주소 공간의 독립성을 보장한다​.

9.8.3 execve 함수 재방문

가상 메모리와 메모리 매핑 개념을 이해하면, execve 함수가 프로그램을 메모리에 어떻게 로드하고 실행하는지 명확하게 이해할 수 있다.

동작 예시

프로세스가 다음과 같은 호출을 한다고 가정하자:

execve("a.out", NULL, NULL);

이 호출은 현재 프로세스를 대체하여 "a.out" 프로그램을 실행한다. 다음은 execve가 수행하는 주요 단계다:


1. 기존 사용자 영역 제거

  • 현재 프로세스의 가상 주소 공간 중 사용자 영역을 구성하는 모든 area struct를 제거한다.

2. 새 프로그램 영역 매핑

  • a.out 실행 파일에 기반해 새 코드(.text), 데이터(.data), BSS, 스택 영역을 생성한다.
  • 코드와 데이터는 private copy-on-write로 설정되고, 각각 .text와 .data 섹션에 매핑된다.
  • .bss, 힙, 스택은 demand-zero 영역으로 익명 파일에 매핑되며, 접근 시 0으로 초기화된다.

3. 공유 라이브러리 매핑

  • a.out이 공유 객체(libc.so 등)를 링크하고 있다면, 이들도 가상 주소 공간의 공유 영역에 매핑된다.

4. 프로그램 카운터 설정

  • 마지막으로, CPU의 프로그램 카운터(PC)를 새 코드 영역의 시작 지점(진입점)으로 설정한다.
  • 이후 스케줄링이 되면, 해당 지점부터 실행이 시작되며, 필요한 페이지들은 요구 페이징에 의해 로딩된다​.

9.8.4 사용자 수준 메모리 매핑 (mmap 함수 사용)

리눅스에서는 프로세스가 가상 메모리 영역을 직접 생성하고, 파일 또는 익명 객체를 그 영역에 매핑할 수 있도록 mmap 함수를 제공한다.

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

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
 
  • start: 매핑을 시작할 가상 주소 (보통 NULL로 지정하여 커널이 자동 선택하게 함)
  • length: 매핑할 바이트 수
  • prot: 접근 권한
    • PROT_EXEC: 실행 가능
    • PROT_READ: 읽기 가능
    • PROT_WRITE: 쓰기 가능
    • PROT_NONE: 접근 불가
  • flags: 매핑 유형
    • MAP_ANON: 익명 객체 매핑 (디스크 없이 0으로 초기화)
    • MAP_PRIVATE: 사적, copy-on-write
    • MAP_SHARED: 공유 객체
  • fd: 파일 디스크립터 (익명 매핑의 경우 -1)
  • offset: 파일에서 매핑을 시작할 위치

예시:

bufp = mmap(NULL, size, PROT_READ, MAP_PRIVATE | MAP_ANON, -1, 0);

→ size 바이트 크기의 읽기 전용, 사적, 요구-제로(demand-zero) 메모리 영역을 생성함.

영역 제거: munmap

int munmap(void *start, size_t length);
  • start부터 length 바이트만큼의 가상 메모리 영역을 제거함.
  • 제거된 영역에 접근하면 segmentation fault 발생​.