I/O 장치와 CPU는 동시에 작동할 수 있다. I/O 작업은 기본적으로 장치(Device)와 컨트롤러(Controller)의 로컬 버퍼 사이에서 데이터를 이동하는 것을 의미한다.
여기서 CPU는 직접 I/O 장치와 통신하는 대신, 컨트롤러의 버퍼와 메인 메모리 사이에서 데이터를 옮기는 역할을 한다.
각 I/O 장치는 장치 컨트롤러라는 전담 관리자를 가진다. 이 컨트롤러는 자체적인 로컬 버퍼와 특별한 레지스터(register)를 가지고 있어서, I/O 장치와의 통신을 제어한다.
A device driver for every device controller라는 문장의 의미는 다음과 같다.
장치 드라이버(Device Driver)는 해당 장치 컨트롤러의 세부 사항을 알고 있는 소프트웨어이다.
이 드라이버는 운영체제(OS)의 나머지 부분에 균일한 인터페이스(uniform interface)를 제공한다.
예를 들어, open, close, read, write와 같은 표준화된 함수들을 통해 개발자가 복잡한 하드웨어의 작동 방식을 일일이 알지 못해도 쉽게 I/O 작업을 할 수 있게 한다. 즉, OS와 애플리케이션이 장치와 소통하는 통역사 역할을 한다.
Memory Mapped I/O
원리 : 메모리 공간과 I/O 장치의 주소 공간이 분리되지 않고, I/O 장치가 마치 일반적인 메모리처럼 취급된다.
작동 방식 : CPU는 I/O 장치에 접근할 때 load (읽기)나 store (쓰기)와 같은 일반적인 메모리 명령어(instruction)를 사용한다.
장점 : 별도의 특별한 I/O 명령어가 필요 없기 때문에 프로그래밍이 간편한다.
현황 : 강의 자료에 따르면 이 방식이 더 일반적으로 사용된다.
Special I/O
원리 : CPU가 I/O 장치를 위한 별도의 독립된 버스(separate bus)를 가진다.
작동 방식 : I/O 장치에 접근하기 위해서는 IN 또는 OUT과 같이 I/O 전용으로 설계된 특별한 명령어(special instructions)가 필요하다.
장점: 메모리 주소와 I/O 주소 공간이 완전히 분리되어 있어, 메모리 보호 기능과 관련하여 더 명확한 구분이 가능하다.
폴링은 programmed I/O라고도 불리며, 모든 I/O 작업이 CPU의 직접적인 제어 아래 이루어지는 방식이다.
작동 방식 : CPU가 I/O 장치의 상태를 계속해서(continuously) 확인하면서 작업이 완료되었는지 묻는 방법이다. 마치 "끝났니? 끝났니?" 하고 끊임없이 물어보는 것과 같다.
비효율성 : 이 방식의 가장 큰 문제는 CPU 낭비이다. CPU는 I/O 장치의 작업이 끝날 때까지 바쁜 대기(busy waiting) 상태에 빠진다. 이 때문에 CPU는 다른 중요한 작업(다른 프로그램 실행 등)을 할 수 없게 되어 전체 시스템의 효율성이 크게 떨어진다.
폴링에서 인터럽트로의 전환
이러한 폴링의 문제를 해결하기 위해 인터럽트(Interrupt) 방식이 도입되었다.
작동 방식 : CPU가 I/O 작업을 시작하라고 지시한 뒤, 작업이 끝날 때까지 기다리지 않고 다른 일을 합니다. I/O 장치가 작업을 완료하면, 직접 CPU에게 "저 끝났습니다!"라고 신호를 보내는데, 이 신호가 바로 인터럽트이다.
효율성 : CPU는 인터럽트 신호가 올 때만 하던 작업을 잠시 멈추고 I/O 장치에게 관심을 기울이면 됩니다. 이 덕분에 CPU는 I/O 작업을 기다리느라 시간을 낭비하지 않고 다른 유용한 작업을 수행할 수 있게 됩니다.
비유
폴링 : 요리사가 냄비 앞에서 물이 끓었는지 계속 뚜껑을 열어보는 것. (CPU 낭비)
인터럽트 : 요리사가 냄비에 타이머를 맞춰두고 다른 요리를 하다가, 타이머가 울리면 그때 가서 냄비를 확인하는 것. (CPU 효율성 증대)
Polling의 다른 문제
원인 : I/O 장치 컨트롤러의 로컬 버퍼는 크기가 제한되어 있다. CPU가 너무 느리게 버퍼의 데이터를 가져가면, 새로운 데이터가 계속 들어와 기존의 데이터를 덮어쓰거나 유실될 수 있다.
결과 : 이로 인해 데이터 유실이라는 심각한 문제가 발생할 수 있으며, 이는 폴링 방식이 고속 I/O 장치에 부적합한 이유 중 하나이다.
작동 방식 : CPU와 I/O 장치 사이에 전용 신호선(line)이 있다. I/O 장치가 작업을 완료하면 이 신호선을 통해 CPU에게 신호를 보낸다. 이 신호는 CPU에게 어떤 사건(event)이 발생했음을 알려주는 역할을 한다.
결과 : 이로써 CPU는 더 이상 I/O 장치를 계속 확인하지 않고도, 필요할 때만 I/O 작업에 응답할 수 있게 된다.
Context : CPU가 현재 실행 중인 작업(프로세스 또는 스레드)에 대한 모든 정보를 담고 있는 상태
정보 : CPU 레지스터의 값, 메모리 상태, 파일 정보
인터럽트가 발생하면 CPU는 하던 작업을 잠시 멈추고 인터럽트 핸들러를 실행해야 한다. 이때 CPU가 기존에 하던 작업을 그대로 이어가려면, 인터럽트가 발생하기 직전의 컨텍스트를 안전하게 저장해야 한다.
저장 (Save Context) : CPU는 인터럽트가 발생하면 현재 실행 중이던 작업의 컨텍스트를 스택이나 특정 메모리 영역에 저장한다. 이 과정은 인터럽트 핸들러가 실행되는 동안 원래 작업의 상태가 손상되지 않도록 보호하는 역할을 한다.
복원 (Restore Context) : 인터럽트 핸들러가 자신의 작업을 모두 마치면, 저장해 두었던 컨텍스트를 다시 불러와 CPU 레지스터에 복원한다. 이렇게 하면 CPU는 인터럽트가 발생하기 전의 작업을 중단 없이 그대로 이어서 실행할 수 있다.
결론적으로, 컨텍스트는 CPU가 여러 작업을 오가며 효율적으로 멀티태스킹을 할 수 있게 해주는 핵심 정보이다.
컨텍스트 저장 : CPU가 현재 실행 중인 작업의 상태(context)를 보존한다.
유형 결정 : 인터럽트의 유형을 파악합니다. 주로 벡터 인터럽트 시스템을 사용한다.
핸들러 실행 : 인터럽트를 처리하기 위한 ISR(Interrupt Service Routine) 또는 인터럽트 핸들러로 제어권을 넘긴다.
인터럽트가 발생하면 CPU는 I/O 작업 완료를 기다리는 대신 다른 작업을 처리하고,
I/O 장치가 작업(예: 데이터 전송)을 완료하면 CPU에 인터럽트 신호를 보낸다.
이 신호를 받은 CPU는 하던 작업을 잠시 멈추고 인터럽트 처리에 돌입합니다.
이로써 CPU와 I/O 장치가 동시에 실행(concurrent execution)될 수 있습니다.
문제점 : 인터럽트 방식은 I/O 작업이 완료될 때마다 CPU가 개입해야 하므로, 특히 디스크 I/O처럼 대량의 데이터를 이동할 때는 오버헤드가 매우 크다. 즉, 한 바이트(byte)마다 인터럽트가 발생하여 비효율적이다.
해결책 : 이러한 높은 오버헤드를 해결하기 위해 DMA(Direct Memory Access) 방식이 도입되었습니다. DMA는 CPU의 개입 없이 I/O 장치와 메인 메모리 사이에서 직접 데이터를 전송합니다.
작동 방식 : CPU는 DMA 컨트롤러에게 전송할 데이터의 시작 주소, 크기 등 필요한 정보를 알려주고 작업을 시작하도록 프로그래밍합니다. 그 후 CPU는 다른 작업을 계속 수행합니다. DMA 컨트롤러는 I/O 장치로부터 데이터를 블록(block) 단위로 직접 메모리로 전송합니다.
결과 : 전체 데이터 블록 전송이 완료되었을 때 단 한 번의 인터럽트만 발생시키므로, CPU의 낭비 시간을 최소화할 수 있습니다. 이는 고속 I/O 장치에 매우 유용합니다.
현대 운영체제는 단순히 인터럽트를 처리하는 것을 넘어, 효율성과 안정성을 위해 더욱 정교한 메커니즘을 요구한다.
다중 레벨 인터럽트 : 중요도에 따라 높고 낮은 우선순위를 구별하는 다중 레벨 인터럽트를 사용한다. 이를 통해 우선순위가 높은 인터럽트가 낮은 우선순위의 처리를 선점(pre-empt)할 수 있다.
PIC(Peripheral Interrupt Controller): 인터럽트 관리를 위해 특별화된 PIC를 사용한다.
두 가지 인터럽트 라인
인터럽트 벡터는 인터럽트가 발생했을 때 적절한 인터럽트 핸들러를 찾기 위한 메커니즘이다.
구조 : 인터럽트 신호에는 해당 인터럽트 벡터를 가리키는 주소(address)가 포함되어 있다.
역할 : 이 인터럽트 벡터 테이블은 각기 다른 인터럽트 핸들러의 주소를 담고 있다.
따라서 OS는 인터럽트 번호를 이용해 인터럽트 벡터 테이블에서 해당 핸들러의 주소를 찾아 실행하게 된다.
체이닝(Chaining): 만약 인터럽트 벡터의 항목 수보다 장치 수가 더 많을 경우, 하나의 주소에 여러 인터럽트 핸들러의 리스트를 연결하는 체이닝(chaining) 방식을 사용한다.

작동방식
2 단계는 인터럽트 발생 시 시스템의 안정성과 연속성을 보장하는 핵심적인 단계이다.
이러한 과정이 없다면, 인터럽트가 발생할 때마다 기존 작업이 손상되어 시스템이 멈추거나 오작동할 수 있다.
보호 메커니즘이 필요한 주된 이유는 시스템의 올바른 작동을 보장하기 위해.
멀티태스킹 환경에서 여러 프로그램이 동시에 실행되면서 다양한 문제가 발생할 수 있다.
애플리케이션 프로그램 문제: 악의적이지 않더라도, 버그가 있는 프로그램이 다른 메모리 영역을 덮어쓰거나(scribbling into memory) 무한 루프에 빠져 CPU를 독점할 수 있다.
사용자 간 간섭: 다른 사용자가 무분별하게(Gluttonous) 자원을 사용하거나 악의적(Evil) 으로 시스템에 해를 가할 수도 있다.
주된 이유
가장 단순한 메모리 보호 방법은 Base and Limit Registers를 사용하는 것.
Base Register (베이스 레지스터): 프로그램이 접근할 수 있는 메모리 영역의 시작 주소를 저장.
Limit Register (리미트 레지스터): 프로그램이 접근할 수 있는 메모리 영역의 크기(길이)를 저장.
이 두 레지스터는 프로그램이 실행되기 전에 운영체제(OS)가 값을 설정한다.
프로그램이 실행되는 동안 CPU가 메모리에 접근할 때마다, 하드웨어는 해당 주소가 베이스 레지스터 값부터 베이스 레지스터 + 리미트 레지스터 값 사이에 있는지 확인한다.
만약 유효한 범위를 벗어나면 하드웨어는 인터럽트를 발생시켜 해당 프로그램을 강제로 종료한다. 이를 통해 다른 프로그램이나 OS의 메모리 영역을 안전하게 보호할 수 있다.
"협력적 접근(Cooperative approach)"은 프로그램이 시스템 콜(System Call)을 자발적으로 호출하여 제어권을 OS에 넘겨주는 방식이다. 이는 프로그램이 OS의 도움을 받기 위해 스스로 요청하는 경우이다.
하지만, 만약 프로그램이 악의적이거나 버그로 인해 시스템 콜을 호출하지 않고 무한 루프(infinite loop)에 빠지면 어떻게 될까? 이 경우 CPU 제어권을 되찾을 수 있는 다른 방법이 필요하다.
타이머(Timer)의 역할
타이머는 사용자 프로그램의 무한 루프(infinite loops)나 CPU 독점(CPU monopoly)을 방지하기 위해 사용된다. 이는 시스템의 안정성과 공정성을 보장하는 핵심 메커니즘이다.
비협력적 접근(Non-cooperative approach) : 타이머는 프로그램이 자발적으로 제어권을 반환하지 않을 때, OS가 강제로 제어권을 회수하는 방식.
타이머 작동 방식
이 과정을 통해 버그가 있거나 악의적인 프로그램이 무한 루프에 빠져도, OS는 반드시 CPU 제어권을 되찾을 수 있습니다.
특권 명령어(Privileged Instruction)
타이머 값을 설정하는 작업은 매우 중요하므로, 특권 명령어(privileged instruction)로 지정됩니다. 이는 오직 OS만이 실행할 수 있는 명령어이며, 사용자 프로그램이 마음대로 타이머를 조작하여 CPU를 독점하는 것을 방지합니다.
특권 명령어 사용 예시
하드웨어 자원에 대한 직접 접근 : 디스크나 프린터와 같은 I/O 장치에 직접 접근하는 명령어.
메모리 관리 상태 조작 : 페이지 테이블 포인터(page table pointers)나 TLB(translation look-aside buffer) 로딩 등 메모리 관리와 관련된 상태를 변경하는 명령어.
시스템 레지스터 접근 : 제어 레지스터(Control registers)나 인터럽트 핸들러 테이블(interrupt handler table)의 위치와 같은 시스템 레지스터에 접근하는 명령어.
CPU 모드 비트 설정 : 듀얼 모드(사용자/커널)를 구분하는 모드 비트를 설정하는 명령어.
시스템 정지 : 컴퓨터를 정지(halt)시키는 명령어.
타이머 설정 : 타이머 값을 설정하는 명령어 는 사용자 프로그램의 무한 루프를 방지하기 위해 오직 OS만 사용 가능하도록 제한.
CPU 작동 모드는 운영체제가 자신과 다른 시스템 구성 요소를 보호하기 위한 메커니즘.
사용자 모드 (User mode): 일반적인 사용자 프로그램이 실행되는 모드. 이 모드에서는 특권 명령어 실행이 제한.
커널 모드 (Kernel mode): 운영체제가 실행되는 모드. 이 모드에서는 모든 명령어와 하드웨어 자원에 접근할 수 있다.
CPU는 하드웨어에 의해 제공되는 모드 비트(mode bit)를 통해 현재 실행 중인 모드를 식별한다. 모드 비트는 커널 모드일 때 0, 사용자 모드일 때 1과 같이 설정된다.
모드 전환
사용자 → 커널 : 사용자 프로그램이 시스템 콜을 호출하면, 트랩(trap)을 통해 모드 비트가 변경되어 커널 모드로 전환.
커널 → 사용자 : 시스템 콜 처리가 끝나면 RTI (return from interrupt) 명령어를 사용하여 모드 비트가 다시 변경되어 사용자 모드로 돌아옴.
사용자 프로그램은 원칙적으로 특권 명령어를 직접 실행할 수 없기 때문에, 이러한 작업이 필요할 때는 시스템 콜(System Call)을 통해 운영체제에게 대신 수행해 달라고 요청해야 한다.
두 아키텍처 모두 보호된 레지스터의 상태 비트(status bit)를 통해 현재의 모드를 설정하고 식별합니다.
이 모드 비트는 시스템 콜을 통해 커널 모드로 전환되고,
RTI(Return from Interrupt) 명령어를 사용하여 다시 사용자 모드로 돌아옵니다.
사용자 프로그램은 시스템의 중요 자원을 보호하기 위해 직접 하드웨어에 접근할 수 없다. 따라서 OS가 제공하는 특권 작업을 수행하려면, 시스템 콜(System Call)이라는 특별한 메커니즘을 통해 OS에 요청해야 한다.
시스템 콜은 사용자 프로그램이 운영체제로부터 서비스를 받기 위해 사용하는 프로그래밍 인터페이스이다.
API와 시스템 콜: 사용자는 API(예: printf())를 호출하고, 이 API는 내부적으로 시스템 콜 구현(예: write())을 호출한다. API는 사용자가 접근하기 쉽도록 높은 수준의 언어로 작성되지만, 실제로는 커널에 존재하는 시스템 콜을 실행하는 것이다.
시스템 보호: OS는 시스템 콜을 통해 시스템을 보호.
불법 요청 거부: 사용자 프로그램이 권한이 없는(illegal) 요청을 하면, OS는 이를 거부하고 에러를 반환하여 시스템의 안정성을 지킨다.
자원 할당: "OS는 특정 자원의 몫을 부과한다"는 말은 OS가 자원을 할당할 때 공정하게 분배한다는 뜻이다. 예를 들어, CPU 시간이나 메모리 같은 자원을 여러 프로그램에 공정하게 나누어 할당하여 한 프로그램이 자원을 독점하지 않도록 한다.
공정성 보장: 여러 프로그램이 동시에 자원을 요청할 때, OS는 공정성(fairness)을 고려하여 순서를 정하고 자원을 공유한다.
보호된 절차 호출: 시스템 콜은 일종의 보호된 프로시저(절차) 호출이다. 시스템 콜 루틴은 OS의 코드 안에 있으며, 실행은 항상 커널 모드에서 이루어진다. 따라서 사용자 모드에서 시스템 콜을 호출할 때 모드가 커널 모드로 바뀌었다가, 시스템 콜 처리가 끝나면 다시 사용자 모드로 돌아온다.
시스템 콜의 접근 방식
"API를 사용하는 프로그램에 의해 가장 접근된다"는 말은, 대부분의 개발자는 시스템 콜을 직접 호출하지 않고, API를 통해 간접적으로 접근한다는 의미이다.
API의 역할: 프로그래밍 라이브러리(C 라이브러리, Java 라이브러리 등)는 개발자가 사용하기 쉬운 API를 제공한다. 이 API들은 내부적으로 해당 작업을 수행하기 위한 시스템 콜을 호출한다.
예시: C 언어에서 파일을 열기 위해 사용하는 fopen() 함수는 API이다. 이 함수를 호출하면, 라이브러리 내부에서는 실제로 파일을 열기 위한 open() 시스템 콜이 호출되어 OS에게 요청이 전달된다.