Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

83235
1036
103653


[Linux][Kernel] 인터럽트(Interrupt) - request_threaded_irq overview(1) [Linux][Kernel] IRQ(Interrupt)

리눅스 커널 인터럽트 개념 중에 top/bottom half란 용어를 많이 들어봤죠?

하드웨어적으로 인터럽트가 처리될 때는 되도록 실시간으로 처리할 데이터만 빨리 처리하고,
빨리 처리해도 안되는 동작은 쓰레드 레벨로 수행한다는 거죠.

수 없이 이런 글을 읽어봤는데, 사실 인터럽트 서비스 루틴에서 발생하는 수 많은 이슈를 대응하기 전까지는 개념이머리 속에 잘 들어오지 않더라구요. 그래서 이번 시간에 좀 좀 정리를 해보려고 해요.

리눅스 커널에서 bottom half로 처리되는 루틴은 크게 세 가지가 있어요.
1> softirq
2> workqueue
3> irq_thread

이번 시간에는 irq_thread에 대해서 touch 드라이버를 예를 들어서 분석을 해보려구요.
touch의 IRQ 관련 루틴은 두 조각으로 크게 나눌 수가 있어요.

인터럽트가 들어왔을 때 즉, IRQ Context에서는 빠른 처리를 해야 하는 코드, 조금 나중에 처리해도 되는 루틴은 IRQ thread 배치하는 거죠

IRQ Context 즉 인터럽트가 올라왔을 때 터치 이벤트를 input event로 유저 공간에 올려주는 짓거리를 하면 어떻게 될까요?
시스템이 이상한 짓 거리를 하면서 와치독으로 리셋 될 꺼에요.

이렇게요. request_irq()를 통해 ISR에서 input event를 전달했더니, 커널은 아래와 같은 유언을 남기고 뻗어 버렸어요.
[   21.719319 01-01 10:20:37.054] [callstack mxt_interrupt,2449] task[InputReader]========= 
[   21.719382 01-01 10:20:37.054] BUG: scheduling while atomic: InputReader/1039/0x00010001
[   21.719417 01-01 10:20:37.054] [<c00169a8>] (unwind_backtrace+0x0/0x144) from [<c083e0e8>] (dump_stack+0x20/0x24)
[   21.719432 01-01 10:20:37.054] [<c083e0e8>] (dump_stack+0x20/0x24) from [<c083edfc>] (__schedule_bug+0x50/0x5c)
[   21.719444 01-01 10:20:37.054] [<c083edfc>] (__schedule_bug+0x50/0x5c) from [<c0845b54>] (__schedule+0x7c4/0x890)
[   21.719455 01-01 10:20:37.054] [<c0845b54>] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
[   21.719468 01-01 10:20:37.054] [<c0845d70>] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
[   21.719480 01-01 10:20:37.054] [<c0843bc0>] (schedule_timeout+0x190/0x33c) from [<c08450b4>] (wait_for_common+0xb8/0x15c)
[   21.719491 01-01 10:20:37.054] [<c08450b4>] (wait_for_common+0xb8/0x15c) from [<c0845198>] (wait_for_completion_timeout+0x1c/0x20)
[   21.719504 01-01 10:20:37.054] [<c0845198>] (wait_for_completion_timeout+0x1c/0x20) from [<c04dbd88>] (tegra_i2c_xfer_msg+0x380/0x958)
[   21.719517 01-01 10:20:37.054] [<c04dbd88>] (tegra_i2c_xfer_msg+0x380/0x958) from [<c04dc674>] (tegra_i2c_xfer+0x314/0x438)
[   21.719531 01-01 10:20:37.054] [<c04dc674>] (tegra_i2c_xfer+0x314/0x438) from [<c04d7bb8>] (i2c_transfer+0xc4/0x128)
[   21.719546 01-01 10:20:37.054] [<c04d7bb8>] (i2c_transfer+0xc4/0x128) from [<c04b3174>] (__mxt_read_reg+0x70/0xc8)
[   21.719560 01-01 10:20:37.054] [<c04b3174>] (__mxt_read_reg+0x70/0xc8) from [<c04b6f60>] (mxt_read_and_process_messages+0x58/0x1648)
[   21.719572 01-01 10:20:37.054] [<c04b6f60>] (mxt_read_and_process_messages+0x58/0x1648) from [<c04b85c8>] (mxt_interrupt+0x78/0x144)
[   21.719588 01-01 10:20:37.054] [<c04b85c8>] (mxt_interrupt+0x78/0x144) from [<c00e8000>] (handle_irq_event_percpu+0x88/0x2ec)
[   21.719601 01-01 10:20:37.054] [<c00e8000>] (handle_irq_event_percpu+0x88/0x2ec) from [<c00e82b0>] (handle_irq_event+0x4c/0x6c)
[   21.719614 01-01 10:20:37.054] [<c00e82b0>] (handle_irq_event+0x4c/0x6c) from [<c00ead98>] (handle_level_irq+0xbc/0x118)
[   21.719626 01-01 10:20:37.054] [<c00ead98>] (handle_level_irq+0xbc/0x118) from [<c00e7780>] (generic_handle_irq+0x3c/0x50)
[   21.719642 01-01 10:20:37.054] [<c00e7780>] (generic_handle_irq+0x3c/0x50) from [<c0311138>] (tegra_gpio_irq_handler+0xa8/0x124)
[   21.719655 01-01 10:20:37.054] [<c0311138>] (tegra_gpio_irq_handler+0xa8/0x124) from [<c00e7780>] (generic_handle_irq+0x3c/0x50)
[   21.719669 01-01 10:20:37.054] [<c00e7780>] (generic_handle_irq+0x3c/0x50) from [<c000fa04>] (handle_IRQ+0x5c/0xbc)
[   21.719682 01-01 10:20:37.054] [<c000fa04>] (handle_IRQ+0x5c/0xbc) from [<c0008504>] (gic_handle_irq+0x34/0x68)
[   21.719694 01-01 10:20:37.054] [<c0008504>] (gic_handle_irq+0x34/0x68) from [<c000ec40>] (__irq_svc+0x40/0x70)


혹시 다니는 회사가 특근비 많이 주면 한번 이렇게 코드를 짜보세요. 이슈 빵빵 터질 께 분명하거든요. 주말에 나와서 실컷 디버깅할 수 있을 꺼에요. 1달 동안 디버깅 하다가 input event는 irq thread에서 처리하는 코드로 수정하시면 되요.

probe로 등록된 touch_drvier_probe() 함수에서 아래 타입으로 등록을 합니다.
인터럽트가 떴을 때 호출되는 touch_irq_handler 핸들러와 irq_thread로 호출되는 함수 touch_irq_thread를 등록하죠.
인터럽트 핸들러도 전달되는 device id 포인터는 struct touch_core_data *ts로 지정하네요.
static int touch_drvier_probe(struct platform_device *pdev)
{
struct touch_core_data *ts;
int ret;

ret = request_threaded_irq(irq, touch_irq_handler, touch_irq_thread
ts->irqflags | IRQF_ONESHOT, "touch", ts);

그럼, 인터럽트 descriptor 변수 중 action(struct irq_desc->action)에 아래와 같이 등록을 하죠.
참고로, 인터럽트 descriptor는 struct irq_desc 타입인데요. 각각 인터럽트에 대한 정보를 담고 있어요.
page descriptor와 비슷하죠.
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
 irq_handler_t thread_fn, unsigned long irqflags,
 const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
// ... 생략 ...
action->handler = handler;  // touch_irq_handler
action->thread_fn = thread_fn;  // touch_irq_thread
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;  // ts

이제, touch_irq_thread가 언제 어떻게 호출되는지 살펴볼까요?
우선 해당 IRQ가 Trigger되어야 하구요. IRQ가 Trigger되면 아래 함수 handle_irq_event_percpu가 호출되거든요.

아래 디버깅 정보는 touch_irq_handler() 함수에 stack trace를 걸고 받아 본 ftrace 로그 중 일부인데요.
 => touch_irq_handler
 => handle_irq_event_percpu
 => handle_irq_event
 => handle_fasteoi_irq
 => generic_handle_irq
 => __handle_domain_irq
 => gic_handle_irq
 => __irq_svc
 => lpm_cpuidle_enter
 => lpm_cpuidle_enter
 => cpuidle_enter_state
 => cpuidle_enter
 => cpu_startup_entry
 => secondary_start_kernel
 => 

아래 코드의 [1] action->handler(irq, action->dev_id); 코드에서 해당 ISR인  touch_irq_handler이 호출되구요.
리턴 값이 [2] IRQ_WAKE_THREAD이면  __irq_wake_thread() 함수 호출로 IRQ Thread를 Runqueue에 넣어줘서 쓰레드 레벨에서 실행되게 해요.
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;

do {
irqreturn_t res;

trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);  //<<--[1]
trace_irq_handler_exit(irq, action, res);

if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interruptsn",
      irq, action->handler))
local_irq_disable();

switch (res) {
case IRQ_WAKE_THREAD:
/*
 * Catch drivers which return WAKE_THREAD but
 * did not set up a thread function
 */
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}

__irq_wake_thread(desc, action);  //<<--[2]

touch_irq_handler 코드를 잠깐 보면 역시나 IRQ_WAKE_THREAD 리턴 하네요.
흠,  IRQ_HANDLED로 리턴할 때는 Suspend 상태라 터치 이벤트를 처리하고 싶지 않은가 봐요.
코드를 보고 유추할 수 있는 점은 이 터치 드라이버는 Platform Device로 등록되어 처리되는 것 같네요.
irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
struct touch_core_data *ts = (struct touch_core_data *) dev_id;

if (atomic_read(&ts->state.pm) >= DEV_PM_SUSPEND) {
TOUCH_I("interrupt in suspend[%d]n",
atomic_read(&ts->state.pm));
atomic_set(&ts->state.pm, DEV_PM_SUSPEND_IRQ);
wake_lock_timeout(&ts->lpwg_wake_lock, msecs_to_jiffies(1000));
return IRQ_HANDLED;
    }
return IRQ_WAKE_THREAD;
}


역시 wake_up_process()를 호출해서 IRQ 쓰레드를 Runqueue에 넣어주죠. 실행 되라구요.
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
if (action->thread->flags & PF_EXITING)
return;
if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
return;
desc->threads_oneshot |= action->thread_mask;
atomic_inc(&desc->threads_active);

wake_up_process(action->thread);  //<<--
}

그럼 정확히 어느 함수에서 IRQ 쓰레드를 Runqueue에 넣어 주냐구요?
아래 함수에요. 친절히 런큐에 넣어주고 프로세스 상태를 TASK_RUNNING로 바꾸어 주네요.
함수 흐름은? (wake_up_process -> try_to_wake_up -> ttwu_remote -> ttwu_do_wakeup) 이렇죠.
static void
ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
check_preempt_curr(rq, p, wake_flags);
trace_sched_wakeup(p, true);

p->state = TASK_RUNNING;
이제 irq_thread가 실행될 차례에요. 
[1] 루틴과 같이 handler_fn에 irq_thread_fn 함수로 함수 포인터를 지정하고,
[2] 루틴에서 해당 touch_irq_thread irq_thread 핸들 함수가 호출되는 거죠. 
static int irq_thread(void *data)
{
struct callback_head on_exit_work;
struct irqaction *action = data;
struct irq_desc *desc = irq_to_desc(action->irq);
irqreturn_t (*handler_fn)(struct irq_desc *desc,
struct irqaction *action);

if (force_irqthreads && test_bit(IRQTF_FORCED_THREAD,
&action->thread_flags))
handler_fn = irq_forced_thread_fn;
else
handler_fn = irq_thread_fn;  //<<--[1]

init_task_work(&on_exit_work, irq_thread_dtor);
task_work_add(current, &on_exit_work, false);

irq_thread_check_affinity(desc, action);

while (!irq_wait_for_interrupt(action)) {
irqreturn_t action_ret;

irq_thread_check_affinity(desc, action);

action_ret = handler_fn(desc, action);  //<<--[2]

콜 트레이스는 어떤 흐름일까요? T32로 봤더니 아래와 같네요.
-011|owner_running(inline)
-011|mutex_spin_on_owner()
-012|__mutex_lock_common(inline)
-012|__mutex_lock_slowpath()
-013|current_thread_info(inline)
-013|mutex_set_owner(inline)
-013|mutex_lock()
-014|himax_bus_read()
-015|himax_read_event_stack()
-016|himax_touch_get()
-017|himax_irq_handler()
-018|touch_irq_thread()
-019|irq_thread_fn(inline)
-019|irq_thread()
-020|kthread()
-021|ret_from_fork(asm)
-022|ret_fast_syscall(asm)

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

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

# Reference (인터럽트 처리)



#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview


덧글

댓글 입력 영역