컴퓨터의 두 가지 주요 작업은 계산(computing)과 입출력 작업(I/O)입니다.
입출력 시 운영체제의 역할은 입출력 작업 및 입출력 장치를 관리하고 제어하는 일입니다.
입출력 장치 관련 기술이 빠르게 발전하면서 갈수록 다양해지고 있고, 관련 인터페이스의 표준화도 늘어나고 있습니다.
서로 다른 장치들의 다양성을 가려주기 위해서 운영체제 커널은 장치 드라이버 모듈(device driver module)을 사용하도록 구성되어 있습니다.
장치 드라이버는 모든 하드웨어를 '일관된 인터페이스'로 표현해 주며 이러한 인터페이스를 그보다 상위층인 커널의 입출력 서브시스템에 제공해 줍니다.
많은 종류의 장치들이 컴퓨터에서 작동됩니다.
하드웨어 장치는 케이블을 통하거나 무선으로 신호를 보냄으로써 컴퓨터 시스템과 통신합니다.
이들 장치는 포트(port)라 불리는 연결점을 통해 컴퓨터와 접속합니다.
만약 하나 이상의 장치들이 공동으로 여러 선(wire)을 사용한다면, 이러한 선을 버스(bus)라고 부릅니다.
위의 그림은 PCI(일반적인 PC system bus)가 프로세서-메모리 서브시스템을 고속 장치와 키보드와 직렬, USB 포트처럼 상대적으로 느린 장치들을 확장 버스(expansion bus)에 연결하는 모습을 나타내고 있습니다.
컨트롤러는 포트, 버스 또는 장치를 작동할 수 있는 전자장치 집합체입니다.
컨트롤러는 직렬 포트 혹은 광섬유 채널(FC) 버스에 있을 수 있으며, 일부 장치는 자체 컨트롤러를 내장하고 있습니다.
모든 컨트롤러는 (제어용 또는 데이터용) 레지스터를 가지고 있습니다.
본체의 프로세서는 이들 컨트롤러의 레지스터에 비트 패턴을 쓰거나 읽음으로써 입출력을 수행합니다.
이러한 통신을 수행하는 방법은 2가지가 존재합니다.
특별한 입출력 명령어는 한 바이트나 워드를 어떤 입출력 포트 주소로 전달하도록 지정합니다.
입출력 명령은 해당 장치에 맞는 '버스 회선'을 선택하여 장치 레지스터로 비트들을 보내거나 읽어 오도록 촉발합니다.
메모리 맵드 입출력을 사용할 경우 각 주변 장치 레지스터들은 메모리 주소와 '일대일 대응'됩니다.
CPU는 물리 메모리에 사상된 장치-제어 레지스터를 읽고 쓸 때 '표준 데이터 전송 명령'을 사용함으로써 입출력 요청을 수행하게 됩니다.
오늘날 대부분의 I/O는 메모리 맵드 I/O를 사용하여 장치 컨트롤러가 수행합니다.
입출력 장치 컨트롤러는 보통 네 개의 레지스터로 구성되어 있습니다.
호스트와 입출력 하드웨어 사이의 프로토콜은 복잡하지만 기본적인 핸드셰이킹(hand-shaking) 개념은 간단합니다. 다음과 같은 절차로 수행됩니다.
단계 1에서 호스트는 바쁜 대기(busy-waiting), 즉 폴링(polling)을 하게 됩니다.
기다리는 기간이 짧다면 문제가 없지만 길어진다면, 호스트는 다른 태스크로 전환하여 다른 일을 하다가 오는 것이 좋을 것입니다.
기본적으로 한 장치를 폴하기 위해 세 개의 CPU 사이클로 충분할 정도로 빠른 성능을 보여주지만, 장치가 서비스할 준비가 되는 데에 시간이 오래걸리면 호율이 떨어지게 됩니다.
이와 같은 경우에는 하드웨어 컨트롤러가 자신의 상태가 바뀔 때 CPU에 그것을 통보해 주는 것이 반복적으로 폴링을 하는 것보다 더욱더 효율적입니다.
입출력 장치가 CPU에 자신의 상태 변화를 통보하는 하드웨어 기법을 인터럽트(interrupt)라고 합니다.
CPU 하드웨어는 입터럽트 요청 라인(interrupt request line)이라고 불리는 선을 하나 갖는데, CPU는 '매 명령어를 끝내고' 다음 명령어를 수행하기 전에 늘 이 선을 감지합니다.
CPU가 신호는를 감지하면 각종 레지스터 값과 상태 정보를 저장한 다음, 메모리 상의 인터럽트 핸들러 루틴으로 이동(jump)합니다.
이후 동작은 Part 1에서 충분히 다루었기 때문에 넘어가도록 하고, 현대 운영체제는 더욱 세분된 인터럽트 핸들링 방법을 필요로 합니다.
현대 컴퓨터는 이들 요소를 CPU와 인터럽트 컨트롤러 하드웨어에 제공하고 있습니다.
대부분의 CPU는 주 종류의 인터럽트 요청 라인을 가집니다.
또한, 많은 인터럽트 기법을 다루기 위해서 인터럽트 벡터를 사용하고 인터럽트 사슬화(chaining) 기술을 사용합니다.
인터럽트 벡터를 사용하여 인터럽트 핸들링 루틴이 있는 메모리 주소들을 가지고 있습니다.
테이블에서 인터럽트 벡터를 탐색하여 인터럽트 핸들링 루틴의 '집합체'를 가져올 수 있습니다.
그리고 인터럽트 벡터의 각 원소가 여러 인터럽트 핸들러로 이루어진 리스트의 형태로 만들어주는 인터럽트 사슬화 기술을 적용하여 필요한 루틴을 찾습니다.
또한, 인터럽트는 인터럽트 우선순위 수준(interrupt priority levels)의 구현을 가능하게 합니다.
이러한 수준들은 CPU가 모든 낮은 우선순위 인터럽트를 일일이 마스크 오프(mask-off)시키지 않더라도 자동으로 높은 우선순위 인터럽트가 낮은 우선순위 인터럽트의 실행을 선점(preempt)할 수 있게 합니다.
디부분의 경우 인터럽트 처리는 시간과 자원이 제한되어 구현하기가 복잡하기 때문에 시스템은 인터럽트 관리를 나눕니다.
가상 메모리에서 인터럽트를 다루는 상황을 살펴보자. 앞서 가상 메모리 파트에서 다뤄봤지만 여기서는 인터럽트에 집중하여 확인해 봅시다.
페이지 폴트는 인터럽트를 일으키는 예외입니다. 페이지 폴트가 발생했다고 가정해 봅시다.
또 다른 예는 시스템 콜(system call)의 수행입니다.
일반적으로 응요 프로그램은 시스템 콜을 수행하기 위해 라이브러리 루틴을 호출합니다.
그 라이브러리 루틴은 '호출 인자'를 점검하고, 커널로 인자를 넘겨주기 위한 자료구조를 구성하고, 소프트웨어 인터럽트(software interrupt) 또는 트랩(trap)이라고 하는 특수한 명령어를 수행합니다.
이 명령어는 커널 서비스를 확인하는 피연산자를 갖습니다.
정리하면, 인터럽트는 모든 현대 시스템에서 비동기적으로 일어나느 이벤트를 처리하고, 커널 내의 슈퍼바이저 루틴을 달려가기 위한 방도로 사용됩니다.
따혼 이러한 일 중에서도 가장 급한 일부터 차례로 수행하기 위해서 현대의 컴퓨터들은 인터럽트 간에도 다른 우선순위를 부여합니다.
인터럽트 구동 I/O는 폴링보다 훨씬 일반적이지만, 폴링은 높은 처리량을 보이는 I/O에서 사용되기도 하며, 둘이 함께 사용되기도 합니다.
CPU가 상태 비트를 반복적으로 검사하면서 1바이트씩 옮기는 입출력 방식을 PIO(Programmed I/O)라고 부릅니다.
컴퓨터는 CPU의 PIO 작업 중 일부를 DMA 컨트롤러라고 불리는 특수 프로세서에 위임함으로써 CPU의 일을 줄여 줍니다.
DMA 전송을 시작시키기 위해서 호스트는 메모리에 DMA 명령 블록을 씁니다.
이 블록에는 다음과 같은 것들이 기록됩니다.
CPU는 이 DMA 명령 블록 주소를 DMA에게 알려주고 자신은 다른 일을 합니다. 그러면 DMA는 CPU의 도움 없이 자신이 직접 버스를 통해 DMA 명령 블록을 액세스하여 입출력을 수행하게 됩니다.
대상 주소가 커널 주소 공간에 있다면 문제없지만, 만약 사용자 공간에 있는 경우에는 사용자가 전송 중에 해당 공간의 내용을 수정할 수 있고 그러면 일부 데이터를 잃어버릴 수 있습니다.
그러나 DMA로 전송된 데이터를 스레드가 액세스 할 수 있게 하려면 커널 메모리에서 사용자 메모리로 두 번째 복사 작업이 요구되는데, 이러한 이중 버퍼링은 비효율적입니다.
따라서 운영체제는 장치와 사용자 주소 공간 간에 직접 I/O 전송을 수행하기 위해 메모리 매핑을 사용하게 되었습니다.
DMA 컨트롤러와 장치 컨트롤러 간의 핸드셰이킹은 DMA-request와 DMA-acknowlege라고 불리는 두 개의 선을 통해 수행됩니다.
다음과 같은 순서로 진행됩니다.
여기서 추가적으로 정리하고 넘어가도록 하겠습니다.
우선, 짚고 넘어가야할 부분이 있습니다.
3번의 동작을 봐보면, 장치 컨트롤러가 데이터를 이동시켜주는 작업을 수행하는 것 같은데 그렇다면 DMA 컨트롤러의 역할은 무엇일까?
이것을 이해하기 위해서 몇 가지 필요한 개념을 정리하겠습니다.
DMA 컨트롤러는 중앙 컨트롤러인가, 아니면 장치 컨트롤러인가?
1. DMA 컨트롤러의 존재와 사용:
일부 시스템에서는 하나의 중앙 DMA 컨트롤러가 여러 장치의 데이터 전송을 관리합니다. 이 경우, 중앙 DMA 컨트롤러는 여러 장치 컨트롤러와 통신하여 필요에 따라 메모리와의 데이터 전송을 조율합니다.
다른 시스템에서는 각 장치 또는 장치 그룹마다 별도의 DMA 기능을 내장한 컨트롤러를 사용할 수도 있습니다.
예를 들어, 고성능의 네트워크 인터페이스 카드(NIC)는 자체적인 DMA 기능을 갖추고 있어, 네트워크 데이터의 효율적인 전송을 담당할 수 있습니다.
2. 장치 드라이버와 장치 컨트롤러:
장치 드라이버는 운영 체제와 하드웨어 장치 간의 통신을 중개하는 소프트웨어입니다.
일반적으로 각 하드웨어 장치마다 또는 장치 유형마다 전용 장치 드라이버가 있으며, 이를 통해 운영 체제는 다양한 하드웨어 장치와 통신할 수 있습니다.
장치 컨트롤러는 특정 하드웨어 장치를 관리하는데 사용되는 전자부품으로, 드라이버와 함께 동작하여 '장치의 데이터 전송' 및 '기타 기능'을 제어합니다.
장치 드라이버는 하드웨어 장치 컨트롤러의 추상화된 인터페이스 역할을 수행합니다.
마지막으로 장치 드라이버의 역할을 정리하자면 다음과 같습니다.
이제, 3번의 내용을 정리할 수 있습니다.
결국, 장치 컨트롤러가 데이터를 이동시켜주는 역할을 수행하는 것은 자신의 역할을 수행한 것입니다.
하지만 데이터를 이동시켜주기 위해서는 메모리 버스를 사용해야 됩니다. 즉, 이동시킬 수 있는 권한이 필요하다고 볼 수 있고 메모리 버스의 사용 권한을 관리하는 것이 DMA 컨트롤러의 역할입니다.
DMA 컨트롤러 역할:
DMA 컨트롤러의 주된 역할을 메모리 버스의 사용 권한을 관리하고, 데이터 전송의 시작과 완료를 조율하는 것입니다. 또한, 전송이 완료되면 CPU에 인터럽트를 보내어 작업의 완료를 알립니다.
다시 동작 과정으로 돌아와서,
DMA가 메모리 버스를 '점유 중'이면 비록 CPU는 캐시에 있는 데이터는 접근할 수 있지만, 일시적으로 주메모리에 있는 데이터는 접근하지 못합니다.
이러한 사이클 스틸링(cycle stealing)은 CPU의 속도를 저하하지만 입출력 작업을 DMA로 넘기는 것은 전체적으로 시스템 성능이 향상합니다.
여기서는 운영체제가 인터페이스를 구성하는 기술에 대해서 설명을 합니다.
예를 들어 응용 프로그램은 디스크가 무슨 종류인지 알 필요 없이 그 디스크에 있는 파일을 오픈할 수 있고, 새로운 디스크가 나오면 기존 운영체제에 혼란을 주지 않고도 이를 쉽게 추가할 수 있어야 합니다.
이러한 기술을 적용하기 위해서 다음과 같은 기법들이 고려되어야 합니다.
장치 드라이버(device driver)라고 부르는 커널 내의 모듈들은 각 입출력 장치를 위한 구체적인 코딩을 제공하여 바로 위에서 정의한 인터페이스의 표준 함수
들을 내부적으로 수행합니다.
장치 드라이버의 목적은 여러 입출력 하드웨어 간의 차이를 숨기고, 이들을 간단한 표준 인터페이스들로 보이도록 포장해서, 이것을 상위의 커널 입출력 서브시스템에 제공하는 것입니다.
그렇다면, 장치를 제조할 때 신경써야 하는 것은 무엇일까요?
장치 드라이버 인터페이스가 운영체제에서 정상적으로 동작하도록 해줘야 합니다.
아래에는 입출력 장치들이 가지는 일반적인 특징들에 대해서 적어놨습니다.
블록 장치 인터페이스는 디스크나 이와 유사한 블록 지향(block-oriented) 장치를 사용하기 위해 필요한 모든 요소를 제공하고 있습니다.
응용 프로그램은 보통 '파일 시스템 인터페이스'를 통해 이 블록 인터페이스에 접근합니다.
만약, 블록 장치를 사용하는 와중에 응용이 자체 버퍼링을 수행하거나 자체 잠금 기능을 제공한다면 운영체제에서 제공하는 기능과 중복될 수 있습니다.
이에 대해 보편화되고 있는 절충안은 운영체제가 버퍼링과 잠금을 하지 않는 모드로 파일에 대한 입출력 작업을 하는 것입니다.
UNIX 시스템에서는 이런 방식을 직접 입출력(direction I/O)라고 합니다.
이러한 방식을 사용하는 경우는 복잡한 설계를 요구하기 때문에, 잘 사용하지 않지만 대규모 파일 시스템 관리 혹은 실시간 데이터 처리를 위해서 사용할 때가 있다고 합니다.
메모리 맵드(memory mapped) 파일 접근은 블록 장치 위의 층으로 구현할 수 있습니다.
메모리 맵드 파일 접근이란 실제로 "디바이스를 읽거나 쓰는 명령"을 사용하는 대신 "메모리의 특정 번지를 읽거나 쓰는 명령"으로 파일 입출력을 대신하는 방법입니다.
대부분의 컴퓨터는 하드웨어 클록과 타이머를 가지고 3가지 기본적인 기능들을 제공합니다.
지나간 시간을 재고 특정 오퍼레이션을 실행시키는 하드웨어를 프로그램 가능 인터럽트 타이머(programmable interval timer)라고 합니다.
이것은 다음과 같은 형태로 인터럽트를 발생시킬 수 있습니다.
이러한 기법은 다음과 같은 곳에서 활용됩니다.
스케줄러
가 타임 슬라이스가 종료되면 현재 진행 중인 프로세스로부터 CPU를 빼앗기 위해 사용디스크 입출력 서브시스템
이 변경된 캐시 버퍼를 주기적으로 디스크에 쏟아내는데 사용네트워크 서브시스템
이 네트워크 혼잡이나 오류로 인해 어떤 작업을 취소하는데 사용이러한 방식으로 활용되는 것 외에도 사용자 프로세스에서 활용할 수도 있습니다.
커널은 운영체제나 일반 응용 프로그램들이 신청한 시간 관련 인터럽트 요청을 마감시간순으로 늘어놓습니다.
그리고는 타이머를 가장 가까운 마감 시한으로 설정해놓습니다.
그 시간이 다 되어 인터럽트가 걸리면, 요청자에게 신호를 보내고, 타이머를 다음으로 빠른 마감 시간을 설정해 놓습니다.
응용 프로그램이 봉쇄형(blocking) 콜을 하면, 호출 스레드는 봉쇄 상태로 들어가게 됩니다. 즉, 운영체제가 이 스레드를 실행큐로부터 대기 큐로 옮깁니다.
추후 입출력이 끝나면 이 스레드는 다시 실행 큐로 되돌아오며, 실행이 재게되면 응용 프로그램은 입출력 시스템 콜이 되돌려 준 값을 받게 됩니다.
보통 예측하기 쉬운 봉쇄형 시스템 콜을 응용 인터페이스에게 제공하고, 비동기식 시스템 콜은 입출력 장치가 수행하는 작업에서 사용됩니다.
비봉쇄형 시스템 콜의 대안으로 비동기식 시스템 콜이 있습니다.
작업을 봉쇄하지 않고 진행한다는 점에서 유사하지만 다음과 같은 차이점이 존재합니다.
비동기식 시스템의 대표적인 예로, 쓰기를 수행하는 동작을 살펴볼 수 있습니다.
디폴트로 응용이 네트워크 송신 요청 또는 보조저장장치 쓰기 요청을 한 경우, 운영체제는 요청을 주목하면 입출력 요청을 버퍼에 넣고 응용으로 되돌아갑니다.
이것은 버퍼링 되었다가 특정 시간 간격마다 플러시가 수행됩니다.
응용에서의 데이터 일관성은 커널에 의해 유지되며 아직 기록되지 않은 데이터를 읽어올 수 있도록 커널은 입출력 명령을 장치에 내리기 전에 버퍼로부터 데이터를 읽습니다.
일련의 입출력 요구를 스케줄 한다는 것은 그 요구를 실행할 순서를 결정하는 것을 의미합니다.
응용 프로그램이 봉쇄형 입출력 시스템 콜을 하면 그 입출력 요청은 해당 장치의 큐에 넣어집니다.
입출력 스케줄러는 시스템의 성능과 각 응용에 대한 평균 응답 시간을 향상하기 위해 큐 안의 순서를 재배치합니다.
이것은 우선순위에 따라 급한 작업을 빨리 처리할 수 있게 해줍니다.
커널이 비동기적 입출력을 제공한다면, 커널은 동시에 많은 입출력 요청을 추적해야 합니다.
이를 위해 운영체제는 각 장치 상태 테이블(device-status table)에 대기 큐를 연동합니다.
이 테이블에는 각 입출력 장치에 대한 정보가 있습니다.
각 테이블 항목은 장치의 종류, 주소, 상태, 유후, 동작 중 등을 나타냅니다.
만약 장치가 '동작 중' 상태이면, 같은 장치에 대한 요청은 그 장치에 해당하는 테이블 항목에 저장될 것입니다.
버퍼링은 다음 세 가지 이유 때문에 필요합니다.
스풀(spool)은 인터리브(interleave)하게 동작할 수 없는 프린터 같은 장치를 위해 출력 데이터를 보관하는 버퍼입니다.
프린터는 한번에는 하나의 작업만을 그것이 다 끝나기까지 처리하여야 하고, 여러 응용 프로그램의 출력을 섞어 번갈아 가며 출력시킬 수는 없습니다.
스풀링 시스템은 큐에서 대기 중인 스풀 파일을 한 번에 하나씩 프린터에 내보냅니다.
입출력 장치나 네트워크 전송은 일시적인 원인 혹은 영구적인 원인에 의해 실패할 수 있습니다.
일반적으로, 입출력 시스템 콜은 성공/실패를 나타내는 한 비트 정보를 반환합니다.
자세한 정보는 운영체제에 의해 응용 프로그램에까지 전달되지는 않습니다.
그래도 sense key
와 같은 형태로 보고되고, 이 키 값에 하드웨어 고장이나 불법적인 요청 등과 같은 문제의 유형을 알려줍니다.
입출력 보호의 경우 사용자가 불법적인 입출력을 못 하게 하기 위해, 모든 입출력 명령은 특권 명령(privileged instruction)으로 정의합니다.
따라서 사용자는 입출력 명령을 직접 수행할 수 없으며, 대신에 운영체제가 입출력 을 대신 수행하도록 시스템 콜을 수행합니다.
커널은 입출력 구성요소에 대한 상태 정보를 유지해야 합니다. 커널은 open 파일 테이블 구조와 같은 다양한 자료구조를 유지합니다. 커널은 네트워크 연결, 문자 장치 통신 그리고 다른 입출력 활동을 관리하기 위해 여러 비슷한 구조를 사용합니다.
운영체제는 각 장치별로 객체지향 기법을 이용하여 하나의 구조로 묶습니다. 따라서 같은 명령어를 사용하는 상황에서도 각기 다른 인터페이스를 사용함으로써 다른 장치에서 기대한 동작을 수행하도록 보장합니다.
다음은 봉쇄형 읽기 요청을 처리하는 순서이고, 이러한 하나의 입출력 작업이 엄청나게 많은 CPU 사이클(cycle)을 사용하며 많은 단계를 거친다는 것을 보여줍니다.
여기서는 필요한 장치 드라이버와 컨트롤러를 바로 찾았는데, 사실 이 과정에도 추가적인 작업이 요구됩니다.
UNIX의 경우에는 마운트 테이블을 이용하여 경로 이름의 접두어를 특정 장치 이름과 연관시킵니다.
이것은 <major, minor>
장치 번호를 얻게 되고, 이 두 장치 번호는 다음과 같은 정보를 가리킵니다.
해당 장치 테이블의 항이 장치 컨트롤러의 포트 주소나 메모리 맵된 주소를 제공합니다.
장치 드라이버의 적재는 운영체제가 부트될 때 정적으로 적재될 수 있고, 필요한 경우 호출할 때 오류를 발생시켜 동적으로 적재할 수 있습니다.