입출력 시스템이란 무엇인가?
컴퓨터의 주요한 두가지 작업은 연산작업 과 입출력 작업 입니다. 많은 경우 연산 작업 보다는 입출력 작업이 중요한데, 가령 우리가 인터넷 서핑을 하거나 혹은 문서작업을 하는 경우 대부분은 컴퓨터 내의 저장된 파일을 열거나 작성하는 경우가 다반사 이기 때문입니다. 그만큼 컴퓨터는 설치된 입출력 장치들과 원활하게 소통해야 하고 운영체제는 이러한 입출력이 잘 이루어 지도록 할 필요가 있습니다. 본 강의 에서는 어떻게 컴퓨터에 연결된 다양한 입출력 하드웨어 장치들과 소통이 이루어 지는지 를 공부하고 그 과정에서 하드웨어 인터페이스 및 소프트웨어 인터페이스의 간극을 운영체제가 어떻게 해결하는 지에 대해 알아봅니다.
컴퓨터에 연결될 장치들을 제어하는 일은 운영체제의 주요한 관심사 입니다. 다양한 장치들은 기능이나 속도면에서 다양한 특성을 보이기 때문에 각각에 맞는 제어가 필요하고, 이와 같은 다양한 제어 방법들이 커널의 입출력 서브 시스템을 형성하여 커널의 다른 부분이 입출력장치를 관리하는 복잡한 일에 신경쓰지 않게 해줍니다.
이렇게 입출력을 관리하기 위한 기술들이 지향하는 바는 새로 나오는 장치들이 기존 시스템에 쉽게 결합될 수 있게 하는 것입니다. 가령 무수히 쏟아져 나오는 마우스, 키보드, 모니터 등 다양한 장치들이 내 컴퓨터와 잘 동작하게 하려면 둘 사이에 무언가 공통된 인터페이스가 존재해야 하며 그 역할을 수행하는 것이 입출력 관리의 핵심이라고 할 수 있습니다. 때문에 이런 인터페이스의 표준화 는 입출력 관리에서 매우 중요합니다. 운영체제 커널 이 이렇게 다양한 입출력 장치들의 차이를 가려주기 위해서 장치 구동기 모듈 을 사용합니다. 장치 구동기는 모든 하드웨어를 일관된 인터페이스로 표현해 주며, 이러한 인터페이스를 그보다 상위층인 커널의 입출력 서브시스템에 제공해 줍니다.
입출력 하드웨어
입출력 하드웨어의 구성
다양한 입출력 장치들이 컴퓨터와 동작을 하는 원리를 알기 위해서 우리는 이런 입출력 장치들이 어떻게 구성되어 있는지 살펴 볼 필요가 있습니다. 이런 입출력 장치들은 크게 저장 장치, 전송 장치, 사용자 인터페이스 장치 등으로 나뉘어집니다. 하지만, 이러한 다양한 입출력 장치가 어떻게 운영체제와 동작하는지 알기 위해서 우리는 몇가지 표준적인 개념만 이해하면 됩니다.
하드웨어 장치는 케이블을 통하거나 무선으로 신호를 보냄으로써 컴퓨터 시스템과 통신하며, 포트 라고 불리는 연결점을 통해 컴퓨터에 접속합니다. 만약 하나 이상의 장치들이 공동으로 여러 선들을 사용한다면 이것을 버스 라고 부릅니다. 여기서 버스는 단순히 선만을 의미하는 것이 아니라 각 선에 어떤 전기적 신호를 보내어 통신이 이루어지는 지를 약속하는 프로토콜 까지를 포함하는 개념입니다. 하드웨어 장치의 또 다른 구성요소는 바로 제어기 입니다. 제어기란 포트나 버스나 입출력 장치를 제어하는 전자회로의 집합체이며 많은 입출력 장치는 제어기를 내장하고 있습니다.
그렇다면 컴퓨터는 어떻게 장치의 제어기에서 입출력을 하도록 명령할 수 있을까요?
모든 제어기는 레지스터를 가지고 있고 컴퓨터의 프로세서는 제어기의 레지스터에 비트 패턴을 쓰거나 읽음으로써 입출력을 실행합니다. 다른 방법으로는 장치 제어 레지스터를 프로세서의 주소 공간으로 사상하는 방법이 있고 이것을 메모리 맵 입출력 이라고 부릅니다. 이 경우에는 각 주변장치 레지스터들은 메모리 주소와 일대일 대응이 되고, 컴퓨터는 이러한 메모리 주소에 데이터를 읽고 쓰는 것으로 장치 제어기의 레지스터에 직접 데이터를 읽고 쓰는 역할을 수행하게 할 수 있습니다. 하지만 이 경우에는 잘못된 포인터 등의 오류로 현재 선점 중인 메모리 주소에 임의값을 작성하는 경우 입출력 시스템에 문제가 생기게 되는 문제점이 있습니다.
다음은 장치의 입출력 포트가 어떻게 구성되어 있는지 알아봅시다. 장치의 입출력 포트 는 보통 4개의 레지스터로 구성되어 있는데, 상태, 제어, 입력, 출력 레지스터가 그것입니다.
입출력 하드웨어의 동작
입출력 하드웨어가 어떻게 생겼는지 알아보았으니, 이제는 그 동작을 알아보도록 합시다.
폴링
컴퓨터와 입출력 하드웨어 사이의 프로토콜을 복잡하지만 기본적인 핸드셰이킹 개념은 간단합니다. 장치의 제어기의 레지스터 에는 비지 비트 라는 것이 존재하는데, 이것은 현재 장치가 사용가능한 상태인지 아니면 다른 작업을 처리중이라 사용이 불가능 한지를 나타냅니다. 제어기는 작업이 하느라 바쁠 때에는 비지 비트를 1로 설정하고 준비 중인 경우에는 0으로 설정하여 컴퓨터가 현재 장치가 사용중인지를 알 수 있게 해줍니다. 그 순서는 다음과 같습니다.
여기서 컴퓨터는 시시때때로 장치가 사용중인지를 검사하기 위해 비지 비트 를 검사해야 하는데, 이것을 계속 돌면서 반복한다고 하여 폴링 이라고 부릅니다. 이러한 폴링에는 컴퓨터 자원이 많이 소요되지 않지만(3 사이클 정도) 장치가 준비하는 시간이 길어지면 매우 비효율적이며, 이 대신 하드웨어가 제어기가 자신의 상태가 바뀔 때 컴퓨터에 통보를 해 주면 이렇한 비효율을 막을 수 있으며 이를 인터럽트 라고 합니다.
인터럽트
인터럽트의 기반 메커니즘은 다음과 같습니다. CPU는 인터럽트 요청 라인 이라고 불리는 선을 가지는데, CPU는 매 명령어를 끝내고 다음 명령어를 수행하기 전에 이 선을 검사합니다. 만약 입출력 장치가 준비가 완료되어 인터럽트 요청 라인 에 신호를 보내면 CPU는 하나의 명령을 끝낸 시점에 인터럽트를 확인하고 인터럽트 핸들러 를 실행합니다. 여기서 인터럽트 핸들러 란 입출력 장치를 서비스함으로써 이 인터럽트를 처리해 주는 것입니다. 하지만 현대의 시스템에서는 이러한 인터럽트를 처리함에 있어 보다 세분화된 인터럽트 핸들링이 필요했고, 다음은 인터럽트 핸들러가 수행해야 하는 기능을 보여줍니다.
- 임계영역을 실행 중에는 인터럽트 처리를 연기시키는 능력이 필요하다.
- 어떤 장치가 인터럽트를 일으켰는지 조사하기 위해 모든 장치를 폴링하지 않고 적절한 인터럽트 핸들러로 이동하는 효율적인 방법이 필요하다. 즉, 인터럽트 요청 라인에 요청이 들어왔을 때 어디서 들어온 요청인지 바로 알아야 한다.
- 운영체제가 높은 우선순위와 낮은 우선순위를 구분하고 긴급한 정도에 따라 응답하기 위한 다수준 인터럽트 가 필요하다.
위의 세가지 기능을 제공하기 위해 CPU와 인터럽트 제어기 하드웨어를 통하여 구현하고 있습니다.
대부분의 CPU는 두 종류의 인터럽트 요청 라인 마스크 불가 인터럽트, 마스크 가능 인터럽트 를 가지고 있습니다. 여기서 마스크 불가 인터럽트는 회복 불가능한 메모리 에러와 같은 이벤트를 처리하며, 마스크 가능 인터럽트는 필요 시 잠시 중단시켜 놓을 수 있는 인터럽트를 처리합니다.
보통의 인터럽트 기법에서는 인터럽트 요청을 할 때 주소라고 하는 하나의 정수를 받아들이는 데 이것은 인터럽트 핸들러들의 메모리 주소들을 가지고 있는 인터럽트 벡터 라 불리는 데이블의 인덱스값으로 사용됩니다. 이러한 벡터형 인터럽트 기법 은 인터럽트 벡터 를 활용하여 모든 가능한 인터럽트의 진원지를 찾아야 할 필요를 줄여 주지만 컴퓨터는 인터럽트 벡터 내에 있는 주소들보다 더 많은 수의 장치를 가지고 있고 이 문제를 해결하기 위해 인터럽트 사슬화 를 사용합니다. 인터럽트 사슬화에서 인터럽트 벡터의 각 원소들은 인터럽트 핸들러 리스트 의 헤더를 가르키고 있고, 만약 인터럽트가 일어나면 해당 핸들러를 찾을 때까지 리스트 상의 핸들러들을 하나씩 검사하게 됩니다.
직접 메모리 접근(Direct Memory Access)
이제까지 컴퓨터와 입출력 장치가 어떻게 구성되어 있고, 어떻게 동작하는 지를 알아보았습니다. 그렇다면 입출력 장치와 컴퓨터 사이의 데이터는 어떤 방식으로 주고 받을까요? 만약 CPU를 사용하여 디스크와 같은 대용량 입출력 장치의 데이터를 읽어들인다면 CPU의 사용량이 매우 높아지고 이는 컴퓨터 성능을 심각하게 저하시킬 것입니다. 즉, CPU가 매번 바이트 전송을 제어하는 것은 심한 낭비인 것이죠. 이렇게 CPU가 1바이트씩 옮기는 입출력 방식을 PIO 라고 부릅니다.
많은 컴퓨터들은 이렇게 CPU의 낭비를 막기 위해 PIO를 DMA 제어기 라고 불리는 특수 프로세서에게 위임함으로써 CPU의 일을 줄여줍니다. 그 과정은 다음과 같이 진행됩니다
먼저, 컴퓨터(호스트)는 메모리에 DMA 명령 블록 을 씁니다. 이 블록에는 전송할 데이터가 있는 곳의 포인터와 전송할 장소에 대한 포인터 그리고 전송될 바이트 수를 기록해 놓습니다. 그러면 CPU는 DMA 명령 블록의 주소를 DMA에게 알려주고 자신은 다른 일을 처리합니다. 그러면 DMA는 CPU의 도움 없이 자신이 직접 버스를 통해 DMA 명령 블록을 액세스하여 입출력을 실행합니다.
그러면 DMA 제어기와 장치 제어기는 어떻게 연결될까요?
이 둘의 핸드셰이킹은 DMA request, 와 DMA acknowledge 라고 불리는 두 개의 선을 통해서 실행됩니다. 장치 제어기는 전송할 자료가 생기면 DMA request를 통해 장치 제어기에서 데이터 전송을 요청합니다. 그러면 DMA 제어기가 메머리 버스를 얻어 거기에 원하는 주소를 올려 놓고 DMA acknowledge 신호를 보냅니다. 장치 제어기가 DMA acknowledge 신호를 받으면 제어기는 한 워드를 메모리로 전송하고 DMA request를 제거합니다. 그리고 전송이 끝나면 DMA 제어기는 CPU에게 인터럽트를 걸어 전송이 완료되었음을 알립니다. 이 과정에서 DMA 제어기가 메모리 버스를 점유 중이면 주 메모리는 주캐시와 보조캐시에 있는 데이터에는 접근할 수 있지만 주 메모리에 있는 데이터는 접근을 할 수 없게 되어 CPU의 속도를 저하시키지만 전체적으로 보았을 때에는 입출력 작업을 DMA로 넘기는 것은 시스템 성능을 향상 시킵니다.
끄어 이건 강좌가 아니고 책 수준이지 않습니까. :) 잘 쓰신 글 잘 보고 갑니다. 보팅하고 팔로우해요~
감사합니다 ㅎㅎ
기술블로그를 하시는 군요! 많이 배우겠습니다 ㅎㅎ