Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이]인터럽트 후반부 처리(Bottom Half) 소개 #CS [라즈베리파이]인터럽트후반부

리눅스 커널이 인터럽트를 어떻게 처리하는지 배운 내용을 잠깐 복습해볼까요? 핵심 개념은 다음과 같습니다.
첫째, 인터럽트가 발생하면 커널은 실행 중인 프로세스를 멈추고 인터럽트 벡터를 실행해서 인터럽트 핸들러를 실행합니다.
둘째, 인터럽트 핸들러는 짧고 빨리 실행해야 합니다.
셋째, 인터럽트를 처리하는 구간이 인터럽트 컨택스트인데 이를 in_interrupt 매크로가 알려줍니다.

욕심이 지나쳐 인터럽트 핸들러에서 많은 일을 하고 싶을 때가 있습니다. 가령 유저 공간에 인터럽트가 발생한 사실을 알리거나 다른 프로세스에게 일을 시키고 싶은 경우죠. 그런데 인터럽트 핸들러에서 이런 동작을 실행하면 어떻게 될까요? 이럴 때 시스템이 아주 느려지거나 커널이 오동작 할 수 있습니다. 그래서 인터럽트 컨택스트에서 많은 일을 하는 함수를 호출하면 커널은 이를 감지하고 커널 패닉을 유발합니다. 

인터럽트 핸들러에서 많은 일을 하다가 커널 패닉이 발생하는 예를 들어 볼까요? 다음 로그는 인터럽트 핸들러 실행 도중 발생한 커널 패닉 로그입니다. 함수가 일렬로 정렬해있습니다.
[21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
[21.719382] BUG: scheduling while atomic: InputReader/1039/0x00010001
[21.719417] (unwind_backtrace+0x0/0x144) from (dump_stack+0x20/0x24)
[21.719432] (dump_stack+0x20/0x24) from (__schedule_bug+0x50/0x5c)
[21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
[21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
[21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
[21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
[21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
[21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
[21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)
[21.719531] (tegra_i2c_xfer+0x314/0x438) from (i2c_transfer+0xc4/0x128)
[21.719546] (i2c_transfer+0xc4/0x128) from (__mxt_read_reg+0x70/0xc8)
[21.719560] (__mxt_read_reg+0x70/0xc8) from (mxt_read_and_process_messages+0x58/0x1648)
[21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
[21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)
[21.719601] (handle_irq_event_percpu+0x88/0x2ec) from (handle_irq_event+0x4c/0x6c)
[21.719614] (handle_irq_event+0x4c/0x6c) from (handle_level_irq+0xbc/0x118)
[21.719626] (handle_level_irq+0xbc/0x118) from (generic_handle_irq+0x3c/0x50)
[21.719642] (generic_handle_irq+0x3c/0x50) from (tegra_gpio_irq_handler+0xa8/0x124)
[21.719655] (tegra_gpio_irq_handler+0xa8/0x124) from (generic_handle_irq+0x3c/0x50)
[21.719669] (generic_handle_irq+0x3c/0x50) from (handle_IRQ+0x5c/0xbc)
[21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
[21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

참고로 위 로그가 동작한 시스템은 엔비디아 Tegra4i SoC 디바이스입니다. 그래서 tegra가 붙은 함수들이 보이죠. 가끔 라즈베리안 이외 다른 리눅스 시스템에서 발생한 문제를 소개할게요.

로그를 꼼꼼히 분석해볼까요? 함수들이 줄 서 있는데 어느 부분 로그부터 읽어봐야 할까요? 함수들이 가장 먼저 실행된 순서로 정렬돼 있으니 가장 아랫부분 로그부터 봐야 합니다. 이제부터 5장에서 배운 내용을 떠 올리면서 로그 분석을 시작할게요.
 
가장 처음 실행된 함수 로그부터 볼게요. 인터럽트가 발생하면 인터럽트 벡터인 __irq_svc가 실행하니 인터럽트가 발생했다고 볼 수 있습니다. 인터럽트 벡터인 __irq_svc 함수부터 실행된 콜스택(함수 흐름)이니 인터럽트 컨택스트이네요. 
[21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
[21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

아래 로그로 인터럽트 핸들러로 mxt_interrupt 함수가 호출됐다는 사실을 알 수 있습니다. 
[21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
[21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)
[21.719601] (handle_irq_event_percpu+0x88/0x2ec) from (handle_irq_event+0x4c/0x6c)

인터럽트 핸들러는 __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#L133]
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);

다음은 커널 패닉이 발생하는 이유를 알려주는 로그입니다. 함수 흐름으로 보아 wait_for_common 함수를 호출해서 complete 함수가 수행되기를 기다리는 상황입니다. 그런데 complete 함수 호출을 안 하니 schedule_timeout 함수를 호출하는군요.
[21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
[21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
[21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
[21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
[21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
[21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
[21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)

이후 __schedule -> __schedule_bug 순서로 함수를 호출합니다.

이제 커널 패닉이 발생하는 이유를 알려주는 로그를 볼 차례입니다. 인터럽트 컨택스트에서 스케줄링을 하니 커널은 이를 감지하고 커널 패닉을 유발하는 것입니다.
[21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
[21.719382] BUG: scheduling while atomicInputReader/1039/0x00010001

두 번째 줄 로그를 보면 InputReader는 프로세스 이름, 1039는 pid 그리고 0x00010001는 struct thread_info의 preempt_count 멤버 변수입니다. 0x00010001 값과 HARDIRQ_OFFSET(0x10000)란 매크로를 AND 연산하면 1이므로 인터럽트 컨택스트이네요.

그리고 위 로그에서 “scheduling while atomic”란 메시지가 보입니다. 여기서 atomic이란 무슨 의미일까요? 커널에서는 어떤 코드나 루틴이 실행 도중 스케줄링 되면 안 될 때 있습니다. 이를 유식한 말로 원자적 처리라고 하며 원어 그대로 atomic operation이라고 합니다. 그런데 커널에서는 인터럽트 컨택스트도 실행 도중 스케쥴링하면 안되는 atomic operation이라고 판단합니다. 그래서 이런 경고 메시지를 출력하는 것이죠. (atomic operation은 커널 동기화 장에서 자세히 다루니 이 점 참고하세요.)

정리하면 인터럽트 컨택스트에서 스케쥴링을 시도하니 커널은 이를 감지하고 커널 패닉을 유발하는 것입니다.

인터럽트가 발생하면 처리할 일이 많을 때는 어떻게 해야 할까요? 인터럽트 컨택스트에서는 빨리 일을 끝내야 하는데 말이죠. 이럴 때는 해야 할 일을 두 개로 나누면 됩니다. 빨리 처리해야 하는 일과 조금 있다가 처리해도 되는 일이죠. 임베디드 세상에선 인터럽트가 발생 후 빨리 처리해야 하는 일은 Top Half, 조금 있다가 처리해도 되는 일은 Bottom Half라고 합니다. 좀 낯선 용어죠. 그런데 사실 용어만 낯설지 그 내용은 별 게 아니에요. 우선 인터럽트 핸들러가 하는 일은 Top Half라고 할 수 있죠. Bottom Half는  인터럽트에 대한 처리를 프로세스 레벨에서 수행하는 방식입니다. 이를 인터럽트 후반부 처리라고도 합니다.

리눅스 커널에서 Bottom Half는 어떻게 구현할까요? 보통 IRQ Thread, Soft IRQ와 워크큐 기법으로 구현합니다. 인터럽트 핸들러는 일하고 있던 프로세스를 멈춘 시점인 인터럽트 컨택스트에서 실행하는데 IRQ Thread, Soft IRQ와 워크큐는 커널 쓰레드 레벨에서 실행합니다. 급하게 처리해야 할 일은 인터럽트 컨택스트 조금 후 실행해도 되는 일은 커널 쓰레드 레벨에서 처리하는 것입니다. 

인터럽트 컨택스트와 커널 쓰레드 레벨에서 어떤 코드를 동작할 때 어떤 차이점이 있을까요?
우선 인터럽트 컨택스트에서는 호출할 수 있는 함수가 제한돼 있습니다. 리눅스 커널에서는 인터럽트 컨택스트에서 많은 일을 하는 함수를 호출할 때 경고 메시지를 출력하거나 커널 패닉을 유발해서 시스템 실행을 중단시킵니다. 예를 들어 스케쥴링과 연관된 뮤텍스나 schedule 함수를 쓰면 커널은 강제로 커널 패닉을 유발합니다.

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

예를 들어 인터럽트가 발생했을 때 이를 유저 공간에 알리고 싶을 경우가 있습니다. 안드로이드  디바이스 같은 경우에 터치를 입력하면 발생하는 인터럽트를 uevent로 유저 공간에 알릴 수 있죠. 이런 동작은 IRQ Thread에서 동작하도록 코드를 구현해야 합니다.

이번 장에서는 리눅스 커널이 Bottom Half을 처리하는 대표적인 기법인 IRQ Thread와 Soft IRQ에 대해 살펴봅니다. 워크큐는 워크큐를 다루는 장에서 살펴볼 예정입니다. 세 가지 기법은 인터럽트가 발생하고 나면 처리하는 방식이 조금씩 다르지만 인터럽트 핸들러에서 해야 할 일을 나눈다는 점은 같습니다. 
이 세가지 기법의 특징이 뭔지 잠깐 알아볼까요?
IRQ Thread(threaded IRQ)
인터럽트를 처리하는 전용 IRQ Thread에서 인터럽트 후속 처리를 합니다. 만약 rasp란 24번 인터럽트가 있으면 “irq/24-rasp”란 IRQ Thread가 24번 인터럽트를 전담해서 처리합니다.
Soft IRQ
인터럽트 핸들러가 실행이 끝나면 이때 일을 시작합니다. 인터럽트 핸들러에서 하지 못한 처리를 하다가 실행 시간이 오래 걸리면 ksoftirqd란 프로세스로 스케쥴링되고 이 ksoftirqd란 프로세스에서 나머지 인터럽트 후반부 잔업을 처리하는 구조입니다.
워크큐
인터럽트 핸들러가 실행될 때 워크를 워크큐에 큐잉하고 프로세스 레벨의 워커 쓰레드에서 인터럽트 후속처리를 하는 방식입니다.

인터럽트를 처리하는 드라이버를 작성할 때 위 세 가지 중 어느 기법을 선택할지는 드라이버 담당자의 몫입니다. 인터럽트 발생 빈도와 이를 처리하는 시나리오에 따라 위 세 가지 기법을 적절히 조합해서 드라이버 코드를 작성해야 합니다. 이를 위해서 인터럽트를 시스템에서 처리하는 방식과 인터럽트 발생 빈도를 알아야 합니다. 사실, 디바이스 드라이버나 시스템 전반을 설계하는 개발자는 인터럽트가 발생하면 소프트웨어적으로 이를 어떻게 설계할지 많은 고민을 합니다. 이런 설계를 잘하려면 우선 이 기법들은 잘 알고 있어야겠죠.

예를 들어 유저가 터치 화면을 입력하면 발생하는 인터럽트는 IRQ Thread나 워크큐를 써서 처리하면 되고, 1초에 수백 번 이상 발생하는 인터럽트를 예정된 시간 내에 처리해야 하는 실행 속도에 민감한 시나리오에서는 Soft IRQ 기법을 써야 합니다.

핑백

덧글

댓글 입력 영역