Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

16192
888
89789


[리눅스커널][인터럽트] 인터럽트 소개 5장. 인터럽트 핸들링

인터럽트 소개

인터럽트란 단어는 다양한 의미로 해석할 수 있습니다. 일상생활에서 소프트웨어 개발까지 폭 넓게 쓰는 용어입니다. 이번 절에서는 인터럽트의 의미를 소개하고 인터럽트 관련 용어를 설명합니다.

5.1.1 인터럽트란

 인터럽트는 일반적인 상황에서 갑자기 발생하는 비동기적인 통지나 이벤트입니다. 인터럽트를 일반 상황부터 임베디드 리눅스에서 어떻게 해석하는지 살펴보겠습니다.

일상 생황에서 인터럽트란

인터럽트란 단어가 여러분들은 생소하신가요? 낯설게 들리는 분도 있고 귀에 익은 분도 있을 것입니다. 평소 인터럽트를 뭐라고 할까요? 일상생활에서 인터럽트는 보통 갑자기 생긴 일이나 하던 일을 멈춘다는 의미입니다. 보통 하던 일을 멈추게 하는 무엇인가 갑자기 발생한 상황을 뜻합니다. 예를 들면 책을 읽다가 전화가 와서 읽던 책을 덮어 놓고 전화를 받는 상황입니다. 

하드웨어 관점에서 인터럽트란

임베디드 시스템 관점으로 인터럽트는 무엇을 의미할까요? 우선 하드웨어 관점으로는 하드웨어 변화를 감지해서 외부 입력으로 전달되는 전기 신호입니다. 예를 들면 키보드를 손으로 입력하면 하드웨어적으로 키보드 하드웨어의 변화를 감지하고 신호를 유발합니다. 보통 하드웨어 개발자들은 종종 오실로스코프란 장비로 인터럽트가 제대로 올라오는지 측정합니다. 인터럽트 신호를 측정하면 아래와 같은 파형을 볼 수 있습니다. 
 
[그림 5.1] 인터럽트 파형 예시

참고로 인터럽트 신호는 아래 박스와 같이 인터럽트를 식별하는 구간에 일정하게 5V(Voltage) 유지하거나 0V에서 5V로 바뀌는 두 가지 종류로 분류합니다.

CPU 입장에서 인터럽트란

이제 소프트웨어 관점으로 인터럽트를 어떻게 처리하는지 생각해 봅시다. 인터럽트가 발생하면 프로세스는 하던 일을 멈추고 이미 정해진 코드를 실행해서 하드웨어 변화에 대해 처리하는 방식으로 동작합니다. 여기서 “이미 정해진 코드 흐름”은 인터럽트 서비스 루틴(Interrupt Service Routine)이라고 부르기도 합니다.

CPU(ARM) 관점으로 인터럽트는 어떻게 처리할까요? 인터럽트는 x86, ARM CPU별로 처리하는 방식이 다릅니다. 라즈베리파이는 ARMv7 버전을 탑재했으니 해당 ARMv7 CPU에서 인터럽트를 처리하는 과정을 알면 됩니다. ARMv7 프로세서에서 인터럽트는 익셉션(Exception)의 한 종류로 처리하니 익셉션 대해 알 필요가 있습니다. 
ARM 프로세서는 외부 하드웨어 입력이나 오류에 대한 사건이 발생하면 익셉션 모드로 진입합니다. 이 때 익셉션 별로 이미 정해 놓은 주소로 브랜치 합니다.  조금 어려운 개념인데 순간 이동과 비슷한 개념으로 생각해도 좋습니다. 

이미 정해진 주소로 브랜치를 하는 동작은 조금만 생각해보면 그리 낯설지는 않습니다. 어떤 코드에서 함수를 호출할 때 어셈블리 코드로 분석하면 이와 유사한 동작을 합니다.

한 가지 예를 들겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
01 asmlinkage __visible void __sched schedule(void)
02 {
...
03 do {
04 preempt_disable();
05 __schedule(false);

05 번째 줄 코드와 같이 __schedule(false); 함수를 호출할 때 어셈블리 코드 관점에서 어떤 동작을 할까요? ARM 코어 프로그램 카운터를 __schedule() 주소로 바꿉니다. 현재 실행 중인 레지스터 세트를 스택에 푸시합니다.

마찬가지로 ARM 이 익셉션 모드를 감지하면 익셉션 모드 별로 정해진 주소로 ARM 코어 프로그램 카운터를 바꿉니다. 이후 실행 중인 코드의 레지스터 세트를 스택에 푸시합니다.

인터럽트나 소프트웨어적인 심각한 오류가 발생하면 ARM 프로세스가 특정 주소를 실행합니다.
각 익셉션의 종류에 따라 이미 정해진 주소의 코드를 실행합니다. 이미 정해진 주소 코드를 익셉션 벡터(Exception Vector)라 하며 ARM 프로세서는 인터럽트를 익셉션 벡터 중 하나의 모드로 처리합니다. 이 동작은 5.3절에서 상세히 다룹니다. 

리눅스 커널이 인터럽트가 발생하면 인터럽터를 어떻게 핸들링하는지 파악하면서 염두해야 할 중요한 부분이 있습니다. 이를 한 문장으로 정리해보겠습니다.
"우리가 보고 있고 있거나 실행하는 어떤 커널 코드도 인터럽트가 발생하면 실행이 멈춰서 인터럽트 벡터로 실행 흐름을 점프할 수 있다."

이 점을 제대로 이해하면 더 안정된 코드를 작성할 수 있습니다.

인터럽트가 발생하면 실행 중인 코드를 멈추고 익셉션 벡터로 이동한다는 사실은 코드만 봐서 이해하기는 어렵습니다. 그러면 어떻게 해야 할까요? 이를 위해 라즈베리파이와 같은 리눅스 시스템에서 ftrace로 인터럽트를 분석할 필요가 있습니다. 
 
5.1.2 리눅스 커널 인터럽트 주요 개념 알아보기 

인터럽트에 대해 소개했으니 이번 장에서 배울 리눅스 커널에서 처리하는 인터럽트 관련 주요 개념을 소개합니다.
 - 인터럽트 핸들러
 - 인터럽트 벡터 
 - 인터럽트 디스크립터 
 - 인터럽트 컨택스트

인터럽트 핸들러란

인터럽트가 발생하면 이를 핸들링하기 위한 함수가 호출됩니다. 이를 인터럽트 핸들러라고 부릅니다. 예를 들어 키보드를 입력해 인터럽트가 발생하면 키보드 인터럽트를 처리하는 키보드 인터럽트 핸들러가 호출됩니다. 그리고 휴대폰에서 화면을 손으로 만지면 터치 인터럽트가 발생합니다. 이 때 터치 인터럽트를 처리하는 터치 인터럽트 핸들러가 실행됩니다.
 
[그림 5.2] 디바이스 별로 실행하는 인터럽트 핸들러

위 그림에서 인터럽트 종류 별로 인터럽트 핸들러가 있습니다. 인터럽트 핸들러는 함수 형태이며 리눅스 커널 인터럽트 함수에서 호출합니다. 인터럽트가 발생해 지정한 인터럽트 핸들러를 호출하려면 적절한 인자와 함께 request_irq() 함수를 호출해야 합니다.

이해를 돕기 위해 컴퓨터에서 마우스를 움직였을 때 인터럽트를 처리하는 관련 코드를 예로 들겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/input/mouse/amimouse.c]
01 static int amimouse_open(struct input_dev *dev)
02 {
03 unsigned short joy0dat;
...
04 error = request_irq(IRQ_AMIGA_VERTB, amimouse_interrupt, 0, "amimouse",
05     dev);

04 번째 줄 코드와 같이 request_irq() 함수 2 번째 인자로 인터럽트 핸들러 함수를 등록합니다.

이후 마우스 인터럽트가 발생하면 request_irq() 함수에서 지정한 amimouse_interrupt() 함수가 호출됩니다.  
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/input/mouse/amimouse.c]
01 static irqreturn_t amimouse_interrupt(int irq, void *data)
02 {
03 struct input_dev *dev = data;
04 unsigned short joy0dat, potgor;
05 int nx, ny, dx, dy;
...
06 input_report_key(dev, BTN_LEFT,   ciaa.pra & 0x40);
07 input_report_key(dev, BTN_MIDDLE, potgor & 0x0100);
08 input_report_key(dev, BTN_RIGHT,  potgor & 0x0400);

인터럽트 핸들러에서는 마우스에서 입력한 데이터 정보를 참고해 유저 공간에 알리는 동작을 수행합니다.

코드가 복잡해 보이지만 다음 그림을 보면 쉽게 이해할 수 있습니다.
 
[그림 5.3] 마우스를 움직일 때 마우스 인터럽트 핸들러를 호출하는 과정

마우스를 움직이면 인터럽트가 발생해 인터럽트 벡터가 실행됩니다. 이후 커널 인터럽트 내부 함수에서 인터럽트 핸들러를 호출합니다. 많은 하드웨어 디바이스가 이 방식으로 하드웨어 변화를 알립니다.

인터럽트 컨택스트란

인터럽트 컨택스트는 현재 코드가 인터럽트를 핸들링 중이란 뜻입니다.

인터럽트 컨택스트에 대한 이해를 돕기 위해 먼저 소프트웨어 관점으로 인터럽트 실행 흐름을 단계별로 보겠습니다. 
1. 프로세스 실행 중
2. 인터럽트 벡터 실행 
3. 커널 인터럽트 내부 함수 호출
4. 인터럽트 종류별 인터럽트 핸들러 호출
  4.1 인터럽트 컨택스트 시작   
5. 인터럽트 핸들러 서브 루틴 실행 시작 
6. 인터럽트 핸들러 서브 루틴 실행 마무리 
  6.1 인터럽트 컨택스트 마무리  

인터럽트가 발생하면 실행 중인 코드를 멈추고 인터럽트 벡터로 이동해 인터럽트에 대한 처리를 수행합니다. 인터럽트 종류별로 지정한 인터럽트 핸들러가 실행합니다.

위 4.1~6.1 사이에 호출된 함수는 인터럽트 컨택스트에서 실행됐다고 할 수 있습니다. 
   
현재 실행 중인 코드가 인터럽트가 컨택스인지 알려면 in_interrupt() 함수를 호출하면 알려줍니다. true를 반환하면 현재 실행 중인 코드가 4.1~6.1 구간이라는 것입니다. 

인터럽트가 발생하면 실행 중인 프로세스를 멈추고 인터럽트 핸들러를 호출합니다. 따라서 인터럽트 컨택스트에서 실행 중인 함수 코드는 빠르고 간결해야 합니다.

인터럽트 디스크립터란

인터럽트 종류별로 다음 인터럽트 세부 속성을 관리하는 자료구조를 인터럽트 디스크립터라고 합니다.
 - 인터럽트 핸들러
 - 인터럽트 핸들러 매개 인자 
 - 논리적인 인터럽트 번호 
 - 인터럽트 실행 횟수 

프로세스의 세부 속성을 표현하는 자료구조가 태스크 디스크립터 이듯 인터럽트에 대한 속성 정보를 저장하는 자료구조가 인터럽트 디스크립터인 것입니다.

커널 인터럽트 세부 함수에서 인터럽트 디스크립터에 접근해 인터럽트 종류 별로 세부 처리를 합니다. 다음 그림은 인터럽트가 발생했을 때 인터럽트 핸들러를 호출하는 흐름입니다.
 
   [그림 5.4] 인터럽트 디스크립터로 인터럽트 핸들러를 호출하는 과정

커널 세부 함수에서 인터럽트 핸들러를 호출하기 위해 인터럽트 종류별 인터럽트 디스크립터를 접근합니다. 인터럽트 디스크립터에 저장된 인터럽트 핸들러 정보로 인터럽트 핸들러를 호출합니다.

인터럽트 디스크립터는 struct irq_desc 구조체이며 선언부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqdesc.h]
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;

이번 소절에서 소개한 인터럽트 주요 개념은 5.2 절부터 상세히 살펴볼 예정입니다.

5.1.3 인터럽트를 잘 알아야 하는 이유

커널이 인터럽트를 처리하는 과정과 자료구조는 왜 잘 알아야 할까요? 인터럽트를 처리하는 방식이 시스템 전반에 큰 영향을 끼치기 때문입니다. 또한, 리눅스 커널 시스템 전반을 잘 이해하기 위해서도 커널이 인터럽트를 어떻게 처리하는지 잘 알고 있어야 합니다. 또 다른 이유는 다음과 같습니다. 
 1| 대부분 리눅스 드라이버는 인터럽트를 통해 하드웨어 디바이스와 통신합니다. 그래서 디바이스 드라이버 코드를 처음 분석할 때 인터럽트 처리하는 함수나 코드를 먼저 확인합니다. 인터럽트 동작 방식을 잘 알고 있으면 디바이스 드라이버 코드를 빨리 이해할 수 있습니다.
 2| 인터럽트가 발생하면 프로세스는 이미 정해진 동작을 수행합니다. 인터럽트 처리 과정을 숙지하면 프로세스가 스택 메모리 공간에서 어떻게 실행되는지 알게 됩니다. 
 3| CPU 아키텍처(x86, ARM)에 따라 인터럽트 벡터는 달리 동작합니다. 인터럽트 벡터가 어떻게 동작하는지 잘 알면 자연히 ARM 아키텍처에 대해 더 많이 알게 됩니다.

무엇보다 리눅스 커널을 새로운 보드에 포팅하거나 시스템 전반을 설계하는 개발자는 커널이 인터럽트를 어떻게 처리하는지 잘 알아야 합니다. 커널 패닉이나 시스템이 느려지는 성능 문제가 인터럽트 동작과 연관된 경우가 많기 때문입니다.

다양한 리눅스 커널 서브 시스템을 제대로 이해하기 위해서 인터럽트 세부 동작을 알 필요가 있습니다. 그 이유는 다음과 같습니다.
 - 스케줄링에서 선점(Preemptive) 스케줄링 진입 경로 중 하나가 인터럽트 핸드링 처리를 끝낸 시점입니다.
 - 유저 공간에서 등록한 시그널 핸들러는 인터럽트 핸들링을 한 다음 처리를 시작합니다.
 - 레이스 컨디션이 발생하는 가장 큰 이유 중 하나가 비동기적으로 인터럽트가 발생해서 코드 실행을 멈추기 때문입니다.

여기서 “비동기적” 이란 용어는 언제 발생할지 모른다는 의미입니다. 

인터럽트를 잘 알아야 하는 이유는 알아봤습니다. 이어서 다음 소절에서 리눅스 커널에서 인터럽트를 처리하는 흐름을 살펴보겠습니다.
 
5.1.4 리눅스 커널에서 인터럽트 처리 흐름

인터럽트가 발생했을 때 커널이 이를 핸들링하는 과정은 다음 3단계로 나눌 수 있습니다.

1 단계: 인터럽트 발생
인터럽트가 발생하면 인터럽트 벡터가 실행된 후 프로세스가 중인 레지스터 세트를 스택에 저장하고 커널 인터럽트 공통 함수를 호출합니다.

2단계: 인터럽트 핸들러 호출
커널에서는 발생한 인터럽트에 대응하는 인터럽트 디스크립터를 읽고 인터럽트 핸들러를 호출합니다.

3단계: 인터럽트 핸들러 실행
인터럽트 핸들러에서 하드웨어를 직접 제어하고 유저 공간에 이 변화를 알립니다.

이해를 돕기 위해 여러분들이 많이 쓰는 휴대폰(안드로이드) 동작을 예로 들겠습니다. 참고로 안드로이드 폰에서도 리눅스 커널이 돌고 있습니다.

여러분이 손으로 휴대폰 화면을 터치하면 내부 동작은 다음 단계로 나눌 수 있습니다.
1단계: 터치 인터럽트 발생
하드웨어적인 터치 모듈이 변화를 감지하고 터치 모듈에 대한 인터럽트 신호를 유발합니다. 이때 인터럽트 벡터가 실행됩니다. 

2단계: 터치 인터럽트 핸들러 호출
커널은 터치 인터럽트에 해당하는 번호로 인터럽트 디스크립터를 읽습니다.
인터럽트 디스크립터로 터치 디바이스 드라이버에서 등록한 인터럽트 핸들러를 호출합니다. 

3단계: 터치 인터럽트 핸들러 실행
결국 터치 인터럽트 핸들러는 해당 터치 인터럽트에 대해 화면을 업데이트하거나 하드웨어 터치 디바이스에 인터럽트를 잘 받았다는 사실을 알립니다.

 “인터럽트 디스크립터”, “인터럽트 벡터”와 같은 낯선 용어로 설명해 드렸는데, 이 단어의 공학적인 의미들은 하나하나 각 장에서 다룰 예정입니다. 

인터럽트 발생 단계를 터치 드라이버 예를 들어 살펴봤습니다.

인터럽트가 발생을 처리하는 단계를 함수 흐름과 실행 주체별로 분류하면 다음 그림과 같습니다. 
 
[그림 5.5] ARM 프로세서/리눅스 커널/디바이스 드라이버 별 인터럽트 핸들링 흐름도

전체 실행 흐름은 다음 3단계로 분류할 수 있습니다.

1. ARM 프로세스
인터럽트가 발생하면 실행 중인 코드를 멈춘 후 인터럽트 벡터로 실행 흐름을 이동합니다. ARM 프로세스와 연관된 동작입니다. 

2. Linux Kernel
인터럽트 벡터로 프로그램 카운터를 브랜치합니다. 커널 인터럽트 내부 함수에서 커널에서 관리하는 인터럽트 자료구조인 인터럽트 디스크립터로 인터럽트 처리를 시작합니다. 인터럽트 디스크립터에 저장된 인터럽트 핸들러를 호출합니다. 

3. 디바이스 드라이버
각 디바이스 드라이버에서 등록한 인터럽트 핸들러를 실행해서 인터럽트 발생에 대한 처리를 수행합니다.

정리하면 다음 질문에 대한 내용을 이번 장에서 다루려 합니다. 
“인터럽트로 하드웨어적인 변화가 발생하면 리눅스 커널에서 어떻게 처리를 하나?” 

이번 절에서는 인터럽트에 대해 전반적인 소개를 했으니 이어서 인터럽트 컨택스트에 대해 살펴보겠습니다. 

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

# Reference (인터럽트 처리)



핑백

덧글

댓글 입력 영역