Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

18113
1478
166888


[리눅스커널][인터럽트] 인터럽트 컨택스트에서 스케줄링을 하면? 5. 인터럽트

인터럽트 컨텍스트에서는 사용할 수 있는 함수가 제한돼 있습니다. 만약 인터럽트 컨텍스트에서 스케줄링을 지원하는 커널 함수를 호출하면 커널 패닉이 발생하거나 WARN() 함수를 호출해 에러 로그를 출력합니다.

이번 절에서는 커널에서 인터럽트 컨텍스트 구간에서 스케줄링을 지원하는 함수 호출을 어떤 방식으로 제한하는지 살펴보겠습니다.

인터럽트 컨텍스트에서 스케줄링 코드를 실행했을 때 발생하는 커널 패닉

인터럽트 컨텍스트에서 스케줄링 관련 함수를 호출하면 안 되는 이유는 무엇일까요? 스케줄링 관련 함수를 호출하면 커널 내부에서 많은 연산을 수행하므로 실행 시간이 오래 걸립니다. 짧은 시간에 인터럽트 핸들러를 실행하고 인터럽트 발생으로 실행을 멈춘 코드로 돌아가야 하는데 시간이 오래 걸리는 함수를 호출하면 시스템이 오동작할 수 있습니다.

하지만 이 사실을 모르고 인터럽트 컨텍스트에서 스케줄링을 지원하는 함수를 호출할 수 있습니다. 이를 대비해 커널은 인터럽트 컨텍스트에서 스케줄링을 지원하는 함수를 호출하면 에러 메시지를 출력하거나 커널 패닉을 유발합니다. 그래서 이번에는 인터럽트 컨텍스트에서 스케줄링 함수를 호출할 때 발생하는 커널 패닉 문제를 분석해보겠습니다.

프로세스가 스케줄링, 즉 휴면할 때는 __schedule() 함수를 호출합니다. 이 함수를 열어보면 함수의 앞부분에 schedule_debug() 함수를 호출해서 현재 프로세스가 휴면할 수 있는 조건인가를 검사합니다. 이 동작을 처리할 때의 함수 호출 흐름은 다음과 같습니다.

 
[그림 5.10] 인터럽트 컨텍스트에서 휴면할 시 커널 패닉이 발생하는 코드 흐름

이어서 인터럽트 컨텍스트에서 schedule_debug() 함수가 호출됐다고 가정하면서 함수 코드를 보겠습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c
01 static inline void schedule_debug(struct task_struct *prev)
02 {
03 #ifdef CONFIG_SCHED_STACK_END_CHECK
04 if (task_stack_end_corrupted(prev))
05 panic("corrupted stack end detected inside scheduler\n");
06 #endif
07
08 if (unlikely(in_atomic_preempt_off())) {
09 __schedule_bug(prev);
10 preempt_count_set(PREEMPT_DISABLED);
11 }


03~06번째 줄은 프로세스 스택이 오염됐는지 점검하는 코드입니다.

다음으로 08번째 줄을 보겠습니다. 인터럽트 컨텍스트에서 08번째 줄을 실행하면 in_atomic_preempt_off() 매크로에서 TRUE를 반환하고 09번째 줄을 실행해 __schedule_bug() 함수를 호출합니다. 

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c
01 static noinline void __schedule_bug(struct task_struct *prev)
02 {
03 /* Save this before calling printk(), since that will clobber it */
04 unsigned long preempt_disable_ip = get_preempt_disable_ip(current);
05
06 if (oops_in_progress)
07 return;
08
09 printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n",
10 prev->comm, prev->pid, preempt_count());

__schedule_bug() 함수의 리눅스 커널 코드를 보면 09번째 줄과 같이 커널 로그로 에러 정보를 출력합니다. 그런데 대부분 SoC 벤더들은 10번째 줄 다음 코드에서 BUG() 함수를 호출해서 강제로 커널 패닉을 유발합니다.

참고로 인터럽트 컨텍스트에서 스케줄링을 하다가 발생하는 커널 패닉은 리눅스 커널 커뮤니티에서 자주 볼 수 있는 로그입니다.

인터럽트 컨텍스트에서 스케줄링 지원 함수 호출 시 발생하는 커널 패닉 사례 연구

이번에는 리눅스 커뮤니티에서 논의된 문제 가운데 앞에서 설명한 상황과 비슷한 문제를 소개하겠습니다.

https://patchwork.kernel.org/patch/4864261
01 BUG: sleeping function called from invalid context at ../kernel/locking/mutex.c:583
02 in_atomic(): 1, irqs_disabled(): 128, pid: 0, name: swapper/0
03 ------------[ cut here ]------------
04 WARNING: CPU: 2 PID: 4828 at ../kernel/locking/mutex.c:479 05 mutex_lock_nested+0x3a0/0x3e8()
05 DEBUG_LOCKS_WARN_ON(in_interrupt()) // [3]
06 Modules linked in:
07 CPU: 2 PID: 4828 Comm: Xorg.bin Tainted: G        W      3.17.0-rc3-00234-gd535c45-dirty #819
08 [<c0216690>] (unwind_backtrace) from [<c0212174>] (show_stack+0x10/0x14)
09 [<c0212174>] (show_stack) from [<c0867cc0>] (dump_stack+0x98/0xb8)
10 [<c0867cc0>] (dump_stack) from [<c02492a4>] (warn_slowpath_common+0x70/0x8c)
11 [<c02492a4>] (warn_slowpath_common) from [<c02492f0>] (warn_slowpath_fmt+0x30/0x40)
12 [<c02492f0>] (warn_slowpath_fmt) from [<c086a3f8>] 13 (mutex_lock_nested+0x3a0/0x3e8)
13 [<c086a3f8>] (mutex_lock_nested) from [<c0294d08>](irq_find_host+0x20/0x9c)// [2]
14 [<c0294d08>] (irq_find_host) from [<c0769d50>] (of_irq_get+0x28/0x48)
15 [<c0769d50>] (of_irq_get) from [<c057d104>] (platform_get_irq+0x1c/0x8c)
16 [<c057d104>] (platform_get_irq) from [<c021a06c>] (cpu_pmu_enable_percpu_irq+0x14/0x38)
17 [<c021a06c>] (cpu_pmu_enable_percpu_irq) from [<c02b1634>] (flush_smp_call_function_queue+0x88/0x178)
18 [<c02b1634>] (flush_smp_call_function_queue) from [<c0214dc0>] (handle_IPI+0x88/0x160)
19 [<c0214dc0>] (handle_IPI) from [<c0208930>] (gic_handle_irq+0x64/0x68)
20 [<c0208930>] (gic_handle_irq) from [<c0212d04>] (__irq_svc+0x44/0x5c)
21 [<c0212d04>] (__irq_svc) from [<c02a2e30>] (ktime_get_ts64+0x1c8/0x200) // [1]
22 [<c02a2e30>] (ktime_get_ts64) from [<c032d4a0>] (poll_select_set_timeout+0x60/0xa8)
23 [<c032d4a0>] (poll_select_set_timeout) from [<c032df64>] (SyS_select+0xa8/0x118)
[<c032df64>] (SyS_select) from [<c020e8e0>] (ret_fast_syscall+0x0/0x48)

위 커널 로그에서 볼 수 있는 에러 메시지를 해석하면 다음과 같습니다.

인터럽트 컨텍스트에서 뮤텍스 획득을 시도
커널에서 인터럽트 컨텍스트를 감지한 후 에러 메시지를 출력

여기서 한 가지 의문이 생깁니다. 인터럽트 컨텍스트에서 뮤텍스를 획득하면 커널이 왜 에러 메시지를 출력할까요?

뮤텍스는 스케줄링을 지원하는 기능이기 때문입니다. 한 가지 예를 들어볼까요? 뮤텍스를 이미 다른 프로세스가 획득했는데 다른 프로세스가 뮤텍스 획득을 시도하면 휴면 상태로 진입합니다. 만약 인터럽트 컨텍스트에서 이런 동작을 수행하면 실행 시간이 오래 걸릴 것입니다.

지금까지 배운 내용을 활용해서 위 로그를 분석해보겠습니다. 먼저 19~21 번째 줄을 보겠습니다.

19[<c0214dc0>] (handle_IPI) from [<c0208930>] (gic_handle_irq+0x64/0x68)
20[<c0208930>] (gic_handle_irq) from [<c0212d04>] (__irq_svc+0x44/0x5c)
21[<c0212d04>] (__irq_svc) from [<c02a2e30>] (ktime_get_ts64+0x1c8/0x200) 

21번째 줄에서 보이는 __irq_svc는 인터럽트 벡터 함수입니다. 이 정보로 인터럽트가 발생했다고 판단할 수 있습니다. 이후 gic_handle_irq() 함수와 handle_IPI() 함수 순서로 인터럽트를 처리하는 함수를 호출합니다. 

19~21번째 함수 흐름을 어떻게 해석할까요? 인터럽트가 발생해서 인터럽트 벡터가 실행됐으므로 인터럽트 컨텍스트라고 말할 수 있습니다.

다음은 13~14 번째 줄입니다.

13 [<c086a3f8>] (mutex_lock_nested) from [<c0294d08>](irq_find_host+0x20/0x9c) 
14[<c0294d08>] (irq_find_host) from [<c0769d50>] (of_irq_get+0x28/0x48)

이를 통해 뮤텍스를 획득하는 동작을 확인할 수 있습니다.

마지막으로 05번째 줄을 봅시다.

04 WARNING: CPU: 2 PID: 4828 at ../kernel/locking/mutex.c:479 05 mutex_lock_nested+0x3a0/0x3e8()
05 DEBUG_LOCKS_WARN_ON(in_interrupt())  

현재 실행 중인 코드가 인터럽트 컨텍스트인지 검사한 후 에러 메시지를 출력합니다. in_interrupt() 함수가 true를 반환하므로 DEBUG_LOCKS_WARN_ON() 매크로 함수 내에서 WARN() 함수가 실행되는 동작입니다. 참고로 WARN() 매크로 함수가 실행되면 해당 시점의 콜스택을 커널 로그로 출력합니다.

이번 절에서는 코드 분석을 통해 다음과 같은 내용을 알게 됐습니다.

인터럽트 컨텍스트는 인터럽트가 발생한 후 인터럽트를 처리하는 코드 흐름이라 빨리 코드를 실행해야 한다.
인터럽트 컨텍스트를 알려주는 매크로 함수는 in_interrupt()다.

이어서 인터럽트 핸들러는 언제 어떻게 호출되는지 알아보겠습니다.



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

Thanks,
Austin Kim(austindh.kim@gmail.com)

# Reference (인터럽트 처리)

인터럽트 소개  
   * 리눅스 커널에서의 인터럽트 처리 흐름    
인터럽트 컨텍스트  
인터럽트 핸들러는 언제 호출될까?  
인터럽트 핸들러는 어떻게 등록할까?  
인터럽트 디스크립터  
인터럽트 디버깅  




    핑백

    덧글

    댓글 입력 영역