Chapter 18

트랩: 예외와 인터럽트

사용자 프로그램이 ecall 한 줄을 실행하면 하트는 하던 일을 멈추고 운영체제 코드로 뛰어든다. 타이머가 울릴 때도, 아직 메모리에 없는 페이지를 건드릴 때도 마찬가지다. 이렇게 정상적인 흐름을 끊고 처리기로 제어를 넘기는 일이 트랩이다. 이 장에서는 트랩이 일어나는 순간 CSR이 어떤 순서로 바뀌는지, 어떤 트랩을 누가 받는지(위임), 여러 인터럽트가 한꺼번에 오면 무엇을 먼저 받는지, 그리고 PLIC·AIA 같은 인터럽트 컨트롤러가 장치의 신호를 하트까지 어떻게 전달하는지를 차례로 따라간다.

트랩: 흐름을 끊고 처리기로

1장에서 본 정의를 다시 떠올리자. 예외(exception)는 지금 실행하는 명령 때문에 생긴 비정상 상황이고, 인터럽트(interrupt)는 명령과 상관없이 바깥에서 비동기적으로 들어온 사건이다. 둘 중 무엇이든 제어가 처리기로 넘어가는 일 자체를 트랩(trap)이라 한다. 17장에서 보았듯 하트가 특권 모드를 바꾸는 정상적인 길은 트랩(올라가기)과 xRET 명령(내려가기)뿐이다.

사용자 프로그램 · U 모드 트랩 처리기 · S 모드 addi a0, a0, 1 lw a1, 0(a2) li a7, 64 ecallpc = 0x10148 add a0, a0, a10x1014c csrrw sp, sscratch, sp 레지스터를 메모리에 저장 scause = 8 → 시스템 호출 처리 sepc ← sepc + 4 sret 1 2 ① ecall → 트랩: sepc ← 0x10148(ecall 자신의 주소), scause ← 8, SPP ← U, pc ← stvec ② sret → pc ← sepc(처리기가 4를 더해 둔 0x1014c), 모드 ← SPP(U), SIE ← SPIE
그림 18-1. 시스템 호출 한 번의 왕복. 금색 점이 ① 트랩으로 올라가고 ② sret으로 내려온다. 하드웨어는 sepc에 ecall 자신의 주소를 적으므로, 다음 명령으로 돌아가려면 처리기가 직접 4를 더해야 한다.

규격은 트랩을 받는 쪽의 모드에 따라 두 가지로 부른다. 특권 수준이 올라가는 트랩(U→S, S→M 등)은 수직 트랩(vertical trap), 같은 수준에 머무는 트랩(S 모드에서 난 예외를 S 모드가 받는 경우 등)은 수평 트랩(horizontal trap)이다. 어느 쪽이든 하드웨어가 하는 일은 같다. 돌아올 주소와 원인을 CSR에 적고, 인터럽트를 끄고, 모드를 바꾸고, 처리기 주소로 뛴다.

정밀한 트랩

트랩이 일어났을 때 “어디까지 실행되었는가”가 분명해야 처리기가 고치고 다시 실행할 수 있다. 트랩 걸린 명령보다 앞선 명령은 모두 끝났고, 그 명령과 뒤따르는 명령은 아무 흔적도 남기지 않은 상태를 정밀한 트랩(precise trap)이라 한다. 비특권 규격은 트랩마다 정밀하게 처리할지를 실행 환경(EEI)이 정하되 가능하면 정밀성을 유지하라고 권고한다. 특권 규격의 PMP 위반은 “항상 정밀하게” 트랩된다고 못 박혀 있고(20장), 반대로 오래된 버스가 돌려주는 오류 응답처럼 정밀하게 잡기 어려운 경우는 부정확한 버스 오류 인터럽트로 보고될 수 있다고 적혀 있다.

동기 예외비동기 인터럽트
원인지금 명령 (잘못된 명령, 페이지 폴트, ecall, ebreak …)바깥 사건 (타이머, 장치, 다른 하트의 신호, 카운터 넘침)
xepc예외를 일으킨 명령의 주소끼어든 시점에 아직 실행하지 않은 명령의 주소
xcause 최상위 비트01
재현성같은 입력이면 같은 곳에서 다시 생긴다언제 올지 모른다
끌 수 있나끌 수 없다 (위임으로 받을 곳만 고른다)mie 비트와 전역 활성 비트로 가린다

트랩을 받아 적는 CSR들

M 모드와 S 모드는 트랩을 받을 수 있는 두 수준이다. 각자 똑같은 모양의 CSR 한 벌을 가진다. 이름의 첫 글자 m/s만 다르고 역할은 짝을 이룬다. 처리기 주소(xtvec), 돌아갈 주소(xepc), 원인(xcause), 부가 정보(xtval), 처리기용 임시 칸(xscratch)이다.

역할M 모드 CSRS 모드 CSR하는 일
상태mstatus 0x300sstatus 0x100전역 인터럽트 활성(xIE)과 트랩 직전 상태(xPIE·xPP)
위임medeleg 0x302
mideleg 0x303
—예외·인터럽트를 S 모드로 넘길지 정한다
인터럽트 활성mie 0x304sie 0x104인터럽트별 켜기·끄기
처리기 주소mtvec 0x305stvec 0x105BASE와 MODE(Direct/Vectored)
임시 칸mscratch 0x340sscratch 0x140처리기가 첫 레지스터를 얻는 데 쓴다
돌아갈 주소mepc 0x341sepc 0x141트랩 걸린(끼어든) 명령의 가상 주소
원인mcause 0x342scause 0x142Interrupt 비트 + 원인 코드
부가 정보mtval 0x343stval 0x143잘못된 주소, 명령 비트, 또는 0
인터럽트 펜딩mip 0x344sip 0x144지금 기다리는 인터럽트
mtvec MXLEN-1210 BASE[MXLEN-1:2] (WARL) MODE (WARL) mcause MXLEN-1MXLEN-20 Interrupt Exception Code (WLRL) mepc MXLEN-10 트랩 걸린(끼어든) 명령의 가상 주소 · mepc[0]은 항상 0 mtval MXLEN-10 예외별 부가 정보: 잘못된 가상 주소 · 잘못된 명령의 비트 · 또는 0
그림 18-2. 트랩 CSR의 모양. mtvec의 BASE는 4바이트 정렬이라 하위 두 비트 자리를 MODE가 쓴다. mcause는 최상위 비트 하나로 인터럽트와 예외를 가르므로, 처리기는 bltz 한 번으로 둘을 구별할 수 있다. S 모드의 stvec·scause·sepc·stval도 모양이 같다(폭만 SXLEN).

몇 가지 세부 규칙이 있다.

mscratch: 처리기의 첫 레지스터

처리기에 막 들어온 순간에는 문제가 하나 있다. 모든 정수 레지스터에 중단된 프로그램의 값이 들어 있어서, 무엇이든 하려면 레지스터 하나를 비워야 하는데 비울 곳을 계산할 레지스터조차 없다. RISC-V는 xscratch와 CSR 교환 명령으로 이 문제를 푼다. 평소 mscratch에 하트별 저장 공간의 주소를 넣어 두고, 처리기 첫 명령에서 sp와 맞바꾸는 것이다.

MIPS의 k0/k1과 다른 길

MIPS는 운영체제 전용 사용자 레지스터 두 개(k0/k1)를 따로 떼어 이 문제를 풀었다. 규격의 비고는 그 방식이 사용자 레지스터를 줄이고, 특권 수준이 늘거나 트랩이 중첩되면 확장되지 않으며, 사용자로 돌아가기 전에 지워야 하는 보안 부담까지 있다고 지적한다. RISC-V는 사용자 ISA를 운영체제 사정으로 “오염”시키지 않으려고 CSR 교환으로 같은 효과를 낸다.

원인 코드: mcause가 말해 주는 것

mcause에 적히는 값은 Interrupt 비트와 원인 코드의 조합이다. RV32에서 머신 타이머 인터럽트는 0x80000007, U 모드의 ecall은 0x00000008로 읽힌다. 아래 표는 v20260120 규격의 표를 그대로 옮긴 것이다. 비어 있는 번호는 예약(Reserved)이다.

코드예외 (Interrupt = 0)xtval에 남는 값 (0이 아니라면)
0Instruction address misaligned (명령 주소 정렬 오류)잘못된 가상 주소
1Instruction access fault (명령 접근 폴트)잘못된 가상 주소
2Illegal instruction (잘못된 명령)명령 비트 (선택)
3Breakpoint (중단점)주소 중단점은 그 주소, ebreak는 0 또는 명령 주소
4Load address misaligned (적재 주소 정렬 오류)잘못된 가상 주소
5Load access fault (적재 접근 폴트)잘못된 가상 주소
6Store/AMO address misaligned잘못된 가상 주소
7Store/AMO access fault잘못된 가상 주소
8Environment call from U-mode0
9Environment call from S-mode0
11Environment call from M-mode0
12Instruction page fault (명령 페이지 폴트)잘못된 가상 주소
13Load page fault (적재 페이지 폴트)잘못된 가상 주소
15Store/AMO page fault잘못된 가상 주소
16Double trap (이중 트랩, Ssdbltrp)원래 원인은 mtval2에
18Software check (소프트웨어 검사, CFI 등)0 · 2(착지 패드) · 3(그림자 스택)
19Hardware error (하드웨어 오류)잘못된 가상 주소
24–31, 48–63사용자 정의(custom) 용도—
코드인터럽트 (Interrupt = 1)보통의 출처
1Supervisor software interrupt (SSI)다른 하트가 보내는 S 수준 신호, 또는 M 모드 소프트웨어
3Machine software interrupt (MSI)메모리 매핑된 msip 레지스터 (하트 간 인터럽트)
5Supervisor timer interrupt (STI)stimecmp(Sstc) 또는 M 모드 소프트웨어가 STIP를 세움
7Machine timer interrupt (MTI)mtime ≥ mtimecmp
9Supervisor external interrupt (SEI)PLIC·APLIC·IMSIC 같은 외부 인터럽트 컨트롤러
11Machine external interrupt (MEI)외부 인터럽트 컨트롤러
13Counter-overflow interrupt (LCOFI)Sscofpmf 성능 카운터 넘침
≥ 16플랫폼 용도플랫폼이 정한다
ecall이 세 개의 코드를 갖는 이유

같은 ecall이 실행한 모드에 따라 8·9·11로 다르게 보고된다. 규격의 비고에 따르면 그래야 “U 모드의 ecall은 S로 위임하고, S 모드의 ecall은 M이 받는” 식의 선택적 위임이 가능하다. 유닉스 계열 운영체제의 전형적인 설정이 바로 이것이다. 10번은 예약되어 있다.

예외가 겹칠 때: 동기 예외 우선순위

명령 하나가 여러 예외 조건을 동시에 만족할 수 있다. 예를 들어 정렬도 어긋나고 페이지도 없는 적재가 그렇다. 이때 mcause에 보고할 예외는 규격의 고정된 우선순위로 고른다.

높음 낮음 3명령 주소 중단점 12, 1명령 주소 변환 중 처음 만난 페이지 폴트·접근 폴트 1물리 주소로 명령을 가져올 때의 명령 접근 폴트 2 · 0 · 8,9,11 · 3잘못된 명령 · 명령 정렬 오류 · ecall · ebreak · 데이터 주소 중단점 4, 6(구현이 고르면) 적재·저장 주소 정렬 오류 13, 15, 5, 7데이터 주소 변환 중 처음 만난 페이지 폴트·접근 폴트 5, 7물리 주소로 데이터에 접근할 때의 접근 폴트 4, 6(앞에서 보고되지 않았다면) 적재·저장 주소 정렬 오류
그림 18-3. 동기 예외 우선순위(규격 Table 7). 명령을 가져오는 단계의 문제가 먼저, 데이터 접근 단계의 문제가 나중이다. 적재·저장 정렬 오류는 두 군데에 있다. 정렬을 아예 지원하지 않는 구현은 변환 전에 바로 정렬 오류를 낼 수 있고, 일부 주소에서만 지원하는 구현은 변환과 검사를 먼저 해야 하기 때문에 순서를 구현이 고르게 했다.

트랩 진입과 xRET 복귀, 한 단계씩

트랩을 받을 모드를 x(M 또는 S), 트랩 직전의 모드를 y라 하자. 규격이 정한 하드웨어 동작은 짧다.

트랩 진입 (y → x)xRET 복귀 (xPP에 y가 들어 있을 때)
xepc ← 트랩 걸린(끼어든) 명령의 주소pc ← xepc
xcause ← Interrupt 비트 + 원인 코드xIE ← xPIE
xtval ← 부가 정보 또는 0모드 ← y (xPP 값)
xPIE ← xIE, 그다음 xIE ← 0xPIE ← 1
xPP ← y, 모드 ← xxPP ← 구현된 가장 낮은 모드 (U가 있으면 U)
pc ← xtvec가 정한 주소y ≠ M이면 mstatus.MPRV ← 0

xPP는 x 이하의 모드만 담을 수 있다. 그래서 MPP는 2비트(U=00, S=01, M=11), SPP는 1비트(U=0, S=1)다. xIE·xPIE·xPP는 깊이 2짜리 작은 스택처럼 동작한다. 트랩으로 “밀어 넣고” xRET으로 “꺼낸다”. 복귀 뒤 xPP를 가장 낮은 모드로 바꿔 두는 것은 스택을 잘못 관리하는 소프트웨어 버그를 드러내기 위해서다.

① 트랩 직전 (S 모드) ② 트랩 진입 직후 (M 모드) ③ MRET 실행 후 현재 모드S MIE1 MPIE0 MPPU (00) 현재 모드M MIE0 MPIE ← MIE1 MPP ← 이전 모드S (01) 모드 ← MPPS MIE ← MPIE1 MPIE ← 11 MPP ← UU (00) 트랩 진입: MPIE ← MIE, MIE ← 0, MPP ← S(이전 모드), 모드 ← M, mepc·mcause·mtval 기록 MRET: MIE ← MPIE, 모드 ← MPP, MPIE ← 1, MPP ← U(가장 낮은 모드), pc ← mepc
그림 18-4. mstatus의 “깊이 2 스택”. 트랩은 MIE를 MPIE로 밀어 넣고 0으로 만들어, 처리기 앞부분이 인터럽트에 방해받지 않게 한다. MRET은 그 값을 되돌리고 MPP가 가리키는 모드로 내려간다.

아래 실행기에서 여러 상황을 골라 한 단계씩 진행해 보자. 단계마다 바뀌는 CSR이 금색으로 표시된다. 값은 RV32 기준이며 mtvec = 0x80000100, stvec = 0x80200800(둘 다 Direct 모드)로 두었다.

SIMULATOR

트랩 단계별 실행기

M머신 모드
S슈퍼바이저
U사용자 모드
실행
단계0 / 0
medeleg (켜진 비트)—
mideleg (켜진 비트)—
“U 모드 중 머신 타이머 (MIE=0)”와 “M 모드 중 머신 타이머 (MIE=0)”를 비교해 보자. 같은 MIE=0인데 앞쪽은 트랩이 일어나고 뒤쪽은 일어나지 않는다. 또 “M 모드 ebreak”에서는 medeleg[3]이 켜져 있어도 트랩이 S로 내려가지 않는 것을 확인하자.
처리기가 직접 해야 하는 일

하드웨어는 ecall·ebreak의 xepc에 그 명령 자신의 주소를 적는다. 그대로 xRET하면 같은 ecall을 무한히 다시 실행한다. 시스템 호출 처리기는 xepc에 4를 더해야 하고(압축 명령 c.ebreak라면 2), 페이지 폴트 처리기는 반대로 xepc를 그대로 두어 같은 명령을 다시 실행시킨다. 또 A 확장이 있으면 xRET이 LR 예약을 지워도 되고 안 지워도 되므로, 필요하면 처리기가 가짜 SC로 예약을 지운다.

처리기 주소: Direct와 Vectored

mtvec의 하위 두 비트 MODE가 처리기 주소를 정하는 방식을 고른다. MODE=0(Direct)이면 모든 트랩이 BASE 한 곳으로 간다. MODE=1(Vectored)이면 동기 예외는 여전히 BASE로 가지만, 인터럽트는 BASE + 4 × 원인 코드로 간다. 예를 들어 머신 타이머 인터럽트(7)는 BASE + 0x1c, stvec에서 S 타이머 인터럽트(5)는 BASE + 0x14다. MODE 값 2 이상은 예약되어 있다.

Direct (MODE = 0) 모든 트랩 BASE: 공통 처리기 csrr t0, mcause bltz t0, irq # 예외: 원인별 분기 irq: # 원인 번호로 표 찾기 Vectored (MODE = 1) 동기 예외 MTI (7) MEI (11) +0x00예외 전부 +0x04SSI (1) +0x0cMSI (3) +0x14STI (5) +0x1cMTI (7) +0x24SEI (9) +0x2cMEI (11) +0x34LCOFI (13) 인터럽트 → BASE + 4 × 원인 코드
그림 18-5. Direct와 Vectored. Vectored 모드의 각 칸은 4바이트, 즉 명령 하나(보통 해당 처리기로 가는 j)가 들어갈 자리다. 원인 코드가 예약된 칸(+0x08, +0x10 …)은 그림에서 생략했다. +0x00 칸은 모든 동기 예외가 함께 쓴다.
SIMULATOR

mtvec 점프 표 계산기

MODE
mtvec 값 (BASE | MODE)—
처리기 주소 (pc)—
계산—
원인을 MEI로 바꾸면 BASE + 0x2c가 된다. 같은 원인에서 MODE를 Direct로 바꾸면 BASE로 돌아간다. 예외 원인은 Vectored 모드에서도 언제나 +0x00 칸으로 간다는 것도 확인하자. BASE 끝자리를 …02처럼 4의 배수가 아니게 쓰면 하위 두 비트가 버려진다.
정렬 요구는 구현마다 다르다

BASE는 언제나 4바이트 정렬이지만, 규격은 Vectored 모드에서 더 엄격한 정렬(예: 64바이트·256바이트)을 요구해도 된다고 허락한다. 덧셈기 없이 비트를 이어 붙이기만 해서 주소를 만들 수 있게 하려는 배려다. mtvec은 WARL이므로, 원하는 값을 쓴 뒤 다시 읽어 실제로 받아들여졌는지 확인하는 것이 안전하다. 리셋 벡터와 NMI 벡터 주소는 플랫폼 규격이 정한다.

위임: 트랩을 S 모드로 바로 보내기

기본적으로 모든 트랩은 M 모드가 받는다. 그러나 사용자 프로그램의 시스템 호출이나 페이지 폴트까지 매번 M 모드 펌웨어를 거쳐 커널로 되돌려 보내면 느리다. 그래서 S 모드가 있는 하트에는 위임 레지스터(medeleg, mideleg)가 반드시 있다. medeleg의 비트 i를 켜면 원인 i인 예외가, mideleg의 비트 i를 켜면 원인 i인 인터럽트가 S 모드나 U 모드에서 일어났을 때 S 모드 처리기로 바로 간다.

U 모드 · ecall (8) S 모드 · 잘못된 명령 (2) M 모드 · ecall (11) medeleg = {8, 12, 13, 15} S 모드 처리기 (stvec)scause · sepc · stval · SPP M 모드 처리기 (mtvec)mcause · mepc · mtval · MPP 예 아니오 M 모드에서 난 트랩은 위임 비트와 상관없이 언제나 M 모드가 받는다 모드 < M 이고 medeleg[원인] = 1 ?
그림 18-6. 위임 판정. U 모드의 ecall(8)은 위임되어 S로 간다. S 모드의 잘못된 명령(2)은 비트가 꺼져 있어 M으로 간다. M 모드의 ecall(11)은 위임 판정을 거치지 않고 바로 M으로 간다.
규격 원문에서
“Traps never transition from a more-privileged mode to a less-privileged mode. For example, if M-mode has delegated illegal-instruction exceptions to S-mode, and M-mode software later executes an illegal instruction, the trap is taken in M-mode, rather than being delegated to S-mode.”

Volume II, Machine-Level ISA — Machine Trap Delegation Registers — 위임은 “낮은 모드에서 난 트랩을 어디서 받을지”만 정한다.

위임된 트랩이 S 모드로 들어오면 S 쪽 CSR만 바뀐다. scause·sepc·stval이 쓰이고, mstatus의 SPP·SPIE·SIE가 갱신된다. mcause·mepc·mtval과 MPP·MPIE는 건드리지 않는다. 그 밖의 규칙을 정리하면 다음과 같다.

하이퍼바이저가 있으면 한 겹 더

H 확장이 있으면 HS 모드가 받은 트랩을 다시 VS 모드로 넘기는 hedeleg·hideleg가 더해진다. 같은 원리가 한 층 더 쌓이는 것이므로 21장에서 다룬다.

인터럽트: 펜딩, 활성, 그리고 우선순위

인터럽트 i는 세 겹의 문을 통과해야 트랩이 된다. ① 펜딩(mip[i]=1), ② 개별 활성(mie[i]=1), ③ 전역 활성이다. mip와 mie의 비트 번호는 mcause의 인터럽트 원인 코드와 같다. 비트 15:0은 표준 인터럽트에, 16 이상은 플랫폼 용도에 쓴다.

그림 18-7. mip/mie의 비트 15:0. 파란 칸은 M 수준, 보라 칸은 S 수준, 청록 칸은 카운터 넘침(LCOFI) 인터럽트다. 아래 줄의 sip/sie는 mip/mie의 부분 보기로, mideleg로 위임된 비트만 보이고 나머지는 읽기 전용 0이다.

전역 활성 규칙

전역 활성 비트는 mstatus의 MIE(M 수준)와 SIE(S 수준)다. 규칙의 핵심은 “지금 모드”를 기준으로 판단한다는 점이다.

지금 모드 ↓ / 인터럽트의 목적지 →M으로 갈 인터럽트 (mideleg[i]=0)S로 갈 인터럽트 (mideleg[i]=1)
MMIE=1일 때만가려짐 (받지 않음)
S항상 켜짐 (MIE 무시)SIE=1일 때만
U항상 켜짐 (MIE 무시)항상 켜짐 (SIE 무시)

규격의 문장 그대로 옮기면 “모드 x에서 실행 중일 때 더 낮은 모드의 인터럽트는 항상 전역적으로 꺼져 있고, 더 높은 모드의 인터럽트는 항상 전역적으로 켜져 있다.” 그래서 높은 모드가 낮은 모드에 제어를 넘기기 전에 특정 인터럽트를 막고 싶다면 전역 비트가 아니라 mie의 개별 비트를 꺼야 한다. 여러 인터럽트가 동시에 들어오면 M으로 갈 인터럽트가 S로 갈 인터럽트보다 먼저이고, 같은 목적지 안에서는 고정된 순서를 따른다.

목적지우선순위 (높음 → 낮음)
M 모드MEI → MSI → MTI → SEI → SSI → STI → LCOFI
S 모드SEI → SSI → STI → LCOFI

규격의 비고는 이 순서의 이유를 이렇게 설명한다. 높은 모드의 인터럽트가 먼저여야 선점이 가능하다. 외부 인터럽트는 빠른 응답이 필요한 장치에서 오므로 내부(타이머·소프트웨어)보다 먼저다. 소프트웨어 인터럽트는 하트 간 메시지에 쓰이는 반면 타이머는 시분할용이라 정밀도가 덜 중요하므로 소프트웨어가 타이머보다 먼저다.

SIMULATOR

인터럽트 중재기

지금 모드
인터럽트 (원인)mipmiemideleg판정
받는 인터럽트—
목적지—
xcause (RV32)—
처음 상태는 S 모드, SIE=0이다. STI는 위임되어 SIE에 막히지만 MTI는 M으로 가므로 MIE와 상관없이 받는다. 모드를 U로 바꾸면 어떻게 될까? MTIP를 끄면 이번에는 STI가 S로 간다. 모드를 M으로 바꾸고 MIE를 꺼서 모든 인터럽트가 기다리게 만들어 보자. M 수준 인터럽트(MSI·MTI·MEI)의 mideleg 비트는 위임하지 않는 일반적인 플랫폼처럼 0으로 고정해 두었다.

타이머: mtime, mtimecmp, 그리고 stimecmp

플랫폼은 일정한 주파수로 증가하는 64비트 실시간 카운터 mtime과 하트별 64비트 비교 레지스터 mtimecmp를 메모리 매핑 레지스터로 제공한다. 주소는 플랫폼이 정한다(흔히 CLINT·ACLINT라고 부르는 장치가 이 레지스터들과 하트 간 인터럽트용 msip 레지스터를 담는다, 22장). 부호 없는 값으로 비교해 mtime ≥ mtimecmp가 되면 MTIP가 켜지고, mtimecmp에 더 큰 값을 쓸 때까지 유지된다. 사이클 카운터 대신 벽시계 시간을 쓰는 이유는, 전력 절약을 위해 클록 주파수가 수시로 바뀌는 프로세서에서도 시간이 정확해야 하기 때문이다.

mtime메모리 매핑 · 계속 증가 mtimecmp하트별 · 메모리 매핑 ≥ ? mip.MTIP mie.MTIE+ 전역 활성 규칙 M 트랩 7 읽기 전용 그림자 time CSR0xC01 · mtime의 그림자 stimecmpCSR 0x14D · Sstc ≥ ? mip.STIP STIE · mideleg[5]+ 전역 활성 규칙 S 트랩 5 Sstc 없이는 S 모드 타이머를 M 모드가 SBI 호출로 대신 관리하고, STIP를 직접 세워 준다
그림 18-8. 두 개의 타이머 경로. 위는 모든 플랫폼에 있는 머신 타이머, 아래는 Sstc 확장이 더한 S 모드 전용 타이머다. 위임된 STI는 S 모드 처리기로 바로 간다. 비교 결과가 바뀐 뒤 MTIP·STIP에 반영되기까지는 약간의 지연이 있을 수 있어서, 비교값을 늘리고 곧바로 돌아오면 가짜 타이머 인터럽트가 한 번 더 올 수도 있다.

Sstc 확장이 생기기 전에는 S 모드 운영체제가 타이머를 맞출 때마다 SBI 호출로 M 모드에 부탁해야 했다. M 모드는 자기 타이머 하나에 여러 논리 타이머를 겹쳐 관리하다가 때가 되면 STIP를 세워 S로 넘겼다. Sstc는 S 모드에 stimecmp(RV32에서는 상위 절반 stimecmph 0x15D)를 주어 이 왕복을 없앤다. time ≥ stimecmp이면 STIP가 켜지며, M 모드가 menvcfg.STCE를 켜 두어야 S 모드에서 stimecmp에 접근할 수 있다.

RV32에서는 mtimecmp를 32비트씩 두 번에 나눠 써야 한다. 규격은 중간값 때문에 엉뚱한 인터럽트가 나지 않도록 다음 순서를 예로 든다. 하위 절반에 먼저 −1(모두 1)을 써서 비교값이 잠시라도 작아지지 않게 한다.

WFI: 인터럽트를 기다리며 쉬기

wfi는 “처리할 인터럽트가 생길 때까지 이 하트를 멈춰도 된다”는 힌트다. 기다리는 동안 인터럽트가 오면 다음 명령에서 트랩이 일어나므로 mepc는 wfi 다음 명령을 가리킨다. 몇 가지 특징이 있다.

인터럽트 컨트롤러: PLIC와 AIA

mip에는 외부 인터럽트를 위한 비트가 MEIP·SEIP 둘뿐이다. 수십, 수백 개의 장치 인터럽트를 이 두 선으로 모아 주는 것이 하트 바깥의 인터럽트 컨트롤러다. 규격의 비고도 “하트의 몇 안 되는 원천 인터럽트는 고정 우선순위로 단순하게 두고, 복잡한 우선순위는 바깥의 컨트롤러가 맡는다”고 설명한다.

PLIC: 플랫폼 수준 인터럽트 컨트롤러

PLIC(Platform-Level Interrupt Controller)는 최대 1023개의 인터럽트 소스(ID 0은 “없음”으로 예약)를 최대 15872개의 문맥(context)으로 나눠 보낸다. 문맥은 대개 “하트 하나의 특권 모드 하나”이며, 어느 문맥이 어느 하트·모드인지는 구현이 문서로 정한다.

장치 · ID 1 장치 · ID 2 장치 · ID 3 ⋮ ID 1023 게이트웨이 게이트웨이 게이트웨이 게이트웨이 PLIC 코어 소스별 우선순위 (0 = 끔) 소스별 펜딩 비트 (IP) 문맥별 활성 비트 문맥별 임계값 문맥별 claim / complete 문맥 0 · 하트 0 M 문맥 1 · 하트 0 S 문맥 2 · 하트 1 M 문맥 3 · 하트 1 S EIP claim 어느 문맥이 어느 하트·모드인지는 구현이 정한다 (그림은 하나의 예)
그림 18-9. PLIC의 구조. 장치 신호는 게이트웨이를 거쳐 펜딩 비트가 된다. 코어는 문맥마다 “활성 + 펜딩 + 임계값보다 높은 우선순위” 조건을 만족하는 인터럽트가 있으면 그 문맥의 외부 인터럽트 선(MEIP 또는 SEIP)을 켠다.

규칙은 간단하다.

오프셋 (base +)내용
0x000004 – 0x000FFC소스 1–1023의 우선순위 (소스마다 32비트, 0x000000은 예약)
0x001000 – 0x00107C펜딩 비트 배열 (ID N은 워드 N/32의 비트 N mod 32)
0x002000 + 0x80 × 문맥문맥별 활성 비트 배열
0x200000 + 0x1000 × 문맥문맥별 우선순위 임계값
0x200004 + 0x1000 × 문맥문맥별 claim(읽기) / complete(쓰기)
ANIMATION

PLIC 따라가기: 알림 → claim → 처리 → complete

소스우선순위문맥 0 활성문맥 1 활성장치
재생
“세 장치 동시에 발생”을 눌러 보자. NIC(#3)와 DMA(#5)는 우선순위가 5로 같으므로 ID가 작은 #3이 먼저 claim된다. 문맥 1의 임계값이 2이므로 우선순위 2 이하인 UART(#1)·GPIO(#4)는 문맥 1에 알림을 주지 못한다. 두 문맥이 같은 소스를 모두 활성해 두면 둘 다 알림을 받지만 claim은 한쪽만 성공하고 다른 쪽은 ID 0을 받는다.

AIA: 메시지 신호 인터럽트 시대의 구조

PCI Express 같은 장치는 선을 당기는 대신 특정 주소에 메모리 쓰기를 해서 인터럽트를 알린다. 이것이 메시지 신호 인터럽트(MSI)다. AIA(Advanced Interrupt Architecture) 규격은 두 가지 컨트롤러와 하트 쪽 CSR 확장(Smaia·Ssaia)을 정의한다.

① IMSIC 없이: APLIC이 중앙 허브 장치 A 장치 B 장치 C APLIC유선 1023개 하트 0 하트 1 M·S 수준별로 따로 제어하는 인터페이스 ② IMSIC 있음: 하트마다 MSI 수신기 PCIe 장치 유선 장치 APLIC 하트 0IMSIC 하트 1IMSIC 메모리 쓰기 (MSI) APLIC은 유선 인터럽트를 MSI로 바꿔 전달한다 IMSIC 하나당 MSI 식별자 최대 2047개
그림 18-10. AIA의 두 구성. 작은 시스템은 ①처럼 APLIC이 전통적인 PLIC 역할을 한다. 큰 시스템, 특히 PCI 장치가 있는 시스템은 ②처럼 하트마다 IMSIC을 두고 장치가 메모리 쓰기로 인터럽트를 보낸다. H 확장과 함께 쓰면 게스트 인터럽트 파일로 가상 머신에 인터럽트를 직접 전달할 수도 있다.

AIA 규격은 최대 16,384개 하트를 지원하고, 하트 쪽 CSR(miselect/mireg를 통한 간접 접근, mtopi 등)을 더해 타이머·소프트웨어 인터럽트까지 포함한 모든 인터럽트의 상대 우선순위를 소프트웨어가 정할 수 있게 한다. 자세한 구성은 22장에서 플랫폼 관점으로 다시 본다.

NMI와 이중 트랩: 처리기가 위험할 때

트랩 처리기의 앞부분, 즉 xepc·xcause 같은 중요한 값을 아직 저장하지 못한 구간에서 또 트랩이 나면 그 값들이 덮어써져 원래 트랩으로 돌아갈 길을 잃는다. 그래서 규격은 처리기가 이 구간에서는 인터럽트를 켜지도 예외를 일으키지도 않도록 짜야 한다고 요구한다. 그런데 하드웨어 오류처럼 피할 수 없는 사건은 어떻게 할까?

NMI (기본)

하드웨어 오류용 마스크 불가 인터럽트. 구현이 정한 벡터로 M 모드에서 바로 뛰며 mepc·mcause를 덮어쓴다. 그래서 M 모드 처리기 도중에 오면 되돌아갈 수 없다.

Smrnmi (재개 가능한 NMI)

mnscratch 0x740, mnepc 0x741, mncause 0x742, mnstatus 0x744를 더해 NMI가 자기만의 칸에 상태를 적는다. mnret으로 돌아가며, mnstatus.NMIE가 0인 동안은 모든 인터럽트가 가려진다.

Smdbltrp (M 모드 이중 트랩)

mstatus.MDT 비트. M 모드로 트랩이 들어올 때 MDT가 0이면 1로 만들고, 이미 1이면 “예기치 않은 트랩”이다. MRET·SRET이 MDT를 0으로 되돌린다.

Ssdbltrp (S 모드 이중 트랩)

sstatus.SDT 비트(menvcfg.DTE로 켬). SDT=1인 채로 S로 트랩이 들어오면 M 모드에 원인 16(double trap)으로 보고하고, 원래 원인은 mtval2에 담는다.

위험 구간: 상태 저장 전 (MDT = 1) 처리 본체 (MDT = 0, 재진입 가능) 트랩 진입MDT ← 1 저장 끝소프트웨어가 MDT ← 0 MRETMDT ← 0 ⚡ 여기서 또 트랩! Smrnmi 있고 NMIE = 1RNMI 처리기로 (mnepc·mncause 기록) 그렇지 않으면치명적 오류 상태: 멈추고 플랫폼에 신호
그림 18-11. Smdbltrp의 이중 트랩 처리. MDT=1인 위험 구간에서 M 모드로 또 트랩이 오면, RNMI 처리기가 있으면 그쪽으로 보내고(규격상 RNMI 처리기는 mtval 정보를 받지 못하므로 필요하면 mnepc의 명령을 직접 해독한다), 없으면 pc를 포함한 어떤 상태도 바꾸지 않고 치명적 오류 상태로 들어가 플랫폼에 알린다.

핵심 정리

  1. 트랩은 예외(동기)나 인터럽트(비동기)로 처리기에 제어가 넘어가는 일이다. 특권 모드를 올리는 유일한 정상 경로이며, xRET이 그 반대 길이다.
  2. 트랩 진입 시 하드웨어는 xepc·xcause·xtval을 쓰고, xPIE ← xIE, xIE ← 0, xPP ← 이전 모드, 모드 ← x, pc ← xtvec을 수행한다. xRET은 그 반대로 꺼내고 xPP를 가장 낮은 모드로 바꾼다.
  3. mcause의 최상위 비트가 인터럽트 여부다. 예외 코드는 0–9, 11, 12, 13, 15, 16, 18, 19가 정의되어 있고, 인터럽트는 1·3·5·7·9·11·13이다.
  4. Vectored 모드에서 인터럽트는 BASE + 4 × 원인으로, 예외는 언제나 BASE로 간다.
  5. medeleg·mideleg는 S·U 모드에서 난 트랩을 S 모드로 바로 보낸다. M 모드에서 난 트랩은 위임과 상관없이 M이 받는다.
  6. 더 높은 모드로 갈 인터럽트는 지금 모드에서 항상 전역적으로 켜져 있고, 더 낮은 모드로 갈 인터럽트는 항상 꺼져 있다. 동시 발생 시 M 목적지가 먼저이고, M 안에서는 MEI·MSI·MTI·SEI·SSI·STI·LCOFI 순서다.
  7. MTIP는 mtime ≥ mtimecmp로, Sstc의 STIP는 time ≥ stimecmp로 켜진다. PLIC은 우선순위·임계값·claim/complete로 외부 인터럽트를 문맥에 나눠 주고, AIA는 APLIC과 IMSIC으로 MSI까지 다룬다.

확인 퀴즈

U 모드에서 주소 0x10148의 ecall이 S 모드로 위임되었다. 트랩 직후 sepc 값은?

ecall과 ebreak는 xepc에 그 명령 자신의 주소를 남긴다. 다음 명령으로 돌아가려면 처리기가 4를 더해야 한다.

RV32에서 mcause를 읽었더니 0x80000007이었다. 무슨 트랩인가?

최상위 비트가 1이므로 인터럽트이고, 원인 7은 MTI다. 최상위 비트가 0이었다면 7은 Store/AMO 접근 폴트다.

mtvec = 0x80000001(BASE 0x80000000, Vectored)일 때 머신 외부 인터럽트의 처리기 주소는?

MEI의 원인 코드는 11이다. BASE + 4 × 11 = BASE + 0x2c.

medeleg[2]=1(잘못된 명령 위임)인 상태에서 M 모드 코드가 잘못된 명령을 실행했다. 트랩은 어디로 가는가?

트랩은 더 높은 모드에서 더 낮은 모드로 내려가지 않는다. 위임은 S·U 모드에서 난 트랩에만 적용된다.

U 모드에서 실행 중, mstatus.MIE=0, mip.MTIP=1, mie.MTIE=1이다. 어떻게 되는가?

더 높은 모드로 갈 인터럽트는 지금 모드에서 항상 전역적으로 켜져 있다. MIE는 M 모드로 실행 중일 때만 의미가 있다.

S 모드로 트랩이 들어갈 때 sstatus의 SIE·SPIE는 어떻게 바뀌는가?

트랩 진입은 xPIE에 xIE를 밀어 넣고 xIE를 끈다. 마지막 보기는 SRET의 동작이다.

MEI와 MTI가 동시에 펜딩·활성이고 M 모드에서 MIE=1이다. 먼저 받는 것은?

M 목적지의 우선순위는 MEI, MSI, MTI, SEI, SSI, STI, LCOFI 순서다. 원인 번호의 크기와는 관계없다.

PLIC 문맥의 임계값이 3이고, 우선순위 3인 소스가 펜딩·활성 상태다. 이 문맥은?

PLIC은 우선순위가 임계값 이하인 인터럽트를 가린다. 다만 claim 자체는 임계값의 영향을 받지 않으므로 직접 claim하면 그 ID를 받을 수 있다.

더 읽을거리