icon

안동민 개발노트

2장 : 컴퓨터 시스템 구조

컴퓨터 하드웨어 구조


운영체제를 이해하려면, OS가 관리하는 하드웨어의 구조를 먼저 알아야 합니다. CPU가 명령어를 실행하는 방식, 메모리의 계층 구조, 데이터가 이동하는 버스 — 이런 기본 구조 위에서 OS의 모든 기능이 동작합니다.

OS를 배우면서 하드웨어까지 알아야 하나?라고 생각할 수 있습니다. 반드시 알아야 합니다. 프로세스가 왜 컨텍스트 스위칭에 비용이 드는지를 이해하려면 레지스터가 무엇인지 알아야 합니다. 가상 메모리가 왜 필요한지를 이해하려면 메모리의 물리적 한계를 알아야 합니다. 캐시가 왜 존재하는지를 이해하려면 메모리와 CPU의 속도 격차를 알아야 합니다. 하드웨어 구조를 모르는 상태에서 OS 개념을 외우는 것은 를 빠뜨린 채 무엇만 아는 것입니다.


폰 노이만 아키텍처

현대 컴퓨터의 대부분은 폰 노이만 아키텍처(Von Neumann Architecture)를 따릅니다. 핵심 아이디어는 놀라울 정도로 단순합니다. 프로그램과 데이터를 같은 메모리에 저장한다.

이것이 왜 혁명적이었을까요? 이전의 컴퓨터(ENIAC 등)는 프로그램을 물리적 배선으로 구현했습니다. 덧셈 프로그램을 실행하려면 수천 개의 스위치와 케이블을 특정 패턴으로 연결해야 했고, 곱셈 프로그램으로 바꾸려면 배선을 전부 다시 해야 했습니다. 프로그램 교체에 며칠이 걸릴 수도 있었습니다.

폰 노이만의 제안은 프로그램도 숫자의 나열(데이터)이니까, 메모리에 저장하면 된다는 것이었습니다. 프로그램을 바꾸고 싶으면 메모리의 내용만 교체하면 됩니다. 이 아이디어가 범용 컴퓨터(General-Purpose Computer)의 토대입니다.

이 구조의 구성 요소는 세 가지입니다.

  • CPU(Central Processing Unit): 명령어를 실행하는 처리 장치. 프로그램의 모든 계산과 판단이 여기서 일어납니다.
  • 메모리(Memory): 프로그램 코드와 데이터를 저장하는 공간. CPU가 직접 접근할 수 있는 전자적 저장 장치입니다.
  • I/O 장치: 외부와 데이터를 주고받는 장치. 키보드, 마우스, 디스크, 모니터, 네트워크 카드 등이 해당합니다.

이 세 가지가 버스(Bus)라는 데이터 전송 통로로 연결됩니다.

한 가지 근본적인 한계가 있습니다. 프로그램 코드와 데이터가 같은 메모리에 있으므로, 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) 주소를 가리킵니다. 함수 호출, 로컬 변수, 반환 주소가 모두 스택에 저장됩니다.
  • 프로세서 상태 레지스터(PSW/FLAGS): 직전 연산의 결과에 대한 상태 비트(제로, 캐리, 오버플로 등)와 인터럽트 활성화 여부, 현재 실행 모드(사용자/커널) 등을 담고 있습니다.

OS가 프로세스를 전환(Context Switch)할 때, 현재 프로세스의 모든 레지스터 값을 저장하고, 다음 프로세스의 저장된 레지스터 값을 복원합니다. 레지스터의 수가 많을수록 저장/복원할 데이터가 많아지므로 컨텍스트 스위칭의 비용이 증가합니다. 5장에서 이 과정을 더 자세히 다릅니다.

명령어 사이클 (Instruction Cycle)

CPU는 명령어 사이클을 끊임없이 반복합니다.

  1. Fetch: PC가 가리키는 메모리 주소에서 명령어를 읽어옵니다.
  2. Decode: 명령어를 해석하여 어떤 연산을 수행할지, 피연산자가 무엇인지 파악합니다.
  3. Execute: ALU가 연산을 수행하거나, 메모리 접근이 이루어집니다.
  4. Write-back: 결과를 레지스터나 메모리에 저장합니다.

이 사이클이 초당 수십억 번 반복됩니다. 3GHz CPU는 1초에 30억 번의 클록 사이클을 실행합니다. 단순한 명령어는 한 클록에 실행될 수 있고, 복잡한 명령어는 여러 클록이 필요합니다.

현대 CPU는 이 과정을 파이프라이닝(Pipelining)으로 중첩합니다. 명령어 1을 Execute하는 동안 명령어 2를 Decode하고 명령어 3을 Fetch합니다. 마치 세탁소에서 세탁기, 건조기, 다림질을 동시에 돌리는 것과 같습니다. 파이프라이닝 덕분에 매 클록마다 하나의 명령어가 완료될 수 있습니다.

하지만 if문(조건 분기)을 만나면 파이프라인에 문제가 생깁니다. 조건의 결과가 나오기 전에는 다음 명령어가 무엇인지 알 수 없기 때문입니다. 이를 해결하기 위해 CPU는 분기 예측(Branch Prediction)을 하는데, 예측이 틀리면 파이프라인을 비우고 다시 시작해야 합니다. 이것이 분기가 많은 코드가 느린 이유 중 하나이며, 성능 최적화를 다루는 책에서 자주 언급되는 주제입니다.


메모리 계층 구조

컴퓨터의 저장 장치는 속도와 용량의 트레이드오프를 가집니다. 빠를수록 비싸고, 비쌀수록 용량이 작습니다. 이 물리적 제약이 메모리 계층 구조를 만듭니다.

계층속도용량특성
레지스터~0.3ns수십~수백 바이트CPU 내부, 가장 빠름
L1 캐시~1ns32~64 KB (코어당)CPU 내부, 코어 전용
L2 캐시310ns256 KB~1 MB (코어당)CPU 내부 또는 근접
L3 캐시1030ns수 MB~수십 MB코어 간 공유
메인 메모리 (RAM)50100ns수 GB~수백 GBDRAM, 휘발성
SSD10100μs수백 GB~수 TB비휘발성, 블록 접근
HDD510ms수 TB비휘발성, 기계적

레지스터와 HDD의 속도 차이는 약 천만 배입니다. 이 차이가 얼마나 큰지 체감하기 어렵습니다. 비유하면, 레지스터 접근이 책상 위의 메모지를 보는 것(0.3초)이라면, HDD 접근은 해외에서 택배를 기다리는 것(3개월)에 해당합니다.

캐시의 동작 원리

캐시는 이 엄청난 속도 차이를 감추기 위한 장치입니다. CPU가 메모리의 데이터를 읽으면, 그 데이터와 주변 데이터가 캐시에 복사됩니다. 다음에 같은 데이터나 근처 데이터를 접근하면 느린 메모리 대신 빠른 캐시에서 읽습니다.

캐시가 효과적인 이유는 지역성(Locality) 원리 때문입니다.

시간적 지역성(Temporal Locality): 최근에 접근한 데이터를 다시 접근할 가능성이 높습니다. 루프에서 같은 변수를 반복적으로 사용하면, 첫 번째 접근 후 캐시에 올라가고 이후 접근은 모두 캐시에서 처리됩니다.

공간적 지역성(Spatial Locality): 접근한 데이터 근처의 데이터도 곧 접근할 가능성이 높습니다. 배열을 순서대로 순회하면, 첫 번째 원소를 읽을 때 근처 원소들도 함께 캐시에 올라오므로 이후 접근이 모두 캐시 히트가 됩니다.

이것이 실무에서 의미하는 바가 있습니다. 배열과 링크드 리스트의 성능 차이를 예로 들어보겠습니다.

배열 순회 vs 연결 리스트 순회
/* 배열: 메모리에 연속 배치 → 공간적 지역성 높음 */
int arr[1000000];
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;
}

같은 양의 데이터를 순회하더라도, 배열이 링크드 리스트보다 수배~수십 배 빠를 수 있습니다. 알고리즘의 시간 복잡도(Big-O)가 같더라도, 캐시 친화적인 자료 구조가 실제 성능에서 압도적으로 유리합니다. Big-O가 같은데 왜 하나가 훨씬 빠르지?의 답이 바로 캐시 효과입니다.

캐시 일관성 문제

멀티코어 CPU에서는 각 코어가 자체 L1/L2 캐시를 가집니다. 코어 0이 변수 x를 수정하고 자신의 캐시에 반영했는데, 코어 1의 캐시에는 x의 이전 값이 남아 있으면 문제가 됩니다. 이것이 캐시 일관성(Cache Coherence) 문제입니다.

하드웨어는 MESI 프로토콜 같은 캐시 일관성 프로토콜로 이 문제를 해결합니다. 한 코어가 데이터를 수정하면, 다른 코어의 캐시에 있는 해당 데이터를 무효화(Invalidate)합니다. 이 프로토콜이 성능에 영향을 주기 때문에, 멀티스레드 프로그래밍에서 여러 스레드가 같은 메모리를 빈번하게 수정하면 (False Sharing) 성능이 크게 저하될 수 있습니다. 4장에서 스레드를 다룰 때 이 주제가 다시 등장합니다.


버스 시스템

버스는 컴퓨터 구성 요소 사이에 데이터를 전달하는 전기적 통로입니다. 공유 버스 방식에서는 한 번에 하나의 장치만 데이터를 보낼 수 있으므로, 여러 장치가 동시에 사용하려 하면 충돌이 발생합니다. 이 충돌을 관리하는 것도 OS의 역할 중 하나입니다.

전통적 버스는 세 종류입니다.

  • 주소 버스(Address Bus): CPU가 메모리나 I/O 장치의 특정 위치를 지정합니다. 주소 버스가 32비트면 2322^{32} = 4GB의 주소 공간을 표현할 수 있습니다. 32비트 OS에서 프로세스당 최대 4GB 메모리를 쓸 수 있는 이유입니다. 64비트 시스템은 이론적으로 2642^{64}바이트(16 EB)를 표현할 수 있지만, 실제 구현은 48~57비트만 사용합니다.
  • 데이터 버스(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 장치의 이벤트에 어떻게 반응하는지, 인터럽트의 동작 원리를 다루겠습니다.

목차