안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

컴퓨터 구조 (Architecture)/컴퓨터의 개념 및 실습

[Chapter 8] IO - 입력 및 출력 장치

피그브라더 2020. 2. 2. 21:05

우리는 지금까지 메모리에 올라가 있는 프로그램의 명령어들을 해석하고 실행하는 것과 관련한 내용들을 다루었다. 그러면 이제 다음 물음을 던져야 할 때이다. "메모리의 데이터는 어디에서 온 것일까??", "메모리의 데이터는 어떤 과정을 거쳐서 인간이 볼 수 있는 형태로 출력이 되는 것일까?" 이 물음에 대한 답은 바로 입출력(I/O) 장치에서 찾을 수 있다. 프로그램을 실행하는 것도 결국은 하드디스크라는 외부 장치로부터 프로그램 코드를 입력받아 메모리에 올리는 것으로 해석할 수 있다. 이번 포스팅에서는 그러한 입출력 장치에 대해 조금 더 자세히 알아보도록 할 것이다.

 

1. 입출력 장치 (Input and Output Device)

1-1. I/O 장치의 분류

I/O 장치는 다음과 같이 대략 두 가지 기준으로 분류될 수 있다. 참고로 하드디스크 이야기가 나온 김에 하드디스크와 관련한 하나의 단위를 기억하고 가도록 하자. 바로 섹터(Sector)이다. 1섹터는 512(=2^9)바이트를 의미한다. 따라서 2G 섹터는 1TB와 동일하다.

 

분류 기준 설명
동작 방식 (Behavior) 입력 장치 : 키보드, 움직임 센서, 네트워크 인터페이스 등
출력 장치 : 모니터, 프린터, 네트워크 인터페이스 등
저장 장치 : 하드디스크, CD-ROM 등
데이터 전송 속도 (Data Rate) EX 1) 키보드 : 100 B/sec
EX 2) 하드디스크 : 30 MB/sec
EX 3) 네트워크 : 1 MB/s ~ 1 GB/s

1-2. I/O 장치의 인터페이스

CPU가 메모리와 통신하기 위해 MAR, MDR이라는 인터페이스가 존재하는 것처럼, I/O 장치도 외부 장치와 통신하기 위해 인터페이스 역할을 하는 레지스터가 존재야 한다. 어떤 인터페이스가 있어야 할까? 대략 다음과 같이 세 가지 정도를 생각해볼 수 있다.

 

① 데이터를 주고받기 위한 인터페이스 : 데이터 레지스터 (Data Register)

② CPU가 I/O 장치에 명령을 내리기 위한 인터페이스 : 컨트롤 레지스터 (Control Register)

③ I/O 장치의 준비 상태를 나타내기 위한 인터페이스 : 상태 레지스터 (Status Register)

 

예를 들어, 특정 값을 출력하고 싶은 경우를 생각해 보자. 그러면 먼저 CPU는 해당 출력 장치의 상태 레지스터 값을 확인하여 데이터를 전달해도 되는 상황인지(즉 출력 장치가 데이터를 받을 준비가 되었는지) 판단한다. 전달해도 되는 상황이면 데이터 레지스터에 출력할 값을 쓰고, 그 값을 어떻게 출력할지에 대한 명령 정보를 컨트롤 레지스터에 쓸 것이다. 그러면 출력 장치는 그 데이터를 받아서 명령받은 대로 출력 동작을 수행하게 될 것이다.

 

 

우리가 공부하는 LC-3 컴퓨터의 경우, 입출력 장치로서 키보드모니터를 지원한다. 그리고 키보드는 KBDR(Keyboard Data Register)KBSR(Keyboard Status Register)이라는 인터페이스를, 모니터는 DDR(Display Data Register)DSR(Display Status Register)이라는 인터페이스를 가지고 있다. 특별히 컨트롤 레지스터는 존재하지 않는데, LC-3의 키보드와 모니터는 단순히 문자의 입력과 출력만 수행하기 때문에 생략한 것으로 추정된다(뇌피셜).

 

더보기

여기서 의문이 생길 수도 있다. 이전 포스팅에서 입출력 장치는 '메모리'와 통신하는 장치라 했는데, 왜 여기서는 CPU와 통신하는 장치인 것처럼 묘사하는 것일까? 이는 앞서 살펴본 LC-3의 전체 Data Path에서 답을 찾을 수 있다. 입출력 장치가 메모리와 데이터를 주고받기 위해서는 중간에 CPU의 (글로벌) 버스를 거쳐야 하기 때문이다. 또한 입출력 장치가 직접적으로 CPU와 통신하는 경우도 있기 때문이다. 예를 들어 메모리가 아닌 특정 레지스터에 들어 있는 값을 모니터로 출력하고 싶은 경우에는 CPU와 입출력 장치가 직접적으로 통신해야 한다.

 

2. 입출력의 구현 방식

입출력을 구현하는 방식은 크게 두 가지 기준에 따라 분류할 수 있다. 먼저 I/O 레지스터에 어떻게 접근하느냐에 따라 Memory-mapped 방식과 Port-mapped IO 방식으로 구분된다. 그리고 I/O 장치가 준비되었는지를 어떻게 감지하느냐에 따라 Polling 방식과 Interrupt 방식으로 구분된다. 우리가 공부할 LC-3 컴퓨터의 경우, I/O 레지스터에 접근하는 방법으로 Memory-mapped 방식을 채택하고 있으며, I/O 장치가 준비되었는지를 감지하는 방법으로는 Polling 방식과 Interrupt 방식을 둘 다 지원한다. LC-3의 입출력 구현 방식을 알아보기에 앞서, 먼저 두 가지 기준에 따라 구분되는 각각의 입출력 방식에 대해서 간단히 알고 넘어가도록 하자.


2-1. I/O 레지스터에 접근하는 방법 (분류 기준 ①)

2-1-1. Memory-mapped

메모리의 주소 중 일부를 각종 I/O 레지스터들에게 부여해주고, 메모리에서 데이터를 읽는 명령어와 메모리에 데이터를 쓰는 명령어를 그대로 사용하여 특정 I/O 레지스터에 접근하는 방식을 말한다. 따라서 I/O 레지스터에 접근하기 위한 별도의 주소 입력 버스가 필요하지 않다. 물론 메모리 주소만 부여할 뿐, 실제로(물리적으로) I/O 레지스터는 메모리와 독립되어 I/O 장치 안에 들어있다. 다만 해당 I/O 레지스터에게 부여된 메모리 주소로 접근을 시도하면 메모리가 아닌 그 I/O 레지스터에게 접근하게끔 구현되어 있을 뿐이다.

 

 

2-1-2. Port mapped IO (Special Instruction)

특정 I/O 레지스터에 접근하기 위한 명령어가 별도로 존재하여, 해당 명령어를 실행하면 메모리 주소 입력 버스와는 별개의 주소 입력 버스를 이용하여 해당 I/O 레지스터에 접근하는 방식이다. 즉 특정 I/O 레지스터 접근을 위한 opcode가 존재하며, 그 명령어의 나머지 비트는 접근하고자 하는 I/O 레지스터의 주소와 수행하고자 하는 연산의 정보로 표현된다.

 


2-2. I/O 장치가 준비되었음을 감지하는 방법 (분류 기준 ②)

2-2-1. Polling

I/O 장치의 준비 상태를 CPU가 수시로 확인하는 방식을 말한다. 예를 들어, 읽어 들일 데이터가 입력됐는지를 CPU가 계속해서 키보드에게 물어보는 것이다. 입출력 장치에는 인터페이스로서 상태 레지스터가 있기 때문에, 상태 레지스터의 값을 수시로 확인하면 해당 장치의 준비 상태를 검사할 수 있다. 그러나 I/O 장치가 준비될 때까지 기다리는 동안에는 CPU가 다른 작업을 수행할 수 없기 때문에 낭비되는 사이클(시간)이 많다는 단점이 있다.

 

더보기

참고로, Time Division Virtualization 메커니즘(CPU가 각 스레드에게 일정한 시간을 나눠주는 방식)에 근거하여 짧은 시간마다 Polling을 위한 스레드로 제어가 이동하게끔 하면 매 순간 입력받는 메커니즘을 구현할 수 있다. 또한 멀티 코어 CPU라면 아무 작업도 하지 않고 있는 코어에게 Polling 작업을 요청함으로써 입출력 반응 속도를 향상시키는 방법도 있다.

 

2-2-2. Interrupt

I/O 장치가 준비되면 CPU에게 신호를 보내서 자신이 준비되었음을 알리는 방식이다. CPU가 I/O 장치를 기다리지 않고 다른 작업을 수행하다가, I/O 장치가 준비되었다고 Interrupt 신호를 보내면 그때서야 인터럽트 루틴을 호출함으로써 해당 입출력 요청을 처리하게 된다. 따라서 Polling 방식에 비해서 사이클(시간) 낭비가 덜하다고 볼 수 있다.

 

3. LC-3의 Memory-mapped IO


3-1. I/O 레지스터에 맵핑된 메모리 주소

LC-3는 I/O 레지스터에 접근하는 방식으로 Memory-mapped IO 방식을 채택한다. 따라서 메모리 주소 중 일부를 I/O 장치의 레지스터들에게 할당하게 된다. 조금 더 구체적으로 말하면, xFE00부터 xFFFF까지의 메모리 주소가 I/O 레지스터들에게 맵핑이 되어 있으며, 대표적으로 키보드와 모니터의 레지스터들은 다음과 같이 메모리 주소에 맵핑이 되어 있다. 가령 MAR에 xFE00을 입력하면 메모리[xFE00]에 접근하는 것이 아니라 KBSR(키보드의 상태 레지스터)에 접근하게 되는 것이다. 이것이 어떻게 구현되어 있는지는 뒷부분에서 설명할 LC-3 I/O Data Path 부분을 참조하기 바란다.

 

 

LC-3는 이렇듯 메모리 주소에 맵핑된 I/O 레지스터들을 이용하여 해당 I/O 장치와 통신하는 인터페이스를 갖추게 되며, 이를 기반으로 Polling 방식의 입출력과 Interrupt 방식의 입출력을 둘 다 구현하고 있다. 각각의 방식에 대해 구체적으로 알아보기에 앞서, 기본적으로 CPU가 키보드 및 모니터와 언제 어떻게 데이터를 교환하게 되는지 알아보도록 하자.


3-2. 키보드 입력 (Input from Keyboard)

키보드로부터 문자 하나가 입력되면 그 문자에 해당하는 아스키 코드 값은 KBDR[7:0]에 입력되고, ready bit에 해당하는 KBSR[15]를 1로 설정함으로써 CPU에게 데이터를 전달할 준비가 되었음을 나타낸다. ready bit가 1인 동안에는 키보드가 비활성화되어, 문자가 입력돼도 그 값이 KBDR[7:0]에 저장되지 못한다. 그러다가 CPU가 KBDR[7:0]의 값을 읽으면, ready bit가 다시 0으로 설정되어 키보드는 다른 문자를 입력받을 수 있게 된다.

 

 

만약 Polling 방식의 입력이라면 CPU가 키보드의 ready bit를 계속 확인하다가 1이 되었을 때 KBDR[7:0]의 값을 읽어 들일 것이고, Interrupt 방식의 입력이라면 키보드가 ready bit을 1로 설정함과 동시에 CPU에게 신호를 보내서 인터럽트를 발생시킬 것이다. (참고로 해당 입력 장치가 인터럽트를 발생시키려면 기본적으로 interrupt enable bit에 해당하는 KBSR[14]가 1로 설정이 되어 있어야 한다. 이 비트는 인터럽트를 발생시킬 권한이 있는 입력 장치라면 1로 설정이 되어 있을 것이고, 그렇지 않다면 0으로 설정이 되어 있을 것이다.)


3-3. 모니터 출력 (Output to Monitor)

모니터가 문자를 출력할 준비가 되어 있다면 ready bit에 해당하는 DSR[15]는 1로 설정이 되어 있을 것이다. 이때 CPU가 출력하고자 하는 문자의 아스키 코드 값을 DDR[7:0]에 입력하면, ready bit은 0으로 설정되어 DDR[7:0]에 더 이상 값이 입력될 수 없게 된다. 그러다가 모니터가 출력 작업을 마치고 나면 다시 ready bit를 1로 설정하여 DDR[7:0]에 값이 입력될 수 있게 한다.

 

 

또한 입력과 마찬가지로, Polling 방식의 출력이라면 CPU가 모니터의 ready bit를 계속 확인하다가 1이 되었을 때 DDR[7:0]에 값을 입력할 것이고, Interrupt 방식의 출력이라면 모니터가 ready bit을 1로 설정함과 동시에 CPU에게 신호를 보내서 인터럽트를 발생시킬 것이다. (참고로 해당 출력 장치가 인터럽트를 발생시키려면 기본적으로 interrupt enable bit에 해당하는 DSR[14]가 1로 설정이 되어 있어야 한다. 이 비트는 인터럽트를 발생시킬 권한이 있는 출력 장치라면 1로 설정이 되어 있을 것이고, 그렇지 않다면 0으로 설정이 되어 있을 것이다.)


3-4. 상태 레지스터의 동기화 효과 (Synchronization)

입출력 장치에 있는 상태 레지스터는 처리 속도가 빠른 CPU와 상대적으로 처리 속도가 느린 입출력 장치 간의 동기화(Synchronization)를 가능하게 한다. 이를 이해하기 위해, 키보드에 KBSR이 없고 모니터에도 DSR이 없는 상황을 상상해보자.

 

3-4-1. 키보드에 KBSR이 없다면 어떻게 될까?

아무리 타자 속도가 빠른 사람이라 할지라도, 문자 하나를 입력하는 데 걸리는 시간이 클락의 한 사이클보다 빠를 수는 없다. 이런 상황에서, 문자 하나가 KBDR[7:0]에 입력되었다고 해보자. 그러면 CPU는 현재 사이클에서 그 값을 바로 읽어 들일 것이다. 문제는 그다음 사이클이다. 이미 읽어 들인 값이기 때문에 다시 읽어서는 안 되지만, 그러한 상태를 나타내는 정보가 존재하지 않으므로 CPU는 그 값을 또다시 읽어 들이게 된다. 즉 같은 값을 여러 번 읽게 되는 것이다.

 

3-4-2. 모니터에 DSR이 없다면 어떻게 될까?

모니터가 DDR[7:0]에 입력된 값을 출력하는 데까지는 어느 정도의 시간이 필요하다. 그리고 그 속도는 CPU의 클락보다 훨씬 느릴 것이다. 그러면 모니터가 당장 DDR[7:0]에 저장되어 있는 값을 출력하는 과정에 있는 동안 다음 사이클에 도달하여 다른 값이 DDR[7:0]에 다시 입력될 것이다. 모니터는 기존 값에 대한 출력 작업을 완전히 마무리하지 않은 상태에서 다른 값을 DDR[7:0]에 입력 받음으로써 출력 작업에 혼선이 생기게 된다.

 

결국 KBSR은 키보드에 입력된 값 하나가 정확히 한 번 읽히도록, DSR은 출력하고자 하는 값 하나가 정확히 한 번 출력되도록 제어해주는 역할을 수행한다. 처리 속도가 빠른 CPU가 처리 속도가 느린 입출력 장치를 기다리게 해서 둘 간의 동기화를 가능케 한 것이다.


3-5. LC-3 I/O Data Path

LC-3의 CPU가 외부 입출력 장치와 통신하기 위해 어떤 구조를 갖추고 있을까? 지금까지 알아본 LC-3의 Data Path에는 입출력 장치 부분을 간략하게만 그려놨었다. 이번에는 입출력 장치와 관련된 부분을 확대해서 자세히 알아보도록 하자. 다음 그림을 보자.

 

 

갑자기 상당히 복잡해졌는데, 이해를 돕기 위해 각 도선과 장치에 색을 부여하였다. 의미를 해석하자면 다음과 같다. 쉽게 말해서 노란색 화살표 경로로 접근할 메모리 주소를 입력하는 것이고, 값을 쓰려는 경우에는 초록색 화살표 경로, 값을 읽으려는 경우에는 빨간색 화살표 경로를 따라가는 것이다.

 

노란색 화살표 경로 : 메모리 주소를 입력하는 부분

초록색 화살표 경로 : CPU 레지스터의 값 → 메모리 혹은 I/O 레지스터

빨간색 화살표 경로 : CPU 레지스터 ← 메모리 혹은 I/O 레지스터의 값

 

그리고 그림에 나타난 컨트롤 신호들을 정리하자면 다음과 같다.

 

MIO.EN : CPU가 메모리 혹은 I/O 레지스터와 데이터를 교환하고 싶을 때 활성화된다.

메모리 주소가 일반적인 메모리 주소인 경우 : 는 메모리로부터의 입력을 선택 + MEM.EN 활성화

메모리 주소가 I/O 레지스터에 맵핑된 주소인 경우 : 는 해당 I/O 레지스터로부터의 입력을 선택 + MEM.EN 비활성화

R.W : (메모리 혹은 I/O 레지스터를 대상으로) 값을 읽고 싶을 때는 READ 신호, 값을 쓰고 싶을 때는 WRITE 신호가 발생된다.

 

4. LC-3의 Polling I/O

LC-3에서는 Polling I/O를 어떻게 구현하고 있을까? 우리가 앞서 알아본 TRAP 명령어 중, TRAP x23TRAP x21이 바로 키보드 입력과 모니터 출력을 Polling 방식으로 구현한 것이다. TRAP 명령어를 실행하면 키보드 입력 혹은 모니터 출력을 위한 서비스 루틴이 호출되는데(시스템 콜), 그 루틴에는 해당 입출력 장치가 준비될 때까지 기다리는 루프가 구현되어 있다. TRAP x23에 의해 호출되는 키보드 입력 서비스 루틴과 TRAP x21에 의해 호출되는 모니터 출력 서비스 루틴의 코드는 이후 포스팅에서 더 자세히 다룰 것이니, 여기서는 그 루틴들의 뼈대를 이루는 기본적인 Polling I/O 구현 방식만 알아보도록 하자.


4-1. 키보드 입력 루틴 (Input Routine)

메모리[KBSRPtr]에는 KBSR에 맵핑된 메모리 주소가, 메모리[KBDRPtr]에는 KBDR에 맵핑된 메모리 주소가 저장되어 있다. 따라서 LDI 명령어를 실행하면 KBSR과 KBDR의 값을 읽어올 수 있다. 우선 ready bit에 해당하는 KBSR[15]의 값이 1이 될 때까지 계속해서 KBSR의 값을 읽어오는 루프를 반복한다. 그러다가 ready bit가 1이 되면, KBDR의 값을 읽어와 R0에 저장함으로써 입력 작업을 마무리한다.

 


4-2. 모니터 출력 루틴 (Output Routine)

메모리[DSRPtr]에는 DSR에 맵핑된 메모리 주소가, 메모리[DDRPtr]에는 DDR에 맵핑된 메모리 주소가 저장되어 있다. 따라서 LDI 명령어를 실행하면 DSR의 값을 읽을 수 있고, STI 명령어를 실행하면 DDR에 값을 쓸 수 있다. 우선 ready bit에 해당하는 DSR[15]의 값이 1이 될 때까지 계속해서 DSR의 값을 읽어오는 루프를 반복한다. 그러다가 ready bit가 1이 되면, 출력하고자 하는 값을 DDR에 입력함으로써 출력 작업을 마무리한다.

 


4-3. 에코 루틴 (Echo Routine)

일반적인 프로그램의 경우, 키보드로 입력한 값은 그대로 출력되어 사람의 눈에 보이게 된다. 이러한 동작을 '에코(echo)'라고 부른다. 에코 루틴은 다음과 같이 구현되어 있다. (앞에서 설명한) 기본적인 입력 루틴을 그대로 실행하여 키보드로부터 입력을 받아 그 값을 R0에 저장한다. 이어서 바로 (앞에서 설명한) 기본적인 출력 루틴을 그대로 실행하여 R0에 저장된 값을 모니터로 출력한다. 즉 입력 루틴과 출력 루틴을 그대로 이으면 곧 에코 루틴이 되는 것이다.

 

 

5. LC-3의 Interrupt I/O

그러면 Interrupt I/O는 어떤 방식으로 동작할까? 인터럽트의 메커니즘에 대해서는 이후 포스팅에서 더 자세히 다룰 것이므로, 여기서는 외부 장치에 의한 인터럽트가 어떻게 발생하고 처리되는지에 대해 핵심만 짚어보도록 할 것이다. 인터럽트의 발생과 처리의 전체 과정은 크게 두 단계로 구분할 수 있다. 첫 번째는 인터럽트 신호를 발생시키는 단계, 두 번째는 해당 인터럽트 요청을 처리하는 단계이다. 각각에 대해 한 번 알아보자.


5-1. 인터럽트 신호의 발생

첫 번째 단계는 외부 입출력 장치에 의해 인터럽트 신호(INT Signal)가 발생하여 현재 실행 중인 프로그램을 중단(Suspend)시키는 것이다. 그 과정을 간단히 한 번 살펴보자. 우선, 인터럽트를 발생시킬 권한이 있는 입출력 장치의 상태 레지스터는 interrupt enable bit가 CPU에 의해 1로 설정된다(이것은 아마도 운영체제 코드의 역할로 추정). 그러한 입출력 장치가 데이터를 교환할 준비가 되면 상태 레지스터의 ready bit는 1이 되는데, 이렇게 ready bit와 interrupt enable bit가 둘 다 1이면 AND 게이트에 의해 인터럽트 요청 신호(Interrupt Request Signal)가 활성화된다(여기서 말하는 인터럽트 요청 신호는 앞에서 말한 인터럽트 신호와 다름). 그리고 그렇게 발생된 여러 장치의 인터럽트 요청 신호들 중 가장 우선순위가 높은 인터럽트 요청 신호가 현재 프로세스의 우선순위보다 높다면 그 입출력 장치에 해당하는 인터럽트 신호(INT Signal)가 활성화된다. CPU는 STORE 단계가 끝난 뒤 FETCH 단계로 돌아가기 전에 인터럽트 신호의 유무를 검사하며, 이때 인터럽트 신호가 활성화되어 있다면 FETCH 단계로 돌아가지 않고 해당 인터럽트 서비스 루틴을 호출하기 위한 준비를 하게 된다. 이렇게 현재 실행 중이던 프로그램이 중단된다.

 

우선순위라는 것의 정체는 무엇인지, 우선순위가 높은 신호를 어떻게 선별해내는지, 왜 인터럽트 신호의 유무를 STORE 단계 다음에 검사하는지, CPU가 인터럽트 신호의 유무를 검사하고 인터럽트 서비스 루틴을 호출하는 메커니즘은 무엇인지 등등에 대한 자세한 내용들은 이후 포스팅에서 설명하므로 여기서는 생략하도록 하겠다.


5-2. 인터럽트 요청의 처리

두 번째 단계는 요청된 인터럽트에 대한 서비스 루틴을 실행하고, 마치 아무 일이 없었다는 듯이 다시 원래 프로그램의 실행 흐름으로 돌아가는 것이다. "아무 일이 없었다는 듯이"의 뜻은, 해당 서비스 루틴을 호출하기 전에 모든 상태 정보를 어딘가에 백업해 두고, 서비스 루틴이 종료되면 백업해둔 상태 정보를 모두 복원하여 아무 흔적도 남기지 않음을 의미한다.

 

두 번째 단계의 메커니즘은 이후 포스팅에서 설명할 스택(Stack)의 개념을 명확히 알아야 이해할 수 있으므로, 여기선 설명을 이쯤에서 마칠 것이다. 어떤 상태 정보를 어떤 방식으로 백업하고 복원하는지, 무슨 인터럽트 서비스 루틴을 호출할지는 어떻게 알 수 있는지 등등에 대한 자세한 내용은 해당 포스팅을 참조하도록 하자.