본문 바로가기

CS

입/출력 장치 개념

I/O 장치

영속성 이야기 전에 입/출력 장치의 개념을 소개하고 운영체제가 이 장치들과 상호 작용하는 방법을 알아본다. 밑의 그림을 보면 CPU와 주메모리가 메모리 버스로 연결되어 있고,  몇 가지 장치들이 범용 I/O 버스에 연결이 되어 있다.  많은 현대 시스템에서는 PCI(많은 파생 버스)를 사용한다. 여기에 그래픽이나 다른 고성능 I/O 장치들이 연결되고 그 밑에 SCSI나 SATA 또는 USB 같은 주변장치용 버스가 있다. 여기서 디스크나 마우스 같은 느린 장치들이 연결된다, 가까울수록 고속화 버스를 사용하는데 여기에 고성능 장치들이 배치된다. 비용적인 문제 때문.

다음은 가상의 표준 장치를 살펴 보자 두 개의 중요한 요소가 있다. 첫 번째는 시스템의 다른 구성 요소에게 제공하는 하드웨어 인터페이스이다. 시스템 소프트웨어가 동작을 제어할 수 있도록 해야 한다.  따라서 모든 하드웨어 장치들은 특정한 상호동작을 위한 방식과 명시적인 인터페이스를 갖고 있다. 두 번째 요소는 내부 구조 시스템에게 제공하는 장치에 대한 추상화를 정의하는 책임을 갖고 있음. 

위에서 보인 그림에서 장치의 인터페이스는 3개의 레지스터로 구성된다. 상태는 장치의 현재 상태이고, 명령어 레지스터는 장치가 특정 동작을 수행하도록 요청할 때, 그리고 데이터는 장치에 데이터를 보내거나 받거나 할 때 사용됨. 이 레지스터들을 읽거나 쓰는 것을 통해 운영체제는 장치의 동작을 제어

방식은 4단계로 이루어졌다. 상태 레지스터를 반복적으로 읽어서 수신 가능 여부를 확인함. 이 동작을 폴링한다고 표현함. 그 뒤 운영체제가 데이터 레지스터에 데이터 전달, 데이터 전송에 메인 CPU가 관여하는 경우를 programmed I/O라고 부른다. 세 번째로 운영체제가 명령 레지스터에 명령어를 기록. 명령어가 기록되면 데이터는 이미 준비되었다고 판단하고 명령어를 처리한다.  마지막으로 폴링 반복문을 돌면서 처리되었는지 기다린다. 이 방식은 매우 비효율적이다. 다른 프로세스에게 CPU를 양도하지 않고 장치가 동작 완료될 때까지 체크함. 너무 느리다. 그럼 어떻게 비용을 줄이는가. 첫 번째는 인터럽트를 이용한 CPU 오버헤드 개선이다. 기존에 입출력 작업을 요청한 프로세스를 블록 시키고 CPU를 다른 프로세스에게 양도한다. 장치가 작업을 끝마치고 나면 하드웨어 인터럽트를 발생시키고 CPU는 운영체제가 미리 정의해놓은 인터럽트 핸들러를 실행. 

기존 폴링 방식

 

인터럽트 사용

하지만 인터럽트가 항상 해법은아닌게 빠른 장치에서는 오히려 context switch 하고 인터럽트를 처리하는 것이 오히려 오래 걸린다. 그냥 폴링 하는 게 빠름. 폴링 하다가 인터럽트 하는 하이브리드 방식도 있다.

인터럽트 사용하지 않은 예시로는 네트워크가 있다. 패킷마다 인터럽트가 발생하는데, 사용자가 많아 버리면 요청을 처리 못하고 인터럽트만 처리 하다가 livelock에 빠질 수도 있다. 이 경우 폴링을 사용하면 시스템 상황을 효율적으로 사용. 다른 인터럽트 최적화 기법은 병합이 있는데, 이건 일정 시간 대기하다가 인터럽트 발생 근데 또 너무 기다리면 지연 시간이 늘어나기 때문에 시스템 절충 시간을 찾아야 됨.

다음은 DMA 방식에 대해 알아보자. 많은 양의 데이터를 디스크로 전달하기 위해 PIO를 사용하면 CPU가 또다시 단순 작업 처리에 소모된다.  디스크에서 한 워드씩 복사 후 디스크에서 I/O 처리를 시작.

programmed I/O
DMA 방식

반면에 Direct Memory Access 방식은 DMA엔진이라는 특수 장치에서 메모리 상의 데이터 위치와 전송할 데이터의 크기와 대상 장치를 프로그램한다. 그 시점에 전송하기 위해 할 일은 끝나기 때문에 운영체제는 다른 일을 진행할 수 있다. DMA가 끝나면 DMA 컨트롤러가 인터럽트를 발생시켜 완료를 알림.

장치와 효율적으로 통신하는 건 알겠는데, 그럼 어떻게 통신하는가?? 2가지 방법이 있다. 첫째는 명시적으로 특권 명령어를 사용해 장치들과 직접 통신한다. 위험할 수 있다. 두 번째는 memory mapped I/O 방식으로 하드웨어 장치의 레지스터들이 마치 메모리 상에 존재하는 것처럼 만든다. 운영체제는 특정 레지스터 접근 위해 해당 주소에 직접 읽기 쓰기를 하면 됨. 

최종적으로 다룰 문제는 서로 다른 인터페이스를 갖는 장치들과 운영체제를 연결시키는 가능한 일반적인 방법을 찾는 것이다. 여기서 추상화를 사용하여 디자인했다. 운영체제 최하위 계층의 일부에 소프트웨어는 디바이스 동작 방식을 반드시 알고 있어야 한다. 이게 디바이스 드라이버이다. 그럼 어떻게 캡슐화되어 있나. 리눅스 파일 시스템 소프트웨어 계층을 보면 디자인과 구현을 알 수 있다.

파일 시스템은 어떤 디스크 종류를 사용하는지 전혀 알지 못한다. 파일 시스템은 범용 블록 계층에 블럭 read/write 요청할 뿐이다. 범용 블럭 계층은 적절한 디바이스 드라이버로 받은 요청을 전달하며, 디바이스 드라이버는 특정 요청을 장치에 내리기 위해 필요한 일들을 처리. 캡슐화의 단점은 특수 기능을 많이 갖고 있는 어떤 장치가 있다고 했을 때, 커널이 범용적인 인터페이스만을 제공할 수밖에 없다면 특수 기능 사용 못한다. 흥미로운 것은 어떤 장치든 디바이스 드라이버 코드가 필요하기 때문에 운영체제 코드의 70% 이상이 디바이스 드라이버를 위한 코드이다. 

'CS' 카테고리의 다른 글

RAID  (0) 2021.10.26
하드 디스크 드라이브  (0) 2021.10.25
Threads  (0) 2021.10.22
Pintos Project3: Virtual Memory  (0) 2021.10.21
Pintos Project2: User Programs  (0) 2021.10.13