컴퓨터 구조 - 다양한 입출력 방법
- 프로그램 입출력(programmed I/O) : 프로그램 속 명령어로 입출력장치를 제어하는 방법입니다. CPU가 프로그램 속 명령어를 실행하는 과정에서 입출력 명령어를 만나면 CPU는 입출력장치에 연결된 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다.



여기서 중요한 점은 CPU가 상태 레지스터를 주기적으로 확인한다는 것입니다. 이를 폴링(polling)이라고 하는데, 인터럽트 방식보다 CPU의 부담이 더 큰 방식입니다.
그런데, CPU 내부에 있는 레지스터들과는 달리 CPU는 여러 장치 컨트롤러 속 레지스터들을 모두 알고 있기란 어렵습니다. 그렇다면 다음과 같은 명령어들은 어떻게 명령어로 표현되고, 메모리에 어떻게 저장되어 있을까요?
- 프린터 컨트롤러의 상태 레지스터를 읽어라
- 프린터 컨트롤러의 데이터 레지스터에 100을 써라
- 키보드 컨트롤러의 상태 레지스터를 읽어라
- 하드 디스크 컨트롤러의 데이터 레지스터에 'a'를 써라
방법에는 크게 2가지 방식이 있습니다.
- 메모리 맵 입출력(memory-mapped I/O) : 메모리의 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 하나의 주소 공간으로 간주하는 방법입니다. 만약 1,024개의 주소를 표현할 수 있는 컴퓨터가 있다면 절반인 512개의 주소는 장치 컨트롤러의 레지스터를 표현하기 위해 사용됩니다.

예를 들어 516번지에 프린터 컨트롤러의 데이터 레지스터를 가리키는 정보가 저장되어 있습니다. 그렇다면 CPU는 메모리 주소에 접근하는 방법과 같은 방법으로 516번지에 접근합니다. 후에 저장된 정보를 참조하여 프린터 컨트롤러의 데이터 레지스터에 접근할 수 있게 됩니다.
- 고립형 입출력(isolated I/O) : 메모리의 주소 공간과 입출력장치를 위한 주소 공간을 분리하는 방법입니다. 제어 버스에 '메모리 읽기/쓰기' 이외에 '입출력 장치 읽기/쓰기' 통로를 따로 두어 CPU가 어떤 명령어를 실행하느냐에 따라서 접근하는 장치를 다르게 하는 방식입니다. 입출력장치를 위한 주소 공간을 따로 두기 때문에 이 주소에 접근하기 위해서는 메모리에 접근하는 명령어와는 다른 입출력 명령어를 사용합니다.



- 인터럽트 기반 입출력(interrupt-Driven I/O) : 프로그램 입출력 방식은 CPU가 장치 컨트롤러의 상태 레지스터를 주기적으로 확인하는 polling 방식을 사용합니다. 그러나 이는 CPU Cycle의 낭비를 야기하는 단점이 있습니다. 이를 보완할 수 있는 방법이 인터럽트 기반 입출력입니다. CPU는 장치 컨트롤러에 입출력 작업을 명령하고, 장치 컨트롤러가 입출력장치를 제어하며 입출력을 수행하는 동안 CPU는 다른 일을 할 수 있게 됩니다. 장치 컨트롤러가 입출력 작업을 끝낸 뒤 CPU에게 인터럽트 요청 신호(알림)를 보내면 CPU는 하던 일을 잠시 백업하고 ISR을 실행합니다.

만약 입출력장치가 더 많아지면 어떻게 될까요? 더 많은 장치 컨트롤러가 CPU에 인터럽트 요청 신호를 보내게 될 것입니다. 그렇다면 CPU는 많은 장치 컨트롤러에서 들어오는 인터럽트 요청 신호를 어떻게 처리할 수 있을까요?
가장 간단한 방법은 인터럽트가 발생한 순서대로 인터럽트를 처리하는 방법입니다. 인터럽트 A를 처리하는 도중 인터럽트 B가 발생해도 이를 받아들이지 않고 인터럽트 A에 대한 ISR이 끝나면 그때 인터럽트 B에 대한 ISR을 실행하는 것입니다.
이는 CPU가 플래그 레지스터 속 인터럽트 비트를 비활성화 채 인터럽트를 처리하는 경우입니다.
하지만 현실적으로 모든 인터럽트를 순차적으로 처리할 수는 없습니다. 인터럽트 중에도 우선순위가 더 높은 인터럽트가 있기 때문입니다. 즉, CPU는 인터럽트 간 우선순위를 고려하여 우선순위가 더 높은 인터럽트부터 처리합니다.
예를 들어 인터럽트 A가 발생하여 ISR을 통해 처리하는 도중에 인터럽트 B가 발생했습니다. 만약 인터럽트 B의 우선순위가 A보다 낮다면 CPU는 A에 대한 ISR이 끝나면 B에 대한 ISR을 실행합니다. 반대로 인터럽트 B의 우선순위가 높다면 A에 대한 ISR을 잠시 멈추고 B에 대한 ISR을 실행한 뒤 다시 A를 처리합니다.
다시 말해 플래지 레지스터 내 인터럽트 비트가 활성화 되어있거나, 인터럽트 비트가 비활성화되어도 무시할 수 없는 인터럽트인 NMI(Non-Maskable Interrupt)가 발생한 경우 CPU는 우선순위가 높은 인터럽트부터 처리합니다.
우선순위를 반영하여 다중 인터럽트를 처리하는 대표적인 방법은 PIC(Programmable Interrupt Controller)라는 하드웨어를 사용하는 방법입니다. PIC는 여러 장치 컨트롤러에 연결되어 장치 컨트롤러들이 보내는 하드웨어 인터럽트 요청들의 우선순위를 판별한 뒤 CPU에 처리해야할 인터럽트를 알려주는 장치입니다.

- PIC가 장치 컨트롤러부터 인터럽트 요청 신호를 받습니다.
- PIC는 인터럽트 우선순위를 판단한 뒤 CPU에 처리해야 할 인터럽트 요청 신호를 보냅니다.
- CPU는 PIC에 인터럽트 확인 신호를 보냅니다.
- PIC는 데이터 버스를 통해 CPU에 인터럽트 벡터를 보냅니다.
- CPU는 인터럽트 벡터를 통해 인터럽트 요청의 주체를 알게 되고, 해당 장치의 ISR을 실행합니다.
- DMA 입출력(Direct Memory Access) : 입출력장치와 메모리 사이에 전송되는 모든 데이터가 반드시 CPU를 거쳐야 한다면 CPU는 입출력장치를 위한 연산 때문에 다른 작업을 수행하기가 어려워집니다. 하드 디스크 백업과 같이 대용량 데이터를 옮길 때는 CPU 부담이 더욱 커지게 됩니다. 이에 입출력장치와 메모리가 CPU를 거치지 않고도 상호작용할 수 있는 입출력 방식을 DMA 입출력이라고 합니다. 이는 직접 메모리에 접근할 수 있는 입출력 기능으로, CPU 대신에 DMA 컨트롤러라는 하드웨어가 수행합니다.
- CPU가 DMA 컨트롤러에 입출력장치의 주소, 수행한 연산(읽기/쓰기), 읽거나 쓸 메모리의 주소 등과 같은 정보로 입출력 작업을 명령합니다.
- DMA 컨트롤러는 CPU 대신 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다. 이때 DMA 컨트롤러는 필요한 경우 메모리에 직접 접근하여 정보를 읽거나 씁니다.
- 입출력 작업이 끝나면 DMA 컨트롤러는 CPU에 인터럽트를 걸어 작업이 끝났음을 알립니다.



여기서 문제가 있습니다. DMA 컨트롤러가 메모리로부터 데이터를 가져오거나 장치 컨트롤러에 데이터를 보내는 과정에서 시스템 버스를 이용하게 됩니다. 중요한 점은 시스템 버스는 공용 자원이기 때문에 한 번에 하나의 장치만 접근할 수 있습니다. 즉, DMA 컨트롤러가 시스템 버스를 사용 중일 때는 CPU는 시스템 버스를 사용할 수 없습니다(반대의 경우도 마찬가지). 이를 사이클 스틸링(cycle stealing)이라고 합니다. 이를 해결할 수 있는 것이 입출력 버스(Input/Output Bus)입니다.
입출력 버스는 말 그대로 입출력장치를 위한 버스입니다. DMA 컨트롤러와 장치 컨트롤러들을 입출력 버스라는 별도의 버스에 연결하여 DMA 컨트롤러와 장치 컨트롤러가 데이터를 주고받을 때 시스템 버스가 아닌 입출력 버스를 사용함으로써 시스템 버스의 사용 빈도를 줄일 수 있습니다.

입출력 버스에는 PCI 버스, PCI Express(PCIe) 버스 등 여러 종류가 있습니다.