운영체제 학습 노트 목차

메모리 관리 — 유한한 RAM을 쥐어짜다

가상 메모리가 "메모리가 무한한 척"을 가능케 했지만 — 물리 RAM은 유한하다. 그래서 OS는 제한된 프레임을 어떻게 나눠 주고, 가득 차면 무엇을 내보내고, 부족이 심해지면 어떻게 버티는지를 끊임없이 결정한다. 이 유한한 자원의 관리가 — 무엇을 메모리에 두고 무엇을 버릴까라는 보편적 고민의 원형임을 본다.

빈틈의 두 종류 — 내부·외부 단편화

메모리를 나눠 주다 보면 *빈틈(단편화)*이 생긴다. **내부 단편화**는 — 필요보다 큰 블록을 줘서 안에 못 쓰는 자투리가 남는 것이다(3KB만 필요한데 4KB 페이지를 통째로 주면 1KB 낭비). 외부 단편화는 — 전체 빈 공간은 충분한데, 흩어져 있어 연속으로 못 쓰는 것이다(여기 2KB, 저기 2KB 비었는데 4KB 연속이 필요하면 못 준다). 흥미롭게도 페이징이 외부 단편화를 거의 없앤다 — 메모리를 고정 크기 프레임으로 나누면 어느 빈 프레임이든 갖다 쓰면 되니까(연속일 필요가 없다). 대신 페이지 크기만큼의 내부 단편화는 남는다 — 단편화는 형태를 바꿀 뿐 공짜로 사라지진 않는다.

가득 차면 누굴 내보내나 — 페이지 교체

물리 메모리가 꽉 찼는데 페이지가 필요하면 — 기존 페이지 하나를 디스크로 내보내고(swap out) 그 자리를 내준다. 누구를 희생시킬지페이지 교체 알고리즘이다. FIFO(먼저 온 것 먼저)는 단순하나 어리석고, 이상적인 OPT(앞으로 가장 오래 안 쓸 것)는 미래를 모르니 불가능하다. 현실적 절충이 LRU(가장 오래 안 쓴 것)최근 안 쓴 페이지는 앞으로도 안 쓸 가능성이 높다는 *지역성(locality)*에 기댄다. (출처: Page replacement.) 캐시 시스템의 LRU/LFU 축출도 바로 이 페이지 교체와 같은 원리다 — 유한한 공간에서 무엇을 버릴지의 보편적 문제다.

너무 부족하면 — 스래싱

부족이 심해지면 재앙이 온다. 모든 프로세스의 *작업 집합(working set, 최근에 실제로 쓰는 페이지들)*을 합친 게 물리 메모리를 넘으면페이지를 내보내자마자 다시 필요해져, 디스크와 메모리 사이로 페이지를 계속 주고받느라 일은 거의 못 하고 페이지 교체 하게 된다. 이게 **스래싱(thrashing)**이다. CPU 사용률은 떨어지는데 디스크는 미친 듯이 돌고 시스템이 기어간다. 해법은 — 작업 집합이 메모리에 맞도록 동시 실행 프로세스를 줄이거나, 메모리를 늘리는 것이다. (출처: Thrashing & working set.)

영리한 절약 — copy-on-write, 그리고 최후의 수단

OS는 메모리를 영리하게 아낀다. 02편의 fork가 부모를 통째로 복사하지 않고 — **copy-on-write(COW)**로 부모·자식이 일단 같은 물리 페이지를 공유하다가 누군가 쓰려는 순간에만 그 페이지를 복사한다(대부분은 안 바뀌니 큰 절약이다). 그래도 메모리가 완전히 바닥나면 — 리눅스의 **OOM Killer**가 덜 중요한 프로세스를 골라 강제 종료해 RAM을 확보한다(프로그램이 만나는 "메모리 부족" 오류의 시스템 버전이다).

실제로 눈으로 보기

스래싱은 이 두 컬럼에서 보인다

vmstat 1
# procs -----------memory---------- ---swap-- -----io----  -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2  8 892340  12044   1832  48120 4512 3908 12044  9822 8221 41022  6  9  3 82  0
#                                    │    │                              │
#                                    │    └ so — 초당 swap out (KB)      └ wa — I/O 대기
#                                    └ si — 초당 swap in (KB)

**si·so가 계속 0이 아니면 스래싱**이다. 위 출력이 교과서적인 모습이다 — CPU는 6%밖에 안 쓰는데(us), I/O 대기가 82%(wa)이고, 컨텍스트 스위치가 초당 4만 번(cs)이다. 일은 안 하고 페이지만 주고받는 상태가 숫자로 그대로 나타난다.

"서버가 느린데 CPU는 놀고 있다" 는 신고가 오면 가장 먼저 볼 곳이 여기다.

작업 집합이 메모리에 맞는지

free -m
#                total   used   free  shared  buff/cache   available
# Mem:            3936   3401     84       12         450         210
# Swap:           2047   1823    224

available이 요점이다 — free는 낮아도 정상이다(안 쓰는 RAM은 낭비이므로 OS가 캐시로 채운다). available이 바닥이고 Swap used가 계속 오르면 작업 집합이 물리 메모리를 넘은 것이다.

copy-on-write를 직접 확인한다

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    size_t n = 256 * 1024 * 1024;          // 256MB
    char *buf = malloc(n);
    for (size_t i = 0; i < n; i += 4096) buf[i] = 1;   // 실제로 건드려 올린다

    if (fork() == 0) {                      // 자식 — 아직 아무것도 안 썼다
        sleep(30);                          // 이때 RSS 를 재 보면 거의 0 이다
        for (size_t i = 0; i < n; i += 4096) buf[i] = 2;  // 쓰는 순간 복사된다
        sleep(30);                          // 이제 RSS 가 256MB 로 뛴다
    }
    sleep(90);
}
ps -o pid,rss,comm -C a.out    # 부모·자식의 RSS 를 비교해 본다

fork 직후 자식의 RSS가 거의 0인데 쓰기를 시작하자 256MB로 뛰는 것이 COW다. 문서로 읽으면 추상적인데, 두 숫자를 직접 보면 원리가 분명해진다.

이 성질 때문에 "메모리를 2GB 쓰는 프로세스fork해도 즉시 4GB가 되지는 않는다." 다만 자식이 전체를 훑기 시작하면 그때 실제로 두 배가 되므로, 메모리가 빠듯할 때 fork는 여전히 위험하다.

OOM Killer가 누구를 골랐나

dmesg -T | grep -i "killed process"
# [Mon Aug 17 03:12:44 2026] Out of memory: Killed process 8123 (myapp)
#   total-vm:6291456kB, anon-rss:4194304kB, file-rss:0kB, oom_score_adj:0

프로세스아무 로그도 안 남기고 사라졌다면 여기를 본다. 애플리케이션 로그에는 흔적이 없다 — SIGKILL이라 잡을 수도, 정리할 수도 없기 때문이다.

cat /proc/1234/oom_score        # 클수록 먼저 죽는다
echo -500 > /proc/1234/oom_score_adj    # 보호하고 싶은 프로세스는 낮춘다

점수는 대체로 메모리를 많이 쓰는 쪽이 높다. 그래서 "가장 중요한 서비스가 가장 먼저 죽는" 일이 흔하다 — 대개 그게 메모리를 제일 많이 쓰기 때문이다.

왜 보편적인가유한한 메모리에서 무엇을 버릴까는 컴퓨팅 전반을 관통한다. 불필요한 데이터 회수, 캐시 축출(LRU/LFU), 큐에 상한 두기, 버퍼 크기 정하기 — 모두 *"공간은 유한하니 관리해야 한다"*는 같은 명제의 변주다. 그리고 *스래싱*은 과부하 보호와 같은 교훈을 준다 — 감당 못 할 양을 욱여넣으면 전부 느려지니, 동시에 처리하는 양을 제한해야 한다.

정리하면, OS는 유한한 물리 RAM을 — 단편화(내부/외부, 페이징이 외부를 해결)를 감수하며 나눠 주고, 가득 차면 페이지 교체(LRU, 지역성)로 희생자를 골라 swap하며, 부족이 심하면 스래싱(작업 집합 초과)으로 무너지는 걸 동시 실행 제한으로 막고, *copy-on-write*로 아끼되 정 안 되면 OOM Killer로 최후의 정리를 한다. 이 모든 *"유한한 공간을 어떻게 쓰나"*는 캐시·큐 같은 모든 자원 관리와 한 형제다. 메모리를 봤으니, 이제 디스크를 파일로 추상화하는 파일 시스템이 다음이다.

가상 메모리 — 페이징·TLB·페이지 폴트파일 시스템 — inode·저널링(WAL)·페이지 캐시