two's complement 2의 보수
음수를 표현하는 방법. 비트를 반전(1의 보수)한 뒤 1을 더하면 음수 표현을 얻는다. 덧셈·뺄셈 회로를 동일하게 쓸 수 있어 하드웨어 구현이 단순하다. overflow는 두 양수의 합이 음수가 되거나 두 음수의 합이 양수가 될 때 발생한다.
Computer Architecture · Introduction · Architectural Support · Processes · Threads · Threads Implementation
이 장은 운영체제를 이해하기 위한 컴퓨터 구조 기초를 복습한다. 프로그램은 high-level language → assembly language → machine code의 계층으로 변환되며, 컴퓨터는 processor, memory, I/O 컨트롤러로 구성된다. 정수 산술(two's complement)과 IEEE 754 부동소수점 표현, 그리고 MIPS 기반 프로세서 구조(레지스터, 명령어 형식, 파이프라인)를 다룬다.
메모리 계층에서는 principle of locality를 기반으로 cache와 virtual memory가 성능과 용량을 동시에 만족시키는 원리를 살펴본다. 저장장치 측면에서는 HDD의 디스크 접근 지연 구조와 SSD/flash의 특성, 그리고 bus를 통한 I/O 연결 구조를 정리한다.
음수를 표현하는 방법. 비트를 반전(1의 보수)한 뒤 1을 더하면 음수 표현을 얻는다. 덧셈·뺄셈 회로를 동일하게 쓸 수 있어 하드웨어 구현이 단순하다. overflow는 두 양수의 합이 음수가 되거나 두 음수의 합이 양수가 될 때 발생한다.
단정도(32-bit)와 배정도(64-bit) 부동소수점을 규정한 표준.
형식은 (-1)S × (1 + Fraction) × 2Exponent − Bias
이며, 단정도 범위는 약 ±1.2×10−38 ~ ±3.4×10+38이다.
hidden bit 덕분에 유효숫자를 1비트 더 쓸 수 있다.
여러 명령어의 실행 단계를 겹쳐 처리해 처리량(throughput)을 높이는 기법. 이상적으로 단계 수만큼 속도가 향상되지만 hazard(구조적·데이터·제어)로 인해 성능이 제한된다.
data hazard를 줄이기 위해 레지스터 파일에 쓰기 전 연산 결과를 다음 단계로 직접 전달하는 기법. 그러나 load-use hazard처럼 값이 아직 계산되지 않은 경우에는 stall(버블)이 불가피하다.
프로그램은 특정 시간에 주소 공간의 일부만 집중적으로 사용한다. temporal locality(최근 접근 항목을 다시 접근)와 spatial locality(인접 항목도 곧 접근)를 활용해 cache와 virtual memory가 설계된다.
각 메모리 블록이 캐시의 단 하나의 위치에만 매핑되는 구조. 주소를 tag / index / block offset으로 분할해 빠르게 검색하며, valid bit로 유효 여부를 표시한다.
page table entry를 캐시하는 작은 하드웨어 버퍼. virtual address를 physical address로 변환할 때 매번 메모리를 참조하는 오버헤드를 제거한다. 일반적으로 16–512개 항목, 히트 시 0.5–1 사이클 이내에 변환이 완료된다.
비휘발성 반도체 저장장치로 HDD보다 100–1000배 빠르며 충격에 강하다. NAND flash는 블록 단위 소거가 필요하고 반복 쓰기로 셀이 마모되므로 wear leveling이 필수적이다.
컴퓨터 시스템은 소프트웨어 계층(application → system software → hardware)으로 구성된다. compiler는 고수준 언어를 기계어로 변환하며, OS는 I/O 처리·메모리 관리·자원 스케줄링 서비스를 제공한다. 모든 컴퓨터(데스크탑·서버·임베디드)는 공통 구성 요소―processor, memory, I/O 장치―를 공유한다.
정수 덧셈·뺄셈은 two's complement를 사용하며, 같은 부호 두 수를 더했을 때 부호가 바뀌면 overflow다. 뺄셈은 피감수의 2의 보수를 더하는 방식으로 구현한다.
IEEE 754 부동소수점: 비트 구조는
S | Exponent | Fraction이며, 실제 값은
(-1)S × (1 + Fraction) × 2Exponent − Bias.
단정도(32-bit)의 경우 Bias = 127, 지수 필드 00000000과 11111111은 예약.
표현 범위: 단정도 ≈ ±1.2×10−38 ~ ±3.4×10+38,
배정도 ≈ ±2.2×10−308 ~ ±1.8×10+308.
instruction set은 컴퓨터가 실행할 수 있는 명령어 목록이다. MIPS에서 산술 연산은 register 3개를 피연산자로 사용하며(소스 2개, 목적지 1개), 메모리 접근은 load word (lw) / store word (sw)로만 수행한다. 레지스터는 메모리보다 훨씬 빠르므로 컴파일러는 가능한 한 레지스터를 활용한다.
R-format 명령어: op(6) | rs(5) | rt(5) | rd(5) | shamt(5) | funct(6).
pipelining은 명령어 실행 단계를 겹쳐 처리량을 높인다. 이상적 속도 향상 = 단계 수. 그러나 세 종류의 hazard가 성능을 제한한다.
| 종류 | 원인 | 해결책 |
|---|---|---|
| structural hazard | 동일 자원 충돌 (단일 메모리) | 명령/데이터 메모리 분리 |
| data hazard | 이전 명령의 결과에 의존 | forwarding, 코드 재배치 |
| control hazard | 분기 결과가 결정되기 전에 명령을 fetch | branch prediction, stall |
load-use hazard는 forwarding으로도 해결 불가능해 반드시 1 사이클 stall이 발생한다. 코드 재배치(code scheduling)로 stall 횟수를 줄일 수 있다.
branch prediction: 정적 예측은 루프 backward branch는 taken, forward branch는 not-taken으로 예측한다. 동적 예측은 하드웨어가 실제 분기 이력을 기록해 예측한다.
메모리 계층: 레지스터(빠름·비쌈) → SRAM cache → DRAM main memory → 디스크(느림·저렴). temporal/spatial locality를 이용해 자주 쓰는 데이터를 상위 계층에 유지한다.
direct-mapped cache: 블록 주소 mod (캐시 블록 수)로 위치 결정. 주소는 tag | index | block offset으로 분할된다. valid bit로 유효 여부, tag로 어느 블록인지 확인한다.
virtual memory는 주 메모리를 디스크의 캐시처럼 사용해 각 프로세스에 독립적인 가상 주소 공간을 제공한다. 가상 주소 → 물리 주소 변환은 page table이 담당하며, 테이블에 없으면 page fault가 발생해 OS가 디스크에서 페이지를 읽어온다.
TLB는 최근 변환 결과를 캐시해 매번 page table을 참조하는 오버헤드를 없앤다. TLB miss 시 하드웨어 또는 소프트웨어(OS)가 처리한다. page table entry (PTE)에는 물리 페이지 번호, 참조 비트, 더티 비트 등이 포함된다.
HDD: 비휘발성 회전 자기 저장장치. 섹터 접근 순서는 Queuing delay → seek(헤드 이동) → rotational latency → data transfer → controller overhead. 디스크 컨트롤러는 논리 섹터 인터페이스(SCSI, ATA, SATA)를 제공한다.
flash / SSD: 비휘발성 반도체 저장장치. NOR flash는 랜덤 읽기/쓰기 가능, 임베디드 명령어 메모리에 사용. NAND flash는 고집적·저가이며 블록 단위 소거. 셀 수명 한계로 wear leveling이 필수.
프로세서·메모리·I/O 장치를 연결하는 bus는 공유 통신 채널이지만 물리적 한계(선 길이, 연결 수)로 병목이 된다. 최근에는 고속 직렬 연결(PCIe 등)이 대세다.
Q1 pipelining이 성능을 향상시키는 원리를 설명하고, 세 가지 hazard 유형(구조적·데이터·제어)을 각각 정의하며 해결 방법을 서술하시오.
파이프라이닝의 원리: 명령어 실행을 IF·ID·EX·MEM·WB 등 단계로 나눠 여러 명령어가 서로 다른 단계를 동시에 실행되도록 겹쳐 처리한다. 이상적으로 n단계 파이프라인은 단일 사이클 대비 n배의 처리량 향상을 가져온다.
① structural hazard: 같은 자원을 동시에 사용하려는 충돌. 예: 명령어 fetch와 data 접근이 단일 메모리를 경합. 해결: 명령어 메모리와 데이터 메모리를 분리(또는 분리된 캐시 사용).
② data hazard: 이전 명령의 결과를 다음 명령이 필요로 하는 의존성. forwarding으로 연산 결과를 레지스터 파일에 쓰기 전에 다음 단계에 전달해 해소한다. 단, load-use hazard는 값이 MEM 단계 이후에야 나오므로 forwarding만으로 해결 불가 → 1 사이클 stall 필요. 코드 재배치(code scheduling)로 stall 횟수를 줄일 수 있다.
③ control hazard: 분기 결과가 결정되기 전에 다음 명령을 fetch해야 하는 문제. branch prediction(정적/동적)을 통해 예측이 맞으면 지연 없이 실행, 틀리면 잘못 fetch한 명령을 flush하고 올바른 명령을 fetch한다.
Q2 virtual memory의 개념과 page fault 처리 과정을 단계별로 설명하고, TLB가 주소 변환 성능을 어떻게 개선하는지 서술하시오.
virtual memory: 주 메모리를 디스크 저장소의 캐시처럼 활용해 각 프로세스에 독립적인 가상 주소 공간을 제공하는 메커니즘. CPU·OS가 협력해 가상 주소를 물리 주소로 변환(address translation)한다. page 단위(예: 4 KB)로 관리하며, 메모리에 없는 페이지 참조 시 page fault가 발생한다.
page fault 처리 단계: ① CPU가 가상 주소 참조 → PTE의 valid bit가 0이면 trap 발생. ② OS가 접근의 유효성을 검사(프로세스 주소 공간 내인지). ③ 빈 프레임을 확보(없으면 교체 알고리즘 수행). ④ 디스크(swap space)에서 해당 페이지를 프레임으로 읽어온다. ⑤ page table 갱신, valid bit 설정. ⑥ page fault를 유발한 명령어를 재실행(restart).
TLB: page table의 최근 변환 결과를 저장하는 하드웨어 캐시. TLB hit 시 0.5–1 사이클 만에 물리 주소를 얻어 메모리 참조 횟수를 절반으로 줄인다. miss 시 page table에서 PTE를 읽어 TLB를 갱신한 뒤 재시도한다.
Q3 IEEE 754 단정도 표현 형식을 설명하고, −0.75를 단정도 부동소수점으로 표현하는 과정을 단계별로 서술하시오.
IEEE 754 단정도(32-bit) 형식:
1비트 부호(S) | 8비트 지수(Exponent, Bias=127) | 23비트 가수(Fraction).
실제 값: (-1)S × (1 + Fraction) × 2Exponent − 127.
hidden bit로 정수부 1을 암묵적으로 포함한다.
지수 필드 00000000과 11111111은 특수값을 위해 예약되어 있다.
−0.75 표현 과정:
① 부호 결정: 음수이므로 S = 1.
② 이진 변환: 0.75 = 0.11₂.
③ 정규화: 0.11₂ = 1.1₂ × 2−1 → 지수 = −1, Fraction = 1000…00₂(23비트).
④ 편향 지수: −1 + 127 = 126 = 01111110₂.
⑤ 최종 비트 패턴: 1 01111110 10000000000000000000000.
operating system (OS)은 응용 프로그램의 실행 환경을 제공하는 동시에(extended machine / virtual machine 관점), CPU·메모리·I/O 장치 등 컴퓨터 자원을 관리하고 공유·보호·효율을 보장하는 소프트웨어(resource manager 관점)다. 구현 측면에서는 interrupt와 exception(system call)에 의해 구동되는 고도의 병렬·이벤트 구동 소프트웨어다.
컴퓨터 세대는 1세대(진공관) → 2세대(트랜지스터, 배치 시스템) → 3세대(IC, 다중 프로그래밍·시분할) → 4세대(VLSI, GUI·네트워크)로 발전했으며, Moore's law가 이 발전을 견인했다. OS 역사에서 Multics는 혁신적 아이디어를 제시했고, Unix는 그 단순화 버전으로 현대 OS의 근간이 됐다.
OS가 하드웨어 세부 사항을 추상화해 응용 프로그램에 편리한 인터페이스를 제공하는 역할. process, virtual memory, file 등의 추상화가 이에 해당한다.
CPU·메모리·I/O 장치·에너지 등 컴퓨터 자원을 여러 프로그램이 공유하도록 공정하고 효율적으로 관리하는 OS의 역할. sharing, protection, fairness, efficiency를 보장한다.
여러 작업을 메모리에 적재해 한 작업이 I/O 대기 중일 때 다른 작업이 CPU를 사용하도록 해 CPU 활용률을 높이는 기법. 3세대 OS에서 도입됐으며 job scheduling, memory management, CPU scheduling이 필요해졌다.
CPU 시간을 짧은 time slice로 나눠 여러 사용자가 동시에 대화식으로 컴퓨터를 사용할 수 있게 하는 기법. 응답 시간(response time)을 개선하며, swapping, virtual memory, file system 등을 필요로 한다.
Gordon Moore(Intel 공동창업자)가 1965년 관찰한 법칙: 칩의 트랜지스터 수가 매년(이후 약 2년마다) 2배씩 증가한다. 컴퓨터 혁명을 이끈 핵심 동인이며 스마트폰·AI 연산 등 새로운 응용을 가능하게 했다.
1965년 MIT·Bell Labs·GE가 공동 개발한 시분할 다중 프로세서 OS. 계층적 파일 시스템, ACL, 심볼릭 링크, dynamic linking, 고수준 언어(PL/1) OS 구현, virtual memory(segmentation+paging) 등 수많은 혁신 아이디어를 도입했으나 복잡성과 비용이 너무 컸다.
1969년 Bell Labs의 Ken Thompson이 Multics 탈퇴 후 개발한 단순하고 저비용의 OS. 계층적 파일 시스템, fork()/exec()/pipe, shell, signal을 갖추며 bottom-up 방식·2 Man-Years으로 완성됐다. 현대 OS(Linux, macOS 등)의 근간이다.
OS를 바라보는 세 가지 관점:
| 세대 | 연도 | 핵심 기술 | OS 특징 |
|---|---|---|---|
| 1세대 | 1945–55 | vacuum tubes, plugboards | OS 없음, 프로그래밍 언어 없음 |
| 2세대 | 1955–65 | transistors, mainframes | batch system (1작업씩), I/O 병목으로 CPU 낭비 |
| 3세대 | 1965–80 | integrated circuits (ICs) | multiprogramming, time-sharing, spooling, IBM System/360 |
| 4세대 | 1980– | LSI / VLSI, 마이크로프로세서 | 개인용 컴퓨터, GUI, 인터넷, 분산 시스템 |
Gordon Moore(Intel 공동창업자, 1929–)가 1965년 관찰: 칩 위 트랜지스터 수가 매년 2배 증가 (이후 수정: ~2년마다). 이 법칙이 컴퓨터 혁명을 이끌어 자동차 내장 컴퓨터, 스마트폰, 인간 유전체 프로젝트, 인터넷, 검색 엔진, AI 연산 등을 가능하게 했다.
Multics(1965, MIT·Bell Labs·GE)의 주요 혁신:
단점: top-down 설계, 200 Man-Years, 너무 복잡하고 비용이 많이 드는 하드웨어 필요.
Unix(1969, Bell Labs): Bell Labs가 Multics 탈퇴 후 Ken Thompson이 DEC PDP-7에서 단 2 Man-Years로 개발. 주요 특징:
| 항목 | Multics | Unix |
|---|---|---|
| 설계 방식 | top-down | bottom-up |
| 개발 규모 | ~200 Man-Years | ~2 Man-Years |
| 복잡성 | 매우 복잡, 고비용 하드웨어 | 단순, 저가 하드웨어 |
| 보급 | 제한적 | 대학 중심으로 확산 |
| 영향 | 많은 혁신적 아이디어 제시 | 현대 OS(Linux 등)의 뿌리 |
Windows: Microsoft의 GUI 기반 OS 계보. Linux: 1991년 Linus Torvalds가 Unix 기반으로 개발. open source와 Unix 호환성이 강점이며, 서버·임베디드·안드로이드의 핵심 OS로 자리 잡았다.
Q1 operating system을 응용 프로그램 관점, 시스템 관점, 구현 관점의 세 가지 시각에서 각각 정의하고 설명하시오.
① 응용 프로그램 관점 (extended/virtual machine): OS는 하드웨어를 추상화해 응용 프로그램에 편리한 실행 환경을 제공한다. 프로세서를 process/thread로, 메모리를 virtual address space로, 저장장치를 file·디렉토리로, I/O 장치와 네트워크를 파일 인터페이스로 추상화한다.
② 시스템 관점 (resource manager): CPU·메모리·I/O 장치·에너지 등의 자원을 여러 프로그램 간에 공유·보호하며 공정성과 효율성을 보장하는 자원 관리자 역할을 한다.
③ 구현 관점 (event-driven software): OS는 interrupt(하드웨어), exception(소프트웨어, 특히 system call), trap에 의해 비동기적으로 구동되는 고도의 병렬 이벤트 구동 소프트웨어다.
Q2 컴퓨터 3세대(1965–80)에서 multiprogramming과 time-sharing이 각각 도입된 배경과 차이점을 설명하시오.
multiprogramming 도입 배경: 2세대 batch system에서는 작업이 I/O 대기 중에 CPU가 유휴 상태로 낭비됐다. multiprogramming은 여러 작업을 메모리에 동시에 올려두어 한 작업이 I/O를 기다리는 동안 다른 작업이 CPU를 사용하게 함으로써 CPU 활용률을 높였다. 이를 위해 job scheduling, memory management, CPU scheduling, protection이 필요해졌다.
time-sharing 도입 배경: multiprogramming은 CPU 활용률을 높였지만 사용자가 대화식으로 상호작용하기 어려웠다. time-sharing은 CPU 시간을 짧은 time slice로 나눠 여러 사용자가 거의 동시에 대화식으로 컴퓨터를 사용하도록 응답 시간을 개선했다. 추가로 swapping, virtual memory, file system, 동기화, 프로세스 간 통신 등이 요구됐다.
차이점 요약: multiprogramming은 CPU 활용률 극대화에 초점, time-sharing은 응답 시간·대화성 개선에 초점. time-sharing은 multiprogramming을 기반으로 하지만 더 복잡한 스케줄링과 보호 기능을 필요로 한다.
Q3 Multics와 Unix의 주요 차이점을 설계 철학, 개발 규모, OS에 미친 영향의 측면에서 비교·서술하시오.
설계 철학: Multics는 top-down 방식으로 가능한 모든 기능을 포함하는 대형 시스템을 목표로 했다. 반면 Unix는 bottom-up 방식으로 단순성과 사용 편의성을 우선시했다.
개발 규모: Multics는 약 200 Man-Years(설계 150 + 개선 50)가 투입됐고 비싼 하드웨어가 필요했다. Unix는 Ken Thompson이 단 ~2 Man-Years로 완성해 저가 하드웨어에서 동작했다.
혁신과 영향: Multics는 계층적 파일 시스템, ACL, dynamic linking, 고수준 언어 OS 구현, 최초 상용 관계형 DBMS 등 현대 OS의 많은 아이디어를 선구적으로 도입했다. Unix는 Multics의 아이디어를 단순화·정제해 대학 중심으로 보급됐고, Linux·macOS·Android 등 현대 OS의 직접적인 뿌리가 됐다.
OS가 올바르게 동작하려면 하드웨어의 적절한 지원이 필수적이다. 사용자 응용 프로그램은 system call을 통해 커널에 서비스를 요청하며, OS 내부는 파일 시스템·메모리 관리·프로세스 관리·I/O 관리 등의 서브시스템으로 구성된다. 하드웨어는 interrupt(비동기, 하드웨어 발생), exception(동기, 소프트웨어 발생), signal(커널→프로세스)을 통해 OS와 응용 프로그램에 이벤트를 전달한다.
DMA는 CPU 개입 없이 대용량 I/O 전송을 처리하고, timer는 OS가 CPU를 주기적으로 되찾을 수 있게 한다. dual-mode(user / kernel 모드)와 protected instruction으로 OS는 자신을 보호한다. 메모리 보호는 base/limit register와 MMU로 구현하며, 동기화는 원자적 명령어나 인터럽트 비활성화로 지원된다.
하드웨어 장치가 비동기적으로 CPU에 보내는 신호. CPU는 현재 상태를 저장하고 interrupt service routine (ISR)으로 제어를 전달한다. vectored interrupt 방식으로 장치 유형에 따라 다른 핸들러를 호출할 수 있다.
명령어 실행 중 소프트웨어가 발생시키는 이벤트. 세 종류로 분류된다: trap(의도적, system call 등, 다음 명령 재개), fault(회복 가능, page fault 등, 현재 명령 재실행), abort(회복 불가, 프로그램 종료).
응용 프로그램이 OS에 특권 서비스를 요청하는 인터페이스. trap을 발생시켜 커널 모드로 전환 후 서비스를 수행하고 사용자 모드로 복귀한다. Linux 6.x 기준 450–550개.
커널이 프로세스에게 비동기적으로 전달하는 표준화된 메시지. 프로세스의 실행 흐름을 중단하고 등록된 signal handler를 실행한다. interrupt가 하드웨어→커널인 반면, signal은 커널→프로세스의 통신이다.
Direct Memory Access: CPU 개입 없이 장치 컨트롤러가 직접 버퍼에서 메인 메모리로 데이터 블록을 전송하는 방식. 블록 전송 완료 후 단 한 번의 interrupt만 발생해 CPU를 효율적으로 활용할 수 있다.
프로세서가 user mode와 kernel mode 두 가지 특권 수준을 지원하는 구조. protected instruction은 커널 모드에서만 실행 가능하며, 이를 통해 OS가 자신과 사용자 프로그램을 보호한다. IA-32는 Ring 0–3의 4단계를 지원한다.
OS가 프로그램 실행 전에 설정하는 두 개의 특권 레지스터. base register는 프로세스 메모리의 시작 주소, limit register는 크기를 저장해 프로세스가 자신의 메모리 영역 밖을 접근하지 못하도록 하드웨어 수준에서 차단한다.
사용자 응용 프로그램이 실행될 때의 계층 구조:
OS 내부 커널 서브시스템들은 상호 협력하며, 아키텍처 의존적 커널 코드(arch-dependent kernel code)가 하드웨어 플랫폼과 커널을 연결한다.
현대 컴퓨터 시스템의 핵심 특성:
OS와 하드웨어 아키텍처는 상호 의존적이다:
interrupt는 하드웨어 장치가 비동기적으로 발생시키는 이벤트다 (x86: INTR 또는 NMI 핀 신호). 처리 과정:
I/O 완료 감지 방법: polling(CPU가 반복 확인, 비효율) vs. hardware interrupt(완료 시 CPU에 신호, 효율적).
exception은 명령어 실행 중 소프트웨어가 발생시키는 이벤트 (x86: INT 명령어). 처리 방식은 interrupt와 동일하다.
예외의 세 가지 분류:
system call은 OS 서비스의 프로그래밍 인터페이스다. system call table에 등록된 핸들러를 호출하며 Linux 6.x 기준 450–550개가 존재한다. 주요 POSIX 시스템 콜: fork, exec, open, read, write, exit 등.
signal은 커널이 프로세스에게 비동기적으로 전달하는 표준화된 메시지로, 프로세스의 종료·일시정지·에러 처리 등에 사용된다. 일종의 제한된 IPC (inter-process communication)다.
신호 전달 시: OS가 프로세스의 정상 실행 흐름을 중단하고 등록된 signal handler를 실행하며, 핸들러가 없으면 기본 핸들러가 실행된다.
| 항목 | interrupt | exception | signal |
|---|---|---|---|
| 발생 주체 | 하드웨어 장치 | 소프트웨어(명령어 실행) | 커널 (→ 프로세스) |
| 발생 시점 | 비동기 | 동기 (명령어 실행 중) | 비동기 |
| 처리 주체 | 커널 (ISR) | 커널 (exception handler) | 프로세스 (signal handler) |
| 예시 | I/O 완료, 타이머 | system call, page fault | SIGKILL, SIGSEGV |
DMA (Direct Memory Access): I/O 데이터 전송 방식 두 가지를 비교하면, PIO (programmed I/O)는 CPU가 직접 데이터를 이동시켜 비효율적이고, DMA는 장치 컨트롤러가 CPU 개입 없이 메인 메모리로 직접 전송 후 완료 시 단 한 번 interrupt를 발생시켜 효율적이다.
Timer (타이머): OS가 실행 중인 프로그램으로부터 CPU를 회수하기 위해 사용하는 하드웨어 장치. 주기적으로 interrupt를 발생시켜 OS에 제어를 돌려준다. Linux 2.4에서 10ms, Linux 2.6에서 1ms 간격. 타이머 설정은 특권 연산이다.
protected (privileged) instruction의 예:
dual-mode: 아키텍처는 최소 kernel mode와 user mode 두 가지 동작 모드를 지원해야 한다. IA-32는 Ring 0(커널) ~ Ring 3(사용자) 4단계를 제공하며, 현재 특권 수준은 CS 레지스터의 CPL (Current Privilege Level)이 나타낸다.
system call 수행 시 보호 경계 전환 과정:
OS는 사용자 프로그램 간 상호 보호 및 자신의 보호를 위해 메모리 보호가 필요하다.
가장 단순한 방법 — base/limit registers: OS가 프로그램 시작 전 base register(시작 주소)와 limit register(크기)를 설정하면, 하드웨어가 모든 메모리 접근이 [base, base+limit) 범위 내인지 검사한다. 범위를 벗어나면 exception(보호 위반)이 발생한다.
고급 방법 — MMU (Memory Management Unit): base/limit 레지스터뿐 아니라 page table, 페이지 보호 비트, TLB, virtual memory, segmentation 등을 통해 더 정교한 메모리 보호를 제공한다. MMU 조작 자체도 특권 연산이다.
동기화 문제: interrupt는 언제든 발생할 수 있어 공유 데이터 접근 중에 인터럽트가 끼어들면 데이터 불일치가 생긴다. OS는 동시 실행 프로세스 간 동기화를 보장해야 한다.
동기화 방법:
Q1 interrupt, exception, signal의 개념을 발생 주체·시점·처리 주체를 기준으로 비교 설명하시오. exception의 세 가지 하위 분류(trap, fault, abort)도 포함하시오.
interrupt: 하드웨어 장치가 비동기적으로 CPU에 보내는 신호. I/O 완료·타이머 만료 등이 원인이며, 커널의 ISR이 처리한다. 처리 전 CPU 상태를 저장하고, vectored interrupt로 장치 유형에 맞는 핸들러를 호출한다.
exception: 명령어 실행 중 소프트웨어가 동기적으로 발생시키는 이벤트. 처리 방식은 interrupt와 유사하다. 세 분류: ① trap: 의도적(system call, breakpoint), 다음 명령 재개. ② fault: 비의도적·회복 가능(page fault), 현재 명령 재실행 또는 abort. ③ abort: 비의도적·회복 불가(패리티 오류), 프로그램 강제 종료.
signal: 커널이 프로세스에게 비동기적으로 전달하는 표준 메시지. 프로세스의 signal handler가 처리하며, interrupt가 하드웨어→커널인 반면 signal은 커널→프로세스의 통신이다.
Q2 OS의 dual-mode(user mode / kernel mode) 동작 원리를 설명하고, system call 수행 시 보호 경계를 어떻게 안전하게 전환하는지 단계별로 서술하시오.
dual-mode: 프로세서는 최소 user mode와 kernel mode 두 가지 특권 수준을 지원한다. 모드는 보호된 상태 레지스터의 비트로 표시되며 (IA-32: CS 레지스터의 CPL). protected instruction(직접 I/O, page table 갱신, TLB 로드, 타이머 설정 등)은 kernel mode에서만 실행 가능하다. 이를 통해 사용자 프로그램이 OS 자원을 임의로 조작하지 못하도록 막는다.
system call 시 보호 경계 전환 과정:
① 사용자 프로그램이 system call 명령(trap 명령어)을 실행한다.
② 하드웨어가 exception을 발생시켜 커널 핸들러를 호출한다.
③ 커널은 호출자의 레지스터·모드 비트를 저장하고 kernel mode로 전환한다.
④ system call table에서 요청된 서비스 핸들러를 찾아 실행한다.
⑤ OS는 사용자가 전달한 매개변수(포인터 등)의 유효성을 검증한다.
⑥ 서비스 완료 후 저장된 상태를 복원하고 user mode로 복귀한다.
Q3 메모리 보호를 위한 base/limit register 방식을 설명하고, MMU가 제공하는 더 정교한 보호 메커니즘과의 차이를 서술하시오. 또한 DMA가 PIO에 비해 가지는 장점을 함께 설명하시오.
base/limit register: OS가 프로그램 시작 전 특권 레지스터에 프로세스 메모리의 시작 주소(base)와 크기(limit)를 저장한다. 하드웨어(MMU)는 모든 메모리 접근이 [base, base+limit) 범위 내인지 자동으로 검사하며, 위반 시 exception을 발생시킨다. 장점: 단순하고 빠름. 단점: 연속적인 물리 메모리 할당 필요, 유연성 부족.
MMU 기반 고급 보호: page table과 페이지별 보호 비트(읽기/쓰기/실행 권한), TLB, virtual memory, segmentation을 통해 비연속 물리 메모리 지원, 세밀한 접근 제어, 프로세스 간 완전한 주소 공간 격리가 가능하다. MMU 조작 자체도 특권 연산이다.
DMA vs. PIO: PIO는 CPU가 루프를 돌며 데이터를 이동시켜 CPU가 I/O에 묶이고 낭비된다. DMA는 장치 컨트롤러가 직접 메모리에 접근해 블록을 전송하고 완료 후 단 한 번의 interrupt만 CPU에 전달한다. CPU는 I/O 전송 중에 다른 작업을 수행할 수 있어 시스템 효율이 크게 향상된다.
process란 실행 중인 프로그램의 인스턴스이며, CPU context(레지스터), OS 자원(메모리·파일), PID 등의 정보를 포함한다. OS는 각 프로세스를 PCB(Process Control Block)로 관리하고, context switch를 통해 CPU를 프로세스 간에 전환한다. UNIX에서는 fork()와 exec()를 이용해 프로세스를 생성·교체하며, NT 계열은 CreateProcess()를 사용한다.
프로세스 간 협력이 필요할 때는 IPC(Inter-Process Communication)를 사용한다. 동일 머신 내에서는 pipe, FIFO, shared memory, socket이 활용되며, 네트워크를 넘어서는 경우 RPC 및 Java RMI가 쓰인다. zombie process와 orphan process는 프로세스 생명 주기 관리에서 반드시 이해해야 할 비정상 상태이다.
실행 중인 프로그램의 인스턴스. CPU context(PC, SP, 레지스터), OS 자원(메모리, 열린 파일), PID·상태·소유자 등의 메타정보를 포함하는 동적 엔티티이며 실행 및 스케줄링의 기본 단위다.
OS가 프로세스마다 유지하는 자료구조. 프로세스 상태, program counter, 레지스터 값, 스케줄링 정보, 메모리 관리 정보, I/O 상태 등을 저장한다. Linux에서는 task_struct(include/linux/sched.h)로 구현된다.
CPU를 한 프로세스에서 다른 프로세스로 전환하는 행위. 현재 프로세스의 레지스터를 PCB에 저장하고, 다음 프로세스의 PCB에서 레지스터를 복원한다. 레지스터·메모리 맵 저장, 캐시 플러시 등의 오버헤드가 발생한다.
UNIX에서 자식 프로세스를 생성하는 시스템 콜. 부모의 가상 주소 공간 전체를 복사하고 내부 자료구조를 초기화한다. 부모에게는 자식의 PID를, 자식에게는 0을 반환한다.
현재 프로세스의 주소 공간에 새 프로그램을 로드한다. 새 프로세스를 생성하지 않고, 기존 프로세스를 지정한 프로그램으로 대체한다. UNIX 쉘에서 fork()와 쌍으로 사용된다.
zombie process: 실행을 마쳤지만 부모가 종료 상태를 아직 읽지 않아 프로세스 테이블에 잔류하는 프로세스. orphan process: 부모가 먼저 종료되어 고아가 된 프로세스로, init 프로세스(PID 1)가 즉시 입양한다.
OS가 시스템 내 모든 프로세스를 상태별로 관리하는 큐 집합. ready queue와 다양한 wait queue(장치·타이머·메시지별)로 구성되며, 상태가 바뀔 때 PCB가 큐 간에 이동한다.
프로세스들이 데이터를 주고받는 메커니즘. 동일 머신 내: pipe, FIFO, shared memory, socket. 네트워크 간: socket, RPC, Java RMI.
process는 "실행 중인 프로그램의 인스턴스"로, 단순한 코드 파일인 program과 구별된다. 프로세스는 다음을 포함한다.
메모리 레이아웃상 하위부터 read-only segment(.init/.text/.rodata), read-write segment(.data/.bss), run-time heap, user stack 순서로 배치된다. 최상위에는 사용자 코드에 보이지 않는 kernel virtual memory가 위치한다.
프로세스는 시스템 콜(fork() / CreateProcess()) 또는
시스템 초기화(init 프로세스, PID 1)를 통해 생성된다.
UNIX에서는 부모-자식 계층 구조(process group)가 형성되며,
ps 명령으로 확인할 수 있다. Windows는 프로세스 계층 개념이 없다.
자원 공유 방식: 부모 자원의 전부 또는 일부를 상속하거나(UNIX: UID, open files 등), 독립적인 자원을 받을 수 있다. 실행 방식: 부모가 자식을 기다리거나(wait), 병렬로 계속 실행할 수 있다.
종료 방식에는 네 가지가 있다.
fork()는 부모의 가상 주소 공간 전체를 복사해 자식을 만든다. 부모는 자식 PID를, 자식은 0을 반환값으로 받아 분기 처리한다. 이후 부모와 자식은 독립된 가상 주소 공간을 가지므로, 한쪽의 변수 수정이 다른 쪽에 영향을 주지 않는다.
| 구분 | 원인 | 특징 | 처리 |
|---|---|---|---|
| zombie process | 자식이 종료됐지만 부모가 아직 exit status를 읽지 않음 | 프로세스 테이블에 항목 잔류, Z 상태 |
부모가 wait()을 호출해 항목 제거 |
| orphan process | 부모 프로세스가 먼저 종료됨 | 자식은 계속 실행 중 | init(PID 1)이 즉시 입양 |
Linux ps 출력에서 볼 수 있는 주요 상태:
PCB는 OS 메모리에 동적으로 할당되며, 프로세스 생성 시 초기화되고 종료 시 해제된다. Linux의 task_struct는 Linux 2.4.18 기준 1456 bytes이다. 프로세스가 실행 중일 때 하드웨어 상태(PC, SP, 레지스터)는 CPU 내부에 있으며, OS가 프로세스를 멈출 때 PCB에 저장하고, 재개 시 PCB에서 복원한다.
context switch는 CPU를 프로세스 P0에서 P1으로 전환하는 과정이다.
하드웨어 지원에 따라 비용이 달라진다(예: UltraSPARC의 다중 레지스터 세트). 일반적으로 초당 수백~수천 번의 context switch가 발생한다.
OS는 모든 프로세스의 상태를 큐로 관리한다.
PCB는 OS 메모리 내에 동적으로 할당된다. 프로세스 생성 시 PCB를 할당·초기화하여 큐에 넣고, 상태 변화에 따라 큐 간을 이동하며, 종료 시 PCB를 해제한다.
UNIX 프로세스 생성의 두 단계:
prog를 주소 공간에 로드하여 재사용한다. 새 프로세스를 생성하지 않는다.
Simplified UNIX Shell 패턴: while(1) 루프 안에서 명령을 읽고,
fork()로 자식을 생성한 뒤 자식이 exec()로
명령을 실행하고, 부모는 wait()으로 자식 종료를 기다린다.
Windows NT 계열은 CreateProcess(char *prog, char *args, …)를 사용한다.
UNIX의 fork+exec 두 단계를 한 번의 호출로 처리한다.
새 PCB와 주소 공간을 생성하고, prog를 로드하며, args를 메모리에 복사하고,
main부터 실행할 하드웨어 컨텍스트를 초기화한 뒤 ready queue에 배치한다.
fork()가 유용한 경우: 자식이 부모와 협력하거나, 부모의 데이터를 이용해야 할 때. 예: 웹 서버에서 accept()로 받은 소켓을 자식이 그대로 사용해 클라이언트를 처리하는 패턴.
각 프로세스는 독립된 가상 주소 공간으로 보호되어 직접 데이터 공유가 어렵다. shared memory는 두 프로세스가 동일한 물리 메모리 영역을 직접 참조하게 한다. 양쪽 모두 갱신 내용을 즉시 볼 수 있으나, 동시 접근 조율이 필요하다.
UNIX POSIX API: shmget()으로 영역 생성·조회, shmat()으로 프로세스 주소 공간에 연결.
같은 key를 사용하는 두 프로세스가 동일한 공유 메모리 세그먼트를 참조한다.
Q1 UNIX에서 fork()와 exec()의 역할을 각각 설명하고, 두 시스템 콜을 조합하여 새 프로그램을 실행하는 과정을 단계별로 서술하시오. 또한 Windows NT의 CreateProcess()와 비교하시오.
① fork(): 현재 프로세스(부모)의 PCB와 가상 주소 공간 전체를 복사하여 새 자식 프로세스를 생성한다. 부모에게는 자식 PID를, 자식에게는 0을 반환한다.
② exec(): 현재 프로세스를 중지하지 않고, 지정한 프로그램을 현재 프로세스의 주소 공간에 덮어 로드한다. 새 프로세스를 생성하지 않는다.
③ 결합 패턴 (Simplified UNIX Shell): 부모가 fork()를 호출 → 자식 프로세스 생성 → 자식이 exec()를 호출하여 원하는 프로그램으로 교체 → 부모는 wait()으로 자식 종료를 대기.
④ 비교: CreateProcess()는 PCB 생성, 주소 공간 생성, 프로그램 로드, 인자 복사, 하드웨어 컨텍스트 초기화를 단일 호출로 처리한다. UNIX의 두 단계(fork+exec)와 달리 부모의 자원을 상속하는 명시적 단계가 없다.
Q2 zombie process와 orphan process의 발생 원인과 OS의 처리 방식을 각각 설명하고, 이를 방지하기 위한 프로그래밍 상의 주의사항을 서술하시오.
zombie process: 자식이 exit()를 호출해 종료했지만 부모가 아직 wait()으로 종료 상태를 수거하지 않아, 프로세스 테이블에 항목이 남아있는 상태(Z 상태). OS는 자동으로 제거하지 않으며, 부모가 wait()/waitpid()를 호출해야 항목이 제거된다.
orphan process: 부모 프로세스가 자식보다 먼저 종료된 경우. 고아가 된 자식은 init(PID 1)에 즉시 입양되어 계속 실행된다.
방지를 위한 주의사항: 부모는 자식 생성 후 반드시 wait()을 호출하거나, SIGCHLD 시그널 핸들러를 등록하여 자식 종료 시 비동기적으로 수거해야 한다. 장기 실행 서버에서 zombie가 누적되면 프로세스 테이블이 고갈될 수 있다.
Q3 context switch의 과정을 단계별로 설명하고, 발생하는 오버헤드의 종류와 이를 줄이기 위한 하드웨어 지원 방식을 서술하시오.
① OS가 현재 실행 중인 프로세스 P0의 레지스터(PC, SP, 범용 레지스터)를 PCB0에 저장한다.
② 다음 실행할 프로세스 P1의 PCB1에서 레지스터 값을 CPU에 복원한다.
③ P1이 CPU를 할당받아 실행을 재개한다.
오버헤드 종류:
하드웨어 지원: UltraSPARC는 다중 레지스터 세트를 제공하여 저장/복원 오버헤드를 줄인다. 고급 메모리 관리 기법은 context switch마다 추가 데이터 전환이 필요할 수 있다. 일반적으로 초당 수백~수천 번 발생하며, Linux 사례에서 약 86 context switches/s(per CPU)가 측정된다.
프로세스는 무거운(heavy-weight) 실행 단위로, 생성 비용과 IPC 비용이 높다. 협력하는 프로세스들이 같은 코드·데이터·자원을 공유하면서 각자 독립적인 실행 흐름만 필요한 경우, 이 실행 흐름(execution state)을 별도로 분리한 것이 thread이다. 스레드는 프로세스 내 주소 공간·자원을 공유하면서 각자 PC, SP, 레지스터, 스택을 갖는다.
스레드를 사용하면 동시성(concurrency) 구현 비용이 크게 줄어들고, 멀티코어 활용, 처리량 향상, 응답성 개선 등의 이점이 생긴다. Pthreads는 POSIX 표준 스레드 API이며, 스레드 사용 시 race condition, 시그널 처리, 스레드 취소, MT-safe 함수 사용 등 여러 이슈를 고려해야 한다.
프로세스 내의 실행 흐름 단위. PC, 범용 레지스터, 스택을 독자적으로 가지며, 프로세스의 코드·데이터·대부분의 OS 상태를 다른 스레드와 공유한다. lightweight process(LWP) 또는 thread of control이라고도 한다.
하나의 프로세스 안에 여러 스레드를 두어 병렬성을 확보하는 기법. 생성 비용이 저렴하고, I/O와 연산을 중첩할 수 있어 처리량과 응답성이 향상된다. 멀티코어/멀티프로세서를 효과적으로 활용할 수 있다.
여러 스레드가 하나의 주소 공간을 공유한다. 각 스레드는 독립적인 스택 영역을 가지며, heap, static data, code 영역은 공유된다. PC와 SP는 스레드마다 별도로 존재한다.
POSIX 표준(IEEE 1003.1c) 스레드 API. pthread_create(), pthread_exit(), pthread_join()으로 스레드 생명 주기를 관리하고, mutex와 condition variable로 동기화한다.
여러 스레드가 공유 데이터를 동시에 읽고 쓸 때 실행 순서에 따라 결과가 달라지는 문제. 예: 20개 스레드가 각각 카운터를 100씩 더하면 기대값 2000 대신 1957처럼 낮은 값이 나올 수 있다.
실행 완료 전에 스레드를 종료시키는 것. asynchronous cancellation: 즉시 종료(자원 누출 위험). deferred cancellation: 취소 지점(cancellation point)에서만 종료. Pthreads는 두 방식 모두 지원한다.
여러 스레드가 동시에 호출해도 안전한 함수. 전역 데이터를 사용하지 않거나 읽기 전용인 함수는 자동으로 MT-safe. 전역 상태를 수정하는 함수는 동기화를 통해 MT-safe하게 만들어야 한다. 대표 이슈: errno는 스레드마다 독립적인 버전이 필요하다.
주소 공간 수와 주소 공간당 스레드 수로 OS를 분류할 수 있다. 단일 주소 공간·단일 스레드(MS-DOS, 초기 Mac), 다중 주소 공간·단일 스레드(전통 UNIX), 다중 주소 공간·다중 스레드(Linux, Windows, macOS 등).
프로세스는 heavy-weight 실행 단위다.
웹 서버처럼 단순 요청 처리를 위해 fork()를 반복하는 것은 과도하다.
병렬 처리를 위해 여러 프로세스를 만들 때, 프로세스들이 같은 코드·데이터·자원을 공유하면서도 각각 PCB, 페이지 테이블, 주소 공간 복사 등을 가져야 하는 것은 낭비다.
협력 프로세스들이 공유하는 것: 코드·데이터(주소 공간), 권한, 자원(파일·소켓). 다른 것: 하드웨어 실행 상태(PC, 레지스터, SP, 스택).
핵심 아이디어: 프로세스 개념에서 실행 상태를 분리한다.
하나의 프로세스 안에 여러 스레드가 각자의 스택을 가지고 코드·데이터·힙을 공유하며 실행된다.
스레드 = 프로그램 내 명령어들의 실행 흐름.
| 특성 | process | thread |
|---|---|---|
| 실행 단위 | 스케줄링의 독립 단위 (컨테이너) | 스케줄링의 기본 단위 |
| 주소 공간 | 독립적 주소 공간 | 프로세스 주소 공간 공유 |
| 데이터 공유 | 공유 시 IPC 필요 (비용 높음) | 같은 주소 공간으로 저렴한 공유 |
| 생성 비용 | ~20K cycles (Linux) | ~10K cycles 이하 (Linux) |
| 개별 보유 | PCB, 페이지 테이블, 주소 공간 | PC, SP, 레지스터, 스택 |
| context switch | 더 비쌈 (주소 공간 교체 포함) | 더 저렴 |
프로세스 하나의 주소 공간에 여러 스레드가 공존한다. 각 스레드는 별도의 스택 영역(SP 포인터 각자 유지)과 PC를 가지고, heap, static data(.data segment), code(.text segment)는 모든 스레드가 공유한다.
| 주소 공간 수 \ 스레드 수 | 스레드 1개 | 스레드 여러 개 |
|---|---|---|
| 주소 공간 1개 | MS-DOS, 초기 Macintosh | 임베디드 OS, VxWorks, uClinux |
| 주소 공간 여러 개 | 전통 UNIX | Linux, Windows, macOS, Solaris, HP-UX |
Pthreads는 POSIX 표준(IEEE 1003.1c) 스레드 API로, UNIX 계열 OS에서 공통으로 사용된다. 주요 함수:
pthread_create(tid, attr, start_routine, arg): 스레드 생성pthread_exit(retval): 현재 스레드 종료pthread_join(tid, thread_return): 스레드 종료 대기pthread_mutex_init/lock/unlock/destroy: mutex 동기화pthread_cond_wait/signal/broadcast: condition variable 동기화
여러 스레드가 같은 변수를 동시에 수정하면 race condition이 발생한다.
예: 20개 스레드가 각각 카운터를 100씩 증가시키면 이론상 2000이 되어야 하지만,
실제로는 1957처럼 더 작은 값이 나올 수 있다. 이는 비원자적 연산(*count = *count+1)에서
읽기-수정-쓰기 사이에 다른 스레드가 개입하기 때문이다.
pthread_sigmask()로 스레드별 시그널 마스크를 설정한다.
Q1 프로세스에서 thread의 개념이 등장한 동기를 설명하고, process와 thread의 공통점 및 차이점을 비교하시오. 특히 주소 공간·생성 비용·데이터 공유 측면에서 서술하시오.
등장 동기: 프로세스는 heavy-weight 단위로 생성 비용(Linux ~20K cycles)과 IPC 비용(시스템 콜·데이터 복사)이 높다. 협력하는 여러 프로세스가 동일한 코드·데이터·자원을 공유하면서 각각 독립적인 실행 흐름만 필요한 경우, 이 흐름만 분리한 것이 스레드다.
공통점:
차이점:
Q2 Pthreads API를 이용하여 스레드를 생성하고 동기화하는 방법을 핵심 함수 중심으로 설명하고, 공유 데이터 접근 시 발생할 수 있는 race condition의 예를 들어 서술하시오.
스레드 생명 주기 함수:
pthread_create()로 스레드를 생성하고 시작 함수를 지정한다.
pthread_join()으로 대상 스레드 종료를 기다리며 반환값을 수집한다.
pthread_exit()으로 스레드 자신을 종료한다.
동기화:
mutex(pthread_mutex_lock/unlock)로
임계 구역에 대한 상호 배제를 보장하고,
condition variable(pthread_cond_wait/signal)로
스레드 간 이벤트 대기/통지를 구현한다.
race condition 예:
20개 스레드가 각각 공유 카운터를 100씩 증가할 때,
*count = *count + 1 연산은 읽기·수정·쓰기의 세 단계로 이루어져 비원자적이다.
스레드 A가 값을 읽은 직후 스레드 B도 같은 값을 읽으면 두 번의 증가가 한 번만 반영된다.
결과: 기대값 2000 대신 1957 등이 출력될 수 있다.
Q3 멀티스레드 환경에서 thread cancellation의 두 가지 방식과 각각의 장단점을 비교하고, signal handling에서 발생하는 이슈 및 Pthreads의 해결 방법을 서술하시오.
Thread cancellation:
Signal handling 이슈:
멀티스레드 프로세스에서 시그널을 어느 스레드에 전달해야 하는지 결정해야 한다.
옵션: 모든 스레드에 전달, 특정 스레드에만 전달.
Pthreads 해결책: pthread_sigmask()로 스레드별 시그널 마스크를 설정하여,
각 스레드가 받을 시그널을 개별적으로 제어한다.
스레드를 구현하는 주체에 따라 kernel-level thread와 user-level thread로 나뉜다. 커널 스레드는 OS가 생성·관리하며 시스템 콜을 통해 동작하여 안전하지만 비용이 크다. 유저 스레드는 사용자 공간 라이브러리가 관리하며 프로시저 콜 수준으로 빠르지만 OS에 보이지 않아 블로킹 I/O 등의 이슈가 생긴다.
두 방식의 매핑 관계를 정의하는 threading model에는 many-to-one(N:1), one-to-one(1:1), many-to-many(M:N) 세 가지가 있다. Java 스레드는 JVM을 통해 하이브리드 방식으로 구현되며, Go의 goroutine은 G:M:P 스케줄러로 경량 동시성을 실현한다.
OS가 직접 생성·관리하는 스레드. 모든 스레드 연산은 시스템 콜을 통해 커널에서 수행된다. 하나가 블록돼도 같은 프로세스의 다른 스레드를 실행할 수 있다. 프로세스보다 가볍지만 시스템 콜 비용이 있다.
사용자 공간 라이브러리(runtime system)가 관리하는 스레드. 커널 개입 없이 프로시저 콜 수준으로 생성·전환·동기화가 이루어진다. 커널 스레드보다 10~100배 빠르며, 스레드별 TCB가 매우 작다.
유저 스레드를 관리하는 자료구조. PC, 레지스터, 스택, 소형 TCB만 있으면 된다. 커널의 PCB에 비해 훨씬 작고 간단하다.
여러 유저 스레드가 단일 커널 스레드에 매핑된다. 커널 스레드가 없는 시스템에서 사용. 한 스레드가 블로킹 시스템 콜을 하면 프로세스 전체가 블록된다. 예: Solaris Green Threads, GNU Portable Threads.
각 유저 스레드가 하나의 커널 스레드에 1:1 매핑된다. 진정한 병렬성을 제공하며 한 스레드 블록 시 다른 스레드 실행 가능. 대부분의 현대 OS(Linux, Windows 등)가 채택.
M개의 유저 스레드가 N개의 커널 스레드에 매핑(M ≥ N). OS가 필요한 만큼 커널 스레드를 생성할 수 있어 유연성이 높다. 구현이 복잡하다.
Go 언어의 경량 동시성 단위. 유저 수준 스레드(그린 스레드)로 초기 스택 2KB로 시작한다. Go 런타임 스케줄러(G:M:P 모델)가 관리하며 커널 스레드 수를 최소화하면서 높은 동시성을 달성한다.
유저 수준 스레드 스케줄러가 OS에 타이머 인터럽트(UNIX 시그널)를 요청해 비협조적인 스레드에서 CPU를 강제 회수하는 방식. 비선점 방식(non-preemptive, yield() 의존)의 한계를 해결한다.
스레드 생성·관리의 주체가 누구인가에 따라 두 가지로 나뉜다.
유저 스레드 관리가 가능한 이유: 스레드들이 같은 주소 공간을 공유하므로 thread manager가 주소 공간을 조작할 필요 없다. 스레드 간 차이는 PC, SP, 레지스터 정도이며, 이는 유저 프로세스 수준에서 조작 가능하다.
특징:
한계:
커널 스레드 구현은 기존 프로세스 관리와 유사하다. 각 스레드는 커널 수준의 PCB와 유사한 구조를 가지며, OS 스케줄러가 스레드 단위로 CPU를 할당한다.
특징:
한계:
유저 스레드 스케줄러는 OS와 프로세스의 관계처럼 유저 레벨 라이브러리로 구현된다. 유지하는 큐:
Thread context switch: 현재 스레드의 머신 상태를 스택에 push → 다음 스레드의 스택에서 pop → 새 스레드의 PC로 복귀.
Non-preemptive scheduling: 스레드가 자발적으로 yield()를 호출해야만 다른 스레드로 전환. 스레드가 yield를 호출하지 않으면 CPU를 독점한다.
Preemptive scheduling: 스케줄러가 OS에 타이머 인터럽트를 요청. 인터럽트(UNIX 시그널)마다 스케줄러가 제어를 획득하여 적절히 context switch.
| 특성 | kernel-level thread | user-level thread |
|---|---|---|
| 관리 주체 | OS 커널 | 유저 공간 라이브러리 |
| 생성/전환 비용 | 시스템 콜 필요 (비쌈) | 프로시저 콜 수준 (10~100x 빠름) |
| 블로킹 I/O | 한 스레드 블록 → 다른 스레드 계속 실행 가능 | 한 스레드 블록 → 프로세스 전체 블록 |
| 병렬 실행 | 멀티코어에서 진정한 병렬 가능 | 단일 커널 스레드 위에서는 병렬 불가 |
| OS 가시성 | OS가 인식하고 스케줄 | OS에 보이지 않음 |
| 스레드 수 제한 | 있음 (커널 자원 소모) | 제한 적음 |
| 이식성 | OS 의존 | 라이브러리 포팅으로 이식 가능 |
| 모델 | 매핑 | 장점 | 단점 | 예시 |
|---|---|---|---|---|
| many-to-one (N:1) | N개 유저 스레드 → 커널 스레드 1개 | 빠른 스레드 관리 | 블로킹 시 전체 블록, 병렬성 없음 | Solaris Green Threads, GNU Portable Threads |
| one-to-one (1:1) | 유저 스레드 1개 → 커널 스레드 1개 | 진정한 병렬성, 한 스레드 블록해도 무관 | 커널 스레드 수만큼 비용 증가 | Linux, Windows, macOS |
| many-to-many (M:N) | M개 유저 스레드 → N개 커널 스레드 (M≥N) | 유연성 높음, 커널 스레드 수 조절 가능 | 구현 복잡 | Solaris(구버전), Windows with fiber |
Java 스레드는 JVM이 관리하며, 하위 OS의 스레드 모델을 이용해 구현된다. 스레드 생성 방법:
class MyThread extends Thread 후 run() 오버라이드class MyThread implements Runnable 후 run() 구현역사적 변천:
Go는 Google 인프라를 위해 개발된 언어다(C++ 대비 빌드 속도와 복잡도 문제 해결). goroutine은 Go의 경량 동시성 단위다.
특징:
Go 런타임 스케줄러 (G:M:P 모델):
규칙: 커널 스레드(M)는 비싸므로 수를 최소화하고, 많은 goroutine으로 높은 동시성을 달성한다.
go say("Async")처럼 go 키워드로 goroutine을 비동기 실행한다.
Q1 kernel-level thread와 user-level thread의 구현 방식 차이를 설명하고, 각각의 장단점을 블로킹 I/O 처리·생성 비용·OS 가시성 측면에서 비교 서술하시오.
kernel-level thread: OS 커널이 직접 생성·관리하며 모든 연산이 시스템 콜로 수행된다. 장점: 하나의 스레드가 블로킹 I/O를 해도 OS가 같은 프로세스의 다른 스레드를 실행할 수 있다. 멀티코어 병렬 실행 가능. 단점: 모든 스레드 연산이 보호 경계를 넘는 시스템 콜이므로 비용이 크다. 커널이 상태를 유지해야 하므로 최대 스레드 수에 제한이 있다(Linux ~256,430).
user-level thread: 사용자 공간 라이브러리가 관리하며 커널 개입 없이 프로시저 콜 수준으로 동작한다. 장점: 생성·전환·동기화가 커널 스레드보다 10~100배 빠르다. TCB가 작고 이식성이 높다. 단점: OS가 스레드를 인식하지 못해 스케줄 결정이 나빠질 수 있다. 한 스레드의 블로킹 시스템 콜이 프로세스 전체를 블록시킨다.
user-level thread context switch 과정: 현재 스레드의 머신 상태를 자신의 스택에 push → 다음 스레드의 스택에서 pop → 다음 스레드의 PC로 리턴.
Q2 스레드 구현의 세 가지 threading model(many-to-one, one-to-one, many-to-many)을 각각 설명하고, 현대 OS와 Java가 어떤 모델을 채택하는지 서술하시오.
many-to-one (N:1): N개의 유저 스레드를 단일 커널 스레드에 매핑. 커널 스레드를 지원하지 않는 시스템에서 사용. 한 스레드가 블로킹 시스템 콜을 하면 프로세스 전체 블록. 멀티코어 병렬성 없음. 예: Solaris Green Threads, GNU Portable Threads.
one-to-one (1:1): 각 유저 스레드가 하나의 커널 스레드와 1:1 대응. 진정한 병렬 실행 가능. 한 스레드 블록 시 다른 스레드 계속 실행. 커널 스레드 수만큼 비용 증가. 현대 OS(Linux, Windows, macOS)의 표준 방식.
many-to-many (M:N): M개의 유저 스레드를 N개의 커널 스레드에 유연하게 매핑(M≥N). OS가 필요한 만큼 커널 스레드를 생성해 두 방식의 장점을 취한다. 구현이 가장 복잡하다.
Java: 초기(1.2 이전) GreenThread = 100% 유저 수준(many-to-one). 현재는 커널·유저 스레드를 결합한 하이브리드(many-to-many) 모델 사용.
Q3 Go 언어의 goroutine이 기존 OS 스레드 대비 가지는 이점을 설명하고, Go 런타임의 G:M:P 스케줄링 모델을 서술하시오.
goroutine의 이점:
G:M:P 스케줄러:
Go 런타임은 M의 수를 최소로 유지하면서 많은 G를 P를 통해 M에 배분하는 M:N 방식으로 멀티코어 병렬성과 경량 동시성을 동시에 달성한다.