ALU와 플래그 레지스터
앞서 [1]장에서 Arithmetic and Logic Unit, Control Unit, Register 들로 이뤄진 CPU의 Core에 대해 이야기했었습니다.
이번에는 이러한 내용을 좀 더 자세히 알아보겠습니다.
*대학 전공수업에서는 실제 회로로 어떻게 구현하는지를 함께 수업하지만, 여기서는 그러한 내용은 생략하도록 하겠습니다.
위 그림은 ALU가 어떻게 정보를 받아들이고 내보내는지를 알 수 있는 그림입니다.
ALU는 레지스터를 통해 피연산자(오퍼랜드)를 받아들이고, CU로부터 수행할 연산이 무엇인지 제어 신호(Control Signal)를 받아들입니다. CU는 IR로부터 어떤 명령어를 ALU에게 전달해야 할 지를 받아들입니다. ALU가 주어진 명령어를 처리하면 이 또한 레지스터에 일시적으로 저장됩니다. (연산 결과를 레지스터가 아닌 메모리에 저장하는 것은 너무나 오래 걸립니다.)
물론 위 도식은 간단히 표현된 것일 뿐이고, 실제로는 훨씬 더 복잡합니다. ALU가 레지스터로 저장하는 정보는 연산의 결과값 뿐만이 아니라, 어떠한 '상태에 대한 정보' 또한 포함됩니다. 이러한 상태(Status) 정보는 플래그(Flag)라고 부르며, 대표적인 플래그들을 아래에 예시로 들겠습니다.
- 부호 플래그: 계산 결과가 양수인지, 0인지를 나타냅니다. 음수일 경우의 플래그가 1입니다.
- 제로 플래그: 계산 결과가 0인지를 나타냅니다. 0인 경우의 플래그가 1입니다.
- 캐리 플래그: 올림수나 빌림수가 발생한 경우(자릿수를 초과하거나 0 미만의 값이 된 경우) 캐리 플래그의 값을 1로 하여 알립니다.
- 오버플로우 플래그: 오버플로우(크기가 더 큰 값을 담으려하는 경우) 발생 시 1로 나타냅니다.
- 인터럽트 플래그: 인터럽트가 가능한지를 나타냅니다. 가능한 경우 1로 나타냅니다.
- 슈퍼바이저 플래그: 커널모드의 실행 여부를 알려줍니다. 커널 모드는 1로, 사용자 모드는 0으로 나타냅니다.
플래그 레지스터는 아래 사진과 같이 이러한 여러 플래그들을 담을 수 있는 레지스터입니다.
Control Unit과 IR, Flags
위에서 ALU와 레지스터가 어떻게 동작하는지 알아보았습니다.
이번에는 ALU에게 제어 신호를 보내는 Control Unit이 어떻게 동작하는지를 알아보겠습니다.
CU의 동작을 이해하기 위해 아래 사진을 먼저 이해하는 것이 좋습니다.
CPU에서 가장 중요한 부분이 ALU라고 생각하셨을 수도 있습니다. 하지만 실제로는 CU가 가장 정교하게 설계된 부품이라고 말할 수 있습니다. 위 사진을 보시면 왼쪽에서 플래그 신호들(플래그 레지스터에서 들어옵니다.)과 클럭 신호를 받는 것을 볼 수 있습니다. 위쪽에서는 명령어 레지스터에서 해석해야 할 명령어들을 받아들입니다. 제어 버스(control bus)에서 제어 신호를 받아들이기도 합니다. CPU에서 제어신호를 레지스터나 ALU, 제어 버스로 보내기도 합니다. 왜 CU가 가장 정교한 부분인지 느낌이 오시지 않나요?
레지스터
레지스터는 CPU내에서 가장 빠르게 정보를 저장하고 인출할 수 있는 저장공간입니다. CPU내에서 유닛들과 붙어있는 메모리다보니 빠르고 효율적이지만 그 용량을 늘리는 데 한계가 있는 저장공간입니다.
이러한 레지스터는 제조사별로 특정 목적에 따라서 그 설계와 배치가 달라지나, 현대 CPU 설계에서 대부분이 포함하는 레지스터에 대해서 알아보겠습니다.
- 프로그램 카운터 (명령어 포인터) → 메모리에서 가져올 명령어의 주소를 저장하는 레지스터
- 명령어 레지스터 → 메모리에서 읽은 명령어를 저장하는 레지스터
- 메모리 주소 레지스터 → 메모리의 주소를 저장하는 레지스터 (주소 버스에 주소값을 보낼 때 거쳐가는 레지스터)
- 메모리 버퍼 레지스터 → 메모리와 주고받을 데이터와 명령어를 저장하는 레지스터 (데이터 버스로 데이터 주고 받을 때 거쳐가는 레지스터)
- 범용 레지스터 → 데이터 및 주소를 모두 저장할 수 있는 레지스터
- 플래그 레지스터 → ALU 연산 결과에 따른 플래그를 저장하는 레지스터
- 스택 포인터 → 스택의 탑을 가리키는 레지스터 [for 스택 주소지정 방식]
- 베이스 레지스터 → 변위 주소 지정 방식에서 베이스 값을 저장하는 레지스터 [for 변위주소지정 방식]
위와 같은 8가지 레지스터는 대부분의 CPU 설계에 포함되어 있습니다.
명령어 사이클과 인터럽트
명령어를 처리하는 과정에는 정해진 흐름이 있고, CPU는 그 흐름을 반복해가며 명령어들을 처리해 나갑니다.
명령어 사이클은 이렇게 하나의 명령어를 처리하는 정형화된 흐름을 지칭하는 용어입니다.
다만 현실 세계의 문제들이 어떻게 발생할지 그 순서를 아는 것은 불가능합니다. 그래서 명령어를 처리하는 흐름이 중간에 끊기고 다른 명령어를 먼저 실행해야 하는 경우도 생길 수 있습니다. 이렇게 흐름이 중간에 끊기는 상황을 '인터럽트(Interrupt)'가 발생했다고 합니다.
뭔가를 어떻게 하면 더 잘 배울까를 고민할 때, '많이 인출하라'라는 조언을 자주 듣게 될 것입니다. 이러한 인출은 기억(저장)한 것을 꺼내어 보는 것으로 영어로 표현하면 fetch입니다. CPU가 메모리에 있는 명령어를 가져오기 위해서도 fetch 과정이 필요합니다. 이러한 흐름은 fetch cycle이라고 합니다.
가져온 명령어는 실행됩니다. 명령어를 실행하는 과정은 실행 사이클이며, 영어로 표현하면 execution cycle입니다. 제어장치가 명령어 레지스터에 담긴 값을 해석하고, 제어 신호를 발생시키는 단계가 execution cycle입니다.
이렇게 프로그램을 이루는 수많은 명령어에 대해 fetch & execution cycle이 반복되며 프로그램이 실행됩니다. 다만 메모리에서 단 한번 명령어를 가져와 실행하지 못하는 경우도 많습니다. CPU 레지스터의 종류에서 언급된 것처럼, 메모리에서 가져온 값이 명령어가 아니라 명령어의 주소일 수도 있습니다. 이렇게 간접적으로 주소가 지정된 경우 한번 더 메모리에 접근이 필요해집니다. 이러한 경우는 간접 사이클이라 하며, 영어로는 indirect cycle이라고 합니다.
Interrupt는 방해하다, 중단시키다 라는 의미입니다. 수행중인 작업이 방해를 받아 잠시 중단될 수 있습니다. 이러한 경우 Interrupt가 발생했다고 할 수 있습니다. 이러한 인터럽트는 먼저 처리해야하는 명령어가 발생한 경우가 있습니다.
인터럽트는 크게 동기 인터럽트와 비동기 인터럽트로 나눌 수 있습니다.
Synchronous interrupts는 CPU에 의해 발생하는 인터럽트입니다. 명령어를 수행하다가 예상치 못한 상황이 발생했을 때(오류 등) 동기 인터럽트가 발생합니다.
이런 점에서 동기 인터럽트는 exception이라고 부릅니다. synchronous interrupts라는 말보다 exception이 훨씬 자주 사용되는 표현입니다.
비동기 인터럽트는 주로 입출력장치에 의해 발생하는 인터럽트입니다. 입출력장치의 작업 완료 알림, 입출력장치에 어떤 입력이 들어온 경우에 CPU에게 관련 신호를 비동기 인터럽트를 통해 보냅니다.
이러한 비동기 인터럽트는 보통 Hardware Interrupts 라고 부릅니다. 하드웨어에 의해 발생하는 인터럽트이기 때문입니다.
하드웨어 인터럽트는 CPU를 더 효율적으로 쓰기 위해서 필요합니다. CPU에 비해 입출력장치는 너무도 느리기에 CPU가 입출력장치의 신호를 '기다리고' 있으면 엄청나게 비효율적인 낭비가 될 것입니다. CPU의 명령어 사이클을 더 효율적으로 활용하려면, 입출력장치에 신호를 주고, 나중에 처리 다하면 인터럽트로 그 내용을 받는 것이 좋을 것입니다.
이러한 인터럽트는 어떻게 처리할까요? 하드웨어 인터럽트에 대해 그 처리과정을 들여다보겠습니다.
- 입출력장치가 CPU에 인터럽트 요청 신호를 보냅니다.
- CPU는 실행 사이클이 끝나고 명령어를 인출하기 전에 항상 인터럽트를 확인합니다.
- CPU가 인터럽트 요청을 확인하면 인터럽트 플래그를 통해 현재 인터럽트를 받아들일 수 있는지의 여부를 확인합니다.
- 인터럽트를 받아들일 수 있는 상태로 확인되면 CPU가 지금까지의 작업을 백업합니다.
- CPU는 인터럽트 벡터를 참조하여 인터럽트 서비스 루틴을 실행합니다.
- 인터럽트 서비스 루틴이 끝나면 백업해둔 작업을 복구하여 실행을 재개합니다.
위에서 낯선 용어들이 있을 수 있습니다.
- Interrupt flag: 인터럽트 플래그는 인터럽트를 받아들일지, 무시할지를 결정하는 플래그입니다. 이전에 다뤘던 '플래그 레지스터'에 있는 레지스터입니다.
- Interrupt Service Routine(ISR): 인터럽트를 처리하기 위한 프로그램으로, 각각의 인터럽트에 대해 어떻게 동작해야하는지에 대한 정보로 이루어져 있습니다. Interrupt handler라고 부르기도 합니다.
- Interrupt vector: 인터럽트 벡터는 ISR을 식별하기 위한 정보입니다. 인터럽트 서비스 루틴은 메모리에 탑재된 프로그램입니다. 인터럽트 발생 시 어떤 주소의 ISR로 가야 하는지에 대한 정보를 인터럽트 벡터가 가지고 있습니다.
명령어 사이클은 위 사진처럼 돌아가며, 이것은 명령어 사이클을 최대한 간략하게 표현한 것입니다.
동기 인터럽트, 즉 명령어를 실행하다 예상치 못한 경우가 발생하는 경우, 다시 말해 예외가 발생하는 경우엔 CPU가 하던 일을 중단하고 해당 예외를 처리해야 합니다. 예외를 다 처리하고 나면 다시 하던 작업을 재개합니다. 예외도 폴트와 트랩, 중단과 소프트웨어 인터럽트로 구분이 됩니다.
폴트와 트랩은 예외 처리 이후 작업 재개를 어떻게 하느냐에 따라 구분됩니다. 예외가 발생했던 명령어부터 실행을 재개하면 폴트(Fault)이고, 예외가 발생한 명령어는 넘기고 다음 명령어부터 실행을 재개하면 트랩(Trap)입니다.
- 보통 에러가 발생한 경우는 Fault 이고, 디버깅 등을 목적으로 인터럽트 시킨 후 다음 명령어를 실행하는 경우는 Trap입니다.
중단은 실행중인 프로그램을 중단하여 예외가 발생한 명령어나 그 다음의 명령어도 실행하지 않고 다른 명령어를 실행하게 되는 예외입니다.
소프트웨어 인터럽트는 시스템 호출이 발생했을 때의 인터럽트입니다.
여기까지 CPU의 동작에 대해서 알아봤습니다.
감사합니다.