3.1 Interrupt
3.1: Interrupt
레슨 1에서부터 이미 하드웨어와 통신하는 방법을 배웠습니다. 그러나 대부분의 통신 방식이 그렇게 간단하지는 않습니다. 보통 이러한 패턴은 비동기입니다: 어떤 명령어를 기기에 보내지만 기기는 즉각적으로 반응하지 않습니다. 대신에 작업이 끝났을 때 알려줍니다. 보통의 실행 흐름을 방해하고 프로세서가 “interrupt handler”를 실행하게 강제하기 때문에 이러한 비동기적인 알림을 “interrupt”라고 부릅니다.
운영체제 개발에 특히 유용한 기기가 있습니다.: system timer. 시스템 타이머는 사전에 정의된 빈도로 프로세서를 주기적으로 인터럽트를 발생하도록 설정할 수 있는 기기입니다. 타이머의 특정 응용은 프로세스 스케줄링에 사용됩니다. 스케줄러는 각 프로세스가 실행이 얼마나 됬는지 계산하고 다음 실행될 프로세스스를 선택하기 위해 이 정보를 사용해야 합니다. 이러한 계산은 타이머 인터럽트에 기초합니다.
다음 레슨에서 프로세스 스케줄링에 대해 다룰 것입니다. 지금은 시스템 타이머를 초기화하고 타이머 인터럽트 핸들러를 구현하는 것이 주된 일이 될 것입니다.
Interrupts vs exceptions
ARM.v8 아키텍처에서 인터럽트는 더 일반적인 개념(예외 exception)의 일부입니다. 4가지 타입의 예외가 있습니다.
- Synchronous exception 이 타입의 예외는 항상 현재 실행된 명령어에 의해 발생됩니다. 예를 들어
str
명령어를 존재하지 않는 메모리 위치에 데이터를 저장하기 위해 사용할 수 있습니다. 이 경우에 동기적 예외가 생성됩니다. 동기적 예외는 “software interrupt”를 발생하기 위해 사용될 수도 있습니다. 소프트웨어 인터럽트는svc
명령어에 의해서 의도적으로 생성된 동기적 예외입니다. 시스템 콜을 구현하기 위해서 레슨 5에서 이 기술을 사용할 것입니다. - IRQ (Interrupt Request) 이것은 일반적인 인터럽트입니다. 이것은 항상 비동기적입니다. 이 말은 현재 실행된 명령어와 연관이 없다는 뜻입니다. 동기적 예외와 상반되게 프로세서 자체에서 항상 생성되는 것이 아니라 외부 하드웨어에 의해서 생성됩니다.
- FIQ (Fast Interrupt Request) 이 유형의 인터럽트는 “fast interrupt”라 불리며 예외를 우선순위화의 목적으로만 존재합니다. 어떤 인터럽트를 “normal” 그리고 “fast”로 설정하는 것이 가능합니다. fast 인터럽트는 우선 시그날 되고 각각의 익셉션 핸들러에 의해 처리 될 것입니다. 리눅스는 fast 인터럽트를 사용하지 않고 우리도 사용하지 않을 것입니다.
- SError (System Error)
IRQ
와FIQ
처럼SError
예외는 비동적이고 외부 하드웨어에 의해 발생합니다.IRQ
와FIQ
와 다르게SError
는 항상 어떤 오류 조건을 나타냅니다. 여기서SError
가 발생하는 경우를 설명하는 예시를 찾으실 수 있습니다.
Exception vectors
각각의 예외 유형은 자체의 핸들러가 필요합니다. 또한 분리된 핸들러는 각 예외가 발생한 다른 실행 상태에 따라 정의되어야 합니다. 익셉션 핸들러 관점에서 흥미로운 4 개의 실행 상태가 있습니다. 만야 EL1에서 동작중이라면 실행 상태는 아래와 같이 정의될 수 있습니다:
- EL1t 스택 포인터가 EL0와 공유중 일 때 EL1에서 예외가 발생합니다.
SPSel
레지스터가0
값을 가질 때 이런 일이 발생합니다. - EL1h 전용 스택 포인터가 EL1에 할당될 때 EL1에서 예외가 발생합니다.
SPSel
이1
값을 가지는 것을 의미하고 이것은 현재 사용중인 모드입니다. - EL0_64 64 비트 모드에서 실행중인 EL0에서 예외가 발생합니다.
- EL0_32 32 비트 모드에서 실행중인 EL0에서 예외가 발생합니다.
총합적으로 16개 익셉션 핸들러를 정의해야합니다.(4개 예외 레벨 곱하기 4개 실행 상태). 모든 익셉션 핸들러의 주소를 가지는 특별한 구조체를 exception vector table 또는 vector table이라 부릅니다. 벡터 테이블의 구조는 AArch64-Reference-Manual 의 1876 페이지에서 Table D1-7 Vector offsets from vector table base address
에서 정의되 있습니다. 벡터 테이블을 익셉션 벡터들의 배열로 생각할 수 있습니다. 각 익셉션 벡터(또는 핸들러)는 특정 예외를 처리하는 역할을 하는 명령어들의 연속적인 순서입니다. AArch64-Reference-Manual
에서 Table D1-7
에 따르면 각 익셉션 벡터는 최대 0x80
바이트를 차지할 수 있습니다. 이것은 크지 않지만 어떤 것도 익셉션 벡터에서 다른 메모리 위치로 점프하는 것을 막지 않습니다.
예시를 통해서 더 명확해질 것이라 생각하여 이제 RPi-OS에서 익셉션 벡터가 어떻게 구현됬는지 확인할 시간입니다. 익셉션 처리와 연관된 모든 것은 entry.S에 정의되 있습니다. 그리고 지금 바로 확인을 시작하겠습니다.
첫 번째로 유용한 매크로 ventry라 불리는 매크로 입니다 그리고 벡터 테이블에서 엔트리를 생성할 때 사용됩니다.
.macro ventry label
.align 7
b \label
.endm
이 정의에서 유추할 수 있듯이 익셉션 벡터안에 예외를 처리하지 않고 대신에 label
변수로 매크로에 전달된 라벨로 점프합니다. 모든 익셉션 벡터는 0x80
오프셋에 위치해야 하기 때문에 .align 7
명령어가 필요합니다.
벡터 테이블은 여기 정의되 있고 16개의 ventry
정의로 구성되 있습니다. 이제는 EL1h
에서 IRQ
를 처리하는 것에만 관심이 이씨만 모든 16개 핸들러를 정의해야합니다. 하드웨어 충족조건 때문이 아니라 무언가 잘못 되었을 때 의미 있는 에러 메시지를 보고 싶기 때문입니다. 정상 흐름에서 실행되면 안되는 모든 핸들러들은 invalid
접미어를 가지며 handle_invalid_entry 매크로를 사용합니다. 이 매크로가 어떻게 정의되는 지 살펴봅시다.
.macro handle_invalid_entry type
kernel_entry
mov x0, #\type
mrs x1, esr_el1
mrs x2, elr_el1
bl show_invalid_entry_message
b err_hang
.endm
첫 번째 줄에서 kernel_entry
라는 다른 매크로가 사용되는 것을 보실 수 있습니다. 짧게 다룰려고 합니다. 그 후 show_invalid_entry_message을 호출하고 이를 위한 3 개의 매개변수를 준비합니다. 첫 번째 매개변수는 이 값들 중 하나를 취할 수 잇는 예외 타입입니다. 어떤 익셉션 핸들러가 실행될지 알려줍니다. 두 번째 매개변수는 가장 중요한 것으로 Exception Syndrome Register를 뜻하는 ESR
입니다. 이 매개변수는 esr_el1
레지스터에서 가져오고 이는 AArch64-Reference-Manual
의 페이지 2431에 설명되 있습니다. 세 번째 매개변수는 동기적 예외의 경우 중요합니다. 이 값은 이미 익숙한 elr_el1
레지스터에서 가져옵니다. elr_el1
은은은 예외가 생성됬을 때 실행된 명령어의 주소를 가집니다. 동기적 예외를 위해 이 것 또한 예외를 발생하는 명령어 입니다. 이러한 정보를 모두 스크린에 출력하는 show_invalid_entry_message
함수 이후에 할 수 있는 일이 없기 때문에 프로세서를 무한 루프 상태로 만듭니다.
Saving register state
익셉션 핸들러가 끝난 후 모든 범용 레지스터가 예외 발생전 값가 동일한 값을 가지게 합니다. 만약 이러한 기능을 구현하지 않으면 현재 실행 중인 코드와 연관 없는 인터럽트가 예측할 수 없게 코드의 수행에 영향을 끼칠 수 있습니다. 예외가 발생하고 하는 첫 번째 일이 프로세서 상태를 저장하는 일인 이유가 이 것입니다. kernel_entry 매크로에서 이러한 일이 수행됩니다. 이 매크로는 매우 간단합니다. 단순히 x0 - x30
레지스터를 스택에 저장합니다. 익셉션 핸들러가 끝난후 호출 되는 kernel_exit 매크로 또한 있습니다. kernel_exit
은 x0 - x30
레지스터의 값을 다시 복사하여 프로세서 상태를 복원합니다. 일반 실행 흐름으로 돌려주는 eret
명령어를 실행시키기도 합니다. 어쨋든 범용 레지스터는 익셉션 핸들러가 실행되기 전에 저장되어야 하는 유일한 것은 아니지만 우리의 간단한 현재 커널에서는 충분합니다. 나중에 레슨에서는 kenrel_entry
와 kernel_exit
매크로에 더 많은 기능을 추가할 것입니다.
Setting the vector table
이제 벡터 테이블을 준비했지만 프로세서는 이게 어디 위치한지 모르기에 사용할 수 없습니다. 익셉션 핸들러가 작동하기 위해서 vbar_el1
(Vector Base Address Register)을 벡터 테이블 주소로 설정해야합니다. 이것은 여기서 수행됩니다.
.globl irq_vector_init
irq_vector_init:
adr x0, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x0 // vector table address
ret
Masking/unmasking interrupts
또 다른 해야 할 일은 모든 타입의 인터럽트를 마스크 하지 않는 것(unmask)입니다. 인터럽트를 “unmask” 한다는 것이 어떤 의미인지 설명하겠습니다. 가끔 특정 코드가 비동기적 인터럽트에 의해 방해되지 않아야 하는 것이 필요할 수 있습니다. 예를 들어 만약 kernel_entry
매크로의 중간에 인터럽트가 발생했다고 상상해 봅시다. 이러한 경우에 프로세서 상태는 덮어씌어지고 잃어버릴 것입니다. 이 것이 익셉션 핸들러가 실행 될 때 마다 프로세서가 자동으로 모든 타입의 인터럽트를 비활성화하는 이유입니다. 이러한 것을 “masking”이라 불리고 필요하다면 직접 수행할 수 있습니다.
많은 사람들이 실수로 인터럽트가 무조건 익셉션 핸들러의 전체 기간동안 마스크 되어야 한다고 생각합니다. 이것은 사실이 아닙니다. 프로세서 상태를 저장한 후 인터럽트를 unmask하는 것은 완전히 적절하기에 내장된 인터럽트(nested interrupt)를 갖는 것 또한 적절합니다. 지금은 이것을 하지 않을 예정이지만 계속 유의해야 하는 중요한 정보입니다.
아래 두 함수는 인터럽트를 masking, unmasking 하는 역할을 담당합니다.
.globl enable_irq
enable_irq:
msr daifclr, #2
ret
.globl disable_irq
disable_irq:
msr daifset, #2
ret
ARM 프로세서 상태는 다른 유형의 인터럽트에 마스크 상태를 가지기 위한 4 비트를 가집니다. 이러한 비트는 아래와 같이 정의됩니다
- D 디버그 예외를 마스크 합니다. 동기적 예외의 특별한 경우입니다. 명백한 이유로 모든 동기적 예외를 마스크 하는 것은 불가능 하지만 디버그 예외를 마스크할 수 있는 분리된 플래그를 갖는 것이 편합니다.
- A
SError
를 마스크합니다.SError
가 가끔 비동기적 중지(abort)라 불리기 때문에A
로 불립니다. - I
IRQs
를 마스크 합니다. - F
FIQs
를 마스크 합니다.
이제 인터럽트 마스크 상태를 바꾸는 역할을 하는 레지스터가 왜 daifclr
과 daifset
으로 불리는지 추측할 수 있을 것입니다. 이러한 레지스터들은 프로세서 상태에 마스크 상태 비트를 설정하거나 클리어합니다.
마지막으로 궁금해할 것은 왜 각 함수에서 2
라는 상수 값을 사용하는 가 입니다. 이것은 오로지 두 번째 비트 (I
) 비트만을 설정하고 클리어하고 싶기 때문입니다.
Configuring interrupt controller
기기는 보통 프로세서를 직접적으로 인터럽트 하지 않습니다: 대신에 인터럽트 컨트롤러(interrupt controller)에 의존합니다. 인터럽트 컨트롤러는 하드웨어에서 전송된 인터럽트를 활성화/비활성화 하기 위해서 사용될 수 있습니다. 어떤 기기가 인터럽트를 발생하는 지 식별할 때 사용할 수 도 있습니다. 라즈베리 파이는 BCM2837 ARM Peripherals manual의 109 쪽에 설명된 자체 인터럽트 컨트롤러를 가지고 있습니다.
라즈베리 파이 인터럽트 컨트롤러는 모든 인터럽트 유형에 활성화/비활성화 상태를 유지하는 3 개의 레지스터를 가집니다. 이제 타이머 인터럽트에만 관심을 가지고 이러한 인터럽트는 ENABLE_IRQS_1 레지스터를 사용해 활성화 될수 있습니다. 이 레지스터는 BCM2837 ARM Peripherals manual
의 116 쪽에 설명되 있습니다. 문서에 따르면 인터럽트는 2 뱅크(bank)로 나눠집니다. 첫 번째 뱅크는 0 - 31
인터럽트들로 구성되 있고 각 인터럽트들은 ENABLE_IRQS_1
레지스터의 다른 비트들을 설정하여 활성화/비활성화 될 수 있습니다. 또한 마지막 32 인터럽트들을 위한 레지스터(ENABLE_IRQS_2
)와 몇 보통 ARM 로컬 인터럽트와 함께 일상 인터럽트를 제어하는 레지스터(ENABLE_BASIC_IRQS
, 이 번 레슨의 다음 챕터에서 ARM 로컬 인터럽트를 다룰 것입니다)도 있습니다. 하지만 Peripherals manual은 많은 실수를 가지고 그 실수들 중 하나는 우리의 논의와 직접적으로 연관이 있습니다. Peripheral interrupt table(메뉴얼 113 페이지에 설명되있습니다)은 시스템 타이머로 부터 0 - 3
줄에서 4 개의 인터럽트를 포함해야 합니다. 리눅스 소스코드를 역공학하고 다른 소스들을 읽어서 타이머 인터럽트 0과 2는 예약되있고 GPU에 의해서 사용되고 인터럽트 1과 3은 다른 목적으로 사용될 수 있음을 알아냈습니다. 따라서 시스템 타이머 IRQ 1을 활성화 하는 함수가 여기 있습니다.
void enable_interrupt_controller()
{
put32(ENABLE_IRQS_1, SYSTEM_TIMER_IRQ_1);
}
Generic IRQ handler
이전 논의에서 모든 IRQs
를 처리하는 역할을 하는 단일 익셉션 핸들러를 가진다는 것을 기억하셔야 합니다. 이 핸들러는 여기 정의 되있습니다.
void handle_irq(void)
{
unsigned int irq = get32(IRQ_PENDING_1);
switch (irq) {
case (SYSTEM_TIMER_IRQ_1):
handle_timer_irq();
break;
default:
printf("Unknown pending irq: %x\r\n", irq);
}
}
핸들러에서 어떤 기기가 인터럽트를 생성하는 역할을 하는 지 찾을 방법이 필요합니다. 인터럽트 컨트롤러는 이 일을 도와줄 수 있습니다. 컨트롤러는 0 - 31
인터럽트를 위한 인터럽트 상태를 가지는 IRQ_PENDING_1
레지스터를 가집니다. 이 레지스터를 사용하여 현재 인터럽트가 타이머에 의해서 발생했는지 다른 기기에 의해서 발생했는지 확인할 수 있고 기기 특정된 인터럽트 핸들러를 호출할 수 있습니다. 복수의 인터럽트는 동시에 지연될 수 있음을 유의하세요. 각각의 기기 특정 인터럽트 핸들러가 인터럽트 처리를 끝내고 IRQ_PENDING_
의 인터럽트 보류 비트가 지워진 후에만 승인해야 하는 이유입니다. 같은 이유로 이미 제품화된 운영체제를 위해 인터럽트 핸들러안에 스위치 구조를 루프로 감싸려는 것입니다, 이런 방식으로 하나의 단일 핸들러 실행으로 복수의 인터럽트를 처리할 수 있습니다.
Timer initialization
라즈베리 파이 시스템 타이머는 매우 간단한 기기입니다. 각 클락 틱 이후에 값을 1 증가하는 카운터를 가집니다. 인터럽트 컨트롤러에 연결된 4 개의 인터럽트 라인을 가지고(따라서 4개의 다른 인터럽트를 생성할 수 있습니다) 4개의 알맞는 비교 레지스터를 가집니다. 카운터의 값이 비교 레지스터의 값중 하나와 일치할 떄 인터럽트가 발생됩니다. 시스템 타이머 인터럽트를 사용하기 전에 비교 레지스터의 하나를 0이 아닌 값으로 초기화 해야하는 이유입니다. 초기화 값이 클 수록 인터럽트가 더 뒤에 발생합니다. 이는 timer_init 함수에서 수행됩니다.
const unsigned int interval = 200000;
unsigned int curVal = 0;
void timer_init ( void )
{
curVal = get32(TIMER_CLO);
curVal += interval;
put32(TIMER_C1, curVal);
}
첫 번째 줄은 카운터 값을 읽고 두 번째 줄은 이 것을 증가시키고 세 번째 줄은 인터럽트 숫자 1을 위한 비교 레지스터의 값을 설정합니다. interval
값을 수정하여서 첫 번째 타이머 인터럽트가 얼마나 일찍 발생할지 조정할 수 있습니다.
Handing timer interrupts
마지막으로 타이머 인터럽트 핸들러로 갑니다. 이것은 매우 간단합니다.
void handle_timer_irq( void )
{
curVal += interval;
put32(TIMER_C1, curVal);
put32(TIMER_CS, TIMER_CS_M1);
printf("Timer iterrupt received\n\r");
}
여기서 우선 비교 레지스터를 갱신하여 다음 인터럽트가 같은 시간 간격에 발생할 수 있게 합니다. 다음으로 TIMER_CS
레지스터에 1을 적어서 인터럽트를 인지할 수 있게 합니다. 문서에서는 TIMER_CS
는 “Timer Contorl/Status” 레지스터로 불립니다. 이 레지스터의 [0:3] 비트들은 4개의 가능한 인터럽트 라인의 하나에서 오는 인터럽트를 인지하기 위해 사용될 수 있습니다.
Conclusion
마지막으로 살펴보고 싶은 것은 모든 이전에 논의된 기능이 융합되는 kernel_main 함수 일 것입니다. 예시를 컴파일하고 실행시킨 후에 인터럽트가 발생한 후 “Timer interrupt received” 메시지가 출력되어야 합니다. 스스로 해보시고 코드를 자세히 살펴보는 것을 잊지 말아주세요
댓글남기기