[OS] I/O Device, Device Controller & I/O Hardware, Direct I/O, Memory-mapped I/O, Polled I/O, Interrupt-driven I/O, Programmed I/O, Direct Memory Access, Blcoking, Non-Blocking I/O

2024. 6. 8. 00:22CS/Operating System

I/O가 연결되어 있는 걸 보면 버스에 프로세서와 캐시, 메모리가 연결되어 있고 나머진 다 똑같은 I/O 장치로 다루게 된다.

운영체제 입장에서 CPU, 메모리를 제외한 장치들은 똑같이 다루는 것이다. 

I/O 장치를 동일하게 다루는 방식은 device driver, device controller가 있어서 가능하다.

예를 들어 키보드를 open해서 작업한다면 키보드 디바이스 드라이버가 작동해서 키보드 디바이스 컨트롤러에게 명령을 내리게 된다. 그럼 밑에 있는 실제 디바이스가 작동한다. 모든 디바이스마다 컨트롤러가 따로따로 있다. 보통은 메인보드 칩셋에 붙어있거나 디바이스에 붙어있는 하드웨어이다.


I/O Device

디바이스는 OS 입장에서 크게 Block, Character로 나눌 수 있다.

Block device

  • fixed size block로 정보를 기록하는 장치이다.
  • block size는 장치마다 다르다.
  • 각 블락별로 address를 가지고 있다.
  • read write가 블락마다 독립적으로 작동한다는 것이 핵심적이다. 이 말은 랜덤한 seek가 가능하다는 것이다. 즉 110번 read, 120번 write, 29번 read 이런 식으로 가능하다. address가 존재하며 seek 가능하다는 게 가장 중요하다.
  • 순서 없이 엑세스 해야 하는 경우에 사용한다.

Character device

  • character의 스트림으로 다루게 된다.
    • 스트림이라는건 키보드를 예로 들면 키보드를 누른 순서대로 데이터가 오고 가는 것을 말한다.
  • 따라서 address가 필요 없고 seek operation도 없다. A처리 → D 처리 → C처리 이런 게 불가능하다는 것이다
  • 순서 있게 액세스 해야 하는 경우 사용한다.
  • 프린터, 키보드 등에 해당한다. 사실 요즘 USB 포트에 꽂는 키보드는 다 block device이다.

I/O 장치의 속도를 대략적으로 알아보자.

USB 2.0 : 60 MB/s

USB 3.0 : 625 MB/s

SATA M.2 :  6 Gb/s

NVMe M.2 : 20 Gb/s

PCIe 4.0 : 1.9 Gb/s

Device Controller & I/O Hardware

I/O 장치는 기계적인 부분과 eletronic 한 부분이 존재한다.

  • 디스크 판이 돌고, 디스크 기계가 움직이고, 출력을 실행하는 기계적인 부분이 있다.
  • 전자적인 device controller가 존재한다.
  • device controller가 항상 붙어있다. 또, 해당 device를 위한 driver가 함께 있어야 한다.

device controller가 하는 일

  • I/O는 디바이스의 버퍼와 메모리를 왔다 갔다 하는 것이다
  • 그 과정에서 byte들을 읽고 쓰고 하는 게 I/O 장치인데, 그 byte를 block단위 혹은 stream으로 변환하는 게 컨트롤러의 일이다.
    • bit stream들이 결국 메인 메모리로 가야 한다. 
  • 필요하다면 error correction도 device controller가 한다.

디바이스는 결국 레지스터를 가지고 있다.

  • 디바이스 드라이버는 그 레지스터를 건드리는 일을 한다. 명령을 받아서 어느 레지스터에 어떤 명령을 수행하는 방식이다. 사전에 약속된 레지스터와 커맨드가 다 정해져 있다.
  • 디바이스 드라이버가 특정한 레지스터(컨트롤러)에 명령을 주면 정해진대로 동작하는 게 디바이스인 것이다.
    • 프린터 레지스터에 1이라는 값을 주고 그 옆에 출력할 글자 a를 주면, 기계 장치가 그대로 읽어서 1을 보고 출력을 실행하고 뭘 출력할지 a를 보고 a를 출력한다. 이 명령과 주소, 데이터는 드라이버가 준다.
  • 드라이버는 OS 회사에서 만드는 게 아니라, 디바이스를 만든 회사에서 제공한다.

디바이스는 address를 가지고 있고 실제 I/O에서 사용된다.

정리하자면 device driver가 device controller에게 커널에서 받은 명령을 전달하고 device controller는 레지스터를 확인하고 명령어를 실행하는 실제 I/O 작업을 수행한다.


Direct I/O, Memory-mapped I/O

드라이버가 어떻게 컨트롤러의 레지스터에 데이터를 쓰는지, 즉 I/O 장치에게 명령을 전달하는 방식은 두 가지로 나뉜다.

Direct I/O

  • 디바이스에게 명령을 직접 주는 것이다. 명령을 직접 주기 위해 instruction, port가 모두 존재한다.
    • CPU 만드는 사람이 I/O 장치를 사용하려면 이런 명령어와 어드레스를 쓰라고 정해놨다. 그럼 이 명령어에 맞춰서 디바이스 컨트롤러를 만들게 된다.
  • 이 방식은 굉장히 제한적이다. 정의되지 않은 디바이스가 새로 추가되면 CPU를 새로 설계해야 한다.
  • 또, 이 방식은 어셈블리어를 기본적으로 사용해야 한다.

Memory-mapped I/O

  • 확장성을 위해 사용하는 방식이다.
  • device controller에 존재하는 레지스터를 address space의 특정 위치에 매핑시키는 방식이다. 레지스터를 마치 파일처럼 읽어서 매핑한다. 메모리에 1을 쓰면 I/O 장치의 컨트롤러에 반영되어 레지스터의 값을 변경한다.
  • device driver를 전부 C로 작성할 수 있다는 것이다.
  • protection도 가능하다. 페이지 테이블에 prot bit가 있으니까 그걸로 가능하다.
  • 값을 읽거나 쓰는 것도 메모리에 쓰면 되므로 매우 쉬워서 테스트하기도 쉽다.
  • 대부분의 운영체제는 디바이스를 file로 취급한다.

Polled I/O, Interrupt-driven I/O

다른 문제도 있는데, 디바이스에게 명령을 줬으면 그게 끝났는지 CPU에게 꼭 알려줘야 한다.

Polled I/O

  • CPU가 디바이스한테 계속 물어보는 것이다. 레지스터에 있는 bit를 계속 체크하기를 while문을 돌면서 반복한다.
  •  간단하다.
  • 소프트웨어적으로 컨트롤하기도 쉽다.
  • CPU가 I/O의 결과를 빨리 알 수 있으면 효율적이긴 한데 스핀락처럼 계속 결과가 안 나오면 CPU가 낭비될 수 있다.
  • priority가 낮은 디바이스에 대해서는 서비스되지 않을 수 있다.

Interrupt-driven I/O

  • I/O가 끝났거나 에러가 생겼거나 하면 인터럽트를 보내서 알려주게 된다.
  • 각 디바이스에 맞는 특정한 ISR이 호출된다.
  • 여러 디바이스가 동일한 인터럽트를 공유할 수 있다.
  • CPU는 디바이스가 필요로 할 때만 사용된다.
  • 일반적으로 폴링 I/O에 비해 효율적이다.
  • 인터럽트를 처리하는 데에도 오버헤드가 생긴다. 몇 바이트 단위의 작은 데이터에 대해서도 인터럽트가 계속 발생하면 오버헤드가 더 커질 수 있다.
  • 과도한 인터럽트는 CPU가 인터럽트를 처리하느라 다른 일을 못할 수 있다.

위의 Interrupt-driven I/O의 작동 과정을 기억하자!


Programmed I/O, Direct Memory Access

Input : 디스크에 있는걸 IO 장치의 버퍼에 써서 메인 메모리에 쓰는 과정

Output : 메인 메모리에 있는걸 IO 장치의 버퍼에 써서 디스크로 쓰는 과정

Input, Output의 버퍼를 통해 데이터를 옮기는 과정, 즉 실제로 데이터를 주고받는 방식은 두 가지로 나뉜다. 

 

Programmed I/O

  • CPU가 I/O device와 memory 사이에서 데이터를 직접 옮기는 방식이다.
  • 특별한 I/O instruction 혹은 memory-mapped I/O로 구현될 수 있다.

DMA

  • 메모리 속도와 비슷한 속도의 I/O device에서 사용된다.
  • I/O 장치가 명령을 받으면 디바이스 컨트롤러가 버퍼에 있는 데이터를 메모리로 직접 전송한다. CPU의 개입이 전혀 일어나지 않는다.
  • 데이터 블록의 전송이 완료되면 인터럽트를 발생시키기에 블록 단위로만 인터럽트를 처리하여 CPU 오버헤드가 더 줄어든다.
  • 대량의 데이터 전송을 위한 Programmed I/O를 피하기 위해 사용된다.
  • 적은 양의 데이터는 Programmed I/O가 더 빠르다. DMA는 오버헤드가 존재하기 때문이다.

DMA의 작동 방식을 자세히 알아보자.

  1. CPU가 DMA 컨트롤러의 레지스터를 설정한다. 데이터를 전송할 메모리 주소, 전송할 데이터의 count(크기), 제어 정보가 포함된다.
  2. DMA 컨트롤러가 메모리로 transfer request
  3. data transfer, 이 과정에서 CPU 개입이 일어나지 않는다.
  4. 전송 완료 및 ACK 전송, DMA에게 전송이 끝났다고 알려준다.
  5. DMA 컨트롤러는 CPU에게 작업이 끝났음을 알리기 위해 인터럽트를 발생시킨다.

좀 더 자세히 알아보자!

  1. device driver가 disk data를 메모리의 buffer address X에 전송하도록 CPU로부터 명령을 받는다.
  2. device dirver는 disk controller에게 C bytes 만큼 디스크에서 읽어서 메모리 버퍼 X로 전송하라고 말한다.
  3. disk controller는 DMA transfer를 시작한다.
  4. disk controller는 각 바이트를 DMA controller에 보낸다.
  5. DMA controller가 bytes를 C가 0이 될 때까지 버퍼 X로 보내고 메모리 주소 X는 증가시키고 C는 감소시킨다. 
  6. C가 0이 되면 CPU에게 전송이 끝났음을 알리기 위해 인터럽트를 발생시킨다.

I/O 작업이 실행되면 해당 작업을 호출한 프로세스는 어떻게 될까? 두 방식으로 나뉜다.

Blcoking, Non-Blocking I/O

Blocking

  • I/O 작업이 끝날 때 까지 프로세스가 정지된다. 
  • 이해하기 쉽고 사용하기도 쉽다. 프로그래머는 I/O 작업이 끝나는 시점을 명확하게 알 수 있다.
  • read(), write()가 여기에 해당된다. I/O 작업이 오래 걸리는 경우 비효율적이다.

Non-Blocking I/O

  • I/O call이 즉시 반환되며, 반환 값은 몇 bytes가 전송되었는지가 된다.
  • 멀티쓰레딩으로 구현된다. 따라서 I/O 작업을 처리하는 동안 다른 스레드가 실행될 수 있다.
  • select() 함수로 데이터가 준비되었는지 확인할 수 있다.