Skip to content

필사 모드: [운영체제] 10. 가상 메모리: 요구 페이징부터 스래싱까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

가상 메모리 개념

가상 메모리(Virtual Memory)는 프로세스 전체가 물리 메모리에 올라와 있지 않아도 실행할 수 있게 해주는 기술이다. 프로그래머에게 실제 물리 메모리보다 훨씬 큰 주소 공간을 제공한다.

[가상 메모리의 동작]

가상 주소 공간 (프로세스 관점): 물리 메모리:

+-----------+ 0xFFFF... +-----------+

| 스택 | | OS |

| | | +-----------+

| v | | 페이지 A | <-- 가상 페이지 0

| | +-----------+

| | | 페이지 C | <-- 가상 페이지 5

| ^ | +-----------+

| | | | 페이지 D | <-- 가상 페이지 2

| 힙 | +-----------+

+-----------+ | 빈 공간 |

| 데이터 | +-----------+

+-----------+

| 코드 | 디스크 (스왑 영역):

+-----------+ 0x0000... +-----------+

| 페이지 B | <-- 가상 페이지 1

4GB 가상 공간 중 일부만 | 페이지 E | <-- 가상 페이지 3

물리 메모리에 올라가 있음 +-----------+

가상 메모리의 장점은 다음과 같다.

- 프로그램이 물리 메모리보다 클 수 있음

- 각 프로세스의 주소 공간이 격리됨

- 여러 프로세스가 페이지를 공유할 수 있음 (공유 라이브러리, fork 시 COW)

- 프로세스 생성이 효율적

요구 페이징 (Demand Paging)

프로그램 시작 시 모든 페이지를 메모리에 올리는 대신, 실제로 필요할 때만(on demand) 페이지를 로드한다.

[요구 페이징 개념]

페이지 테이블:

+-------+------+

| 프레임 | 유효 |

+-------+------+

| 3 | v | <- 메모리에 있음

| - | i | <- 디스크에 있음 (아직 로드 안 됨)

| 7 | v | <- 메모리에 있음

| - | i | <- 디스크에 있음

+-------+------+

v = valid (메모리에 있음)

i = invalid (메모리에 없음, 디스크에 있음)

페이지 폴트 (Page Fault)

유효하지 않은 페이지에 접근하면 페이지 폴트가 발생한다.

[페이지 폴트 처리 과정]

1. CPU가 페이지 테이블 참조

2. 유효 비트가 i (invalid) -> 트랩 발생

|

v

3. OS가 페이지 폴트 확인

- 잘못된 참조(주소 범위 밖)? -> 프로세스 종료

- 유효하지만 메모리에 없음? -> 계속 진행

|

v

4. 빈 프레임 찾기

|

v

5. 디스크에서 페이지를 빈 프레임으로 읽기 (I/O)

|

v

6. I/O 완료 시:

- 페이지 테이블 갱신 (프레임 번호, 유효 비트 = v)

- 프로세스 재시작 (폴트를 일으킨 명령어부터)

요구 페이징의 성능

[유효 접근 시간(EAT)]

p = 페이지 폴트 확률

ma = 메모리 접근 시간 (200ns)

페이지 폴트 처리 시간 = 8ms (8,000,000ns)

EAT = (1 - p) * ma + p * 페이지 폴트 시간

= (1 - p) * 200 + p * 8,000,000

p = 0.001 (1000번에 1번 폴트) 이면:

EAT = 0.999 * 200 + 0.001 * 8,000,000

= 199.8 + 8,000

= 8,199.8ns

성능 저하 40배!

10% 이내 성능 저하를 위해서는:

220 > 200 + 7,999,800 * p

p < 0.0000025

즉, 400,000번 접근에 1번 이하의 페이지 폴트 필요

쓰기 시 복사 (Copy-on-Write, COW)

fork() 시 부모의 페이지를 즉시 복사하지 않고, 두 프로세스가 같은 페이지를 공유한다. 어느 한쪽이 페이지를 수정하려 할 때만 해당 페이지를 복사한다.

[Copy-on-Write 동작]

fork() 직후:

부모 페이지 테이블: 자식 페이지 테이블:

페이지0 -> 프레임 A (COW) 페이지0 -> 프레임 A (COW)

페이지1 -> 프레임 B (COW) 페이지1 -> 프레임 B (COW)

페이지2 -> 프레임 C (COW) 페이지2 -> 프레임 C (COW)

자식이 페이지1을 수정하면:

부모 페이지 테이블: 자식 페이지 테이블:

페이지0 -> 프레임 A (COW) 페이지0 -> 프레임 A (COW)

페이지1 -> 프레임 B 페이지1 -> 프레임 D (복사본)

페이지2 -> 프레임 C (COW) 페이지2 -> 프레임 C (COW)

수정된 페이지만 복사 -> 불필요한 복사 제거

// COW가 적용되는 fork + exec 패턴

#include <unistd.h>

#include <sys/wait.h>

int main() {

pid_t pid = fork();

// fork 시점: 부모의 페이지를 COW로 공유

if (pid == 0) {

// exec() 호출 시 자식의 주소 공간이 완전히 교체됨

// COW 덕분에 fork에서 불필요한 페이지 복사 없음

execlp("ls", "ls", "-la", NULL);

} else {

wait(NULL);

}

return 0;

}

페이지 교체 (Page Replacement)

물리 메모리가 가득 찼을 때 새 페이지를 올리려면, 기존 페이지 중 하나를 디스크로 내보내야(교체해야) 한다.

[페이지 교체 과정]

1. 디스크에서 페이지 위치 확인

2. 빈 프레임 찾기

- 빈 프레임이 있으면 사용

- 없으면 교체 알고리즘으로 희생 페이지 선택

- 희생 페이지가 수정되었으면(dirty bit) 디스크에 기록

- 수정되지 않았으면 그냥 덮어쓰기

3. 새 페이지를 빈 프레임에 로드

4. 페이지 테이블 갱신

5. 프로세스 재시작

1. FIFO 페이지 교체

가장 먼저 올라온 페이지를 가장 먼저 교체한다.

[FIFO 예시] 프레임 3개, 참조열: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

참조 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

[7][ ][ ] -> 폴트

[7][0][ ] -> 폴트

[7][0][1] -> 폴트

[2][0][1] -> 폴트 (7 교체)

[2][0][1] -> 히트

[2][3][1] -> 폴트 (0 교체)

[2][3][0] -> 폴트 (1 교체)

[4][3][0] -> 폴트 (2 교체)

[4][2][0] -> 폴트 (3 교체)

[4][2][3] -> 폴트 (0 교체)

[0][2][3] -> 폴트 (4 교체)

[0][2][3] -> 히트

[0][2][3] -> 히트

[0][1][3] -> 폴트 (2 교체)

[0][1][2] -> 폴트 (3 교체)

[0][1][2] -> 히트

[0][1][2] -> 히트

[7][1][2] -> 폴트 (0 교체)

[7][0][2] -> 폴트 (1 교체)

[7][0][1] -> 폴트 (2 교체)

총 페이지 폴트: 15

**벨래디의 이상현상(Belady's Anomaly)**: FIFO에서는 프레임 수를 늘려도 페이지 폴트가 증가할 수 있다.

2. 최적 페이지 교체 (OPT)

앞으로 가장 오랫동안 사용되지 않을 페이지를 교체한다. 이론적 최적이지만 미래를 알 수 없으므로 실제 구현이 불가능하다. 다른 알고리즘의 비교 기준으로 사용한다.

[OPT 예시] 프레임 3개, 참조열: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

참조 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

[7][ ][ ] -> 폴트

[7][0][ ] -> 폴트

[7][0][1] -> 폴트

[2][0][1] -> 폴트 (7: 가장 나중에 사용)

[2][0][1] -> 히트

[2][0][3] -> 폴트 (1: 가장 나중에 사용)

[2][0][3] -> 히트

[2][4][3] -> 폴트 (0을 곧 사용하므로 다른 것 교체)

[2][4][3] -> 히트

[2][4][3] -> 히트

[2][0][3] -> 폴트

[2][0][3] -> 히트

[2][0][3] -> 히트

[2][0][1] -> 폴트

[2][0][1] -> 히트

[2][0][1] -> 히트

[2][0][1] -> 히트

[7][0][1] -> 폴트

[7][0][1] -> 히트

[7][0][1] -> 히트

총 페이지 폴트: 9 (최적)

3. LRU 페이지 교체 (Least Recently Used)

가장 오랫동안 사용되지 않은 페이지를 교체한다. OPT의 근사치로, 과거 정보를 미래의 근사로 사용한다.

[LRU 예시] 프레임 3개, 참조열: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

참조 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

[7][ ][ ] -> 폴트

[7][0][ ] -> 폴트

[7][0][1] -> 폴트

[2][0][1] -> 폴트 (7: 가장 오래 전 사용)

[2][0][1] -> 히트

[2][0][3] -> 폴트 (1: 가장 오래 전 사용)

[2][0][3] -> 히트 (0이 최근 사용)

[4][0][3] -> 폴트 (2: 가장 오래 전 사용)

[4][0][2] -> 폴트 (3: 가장 오래 전 사용)

[4][3][2] -> 폴트 (0: 가장 오래 전 사용)

[0][3][2] -> 폴트 (4: 가장 오래 전 사용)

[0][3][2] -> 히트

[0][3][2] -> 히트

[1][3][2] -> 폴트 (0: 가장 오래 전 사용)

[1][3][2] -> 히트

[1][0][2] -> 폴트 (3: 가장 오래 전 사용)

[1][0][2] -> 히트

[1][0][7] -> 폴트 (2: 가장 오래 전 사용)

[1][0][7] -> 히트

[1][0][7] -> 히트

총 페이지 폴트: 12

LRU 구현 방법은 다음과 같다.

- **카운터 방식**: 각 페이지 테이블 항목에 접근 시간 기록. 교체 시 가장 오래된 것 선택

- **스택 방식**: 페이지 접근 시 스택 맨 위로 이동. 맨 아래가 LRU 페이지

두 방법 모두 하드웨어 지원이 필요하며, 순수 LRU 구현은 비용이 높다.

4. 클럭 알고리즘 (Second-Chance / Clock)

LRU의 근사 알고리즘으로, 참조 비트(reference bit)를 사용한다.

[클럭 알고리즘]

프레임들이 원형 큐를 이룸

각 프레임에 참조 비트(ref)가 있음

포인터

|

v

[A, ref=1] -> [B, ref=0] -> [C, ref=1] -> [D, ref=0]

^ |

| |

+------------------------------------------+

교체가 필요할 때:

1. 포인터가 가리키는 프레임 확인

2. ref=0이면: 이 페이지를 교체

3. ref=1이면: ref를 0으로 리셋하고, 포인터를 다음으로 이동

4. 2-3을 반복

동작 과정:

포인터 -> [A, ref=1]: ref=0으로 변경, 다음으로

포인터 -> [B, ref=0]: 이 페이지를 교체!

참조 비트는 하드웨어가 페이지 접근 시 자동으로 1로 설정

// 클럭 알고리즘 구현 (개념적)

#define NUM_FRAMES 64

struct frame {

int page_number;

int reference_bit;

int dirty_bit;

};

struct frame frames[NUM_FRAMES];

int clock_hand = 0;

int clock_replace() {

while (1) {

if (frames[clock_hand].reference_bit == 0) {

// 교체 대상 발견

int victim = clock_hand;

clock_hand = (clock_hand + 1) % NUM_FRAMES;

return victim;

}

// 두 번째 기회 부여: 참조 비트 초기화

frames[clock_hand].reference_bit = 0;

clock_hand = (clock_hand + 1) % NUM_FRAMES;

}

}

알고리즘 비교

[페이지 교체 알고리즘 비교]

알고리즘 성능 벨래디 이상 구현 비용 비고

-------- ------ ---------- -------- ----

FIFO 나쁨 O 낮음 가장 단순

OPT 최적 X 불가능 이론적 기준

LRU 좋음 X 높음 하드웨어 지원 필요

클럭 양호 X 중간 LRU 근사, 실용적

프레임 할당 (Frame Allocation)

여러 프로세스가 실행될 때, 각 프로세스에 몇 개의 프레임을 할당할지 결정해야 한다.

할당 전략

[프레임 할당 방식]

1. 균등 할당: 모든 프로세스에 동일한 수의 프레임

100 프레임, 5 프로세스 -> 각 20 프레임

2. 비례 할당: 프로세스 크기에 비례하여 할당

총 프레임 = 62

P1 크기 = 10페이지 -> 10/137 * 62 = 4.5 -> 5 프레임

P2 크기 = 127페이지 -> 127/137 * 62 = 57.5 -> 57 프레임

전역 교체 vs 지역 교체

- **전역 교체(Global Replacement)**: 모든 프레임 중에서 희생 페이지 선택. 처리량이 높지만 예측 불가능

- **지역 교체(Local Replacement)**: 해당 프로세스의 프레임 내에서만 교체. 예측 가능하지만 비효율적일 수 있음

스래싱 (Thrashing)

프로세스가 실행보다 페이지 교체에 더 많은 시간을 보내는 현상이다.

[스래싱 발생 과정]

1. 프로세스에 프레임이 부족

2. 페이지 폴트 빈번 발생

3. CPU 이용률 저하 (I/O 대기)

4. OS가 "CPU가 놀고 있다"고 판단

5. 다중 프로그래밍 정도 증가 (프로세스 추가)

6. 기존 프로세스의 프레임이 더 줄어듦

7. 더 많은 페이지 폴트 -> 악순환!

CPU 이용률

^

| /\

| / \

| / \

| / \------> 스래싱!

| /

| /

+--+---+---+---+----> 다중 프로그래밍 정도

1 2 3 4

워킹 셋 모델 (Working-Set Model)

특정 시점에서 프로세스가 활발히 사용하는 페이지의 집합을 워킹 셋이라 한다.

[워킹 셋 개념]

참조열: ... 2 6 1 5 7 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 ...

|<-- 윈도우 크기 (delta) -->|

시점 t1의 워킹 셋(delta=10):

WS(t1) = 페이지 1, 2, 3, 4 (윈도우 내 고유 페이지)

워킹 셋 크기 = 활발히 사용 중인 페이지 수

[워킹 셋 기반 프레임 할당]

총 필요 프레임 D = 모든 프로세스의 워킹 셋 크기 합

if D > 가용 프레임 수:

스래싱 발생 가능

-> 일부 프로세스를 일시 중단(swap out)

if D <= 가용 프레임 수:

각 프로세스에 워킹 셋 크기만큼 프레임 할당

-> 스래싱 방지

페이지 폴트 빈도 (Page-Fault Frequency)

워킹 셋보다 직접적인 방법으로, 페이지 폴트 비율을 직접 모니터링한다.

[PFF 기반 프레임 조절]

페이지 폴트율

^

|

|---- 상한선 ---- 폴트율이 이 위면: 프레임 추가 할당

|

|

|---- 하한선 ---- 폴트율이 이 아래면: 프레임 회수

|

+--+---+---+---+----> 할당된 프레임 수

상한선 초과: 프로세스에 프레임을 더 할당

하한선 미달: 프로세스에서 프레임을 회수

추가 할당할 프레임이 없으면: 프로세스 일시 중단

메모리 매핑 파일 (Memory-Mapped Files)

파일 I/O를 메모리 접근으로 처리하는 기법이다. 파일의 일부 또는 전체를 프로세스의 가상 주소 공간에 매핑한다.

#include <stdio.h>

#include <sys/mman.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <string.h>

int main() {

// 파일 열기

int fd = open("data.txt", O_RDWR);

struct stat sb;

fstat(fd, &sb);

// 파일을 메모리에 매핑

char *mapped = mmap(NULL, sb.st_size,

PROT_READ | PROT_WRITE,

MAP_SHARED, fd, 0);

if (mapped == MAP_FAILED) {

perror("mmap 실패");

return 1;

}

// 파일 내용을 메모리처럼 접근

printf("파일 내용: %.50s\n", mapped);

// 메모리에 쓰면 파일도 변경됨 (MAP_SHARED)

memcpy(mapped, "수정된 내용", 15);

// 변경 사항을 디스크에 동기화

msync(mapped, sb.st_size, MS_SYNC);

// 매핑 해제

munmap(mapped, sb.st_size);

close(fd);

return 0;

}

[메모리 매핑 파일 동작]

프로세스 가상 주소 공간: 물리 메모리: 디스크:

+------------------+ +-----------+ +-------+

| | | | | |

| mmap 영역 -------|-------->| 파일 페이지 |<----->| 파일 |

| | | | | |

+------------------+ +-----------+ +-------+

1. mmap()으로 파일을 주소 공간에 매핑

2. 해당 영역 접근 시 페이지 폴트 -> 파일에서 로드

3. 메모리 쓰기 시 dirty 페이지 표시

4. 주기적으로 또는 munmap 시 디스크에 기록

장점:

- read/write 시스템 콜 오버헤드 없음

- 여러 프로세스가 같은 파일을 공유 가능

- 페이지 캐시를 자동으로 활용

프로세스 간 공유 메모리로서의 mmap

// 프로세스 간 공유 메모리 (MAP_SHARED + MAP_ANONYMOUS)

#include <sys/mman.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/wait.h>

int main() {

// 익명 공유 매핑 생성

int *shared = mmap(NULL, sizeof(int),

PROT_READ | PROT_WRITE,

MAP_SHARED | MAP_ANONYMOUS,

-1, 0);

*shared = 0;

pid_t pid = fork();

if (pid == 0) {

// 자식 프로세스

*shared = 42;

printf("자식: shared = %d\n", *shared);

} else {

// 부모 프로세스

wait(NULL);

printf("부모: shared = %d\n", *shared);

// 출력: 부모: shared = 42

munmap(shared, sizeof(int));

}

return 0;

}

정리

[가상 메모리 핵심 개념 요약]

+-------------------+--------------------+

| 요구 페이징 | 필요할 때만 로드 |

| COW | fork 시 복사 최소화 |

+-------------------+--------------------+

| FIFO | 단순하지만 성능 낮음 |

| OPT | 이론적 최적 (구현 불가)|

| LRU | 실용적이고 성능 좋음 |

| 클럭 | LRU 근사, 대부분 사용 |

+-------------------+--------------------+

| 스래싱 | 프레임 부족으로 폴트 |

| | 폭증하는 현상 |

| 워킹 셋 | 활발한 페이지 집합 |

| | 추적으로 스래싱 방지 |

+-------------------+--------------------+

| 메모리 매핑 파일 | 파일 I/O를 메모리 |

| | 접근으로 처리 |

+-------------------+--------------------+

가상 메모리는 물리 메모리의 한계를 극복하고 프로세스에게 독립적인 주소 공간을 제공하는 핵심 기술이다. 요구 페이징으로 필요한 페이지만 로드하고, LRU나 클럭 알고리즘으로 효율적인 페이지 교체를 수행하며, 워킹 셋 모델로 스래싱을 방지한다. 메모리 매핑 파일은 효율적인 파일 I/O와 프로세스 간 통신을 가능하게 한다.

현재 단락 (1/374)

가상 메모리(Virtual Memory)는 프로세스 전체가 물리 메모리에 올라와 있지 않아도 실행할 수 있게 해주는 기술이다. 프로그래머에게 실제 물리 메모리보다 훨씬 큰 주소 공...

작성 글자: 0원문 글자: 7,614작성 단락: 0/374