(25.03.17)
[이것이 취업을 위한 컴퓨터 과학이다 with CS 기술 면접]의 [Chapter 2. 컴퓨터구조]를 읽고, [Computer Organization and Design, Risc-V Edition] 및 구글링을 통해 보충하여 정리한 내용
1. Levels of Program Code
-
High-Level Language: 인간이 작성하는 Java, C 등의 프로그래밍 언어이다. 프로그래밍 언어의 종류와 CPU 아키텍쳐에 따른 컴파일러를 사용하여 어셈블리어로 변환된다.
-
Assembly Language: CPU에 따라 사용하는 명령어 셋 (instruction set architecture, ISA)이 다르다. 따라서 CPU에 따른 어셈블러를 사용하여 기계어로 변환된다.
- ISA는 Software와 Hardware 사이에서 중요한 Interface 역할을 한다. 프로그래머는 하드웨어의 세부 사항을 몰라도 동작하게끔 할 수 있는 Abstraction이 이루어진 것이다.
- 어셈블리 코드를 살펴보면 프로그램은 명령어와 (레지스터에 저장된) 데이터로 구성된 것으로 이해할 수 있다.

-
Hardware representation: 컴퓨터(CPU)가 이해할 수 있는 0과 1로만 구성된 바이너리 코드이다.
2. 컴퓨터의 구성부품
- 컴퓨터는 CPU, 메인 메모리(RAM), 보조기억장치, 입출력장치로 구성된다.

- 시스템 버스를 통해 각 부품들 간에 정보(데이터)를 주고 받는다.
1) CPU
- 명령어를 이해하고 실행하는 주체이다. CPU의 종류에 따라 명령어의 종류와 처리의 양상(ISA)이 달라진다.
- CPU가 한 번에 처리할 수 있는 데이터의 크기를 워드라고 한다. CPU 내부의 레지스터 하나는 워드 크기만큼의 데이터를 저장할 수 있으며, 현대 대부분의 컴퓨터의 워드 크기는 32비트 또는 64비트이다.
2) 메모리
- CPU 내부의 레지스터, CPU와 메인 메모리 사이에 존재하는 캐시 메모리, 메인 메모리는 휘발성 저장장치이다.
- 레지스터는 데이터와 명령어를 처리하는 과정(연산)에서 중간값을 임시적으로 저장하는데 사용한다. 레지스터의 종류에는 프로그램 카운터, 명령어 레지스터, 범용 레지스터, 플래그 레지스터 등이 여러 가지가 있다.
- 메인 메모리에는 현재 실행중인 프로그램에 대한 모든 정보를 저장한다.
- 보조기억장치는 비휘발성 저장장치이다.
- 보조기억장치에는 보관할 프로그램을 저장해둔다. CPU에서 보조기억장치에 보관된 프로그램을 실행하려면 메인 메모리로 복사해온 후에 실행할 수 있다.
- 뒤에서 언급된 저장장치일수록 접근하는데 속도가 느리고, 용량이 크며 비용이 저렴하다.
3) 입출력장치와 보조기억장치
- 보조기억장치는 메모리를 보조하는 임무를 수행하는 입출력장치의 일종으로 이해할 수 있다. 컴퓨터 내부와 정보를 주고 받는 방식이 유사하기 때문에, 입출력장치와 보조기억장치를 묶어 주변장치라고 하기도 한다.
3. CPU
- 앞에서 본 어셈블리어와 같이, 하나의 프로그램은 데이터를 포함하고 있는 명령어 집합이라고 이해할 수 있다. 코드의 흐름에 따라 명령어를 하나씩 처리해 나간다. 이 흐름을 확실하게 이해하고 넘어가자.

- 위 그림은 프로그램이 실행될 동안의 메모리 구조이다. Text 부분에 Program code가 저장되어 있는데, 명령어들이 고유의 주소를 가지고 순서대로 저장되어있다.
- CPU 내부 레지스터 중 명령어와 관련된 레지스터는 프로그램 카운터와 명령어 레지스터가 있다.
- 프로그램 카운터에는 다음으로 읽어들일 명령어의 메모리 주소를 저장해둔다.
- 명령어 레지스터에는 방금 읽어들인 명령어를 저장해둔다.
- CPU는 프로그램 카운터에 저장된 주소값을 가지고 메모리에 접근하여 명령어를 명령어 레지스터로 가져오는 인출 사이클, 가져온 명령어를 ALU 등을 이용하여 처리하는 실행 사이클을 반복한다. 가져온 명령어가 메모리 접근을 필요로 할 때에는 인출 사이클 이후에 실행 사이클로 가기 전에 간접 사이클이 추가된다.
- CPU의 작업을 방해하는 신호인 인터럽트는 크게 동기 인터럽트(Synchronous interrupts)와 비동기 인터럽트(Asynchronous interrupts)로 나눌 수 있다.
- 프로그램 실행 중 인터럽트가 발생할 경우 실행 사이클에서 인터럽트 사이클로 이동하고 인출 사이클로 이동하는 경로가 추가된다.
1) 동기 인터럽트
- 동기 인터럽트는 CPU에 의해 발생하는 인터럽트로, 예외(Exception)라고도 부른다. CPU는 예외 발생 시 하던 일을 즉시 중단하고 예외를 처리한다. 예외의 종류에 따라 예외 처리 후의 동작이 다르다.
- 폴트: 예외가 발생한 명령어부터 다시 실행한다.
- (예) 페이지 폴트: 명령어를 실행하기 위해 필요한 데이터가 메모리가 아닌 보조기억장치에 존재하는 예외로, 예외 발생시 보조기억장치로부터 메모리로 데이터를 가져온 후 다시 해당 명령어를 실행한다.
- 트랩: 예외가 발생한 명령어의 다음 명령어부터 실행한다.
- 중단: 프로그램을 강제로 중단시킨다.
- 소프트웨어 인터럽트: 시스템 콜이 발생했을 때 발생하는 예외이다.
- 발생한 예외의 종류에 따른 인터럽트 핸들러를 실행한다.
2) 비동기 인터럽트
- 비동기 인터럽트는 주로 입출력장치에 의해 발생하는 인터럽트로, 하드웨어 인터럽트라고도 부른다.
- 하드웨어 인터럽트는 CPU가 효율적으로 명령어를 처리할 수 있도록 한다. 예를 들어, 입출력 장치에 명령하고 입출력 장치의 작업이 끝날 때 까지 기다리고 있는 것이 아니라 다른 작업을 수행하고 있으면 입출력 장치의 알림, 즉 하드웨어 인터럽트를 통해 작업이 완료되었는지 확인할 수 있다. 하드웨어 인터럽트 덕분에 입출력장치의 느린 작업 속도를 기다리지 않고 그동안 효율적으로 다른 작업을 수행하고 있을 수 있는 것이다.
- CPU가 하드웨어 인터럽트를 처리하는 과정은 다음과 같다.
- 입출력장치가 CPU에게 인터럽트 요청 신호를 보낸다.
- CPU는 매 실행 사이클이 끝나고 인출 사이클에 들어가기 전, 인터럽트 요청 신호가 들어왔는지 확인한다.
- CPU는 인터럽트 요청을 확인하고, CPU 내부의 플래그 레지스터 중 인터럽트 플래그를 확인하여 현재 인터럽트를 받을 수 있는 상태인지 확인한다.
- 하드웨어 인터럽트는 막을 수 있는 인터럽트(Maskable Interrupt)와 막을 수 없는 인터럽트(Non Maskable Interrupt)로 나누어지는데, 막을 수 없는 인터럽트의 경우 인터럽트 플래그가 불가능의 상태이더라도 반드시 처리해야 한다.
- 인터럽트를 받을 수 있는 상태이면 CPU는 현재 작업 상태를 메모리의 스택 영역에 저장해두고(백업), 입출력장치는 CPU로 인터럽트 벡터를 전달한다.
- 인터럽트 벡터는 해당 인터럽트에 대해 CPU가 실행해야 할 핸들러, 즉 인터럽트 서비스 루틴이 저장된 메모리 주소를 포함하고 있다.
- CPU는 인터럽트 벡터를 참조하여 인터럽트 서비스 루틴을 실행한다.
- 인터럽트 서비스 루틴 실행이 끝나면 스택 영역에 저장해둔 작업을 복구하고 프로그램 실행을 재개한다.
4. CPU 성능 향상을 위한 설계
1) CPU 클럭 속도를 높인다.
- CPU가 명령어 하나를 실행하는데 걸리는 시간(Execution Time)은 CPI(Clock Per Instruction)를 클럭 속도로 나눔으로써 정해진다. 따라서 CPI 값을 낮추거나 클럭 속도를 높임으로써 성능을 향상시킬 수 있다.
- 명령어의 종류와 CPU 내부 설계에 따라 CPI 값이 다르다.
- 클럭 속도를 과도하게 높이면 발열 문제가 생기므로 클럭 속도에만 의존해서는 안된다.
2) 멀티 코어
- CPU가 명령어를 읽어 들이고, 해석하고, 실행하는데 필요한 부품의 집합은 ALU, 제어장치, 레지스터 등이 있다. 이 집합을 코어라고 이름 붙이고, 하나의 CPU가 n개의 코어를 가진다면(멀티코어 CPU), 명령어를 처리하는 주체가 n개인 것이다.

3) 하드웨어 스레드와 소프트웨어 스레드
- 스레드(thread)는 실행 흐름의 단위를 의미한다.
- 하드웨어 스레드는, 하드웨어 설계에 의해 하나의 코어가 동시에 여러개의 명령어를 병렬적으로 처리할 수 있는 것이다 (병렬성). 예를 들어, 2코어 4스레드 CPU는 각 코어가 하드웨어 스레드를 2개씩 가짐을 의미한다.
- 1코어 1스레드 CPU여도 프로그램을 실행할 때 여러 개의 실행 흐름을 가질 수 있다. 소프트웨어 스레드는 프로그램 코드 내부에서 생성된 스레드이다. 실제로 같은 시간에 여러 작업을 동시에 처리하는 것이 아니라, 각 작업을 여러 작업 단위로 나눈 후, 스케줄링과 (소프트웨어) 스레드 간의 빠른 전환을 통해 동시에 여러(종류) 작업이 처리되는 것처럼 보이도록 하는 것이다 (동시성).
- 병렬성(Parallelism)과 동시성(Concurrency)의 개념을 다음 그림을 통해 쉽게 이해할 수 있다.

4) 명령어 파이프라이닝
-
하나의 스레드에서도 여러 개의 명령어를 동시에 다룰 수 있도록 하는 것이 명령어 파이프라이닝이다.
- CPU -> 코어 -> 스레드 -> 사이클 로의 단위 흐름을 이해하자.
-
하나의 명령어가 처리되는 과정은 다음과 같이 4단계로 나눌 수 있다.
- 명령어 인출(Fetch): PC에 저장된 주소값에 접근하여 처리할 명령어를 명령어 레지스터로 가져온다.
- 명령어 해석(Decode): 명령어 레지스터에 가져온 명령어를 읽고 해석한다.
- 명령어 실행(Execute): 명령어를 처리한다.
- 결과 저장(Write Back): 명령어 수행 후 (필요한 경우) 레지스터에 기록한다.
-
같은 단계가 아니라면 동시에 여러 명령어를 처리할 수 있다 (단계별로 사용하는 부품이 다르기 때문이다). Data access 단계를 포함하여 나타낸 파이프라이닝 은 다음과 같다.

-
Pipeline Hazard: 파이프라이닝이 실패하여 결과적으로 성능 향상에 도달하지 못하는 경우로, 3가지 종류가 존재한다.
- Data Hazard: 명령어 간의 데이터 의존성이 존재하는 경우
- 앞 명령어가 특정 레지스터에 저장된 값을 바꾼 후 WB하기 전에 뒤 명령어가 아직 바뀌지 않은 값을 레지스터에서 꺼내와 사용하는 경우
- Control Hazard: 프로그램 카운터가 순차적인 흐름을 가지지 않은 경우
- Structural Hazard: 서로 다른 명령어가 다른 단계임에도 불구하고 동시에 같은 CPU 부품을 사용하려고 하는 경우
5. 메모리
1) RAM: Random Access Memory
- 컴퓨터의 메인 메모리로 사용되는 하드웨어로, 휘발성 기억장치이다.
- Random Access, 즉 임의 접근은 특정 주소값에 접근하려면 메모리의 처음 주소에서부터 순차적으로 접근해나가야 하는 순차 접근과 다르게, 곧장 특정 주소값에 방문할 수 있는 특성이다.
- RAM의 종류에는 DRAM, SRAM, SDRAM, DDR SDRAM 등이 있다.
- DRAM: DRAM의 D는 Dynamic을 의미하며, 시간이 지나면 저장된 데이터가 점차 사라진다. 따라서 데이터의 소실을 막기 위해 주기적으로 데이터를 재활성화 하는 작업이 필요하다.
- SRAM: SRAM의 S는 Static을 의미하며, DRAM과 다르게 시간이 지나더라도 데이터가 사라지지 않는다. 물론 휘발성 저장장치인 RAM의 일종이므로 전원이 꺼지면 데이터가 사라지는 것은 당연하다. SRAM은 DRAM보다 속도가 빨라 캐시 메모리에 사용한다.
- SDRAM: Synchronous Dynamic RAM을 의미하며, 클럭 타이밍에 맞춰 CPU와 정보를 주고 받을 수 있어 효율적이다.
- DDR SDRAM: Double Data Rate SDRAM으로, 대역폭의 너비가 SDRAM의 2배이므로 전송 속도가 약 2배 빠르다. 마찬가지로 DDR SDRAM의 대역폭의 2배 너비를 가지는 DDR2 SDRAM, DDR2 SDRAM의 대역폭의 2배 너비를 가지는 DDR4 SDRAM이 존재한다. 최근 컴퓨터에 사용되는 메인메모리는 대부분 DDR4 SDRAM이다.
2) 메모리의 데이터 저장 방식
- 메모리는 워드(32비트=4바이트 또는 64비트=8바이트) 단위로 데이터를 받아들이지만, 저장할 때는 대부분 바이트 단위로 저장한다. 하나의 데이터를 여러 주소에 걸쳐 저장하는 것이다.
- 하나의 데이터를 저장하는 방식에는 순서에 따라 빅 엔디안과 리틀 엔디안이 있다. 예시로 16진수 1A2B3C4D (4바이트)가 각 방식에 따라 어떻게 저장되는지 확인하자.
- 빅 엔디안: MSB(Most Significant Bit), 즉 가장 큰 값을 나타내는 비트부터 먼저(낮은 주소에) 저장한다. 낮은 주소부터 1A, 2B, 3C, 4D가 저장된다.
- 리틀 엔디안: LSB(Least Significant Bit), 즉 가장 작은 값을 나타내는 비트부터 먼저 저장한다. 낮은 주소부터 4D, 3C, 2B, 1A가 저장된다.
3) 캐시 메모리
- 캐시 메모리는 앞에서 언급했듯, CPU와 메모리 사이에 존재하는 SRAM 기반의 저장장치이다.
- CPU 코어와 가까운 순서대로 L1, L2, L3 Cache로 분류되며 코어와 가까울 수록 속도가 빠르지만 용량이 작다. 보통 L1, L2 Cache는 CPU 내부, L3는 외부에 존재한다.
- 캐시에는 참조 지역성의 원리에 따라 CPU가 사용할 법한 데이터들을 메모리에서 가져와 저장해둔다. 하지만 항상 예측이 맞을 수는 없다. 예측이 맞은 정도를 수치화하여 캐시 적중률로 나타내는데, 이는 필요한 데이터를 캐시에서 찾은 경우 Cache hit, 캐시에서 찾지 못해 메모리에 접근한 경우를 Cache miss로 정의하여 (Cache hit)/(Cache hit+Cache miss) 로 계산한다.
- 참조 지역성의 원리에는 시간 지역성, 공간 지역성이 있으며 시간 지역성은 최근에 접근한 메모리 공간에 다시 접근하려는 경향이 있다는 것이고, 공간 지역성은 최근에 접근한 메모리 공간과 가까운 곳에 다시 접근하려는 경향이 있다는 것이다.
- CPU에서 캐시 메모리에 저장된 값을 업데이트하면 메모리에 저장된 원본 값도 업데이트 해주어야 한다.
- 즉시 쓰기(write-through): 값을 쓰는 명령을 처리할 때 캐시 메모리와 함께 즉시 메인 메모리의 값도 업데이트한다. 항상 캐시 메모리와 메인 메모리가 동기화된 상태를 유지할 수 있어 일관성을 가질 수 있으나, 쓰기가 일어날때마다 매번 메모리 접근이 일어나 비효율이 발생한다.
- 지연 쓰기(write-back): 값을 쓰는 명령을 처리할 때 캐시 메모리의 값만 업데이트하고, 추후 수정된 값들을 한번에 메모리에 반영한다. 메모리 접근 횟수는 줄일 수 있지만, 일관성을 보장할 수 없다는 단점이 있다.
6. 보조 기억 장치와 입출력장치
- 보조 기억 장치로는 하드 디스크 드라이브(HDD), 플래시 메모리 기반 저장장치 중 하나인 SSD 등이 사용된다. 보조 기억 장치는 데이터를 안전하고 빠르게 다룰 수 있어야 한다.
1) RAID: Redundant Array of Independent Disks
- RAID는 여러 개의 독립된 보조기억장치를 마치 하나의 보조기억장치처럼 사용하여 안전성과 성능을 높이는 기술이다. 구성 방법에 따라 RAID0, RAID1, RAID4, RAID5, RAID6 등의 레벨이 존재한다.

- RAID0: 데이터를 단순하게 여러 보조기억장치에 나누어(stripping) 저장한다.
- 동시에 읽고 쓸 수 있어 입출력 속도가 빠르다. (RAID의 기본 장점)
- 하나의 장치에 문제가 생기면 데이터가 불완전해지며 복구할 수 없다.
- RAID1: 완전한 복사본을 만들어 저장한다. 하나의 장치에 문제가 생기더라도 복사본의 데이터를 참조하면 복구할 수 있다.
- 쓸 때는 원본과 복사본 모두에 써야 하므로 쓰기 속도가 느리다. 또한 실제로 저장 가능한 데이터의 수가 적어진다.

- RAID4: 데이터를 나누어 저장하되 각 데이터의 패리티 정보를 저장하는 디스크를 따로 하나 둔다.
- 복사본을 두지 않더라도 패리티 정보를 이용하여 데이터를 복구할 수 있다.
- 쓰기 작업이 일어날 때마다 패리티 정보를 업데이트 해야 하므로 패리티 정보를 저장하는 디스크 하나가 매우 바빠진다. 이를 병목 현상이라고 한다.

2) 장치 컨트롤러와 장치 드라이버
- 보조기억장치와 입출력장치, 즉 외부 장치는 종류에 따라 작동 방식이 다르기 때문에 CPU가 직접적으로 외부 장치와 정보를 주고 받는 것에는 무리가 있다. 외부 장치마다 CPU와의 소통을 중개하는 역할의 하드웨어인 장치 컨트롤러가 존재한다. CPU는 장치 컨트롤러와 상호작용함으로써 외부장치를 작동시킬 수 있는 것이다.
- 장치 컨트롤러마다 장치 컨트롤러를 작동시킬 수 있는 프로그램인 장치 드라이버가 존재한다. CPU는 해당 프로그램을 실행함으로써 장치 컨트롤러를 조작해 외부장치까지 작동하게 하는 것이다.
- 대중적인 장치 드라이버의 경우 운영체제에 포함되어 있다. 그렇지 않은 경우에는, 별도의 장치 드라이버를 설치하여야 장치를 사용할 수 있다.
- 장치 드라이버는 추상화를 수행한다. 예를 들어, 키보드의 입력을 받아오고 싶을 때, 사용자는 키보드의 장치 컨트롤러에게 어떻게 명령해야할지를 모를 뿐더러 키보드의 브랜드마다 그 방법이 다를 것이다. 사용자가 고수준 함수를 사용하여 키보드의 입력을 받아오는 코드를 작성하면, 이를 장치 드라이버가 해석하여 장치 컨트롤러에게 올바른 동작을 시킨다. (운영체제에서 더 명확한 개념을 학습하자.)
- 장치 드라이버가 실행될 때, CPU가 데이터를 처리하는 방식에는 프로그램 입출력, 인터럽트 기반 입출력, DMA 입출력의 3가지 종류가 있다.
3) 프로그램 입출력
- 프로그램 입출력은 입출력 동작이 필요할 때 직접 CPU가 명령을 내려 입출력 작업을 수행하게 한다. 예를 들어, 사용자 입력이 필요할 때 scanf 함수 호출을 통해 키보드의 입력을 기다린다. 키보드의 입력이 들어올 때까지 다른 작업을 수행하지 못하고 계속 기다린다 (폴링).
4) 인터럽트 기반 입출력
- 앞에서 살펴본 (하드웨어) 인터럽트는 입출력장치의 작업이 끝날 때 까지 다른 작업을 수행하고, 인터럽트 발생 시 올바른 인터럽트 서비스 루틴이 실행되게 한다.
- 여러 종류의 인터럽트가 동시다발적으로 발생한 상황(다중 인터럽트)을 고려해보자. 설정(인터럽트 네스팅 허용여부)에 따라 다르게 동작할 수 있다.
- 동시에 여러 인터럽트가 발생했을 경우에는, 우선순위가 높은 인터럽트의 서비스 루틴부터 실행된다.
- 인터럽트 비트를 비활성화한 상태로 특정 인터럽트의 인터럽트 서비스 루틴이 실행하는 경우에는 추가적으로 발생한 인터럽트가 maskable이라면 처리 중이던 인터럽트보다 우선순위가 높더라도 처리 중이던 인터럽트를 모두 처리 한 후에 처리된다. non-maskable 이라면 우선순위가 높은 경우 해당 인터럽트의 서비스 루틴을 실행 한 후 처리 중이던 루틴을 마저 실행한다.
- 인터럽트 비트를 활성화해두고 처리중이었다면, 우선순위가 높은 인터럽트가 발생했을 경우 해당 인터럽트의 서비스 루틴을 실행한 후 처리 중이던 루틴을 마저 실행한다.
- PIC(Programmable Interrupt Controller) 라는 하드웨어를 사용하여 다중 인터럽트를 처리할 수 있다. PIC는 여러 장치 컨트롤러와 연결되어 각 장치 컨트롤러에서 보내오는 인터럽트 간의 우선순위를 판별한 후, CPU에게는 현재 처리해야 할 인터럽트 만을 알려주어 부담을 덜어준다.
5) DMA 입출력: Direct Memory Access
- 앞의 두 방법에서는, 예를 들어 입출력장치로부터 입력 받은 데이터를 메모리에 저장하기 위해서는, 입출력장치로부터 들어온 데이터를 CPU의 레지스터에 저장하고, 이 데이터를 다시 메모리에 저장해야 한다. 즉, 메모리와 입출력장치 간의 데이터 이동에 항상 CPU를 거쳐야 한다. 이와는 다르게 CPU를 거치지 않고도 메모리와 장치 컨트롤러 사이에서 데이터가 이동할 수 있게 하는 방식이다.

- 시스템 버스에 연결된 DMA Controller라는 하드웨어가 입출력 버스에서 입출력 장치들의 장치 컨트롤러와 연결된다. 해당 구조를 통해 입출력이 수행되는 과정은 다음과 같다.
- CPU가 DMA 컨트롤러에게 입출력장치의 주소와 수행할 연산에 관한 정보와 함께 입출력 작업을 명령한다.
- DMA 컨트롤러가 입출력 버스를 통해 장치 컨트롤러들과 소통하여 입출력 작업을 수행하여 메모리에 값을 쓰거나 읽는다.
- 입출력 작업이 끝나면 DMA 컨트롤러가 CPU에 하드웨어 인터럽트를 보내 작업이 끝났음을 알린다.