← --library
이과 · 19이과

운영체제 및 시스템 아키텍처

단계1단계2단계3단계4단계5

운영체제 및 시스템 아키텍처 — 1단계 완전 가이드


1부. 이론적 기초 — 컴퓨터라는 기계의 본질적 딜레마

1.1 자원은 하나, 원하는 놈은 여럿

지금 이 순간 너의 노트북 CPU는 몇 가지 일을 동시에 처리하고 있을까? 브라우저, 카카오톡, 유튜브 플레이어, 백그라운드 시스템 프로세스들까지. 그런데 재미있는 사실이 있다. 일반적인 CPU 코어는 한 순간에 딱 하나의 명령어만 실행한다. 그렇다면 이 모든 것이 어떻게 "동시에" 돌아가는 것처럼 느껴지는 걸까? 이 질문의 답이 바로 운영체제(OS)의 핵심 존재 이유다.

1950년대 초기 컴퓨터는 운영체제가 없었다. 프로그래머가 천공카드를 들고 가서 집어넣으면, 그 프로그램이 끝날 때까지 컴퓨터 전체가 오직 그 사람 것이었다. 이를 **배치 처리(Batch Processing)**라고 부르는데, 학교에 화장실이 하나뿐인데 한 명이 나올 때까지 나머지가 복도에서 기다리는 것과 같다. 자원(컴퓨터)은 하나고 쓰고 싶은 프로그램은 여럿일 때, 이 충돌을 조정하는 소프트웨어가 운영체제다. Andrew S. Tanenbaum의 Modern Operating Systems(4판)에 따르면, OS는 두 역할을 동시에 수행한다: **자원 관리자(Resource Manager)**로서 CPU·메모리·I/O를 공정하게 분배하고, **추상화 레이어(Abstraction Layer)**로서 복잡한 하드웨어를 단순한 인터페이스로 제공한다. 파일을 "저장"할 때 실제로는 디스크 특정 섹터에 자기 신호를 기록하는 복잡한 과정이 일어나지만, 프로그래머는 그냥 write() 하나만 호출하면 된다.

[노트 기록] 운영체제의 두 역할: ① 자원 관리자 — CPU/메모리/I/O를 공정하게 분배, ② 추상화 레이어 — 하드웨어의 복잡성을 단순한 인터페이스로 숨김.

1.2 폰 노이만 아키텍처와 그 한계

1945년 John von Neumann이 제안한 **폰 노이만 아키텍처(Von Neumann Architecture)**는 현재 우리가 쓰는 거의 모든 컴퓨터의 기반이다. 핵심은 간단하다: CPU가 메모리에서 명령어를 하나씩 가져와서(Fetch), 해석하고(Decode), 실행한다(Execute). 이를 **FDE 사이클(Instruction Cycle)**이라 부른다. 중요한 건, CPU와 메모리가 같은 버스를 공유한다는 점이다. CPU가 아무리 빠르게 연산해도 메모리에서 데이터를 가져오는 속도에 전체 성능이 제한되는 것을 **폰 노이만 병목(Von Neumann Bottleneck)**이라 한다. 현재 일반 CPU의 클럭 속도는 35GHz(1초에 3050억 사이클)인데, RAM에서 데이터를 읽어오는 데는 약 100나노초가 걸린다. 이 100나노초 동안 CPU는 수백 번의 연산을 그냥 기다리며 낭비한다. 이 병목을 해결하기 위해 발명된 것이 **캐시 메모리(Cache Memory)**인데, 이는 2부에서 본격적으로 다룬다.

1.3 커널 공간과 유저 공간

OS를 이해하려면 반드시 알아야 할 경계가 있다. **커널 모드(Kernel Mode)**에서는 모든 하드웨어 명령어를 실행할 수 있지만, **유저 모드(User Mode)**에서는 제한된 명령어만 실행 가능하다. 왜 이 구분이 필요할까? 만약 일반 사용자 프로그램이 마음대로 하드웨어에 접근할 수 있다면, 악의적인 프로그램이 다른 프로그램의 메모리를 읽거나 디스크를 통째로 날려버릴 수 있다. 유저 프로그램이 OS 기능(파일 읽기, 네트워크 등)을 요청할 때는 **시스템 콜(System Call)**을 통해서만 가능하다. printf() 같은 C 라이브러리 함수도 내부에서 결국 write() 시스템 콜을 호출하며, 이 순간 유저 모드에서 커널 모드로 전환된다. 계층 구조를 머릿속에 새겨두어라: 애플리케이션 → C 라이브러리 → 시스템 콜 → 커널 → 하드웨어.

[노트 기록] 커널 모드 vs 유저 모드: 모드 분리로 보안·안정성 확보. 유저 프로그램은 시스템 콜을 통해서만 커널에 요청 가능.


2부. 본론 — 프로세스·스레드·메모리·I/O의 실체

2.1 프로세스: 살아있는 프로그램의 정체

프로그램(Program)과 프로세스(Process)의 차이를 생각해본 적 있는가? 프로그램은 디스크에 저장된 실행 파일, 즉 그냥 데이터 덩어리다. 반면 프로세스는 그 프로그램이 실행되면서 OS가 메모리를 할당하고 CPU 시간을 배정하여 "생명을 불어넣은" 상태다. 크롬을 두 번 실행하면 메모리 속에 두 개의 독립적인 프로세스가 생긴다.

OS는 각 프로세스를 관리하기 위해 **PCB(Process Control Block, 프로세스 제어 블록)**이라는 자료구조를 유지한다. PCB에는 프로세스 ID(PID), 현재 실행 상태, CPU 레지스터 값, 메모리 할당 정보, 열려있는 파일 목록이 들어있다. OS 입장에서 프로세스란 곧 PCB다. 프로세스가 CPU를 빼앗기면 현재 레지스터 상태를 PCB에 저장하고, 나중에 다시 CPU를 받으면 PCB에서 꺼내 이어서 실행하는 과정을 **컨텍스트 스위칭(Context Switching)**이라 한다. 이 컨텍스트 스위칭이 초당 수백~수천 번 일어나기 때문에, 프로그램들이 "동시에" 실행되는 것처럼 보이는 것이다. 앞서 1부에서 던진 질문의 답이 여기 있다.

프로세스 상태는 생성(New) → 준비(Ready) ↔ 실행(Running) → 종료(Terminated), 그리고 실행 중 I/O를 기다리면 대기(Waiting) 상태로 빠졌다가 I/O가 완료되면 다시 Ready로 돌아온다. CPU 스케줄러는 Ready 상태의 프로세스 중에서 다음 실행할 것을 선택한다.

[노트 기록] 프로세스 상태 다이어그램: New → Ready ↔ Running → Terminated. Running 중 I/O 발생 → Waiting → Ready(I/O 완료 시).

2.2 스레드: 프로세스의 경량화, 그리고 위험

프로세스만으로 충분할 것 같은데, 왜 **스레드(Thread)**라는 개념이 따로 필요할까? 1,000명이 동시에 접속하는 웹 서버를 생각해보자. 요청마다 새 프로세스를 만드는 건 너무 무겁다 — 프로세스 생성에는 메모리 복사, PCB 생성 등 큰 오버헤드가 따른다. 스레드는 한 프로세스 안에서 코드, 데이터, 힙 메모리를 공유하면서도 각자 독립적인 **스택(Stack)**과 레지스터를 가진 실행 흐름이다. 같은 자원을 공유하니 생성이 빠르고, 프로세스 간 통신(IPC)보다 데이터 교환이 훨씬 쉽다.

그런데 바로 이 공유가 가장 큰 위험 요소다. 두 스레드가 동시에 count++를 실행하면 어떤 일이 생길까? Bryant & O'Hallaron의 Computer Systems: A Programmer's Perspective(CS:APP, 3판)에서 명시하듯, count++는 어셈블리 수준에서 세 단계다: ① 메모리에서 count 읽기, ② 값을 1 증가, ③ 결과를 메모리에 쓰기. 스레드 A가 ①을 마친 직후 컨텍스트 스위칭이 발생해서 스레드 B가 ①②③을 모두 끝내고 돌아온다면, 스레드 A는 오래된 값을 가지고 ②③을 실행한다. 두 번 증가시켰는데 결과는 1만 늘어난 셈이다. 이것이 **경쟁 조건(Race Condition)**이다. 무서운 점은 이 버그가 재현하기도 힘들고 — 실행할 때마다 결과가 달라지는 비결정론적(Non-deterministic) 동작이기 때문이다 — 찾기도 지독히 어렵다.

[노트 기록] 스레드 vs 프로세스: 스레드 = 코드/데이터/힙 공유 + 스택/레지스터 독립. 경쟁 조건: 공유 자원에 대한 비원자적(Non-atomic) 접근에서 발생. count++는 원자적 연산이 아니다!

2.3 동기화: 혼돈을 질서로

경쟁 조건을 막으려면 **상호 배제(Mutual Exclusion)**가 필요하다. 한 스레드가 공유 자원에 접근하는 코드 구간 — 이를 임계 구역(Critical Section) — 에 있을 때, 다른 스레드는 그 구간에 진입하지 못하도록 막아야 한다. 이를 구현하는 기본 도구가 **뮤텍스(Mutex, MUTual EXclusion Lock)**다. 들어갈 때 잠그고(lock), 나올 때 열어주면(unlock) 다음 스레드가 들어갈 수 있다. 화장실 열쇠가 하나뿐인 것과 같다.

pthread_mutex_lock(&mutex);
count++;            // 임계 구역
pthread_mutex_unlock(&mutex);

1965년 Edsger Dijkstra가 제안한 **세마포어(Semaphore)**는 뮤텍스를 일반화한 개념이다. 정수 카운터를 가지며, wait()(P 연산)으로 카운터를 1 감소시키고 0 이하가 되면 대기하며, signal()(V 연산)으로 1 증가시킨다. 카운터가 1이면 뮤텍스와 동일하고, N이면 N개의 스레드가 동시에 접근 가능하다. 동시 접속자를 100명으로 제한하려면 카운터 100짜리 세마포어를 쓰면 된다. 여기서 스스로 생각해볼 것: 뮤텍스는 lock을 건 스레드만 unlock할 수 있지만 세마포어는 아무 스레드나 signal을 호출할 수 있다. 이 차이가 어떤 상황에서 유용한지, 어떤 상황에서 위험한지 생산자-소비자 문제를 떠올리며 추론해봐라.

**생산자-소비자 문제(Producer-Consumer Problem)**는 동기화의 교과서적 예제다. 생산자 스레드는 데이터를 만들어 버퍼에 넣고, 소비자는 꺼내 처리한다. 버퍼가 꽉 차면 생산자가 기다리고, 비면 소비자가 기다린다. 이를 세마포어 3개(empty: 빈 슬롯 수, full: 찬 슬롯 수, mutex: 버퍼 보호)로 해결하는 패턴은 현실의 메시지 큐, 스트리밍 파이프라인, 웹 서버 어디서나 나타난다.

[노트 기록] 동기화 도구: ① 뮤텍스 — 1개 허용, lock 건 스레드만 unlock 가능 ② 세마포어 — N개 허용, 누구나 signal 가능 ③ 모니터 — 뮤텍스 + 조건 변수, 고수준 추상화 (Java synchronized 블록의 근간).

2.4 데드락: 영원히 끝나지 않는 기다림

더 무서운 이야기를 해야 한다. **데드락(Deadlock, 교착 상태)**은 두 개 이상의 스레드가 서로가 가진 자원을 기다리며 영원히 진행하지 못하는 상태다. 4차선 교차로에서 사방의 차가 동시에 들어와 꼼짝달싹 못하는 장면을 상상해봐라. 1971년 Coffman et al.이 발표한 논문("System deadlocks", ACM Computing Surveys)에서는 데드락 발생에 4가지 조건이 동시에 성립해야 함을 증명했다.

① 상호 배제(Mutual Exclusion): 자원은 한 번에 하나의 스레드만 사용 가능. ② 점유 및 대기(Hold and Wait): 자원을 하나 이상 점유한 채로 다른 자원을 기다림. ③ 비선점(Non-preemption): 이미 할당된 자원을 강제로 빼앗을 수 없음. ④ 순환 대기(Circular Wait): 스레드 A가 B의 자원을, B가 A의 자원을 기다리는 사이클이 형성됨. 이 4가지 중 하나라도 깨면 데드락이 발생하지 않는다. 실무에서 가장 흔한 예방법은 자원 획득 순서를 일관되게 강제하여 순환 대기를 차단하는 것이다(예: 항상 번호가 낮은 락부터 획득).

[노트 기록] Coffman 조건 4가지: 상호 배제 + 점유 및 대기 + 비선점 + 순환 대기. 4가지 중 하나만 깨도 데드락 불가. 가장 실용적인 방법 = 자원 획득 순서 고정(순환 대기 차단).

2.5 메모리 계층 구조: 속도·용량·비용의 삼각 딜레마

앞서 언급한 폰 노이만 병목으로 돌아오자. CPU는 빠르고 메모리는 느리다. 이 gap을 메우기 위해 아키텍트들은 **메모리 계층 구조(Memory Hierarchy)**를 도입했다. Patterson & Hennessy의 Computer Organization and Design에 따르면, 이 계층 구조는 단순한 원칙에 기반한다: 자주 쓰는 데이터는 빠르고 비싼 메모리에, 덜 쓰는 데이터는 느리고 싼 저장장치에 두어라.

계층을 위에서 아래로 나열하면: 레지스터L1 캐시L2 캐시L3 캐시메인 메모리(RAM)SSDHDD 순으로 속도는 느려지고, 용량은 커지고, 비용은 낮아진다. 숫자로 보면 충격적이다: 레지스터 접근 < 1ns, L1 캐시 약 1.5ns, L2 약 5ns, L3 약 20ns, RAM 약 100ns, SSD 수십~수백 μs, HDD 수 ms. CPU → RAM이 약 300배 차이라면, CPU → HDD는 약 10만 배다. 감각적으로 이해하자면: CPU가 1초에 하나씩 연산한다고 치면 RAM 접근은 5분 기다리는 것이고, HDD 접근은 나흘을 기다리는 것이다.

[노트 기록] 메모리 계층 속도: 레지스터(<1ns) → L1(~1.5ns) → L2(~5ns) → L3(~20ns) → RAM(~100ns) → SSD(~100μs) → HDD(~10ms). 빠를수록 비싸고 용량 적음. 트레이드오프 삼각형: 속도 ↔ 용량 ↔ 비용.

2.6 지역성 원리와 캐시 최적화

캐시가 효과적으로 작동하려면 한 가지 가정이 필요하다: 방금 접근한 데이터 근처를 곧 다시 쓸 것이라는 가정. 이를 **지역성 원리(Principle of Locality)**라 부르며, 두 종류가 있다. **시간적 지역성(Temporal Locality)**은 "최근에 접근한 데이터는 곧 다시 접근할 가능성이 높다"는 원리다 — 루프 변수 i가 루프가 도는 내내 반복 사용되는 것을 생각하면 된다. **공간적 지역성(Spatial Locality)**은 "방금 접근한 주소 근처에 곧 접근할 가능성이 높다"는 원리다 — 배열 원소를 순서대로 접근하는 것이 대표적이다.

실제로 캐시는 데이터를 캐시 라인(Cache Line, 일반적으로 64바이트) 단위로 가져온다. arr[0] 하나를 읽어도 그 주변 64바이트가 통째로 캐시에 올라온다. 이 원리가 실제 성능에 어떤 영향을 주는지 가장 잘 보여주는 예제가 2D 배열 접근 패턴이다. C에서 int a[N][N]은 메모리에 행 우선(Row-major order)으로 저장된다 — a[0][0], a[0][1], ..., a[0][N-1], a[1][0], ... 순서로. 그렇다면 a[i][j] 순서(행 우선 접근)와 a[j][i] 순서(열 우선 접근) 중 어느 쪽이 캐시 미스를 더 많이 일으킬까? 정답을 말해주기 전에 스스로 추론해봐라. 힌트: 열 우선으로 접근하면 매번 N×4바이트씩 건너뛰게 되고, N=4096이면 16KB를 건너뛴다. 이 거리가 캐시 라인 크기(64B)보다 훨씬 크면 어떻게 될까? 실제 벤치마크에서 N이 커질수록 열 우선 접근이 행 우선 접근보다 10배 이상 느린 경우도 있다. 이것이 **캐시 친화적 코딩(Cache-friendly Coding)**이 중요한 이유다 — 알고리즘의 시간복잡도가 O(N²)으로 같아도, 메모리 접근 패턴에 따라 실제 실행 시간이 수배 차이 난다.

[노트 기록] 지역성 원리: ① 시간적 — 최근 접근 데이터 재사용 가능성 높음(루프 변수) ② 공간적 — 인접 주소 곧 접근 가능성 높음(배열 순차). 캐시 라인 = 64바이트 단위 적재. 캐시 미스(Cache Miss) = 캐시에 없어서 하위 계층(RAM 등)까지 내려가야 하는 상황.

2.7 파일 시스템과 inode 구조

메모리는 전원이 꺼지면 사라진다. 데이터를 영구 저장하려면 **파일 시스템(File System)**이 필요하다. Unix 계열 파일 시스템에서 핵심 개념은 **inode(Index Node)**다. inode는 파일의 실제 데이터가 저장된 디스크 블록의 위치 정보와 파일 크기, 권한, 소유자, 수정 시간 등의 메타데이터를 담은 자료구조다. 중요한 점: 파일 이름은 inode에 없다. 디렉토리는 단순히 "파일 이름 → inode 번호"의 매핑 테이블이다. 파일 이름은 inode를 가리키는 포인터일 뿐이다. 이 덕분에 **하드 링크(Hard Link)**가 가능하다 — 다른 이름이 같은 inode를 가리키면 그 두 이름은 완전히 동일한 파일이다. 반면 파일을 지운다는 것은 inode 번호를 지우는 것일 뿐이고, inode를 가리키는 이름(링크)이 하나도 없을 때 비로소 실제 데이터가 삭제된다.

[노트 기록] inode 구조: 메타데이터(크기/권한/시간) + 데이터 블록 포인터 보유. 파일 이름 없음. 디렉토리 = "이름→inode번호" 테이블. 하드 링크 = 동일 inode를 가리키는 다른 이름.

2.8 I/O 스케줄링: 병목 해소

파일 시스템을 이해했으니, "어떻게 디스크에서 빠르게 읽고 쓸 것인가"로 넘어간다. HDD는 기계적 장치로, 데이터를 읽으려면 물리적으로 헤드를 원하는 트랙으로 이동시켜야 한다(이 시간을 **탐색 시간(Seek Time)**이라 한다). 따라서 여러 I/O 요청이 들어왔을 때 헤드 이동을 최소화하는 순서로 처리하면 성능이 크게 향상된다.

**FCFS(First Come, First Served)**는 단순하지만 헤드 이동이 많다. **SSTF(Shortest Seek Time First)**는 현재 헤드에서 가장 가까운 요청을 먼저 처리해서 평균 탐색 시간을 줄이지만, 멀리 있는 요청이 오래 기다리는 기아(Starvation) 문제가 생긴다. **SCAN 알고리즘(엘리베이터 알고리즘)**은 헤드가 한 방향으로 이동하면서 모든 요청을 처리하다가 끝에 도달하면 방향을 바꾼다 — 마치 엘리베이터처럼. 공정성과 탐색 시간 모두 준수하다. 현대 SSD는 기계적 헤드가 없으므로 탐색 시간이 거의 없다. 그래서 SSD용 Linux 스케줄러(none, mq-deadline)는 SCAN 같은 헤드 최적화가 아니라 요청 병합과 큐 깊이 관리에 집중한다.

I/O 병목을 근본적으로 줄이는 또 다른 방법은 디스크 접근 자체를 줄이는 것이다. OS가 최근에 읽은 파일 블록을 메모리에 유지하는 **페이지 캐시(Page Cache)**가 이 역할을 한다. free -m을 실행할 때 보이는 "buff/cache" 항목이 바로 이것이다. 이 개념은 앞서 배운 메모리 계층 구조의 논리를 파일 시스템에 그대로 적용한 것이다 — 1부에서 설명한 "자주 쓰는 데이터는 빠른 곳에" 원칙의 연장선이다.

[노트 기록] I/O 스케줄링 비교: FCFS(순서대로, 단순), SSTF(가장 가까운 것 먼저, 기아 위험), SCAN(엘리베이터, 공정). 페이지 캐시: OS가 파일 블록을 RAM에 자동 캐시 → 반복 접근 시 디스크 I/O 생략.


3부. 프로젝트 — 직접 설계하고 구현하라

이제 배운 개념들을 직접 손에 익힐 시간이다. 아래 프로젝트들은 1단계 학습 목표 ① 동시성 기초, ② 메모리 접근 패턴 성능 차이, ③ I/O 병목 해소를 몸으로 경험하도록 설계되었다. 각 문제의 정답은 제공하지 않는다. 스스로 설계·구현·관찰하며 이해를 완성해라. 전체 풀이 목표 시간은 약 40분이다.


프로젝트 1: 경쟁 조건 재현 및 수정 (약 10분)

아래 C 코드를 컴파일하고 실행하라. (컴파일: gcc -o race race.c -lpthread)

#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS    10
#define NUM_INCREMENTS 100000

long long counter = 0;

void* increment(void* arg) {
    for (int i = 0; i < NUM_INCREMENTS; i++)
        counter++;
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; i++)
        pthread_create(&threads[i], NULL, increment, NULL);
    for (int i = 0; i < NUM_THREADS; i++)
        pthread_join(threads[i], NULL);
    printf("Expected: %lld\n", (long long)NUM_THREADS * NUM_INCREMENTS);
    printf("Got:      %lld\n", counter);
    return 0;
}

문제 1-1. 이 코드를 5번 실행하라. 결과가 매번 다른가? 2.2절에서 배운 count++의 어셈블리 3단계를 참고하여, 정확히 어느 타이밍에 어떤 일이 벌어지는지 타임라인 그림을 그려봐라. (두 스레드의 실행 순서를 열로 나란히 그리고, 컨텍스트 스위칭이 언제 끼어드는지 화살표로 표시하라.)

문제 1-2. pthread_mutex_t를 사용해서 경쟁 조건을 수정하라. 수정 후 ExpectedGot이 항상 일치하는가?

문제 1-3. time ./race_originaltime ./race_fixed를 실행하여 뮤텍스 유/무 버전의 실행 시간을 비교하라. 뮤텍스로 인한 오버헤드는 어디서 발생하는가? (힌트: 2.4절 컨텍스트 스위칭 비용, 캐시 라인 무효화(Cache Line Invalidation)를 떠올려라.)

문제 1-4 (심화). __atomic_fetch_add(&counter, 1, __ATOMIC_SEQ_CST) 원자적 연산을 사용한 세 번째 버전을 구현하고, 뮤텍스 버전과 성능을 비교하라. 왜 차이가 나는지 CPU의 LOCK 명령어 수준에서 설명하라.


프로젝트 2: 캐시 성능 측정 — 행 우선 vs 열 우선 (약 10분)

2.6절의 공간적 지역성을 실험으로 확인한다.

#include <stdio.h>
#include <time.h>

#define N 4096
int a[N][N];

void row_major() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            a[i][j]++;
}

void col_major() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            a[j][i]++;
}

문제 2-1. clock_gettime(CLOCK_MONOTONIC, ...)으로 두 함수의 실행 시간을 측정하라. 결과는 어떤가?

문제 2-2. N을 256 → 512 → 1024 → 2048 → 4096으로 바꿔가며 실행 시간 비율(col_major / row_major)을 측정하라. N이 커질수록 비율이 어떻게 변하는가? 이 변화를 캐시 크기(L1: 32KB, L2: 256KB, L3: 수 MB)와 연결지어 설명하라. N이 얼마일 때부터 비율이 급격히 커지는가? 왜 그 지점에서 커지는지 추론하라.

문제 2-3 (심화). 행렬 곱셈 C = A × B를 ① 일반적인 ijk 순서, ② 캐시 친화적인 ikj 순서로 구현하고 실행 시간을 비교하라. ikj 순서가 왜 더 빠른지 배열 메모리 레이아웃을 그려가며 설명하라. (힌트: ijk 순서에서 행렬 B의 접근 패턴을 추적해봐라.)


프로젝트 3: I/O 스케줄러 시뮬레이터 (약 10분)

2.8절의 I/O 스케줄링 알고리즘 3가지를 Python으로 구현하고 성능을 비교한다.

설정: 총 200트랙의 디스크(트랙 번호 0~199). 초기 헤드 위치 50. 아래 요청 큐를 처리하라:

요청 큐: [98, 183, 37, 122, 14, 124, 65, 67]

문제 3-1. FCFS 알고리즘을 구현하고, 처리 순서와 전체 헤드 이동 거리를 계산하라.

문제 3-2. SSTF 알고리즘을 구현하고, 전체 헤드 이동 거리를 구하라. FCFS 대비 얼마나 개선되었는가? SSTF가 위험한 시나리오를 인위적으로 만들어봐라 — 어떤 요청 분포에서 기아(Starvation)가 발생하는가?

문제 3-3. SCAN 알고리즘을 구현하라. 헤드는 트랙 50에서 증가 방향(높은 번호 방향)으로 출발한다고 가정한다. 처리 순서와 전체 헤드 이동 거리를 구하라.

문제 3-4 (심화). 랜덤 1,000개 요청을 생성하고 세 알고리즘의 평균 응답 시간과 **최악의 경우 대기 시간(worst-case latency)**을 100회 시뮬레이션 평균으로 비교하라. 어떤 알고리즘이 어떤 metric에서 유리한가? 실시간 시스템(응급실 예약 시스템처럼 최악 지연을 최소화해야 하는 경우)에는 어떤 알고리즘을 선택할 것인가?


프로젝트 4: 멀티스레드 웹 서버 설계 문서 (약 10분)

이 프로젝트는 코드 구현보다 설계 문서 작성에 초점을 맞춘다.

시나리오: 동시에 최대 1,000명이 접속하는 HTTP 웹 서버. 요청당 평균 처리 시간 50ms. CPU 코어 8개.

문제 4-1. 스레드 풀(Thread Pool) 아키텍처를 다이어그램으로 설계하라. 다음 요소가 모두 포함되어야 한다: ① 클라이언트 요청을 받는 메인 스레드, ② 공유 작업 큐(Task Queue), ③ 워커 스레드들, ④ 각 컴포넌트 사이에서 필요한 동기화 메커니즘(뮤텍스/세마포어). 각 동기화 도구를 선택한 이유를 명시하라 — "생산자-소비자 문제"의 구조와 어떻게 대응되는지 연결지어 설명하라.

문제 4-2. 스레드 수를 몇 개로 설정하는 게 이론적 최적인가? Little's Law(L = λW: 시스템 내 평균 항목 수 = 도착률 × 체류 시간)를 이용해 계산하라. 1,000명이 동시 접속하고 요청당 50ms 처리 시간이면, 처리 처리량(throughput)을 맞추려면 최소 몇 개의 스레드가 필요한가? CPU 바운드(연산 집약) 작업과 I/O 바운드(파일/네트워크 대기) 작업에서 최적 스레드 수가 다른 이유는 무엇인가?

문제 4-3. 아래 두 시나리오에서 발생 가능한 동시성 버그를 분석하고 방지 방법을 제안하라. ① 전체 활성 접속자 수를 추적하는 전역 카운터 변수, ② 100명의 사용자가 동시에 같은 파일에 append 쓰기를 요청하는 상황.

문제 4-4 (심화). 스레드 풀 기반 서버와 비동기 I/O + 이벤트 루프(Event Loop) 기반 서버(Node.js, Python asyncio 방식)의 트레이드오프를 분석하라. CPU 바운드 작업에서는 어느 쪽이 유리한가? I/O 바운드 작업에서는? 스레드 풀 서버에서 하나의 스레드가 무한루프에 빠지면 어떤 일이 생기는가? 이벤트 루프 기반 서버에서 하나의 콜백이 무한루프에 빠지면? 이 비교를 통해 각 아키텍처의 결함 격리(Fault Isolation) 특성을 논하라.


평가 기준

아래 세 가지 기준을 통해 스스로 채점해보라.

① 동시성 버그 테스트 (40점): 프로젝트 1·4에서 경쟁 조건과 데드락을 정확히 식별하고 수정했는가? 뮤텍스·세마포어의 선택 이유를 논리적으로 설명할 수 있는가? 단순히 "잠그면 된다"가 아니라, 어떤 임계 구역을 어떤 도구로 보호해야 하는지 스스로 결정할 수 있는가?

② 처리량 벤치마크 (40점): 프로젝트 2·3에서 측정 결과가 이론(지역성 원리, 캐시 크기, 알고리즘 특성)과 일치하는가? 숫자 하나를 보여주는 데 그치지 않고, 왜 그 숫자가 나왔는지 메커니즘을 설명할 수 있는가? N 변화에 따른 비율 변화처럼 **경향성(Trend)**을 읽어낼 수 있는가?

③ 설계 문서 (20점): 프로젝트 4의 설계 문서가 아키텍처 결정의 이유를 논리적으로 서술하는가? "이렇게 했다"가 아니라 "이렇게 한 이유는 ~이기 때문이다, 단 ~한 트레이드오프가 있다"는 형태로 작성되어 있는가? 다이어그램이 코드 없이 구조를 충분히 전달하는가?


참고 문헌

  • A. S. Tanenbaum, Modern Operating Systems, 4th ed., Pearson, 2015.
  • A. Silberschatz et al., Operating System Concepts, 10th ed., Wiley, 2018. ("공룡책")
  • R. E. Bryant & D. R. O'Hallaron, Computer Systems: A Programmer's Perspective, 3rd ed., Pearson, 2016. (CS:APP)
  • J. L. Hennessy & D. A. Patterson, Computer Organization and Design, 5th ed., Morgan Kaufmann, 2014.
  • E. G. Coffman et al., "System deadlocks," ACM Computing Surveys, vol. 3, no. 2, 1971.
단계 2