[ CS > 운영체제 ]
[운영체제] I/O Systems
[운영체제] I/O Systems
하드웨어는 연산과 I/O 작업을 번갈아가며 수행한다. 예를 들면 파일 수정작업 시에도 키보드로 입력되는 I/O 작업과 동시에, 정보를 파일에 기입하는 연산이 함께 수행한다. 오늘은 이 I/O 시스템을 OS에서 어떻게 처리하는지 설명하려고 한다.
1 I/O 하드웨어
OS 설계자는 컴퓨터에 연결할 아주 다양한 I/O 장치를 관리할 수 있도록 해야한다. 그래서 아주 다양한 디바이스를 구현하는데 일관된 인터페이스로 OS와 상호작용할 수 있도록 하는 것이 device driver(장치 드라이버)이다. 이 device driver는 OS의 I/O Subsystem에 일관된 인터페이스를 제공하며, System Call을 통해서 앱과 운영체제 사이에 표준 인터페이스를 제공하는 것과 같은 개념이다.
I/O 하드웨어는 저장 장치(disk), 전송 장치(network, bluetooth), 인간 상호 작용 장치(모니터, 키보드, 마우스, 오디오) 등이 있다. 그리고 이 장치간 통신이우선 설명에 필요한 용어 정리부터 해보겠다.
bus(버스): 여러 장치가 공유하는 전선 묶음과 통신 규약
daisy chain(데이지 체인): 장치를 순서대로 이어서 연결하는 방식(A->B, B->C 로 이어서 연결하는 방식)
PCIe(PCI Express): 주변 기기(메모리, 그래픽 카드 등) 연결에 사용되는 고속 직렬 버스
SAS(Serial-Attached SCSI): 고속 저장 장치를 연결하는 직렬 버스 방식
Lane(레인): PCle에서 데이터를 송수신하는 통로로 한 개의 레인은 2쌍의 전선(수신용/송신용)으로 구성
1.1 I/O 하드웨어의 물리적 연결
I/O 하드웨어간에 어떻게 물리적으로 연결되어 통신할 수 있는지 알아보려고 한다. 아래 전형적인 PC의 버스 구조다.

사진을 통해 PCIe 버스에 Controller들이 연결되어 통신할 수 있는 것을 알 수 있다. PCIe는 고속 직렬 버스로 옛날에는 병렬이였는데 컨트롤이 힘들어 오히려 더 느려서 직렬로 쓰는 추세다. 그리고 Controller는 포트, 버스, 장치를 제어하는데 쓰이는 전자 회로로 별도 연산, 캐싱까지도 수행하기도 한다. 그리고 상대적으로 빠른 버스로 연결하기엔 비효율적인 확장 장치(느린 장치 ex. 키보드 ,마우스)들은 별도의 expansion bus를 통해서 연결한다.
1.2 Memory-Mapped I/O
이제 어떻게 I/O 전송을 컨트롤러와 데이터와 명령어를 주고 받는지 알아보려고 한다. 우선 간단하게 말하면 프로세서는 컨트롤러에 있는 1개 이상 레지스터에 비트 패턴을 읽고 쓰는 방식으로 명령과 데이터를 전달하여 I/O 작업을 수행한다.
그리고 이 I/O에게 명령과 데이터를 전달하는 방법은 2가지가 있다.
특수한 I/O 명령어를 사용해 포트 주소로 데이터 전송
메모리 매핑 I/O를 사용해 장치 레지스터를 메모리 주소 공간에 포함시켜 일반적인 메모리 명령으로 접근
아래 표가 어떤 메모리 주소에 어떤 deivce가 맵핑 되었있는지 보여주는 일부 예시다.
I/O 주소 범위 (hexadecimal) | 장치 |
000-00F | DMA controller |
020-021 | interrupt controller |
040-043 | timer |
200-20F | game controller |
2F8-2FF | serial port (secondary) |
320-32f | hard-disk controller |
378-37F | parallel port |
3D0-3DF | graphics controller |
3F0-3F7 | diskette-drive controller |
3F8-3FF | serial port (primary) |
위와 같은 메모리 주소에 장치가 맵핑되어 있고 아래와 같이 보낸다고 생각하면 된다.
OUT 0x60, AL ; CPU의 특수한 명령어 OUT으로 키보드 컨트롤러(주소 0x60)에 AL 레지스터의 값을 보냄
*(volatile unsigned int *)0xFFFF0000 = 0x1; // 메모리에 할당된 어떤 장치에 1을 써서 동작시킴
그런데 I/O 명령어로 통신하는 방식은 이제 잘 쓰이지 않는다. 왜냐하면 그래픽 카드로 모니터에 화면을 출력한다고 할 때 그래픽 컨트롤러는 많은 메모리 매핑 영역에 데이터를 써서 화면에 출력할 값으로 바꾼 다음에, 그 값을 그래픽 컨트롤러가 모니터에 출력하게 하는 것 보다 직접 수백만 바이트를 그래픽 메모리에 직접 쓰는 게 더 빠르기 때문에 요즘은 대부분 I/O 작업은 memory-mapped I/O 방식을 사용한다.
그리고 아래는 memory-mapped I/O 방식의 디바이스 제어의 레지스터 구성이다.
data-in 레지스터
호스트가 읽어서 입력 데이터를 받음
data-out 레지스터
호스트가 써서 출력 데이터를 디바이스로 보냄
status 레지스터
호스트가 읽어서 디바이스 상태를 확인함
예: 명령 완료 여부, 읽을 데이터 유무, 에러 발생 여부 등
control 레지스터
호스트가 써서 명령 시작 또는 디바이스 모드 변경
예: half/full duplex, 패리티 검사 여부, word 길이, 통신 속도 등 설정
데이터 레지스터 크기는 보통 1~4 바이트이며, 일부 컨트롤러는 FIFO가 있어 여러 바이트 임시 저장이 가능해 장치나 호스트가 데이터를 처리할 수 있을 때까지 잠시 보관한다.
1.3 CPU가 장치에 명령을 주는 방법 - Polling 방식
CPU는 컨트롤러에 데이터를 보내기위해서 장치가 준비되었는지 알고 있어야 한다. 그리고 그 방식에는 Polling방식과 Interrupt 방식이 있다.
Polling은 CPU(호스트)가 계속 장치(컨트롤러)의 상태를 확인하며, 장치가 준비될 때까지 반복해서 Status 레지스터를 읽는 방식을 말한다. 그리고 이 Polling 과정을 통해 호스트가 컨트롤러로 출력 데이터를 보내는 Handshaking 과정은 다음과 같다.
호스트가 busy bit를 계속 확인
→ 컨트롤러가 바쁠 때는 busy bit가 1.
→ 컨트롤러가 준비되면 0이 됨.
→ 호스트는 busy bit가 0이 될 때까지 기다림.호스트가 데이터 전송 준비
→ 데이터 한 바이트를 data-out 레지스터에 씀.
→ command register의 write bit를 설정함.호스트가 command-ready bit를 설정
→ 이제 컨트롤러가 처리할 준비가 되었다고 알림.컨트롤러가 command-ready를 감지하고 busy bit 설정
→ 작업 중이라는 의미로 busy bit를 1로 만듦.컨트롤러가 명령 수행
→ command register를 읽고 write 명령인지 확인.
→ data-out에서 바이트 읽어 장치에 출력함.컨트롤러가 작업 완료 후 상태 초기화
→ command-ready bit를 0으로 초기화.
→ 오류가 없음을 알리기 위해 error bit를 0으로 설정.
→ busy bit도 0으로 만들어 완료 알림.
위 과정은 바이트마다 반복된다. 즉, 각 바이트를 하나씩 차례대로 보내기 위해서 위 핸드셰이킹 과정이 반복된다. 그리고 지속적으로 장치가 준비되었는지 확인해야 하기 때문에 기다려야한다.(장치가 busy-waiting, polling 상태가 된)
1.4 CPU가 장치에 - Interrupt 방식
Interrupt 방식은 장치가 준비되면 CPU에게 직접 알림(Interrupt)를 보내는 방식이다. 이 방식은 CPU가 다른 일을 하다가 장치가 준비됐을 때만 처리할 수 있어 더 효율적이라 본다. Interrupt 방식은 CPU가 interrupt-request line을 가지고 있고, 명령을 실행할 때마다 interrupt-request line을 감지하고 interrupt-handler routine을 작동하는 방이다. 그리고 장치가 interrupt-request line에 신호를 보내 interrupt를 발생시켰을 때, CPU가 감지할 수 있는 것이다. (장치 interrupt 발생 => CPU interrupt-request line 감지 => interrupt-handler에 interrupt 위임 => interrupt handler가 처리 => 다시 원래 작업 실행)

요약 하자면, 인터럽트 기반 I/O는 CPU가 비동기적인 이벤트에 반응하게하는 매커니즘이다.
1.5 Direct Memory Access(DMA)
디스크 드라이브처럼 큰 용량의 데이터를 처리하는 장치는 CPU가 데이터를 4kb 단위씩(일정 단위)로 수십만번씩 받아서 메모리에 올리는 작업을 하면 너무 비효율 적이다. (또는 4mb의 파일을 보내는데, 4kb씩 1000번 넘게 CPU에서 받아서 처리한다고 생각하면 매우 비효율 적이다.) 그래서 큰 용량 데이터를 메모리에 올리기 위해서 DMA라는 새로운 방식이 적용됐다. CPU 대신 드라이브 컨트롤러가 DMA transfer로 CPU대신 직접 메모리에 데이터를 전송해주는 방식이다.
CPU는 x 바이트들을 y라는 주소로 전송해달라는 명령 블록을 드라이브 컨트롤러에 전달하면, 드라이브 컨트롤러가 DMA 전송을 통해 메모리에 직접 x바이트들을 y라는 주소에 올려준다. 그리고 장치 컨트롤러가 DMA 전송을 하는 동안 CPU는 다른작업을 수행할 수 있다. 그리고 DMA 컨트롤러가 임무를 완수하면 Interrupt 방식으로 CPU에 완료를 알린다.

마무리
이제 Memory-Mapped I/O, Polling, Interrupt, DMA에 대해서 표 한개로 정리해보겠다!!
Memory-Mapped I/O, DMA, CPU 직접 복사
과정
Memory-Mapped I/O (제어용)
CPU -> 컨트롤러 레지스터
CPU 직접 복사
컨트롤러 레지스터-> CPU -> 시스템 메모리
DMA (데이터 용)
컨트롤러 레지스 -> 시스템 메모리
Polling vs Interrupt
설명
Polling
CPU가 장치 상태를 계속 확인
Interrupt
완료/에러 시 하드웨어가 CPU에 알림
바로 이해 가겠죠? 근데 어떻게 Memory-Mapped I/O로 CPU가 컨트롤러 레지스터에 데이터를 쓰는지 신기하지 않나요?? 그건 과거에는 CPU도 Memory와 데이터를 주고 받을때 Memory Controller가 사용되는데, 이 Memory Controller에서 주소값을 보고 알아서 PCIe로 알맞는 장치에 보낼 수도 있고 요즘에는 그냥 알아서 주소가 해석되어 I/O Controller로 명령이 전해진다고 합니다.