컴퓨터 하드웨어 구조
폰 노이만 구조에서 CPU의 명령어 사이클과 레지스터, 메모리 계층, 버스가 데이터를 주고받는 방식을 익힙니다.
운영체제를 이해하려면, OS가 관리하는 하드웨어의 구조를 먼저 알아야 합니다.
CPU가 명령어를 실행하는 방식, 메모리의 계층 구조, 데이터가 이동하는 버스 — 이런 기본 구조 위에서 OS의 모든 기능이 동작합니다.
OS를 배우면서 하드웨어까지 알아야 하나?라고 생각할 수 있습니다.
반드시 알아야 합니다.
프로세스의 컨텍스트 스위칭에 왜 비용이 드는지를 이해하려면 레지스터가 무엇인지 알아야 합니다.
가상 메모리가 왜 필요한지를 이해하려면 메모리의 물리적 한계를 알아야 합니다.
캐시가 왜 존재하는지를 이해하려면 메모리와 CPU의 속도 격차를 알아야 합니다.
하드웨어 구조를 모르는 상태에서 OS 개념을 외우는 것은 왜를 빠뜨린 채 무엇만 아는 것입니다.
폰 노이만 아키텍처
현대 컴퓨터의 대부분은 폰 노이만 아키텍처(Von Neumann Architecture)를 따릅니다.
핵심 아이디어는 놀라울 정도로 단순합니다.
프로그램과 데이터를 같은 메모리에 저장한다.
이것이 왜 혁명적이었을까요?
이전의 컴퓨터(ENIAC 등)는 프로그램을 물리적 배선으로 구현했습니다.
덧셈 프로그램을 실행하려면 수천 개의 스위치와 케이블을 특정 패턴으로 연결해야 했고, 곱셈 프로그램으로 바꾸려면 배선을 전부 다시 해야 했습니다.
프로그램 교체에 며칠이 걸릴 수도 있었습니다.
폰 노이만의 제안은 프로그램도 숫자의 나열(데이터)이니까, 메모리에 저장하면 된다는 것이었습니다.
프로그램을 바꾸고 싶으면 메모리의 내용만 교체하면 됩니다.
이 아이디어가 범용 컴퓨터(General-Purpose Computer)의 토대입니다.
이 구조의 구성 요소는 세 가지입니다.
- CPU(Central Processing Unit): 명령어를 실행하는 처리 장치. 프로그램의 모든 계산과 판단이 여기서 일어납니다.
- 메모리(Memory): 프로그램 코드와 데이터를 저장하는 공간. CPU가 직접 접근할 수 있는 전자적 저장 장치입니다.
- I/O 장치: 외부와 데이터를 주고받는 장치. 키보드, 마우스, 디스크, 모니터, 네트워크 카드 등이 해당합니다.
이 구성 요소들은 주소·데이터·제어 정보를 주고받는 연결망으로 이어집니다.
하드웨어 · 구성 요소의 관계
CPU는 메모리와 I/O 컨트롤러에 연결되어 있습니다
메모리는 명령어와 데이터를 저장하고, I/O 컨트롤러는 장치와 데이터를 교환합니다. 선은 구성 요소의 관계이며 실행 순서가 아닙니다.
선은 구성 요소의 연결 관계입니다. 메모리는 명령어와 데이터를 함께 저장합니다. 실제 하드웨어의 캐시·전용 채널·PCIe 토폴로지를 모두 하나의 물리적 버스로 나타낸 것은 아닙니다.
한 가지 근본적인 한계가 있습니다.
프로그램 코드와 데이터가 같은 메모리에 있으므로, CPU가 명령어를 가져오려면 메모리에 접근해야 하고, 데이터를 가져오려면 또 메모리에 접근해야 합니다.
메모리 접근이 병목이 되는 이 현상을 폰 노이만 병목(Von Neumann Bottleneck)이라 합니다.
CPU 속도는 해마다 빠르게 발전했지만, 메모리 접근 속도의 발전은 상대적으로 느려서 이 격차가 점점 벌어졌습니다.
이 병목을 완화하기 위해 캐시 메모리가 등장합니다.
한편 하버드 아키텍처(Harvard Architecture)는 명령어 메모리와 데이터 메모리를 물리적으로 분리합니다.
명령어와 데이터를 동시에 가져올 수 있어 병목이 줄지만, 설계가 복잡해집니다.
마이크로컨트롤러(Arduino의 AVR 등) 같은 임베디드 프로세서에서 주로 사용됩니다.
현대의 고성능 CPU들은 외부적으로는 폰 노이만 구조를 따르지만, 내부적으로는 L1 캐시를 명령어용(I-Cache)과 데이터용(D-Cache)으로 분리하는 수정된 하버드 아키텍처를 사용하여 두 방식의 장점을 취합니다.
CPU 구조
CPU는 컴퓨터의 두뇌이지만, 실제로 하는 일은 단순합니다.
메모리에서 명령어를 하나 읽고, 해석하고, 실행하고, 다음 명령어로 넘어갑니다.
이것을 초당 수십억 번 반복할 뿐입니다.
CPU 내부는 크게 세 부분으로 나뉩니다.
ALU (Arithmetic Logic Unit)
산술 연산(덧셈, 뺄셈, 곱셈, 나눗셈)과 논리 연산(AND, OR, NOT, XOR, 비교)을 수행합니다.
정수 산술과 논리 연산을 처리합니다. 부동소수점·벡터 등은 별도 실행 장치가 맡을 수 있습니다.
if (a > b) 같은 비교도, x = y + z 같은 연산도 ALU가 처리합니다.
부동소수점 연산을 위한 FPU(Floating-Point Unit)가 별도로 존재하기도 합니다.
제어 장치 (Control Unit)
명령어를 해석하고, 다른 구성 요소에 제어 신호를 보냅니다.
오케스트라의 지휘자와 비슷합니다.
메모리에서 주소 0x1000의 데이터를 가져와라, ALU에서 덧셈을 수행해라, 결과를 레지스터 R3에 저장해라 같은 지시를 내립니다.
제어 장치 자체는 계산을 하지 않습니다.
각 부품이 올바른 타이밍에 올바른 동작을 하도록 조율할 뿐입니다.
레지스터 (Register)
CPU 내부의 초고속 저장 공간입니다.
메모리보다 수십~수백 배 빠르지만, 크기가 매우 작습니다(x86_64에서 범용 레지스터는 16개, 각 8바이트).
현재 실행 중인 명령어의 피연산자, 연산 결과, 중간값을 임시로 저장합니다.
OS 관점에서 특별히 중요한 레지스터들이 있습니다.
- 프로그램 카운터(PC, 또는 IP — Instruction Pointer): 다음에 실행할 명령어의 메모리 주소를 담고 있습니다. CPU가 명령어를 하나 실행할 때마다 자동으로 증가합니다.
goto나 함수 호출 시 이 값이 점프합니다. - 스택 포인터(SP): 현재 스택의 최상단(top) 주소를 가리킵니다. 함수 호출 상태와 일부 로컬 변수·반환 정보가 스택에 놓일 수 있습니다. 실제 배치는 ABI·최적화·명령어 집합에 따라 달라집니다.
- 프로세서 상태 레지스터(PSW/FLAGS): 직전 연산의 결과에 대한 상태 비트(제로, 캐리, 오버플로 등)와 인터럽트 활성화 여부, 아키텍처별 제어 정보를 담습니다. x86의 현재 특권 수준은 FLAGS 하나가 아니라 CS 등 관련 상태와 함께 구분합니다.
OS가 작업을 전환(Context Switch)할 때는 그 작업을 이어 실행하는 데 필요한 레지스터와 스택 등 문맥 상태를 보존하고, 다음 작업의 문맥을 복원합니다. 보존할 상태와 그 위치는 CPU 아키텍처·커널 구현·ABI에 따라 달라집니다.
예를 들어 Linux x86-64의 진입·작업 전환 구현(v6.12)을 보면, 사용자 모드에서 커널로 들어올 때 기록하는 상태와 커널 안에서 작업을 바꿀 때 보존하는 재개 상태가 나뉩니다. 후자는 호출 규약에 맞는 레지스터와 커널 스택 등을 다루므로, 전환할 때마다 CPU의 모든 레지스터를 새로 복사하는 모델로 읽지 않습니다.
실제로 저장·복원하는 상태가 늘면 그 작업량은 커질 수 있지만, CPU가 제공하는 레지스터의 총 개수만으로 컨텍스트 스위칭 비용을 계산할 수는 없습니다. 주소 공간·캐시·TLB 상태와 커널의 저장 전략도 영향을 줍니다.
5장에서 이 과정을 더 자세히 다룹니다.
명령어 사이클 (Instruction Cycle)
CPU는 명령어 사이클을 끊임없이 반복합니다.
- Fetch: PC가 가리키는 메모리 주소에서 명령어를 읽어옵니다.
- Decode: 명령어를 해석하여 어떤 연산을 수행할지, 피연산자가 무엇인지 파악합니다.
- Execute: ALU가 연산을 수행하거나, 메모리 접근이 이루어집니다.
- Write-back: 결과를 레지스터나 메모리에 저장합니다.
이 사이클이 초당 수십억 번 반복됩니다.
3GHz CPU는 1초에 30억 번의 클록 사이클을 실행합니다.
단순한 명령어는 한 클록에 실행될 수 있고, 복잡한 명령어는 여러 클록이 필요합니다.
현대 CPU는 이 과정을 파이프라이닝(Pipelining)으로 중첩합니다.
명령어 1이 Execute 단계에 있을 때 명령어 2는 Decode 단계, 명령어 3은 Fetch 단계에 있을 수 있습니다.
단계별 처리를 겹치면 전체 처리량이 증가하고, 이상적인 조건에서는 매 클록마다 하나의 명령어가 완료될 수 있습니다.
단계 수로 직접 계산하기: Fetch·Decode·Execute·Write-back 네 단계가 각각 1클록이며, 명령어 세 개 사이에 데이터 의존·분기·캐시 미스가 없다고 가정합니다. 한 명령어를 끝낸 뒤 다음 것을 시작하면 클록입니다. 파이프라인에서는 첫 명령어가 4클록째, 두 번째가 5클록째, 세 번째가 6클록째에 완료되어 전체는 6클록입니다. 처리량은 늘지만 첫 명령어의 지연은 여전히 4클록입니다. 이 단순 모델의 결과를 모든 3GHz CPU의 실제 성능으로 읽지는 않습니다.
독립 변형: 같은 네 단계에서 독립 명령어를 다섯 개 실행하면 순차 실행과 파이프라인 실행에 각각 몇 클록이 필요할까요? 첫 명령어의 완료 시점도 따로 답해 보세요.
계산과 지연·처리량 구분 확인
순차 실행은 4 × 5 = 20클록입니다. 파이프라인은 채우는 4클록 뒤 나머지 네 명령어가 매 클록 하나씩 끝나므로 4 + 4 = 8클록입니다. 첫 명령어는 여전히 4클록째 완료됩니다. 전체 시간은 이 조건에서 20/8 = 2.5배 개선되지만 개별 명령어의 지연이 1클록이 된 것은 아닙니다. 의존성·분기·메모리 대기로 빈 단계가 생기면 이 계산에 지연을 더해야 합니다.
하지만 if문(조건 분기)을 만나면 파이프라인에 문제가 생깁니다.
조건의 결과가 나오기 전에는 다음 명령어가 무엇인지 알 수 없기 때문입니다.
이를 해결하기 위해 CPU는 분기 예측(Branch Prediction)을 하는데, 예측이 틀리면 파이프라인을 비우고 다시 시작해야 합니다.
이것이 분기가 많은 코드가 느린 이유 중 하나이며, 성능 최적화를 다루는 책에서 자주 언급되는 주제입니다.
메모리 계층 구조
컴퓨터의 저장 장치는 속도와 용량의 트레이드오프를 가집니다.
일반적으로 낮은 접근 지연을 제공하는 저장 공간은 비트당 비용이 높고 용량이 작습니다.
이 물리적 제약이 메모리 계층 구조를 만듭니다.
다음 수치는 계층의 규모 차이를 보기 위한 예시입니다. 특정 장비의 측정 결과나 보장 지연이 아니며 CPU·용량·접근 패턴에 따라 달라집니다.
| 계층 | 속도 | 용량 | 특성 |
|---|---|---|---|
| 레지스터 | ~0.3ns | 수십~수백 바이트 | CPU 내부, 가장 빠름 |
| L1 캐시 | ~1ns | 32~64 KB (코어당) | CPU 내부, 코어 전용 |
| L2 캐시 | ~3~10ns | 256 KB~1 MB (코어당) | CPU 내부 또는 근접 |
| L3 캐시 | ~10~30ns | 수 MB~수십 MB | 코어 간 공유 |
| 메인 메모리 (RAM) | ~50~100ns | 수 GB~수백 GB | DRAM, 휘발성 |
| SSD | ~10~100μs | 수백 GB~수 TB | 비휘발성, 블록 접근 |
| HDD | ~5~10ms | 수 TB | 비휘발성, 기계적 |
레지스터와 HDD의 접근 지연 시간 차이는 약 천만 배입니다.
이 차이 때문에 CPU는 데이터를 곧바로 보조기억장치에서 읽지 않고, 레지스터·캐시·메인 메모리·보조기억장치로 이어지는 계층 구조를 사용합니다.
캐시의 동작 원리
캐시는 이 엄청난 속도 차이를 감추기 위한 장치입니다.
CPU가 메모리의 데이터를 읽으면, 그 데이터와 주변 데이터가 캐시에 복사됩니다.
다음에 같은 데이터나 근처 데이터에 접근하면 느린 메모리 대신 빠른 캐시에서 읽습니다.
캐시가 효과적인 이유는 지역성(Locality) 원리 때문입니다.
시간적 지역성(Temporal Locality): 최근에 접근한 데이터에 다시 접근할 가능성이 높습니다.
루프에서 같은 변수를 반복적으로 사용하면, 첫 번째 접근 후 캐시에 올라가고 이후 접근에서 캐시를 활용할 가능성이 높아집니다. 용량·충돌·다른 코어의 쓰기 등으로 캐시 미스도 발생할 수 있습니다.
공간적 지역성(Spatial Locality): 접근한 데이터 근처의 데이터도 곧 접근할 가능성이 높습니다.
배열을 순서대로 순회하면, 첫 번째 원소를 읽을 때 근처 원소들도 함께 캐시에 올라오므로 같은 캐시 라인 안의 다음 원소를 빠르게 읽을 가능성이 높아집니다.
지역성은 실무의 성능에도 영향을 줍니다.
배열과 링크드 리스트의 성능 차이를 예로 들어보겠습니다.
/* 배열: 메모리에 연속 배치 → 공간적 지역성 높음 */
int arr[1000000] = {0};
long sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += arr[i]; /* 캐시 히트율 높음 */
}
/* 연결 리스트: 노드가 메모리에 흩어져 있음 → 캐시 미스 빈번 */
struct Node { int val; struct Node *next; };
struct Node *cur = head;
long sum2 = 0;
while (cur) {
sum2 += cur->val; /* 캐시 미스 빈번 */
cur = cur->next;
}위 코드는 접근 패턴을 비교하는 조각입니다. 연결 리스트의 head·노드 초기화는 생략되어 있으므로 독립 실행 코드나 벤치마크가 아닙니다. 같은 데이터 양에서도 실제 차이는 배치·작업·컴파일러에 따라 달라집니다.
캐시 라인 조건을 바꿔 보기: 4바이트 정수가 64바이트 캐시 라인 경계에 맞춰 연속 배치되어 있고, 이 라인들은 처음에는 캐시에 없다고 가정합니다. 16개 정수를 순서대로 읽으면 필요한 데이터가 한 라인에 들어가지만, 17개째까지 읽으면 두 번째 라인도 필요합니다. 이는 필요한 라인의 수를 계산한 것이며, 실제 미스 횟수는 미리 읽기·충돌·다른 접근 등의 영향까지 측정해야 합니다.
독립 변형: 16개 정수의 첫 원소가 라인 시작점에서 4바이트 뒤에 놓였다면 몇 개의 라인에 걸칠까요? 원소 수가 같다는 이유로 위의 한 라인 결과를 그대로 사용할 수 있는지 설명해 보세요.
배치 경계로 확인
첫 라인에는 offset 4부터 60까지의 정수 15개가 놓이고, 마지막 정수는 다음 라인 offset 0에 놓입니다. 따라서 두 라인에 걸칩니다. 이 경우에도 합계와 원소 수는 동일하며 배치가 바뀐 것입니다. 캐시 라인의 크기·정렬 조건 없이 연결 리스트나 배열의 실제 실행 시간을 확정할 수는 없습니다.
알고리즘의 시간 복잡도(Big-O)가 같더라도, 캐시 친화적인 자료 구조가 실제 실행 시간에서 유리합니다.
Big-O가 같은데 왜 하나가 훨씬 빠르지?라는 질문에 대한 답이 바로 캐시 효과입니다.
캐시 일관성 문제
캐시의 코어 전용·공유 구성은 CPU 설계에 따라 다릅니다. 예를 들어 Arm Cortex-A53 사양은 코어별 L1과 코어들이 공유하는 L2를 설명합니다. 위 표의 L2 용량에 붙은 코어당 표기는 L2가 코어 전용인 구성의 예시이며, 아래 일관성 사례는 두 코어의 전용 캐시에 같은 데이터의 사본이 들어 있는 상황입니다.
코어 0이 변수 x를 수정하고 자신의 캐시에 반영했는데, 코어 1의 캐시에는 x의 이전 값이 남아 있으면 문제가 됩니다.
이것이 캐시 일관성(Cache Coherence) 문제입니다.
하드웨어는 MESI 프로토콜 같은 캐시 일관성 프로토콜로 이 문제를 해결합니다.
한 코어가 데이터를 수정하면, 다른 코어의 캐시에 있는 해당 데이터를 무효화(Invalidate)합니다.
이 프로토콜이 성능에 영향을 주기 때문에, 멀티스레드 프로그래밍에서 스레드들이 서로 다른 변수를 수정하더라도 같은 캐시 라인에 놓여 있으면 False Sharing으로 성능이 저하될 수 있습니다. 같은 변수에 대한 실제 공유·경합과 구분합니다.
4장에서 스레드를 다룰 때 이 주제가 다시 등장합니다.
버스 시스템
버스는 컴퓨터 구성 요소 사이에 데이터를 전달하는 전기적 통로입니다.
공유 버스 방식에서는 한 번에 하나의 장치만 데이터를 보낼 수 있으므로, 여러 장치가 동시에 사용하려 하면 충돌이 발생합니다.
버스 사용의 중재는 주로 하드웨어가 수행하고, OS는 요청 스케줄링과 장치 설정으로 부하에 영향을 줍니다.
전통적 버스는 세 종류입니다.
- 주소 버스(Address Bus): CPU가 메모리나 I/O 장치의 특정 위치를 지정합니다. 바이트 주소가 32비트이면 바이트(4 GiB)를 표현합니다. 프로세스의 가상 주소 폭과 물리 주소 버스 폭은 다르며, 실제 사용자 공간 한계는 OS·ABI에 따라 달라집니다. 64비트 가상 주소의 이론적 공간은 16 EiB이고 구현이 사용하는 비트 수는 아키텍처마다 다릅니다.
- 데이터 버스(Data Bus): 실제 데이터가 오가는 경로입니다. 데이터 버스의 폭이 넓을수록 한 번에 더 많은 데이터를 전송할 수 있습니다.
- 제어 버스(Control Bus): 읽기/쓰기 구분, 인터럽트 신호, 클록 신호 같은 제어 정보를 전달합니다.
현대 컴퓨터에서는 단일 공유 버스 대신 포인트-투-포인트(Point-to-Point) 연결이 주류입니다.
Intel의 QPI(QuickPath Interconnect), AMD의 Infinity Fabric이 대표적입니다.
CPU와 메모리가 전용 채널로 직접 연결되어, 공유 버스 병목을 완화합니다. 전용 채널에도 대역폭·경합의 한계는 있습니다.
I/O 장치는 PCIe(Peripheral Component Interconnect Express) 버스로 연결됩니다.
PCIe는 레인(Lane) 개수에 따라 대역폭이 달라지며, 그래픽 카드(x16), SSD(NVMe, x4), 네트워크 카드(x8) 등이 PCIe 슬롯에 연결됩니다.
I/O 장치와 컨트롤러
CPU가 I/O 장치와 직접 통신하지는 않습니다.
각 I/O 장치에는 장치 컨트롤러(Device Controller)가 있고, CPU는 컨트롤러의 레지스터에 명령을 쓰고 데이터를 읽습니다.
예를 들어 디스크에서 데이터를 읽으려면, CPU가 디스크 컨트롤러의 명령 레지스터에 섹터 N을 읽어라라고 쓰고, 컨트롤러가 물리적 디스크를 제어하여 데이터를 가져옵니다.
데이터가 준비되면 컨트롤러가 CPU에 인터럽트를 보내거나, CPU가 상태 레지스터를 확인합니다.
OS 커널의 디바이스 드라이버(Device Driver)가 이 컨트롤러를 다루는 소프트웨어입니다.
드라이버는 특정 하드웨어의 세부 사항을 알고 있으며, OS의 나머지 부분에는 표준화된 인터페이스를 제공합니다.
덕분에 OS의 파일 시스템 코드는 디스크에서 블록을 읽어라라는 일반적인 요청만 하면 되고, 그것이 SATA SSD인지 NVMe SSD인지 USB 외장 하드인지는 드라이버가 알아서 처리합니다.
Linux에서 lspci 명령어를 실행하면 시스템에 연결된 PCI 장치 목록을, lsblk를 실행하면 블록 디바이스(디스크) 목록을 확인할 수 있습니다.
dmesg는 커널이 부팅 시 하드웨어를 감지하고 드라이버를 로드하는 과정을 보여주므로, 하드웨어 문제를 진단할 때 가장 먼저 확인하는 명령어입니다.
다음 절에서는 CPU가 I/O 장치의 이벤트에 어떻게 반응하는지, 인터럽트의 동작 원리를 다루겠습니다.