Sqix

CS - OS - 03. 인터럽트 본문

CS/OS

CS - OS - 03. 인터럽트

Sqix_ow 2021. 11. 18. 02:36

인터럽트

인터럽트란?

  • 소프트웨어나 하드웨어로 인해 발생하는, CPU의 처리가 필요한 이벤트를 CPU에 알려서 처리하도록 하는 기술
  • 일종의 이벤트로, 발생하는 이벤트에 맞게 운영체제에서 처리
  • 인터럽트를 실행하기 위한 opcode는 int
  • 인터럽트는 컴퓨터 부팅 시 미리 정의되어 이벤트와 실행코드 주소가 IDT에 기록되어 있음.
  • 인터럽트는 CPU에 직접 전달되지 않는다.
    • PIC(Programmable Interrupt Controller)에 의해서 인터럽트 요청을 순차적으로 처리
    • 최근에는 Advanced PIC를 활용하여 Local / I/O APIC로 나누어 활용
    • Local APIC : Timer, 열 센서, 기기 직접 연결 I/O 장치에서 발생된 Interrupt 핸들링
    • I/O APIC : CPU 코어들 사이의 외부 Interrupt를 분산하기 위해 사용

인터럽트 사용 예시

  • 선점형 스케줄러 구현 시
    • 프로세스 Running 중 이를 중단시키고 다른 프로세스로 교체하기 위해 running -> ready 상태로 만들기 위해 인터럽트를 사용
  • I/O Device 혹은 event와의 상호작용 시
    • waiting 상태에서 ready 상태로 가기 위해서는 I/O 및 event가 종료됨을 알리기 위해 인터럽트 사용
  • 예외 상황 핸들링
    • CPU가 프로그램을 실행하던 도중 이상 동작이 발생하는 경우 CPU가 해당 처리를 할 수 있도록 CPU에 고지함

인터럽트의 종류

  • 내부 인터럽트(트랩 / 소프트웨어 인터럽트)
    • 주로 소프트웨어 내부에서 잘못된 명령 혹은 잘못된 데이터 사용 시 발생
  • 외부 인터럽트(인터럽트 / 하드웨어 인터럽트)
    • 주로 하드웨어에서 발생하는 이벤트

주요 인터럽트 우선순위

  • 하드웨어 리셋 혹은 장치 검사
    • RESET / Machine Check
  • 태스크 스위치의 T 플래그 셋(1)
    • TSS의 T 플래그가 1로 세팅
  • 외부 하드웨어 간섭
    • FLUSH / STOPCLK / SMI / INIT 등
  • 이전 명령어에서 소프트웨어적인 인터럽트 발생 시
    • Breakpoint 혹은 Debug 관련 예외사항
  • 요청 거부를 할 수 없는 인터럽트(Nonmaskable)
  • 요청 거부가 가능한 하드웨어 인터럽트(Maskable Hardware Interrupts)
  • Code Breakpoint에서의 폴트 발생
  • 다음 명령어 페치 과정에서의 폴트
    • Code Segment Limit 오류
    • Code Page 폴트 발생
  • 다음 명령어의 디코딩 과정에서의 폴트
    • 명령어의 길이가 15바이트보다 긴 경우
    • 유효하지 않은 Opcode
    • 코프로세서의 사용이 불가능한 경우
  • 명령어 실행 시 발생하는 폴트
    • 오버플로우
    • 바운드 에러
    • 유효하지 않은 TSS
    • 존재하지 않는 세그먼트
    • 스택 폴트
    • 보호 기법에 의한 폴트
    • 데이터 페이지 폴트
    • 정렬되지 않은 피연산자 감지
    • X87 부동소수점 예외
    • SIMD 부동소수점 예외
    • 가상화 예외

인터럽트의 발생 및 처리


참고 : linux-insides

인터럽트 실행 흐름

  • 커널은 현재 프로세스의 실행을 중지(작업에 대한 선점)
  • 인터럽트 핸들러와 인터럽트 전송 컨트롤러를 찾아서 핸들러 실행
  • 인터럽트 핸들러의 동작이 완료된 이후 중지된 프로세스 실행 재개

IDT (Interrupt Descriptor Table)

IDT 구조

  • Interrupt들과 exception handler의 엔트리 포인트를 저장하는 구조체로, gate 형태를 이루고 있음
  • 서로 다른 권한 레벨의 Interrupt Handler 혹은 프로시저를 호출할 수 있도록 해주는 gateway 역할을 하는 구조체이다.
  • IDT에 들어갈 수 있는 게이트의 종류는 Task Gate / Trap Gate / Interrupt Gate로 총 3가지가 있다.
  • IDT는 최대 256개의 gate를 포함할 수 있고, 이 내부에는 다음과 같은 정보들이 포함되어 있다.
    • Handler Offset : 인터럽트 혹은 Exception Handler의 엔트리 포인트
    • Segment Selector : 인터럽트 혹은 Exception Handler 수행 시 코드 세그먼트를 교체해 동작에 필요한 권한으로 상승시킴
    • Interrupt Stack Table : 동작 수행 시 생성되는 별도의 스택 공간. 스택 공간 부족 및 데이터 덮어쓰기 방지를 위한 장치
    • Type : 어떤 IDT 게이트인지 설정해주는 비트로, 0110 - 인터럽트 게이트, 0111 - 트랩 게이트이다[1]
    • Descriptor Privilege Level(DPL) : 디스크립터 사용에 필요한 권한으로, 0~3(Ring)의 범위를 가지며 접근 권한을 제어
    • P : 현재 사용하는 디스크립터가 유효한지(1) 혹은 유효하지 않은지(0)를 나타낸다.


[1]: 인터럽트 게이트로 Type이 세팅되면 핸들러 수행 도중 방해받지 않으며, 트랩 게이트로 설정 시 다른 인터럽트가 발생할 수 있다.


IST는 x86_64에서 새로 도입된 스택 스위칭 메커니즘이다. 인터럽트 혹은 exception 상황은 코드 수행 도중 발생하는 것이므로, 이를 처리하기 위한 핸들러가 동작할 때의 스택 상태를 예측할 수 없다. 기존 스택이 다 할당된 상태에서 코드가 수행되면 다른 영역을 침범할 수 있으므로 이를 해결하기 위한 방식이 IST다. 다만 모든 인터럽트의 핸들링에 사용되는 것은 아니고, 일부 인터럽트에서는 레거시 스택 스위칭 방식을 사용한다. IST 스택 전환 메커니즘을 위해서 TSS에서는 최대 7개의 IST 포인터를 제공하고, 이는 IDT의 인터럽트 게이트에 의해서 참조된다.


IDT는 다음 구조체 자료형으로 선언된다.

extern gate_desc idt_table[];

그리고 gate_desc는 다음과 같이 선언되어 있다.

typedef struct desc_struct gate_desc;

desc_struct 구조체의 구조는 다음과 같다.

struct desc_struct {
    union {
        struct {
            unsigned int a;
            unsigned int b;
        };
        struct {
            u16 limit0;
            u16 base0;
            unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
            unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
        };
    };
} __attribute__((packed));

활성화된 각각의 쓰레드에는 x86_64 아키텍처를 위한 큰 사이즈의 스택이 있다. 스택의 크기는 다음과 같이 정의된다.

#define PAGE_SHIFT    12
#define PAGE_SIZE    (_AC(1,UL)) << PAGE_SHIFT)

#define THREAD_SIZE_ORDER    (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE            (PAGE_SIZE << THREAD_SIZE_ORDER)

PAGE_SIZE는 4096바이트이고, THREAD_SIZE_ORDERKASAN_SIZE_ORDER에 의해서 결정된다. KASAN_STACK은 커널 설정의 CONFIG_KASAN 파라미터에 의해서 결정되고, 다음과 같이 선언되어 있다.

#ifdef CONFIG_KASAN
    #define KASAN_STACK_ORDER 1
#else
    #define KASAN_STACK_ORDER 0
#endif

KASAN은 런타임 메모리 디버거이다. THREAD_SIZE는 만약 CONFIG_KASAN 옵션이 비활성화된 경우 16384, 그렇지 않은 경우 32768 바이트다.


쓰레드가 User-space에 존재하는 동안, 커널 스택은 최하단의 Thread_info를 제외하고는 비어 있는 상태가 된다. 활성화 혹은 좀비 상태인 쓰레드만 자체적으로 스택을 가지고 있는 것은 아니다. 사용 가능한 CPU와 연결된 특수한 스택들도 있다. 이러한 스택은 커널이 CPU를 동작시키는 경우 활성화된다. 만약 User-space에서 CPU를 실행시킨다면 이 스택 안에는 유효한 정보가 담겨있지 않는다.


각각의 CPU에는 특수한 per-cpu 스택들도 있다. 첫번째는 인터럽트 스택으로, 외부 하드웨어 인터럽트에 사용되며 크기는 다음과 같이 결정되거나 16384 bytes가 된다.

#define IRQ_STACK_ORDER (2 + KASAN_STACK_ORDER)
#define IRQ_STACK_SIZE (PAGE_SIZE << IRQ_STACK_ORDER)

per-cpu 인터럽트 스택은 irq_stack_union 유니언으로 대표되는데, 이는 x86_64 CPU 기준으로 다음과 같이 작성되어 있다.

union irq_stack_union {
    char irq_stack[IRQ_STACK_SIZE];

    struct {
        char gs_base[40];
        unsigned long stack_canary;
    };
};

irq_stack 필드는 16KB 길이의 배열이다.

  • gs_base : gs 레지스터는 항상 irqstack의 최하단을 가리킨다. x86_64에서 gs 레지스터는 per-cpu 영역과 스택 canary(보호기법)에 의해서 공유된다. 모든 per-cpu의 심볼은 0-base이고, gs는 per-cpu 영역의 최하단을 가리킨다. 롱 모드에서는 세그먼트 메모리 모델이 사용되지 않지만, 우리는 MSR를 사용해서 fs와 gs의 base-address를 세팅할 수 있다.


부팅 프로세스에서의 코드를 잠시 살펴보면

movl    $MSR_GS_BASE,%ecx
movl    initial_gs(%rip),%eax
movl    initial_gs+4(%rip),%edx
wrmsr

위 코드에서 gs 레지스터를 세팅하게 되는데, 여기서 initial_gsirq_stack_union을 가리킨다.

GLOBAL(initial_gs).quad    INIT_PER_CPU_VAR(irq_stack_union)

irq_stack_unionper-cpu 영역의 첫 데이터이고, 우리는 이를 System.map에서 확인할 수 있다. 코드에서의 정의부는 다음과 같다.

DECLARE_PER_CPU_FIRST(union irq_stack_union, irq_stack_union) __visible;

irq_stack_union의 초기화는 다음과 같이 이루어진다.

DECLARE_PER_CPU(char *, irq_stack_ptr);
DECLARE_PER_CPU(unsigned int, irq_count);

먼저 irq_stack_ptr이다. 변수 명으로 미루어보아 이는 스택의 최상위 포인터이다. 그 다음은 irq_count인데, 이는 CPU가 이미 인터럽트 스택에 있는지 혹은 그렇지 아니한지를 검사하는 것이다. arch/x86/kernel/setup_percpu.c에 위치한 setup_per_cpu_areas 함수 안에 irq_stack_ptr의 초기화 과정이 위치하여 있다.

void __init setup_per_cpu_areas(void)
{
...
...
#ifdef CONFIG_X86_64
for_each_possible_cpu(cpu) {
    ...
    ...
    ...
    per_cpu(irq_stack_ptr, cpu) =
            per_cpu(irq_stack_union.irq_stack, cpu) +
            IRQ_STACK_SIZE - 64;
    ...
    ...
    ...
#endif
...
...
}

여기서 모든 CPU들을 하나하나 살펴본 뒤, irq_stack_ptr을 설정한다. 이는 인터럽트 스택의 최상단에서 64를 뺀 값과 동일하다.

void load_percpu_segment(int cpu)
{
        ...
        ...
        ...
        loadsegment(gs, 0);
        wrmsrl(MSR_GS_BASE, (unsigned long)per_cpu(irq_stack_union.gs_base, cpu));
}
movl    $MSR_GS_BASE,%ecx
movl    initial_gs(%rip),%eax
movl    initial_gs+4(%rip),%edx
wrmsr

GLOBAL(initial_gs)
.quad    INIT_PER_CPU_VAR(irq_stack_union)

gs레지스터는 인터럽트 스택의 최하단을 가리킨다

위 코드들에서 우리는 ecx 레지스터에 의해서 edx:eax의 데이터를 MSR에 로드하는 것을 볼 수 있다. 위의 경우에는 MSR은 gs레지스터가 가리키는 메모리 세그먼트의 base-address를 가리키는 MSR_GS_BASE가 된다. 위에서 언급하였다시피 x86_64에는 IST 기능이 있어 non-maskable 인터럽트, double-fault 등의 특수한 이벤트에 대해서는 새로운 스택을 사용할 수 있다. 대표적으로 다음과 같다.

  • DOUBLEFAULT STACK
  • NMI_STACK
  • DEBUG_STACK
  • MCE_STACK

혹은

#define DOUBLEFAULT_STACK 1
#define NMI_STACK 2
#define DEBUG_STACK 3
#define MCE_STACK 4

이다.

IST로 인해 새로운 스택으로 스위칭되는 interrupt gate descriptorset_intr_gate_ist 함수에 의해 초기화된다.

set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
...
set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);

여기서 &nmi&double_fault는 인터럽트 핸들러의 각 항목의 주소를 나타낸다.

asmlinkage void nmi(void);
asmlinkage void double_fault(void);

만약 인터럽트 및 예외가 발생하면, ss selector는 NULL로 설정되고, ss selector의 rpl필드는 새 cpl로 설정된다. 이전 ss, rsp, 레지스터 플래그, cs, rip은 새로운 스택에 push된다. 64비트 모드에서는 인터럽트의 스택 프레임을 푸쉬하는 사이즈가 8바이트로 고정되어 있다.

+---------------+
|               |
|      SS       | 40
|      RSP      | 32
|     RFLAGS    | 24
|      CS       | 16
|      RIP      | 8
|   Error code  | 0
|               |
+---------------+

게이트의 IST 필드가 0이 아니라면, 우리는 rsp에서부터 IST를 읽어온다. Interrupt 벡터 번호가 그와 관련된 에러를 포함한다면 에러 코드를 stack에 푸시한다. 만약 에러 코드가 포함되어 있지 않다면 스택의 일관성을 유지하기 위해 더미 에러 코드를 스택에 푸시한다.


그 다음, Gate Descriptor에서 Segment-Selector의 필드를 CS 레지스터에 로드하고 GDT의 L 비트(21 점검 비트)를 통해 해당 코드 세그먼트가 64비트모드 세그먼트인지 확인한다. 이후 Gate Descriptor에서 인터럽트 핸들러의 EP가 될 Offset Field를 로드한다. 이로서 인터럽트 핸들러가 실행되고, 실행을 마칠 때 iret 명령어를 통해 제어권을 반환한다.

Comments