Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

5363
1898
209234


[Linux][Kernel] irq - in_interrupt irq context(BEST) [Linux][Kernel] IRQ(Interrupt)

커널이나 드라이버 코드 리뷰를 하는 도중에 preempt_add, preempt_sub 그리고 in_interrupt 함수를 자주 마주칩니다. 이 함수들의 사용 예와 구현부에 대해서 좀 더 짚어 볼까요?

리눅스 커널에서 보는 모든 코드는 두 가지 모드에서 돌고 있어요.
process context: 우리가 보는 대부분의 코드라고 할 수 있는데, 커널 쓰레드로 돌고 있는 상태죠.
IRQ context: 어떤 디바이스던 인터럽트 전기 신호로 IRQ가 Trigger될 수 있어요. 그래서 해당 IRQ에 매핑되는 Interrupt Subroutine(ISR) 핸들러나 이 서브 루틴에서 돌고 있는 상태죠.

그럼 어떤 함수가 IRQ/process context인지는 어떻게 알 수 있을까요? 스스로 참 알기 어렵죠.
이를 식벽할 수 있는 매크로가 in_interrupt랍니다.

in_interrupt 매크로의 역할을 간단히 말하면, 현재 실행 중인 코드가 process context 혹은 interrupt context 상에서 돌고 있는 지 알려줍니다. process context 혹은 interrupt context의 개념은 여러 리눅스 커널 교재에서 마르고 닳도록 설명하고 있는데요, 아래 스택 트레이스를 보면 간단히 파악할 수 있습니다.

__irq_svc(asm) -- unwind_backtrace() 사이에 보이는 함수(붉은색으로 마킹)들은 irq context에서 실행되는 함수들, 
start_kernel() --arch_cpu_idle() 구간에 보이는 함수들은 process context에서 구동이 된다고 보시면 됩니다.
[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/commit/?id=bbe097f092b0d13e9736bd2794d0ab24547d0e5d]

 WARNING: CPU: 0 PID: 0 at include/linux/usb/gadget.h:405
 ecm_do_notify+0x188/0x1a0
 Modules linked in:
 CPU: 0 PID: 0 Comm: swapper Not tainted 4.7.0+ #15
 Hardware name: Atmel SAMA5
 [<c010ccfc>] (unwind_backtrace) from [<c010a7ec>] (show_stack+0x10/0x14)
 [<c010a7ec>] (show_stack) from [<c0115c10>] (__warn+0xe4/0xfc)
 [<c0115c10>] (__warn) from [<c0115cd8>] (warn_slowpath_null+0x20/0x28)
 [<c0115cd8>] (warn_slowpath_null) from [<c04377ac>] (ecm_do_notify+0x188/0x1a0)
 [<c04377ac>] (ecm_do_notify) from [<c04379a4>] (ecm_set_alt+0x74/0x1ac)
 [<c04379a4>] (ecm_set_alt) from [<c042f74c>] (composite_setup+0xfc0/0x19f8)
 [<c042f74c>] (composite_setup) from [<c04356e8>] (usba_udc_irq+0x8f4/0xd9c)
 [<c04356e8>] (usba_udc_irq) from [<c013ec9c>] (handle_irq_event_percpu+0x9c/0x158)
 [<c013ec9c>] (handle_irq_event_percpu) from [<c013ed80>] (handle_irq_event+0x28/0x3c)
 [<c013ed80>] (handle_irq_event) from [<c01416d4>] (handle_fasteoi_irq+0xa0/0x168)
 [<c01416d4>] (handle_fasteoi_irq) from [<c013e3f8>] (generic_handle_irq+0x24/0x34)
 [<c013e3f8>] (generic_handle_irq) from [<c013e640>] (__handle_domain_irq+0x54/0xa8)
 [<c013e640>] (__handle_domain_irq) from [<c010b214>] (__irq_svc+0x54/0x70)
 [<c010b214>] (__irq_svc) from [<c0107eb0>] (arch_cpu_idle+0x38/0x3c)
 [<c0107eb0>] (arch_cpu_idle) from [<c0137300>] (cpu_startup_entry+0x9c/0xdc)
 [<c0137300>] (cpu_startup_entry) from [<c0900c40>] (start_kernel+0x354/0x360)
 [<c0900c40>] (start_kernel) from [<20008078>] (0x20008078)
 ---[ end trace e7cf9dcebf4815a6 ]---J6
 
in_interrupt 함수는 언제 쓰면 좋을까요? 샘플 패치를 하나 만들어 보았는데요. 
__rh_alloc() 함수가 Interrupt Context 즉 IRQ가 Trigger되어 호출이 될 경우, GFP_ATOMIC: atomic operation만으로 메모리 할당을 시도하고, 반대 Process Context인 경우 메모리 할당 실패 시 Sleep되어 성공할 때 까지 반복하는 GFP_KERNEL 옵션으로 메모리 할당를 수행하는 패치입니다. IRQ context는 빨리 해당 코드를 수행하고 종료시켜야 하는 에티켓을 가져야 해요. 그런데 페이지를 할당하려다가 잠들어버리면 낭패죠.
diff --git a/drivers/md/dm-region-hash.c b/drivers/md/dm-region-hash.c
 index b929fd5..1325a8a 100644
 --- a/drivers/md/dm-region-hash.c
 +++ b/drivers/md/dm-region-hash.c
 @@ -289,7 +289,12 @@ static struct dm_region *__rh_alloc(struct dm_region_hash *rh, region_t region)
  {
         struct dm_region *reg, *nreg;
 
 -       nreg = mempool_alloc(rh->region_pool, GFP_ATOMIC);
 +       gfp_t gfp_flag = GFP_KERNEL;
 +       if (in_interrupt()) {
 +               gfp_flag = GFP_ATOMIC;
 +       }
 +       nreg = mempool_alloc(flush_entry_pool, gfp_flag);
 +
         if (unlikely(!nreg))
                 nreg = kmalloc(sizeof(*nreg), GFP_NOIO | __GFP_NOFAIL);

 
이와 비슷한 개념으로 생성한 다른 패치도 있네요.
[https://patchwork.kernel.org/patch/3623051/]
diff --git a/drivers/gpu/drm/ast/ast_fb.c b/drivers/gpu/drm/ast/ast_fb.c
 index 66ecc16..d56d2bf 100644
 --- a/drivers/gpu/drm/ast/ast_fb.c
 +++ b/drivers/gpu/drm/ast/ast_fb.c
 @@ -64,7 +64,7 @@  static void ast_dirty_update(struct ast_fbdev *afbdev,
    * then the BO is being moved and we should
    * store up the damage until later.
    */
 - if (!in_interrupt())
 + if (!drm_can_sleep())
    ret = ast_bo_reserve(bo, true);
   if (ret) {
    if (ret != -EBUSY)
 
in_interrupt 매크로 구현부를 확인해보면, 실제 irq_count() 매크로를 호출하는데 preempt_count() 매크로로 리턴되는 값과
HARDIRQ_MASK | SOFTIRQ_MASK 마스크와 Oring Operation을 수행합니다.
#define in_interrupt()  (irq_count())
    
 #define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK
      | NMI_MASK))  
 
 SOFTIRQ_MASK: 0xff00,  SOFTIRQ_OFFSET: 0x100 
 HARDIRQ_MASK: 0xf0000, HARDIRQ_OFFSET: 0x10000
 
그럼, preempt_count의 정체는 뭐냐? 코드를 좀 더 살펴보면, 현재 구동 중인 함수 내에서 확인되는 스택 주소를 통해
스택 top address를 얻어 온 후 (struct thread_info *) 구조체의 preempt_count 멤버에서 얻어오는 값입니다.
static __always_inline int preempt_count(void)
 {
  return current_thread_info()->preempt_count;
 }
 
 [kernel/arch/arm/include/asm/thread_info.h]
 static inline struct thread_info *current_thread_info(void)
 {
  register unsigned long sp asm ("sp");
  return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));  // 0x1FFF = THREAD_SIZE - 1
 }

 
예를 들어 위에서 언급된 콜스택의 경우, 아래 계산식으로 preempt_count() 값을 얻어올 수 있습니다.
preempt_count이 0x00010002이군요.
  (struct thread_info*)(0xE359B908 & ~0x1fff) = 0xE359A000 -> (
     flags = 0x2,
     preempt_count = 0x00010002, // HARDIRQ_OFFSET
     addr_limit = 0xBF000000,
     task = 0xD0B5EA40,  //<<-- task descriptor
     exec_domain = 0xC1A1AF1C,
     cpu = 0x0,
     cpu_domain = 0x15,
 
 (where)
 커널 패닉 시의 Stack address: 0xE359B908  
 
그럼, struct thread_info.preempt_count 멤버에 HARDIRQ_OFFSET 비트를 어느 함수에서 설정할까요?
아래 함수 흐름에서 __irq_enter 매크로에서 HARDIRQ_OFFSET 비트를 설정하고 있음을 알 수 있습니다.
__irq_svc 
 -gic_handle_irq
 --__handle_domain_irq
 ---irq_enter
 ----__irq_enter
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
    bool lookup, struct pt_regs *regs)
 {
  struct pt_regs *old_regs = set_irq_regs(regs);
  unsigned int irq = hwirq;
  int ret = 0;
 
  irq_enter();  //<<--
 
 //skip
  if (unlikely(!irq || irq >= nr_irqs)) {
   ack_bad_irq(irq);
   ret = -EINVAL;
  } else {
   generic_handle_irq(irq); //<<-- ISR 호출
  }
 
  irq_exit();  //<<--
  set_irq_regs(old_regs);
  return ret;
 }

void irq_enter(void)
 {
  rcu_irq_enter();
  if (is_idle_task(current) && !in_interrupt()) {
   /*
    * Prevent raise_softirq from needlessly waking up ksoftirqd
    * here, as softirq will be serviced on return from interrupt.
    */
   local_bh_disable();
   tick_irq_enter();
   _local_bh_enable();
  }
 
  __irq_enter(); //<<--
 }

#define __irq_enter()     
  do {     
   account_irq_enter_time(current);
   preempt_count_add(HARDIRQ_OFFSET); //<<--
   trace_hardirq_enter();   
  } while (0)

 
struct thread_info.preempt_count 멤버에서 HARDIRQ_OFFSET bit는 아래 코드 흐름에서 Clear됩니다.
__handle_domain_irq
 (irq 처리 완료)
 -irq_exit
 --__irq_exit

#define __irq_exit()     
  do {     
   trace_hardirq_exit();   
   account_irq_exit_time(current); 
   preempt_count_sub(HARDIRQ_OFFSET);
  } while (0)
 
 /*

그럼 만약 IRQ context에서 schedule되면 어떻게 될까요?
걱정할 필요가 없어요. 커널이 친철하게 커널 패닉을 유발시키죠. __sched __schedule() 이란 함수가 실행될 때 schedule_debug() 함수를 호출하거든요.
static inline void schedule_debug(struct task_struct *prev)
{
#ifdef CONFIG_SCHED_STACK_END_CHECK
if (unlikely(task_stack_end_corrupted(prev)))
panic("corrupted stack end detected inside schedulern");
#endif
/*
 * Test if we are atomic. Since do_exit() needs to call into
 * schedule() atomically, we ignore that path. Otherwise whine
 * if we are scheduling when we should not.
 */
if (unlikely(in_atomic_preempt_off() && prev->state != TASK_DEAD)) //<<--
__schedule_bug(prev); //<<--
rcu_sleep_check();

profile_hit(SCHED_PROFILING, __builtin_return_address(0));

schedstat_inc(this_rq(), sched_count);
}

최근에 이렇게 커널 패닉이 난 이슈가 있었는데요.
커널 패닉 전 친철하게 콜 트레이스를 뿌려주세요. 아래 트레이스도 마찬가지로 IRQ context죠.
[12402.719933 / 05-13 00:44:07.295] CPU: 5 PID: 0 Comm: swapper/5 Tainted: P B W O 3.10.49-g72eb1a8 #1
[12402.719997 / 05-13 00:44:07.295] Call trace:
[12402.720151 / 05-13 00:44:07.295] [<ffffffc0002069d0>] dump_backtrace+0x0/0x270
[12402.720235 / 05-13 00:44:07.295] [<ffffffc000206c50>] show_stack+0x10/0x1c
[12402.720409 / 05-13 00:44:07.295] [<ffffffc000c90470>] dump_stack+0x1c/0x28
[12402.720663 / 05-13 00:44:07.295] [<ffffffc0002488a4>] __schedule_bug+0x44/0x60
[12402.720898 / 05-13 00:44:07.295] [<ffffffc000c96f20>] __schedule+0x90/0x750
[12402.720975 / 05-13 00:44:07.295] [<ffffffc000c97644>] schedule+0x64/0x70
[12402.721052 / 05-13 00:44:07.295] [<ffffffc000c95880>] schedule_timeout+0x210/0x258
[12402.721128 / 05-13 00:44:07.295] [<ffffffc000c96b70>] wait_for_common+0xe8/0x12c
[12402.721208 / 05-13 00:44:07.295] [<ffffffc000c96c1c>] wait_for_completion_interruptible_timeout+0xc/0x18
[12402.721297 / 05-13 00:44:07.295] [<ffffffc00083194c>] pompeii_vfe40_axi_halt+0x1cc/0x200
[12402.721372 / 05-13 00:44:07.295] [<ffffffc000823980>] pompeii_axi_halt+0x58/0x90
[12402.721445 / 05-13 00:44:07.295] [<ffffffc000823cf0>] pompeii_process_done_buf+0x338/0x550
[12402.721518 / 05-13 00:44:07.295] [<ffffffc000825488>] pompeii_process_axi_irq+0x2a4/0x4a4
[12402.721600 / 05-13 00:44:07.295] [<ffffffc000820610>] pompeii_do_tasklet+0x19c/0x1f0
[12402.721680 / 05-13 00:44:07.295] [<ffffffc0002246f0>] tasklet_action+0x90/0xf8
[12402.721755 / 05-13 00:44:07.295] [<ffffffc000223e2c>] __do_softirq+0x148/0x274
[12402.721829 / 05-13 00:44:07.295] [<ffffffc000223fec>] do_softirq+0x40/0x54
[12402.721903 / 05-13 00:44:07.295] [<ffffffc0002241f8>] irq_exit+0x70/0xa8
[12402.721981 / 05-13 00:44:07.295] [<ffffffc0002040d0>] handle_IRQ+0x80/0xa0
[12402.722057 / 05-13 00:44:07.295] [<ffffffc0002006ec>] gic_handle_irq+0x6c/0xb0



덧글

댓글 입력 영역