본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/장치 제어와 입출력: 두 판 사이의 차이

~ldmsys
인터럽트를 비동기 사건 통지 중심으로 재구성; 타이머 선점은 활용 사례로 배치
BIOS 부팅·i8042 키보드 각론의 새 문서명으로 내부 링크 갱신
 
(같은 사용자의 중간 판 4개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본/보호|이전: 보호와 시스템 호출]] · [[운영체제 기본|목차]] · [[운영체제 기본/프로세스와 스레드|다음: 프로세스와 스레드]]
[[운영체제 기본/보호|이전: 보호와 시스템 콜]] · [[운영체제 기본|목차]] · [[운영체제 기본/프로세스와 스레드|다음: 프로세스와 스레드]]


== 실행 흐름과 독립적인 사건의 통지 ==
장치와 소프트웨어의 상호작용에는 세 가지 일이 있다. '''제어 명령을 전달하는 일, 데이터를 옮기는 일, 사건이나 완료를 확인하는 일'''이다. 장치 레지스터 접근, DMA, 인터럽트는 각각 이 과정의 다른 부분을 담당한다. DMA로 데이터를 전송하면서 완료는 인터럽트로 통지받을 수도 있고, 같은 DMA 전송의 완료를 폴링으로 확인할 수도 있다.
키보드 입력, 네트워크 패킷 도착, 저장장치의 입출력 완료는 프로그램이 다음 명령을 실행하는 순서에 맞춰 발생하지 않는다. CPU가 계산을 진행하는 동안에도 이런 사건은 일어난다. '''하드웨어 인터럽트는 사건의 발생을 CPU에 알리고, 운영체제가 이를 처리할 기회를 만드는 통지 수단'''이다.


인터럽트가 없다면 프로그램이나 운영체제가 장치의 상태를 반복해서 확인하는 폴링을 사용할 수 있다. 자주 확인하면 응답 지연은 줄지만 확인할 일이 없어도 CPU 시간을 쓴다. 확인 간격을 늘리면 그만큼 처리가 늦어진다. 인터럽트는 사건이 발생했을 때 처리를 요청하므로 이 반복 확인을 줄일 수 있다. 반대로 사건이 아주 빈번하면 잦은 진입·복귀의 비용이 커져 폴링이나 묶음 처리와 조합하는 편이 유리할 수 있다.<ref name="interrupt-polling">[https://docs.kernel.org/networking/napi.html Linux 커널 문서: NAPI], 인터럽트와 폴링의 결합.</ref>
== 장치 제어와 데이터 전송 ==
=== 레지스터를 통한 장치 제어 ===


인터럽트의 원인에 따라 할 일은 다르다. 키보드 입력은 도착한 데이터를 읽어 입력 계층에 넘기고, 네트워크 수신은 도착한 패킷의 처리를 진행하며, 저장장치 완료는 요청 결과를 확인한다. 타이머 만료는 시간 경과를 알린다. '''스케줄링과 선점은 이 통지를 활용하는 기능 중 하나'''이며, 모든 인터럽트가 CPU 양보나 프로세스 교체를 목적으로 발생하는 것은 아니다.
장치는 소프트웨어가 읽거나 쓸 수 있는 레지스터를 제공한다. 제어 레지스터에는 시작이나 중지 명령을 쓰고, 상태 레지스터에서는 준비 상태나 오류를 확인한다. 데이터 레지스터를 통해 실제 데이터를 주고받는 장치도 있다. 드라이버는 장치 규약과 운영체제 요청을 연결한다.


== 인터럽트와 동기적 예외 ==
메모리 매핑 입출력인 MMIO(Memory-Mapped Input/Output)는 CPU의 주소 공간 일부를 장치 접근에 사용한다. 포트 입출력은 일반 메모리와 구별되는 포트 주소 공간을 사용하며, x86에서는 별도의 입출력 명령으로 접근한다. 두 방식은 레지스터에 접근하는 방법의 차이다. 데이터 전송을 CPU가 담당할지 다른 하드웨어에 맡길지와는 별개의 구분이다.<ref name="device-io">Linux Kernel Documentation, [https://docs.kernel.org/driver-api/device-io.html Bus-Independent Device Accesses].</ref>


하드웨어 인터럽트는 현재 실행하는 명령의 의미와 독립적으로 발생하는 비동기적 사건을 CPU에 알린다. 타이머 만료와 장치의 입출력 완료가 대표적이다. 반면 잘못된 메모리 접근이나 허용되지 않은 명령 실행으로 발생하는 예외는 해당 명령과 연결된 동기적 사건이다. 시스템 호출도 프로그램이 정해진 진입 명령을 실행하여 요청하므로, 장치가 비동기적으로 알리는 인터럽트와 구분해야 한다.
장치 레지스터는 읽기가 상태를 바꾸거나 쓰기가 동작을 시작할 수 있다. 따라서 일반 변수처럼 접근을 생략하거나 순서를 바꿔도 된다고 가정할 수 없다. 쓰기 명령의 실행이 끝났더라도 실제 장치에 쓰기가 도착했는지는 별도로 판단해야 하는 하드웨어도 있다.<ref name="device-io" />


트랩이라는 용어의 범위는 아키텍처와 문헌마다 다르다. RISC-V에서는 예외나 인터럽트로 처리기에 제어가 넘어가는 일을 모두 트랩이라고 부른다. 따라서 “시스템 호출도 트랩이다”라는 설명에서 “시스템 호출도 비동기 하드웨어 인터럽트다”라는 결론을 내리면 안 된다.<ref name="riscv-terms">RISC-V International, [https://docs.riscv.org/reference/isa/v20240411/unpriv/intro.html#_exceptions_traps_and_interrupts The RISC-V Instruction Set Manual, Volume I: Exceptions, Traps, and Interrupts].</ref>
=== DMA와 버퍼의 소유권 ===


== 진입, 상태 보존, 복귀 ==
CPU가 데이터 레지스터를 읽어 메모리에 저장하는 방식에서는 CPU 명령 실행이 데이터 이동을 담당한다. i8042 입력에서 CPU가 컨트롤러의 데이터 레지스터를 읽는 것도 이쪽에 해당한다. 인터럽트가 읽을 시점을 알려주더라도 그 전송이 DMA가 되지는 않는다.


CPU는 인터럽트를 받아들일 때 복귀 위치와 이전 실행 권한 등 필요한 최소 상태를 아키텍처가 정한 방식으로 보존하고, 미리 설정된 처리기 진입점으로 이동한다. 사용자 코드 실행 중이었다면 커널 처리를 위한 권한 전환도 이루어진다. 단순히 권한만 높여 사용자 코드를 계속 실행시키는 것이 아니다. 진입점과 권한 전환을 함께 통제해야 [[운영체제 기본/보호|보호 경계]]를 유지할 수 있다.
직접 메모리 접근인 DMA(Direct Memory Access)에서는 장치 자체의 전송 엔진이나 별도의 DMA 컨트롤러가 메모리 접근을 수행한다. CPU가 전송되는 데이터마다 읽기와 쓰기 명령을 실행할 필요는 없다. 주변장치 대신 별도 컨트롤러가 전송하는 구조도 있고, 장치가 직접 버스 접근을 시작하는 버스 마스터 구조도 있다.<ref name="dma-engine">Linux Kernel Documentation, [https://docs.kernel.org/driver-api/dmaengine/client.html DMA Engine API Guide].</ref><ref name="dma-map">Linux Kernel Documentation, [https://docs.kernel.org/core-api/dma-api-howto.html Dynamic DMA mapping Guide].</ref>


하드웨어가 모든 레지스터를 저장하는 것은 아니다. 진입 코드가 나머지 필요한 레지스터를 보존하고, 커널 스택이나 전용 인터럽트 스택 등 처리기에 적합한 실행 환경을 마련한다. 하드웨어가 담당하는 범위는 아키텍처마다 다르다. 예를 들어 RISC-V에서는 일반 레지스터 저장과 커널 스택 선택을 소프트웨어가 수행한다.<ref name="xv6-entry">Russ Cox, Frans Kaashoek, Robert Morris, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6: a simple, Unix-like teaching operating system], 2025, 4.1절과 4.2절.</ref>
DMA에서도 드라이버는 버퍼를 준비하고 장치가 사용할 주소와 길이와 방향을 설정하며 전송을 제출한다. 전송 중인 버퍼를 덮어쓰거나 해제하면 데이터가 손상될 수 있으므로, CPU와 장치가 언제 데이터를 변경해도 되는지 소유권 규약이 필요하다. 버퍼와 주소 매핑은 전송이 끝날 때까지 유효해야 하며, 완료와 필요한 동기화를 확인한 뒤 재사용한다.<ref name="dma-engine" />


처리를 마친 뒤 원래 실행을 재개한다면 저장한 상태를 복원하고 인터럽트 복귀 절차를 거친다. 처리기가 잠깐 실행되었다는 사실과 다른 스레드로 '''문맥 교환'''했다는 사실은 다르다. 문맥 교환은 스케줄러가 실행 대상을 바꾸기로 결정했을 때 기존 실행 문맥을 보존하고 다른 문맥을 복원하는 별도 단계다. 진입만으로 실행 대상이 바뀌지는 않는다.<ref name="xv6-switch">Cox 외, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6], 8.1절과 8.2절.</ref>
DMA는 데이터 이동 방식이고 폴링과 인터럽트는 상태를 확인하거나 사건을 알리는 방식이다. DMA 완료를 폴링으로 확인할 수도 있다. 네트워크 수신과 저장장치 블록 전송은 대표적인 용도지만, 모든 전송에 DMA가 필요한 것은 아니다.


== 입출력 완료와 깨우기 ==
=== 주소와 일관성과 전송 비용 ===


장치에서 데이터가 오기를 기다리는 스레드는 CPU를 받아도 해당 작업을 진행할 수 없다. 운영체제는 이 스레드를 대기 상태로 두고 다른 실행 가능 작업에 CPU를 준다. 장치가 완료 인터럽트를 보내면 드라이버가 완료 상태를 확인하고 기다리던 작업을 깨운다. 깨우기는 대기 상태를 실행 가능 상태로 바꾸는 일이며, 준비된 작업은 실행 대기열에서 스케줄러의 선택을 기다린다.<ref name="xv6-io">Cox 외, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6], 6.1절 및 9.1절.</ref>
CPU의 가상 주소, CPU의 물리 주소, 장치의 DMA 주소는 구별해야 한다. 단순한 시스템에서는 DMA 주소와 물리 주소가 같지만 일반적으로 이를 가정할 수 없다. 입출력 메모리 관리 장치인 IOMMU(Input-Output Memory Management Unit)는 장치의 주소를 물리 메모리로 변환하고, 설정에 따라 접근 가능한 영역을 제한하는 데에도 쓰인다. 모든 시스템에 IOMMU가 있는 것은 아니다.<ref name="dma-map" />
 
CPU 캐시와 장치가 보는 메모리 사이에는 동기화가 필요할 수 있다. 하드웨어가 캐시 일관성을 제공하더라도 접근 순서까지 자동으로 보장하지는 않는다. 전송 요청을 기록한 디스크립터에서 주소와 길이보다 유효 표시가 먼저 보이면 장치가 미완성 작업을 실행할 수 있다. 메모리 배리어는 이런 공개 순서를 보장하는 데 사용한다.<ref name="dma-map" />
 
DMA는 CPU의 반복적인 복사를 줄이지만 설정과 완료 처리, 주소 매핑, 캐시 동기화 비용이 든다. 장치는 버스와 메모리 대역폭을 소비하고 다른 작업과 경합한다. 임시 버퍼 때문에 추가 복사가 필요할 수도 있다. 버퍼 부족과 요청을 모으는 정책은 대기 시간에도 영향을 준다. 작은 전송은 CPU가 직접 처리하는 편이 단순할 수 있으며, DMA 사용 여부는 처리량뿐 아니라 지연과 자원 사용까지 고려해야 한다.
 
== 사건 통지와 인터럽트 ==
 
=== 실행 흐름과 독립적인 사건의 통지 ===
키보드 입력, 네트워크 패킷 도착, 저장장치의 입출력 완료는 실행 중인 프로그램의 명령 순서와 독립적으로 발생한다. '''하드웨어 인터럽트는 사건을 CPU에 알리고 운영체제가 처리할 기회를 만드는 통지 수단'''이다. 키보드의 입력 바이트를 읽거나 패킷을 처리하는 일, 완료된 요청을 확인하는 일은 통지를 받은 뒤 각 처리기가 수행한다. 스케줄링과 선점은 그 활용 중 하나이며, 모든 인터럽트가 CPU 양보를 목적으로 발생하지는 않는다.
 
폴링은 장치 상태를 반복해서 확인한다. 자주 확인하면 응답은 빨라지지만 사건이 없을 때도 CPU 시간을 쓰고, 확인 간격을 늘리면 처리가 늦어진다. 인터럽트는 이런 반복 확인을 줄일 수 있으나 사건이 빈번하면 진입·복귀 비용이 커진다. 인터럽트로 처리를 시작한 뒤 폴링으로 여러 결과를 묶어 처리하는 설계도 사용한다.<ref name="interrupt-polling">[https://docs.kernel.org/networking/napi.html Linux 커널 문서: NAPI], 인터럽트와 폴링의 결합.</ref>
 
=== 인터럽트와 동기적 예외 ===
장치 인터럽트는 비동기적 사건인 반면, [[운영체제 기본/보호#보호 위반과 예외|메모리 접근 예외]]는 현재 명령의 실행과 연결된다. 시스템 콜(System Call)도 프로그램이 정해진 진입 명령으로 요청하는 동기적 사건이다. 트랩이라는 용어의 범위는 아키텍처마다 다르며, RISC-V에서는 인터럽트와 예외에 따른 제어 이전을 모두 트랩이라고 부른다. 같은 용어를 쓴다고 발생 원인까지 같은 것은 아니다.<ref name="riscv-terms">[https://docs.riscv.org/reference/isa/v20240411/unpriv/intro.html#_exceptions_traps_and_interrupts RISC-V Instruction Set Manual: Exceptions, Traps, and Interrupts].</ref>
 
=== 처리 진입점과 IDT ===
[[운영체제 기본/보호#보호 위반과 예외|보호]]에서 본 예외와 장치 인터럽트는 발생 원인이 다르지만, 사건의 종류에 맞는 처리 코드로 실행을 넘겨야 한다. 이를 '''사건 번호인 벡터(vector)와 처리 진입점의 대응 관계'''로 생각할 수 있다.
 
x86-64의 전통적인 처리 방식에서는 운영체제가 '''인터럽트 서술자 테이블(Interrupt Descriptor Table, IDT)'''을 초기화하고 CPU에 그 위치를 등록한다. CPU는 예외나 인터럽트의 벡터를 인덱스로 사용해 IDT 항목을 선택한다. 예를 들어 #PF는 14번, #GP는 13번 항목을 선택하여 해당 게이트가 가리키는 커널 진입 코드로 넘어간다. 각 항목은 단순한 함수 주소가 아니라, 처리 코드의 주소와 커널 진입에 필요한 속성을 담은 '''게이트(gate)'''다. 여기에는 대상 코드 영역, 스택 선택 정보, 게이트 유형과 접근 제한 등이 포함된다.<ref name="intel-idt">Intel, [https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A], 2016년 9월, 6.10~6.14절 및 5.8.8절.</ref>


<pre>
<pre>
입출력 대기 -> 장치 완료 인터럽트 -> 드라이버의 완료 확인과 깨우기
잘못된 페이지 접근 → #PF(14) → IDT[14] → 커널의 페이지 폴트 진입 코드
          -> 실행 가능 상태와 실행 대기열 -> 스케줄러 선택 -> 실행 재개
일반 보호 예외    → #GP(13) → IDT[13] → 커널의 일반 보호 예외 진입 코드
장치의 인터럽트    → 벡터 v  → IDT[v]  → 커널의 인터럽트 진입 코드
</pre>
</pre>


장치 인터럽트는 다른 프로세스가 실행 중일 때도 도착한다. 따라서 인터럽트에서 복귀하는 대상과 방금 깨어난 스레드가 같다고 볼 수 없다. 타이머가 CPU를 회수할 기회를 만든다면, 장치 인터럽트는 기다리던 조건이 충족되었음을 알리는 역할을 한다.
게이트의 '''서술자 특권 수준(Descriptor Privilege Level, DPL)'''은 <code>INT n</code> 같은 소프트웨어 명령으로 그 진입점을 호출할 수 있는지를 제한한다. 하드웨어 인터럽트나 CPU가 검출한 예외에는 이 게이트 DPL 검사를 적용하지 않는다.<ref name="intel-idt" />
 
진입할 때 CPU가 복귀에 필요한 일부 상태를 저장하고, 운영체제의 진입 코드가 필요한 상태를 추가로 보존한다. 처리가 끝나면 소프트웨어가 상태를 복원하고 <code>IRETQ</code> 같은 복귀 명령으로 실행을 이어 간다. 이 진입과 복귀 자체가 다른 프로세스로의 컨텍스트 스위치(Context Switch)를 뜻하지는 않는다. 다른 프로세스를 실행할지는 스케줄러의 별도 결정이다.<ref name="intel-idt" />
 
구체적인 x86 진입 코드는 [[운영체제 기본/x86-64 Linux의 i8042 키보드 입력/IRQ 진입과 디스패치#IDT에서 C 진입점까지|키보드 IRQ 각론]]으로 이어진다. IDT는 모든 CPU의 공통 구조는 아니다. 또한 x86-64의 <code>SYSCALL</code>은 별도로 설정된 진입 주소를 사용하므로, 모든 시스템 콜이 IDT를 거친다고 설명해서는 안 된다.<ref name="intel-idt" />


인터럽트 대신 장치 상태를 반복 확인하는 폴링도 가능하다. 폴링은 사건이 드물면 CPU 시간을 낭비하지만, 사건이 빈번하면 여러 결과를 연속 처리하여 잦은 인터럽트 진입 비용을 줄일 수 있다. 인터럽트로 처리를 시작하고 폴링으로 묶어서 처리하는 조합도 사용한다.<ref name="napi">Linux kernel documentation, [https://docs.kernel.org/networking/napi.html NAPI], Datapath API 및 Scheduling and IRQ masking.</ref>
=== 진입, 상태 보존, 복귀 ===
CPU는 인터럽트를 받아들일 때 복귀 위치와 이전 실행 권한 등 필요한 최소 상태를 보존하고 설정된 처리기 진입점으로 이동한다. 진입 코드는 나머지 레지스터와 처리기에 필요한 스택을 준비한다. 하드웨어와 소프트웨어가 맡는 범위는 아키텍처마다 다르다. 사용자 코드 실행 중이었다면 커널 처리를 위한 권한 전환도 이루어지며, 진입점과 권한을 함께 통제해 보호 경계를 유지한다.<ref name="xv6-entry">Cox 외, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6], 2025, 4.1·4.2절.</ref>


== 타이머 인터럽트와 선점 ==
원래 실행을 재개할 때는 저장 상태를 복원하고 복귀 절차를 거친다. 처리기가 잠깐 실행된 것과 다른 스레드로 컨텍스트 스위치를 한 것은 다르다. 실행 대상을 바꾸는 결정은 스케줄러가 별도로 수행한다.<ref name="xv6-switch">Cox 외, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6], 8.1·8.2절.</ref>


시스템 호출이 일어날 때마다 커널이 실행 중인 작업을 양보시킨다고 가정하자. 파일을 읽거나 출력하는 프로그램에서는 이 방법으로 실행 기회를 나눌 수 있다. 그러나 사용자 코드가 다음 C 반복문에 들어가면 시스템 호출 자체가 발생하지 않는다.
=== 입출력 완료와 깨우기 ===
입출력을 기다리는 스레드는 대기 상태로 두고 다른 작업에 CPU를 줄 수 있다. 장치가 완료 인터럽트를 보내면 드라이버가 요청의 결과를 확인하고 해당 요청을 기다리던 스레드를 깨운다. '''깨우기는 실행 가능 상태로 바꾸는 일'''이지, 인터럽트 처리기 안에서 사용자 프로그램을 호출하는 일이 아니다.<ref name="xv6-io">Cox 외, [https://pdos.csail.mit.edu/6.1810/2025/xv6/book-riscv-rev5.pdf xv6], 6.1·9.1절.</ref>


<pre>
<pre>
while (1) {
입출력 대기 → 완료 통지 → 결과 확인과 깨우기
}
          → 실행 가능 상태 → 스케줄러 선택 → 실행 재개
</pre>
</pre>


시스템 호출을 유일한 양보 지점으로 삼았다면, 커널은 이 반복문에서 다음 양보 지점을 기다려도 제어권을 돌려받지 못한다. 해당 논리 CPU에서 실행을 기다리는 다른 작업은 차례를 얻지 못한다. '''시스템 호출마다 양보하도록 만드는 것과, 시스템 호출을 하지 않는 프로그램에서도 CPU를 회수할 수 있는 것은 다른 문제'''다.
장치 인터럽트는 다른 작업이 실행 중일 때도 도착한다. 따라서 인터럽트에서 복귀하는 대상과 방금 깨어난 스레드가 같다고 볼 수 없다. DMA 전송이라면 완료 확인 뒤 버퍼를 사용할 수 있는 상태로 동기화하는 일도 필요하다.
 
협력형 스케줄링은 프로그램의 자발적인 양보에 의존한다. 일반적인 선점형 시분할에서는 타이머 통지를 이용해 그 협조 없이 실행 기회를 나눈다.


커널은 타이머에 다음 통지 시점을 설정한다. 시간의 진행과 만료 통지는 사용자 프로그램의 반복문과 독립적으로 이루어지므로, 프로그램이 시스템 호출을 전혀 하지 않아도 CPU는 타이머 인터럽트를 받아 커널로 진입할 수 있다. 사용자 프로그램이 이 타이머나 인터럽트 허용 상태를 마음대로 바꿀 수 없어야 이 장치가 제어권 회수 수단으로 기능한다.<ref name="riscv-timer">RISC-V International, [https://docs.riscv.org/reference/isa/v20260120/priv/machine.html The RISC-V Instruction Set Manual, Volume II: Machine-Level ISA], Machine Timer Registers 및 Machine Interrupt Registers.</ref>
=== 타이머 인터럽트와 선점 ===
시스템 콜마다 실행 중인 작업을 양보시킨다고 가정하자. 다음 C 반복문에는 시스템 콜 자체가 없다.


<pre>
<pre>
사용자 코드 실행 -> 타이머 만료 -> 인터럽트 진입과 상태 보존
while (1) {
                                -> 시간 계산과 재스케줄 필요 판단
}
                                -> 기존 실행 재개 또는 다른 실행 문맥 선택
</pre>
</pre>


타이머 사건은 CPU 사용 시간을 반영하고 선점 필요성을 판단할 기회를 제공한다. 실행 가능한 다른 작업이 없거나 현재 작업이 계속 실행되어야 한다면 그대로 복귀할 수 있다. 반대로 다른 작업을 실행해야 한다면, 안전한 시점에서 스케줄러가 문맥 교환을 수행한다. 따라서 “타이머 인터럽트마다 반드시 다른 프로세스로 교체한다”는 설명은 일반적인 규칙이 아니다.
시스템 콜을 유일한 양보 지점으로 삼았다면 커널은 다음 양보 지점을 기다려도 제어권을 돌려받지 못한다. '''시스템 콜마다 양보하게 하는 것과, 시스템 콜을 하지 않는 코드에서도 CPU를 회수하는 것은 다르다.''' 일반적인 선점형 시분할은 사용자 코드와 독립적으로 만료를 알리는 타이머를 사용한다. 커널이 통지 시각을 설정하면 반복문이 계속되어도 인터럽트로 다시 제어권을 얻을 수 있다.<ref name="riscv-timer">[https://docs.riscv.org/reference/isa/v20260120/priv/machine.html RISC-V Instruction Set Manual: Machine-Level ISA], 타이머와 인터럽트 레지스터.</ref>
 
타이머 통지가 곧바로 선점 완료를 보장하지도 않는다. 커널은 공유 상태를 갱신하는 짧은 구간에서 해당 CPU의 마스킹 가능한 인터럽트를 막거나, 인터럽트는 허용하면서 스레드 선점을 미룰 수 있다. 두 제약은 서로 다르다. 전자는 처리기 진입을 늦추고 후자는 실행 대상 변경을 늦춘다. 선점 요청이 생겨도 보호 구간을 벗어날 때까지 기다릴 수 있으므로, 이런 구간의 길이가 응답 지연에 영향을 준다.<ref name="preempt-safe">Linux kernel documentation, [https://docs.kernel.org/locking/preempt-locking.html Proper Locking Under a Preemptible Kernel: Keeping Kernel Code Preempt-Safe].</ref>


== 통지 빈도와 처리 비용 ==
타이머 통지는 시간 계산과 선점 판단의 기회다. 다른 실행 가능 작업이 없거나 현재 작업을 유지할 이유가 있으면 그대로 복귀한다. 또한 인터럽트 진입을 잠시 막는 것과 스레드 선점을 미루는 것은 다른 제약이며, 안전한 시점까지 컨텍스트 스위치가 지연될 수 있다.<ref name="preempt-safe">[https://docs.kernel.org/locking/preempt-locking.html Linux 커널 문서: Proper Locking Under a Preemptible Kernel].</ref>


장치가 사건마다 통지하면 개별 사건에 빠르게 반응할 수 있지만, 처리할 때마다 진입·복귀 비용이 든다. 여러 사건을 모아 통지하거나 한 번 진입한 뒤 묶어서 처리하면 처리량을 높일 수 있는 대신, 먼저 도착한 사건이 기다리는 시간이 생긴다. 네트워크의 인터럽트와 폴링 결합도 이 절충을 이용한다.<ref name="interrupt-polling" />
=== 통지 빈도와 처리 비용 ===
인터럽트 통지나 컨텍스트 스위치를 자주 하면 응답 기회는 늘지만 처리기·스케줄러 실행과 캐시 교란 비용도 늘 수 있다. 여러 사건을 묶으면 처리량은 좋아질 수 있지만 먼저 도착한 사건은 기다려야 한다. 처리량과 개별 요청의 지연 시간을 함께 고려해야 한다.<ref name="switch-cache">Mogul·Borg, [https://decdoc.itsx.net/WRL/WRL-TN-16.pdf The Effect of Context Switches on Cache Performance], 1990.</ref>


라운드 로빈처럼 시간 할당량을 두는 정책에서는 할당량이 짧을수록 다른 실행 가능 작업이 CPU를 받을 기회가 자주 온다. 대신 상태 저장과 복원, 스케줄러 실행 비용을 더 자주 지불한다. 작업이 바뀌면 이전 작업에 유리했던 캐시 내용이 다음 작업에는 유용하지 않을 수도 있다. 할당량을 길게 잡으면 이 비용을 줄이기 쉽지만 실행을 기다리는 다른 작업의 응답은 늦어진다. 타이머 자체가 공정성을 만드는 것은 아니며, 공정성은 시간 계산과 선택 정책이 함께 결정한다.<ref name="switch-cache">Jeffrey C. Mogul, Anita Borg, [https://decdoc.itsx.net/WRL/WRL-TN-16.pdf The Effect of Context Switches on Cache Performance], DEC WRL Technical Note TN-16, 1990.</ref>
고정 주기의 타이머만 가능한 것은 아니다. 다음 필요한 시각에 한 번 울리는 원샷 타이머를 재설정할 수 있다. 틱리스 설계는 불필요한 주기적 통지를 줄이는 것이며, 선점 자체를 포기하는 방식은 아니다.<ref name="dynamic-ticks">[https://docs.kernel.org/timers/highres.html High resolution timers and dynamic ticks], [https://docs.kernel.org/timers/no_hz.html NO_HZ].</ref>


고정 간격의 주기적 틱만 가능한 것은 아니다. 다음 필요한 시각에 한 번 울리는 원샷 타이머를 재설정할 수도 있다. 틱리스 설계는 유휴 상태나 실행 가능한 작업이 하나인 상황에서 불필요한 주기적 통지를 줄인다. 이는 선점을 포기한다는 뜻이 아니라, 제어권 회수나 시간 처리가 필요한 시점에 맞춰 통지를 구성한다는 뜻이다.<ref name="dynamic-ticks">Linux kernel documentation, [https://docs.kernel.org/timers/highres.html High resolution timers and dynamic ticks design notes], [https://docs.kernel.org/timers/no_hz.html NO_HZ: Reducing Scheduling-Clock Ticks].</ref>
== 실시간 제어와 RTOS ==
장치 제어에서는 결과뿐 아니라 도착 시점도 중요하다. 실시간 운영체제(RTOS: Real-Time Operating System)는 평균 처리량보다 '''응답 시간의 상한과 예측 가능성'''을 우선한다. 인터럽트를 막은 구간, 처리기 실행, 스케줄링과 잠금 대기뿐 아니라 버스 경쟁과 DMA 완료 대기도 실제 출력의 마감시간에 영향을 준다. 높은 우선순위나 RTOS 도입만으로 마감시간이 보장되지는 않으며, 주어진 작업량과 하드웨어 조건에서 최악의 실행·대기 시간을 검토해야 한다.<ref name="qnx-rtos-latency">[https://qnx.com/developers/docs/7.1/com.qnx.doc.neutrino.sys_arch/topic/kernel_Scheduling_latency.html QNX Neutrino 7.1: Scheduling latency], 인터럽트 지연과 스케줄링 지연의 구분.</ref>


== 관련 각론 ==
== 관련 문서 ==
* [[운영체제 기본/Linux의 키보드 입력]]: i8042 키보드의 IRQ에서 입력 이벤트와 프로세스 깨우기까지.
* [[운영체제 기본/파일과 입출력]]: 장치 위에서 제공하는 파일·버퍼링·영속성.
* [[운영체제 기본/x86-64 Linux의 i8042 키보드 입력]]: i8042의 IRQ부터 입력 이벤트와 프로세스 깨우기까지.


== 참고 문헌 ==
== 참고 문헌 ==
<references />
<references />

2026년 10월 31일 (토) 09:01 기준 최신판

이전: 보호와 시스템 콜 · 목차 · 다음: 프로세스와 스레드

장치와 소프트웨어의 상호작용에는 세 가지 일이 있다. 제어 명령을 전달하는 일, 데이터를 옮기는 일, 사건이나 완료를 확인하는 일이다. 장치 레지스터 접근, DMA, 인터럽트는 각각 이 과정의 다른 부분을 담당한다. DMA로 데이터를 전송하면서 완료는 인터럽트로 통지받을 수도 있고, 같은 DMA 전송의 완료를 폴링으로 확인할 수도 있다.

장치 제어와 데이터 전송

레지스터를 통한 장치 제어

장치는 소프트웨어가 읽거나 쓸 수 있는 레지스터를 제공한다. 제어 레지스터에는 시작이나 중지 명령을 쓰고, 상태 레지스터에서는 준비 상태나 오류를 확인한다. 데이터 레지스터를 통해 실제 데이터를 주고받는 장치도 있다. 드라이버는 장치 규약과 운영체제 요청을 연결한다.

메모리 매핑 입출력인 MMIO(Memory-Mapped Input/Output)는 CPU의 주소 공간 일부를 장치 접근에 사용한다. 포트 입출력은 일반 메모리와 구별되는 포트 주소 공간을 사용하며, x86에서는 별도의 입출력 명령으로 접근한다. 두 방식은 레지스터에 접근하는 방법의 차이다. 데이터 전송을 CPU가 담당할지 다른 하드웨어에 맡길지와는 별개의 구분이다.[1]

장치 레지스터는 읽기가 상태를 바꾸거나 쓰기가 동작을 시작할 수 있다. 따라서 일반 변수처럼 접근을 생략하거나 순서를 바꿔도 된다고 가정할 수 없다. 쓰기 명령의 실행이 끝났더라도 실제 장치에 쓰기가 도착했는지는 별도로 판단해야 하는 하드웨어도 있다.[1]

DMA와 버퍼의 소유권

CPU가 데이터 레지스터를 읽어 메모리에 저장하는 방식에서는 CPU 명령 실행이 데이터 이동을 담당한다. i8042 입력에서 CPU가 컨트롤러의 데이터 레지스터를 읽는 것도 이쪽에 해당한다. 인터럽트가 읽을 시점을 알려주더라도 그 전송이 DMA가 되지는 않는다.

직접 메모리 접근인 DMA(Direct Memory Access)에서는 장치 자체의 전송 엔진이나 별도의 DMA 컨트롤러가 메모리 접근을 수행한다. CPU가 전송되는 데이터마다 읽기와 쓰기 명령을 실행할 필요는 없다. 주변장치 대신 별도 컨트롤러가 전송하는 구조도 있고, 장치가 직접 버스 접근을 시작하는 버스 마스터 구조도 있다.[2][3]

DMA에서도 드라이버는 버퍼를 준비하고 장치가 사용할 주소와 길이와 방향을 설정하며 전송을 제출한다. 전송 중인 버퍼를 덮어쓰거나 해제하면 데이터가 손상될 수 있으므로, CPU와 장치가 언제 데이터를 변경해도 되는지 소유권 규약이 필요하다. 버퍼와 주소 매핑은 전송이 끝날 때까지 유효해야 하며, 완료와 필요한 동기화를 확인한 뒤 재사용한다.[2]

DMA는 데이터 이동 방식이고 폴링과 인터럽트는 상태를 확인하거나 사건을 알리는 방식이다. DMA 완료를 폴링으로 확인할 수도 있다. 네트워크 수신과 저장장치 블록 전송은 대표적인 용도지만, 모든 전송에 DMA가 필요한 것은 아니다.

주소와 일관성과 전송 비용

CPU의 가상 주소, CPU의 물리 주소, 장치의 DMA 주소는 구별해야 한다. 단순한 시스템에서는 DMA 주소와 물리 주소가 같지만 일반적으로 이를 가정할 수 없다. 입출력 메모리 관리 장치인 IOMMU(Input-Output Memory Management Unit)는 장치의 주소를 물리 메모리로 변환하고, 설정에 따라 접근 가능한 영역을 제한하는 데에도 쓰인다. 모든 시스템에 IOMMU가 있는 것은 아니다.[3]

CPU 캐시와 장치가 보는 메모리 사이에는 동기화가 필요할 수 있다. 하드웨어가 캐시 일관성을 제공하더라도 접근 순서까지 자동으로 보장하지는 않는다. 전송 요청을 기록한 디스크립터에서 주소와 길이보다 유효 표시가 먼저 보이면 장치가 미완성 작업을 실행할 수 있다. 메모리 배리어는 이런 공개 순서를 보장하는 데 사용한다.[3]

DMA는 CPU의 반복적인 복사를 줄이지만 설정과 완료 처리, 주소 매핑, 캐시 동기화 비용이 든다. 장치는 버스와 메모리 대역폭을 소비하고 다른 작업과 경합한다. 임시 버퍼 때문에 추가 복사가 필요할 수도 있다. 버퍼 부족과 요청을 모으는 정책은 대기 시간에도 영향을 준다. 작은 전송은 CPU가 직접 처리하는 편이 단순할 수 있으며, DMA 사용 여부는 처리량뿐 아니라 지연과 자원 사용까지 고려해야 한다.

사건 통지와 인터럽트

실행 흐름과 독립적인 사건의 통지

키보드 입력, 네트워크 패킷 도착, 저장장치의 입출력 완료는 실행 중인 프로그램의 명령 순서와 독립적으로 발생한다. 하드웨어 인터럽트는 사건을 CPU에 알리고 운영체제가 처리할 기회를 만드는 통지 수단이다. 키보드의 입력 바이트를 읽거나 패킷을 처리하는 일, 완료된 요청을 확인하는 일은 통지를 받은 뒤 각 처리기가 수행한다. 스케줄링과 선점은 그 활용 중 하나이며, 모든 인터럽트가 CPU 양보를 목적으로 발생하지는 않는다.

폴링은 장치 상태를 반복해서 확인한다. 자주 확인하면 응답은 빨라지지만 사건이 없을 때도 CPU 시간을 쓰고, 확인 간격을 늘리면 처리가 늦어진다. 인터럽트는 이런 반복 확인을 줄일 수 있으나 사건이 빈번하면 진입·복귀 비용이 커진다. 인터럽트로 처리를 시작한 뒤 폴링으로 여러 결과를 묶어 처리하는 설계도 사용한다.[4]

인터럽트와 동기적 예외

장치 인터럽트는 비동기적 사건인 반면, 메모리 접근 예외는 현재 명령의 실행과 연결된다. 시스템 콜(System Call)도 프로그램이 정해진 진입 명령으로 요청하는 동기적 사건이다. 트랩이라는 용어의 범위는 아키텍처마다 다르며, RISC-V에서는 인터럽트와 예외에 따른 제어 이전을 모두 트랩이라고 부른다. 같은 용어를 쓴다고 발생 원인까지 같은 것은 아니다.[5]

처리 진입점과 IDT

보호에서 본 예외와 장치 인터럽트는 발생 원인이 다르지만, 사건의 종류에 맞는 처리 코드로 실행을 넘겨야 한다. 이를 사건 번호인 벡터(vector)와 처리 진입점의 대응 관계로 생각할 수 있다.

x86-64의 전통적인 처리 방식에서는 운영체제가 인터럽트 서술자 테이블(Interrupt Descriptor Table, IDT)을 초기화하고 CPU에 그 위치를 등록한다. CPU는 예외나 인터럽트의 벡터를 인덱스로 사용해 IDT 항목을 선택한다. 예를 들어 #PF는 14번, #GP는 13번 항목을 선택하여 해당 게이트가 가리키는 커널 진입 코드로 넘어간다. 각 항목은 단순한 함수 주소가 아니라, 처리 코드의 주소와 커널 진입에 필요한 속성을 담은 게이트(gate)다. 여기에는 대상 코드 영역, 스택 선택 정보, 게이트 유형과 접근 제한 등이 포함된다.[6]

잘못된 페이지 접근 → #PF(14) → IDT[14] → 커널의 페이지 폴트 진입 코드
일반 보호 예외     → #GP(13) → IDT[13] → 커널의 일반 보호 예외 진입 코드
장치의 인터럽트    → 벡터 v  → IDT[v]  → 커널의 인터럽트 진입 코드

게이트의 서술자 특권 수준(Descriptor Privilege Level, DPL)은 INT n 같은 소프트웨어 명령으로 그 진입점을 호출할 수 있는지를 제한한다. 하드웨어 인터럽트나 CPU가 검출한 예외에는 이 게이트 DPL 검사를 적용하지 않는다.[6]

진입할 때 CPU가 복귀에 필요한 일부 상태를 저장하고, 운영체제의 진입 코드가 필요한 상태를 추가로 보존한다. 처리가 끝나면 소프트웨어가 상태를 복원하고 IRETQ 같은 복귀 명령으로 실행을 이어 간다. 이 진입과 복귀 자체가 다른 프로세스로의 컨텍스트 스위치(Context Switch)를 뜻하지는 않는다. 다른 프로세스를 실행할지는 스케줄러의 별도 결정이다.[6]

구체적인 x86 진입 코드는 키보드 IRQ 각론으로 이어진다. IDT는 모든 CPU의 공통 구조는 아니다. 또한 x86-64의 SYSCALL은 별도로 설정된 진입 주소를 사용하므로, 모든 시스템 콜이 IDT를 거친다고 설명해서는 안 된다.[6]

진입, 상태 보존, 복귀

CPU는 인터럽트를 받아들일 때 복귀 위치와 이전 실행 권한 등 필요한 최소 상태를 보존하고 설정된 처리기 진입점으로 이동한다. 진입 코드는 나머지 레지스터와 처리기에 필요한 스택을 준비한다. 하드웨어와 소프트웨어가 맡는 범위는 아키텍처마다 다르다. 사용자 코드 실행 중이었다면 커널 처리를 위한 권한 전환도 이루어지며, 진입점과 권한을 함께 통제해 보호 경계를 유지한다.[7]

원래 실행을 재개할 때는 저장 상태를 복원하고 복귀 절차를 거친다. 처리기가 잠깐 실행된 것과 다른 스레드로 컨텍스트 스위치를 한 것은 다르다. 실행 대상을 바꾸는 결정은 스케줄러가 별도로 수행한다.[8]

입출력 완료와 깨우기

입출력을 기다리는 스레드는 대기 상태로 두고 다른 작업에 CPU를 줄 수 있다. 장치가 완료 인터럽트를 보내면 드라이버가 요청의 결과를 확인하고 해당 요청을 기다리던 스레드를 깨운다. 깨우기는 실행 가능 상태로 바꾸는 일이지, 인터럽트 처리기 안에서 사용자 프로그램을 호출하는 일이 아니다.[9]

입출력 대기 → 완료 통지 → 결과 확인과 깨우기
           → 실행 가능 상태 → 스케줄러 선택 → 실행 재개

장치 인터럽트는 다른 작업이 실행 중일 때도 도착한다. 따라서 인터럽트에서 복귀하는 대상과 방금 깨어난 스레드가 같다고 볼 수 없다. DMA 전송이라면 완료 확인 뒤 버퍼를 사용할 수 있는 상태로 동기화하는 일도 필요하다.

타이머 인터럽트와 선점

시스템 콜마다 실행 중인 작업을 양보시킨다고 가정하자. 다음 C 반복문에는 시스템 콜 자체가 없다.

while (1) {
}

시스템 콜을 유일한 양보 지점으로 삼았다면 커널은 다음 양보 지점을 기다려도 제어권을 돌려받지 못한다. 시스템 콜마다 양보하게 하는 것과, 시스템 콜을 하지 않는 코드에서도 CPU를 회수하는 것은 다르다. 일반적인 선점형 시분할은 사용자 코드와 독립적으로 만료를 알리는 타이머를 사용한다. 커널이 통지 시각을 설정하면 반복문이 계속되어도 인터럽트로 다시 제어권을 얻을 수 있다.[10]

타이머 통지는 시간 계산과 선점 판단의 기회다. 다른 실행 가능 작업이 없거나 현재 작업을 유지할 이유가 있으면 그대로 복귀한다. 또한 인터럽트 진입을 잠시 막는 것과 스레드 선점을 미루는 것은 다른 제약이며, 안전한 시점까지 컨텍스트 스위치가 지연될 수 있다.[11]

통지 빈도와 처리 비용

인터럽트 통지나 컨텍스트 스위치를 자주 하면 응답 기회는 늘지만 처리기·스케줄러 실행과 캐시 교란 비용도 늘 수 있다. 여러 사건을 묶으면 처리량은 좋아질 수 있지만 먼저 도착한 사건은 기다려야 한다. 처리량과 개별 요청의 지연 시간을 함께 고려해야 한다.[12]

고정 주기의 타이머만 가능한 것은 아니다. 다음 필요한 시각에 한 번 울리는 원샷 타이머를 재설정할 수 있다. 틱리스 설계는 불필요한 주기적 통지를 줄이는 것이며, 선점 자체를 포기하는 방식은 아니다.[13]

실시간 제어와 RTOS

장치 제어에서는 결과뿐 아니라 도착 시점도 중요하다. 실시간 운영체제(RTOS: Real-Time Operating System)는 평균 처리량보다 응답 시간의 상한과 예측 가능성을 우선한다. 인터럽트를 막은 구간, 처리기 실행, 스케줄링과 잠금 대기뿐 아니라 버스 경쟁과 DMA 완료 대기도 실제 출력의 마감시간에 영향을 준다. 높은 우선순위나 RTOS 도입만으로 마감시간이 보장되지는 않으며, 주어진 작업량과 하드웨어 조건에서 최악의 실행·대기 시간을 검토해야 한다.[14]

관련 문서

참고 문헌

  1. ↑ 1.0 1.1 Linux Kernel Documentation, Bus-Independent Device Accesses.
  2. ↑ 2.0 2.1 Linux Kernel Documentation, DMA Engine API Guide.
  3. ↑ 3.0 3.1 3.2 Linux Kernel Documentation, Dynamic DMA mapping Guide.
  4. ↑ Linux 커널 문서: NAPI, 인터럽트와 폴링의 결합.
  5. ↑ RISC-V Instruction Set Manual: Exceptions, Traps, and Interrupts.
  6. ↑ 6.0 6.1 6.2 6.3 Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, 2016년 9월, 6.10~6.14절 및 5.8.8절.
  7. ↑ Cox 외, xv6, 2025, 4.1·4.2절.
  8. ↑ Cox 외, xv6, 8.1·8.2절.
  9. ↑ Cox 외, xv6, 6.1·9.1절.
  10. ↑ RISC-V Instruction Set Manual: Machine-Level ISA, 타이머와 인터럽트 레지스터.
  11. ↑ Linux 커널 문서: Proper Locking Under a Preemptible Kernel.
  12. ↑ Mogul·Borg, The Effect of Context Switches on Cache Performance, 1990.
  13. ↑ High resolution timers and dynamic ticks, NO_HZ.
  14. ↑ QNX Neutrino 7.1: Scheduling latency, 인터럽트 지연과 스케줄링 지연의 구분.