대용량 파일을 처리할 때 일반적인 read()write() 함수를 사용하면 성능이 떨어지는 경우가 많다. 특히 파일의 일부분만 수정하거나, 파일을 여러 프로세스가 공유해야 할 때는 더욱 비효율적이다. 이럴 때 Memory Mapping을 사용하면 파일을 마치 메모리 배열처럼 직접 접근할 수 있어 성능을 크게 향상시킬 수 있다.

Memory Mapping이란?

image

Memory Mapping은 파일을 프로세스의 가상 주소 공간에 직접 매핑하는 방법이다. 이를 통해 read()write() 함수 없이도 일반 변수를 다루듯 파일 데이터에 접근할 수 있다.

기본 개념

// 일반적인 파일 I/O
char buffer[1024];
read(fd, buffer, 1024);
strcpy(buffer, "새로운 데이터");
write(fd, buffer, strlen(buffer));
 
// Memory Mapping 방식
char *mapped_addr = mmap(...);
strcpy(mapped_addr, "새로운 데이터");  // 직접 메모리 접근

Memory Mapping을 사용하면 운영체제가 파일 I/O를 페이지 단위로 자동 관리하여 성능이 크게 향상된다.

핵심 제약사항

Memory Mapping에는 중요한 제약이 있다.

매핑할 메모리 크기 ≤ 파일 크기

// 파일 크기: 100 bytes
// 매핑 크기: 200 bytes → SIGBUS 에러 발생
 
// 올바른 사용
// 파일 크기: 200 bytes  
// 매핑 크기: 100 bytes → 정상 동작

image
Virtual memory와 physical memory 간 매핑 구조

만약 매핑된 메모리보다 파일이 작다면, 파일 범위를 벗어난 주소에 접근할 때 SIGBUS(버스 오류)가 발생한다. 따라서 파일 크기가 작다면 truncate()ftruncate()로 먼저 파일을 확장해야 한다.

mmap()

→ 주소 공간에 매핑하는 함수

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
매개변수 설명
addr 매핑할 메모리 주소 (보통 NULL로 시스템이 자동 선택)
length 매핑할 메모리 공간 크기
prot 메모리 보호 모드 (읽기/쓰기/실행 권한)
flags 매핑 방식과 동작 제어
fd 매핑할 파일의 디스크립터
offset 파일에서 매핑을 시작할 오프셋

보호 모드 (prot)

보호 모드 (prot) 설명
PROT_READ 읽기 전용으로 접근 가능하게 설정한다. (메모리 내용을 읽을 수 있음)
PROT_WRITE 쓰기 허용. (메모리 내용을 수정할 수 있음)
PROT_EXEC 실행 가능. 해당 메모리에 있는 코드를 실행할 수 있게 한다.
PROT_NONE 접근 불가. 읽기, 쓰기, 실행 모두 금지된다.
PROT_READ | PROT_WRITE 읽기 + 쓰기 권한을 동시에 설정. (읽고 쓸 수 있음)

플래그 (flags)

플래그 이름 설명
MAP_SHARED 다른 프로세스와 데이터 변경 내용을 공유한다. → 쓰기 동작은 매핑된 메모리의 내용을 변경한다.
MAP_SHARED_VALIDATE MAP_SHARED와 같으나 전달받은 플래그를 커널이 확인하고 모르는 플래그가 있을 경우 오류로 처리한다.
MAP_PRIVATE 데이터의 변경 내용을 공유하지 않는다. → 처음 쓰기 동작이 생기면 매핑된 메모리의 사본을 복제해서 매핑 주소는 사본을 가리킨다.
MAP_SHARED 또는 MAP_PRIVATE 중 반드시 하나만 지정해야 하며, 둘을 같이 지정할 수 없다.
→ 매핑에 할당된 메모리 공간만큼 스왑 영역을 할당하고, 매핑된 데이터의 사본을 저장하는 데 사용된다.
MAP_ANONYMOUS fd를 무시하고 할당된 메모리 영역을 0으로 초기화한다 (offset은 0이어야 함).
MAP_FIXED 매핑할 주소를 정확히 지정하는 플래그. 이 플래그를 가진 mmap() 함수가 성공하면 해당 메모리 영역은 매핑된 내용으로 변경된다.
MAP_NORESERVE MAP_PRIVATE에서의 스왑 영역 할당을 이 플래그를 지정하면 하지 않게 된다.

리턴값과 에러 처리

image
void *result = mmap(...);
if (result == MAP_FAILED) {
    perror("mmap failed");
    exit(1);
}

주의사항: 크기가 0인 파일은 mmap()으로 매핑할 수 없다. 운영체제는 보통 4KB 페이지 단위로 메모리를 관리하는데, 빈 파일은 매핑할 실제 데이터 블록이 없기 때문이다.

예제

1. 기본 파일 매핑

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[]) {
    int fd;
    char *addr;
    struct stat statbuf;
 
    if (argc != 2) {
        fprintf(stderr, "Usage: %s filename\n", argv[0]);
        exit(1);
    }
 
    // 파일 정보 가져오기
    if (stat(argv[1], &statbuf) == -1) {
        perror("stat");
        exit(1);
    }
 
    // 파일 열기
    if ((fd = open(argv[1], O_RDWR)) == -1) {
        perror("open");
        exit(1);
    }
 
    // 메모리 매핑
    addr = mmap(NULL, statbuf.st_size, 
                PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
 
    // 파일을 닫아도 매핑된 메모리는 계속 사용 가능
    close(fd);
 
    // 파일 내용을 메모리처럼 직접 출력
    printf("%s\n", addr);
 
    // 메모리 매핑 해제
    if (munmap(addr, statbuf.st_size) == -1) {
        perror("munmap");
        exit(1);
    }
 
    return 0;
}

파일을 열고 mmap()을 통해 메모리에 매핑한 뒤, 파일 내용을 포인터처럼 직접 접근하여 출력한다. close() 후에도 munmap() 전까지 메모리 접근은 가능하다.

2. 빈 파일 생성 후 매핑

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main() {
    int fd, page_size, length;
    char *addr;
 
    // 시스템 페이지 크기 확인
    page_size = sysconf(_SC_PAGESIZE);
    length = 1 * page_size;  // 1페이지 크기로 설정
 
    // 빈 파일 생성
    if ((fd = open("test.dat", O_RDWR | O_CREAT | O_TRUNC, 0666)) == -1) {
        perror("open");
        exit(1);
    }
 
    // 파일 크기를 페이지 크기로 확장
    if (ftruncate(fd, length) == -1) {
        perror("ftruncate");
        exit(1);
    }
 
    // 메모리 매핑
    addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
 
    close(fd);
 
    // 매핑된 메모리에 직접 데이터 쓰기
    strcpy(addr, "Memory Mapping Test Data\n");
 
    printf("데이터가 파일에 저장되었습니다.\n");
 
    // 정리
    munmap(addr, length);
    return 0;
}

시스템 페이지 크기만큼 빈 파일을 만들고 확장한 후, mmap()으로 매핑하여 문자열 데이터를 직접 메모리에 기록한다. 이후 파일에도 저장된다.

관련 함수들

munmap(): 메모리 매핑 해제

int munmap(void *addr, size_t length);
// 매핑 해제
if (munmap(addr, statbuf.st_size) == -1) {
    perror("munmap");
    exit(1);
}
 
// 해제 후 접근하면 Segmentation Fault 발생
printf("%s\n", addr);

mprotect(): 보호 모드 변경

int mprotect(void *addr, size_t len, int prot);
// 읽기 전용으로 변경
if (mprotect(addr, length, PROT_READ) == -1) {
    perror("mprotect");
    exit(1);
}
 
// 이제 쓰기 시도하면 에러 발생
strcpy(addr, "test");  // Segmentation Fault

truncate()/ftruncate(): 파일 크기 조정

// 경로로 파일 크기 변경
int truncate(const char *path, off_t length);
 
// 파일 디스크립터로 크기 변경  
int ftruncate(int fd, off_t length);

동작 방식:

  • 파일이 더 큰 경우: 지정된 길이를 초과하는 부분 삭제
  • 파일이 더 작은 경우: 파일 크기를 증가시키고 추가 부분을 NULL 바이트('\0')로 채움
// 파일을 1MB로 확장
if (ftruncate(fd, 1024 * 1024) == -1) {
    perror("ftruncate");
    exit(1);
}

Memory Mapping의 활용 사례

1. 로그 파일 분석

// 로그 파일 분석 예제
void analyze_log_file(const char *filename) {
    int fd = open(filename, O_RDONLY);
    struct stat st;
    stat(filename, &st);
 
    char *data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (data == MAP_FAILED) return;
 
    char *line = data;
    char *end = data + st.st_size;
 
    while (line < end) {
        char *newline = memchr(line, '\n', end - line);
        if (!newline) break;
 
        *newline = '\0';
        if (strstr(line, "ERROR")) {
            printf("Error found: %s\n", line);
        }
        *newline = '\n';
 
        line = newline + 1;
    }
 
    munmap(data, st.st_size);
    close(fd);
}

로그 파일 전체를 읽어 메모리에 매핑한 후, 줄 단위로 탐색하면서 “ERROR”가 포함된 로그를 출력한다. 대용량 로그 분석에 유용하다.

2. 프로세스 간 통신 (IPC)

typedef struct {
    int counter;
    char message[256];
} shared_data_t;
 
void create_shared_memory() {
    int fd = open("/tmp/shared_mem", O_RDWR | O_CREAT, 0666);
    ftruncate(fd, sizeof(shared_data_t));
 
    shared_data_t *shared = mmap(NULL, sizeof(shared_data_t),
                                PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
 
    // 데이터 초기화
    shared->counter = 0;
    strcpy(shared->message, "Hello from parent");
 
    if (fork() == 0) {
        // 자식 프로세스
        shared->counter++;
        strcpy(shared->message, "Hello from child");
        exit(0);
    } else {
        // 부모 프로세스
        wait(NULL);
        printf("Counter: %d, Message: %s\n", shared->counter, shared->message);
    }
 
    munmap(shared, sizeof(shared_data_t));
    close(fd);
}

부모와 자식 프로세스가 MAP_SHARED를 통해 같은 메모리 영역을 공유하며 데이터를 주고받는다.