메모리 관리 기초 ⭐⭐

메모리 관리 기초 ⭐⭐

개요

운영체제의 메모리 관리는 프로그램 실행에 필요한 메모리를 효율적으로 할당하고 관리하는 핵심 기능입니다. 이 장에서는 주소 바인딩, 논리/물리 주소 변환, 그리고 동적 로딩과 스와핑에 대해 학습합니다.


목차

  1. 메모리 관리의 필요성
  2. 주소 바인딩
  3. 논리 주소와 물리 주소
  4. MMU (Memory Management Unit)
  5. 동적 로딩
  6. 동적 링킹
  7. 스와핑
  8. 연습 문제

1. 메모리 관리의 필요성

다중 프로그래밍 환경

┌─────────────────────────────────────────────────────────────┐
│                        물리 메모리                           │
├─────────────────────────────────────────────────────────────┤
│  운영체제 (커널)                                            │
├─────────────────────────────────────────────────────────────┤
│  프로세스 A                                                  │
├─────────────────────────────────────────────────────────────┤
│  프로세스 B                                                  │
├─────────────────────────────────────────────────────────────┤
│  프로세스 C                                                  │
├─────────────────────────────────────────────────────────────┤
│  빈 공간 (Free)                                              │
└─────────────────────────────────────────────────────────────┘

메모리 관리의 목표

목표 설명
보호 프로세스 간 메모리 영역 보호
재배치 프로세스를 메모리 어디든 배치 가능
공유 여러 프로세스가 공통 코드 공유
효율성 메모리 낭비 최소화
논리적 구성 모듈 단위로 프로그램 구성

2. 주소 바인딩

주소 바인딩은 프로그램의 명령어와 데이터를 메모리 주소에 연결하는 과정입니다.

바인딩 시점

┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  소스 코드    │───▶│  목적 코드    │───▶│  실행 파일    │───▶│   메모리     │
│   (주소 無)   │    │ (재배치가능)  │    │ (로드 가능)   │    │  (물리주소)   │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
                         ↑                    ↑                    ↑
                    컴파일 시간           로드 시간            실행 시간
                      바인딩              바인딩               바인딩

2.1 컴파일 시간 바인딩 (Compile-time Binding)

프로세스가 메모리의 어느 위치에 적재될지 컴파일 시점에 알 수 있는 경우:

// 절대 주소를 사용하는 예 (과거 MS-DOS)
// 프로그램이 항상 주소 0x1000에서 시작한다고 가정
#define BASE_ADDRESS 0x1000

int main() {
    int* ptr = (int*)(BASE_ADDRESS + 0x100);  // 절대 주소
    *ptr = 42;
    return 0;
}

특징: - 절대 코드(Absolute Code) 생성 - 위치가 변경되면 재컴파일 필요 - 임베디드 시스템에서 주로 사용

2.2 로드 시간 바인딩 (Load-time Binding)

프로세스가 어디에 적재될지 실행 전까지 모르는 경우:

┌─────────────────────────────────────────────────────────────┐
│                     재배치 가능 코드                          │
├─────────────────────────────────────────────────────────────┤
│  LOAD  R1, [0x100]     ; 상대 주소 0x100                    │
│  ADD   R1, R2                                                │
│  STORE R1, [0x200]     ; 상대 주소 0x200                    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼ 로더가 기준 주소 0x5000 설정
┌─────────────────────────────────────────────────────────────┐
│                     메모리 적재 후                            │
├─────────────────────────────────────────────────────────────┤
│  LOAD  R1, [0x5100]    ; 0x5000 + 0x100                     │
│  ADD   R1, R2                                                │
│  STORE R1, [0x5200]    ; 0x5000 + 0x200                     │
└─────────────────────────────────────────────────────────────┘

특징: - 재배치 가능 코드(Relocatable Code) 생성 - 로더가 모든 주소를 수정 - 로드 후에는 이동 불가

2.3 실행 시간 바인딩 (Execution-time Binding)

프로세스가 실행 중에도 메모리 위치를 변경할 수 있는 경우:

┌──────────────────┐                  ┌──────────────────┐
│   CPU (논리주소)  │                  │   물리 메모리     │
│                  │                  │                  │
│   주소: 0x100    │─────┐            │                  │
└──────────────────┘     │            │                  │
                         ▼            │                  │
                 ┌──────────────┐     │  ┌────────────┐ │
                 │     MMU      │     │  │ 0x5100     │◀┤
                 │              │     │  │ (실제위치)  │ │
                 │ Base: 0x5000│────▶│  └────────────┘ │
                 │              │     │                  │
                 │ 0x100+0x5000│     │                  │
                 │  = 0x5100   │     │                  │
                 └──────────────┘     └──────────────────┘

특징: - 하드웨어 지원 필요 (MMU) - 현대 운영체제의 표준 방식 - 프로세스 이동(스와핑) 가능


3. 논리 주소와 물리 주소

3.1 개념 비교

구분 논리 주소 물리 주소
별칭 가상 주소 (Virtual Address) 실제 주소 (Real Address)
생성 CPU에서 생성 메모리 장치가 인식
범위 0 ~ 프로세스 크기 0 ~ 물리 메모리 크기
프로그래머 사용 인식 불필요

3.2 주소 공간

    프로세스 A의              프로세스 B의                 물리 메모리
    논리 주소 공간            논리 주소 공간

┌──────────────┐          ┌──────────────┐          ┌──────────────┐
│ 0x0000       │          │ 0x0000       │          │ 0x0000 OS    │
│              │          │              │          ├──────────────┤
│ 코드         │──────────┼──────────────┼─────────▶│ 0x1000 A코드 │
│              │          │ 코드         │──┐       ├──────────────┤
├──────────────┤          │              │  │       │ 0x2000 A데이터│
│ 데이터       │──────────┼──────────────┼──┼──────▶│              │
│              │          ├──────────────┤  │       ├──────────────┤
├──────────────┤          │ 데이터       │──┼──────▶│ 0x3000 B코드 │
│ 힙          │          │              │  │       ├──────────────┤
│              │          ├──────────────┤  │       │ 0x4000 B데이터│
├──────────────┤          │ 힙          │  │       ├──────────────┤
│              │          │              │  │       │ 0x5000 A힙   │
│ (빈 공간)    │          ├──────────────┤  │       ├──────────────┤
│              │          │              │  │       │ 0x6000 B힙   │
├──────────────┤          │              │  │       ├──────────────┤
│ 스택        │          ├──────────────┤  │       │              │
│ 0xFFFF       │          │ 스택        │  │       │ Free         │
└──────────────┘          │ 0xFFFF       │  │       │              │
                          └──────────────┘  │       └──────────────┘
                                            │
                                   MMU가 주소 변환

4. MMU (Memory Management Unit)

4.1 기본 구조

┌─────────────────────────────────────────────────────────────────┐
│                            CPU                                   │
│  ┌─────────────┐                                                │
│  │   프로그램   │                                                │
│  │   카운터    │──▶ 논리 주소: 0x1234                           │
│  └─────────────┘                                                │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                           MMU                                     │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                                                              │ │
│  │   논리 주소        재배치 레지스터          물리 주소        │ │
│  │     0x1234    +      0x8000        =       0x9234          │ │
│  │                                                              │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │   한계 레지스터: 0x4000                                      │ │
│  │   논리 주소 0x1234 < 0x4000 ? ──▶ OK (접근 허용)            │ │
│  │   논리 주소 0x5000 < 0x4000 ? ──▶ TRAP! (보호 위반)         │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                        물리 메모리                                │
│                                                                   │
│                        주소 0x9234 접근                          │
└──────────────────────────────────────────────────────────────────┘

4.2 재배치 레지스터와 한계 레지스터

// MMU 동작의 의사 코드
typedef struct {
    uint32_t relocation_register;  // 재배치 레지스터 (기준 주소)
    uint32_t limit_register;       // 한계 레지스터 (프로세스 크기)
} MMU;

uint32_t translate_address(MMU* mmu, uint32_t logical_address) {
    // 1. 한계 검사
    if (logical_address >= mmu->limit_register) {
        // 보호 위반! 트랩 발생
        raise_trap(SEGMENTATION_FAULT);
        return 0;
    }

    // 2. 주소 변환
    uint32_t physical_address = logical_address + mmu->relocation_register;

    return physical_address;
}

// 예시
MMU mmu = {
    .relocation_register = 0x8000,  // 프로세스가 0x8000에서 시작
    .limit_register = 0x4000        // 프로세스 크기 16KB
};

// 논리 주소 0x1234 → 물리 주소 0x9234 (OK)
// 논리 주소 0x5000 → SEGMENTATION FAULT (한계 초과)

4.3 컨텍스트 스위칭과 MMU

┌───────────────────────────────────────────────────────────────────┐
│                     컨텍스트 스위칭 과정                           │
├───────────────────────────────────────────────────────────────────┤
│                                                                    │
│   프로세스 A 실행 중          프로세스 B로 전환                    │
│   ┌──────────────┐                                                │
│   │ MMU 레지스터 │            1. A의 상태 저장                    │
│   │ Base: 0x8000 │               - 레지스터들                     │
│   │ Limit: 0x4000│               - MMU 설정값                     │
│   └──────────────┘                                                │
│           │                   2. B의 상태 복원                    │
│           │                      - 레지스터들                     │
│           ▼                      - MMU 설정값                     │
│   ┌──────────────┐                                                │
│   │ MMU 레지스터 │            3. 실행 재개                        │
│   │ Base: 0x14000│                                                │
│   │ Limit: 0x6000│                                                │
│   └──────────────┘                                                │
│                                                                    │
└───────────────────────────────────────────────────────────────────┘

5. 동적 로딩

5.1 개념

프로그램의 모든 코드를 메모리에 로드하지 않고, 필요할 때만 로드하는 기법입니다.

┌─────────────────────────────────────────────────────────────────┐
│                    동적 로딩 과정                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 프로그램 시작: main() 만 로드                               │
│                                                                  │
│     메모리                    디스크                            │
│  ┌──────────────┐         ┌──────────────┐                     │
│  │ main()       │         │ func_A()     │                     │
│  │              │         │ func_B()     │                     │
│  │              │         │ func_C()     │                     │
│  └──────────────┘         └──────────────┘                     │
│                                                                  │
│  2. func_A() 호출 시: 디스크에서 로드                           │
│                                                                  │
│     메모리                    디스크                            │
│  ┌──────────────┐         ┌──────────────┐                     │
│  │ main()       │    ◀──  │ func_A()     │                     │
│  │ func_A()     │         │ func_B()     │                     │
│  │              │         │ func_C()     │                     │
│  └──────────────┘         └──────────────┘                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5.2 구현 예제

#include <stdio.h>
#include <dlfcn.h>  // 동적 로딩을 위한 헤더

// 함수 포인터 타입 정의
typedef int (*MathFunc)(int, int);

int main() {
    void* handle;
    MathFunc add_func;
    char* error;

    // 1. 라이브러리 동적 로드
    handle = dlopen("./libmath.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "로드 실패: %s\n", dlerror());
        return 1;
    }

    // 2. 함수 심볼 찾기
    dlerror();  // 이전 에러 클리어
    add_func = (MathFunc)dlsym(handle, "add");
    error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "심볼 찾기 실패: %s\n", error);
        return 1;
    }

    // 3. 함수 호출
    printf("결과: %d\n", add_func(10, 20));  // 출력: 결과: 30

    // 4. 라이브러리 언로드
    dlclose(handle);

    return 0;
}
# 공유 라이브러리 생성
gcc -shared -fPIC -o libmath.so math.c

# 실행 파일 컴파일 (동적 로딩 라이브러리 링크)
gcc -o main main.c -ldl

# 실행
./main

5.3 장단점

장점 단점
메모리 사용량 감소 첫 호출 시 지연 발생
미사용 코드 로드 안 함 구현 복잡성 증가
운영체제 지원 불필요 오류 처리 필요

6. 동적 링킹

6.1 정적 링킹 vs 동적 링킹

┌─────────────────────────────────────────────────────────────────┐
│                        정적 링킹                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   프로그램 A          프로그램 B          프로그램 C            │
│  ┌──────────┐        ┌──────────┐        ┌──────────┐          │
│  │ 코드     │        │ 코드     │        │ 코드     │          │
│  ├──────────┤        ├──────────┤        ├──────────┤          │
│  │ libc     │        │ libc     │        │ libc     │          │
│  │ (복사본) │        │ (복사본) │        │ (복사본) │          │
│  └──────────┘        └──────────┘        └──────────┘          │
│                                                                  │
│  문제: 라이브러리 코드가 3번 중복!                              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                        동적 링킹                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   프로그램 A    프로그램 B    프로그램 C        공유 라이브러리   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐     ┌──────────┐     │
│  │ 코드     │  │ 코드     │  │ 코드     │     │ libc.so  │     │
│  ├──────────┤  ├──────────┤  ├──────────┤  ┌──│          │     │
│  │ stub     │──┼──────────┼──┼──────────┼──┤  │ printf() │     │
│  └──────────┘  │ stub     │──┼──────────┼──┤  │ malloc() │     │
│                └──────────┘  │ stub     │──┘  │ ...      │     │
│                              └──────────┘     └──────────┘     │
│                                                                  │
│  장점: 라이브러리 코드 1개만 메모리에!                          │
└─────────────────────────────────────────────────────────────────┘

6.2 스텁 (Stub)

// 동적 링킹의 스텁 동작 (의사 코드)

// 첫 번째 호출 시
void printf_stub() {
    // 1. 라이브러리가 메모리에 있는지 확인
    if (!is_library_loaded("libc.so")) {
        // 2. 없으면 로드
        load_library("libc.so");
    }

    // 3. 실제 함수 주소 획득
    void* real_printf = get_symbol_address("printf");

    // 4. 스텁을 실제 주소로 대체 (다음 호출은 직접 점프)
    replace_stub_with_address(real_printf);

    // 5. 실제 함수 호출
    jump_to(real_printf);
}

6.3 공유 라이브러리 확인

# 실행 파일이 사용하는 공유 라이브러리 확인
$ ldd /bin/ls
    linux-vdso.so.1 (0x00007ffd12345000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
    /lib64/ld-linux-x86-64.so.2 (0x00007f1234567000)

# 메모리에 로드된 공유 라이브러리 확인
$ cat /proc/self/maps | grep "\.so"

7. 스와핑

7.1 개념

┌─────────────────────────────────────────────────────────────────┐
│                        스와핑 과정                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   메모리 (RAM)                      디스크 (Backing Store)      │
│  ┌──────────────┐                  ┌──────────────┐             │
│  │ 운영체제     │                  │              │             │
│  ├──────────────┤                  │              │             │
│  │ 프로세스 A   │  ──Swap Out──▶  │ 프로세스 A   │             │
│  ├──────────────┤                  │ (이미지)     │             │
│  │ 프로세스 B   │                  │              │             │
│  ├──────────────┤  ◀──Swap In───  │ 프로세스 C   │             │
│  │ 프로세스 C   │                  │ (이미지)     │             │
│  │ (새로 로드)  │                  │              │             │
│  └──────────────┘                  └──────────────┘             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

7.2 스왑 영역

┌─────────────────────────────────────────────────────────────────┐
                      디스크 구조                                 
├─────────────────────────────────────────────────────────────────┤
                                                                  
  ┌────────────────────────────────────────────────────────────┐ 
                파일 시스템 파티션                               
    /home, /usr, /var, ...                                     
  └────────────────────────────────────────────────────────────┘ 
                                                                  
  ┌────────────────────────────────────────────────────────────┐ 
                스왑 파티션                                      
    프로세스 이미지 저장 공간                                   
    빠른 접근을 위해 연속 할당                                  
  └────────────────────────────────────────────────────────────┘ 
                                                                  
└─────────────────────────────────────────────────────────────────┘

7.3 스왑 시간 계산

스왑 시간 = 전송 시간 + 탐색 시간 + 회전 지연

예제:
- 프로세스 크기: 100MB
- 디스크 전송률: 50MB/sec
- 평균 탐색 시간: 8ms
- 평균 회전 지연: 4ms

전송 시간 = 100MB / 50MB/sec = 2초
스왑 아웃 시간 = 2초 + 8ms + 4ms ≈ 2.01초
스왑 인 시간  = 2초 + 8ms + 4ms ≈ 2.01초
총 스왑 시간  ≈ 4.02초

→ 매우 느림! 최소화 필요

7.4 Linux 스왑 관리

# 스왑 상태 확인
$ free -h
              total        used        free      shared  buff/cache   available
Mem:           15Gi       8.0Gi       2.5Gi       500Mi       5.0Gi       6.5Gi
Swap:          8.0Gi       1.2Gi       6.8Gi

# 스왑 파티션/파일 확인
$ swapon --show
NAME      TYPE      SIZE   USED PRIO
/dev/sda2 partition 8G     1.2G   -2

# 스왑 사용량 상세
$ cat /proc/swaps

# 스왑 파일 생성 (예: 4GB)
$ sudo fallocate -l 4G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile

# swappiness 설정 (0-100, 높을수록 적극적 스왑)
$ cat /proc/sys/vm/swappiness
60

# swappiness 변경
$ sudo sysctl vm.swappiness=10

7.5 모바일 시스템의 스와핑

┌─────────────────────────────────────────────────────────────────┐
│                    모바일 OS 메모리 관리                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  특징:                                                          │
│  - 전통적 스와핑 미사용 (플래시 메모리 수명 고려)               │
│  - 대신 앱 종료/재시작 방식                                     │
│                                                                  │
│  iOS:                                                           │
│  - 메모리 부족 시 백그라운드 앱 종료                            │
│  - 앱은 상태 저장 후 종료, 재시작 시 복원                       │
│                                                                  │
│  Android:                                                       │
│  - zRAM (압축 스왑)                                             │
│  - Low Memory Killer (LMK)                                      │
│  - 우선순위에 따라 프로세스 종료                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

연습 문제

문제 1: 주소 변환

프로세스의 재배치 레지스터가 0x4000이고 한계 레지스터가 0x3000일 때: 1. 논리 주소 0x1500의 물리 주소는? 2. 논리 주소 0x3500에 접근하면 어떤 일이 발생하는가?

정답 보기 1. 물리 주소 = 0x4000 + 0x1500 = 0x5500 2. 0x3500 >= 0x3000 (한계 초과) → Segmentation Fault 발생

문제 2: 동적 로딩 vs 동적 링킹

다음 설명이 동적 로딩과 동적 링킹 중 어느 것에 해당하는지 구분하시오.

  1. 여러 프로그램이 같은 라이브러리 코드를 공유한다
  2. 운영체제의 특별한 지원 없이 사용자 프로그램에서 구현 가능하다
  3. 스텁(stub) 코드를 사용한다
  4. 필요할 때 루틴을 메모리에 적재한다
정답 보기 1. 동적 링킹 - 공유 라이브러리 사용 2. 동적 로딩 - 프로그래머가 직접 구현 가능 3. 동적 링킹 - 스텁이 실제 함수 주소를 찾음 4. 동적 로딩 - 호출 시점에 로드

문제 3: 스왑 시간 계산

다음 조건에서 프로세스의 스왑 아웃 시간을 계산하시오. - 프로세스 크기: 200MB - 디스크 전송률: 100MB/sec - 탐색 시간: 10ms - 회전 지연: 5ms

정답 보기 전송 시간 = 200MB / 100MB/sec = 2초 = 2000ms 스왑 아웃 시간 = 2000ms + 10ms + 5ms = 2015ms ≈ 2.015초

문제 4: 코드 분석

다음 코드의 문제점을 찾고 수정하시오.

void* handle = dlopen("./plugin.so", RTLD_LAZY);
void (*func)() = dlsym(handle, "process");
func();
dlclose(handle);
정답 보기 문제점: 1. dlopen() 실패 시 NULL 체크 없음 2. dlsym() 실패 시 NULL 체크 없음 수정된 코드:
void* handle = dlopen("./plugin.so", RTLD_LAZY);
if (!handle) {
    fprintf(stderr, "Error: %s\n", dlerror());
    return;
}

dlerror();  // 이전 에러 클리어
void (*func)() = dlsym(handle, "process");
char* error = dlerror();
if (error != NULL) {
    fprintf(stderr, "Error: %s\n", error);
    dlclose(handle);
    return;
}

func();
dlclose(handle);

문제 5: 시스템 설계

임베디드 시스템에서 컴파일 시간 바인딩을 사용하는 이유를 설명하시오.

정답 보기 1. **결정론적 동작**: 실행 시간 변동이 없어 실시간 시스템에 적합 2. **오버헤드 감소**: MMU 없이 직접 물리 주소 사용으로 성능 향상 3. **메모리 제약**: MMU 하드웨어가 없거나 제한적인 환경 4. **단일 프로그램**: 멀티태스킹이 불필요한 경우 5. **비용 절감**: 단순한 하드웨어로 충분

다음 단계

11_Contiguous_Memory_Allocation.md에서 메모리 분할과 할당 전략을 배워봅시다!


참고 자료

  • Silberschatz, "Operating System Concepts" Chapter 8
  • Tanenbaum, "Modern Operating Systems" Chapter 3
  • Linux man pages: dlopen(3), mmap(2)
  • /proc/[pid]/maps - 프로세스 메모리 맵 확인
to navigate between lessons