현대 컴퓨터 시스템에서 I/O(입출력) 시스템은 중요한 역할을 한다. 이는 하드웨어 장치인 I/O 디바이스, 이들을 제어하는 디바이스 드라이버, 그리고 커널과 사용자 프로그램이 장치와 직접 상호작용하지 않도록 추상화해주는 I/O 서브 시스템으로 구성되어 있다. I/O 서브시스템의 핵심 목적은 두 가지이다. 첫째, 커널이나 응용 프로그램 개발자가 하드웨어 세부 사항을 몰라도 I/O를 수행할 수 있게 하는 추상화 제공. 둘째, 다양한 입출력 장치에 대해 통일된 접근 방식(예: open, close, reasd, write, ioctl 시스템콜)을 제공하는 것이다. 이로써 파일 시스템, 가상 메모리 시스템 등의 상위 계층에서 장치 특성을 신경 쓰지 않고도 I/O 처리를 할 수 있게 된다.

컴퓨터 시스템은 정상적인 프로그램 실행을 방해하는 이벤트가 발생했을 때, 특졀한 명령을 수행하도록 설계되어 있다. 이러한 이벤트는 크게 두 종류로 나눌 수 있는데, 예외(Exception)와 인터럽트(Interrupt)이다.
예외는 CPU 내부에서 발생하는 이벤트로, 주로 명령어 실행 중 오류나 특별한 요청에 의해 발생한다. 대표적인 예로는 페이지 폴트, 0으로 나누기와 같은 산술 예외, 그리고 시스템 콜을 처리하기 위한 트랩(Trap) 등이 있다.
반면 인터럽트는 CPU 외부에서 발생하는 이벤트로, 하드웨어 장치가 CPU에게 특정 이벤트가 발생했음을 알릴 때 사용된다. 예를 들어 디스크 작업이 완료되었거나, 키보드에서 입력이 들어온 경우 인터럽트가 발생한다. 이런 인터럽트는 외부 장치가 CPU의 주의를 끌기 위해 보내는 신호로, CPU는 이를 감지하고 적절한 처리를 수행해야 한다.
인터럽트는 하드웨어 동시성을 실현하는 데 핵심적인 도구이다. CPU는 여러 장치가 동시에 동작하고 있는 동안에도 자신이 수행해야 할 명령을 계속해서 처리할 수 있어야 한다. 예를 들어 디스크 장치는 데이터 전송 작업을 수행하면서, 완료되었을 때만 CPU에 인터럽트를 보내 알려준다. 이 방식 덕분에 CPU는 장치의 작업이 끝날 때까지 기다리지 않고도 다른 작업을 처리할 수 있다.
또한 외부 장치가 CPU에게 특정 작업을 요청해야 할 때도 인터럽트를 사용한다. 예를 들어 네트워크 카드가 새로운 패킷을 수신했을 때, 이 사실을 CPU에 알려주기 위해 인터럽트를 발생시키는 것이다. 이렇게 인터럽트는 장치와 CPU 간의 중요한 비동기적 통신 수단이다.
여러 하드웨어 장치가 인터럽트를 발생시킬 수 있기 때문에, 이를 효율적으로 처리하기 위한 하드웨어가 필요하다. 이를 위해 IRQ(Interrupt Request) 라인과 PIC(Programmable Interrupt Controller)라는 개념이 도입된다.
모든 장치는 하나의 IRQ 라인을 가지고 있으며, 이를 통해 인터럽트 요청을 전송한다. 이 IRQ 라인들은 PIC에 연결되어 있고, PIC는 다음과 같은 역할을 수행한다.
다수의 인터럽트 요청 중에서 우선순위를 판별하여 가장 중요한 인터럽트를 CPU에 전달한다.
CPU가 어느 장치에서 인터럽트가 발생했는지 식별하는 작업을 분담하여 CPU의 부담을 줄인다.

PIC는 각 IRQ 라인을 감시하고, 신호가 감지되면 해당 신호를 인터럽트 벡터로 변환한 뒤, 이를 PIC의 포트에 저장하여 CPU가 데이터 버스를 통해 읽을 수 있도록 한다. 이후 CPU가 해당 인터럽트를 인식하고 이를 처리하게 된다. 인터럽트 처리 후 CPU는 PIC의 특정 포트에 확인 신호를 보냄으로써 인터럽트 처리가 완료되었음을 알린다. 예전에는 Intel 8259 PIC가 사용되었지만, 현대 시스템에서는 APIC(Advanced PIC)으로 대체되었다.
인터럽트는 소프트웨어적으로 막을 수 있는가에 따라 두 가지로 분류된다.
모든 프로세서는 예외와 인터럽트를 정해진 우선순위 순서에 따라 처리한다. 일반적으로 아래와 같은 순서로 우선순위가 주어진다.
Non-maskable Interrupts : 가장 높은 우선순위를 가진다.
Exceptions : 소프트웨어 내부 이벤트로 발생하며, 쓰레드보다 우선적으로 처리된다.
Maskable Interrupts : 일반적인 외부 인터럽트
Threads : 가장 낮은 우선순위이며, 위의 이벤트가 없다면 실행된다.

이러한 구조 덕분에 시스템은 중요한 이벤트에 신속하게 반응할 수 있으며, 전체 시스템 안정성과 반응성을 유지할 수 있다.
Exception & Interrupt Handling

컴퓨터 시스템이 예외(Exception)나 인터럽트(Interrupt)를 처리할 수 있는 이유는, 시스템이 이를 벡터 테이블(Vector table)이라는 데이터 구조로 관리하기 때문이다. 이 벡터 테이블은 특정 예외나 인터럽트가 발생했을 때, 해당 이벤트를 처리할 수 있는 서비스 루틴의 주소를 저장하고 있는 표이다.
운영체제는 부팅 과정이나 드라이버 설치 중에 각각의 예외 처리 루틴(ESR, Exception Service Routine)을 등록하게 된다. 이렇게 등록된 루틴은 각 벡터 항목에 대응되며, 특정 이벤트가 발생하면 해당 루틴이 자동으로 호출되어 이벤트를 처리한다.

앞서 언급한 PIC(Programmable Interrupt Controller)는 외부 장치에서 발생하는 모든 인터럽트를 수신하는 중앙 처리 장치 역할을 한다. 이때 PIC는 단순히 인터럽트를 전달하는 것에 그치지 않고, 마스킹(Masking) 기능을 통해 어떤 인터럽트를 무시할지 걸러내는 필터 역할도 수행한다.
운영체제는 특정 인터럽트를 일시적으로 처리하지 않도록 마스킹 설정 목록을 PIC에 전달할 수 있다. 이때 사용되는 것이 프로세서 제어 레지스터(Processor control register)이며, 이를 통해 어떤 인터럽트가 현재 활성화될 수 있는지를 PIC가 판단한다. 예를 들어, 현재 중요한 작업을 처리 중이라면, 덜 중요한 외부 장치의 인터럽트는 마스킹되어 무시될 수 있다.
운영체제는 인터럽트를 다음 세 가지 상태로 구분하여 관리한다.
시스템에는 다양한 종류의 인터럽트가 존재하고, 이들은 언제든지 발생할 수 있다. 특히 한 인터럽트를 처리하고 있는 도중에 또 다른 인터럽트가 발생할 수 있기 때문에, 이를 효율적으로 제어하기 위한 우선순위 기반 인터럽트 처리가 필요하다.
이를 위해 운영체제는 각 인터럽트에 Interrupt Priority Level IPL)이라는 우선순위를 할당한다. 이 우선순위에 따라 더 중요한 인터럽트가 덜 중요한 인터럽트보다 먼저 처리될 수 있도록 설정된다. 구체적으로는, 우선순위가 더 높은 인터럽트가 현재 진행 중인 낮은 우선순위의 인터럽트 처리를 중단하고(preempt) 자신의 처리를 시작할 수 있다.
예를 들어, 타이머 인터럽트는 시스템 전체의 시간 관리와 관련되기 때문에 매우 높은 우선순위를 부여받는 반면, 네트워크 인터럽트는 상대적으로 낮은 우선순위를 가질 수 있다. 이러한 설계 덕분에 시스템은 더 중요한 작업을 놓치지 않고 빠르게 대응할 수 있다.
인터럽트를 효율적으로 처리하기 위해서는 ISR(Interrupt Service Routine)이 빠르게 실행되어야 한다. 그러나 어떤 경우에는 ISR이 너무 오래 실행되거나, ISR 내부에서 모든 인터럽트를 비활성화(disable)해 놓는 경우가 있다. 이럴 때 새로운 인터럽트가 발생하더라도 CPU가 이를 인지하지 못하는 현상이 발생하는데, 이를 인터럽트 손실(interrupt miss) 이라고 한다.
이 현상은 특히 다음과 같은 문제를 유발할 수 있다:

인터럽트의 반응 속도를 정확히 이해하기 위해서는 몇 가지 시간 요소를 구분해야 한다:
이 모든 시간을 합한 것이 TD (Interrupt Response Time) = TB + TC 이다. 즉, 인터럽트 발생부터 완전히 처리되기까지의 총 소요 시간이다.
인터럽트 지연(latency)을 유발하는 요소들은 다음과 같다.
특히, 높은 우선순위의 인터럽트가 처리중인 경우 그 시간이 길어질수록, 더 낮은 우선순위의 인터럽트는 계속해서 대기해야 하므로 상대적인 지연이 커지고, 결국 인터럽트 손실로 이어질 수 있다.
운영체제는 인터럽트 처리 시, 전체 작업을 하나의 ISR에서 모두 수행하지 않고 두 부분으로 나누는 전략을 취한다. 이를 Top Half와 Bottom Half로 구분한다.
이렇게 역할을 분리함으로써, ISR이 실행되는 시간을 최소화하고, 더 많은 인터럽트를 놓치지 않고 처리할 수 있도록 한다. 이 방식은 Linux에서는 softirq, tasklet, workqueue 등의 다양한 형태로 구현된다.

컴퓨터와 I/O 장치 간의 물리적인 통신은 포트(port) 와 버스(bus) 를 통해 이루어진다.
각 장치는 디바이스 컨트롤러(device controller) 라는 칩을 통해 제어되며, 이 컨트롤러는 데이터를 장치로 전송하거나 장치로부터 수신하는 일을 맡는다. 또한, 장치 제어 레지스터(device control registers) 는 장치를 초기화하고 요청을 보내는 데 사용되며, 일반적으로 CPU 외부에 위치한다. 상위 계층에서는 이러한 레지스터를 통해 장치에게 작업을 지시하고, 데이터를 읽거나 쓴다. 이러한 과정을 디바이스 제어(device control) 라고 한다.
입출력 장치가 준비되었는지를 감지하는 방법에는 대표적으로 Polling 방식과 Interrupt 방식이 있다.
Polling은 CPU가 장치의 상태 레지스터(status register)에 있는 busy 비트를 반복적으로 읽으며 장치가 준비되었는지 확인하는 방식이다. 장치가 작업 중이면 busy 비트가 설정되고, 작업을 마치면 해당 비트가 클리어된다. CPU는 이 busy 비트를 확인하기 위해 루프를 돌며 감시하며, 이러한 방식은 busy-waiting(바쁜 대기) 라고도 불린다. 장치가 준비되었을 때 CPU는 control register에 command-ready 비트를 설정하여 장치에 명령을 전달한다.
이 방식의 큰 문제는 CPU가 계속해서 상태를 감시해야 하므로, 다른 중요한 작업을 처리하지 못하고 시간만 낭비하게 된다는 점이다. 시스템 리소스를 비효율적으로 사용하는 대표적인 예다.
이에 대한 해결책으로 Interrupt 방식이 있다. 이 방식에서는 장치가 작업을 완료했을 때 스스로 CPU에 인터럽트를 발생시켜 알린다. 덕분에 CPU는 장치의 상태를 계속 확인할 필요 없이, 본연의 다른 작업을 수행할 수 있으며, 필요한 순간에만 장치에 반응하면 된다. 이로써 시스템 자원을 효율적으로 사용할 수 있게 된다.
Polling이나 인터럽트를 이용한 입출력 방식은 모두 일정 부분 CPU가 관여해야 한다. 특히 Programmed I/O (PIO) 방식에서는 CPU가 매번 장치의 상태를 확인하고, 하나의 데이터 단위를 직접 읽고 쓰는 방식으로 I/O 작업을 수행한다. 예를 들어 데이터를 한 바이트씩 장치 레지스터로 전송하는 식이다.
이 방식은 데이터 전송량이 많아질수록 CPU의 개입이 지나치게 많아지고, 결국 CPU 자원을 낭비하게 된다.
이러한 문제를 해결하기 위해 등장한 것이 DMA (Direct Memory Access) 이다. DMA는 I/O 관련 반복 작업을 CPU 대신 처리하는 전용 컨트롤러(DMA controller) 를 사용하는 방식이다. 즉, 데이터를 전송하거나 상태를 확인하는 등의 작업을 DMA 컨트롤러가 담당하고, CPU는 작업 시작 시 필요한 설정만 해주면 이후에는 관여하지 않아도 된다.

DMA 방식이 사용될 때 CPU는 다음과 같은 초기화 작업만 수행한다.
이후 실제 데이터 전송은 DMA 컨트롤러가 독립적으로 수행하며, 작업이 완료되면 인터럽트를 통해 CPU에 알릴 수 있다. 이 방식은 특히 대량의 데이터를 전송해야 할 때 CPU의 부하를 줄이는 데 탁월하다.
