Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

10206
629
98792


[리눅스커널] 인터럽트 후반부 처리(Bottom Half) 소개 6장. 인터럽트 후반부 처리

6.1 인터럽트 후반부 기법 소개

이번 절에서는 인터럽트 후반부에 대해 소개합니다.
- 인터럽트 후반부 기법을 적용하는 이유
- 인터럽트 핸들러(인터럽트 컨택스트)에서 스케줄링 함수를 호출하면 생기는 문제점
- 인터럽트 후반부 기법 종류와 차이점 소개

먼저 인터럽트 후반부 기법을 적용하는 이유를 알아보겠습니다.

6.1.1 인터럽트 후반부 기법을 적용하는 이유

인터러트 후반부 기법을 적용하는 이유를 살펴보기 전에 먼저 리눅스 커널이 인터럽트를 어떤 방식으로 처리하는지 소개합니다. 5장에서 다룬 인터럽트 처리 과정을 요약하면 다음과 같습니다. 
  1. 인터럽트가 발생하면 커널은 실행 중인 프로세스를 멈추고 인터럽트 벡터를 실행해서 인터럽트 핸들러를 실행합니다.
  2. 인터럽트 핸들러는 짧고 빨리 실행해야 합니다.
  3. 인터럽트를 처리하는 구간이 인터럽트 컨택스트인데 이를 in_interrupt() 함수가 알려줍니다.

다음 그림은 라즈베리파이에서 인터럽트가 발생했을 때 함수 흐름입니다.
                     
[그림 6.1] 인터럽트가 발생해 프로세스 실행이 멈추는 함수 흐름

위 그림에서 인터럽트가 발생해서 arch_cpu_idle() 함수 실행 도중 __irq_svc 인터럽트 벡터로 실행 흐름이 바뀌었습니다. 프로세스 입장에서 보면 arch_cpu_idle() 함수 실행 도중 멈추게 된 것입니다.

이렇게 인터럽트가 발생하면 실행 중인 프로세스를 멈추고 인터럽트 벡터로 이동해서 인터럽트 핸들러를 실행합니다. 따라서 인터럽트 핸들러는 빨리 실행해야 합니다.

이 과정에서 임베디드 리눅스 개발자뿐만 아니라 임베디드 개발에서 다음과 같은 고민을 하게 됐습니다.

여러 고민 끝에 인터럽트가 발생하면 인터럽트를 핸들링할 코드를 2 단계로 나눴습니다. 
- 빨리 실행할 코드: 인터럽트 핸들러 및 인터럽트 컨택스트
- 실시간으로 빨리 실행하지 않아도 되는 코드: 인터럽트 후반부 기법

결국 다음과 같은 인터럽트 후반부 처리 기법을 이끌어냈습니다. 
- IRQ 스레드
- Soft IRQ
- 태스크릿
- 워크큐

다음 소절에서는 인터럽트 후반부를 왜 적용해야 하는지 이해를 돕기 위해 인터럽트 컨택스트에서 실행 시간이 오래 걸리는 함수를 호출해 생기는 문제를 소개합니다.

6.1.1 인터럽트 컨택스트에서 많은 일을 하면 어떻게 될까?

인터럽트 컨택스트에서 실행 시간이 오래 걸리면 대부분 시스템은 오동작하게 됩니다. 디바이스 드라이버를 개발하다 보면 인터럽트 핸들러에 실수로 실행 시간이 오래 걸리는 코드를 입력할 수 있습니다. 다음과 같은 예를 들 수 있습니다.
- I/O을 시작하는 코드
- 과도한 while loop
- 유저 공간으로uevent를 전달해서 인터럽트 발생을 알림
- 스케줄링을 지원하는 커널 함수 호출

위와 같은 코드가 인터럽트 핸들러나 인터럽트 핸들러 서브 함수에서 동작하면 시스템 반응 속도가 아주 느려집니다. 또한 커널 로그를 열어보면 평소에 볼 수 없는 에러 메시지를 볼 수도 있습니다. 

여기서 중요한 의문점이 생깁니다.
인터럽트 컨택스트에서 인터럽트를 핸들링 하는 코드 실행 시간을 어떻게 측정할 수 있을까?

이를 위해 ftrace 기능에서 지원하는 graph_function 트레이서로 인터럽트 핸들링을 할 때 시간을 측정할 필요가 있습니다. 이 방법은 이번 장 디버깅 절에서 소개합니다.

인터럽트 컨택스트에서 발생한 커널 패닉

이번에 인터럽트 핸들러에서 시간이 오래 걸리는 함수를 실행해 커널 패닉이 발생했던 사례를 소개합니다. 

다음은 로그는 인터럽트 핸들러 실행 도중 발생한 커널 패닉 로그입니다. 
01 [21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
02 [21.719382] BUG: scheduling while atomic: InputReader/1039/0x00010001
03 [21.719417] (unwind_backtrace+0x0/0x144) from (dump_stack+0x20/0x24)
04 [21.719432] (dump_stack+0x20/0x24) from (__schedule_bug+0x50/0x5c)
05 [21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
06 [21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
07 [21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
08 [21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
09 [21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
10 [21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
11 [21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)
12 [21.719531] (tegra_i2c_xfer+0x314/0x438) from (i2c_transfer+0xc4/0x128)
13 [21.719546] (i2c_transfer+0xc4/0x128) from (__mxt_read_reg+0x70/0xc8)
14 [21.719560] (__mxt_read_reg+0x70/0xc8) from (mxt_read_and_process_messages+0x58/0x1648)
15 [21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
16 [21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)
17 [21.719601] (handle_irq_event_percpu+0x88/0x2ec) from (handle_irq_event+0x4c/0x6c)
18 [21.719614] (handle_irq_event+0x4c/0x6c) from (handle_level_irq+0xbc/0x118)
19 [21.719626] (handle_level_irq+0xbc/0x118) from (generic_handle_irq+0x3c/0x50)
20 [21.719642] (generic_handle_irq+0x3c/0x50) from (tegra_gpio_irq_handler+0xa8/0x124)
21 [21.719655] (tegra_gpio_irq_handler+0xa8/0x124) from (generic_handle_irq+0x3c/0x50)
22 [21.719669] (generic_handle_irq+0x3c/0x50) from (handle_IRQ+0x5c/0xbc)
23 [21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
24 [21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

참고로 위 로그가 동작한 시스템은 엔비디아 Tegra4i SoC 디바이스입니다. 그래서 tegra가 붙은 함수들이 보입니다. 라즈베리파이 이외에 다른 리눅스 시스템에서 인터럽트를 어떻게 처리하는지 알면 좋으니 다른 리눅스 시스템에서 발생한 문제를 소개합니다.

함수들이 줄 서 있는데 어느 부분 로그부터 읽어봐야 할까요? 
함수들이 가장 먼저 실행된 순서로 정렬돼 있으니 가장 아랫부분인 24 번째 로그부터 봐야 합니다. 이제부터 5장에서 배운 내용을 떠 올리면서 로그 분석을 시작합니다.
 
가장 처음 실행된 함수 로그부터 보겠습니다. 24 번째 줄 코드를 눈으로 따라가 보면 __irq_svc 레이블이 보일 것입니다. 
21 [21.719655] (tegra_gpio_irq_handler+0xa8/0x124) from (generic_handle_irq+0x3c/0x50)
22 [21.719669] (generic_handle_irq+0x3c/0x50) from (handle_IRQ+0x5c/0xbc)
23 [21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
24 [21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

함수 이름으로 인터럽트가 발생했다고 알 수 있습니다. 5장에서 배운 대로 인터럽트가 발생하면 인터럽트 벡터인 __irq_svc가 실행합니다. 여기서 24 번째 줄에서 21 번째 줄 방향으로 함수가 호출됩니다. 22 번째 줄에 generic_handle_irq() 함수가 보이니 인터럽트 컨택스트란 사실을 알 수 있습니다. 

다음 14~16 번째 로그를 보겠습니다.
14 [21.719560] (__mxt_read_reg+0x70/0xc8) from (mxt_read_and_process_messages+0x58/0x1648)
15 [21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
16 [21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)

인터럽트 핸들러로 mxt_interrupt() 함수가 호출됐습니다. 

우리는 5장에서 인터럽트 핸들러는 __handle_irq_event_percpu() 함수에서 호출한다고 배웠습니다. 그런데 위 로그에서는 handle_irq_event_percpu() 함수에서 인터럽트 핸들러를 호출합니다.

그 이유는 이 로그를 출력한 시스템의 리눅스 커널 버전이 3.10.77 버전이기 때문입니다. 다음 코드를 보면 5번째 줄 코드에서 인터럽트 핸들러를 호출합니다.
[https://elixir.bootlin.com/linux/v3.10.77/source/kernel/irq/handle.c]
1 irqreturn_t
2 handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
3 {
...
4 do {
...
5 res = action->handler(irq, action->dev_id);

리눅스 커널 버전에 따라 인터럽트 핸들러를 호출하는 함수 이름이 달라질 수 있습니다.

이어서 다음 로그를 보겠습니다.  
05 [21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
06 [21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
07 [21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
08 [21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
09 [21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
10 [21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
11 [21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)
12 [21.719531] (tegra_i2c_xfer+0x314/0x438) from (i2c_transfer+0xc4/0x128)
13 [21.719546] (i2c_transfer+0xc4/0x128) from (__mxt_read_reg+0x70/0xc8)

위 함수 흐름에서 리눅스 커널의 웨이트 큐를 써서 실행 흐름을 동기화하는 부분이 보입니다.

다음 그림과 같이 wait_for_common() 함수를 호출해서 complete() 함수가 수행되기를 기다리는 상황입니다. 그런데 complete() 함수 호출을 안 하니 schedule_timeout() 함수를 호출합니다. 
                    
     [그림 6.2] wait_for_common() 함수가 실행 중 타임아웃되는 과정

커널의 웨이트 큐 세부 동작은 이 책의 범위를 벗어나 다루지 않습니다. 세부 내용은 저자의 다음 블로그 페이지를 참고하세요.
[http://rousalome.egloos.com/9989042]


이번에 커널 패닉이 발생하는 이유를 알려주는 로그를 소개합니다. 
01 [21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
02 [21.719382] BUG: scheduling while atomic: InputReader/1039/0x00010001

인터럽트 컨택스트에서 스케줄링을 하니 커널은 이를 감지하고 커널 패닉을 유발합니다.

02 번째 줄 로그를 보면 InputReader는 프로세스 이름, pid는 1039 그리고 0x00010001는 프로세스 struct thread_info 구조체 preempt_count 필드 값입니다. 

로그 핵심 정보는 프로세스 struct thread_info 구조체 preempt_count 필드입니다. 이 값이 0x00010001이니 현재 실행 중인 코드는 인터럽트 컨택스트입니다. 0x00010001와 HARDIRQ_OFFSET(0x10000) 를 AND 비트 연산한 결과가 1이기 때문입니다.

위 로그에서 “scheduling while atomic”란 메시지가 보입니다. 메시지를 그대로 풀면 “아토믹 도중에 스케줄링 중이다.”입니다.


여기서 atomic이란 무슨 의미일까요? 커널에서는 다음과 같은 상황을 atomic이라고 말합니다.
 - 선점 스케줄링이 되지 않는 실행 단위(어셈블리 명령어) 
 - 어떤 코드나 루틴이 실행 도중 스케줄링을 수행하면 안되는 컨택스트

커널에서는 인터럽트 컨택스트도 스케줄링하면 안되는 컨택스트로 판단합니다. 즉 인터럽트 컨택스트는 atomic operation입니다. 따라서 이런 경고 메시지를 출력하는 겁니다. (atomic operation은 커널 동기화 장에서 자세히 다룹니다.)

정리하면 인터럽트 컨택스트에서 스케줄링을 시도하니 커널은 이를 감지하고 커널 패닉을 유발했습니다. 스케줄링을 실행하면 시간이 오래 걸리기 때문입니다.

이렇게 커널은 인터럽트 컨택스트에서 호출하는 커널 함수 호출을 제한합니다. 이 과정에서 자연스럽게 임베디드 개발에서 다음과 같은 고민이 생깁니다.
인터럽트 컨택스트에선 빨리 일을 해야 하는데, 어떻게 해야 이런 문제를 피할 수 있을까? 

Bottom Half와 Top Half란 개념을 이끌어 냈으며 이 기준으로 인터럽트 후반부 처리 기법을 적용하기 시작했습니다. 

다음 소절에서는 Top Half와 Bottom Half에 대해서 알아보겠습니다.

6.1.2 Top Half/Bottom Half 란 무엇일까?

인터럽트 컨택스트에서 인터럽트 핸들링을 빠른 시간에 마무리해야 합니다. 또한 시간이 오래 걸리는 함수를 호출하면 시스템은 커널 패닉으로 시스템이 오동작 할 수 있습니다.

여기서 한 가지 걱정이 생깁니다.
인터럽트가 발생해 인터럽트 핸들러에서 처리할 일이 많을 때는 어떻게 해야 할까?

이럴 때 해야 할 작업을 2단계로 나누면 됩니다. 
빨리 처리해야 하는 일과 조금 있다가 처리해도 되는 일입니다. 임베디드 용어로 인터럽트가 발생 후 빨리 처리해야 하는 일은 Top Half, 조금 있다가 처리해도 되는 일은 Bottom Half라고 말합니다. 쉽게 다음과 같이 설명할 수 있습니다.
인터럽트 핸들러가 하는 일은 Top Half라고 볼 수 있습니다. Bottom Half는 인터럽트 처리를 프로세스 레벨에서 수행하는 방식입니다.
 
리눅스 커널에서 Bottom Half을 어떤 방식으로 구현할까요? 
인터럽트 핸들러는 일하고 있던 프로세스를 멈춘 시점인 인터럽트 컨택스트에서 실행합니다. 급하게 처리해야 할 일은 인터럽트 컨택스트에서 처리하고 조금 후 실행해도 되는 일은 프로세스 컨택스트에서 처리합니다. 이를 위해 커널에서 다음과 같은 기법을 인터럽트 후반부 기법을 지원합니다.
 - IRQ 스레드
 - Soft IRQ
 - 워크큐

인터럽트 컨택스트와 프로세스 컨택스트 레벨에서 어떤 코드를 실행할 때 어떤 차이점이 있을까요? 먼저 인터럽트 컨택스트에서는 호출할 수 있는 함수가 제한돼 있습니다. 

리눅스 커널에서는 인터럽트 컨택스트에서 많은 일을 하는 함수를 호출할 때 경고 메시지를 출력하거나 커널 패닉을 유발해서 시스템 실행을 중단시킵니다. 예를 들어 스케줄링을 지원하는 뮤텍스나 schedule() 함수를 쓰면 커널은 강제로 커널 패닉을 유발합니다.

뮤텍스 함수는 스케줄링 동작과 연관돼 있습니다. 프로세스가 뮤텍스를 획득하려고 시도하는데 만약 다른 프로세스가 이미 뮤텍스를 획득했으면 휴면에 진입합니다.

그런데 인터럽트 컨택스트에 비해 커널 스레드에서는 커널이 제공하는 스케쥴링을 포함한 모든 함수를 쓸 수 있습니다. 따라서 시나리오에 따라 유연하게 코드를 설계할 수 있습니다.

예를 들어 인터럽트가 발생했을 때 이를 유저 공간에 알리고 싶을 경우가 있습니다. 안드로이드 디바이스 같은 경우에 터치를 입력하면 발생하는 인터럽트를 uevent로 유저 공간에 알릴 수 있습니다. 유저 공간에 uevent를 보내는 동작은 시간이 오래 걸리는 일입니다. 따라서 시간이
 오래 걸리는 코드는 인터럽트 후반부에서 처리하도록 드라이버 구조를 잡아야 합니다.

6.1.3 인터럽트 후반부 처리 기법 종류 및 소개

이번 장에서는 리눅스 커널이 Bottom Half을 처리하는 대표적인 기법인 IRQ 스레드와 Soft IRQ 기법과 태스크릿을 다룹니다. 워크큐는 워크큐를 다루는 장에서 살펴볼 예정입니다. 세 가지 기법 인터럽트 후반부 처리하는 방식이 조금씩 다릅니다. 하지만 인터럽트 핸들러에서 해야 할 일을 2 단계로 나눈다는 점은 같습니다. 

세 가지 기법의 특징을 요약하면 소개합니다.

IRQ 스레드(threaded IRQ)
인터럽트를 처리하는 전용 IRQ 스레드에서 인터럽트 후속 처리를 합니다. 만약 rasp란 24번 인터럽트가 있으면 “irq/24-rasp”란 IRQ 스레드가 24번 인터럽트 후반부를 전담해 처리합니다.

Soft IRQ
인터럽트 핸들러 실행이 끝나면 바로 일을 시작합니다. 인터럽트 핸들러 바로 처리해야 할 일을 마무리한 후 인터럽트 후반부 처리를 Soft IRQ 컨택스트에서 실행합니다. Soft IRQ 서비스 핸들러 실행 도중 시간이 오래 걸리면 ksoftirqd 프로세스를 깨우면 Soft IRQ 서비스를 종료합니다. ksoftirqd란 프로세스에서 나머지 인터럽트 후반부를 처리하는 구조입니다.  

태스크릿
Soft IRQ 서비스를 동적으로 쓸 수 있는 인터페이스이자 자료구조입니다.

워크큐
인터럽트 핸들러가 실행될 때 워크를 워크큐에 큐잉하고 프로세스 레벨의 워커 스레드에서 인터럽트 후반부 처리를 하는 방식입니다.

그러면 위에서 세 가지 기법 중 어떤 방식을 인터럽트 후반부 처리로 적용해야 할까요?

사실 인터럽트를 처리하는 드라이버를 작성할 때 어떤 기법을 쓸 지는 드라이버 담당자가 결정해야 합니다. 인터럽트 발생 빈도와 드라이버 시나리오에 따라 위 세 가지 기법을 적절히 조합해서 드라이버 구조를 잡으면 됩니다. 이를 위해서 인터럽트를 시스템에서 처리하는 방식과 인터럽트가 얼마나 자주 발생하는지를 알아야 합니다. 

리눅스 디바이스 드라이버를 개발할 때 다양한 하드웨어 디바이스를 인터럽트로 제어합니다. 또한 인터럽트를 관리하는 방식도 리눅스 시스템 마다 다릅니다. 

6.1.4 어떤 인터럽트 후반부 처리 기법을 적용해야 할까?

인터럽트 후반부 기법을 소개했습니다. 이번에 다양한 조건에서 어떤 인터럽트 방식을 적용해야 할지를 Q/A로 정리하면 다음과 같습니다.
Q: 인터럽트가 1초에 수 백번 발생하는 디바이스의 경우 어떤 인터럽트 후반부 기법을 적용해야 할까?

A: IRQ 스레드 방식과 워크큐 방식은 그리 적합하지 않습니다. IRQ 스레드는 RT 프로세스로 구동됩니다. 인터럽트가 많이 발생하면 IRQ 스레드를 깨워야 하고 IRQ 스레드는 RT 프로세스로 구동하므로 다른 프로세스들이 선점 스케줄링을 할 수 없습니다. IRQ 스레드 핸들러 실행 시간이 조금이라도 길어지면 다른 프로세스들이 실행을 못하고 대기해야 하므로 시스템 반응 속도가 느려질 수 있습니다.

만약 IRQ 스레드 방식을 적용해야 한다면 IRQ 스레드 핸들러 함수 실행 시간이 매우 짧아야 합니다. 예를 들면, IRQ 스레드 핸들러 함수에 printk() 함수와 같이 커널 로그를 출력하는 코드도 되도록 입력하지 말아야 합니다. prink() 함수는 시스템 관점으로 비용이 많이 듭니다.

또한 워크큐를 실행하는 워커 스레드는 일반 프로세스로 프로세스 우선 순위가 높지 않습니다.
인터럽트 발생 횟수만큼 워크 핸들러가 실행을 못할 수 있습니다. 

따라서 인터럽트가 자주 발생하는 디바이스는 Soft IRQ나 태스크릿 방식을 적용하는 것이 바람직합니다.

Q. 현재 개발 중인 시스템은 인터럽트 개수가 200개 정도된다. 어떤 방식을 적용하면 좋을까?

1초에 인터럽트가 수 백번 이상 발생하는 경우를 제외하곤 인터럽트 후반부 기법으로 IRQ 스레드 방식을 적용하면 됩니다. 그런데 인터럽트 개수만큼 IRQ 스레드를 생성하면 불필요한 메모리를 쓰게 됩니다. IRQ 스레드도 커널 스레드이므로 기본으로 프로세스를 관리할 때 필요한 태스크 디스크립터 만큼 메모리 공간을 써야 합니다. 만약 현재 개발 중인 시스템 RAM 용량이 8G 이상이면 별 문제가 되지 않을 것입니다.

인터럽트가 발생 빈도가 낮고 빠른 시간에 인터럽트 후반부를 처리하지 않아도 될 경우 워크큐 기법을 적용하는 것도 좋습니다.

여러 인터럽트 후반부 기법 중 어떤 방식을 적용할지 결정하는 것은 어려운 일입니다. 정답이 없기 때문입니다. 이런 상황에서 필요한 것이 최적화인 것 같습니다. 인터럽트 후반부 단계에서 인터럽트 처리를 최적화하도록 설계를 잘 하려면 먼저 커널이 인터럽트를 처리하는 세부 동작과 인터럽트 후반부 기법들의 세부 구현 방식을 잘 알고 있어야 합니다.

다음 절에서 인터럽트 후반부 기법 중 IRQ 스레드 방식을 살펴보겠습니다.


     "혹시 글을 읽고 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실하게 답변 올려 드리겠습니다!"


# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



    핑백

    덧글

    댓글 입력 영역