운영체제 기본/장치 제어와 입출력
이전: 보호와 시스템 호출 · 목차 · 다음: 프로세스와 스레드
실행 중인 프로그램에서 제어권을 되찾는 문제
시스템 호출이 일어날 때마다 커널이 실행 중인 작업을 양보시킨다고 가정하자. 파일을 읽거나 출력하는 프로그램에서는 이 방법으로 실행 기회를 나눌 수 있다. 그러나 사용자 코드가 다음 C 반복문에 들어가면 시스템 호출 자체가 발생하지 않는다.
while (1) {
}
시스템 호출을 유일한 양보 지점으로 삼았다면, 커널은 이 반복문에서 다음 양보 지점을 기다려도 제어권을 돌려받지 못한다. 해당 논리 CPU에서 실행을 기다리는 다른 작업은 차례를 얻지 못한다. 시스템 호출마다 양보하도록 만드는 것과, 시스템 호출을 하지 않는 프로그램에서도 CPU를 회수할 수 있는 것은 다른 문제다.
협력형 스케줄링은 프로그램이 자발적으로 양보하거나 입출력을 기다릴 때 제어권을 돌려받는다. 일반적인 선점형 시분할 운영체제는 그 협조에 의존하지 않도록 타이머 인터럽트를 사용한다. 타이머는 사용자 코드가 무엇을 실행하는지와 독립적으로 만료를 알리므로, 위 반복문이 계속되어도 커널이 다시 실행되어 스케줄링을 판단할 기회를 얻는다. 구체적인 경로는 타이머 인터럽트와 선점에서 다룬다.
인터럽트와 동기적 예외
하드웨어 인터럽트는 현재 실행하는 명령의 의미와 독립적으로 발생하는 비동기적 사건을 CPU에 알린다. 타이머 만료와 장치의 입출력 완료가 대표적이다. 반면 잘못된 메모리 접근이나 허용되지 않은 명령 실행으로 발생하는 예외는 해당 명령과 연결된 동기적 사건이다. 시스템 호출도 프로그램이 정해진 진입 명령을 실행하여 요청하므로, 장치가 비동기적으로 알리는 인터럽트와 구분해야 한다.
트랩이라는 용어의 범위는 아키텍처와 문헌마다 다르다. RISC-V에서는 예외나 인터럽트로 처리기에 제어가 넘어가는 일을 모두 트랩이라고 부른다. 따라서 “시스템 호출도 트랩이다”라는 설명에서 “시스템 호출도 비동기 하드웨어 인터럽트다”라는 결론을 내리면 안 된다.[1]
진입, 상태 보존, 복귀
CPU는 인터럽트를 받아들일 때 복귀 위치와 이전 실행 권한 등 필요한 최소 상태를 아키텍처가 정한 방식으로 보존하고, 미리 설정된 처리기 진입점으로 이동한다. 사용자 코드 실행 중이었다면 커널 처리를 위한 권한 전환도 이루어진다. 단순히 권한만 높여 사용자 코드를 계속 실행시키는 것이 아니다. 진입점과 권한 전환을 함께 통제해야 보호 경계를 유지할 수 있다.
하드웨어가 모든 레지스터를 저장하는 것은 아니다. 진입 코드가 나머지 필요한 레지스터를 보존하고, 커널 스택이나 전용 인터럽트 스택 등 처리기에 적합한 실행 환경을 마련한다. 하드웨어가 담당하는 범위는 아키텍처마다 다르다. 예를 들어 RISC-V에서는 일반 레지스터 저장과 커널 스택 선택을 소프트웨어가 수행한다.[2]
처리를 마친 뒤 원래 실행을 재개한다면 저장한 상태를 복원하고 인터럽트 복귀 절차를 거친다. 처리기가 잠깐 실행되었다는 사실과 다른 스레드로 문맥 교환했다는 사실은 다르다. 문맥 교환은 스케줄러가 실행 대상을 바꾸기로 결정했을 때 기존 실행 문맥을 보존하고 다른 문맥을 복원하는 별도 단계다. 진입만으로 실행 대상이 바뀌지는 않는다.[3]
타이머 인터럽트와 선점
커널은 타이머에 다음 통지 시점을 설정한다. 시간의 진행과 만료 통지는 사용자 프로그램의 반복문과 독립적으로 이루어지므로, 프로그램이 시스템 호출을 전혀 하지 않아도 CPU는 타이머 인터럽트를 받아 커널로 진입할 수 있다. 사용자 프로그램이 이 타이머나 인터럽트 허용 상태를 마음대로 바꿀 수 없어야 이 장치가 제어권 회수 수단으로 기능한다.[4]
사용자 코드 실행 -> 타이머 만료 -> 인터럽트 진입과 상태 보존
-> 시간 계산과 재스케줄 필요 판단
-> 기존 실행 재개 또는 다른 실행 문맥 선택
타이머 사건은 CPU 사용 시간을 반영하고 선점 필요성을 판단할 기회를 제공한다. 실행 가능한 다른 작업이 없거나 현재 작업이 계속 실행되어야 한다면 그대로 복귀할 수 있다. 반대로 다른 작업을 실행해야 한다면, 안전한 시점에서 스케줄러가 문맥 교환을 수행한다. 따라서 “타이머 인터럽트마다 반드시 다른 프로세스로 교체한다”는 설명은 일반적인 규칙이 아니다.
타이머 통지가 곧바로 선점 완료를 보장하지도 않는다. 커널은 공유 상태를 갱신하는 짧은 구간에서 해당 CPU의 마스킹 가능한 인터럽트를 막거나, 인터럽트는 허용하면서 스레드 선점을 미룰 수 있다. 두 제약은 서로 다르다. 전자는 처리기 진입을 늦추고 후자는 실행 대상 변경을 늦춘다. 선점 요청이 생겨도 보호 구간을 벗어날 때까지 기다릴 수 있으므로, 이런 구간의 길이가 응답 지연에 영향을 준다.[5]
시간 간격과 통지 비용
라운드 로빈처럼 시간 할당량을 두는 정책에서는 할당량이 짧을수록 다른 실행 가능 작업이 CPU를 받을 기회가 자주 온다. 대신 상태 저장과 복원, 스케줄러 실행 비용을 더 자주 지불한다. 작업이 바뀌면 이전 작업에 유리했던 캐시 내용이 다음 작업에는 유용하지 않을 수도 있다. 할당량을 길게 잡으면 이 비용을 줄이기 쉽지만 실행을 기다리는 다른 작업의 응답은 늦어진다. 타이머 자체가 공정성을 만드는 것은 아니며, 공정성은 시간 계산과 선택 정책이 함께 결정한다.[6]
고정 간격의 주기적 틱만 가능한 것은 아니다. 다음 필요한 시각에 한 번 울리는 원샷 타이머를 재설정할 수도 있다. 틱리스 설계는 유휴 상태나 실행 가능한 작업이 하나인 상황에서 불필요한 주기적 통지를 줄인다. 이는 선점을 포기한다는 뜻이 아니라, 제어권 회수나 시간 처리가 필요한 시점에 맞춰 통지를 구성한다는 뜻이다.[7]
입출력 완료와 깨우기
장치에서 데이터가 오기를 기다리는 스레드는 CPU를 받아도 해당 작업을 진행할 수 없다. 운영체제는 이 스레드를 대기 상태로 두고 다른 실행 가능 작업에 CPU를 준다. 장치가 완료 인터럽트를 보내면 드라이버가 완료 상태를 확인하고 기다리던 작업을 깨운다. 깨우기는 대기 상태를 실행 가능 상태로 바꾸는 일이며, 준비된 작업은 실행 대기열에서 스케줄러의 선택을 기다린다.[8]
입출력 대기 -> 장치 완료 인터럽트 -> 드라이버의 완료 확인과 깨우기
-> 실행 가능 상태와 실행 대기열 -> 스케줄러 선택 -> 실행 재개
장치 인터럽트는 다른 프로세스가 실행 중일 때도 도착한다. 따라서 인터럽트에서 복귀하는 대상과 방금 깨어난 스레드가 같다고 볼 수 없다. 타이머가 CPU를 회수할 기회를 만든다면, 장치 인터럽트는 기다리던 조건이 충족되었음을 알리는 역할을 한다.
인터럽트 대신 장치 상태를 반복 확인하는 폴링도 가능하다. 폴링은 사건이 드물면 CPU 시간을 낭비하지만, 사건이 빈번하면 여러 결과를 연속 처리하여 잦은 인터럽트 진입 비용을 줄일 수 있다. 인터럽트로 처리를 시작하고 폴링으로 묶어서 처리하는 조합도 사용한다.[9]
관련 각론
- 운영체제 기본/Linux의 키보드 입력: i8042 키보드의 IRQ에서 입력 이벤트와 프로세스 깨우기까지.
참고 문헌
- ↑ RISC-V International, The RISC-V Instruction Set Manual, Volume I: Exceptions, Traps, and Interrupts.
- ↑ Russ Cox, Frans Kaashoek, Robert Morris, xv6: a simple, Unix-like teaching operating system, 2025, 4.1절과 4.2절.
- ↑ Cox 외, xv6, 8.1절과 8.2절.
- ↑ RISC-V International, The RISC-V Instruction Set Manual, Volume II: Machine-Level ISA, Machine Timer Registers 및 Machine Interrupt Registers.
- ↑ Linux kernel documentation, Proper Locking Under a Preemptible Kernel: Keeping Kernel Code Preempt-Safe.
- ↑ Jeffrey C. Mogul, Anita Borg, The Effect of Context Switches on Cache Performance, DEC WRL Technical Note TN-16, 1990.
- ↑ Linux kernel documentation, High resolution timers and dynamic ticks design notes, NO_HZ: Reducing Scheduling-Clock Ticks.
- ↑ Cox 외, xv6, 6.1절 및 9.1절.
- ↑ Linux kernel documentation, NAPI, Datapath API 및 Scheduling and IRQ masking.