ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

201239
1625
172598


[리눅스커널] 인터럽트 후반부 처리: IRQ 스레드를 깨우는 코드 분석 6. 인터럽트 후반부 처리


IRQ 스레드는 생성된 후 다음과 같은 동작을 계속 반복해 수행합니다.   

1. 인터럽트 핸들러에서 IRQ_WAKE_THREAD 반환
2. IRQ 스레드 깨움
3. IRQ 스레드 핸들러 실행

그렇다면 IRQ 스레드는 누가 언제 실행을 시작할까요?
   
     IRQ 스레드를 실행하는 출발점은 인터럽트 핸들러가 IRQ_WAKE_THREAD 를 반환하는 
    시점입니다. 
이번 절에서는 이 부분부터 IRQ 스레드를 어떤 과정으로 깨우는지 살펴보겠습니다. 


IRQ 스레드를 깨우는 코드 분석

다음 질문과 함께 IRQ 스레드를 실행하는 과정을 살펴보겠습니다. 

    IRQ 스레드 실행의 출발점은 어디일까?

인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하는 시점이 IRQ 스레드를 실행하는 출발점입니다. 인터럽트가 발생하면 인터럽트 핸들러가 실행됩니다. 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하면 해당 IRQ 스레드를 깨웁니다.

라즈베리파이를 예로 들어 볼까요? 86번 “mmc1” 인터럽트 핸들러인 bcm2835_mmc_irq() 함수에서 IRQ_WAKE_THREAD를 반환하면 irq/86-mmc1이란 IRQ 스레드를 깨웁니다. IRQ 스레드를 깨우면 스케줄러는 우선 순위를 고려한 후 IRQ 스레드를 실행합니다. 프로세스 컨택스트에서 IRQ 스레드가 실행하는 것입니다. 

IRQ 스레드 전체 흐름도 파악해보기

IRQ 스레드 전체 흐름도를 보면서 IRQ 스레드 실행 시점에 대해 배워볼까요?



 
[그림 6.5] IRQ 스레드 전체 실행 흐름도

위 그림 왼쪽 부분을 보면 '인터럽트 핸들링'으로 표시된 박스에 커널 함수가 보일 것입니다. 

    인터럽트가 발생한 후 인터럽트 벡터인 __irq_svc 레이블로부터 커널 내부 함수의 실행 
     흐름입니다.

이어서 가운데 '인터럽트 핸들러'로 표시된 부분은 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하는 동작을 나타냅니다. 화살표 가장 윗 부분을 눈으로 따라가보면 '__irq_wake_thread'라고 표시된 부분이 보일 것입니다. 이는 다음 동작을 표시합니다.

    IRQ 스레드를 깨운다.

이렇게 커널에게 IRQ 스레드 실행 요청을 하면 스케줄러는 우선 순위를 고려해 IRQ 스레드를 실행합니다. 그런데 IRQ 스레드는 RT(Real-Time) 스레드라 일반적인 상황에서 바로 실행을 시작합니다.

이번에는 가장 오른쪽 'IRQ 스레드'로 표시된 박스를 보겠습니다. 이 부분은 IRQ 스레드가 실행하는 동작을 표현합니다. 박스 아랫 부분은 IRQ 스레드가 실행하는 함수 호출 흐름입니다. 

    IRQ 스레드 핸들인 irq_thread() 함수가 호출되고 이어서 IRQ 스레드 핸들러 함수가 
   호출됩니다.

이전에 irq/86-mmc1 IRQ 스레드가 생성되는 과정을 살펴봤으니 이번에는 라즈베이파이에서 86번 인터럽트가 발생하고 나서 irq/86-mmc1 IRQ 스레드가 실행하는 과정까지 살펴보겠습니다.

IRQ 스레드 실행의 출발점인 __handle_irq_event_percpu() 함수 분석하기

IRQ 스레드 실행의 출발점은 인터럽트 핸들러가 IRQ_WAKE_THREAD 를 반환하는 시점이라고 설명을 드렸습니다. 커널 내부 코드를 보면서 인터럽트 핸들러가 IRQ_WAKE_THREAD 를 반환하면 어떻게 IRQ 스레드를 깨우는지 살펴봅시다.

먼저 다음 코드를 함께 볼까요?

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c 
1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
2 {
3 irqreturn_t retval = IRQ_NONE;
4 unsigned int irq = desc->irq_data.irq;
5 struct irqaction *action;
6
7 for_each_action_of_desc(desc, action) {
8 irqreturn_t res;
9
10 trace_irq_handler_entry(irq, action);
11 res = action->handler(irq, action->dev_id);   
12 trace_irq_handler_exit(irq, action, res);
13
14 if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
15       irq, action->handler))
16 local_irq_disable();
17
18 switch (res) {
19 case IRQ_WAKE_THREAD:
20
21 if (unlikely(!action->thread_fn)) {
22 warn_no_thread(irq, action);
23 break;
24 }
25
26 __irq_wake_thread(desc, action);   


우리는 __handle_irq_event_percpu() 함수의 11번째 줄 코드에서 인터럽트 핸들러를 호출한다고 배웠습니다.  __handle_irq_event_percpu() 함수 첫 번째 인자는 struct irq_desc 구조체인 desc입니다. struct irq_desc 구조체는 인터럽트 디스크립터라고 부릅니다.

7번째 코드를 보면 인터럽트 디스크립터 필드 중 struct irqaction 구조체 action 필드가 저장한 주소를 *action이란 지역변수에 전달합니다.

1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
2 {
3 irqreturn_t retval = IRQ_NONE;
4 unsigned int irq = desc->irq_data.irq;
5 struct irqaction *action;
6
7 for_each_action_of_desc(desc, action) {
8 irqreturn_t res;


for_each_action_of_desc 매크로 코드를 선언부는 다음과 같습니다. 
1 #define for_each_action_of_desc(desc, act) \
2 for (act = desc->action; act; act = act->next)

1번째 줄 코드는 2번째 줄 코드로 치환됩니다. 2 번째 줄 코드는 desc->action 필드를 act에 저장하는 동작입니다.

다음 11~26번째 줄 코드를 보겠습니다.

11 res = action->handler(irq, action->dev_id);
...
18 switch (res) {
19 case IRQ_WAKE_THREAD:
20
21 if (unlikely(!action->thread_fn)) {
22 warn_no_thread(irq, action);
23 break;
24 }
25
26 __irq_wake_thread(desc, action);

11번째 줄 코드를 보면 action->handler 함수 포인터로 인터럽트 핸들러를 호출합니다. 그리고 res 지역 변수는 인터럽트 핸들러가 반환하는 값을 저장합니다. 18번째 줄 코드에서 res 값을 기준으로 switch~case 문을 처리합니다.

그런데 res가 IRQ_WAKE_THREAD 이면 26번째 줄 코드를 실행합니다.   

    __irq_wake_thread() 함수를 호출해 IRQ 스레드를 깨운다. 

이 부분이 IRQ 스레드의 실행하는 출발점입니다.

21번째 줄 코드는 인터럽트 디스크립터로 action->thread_fn 함수 포인터가 NULL인지를 체크하는 조건문입니다. IRQ 스레드를 등록할 때 IRQ 스레드 핸들러 함수를 지정했으면 action->thread_fn 필드는 NULL이 아닐 것입니다. 대신 IRQ 스레드 핸들러 함수 주소를 저장할 것입니다. 

이런 조건문을 추가한 이유는 무엇일까요? 다음과 같은 예외 처리를 하기 위해서입니다. 

IRQ 스레드를 등록하지 않았는데 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환할 경우
IRQ 스레드를 생성할 때 IRQ 스레드 핸들러 함수를 제대로 등록했는지 확인

디바이스 드라이버 개발자가 실수로 request_threaded_irq() 함수를 호출할 때 IRQ 스레드 핸들러 함수를 지정하지 않을 수 있습니다. 그러면 다음과 같은 오동작을 수행할 것입니다. 

    인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하면 생성하지도 않은 IRQ 스레드를 
     깨우려고 시도할 것이다. 

이전 소절에서 배웠다시피 IRQ 스레드 핸들러 함수를 등록해야 IRQ 스레드를 생성하기 때문입니다. 이런 실수를 막기 위한 예외 처리 코드입니다.

만약 IRQ 스레드 핸들러 함수를 지정하지 않았다면 다음과 같이 warn_no_thread() 함수를 호출해서 친절하게 커널 로그로 경고 메시지를 출력합니다. 이 후 break 문을 실행해서 switch~case 문을 종료합니다.


warn_no_thread() 함수 코드를 보면 다음 6~7 번째 줄 메시지와 같이 인터럽트 이름과 번호를 커널 로그로 출력합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c 
1 static void warn_no_thread(unsigned int irq, struct irqaction *action)
2 {
3 if (test_and_set_bit(IRQTF_WARNED, &action->thread_flags))
4 return;
5
6 printk(KERN_WARNING "IRQ %d device %s returned IRQ_WAKE_THREAD "
7        "but no thread function available.", irq, action->name);
8 }

7 번째 줄 "but no thread function available." 메시지는 IRQ 스레드 핸들 함수를 지정하지 않았다는 의미입니다.


IRQ_WAKE_THREAD 를 반환하는 인터럽트 핸들러 분석하기
인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하면 IRQ 스레드를 깨우는 코드를 분석했습니다. 이번엔 86번 인터럽트 핸들러인 bcm2835_mmc_irq() 함수 어느 부분에서 IRQ_WAKE_THREAD를 반환하는지 살펴보겠습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/mmc/host/bcm2835-mmc.c 
1 static irqreturn_t bcm2835_mmc_irq(int irq, void *dev_id)
2 {
3 irqreturn_t result = IRQ_NONE;
4 struct bcm2835_host *host = dev_id;
5 u32 intmask, mask, unexpected = 0;
6 int max_loops = 16;
7
8 spin_lock(&host->lock);
9
10 intmask = bcm2835_mmc_readl(host, SDHCI_INT_STATUS);
11
...
12 do {
13 /* Clear selected interrupts. */
14 mask = intmask & (SDHCI_INT_CMD_MASK | SDHCI_INT_DATA_MASK |
15   SDHCI_INT_BUS_POWER);
16 bcm2835_mmc_writel(host, mask, SDHCI_INT_STATUS, 8);
...
17 if (intmask & SDHCI_INT_CARD_INT) {
18 bcm2835_mmc_enable_sdio_irq_nolock(host, false);
19 host->thread_isr |= SDHCI_INT_CARD_INT;
20 result = IRQ_WAKE_THREAD;
21 }
...
22 out:
...
23 return result;
24 }

20번째 코드를 보면 result에 IRQ_WAKE_THREAD 을 저장합니다. 이 코드는 다음과 같은 의도가 있습니다.

    IRQ 스레드를 깨워서 후속 처리를 해야겠다.

이후 22번째 줄 코드와 같이 out 레이블을 실행한 후 23번째 코드를 보면 이 result 변수를 반환합니다.

이렇게 인터럽트 핸들러인 bcm2835_mmc_irq() 함수가 IRQ_WAKE_THREAD를 반환하면 __handle_irq_event_percpu() 함수에서 __irq_wake_thread() 함수를 호출하게 됩니다. 

__handle_irq_event_percpu() 함수에서 이 동작을 수행하는 코드만 모아서 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c 
1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
...
11 res = action->handler(irq, action->dev_id); /* IRQ_WAKE_THREAD 반환 */
12 trace_irq_handler_exit(irq, action, res);
13
...
17
18 switch (res) {
19 case IRQ_WAKE_THREAD:
20
...
26 __irq_wake_thread(desc, action);

11 번 째 줄 코드에서 인터럽트 핸들러가 IRQ_WAKE_THREAD를 반환하면 26번째 줄 코드를 실행합니다.

보통 인터럽트 핸들러만 수행하면 후속 동작이 없을 것이라 예상하고 코드를 읽습니다. 하지만 인터럽트 핸들러가 IRQ_WAKE_THREAD를 반환하는 코드를 보면 다음과 같은 사실을 떠 올릴 수 있습니다.  

    해당 인터럽트는 IRQ 스레드 핸들러를 등록해 IRQ 스레드가 인터럽트 후반부 처리를 
    한다.

이렇게 코드를 예측하면서 분석하면 전체 구조를 더 빨리 이해할 수 있습니다.

여기서 한 가지 생각해 볼 점이 있습니다.  

    인터럽트가 핸들러가 IRQ 스레드를 깨우고 싶지 않을 때도 있지 않을까?

인터럽트 핸들러에서 해야 할 일을 마무리한 상황입니다. 즉, ‘인터럽트 후반부’를 처리하지 않아도 되는 상황입니다. 

    인터럽트 핸들러에서 IRQ_HANDLED을 반환하면 됩니다. 

이때 IRQ 스레드를 깨우지 않고 인터럽트 핸들링을 마무리합니다.

이 방식으로 인터럽트를 제어하면 조금 더 유연하게 인터럽트 후반부를 처리하는 드라이버 코드를 작성할 수 있습니다.

IRQ 스레드를 깨우는 irq_wake_thread() 함수 분석하기 

irq_wake_thread() 함수를 호출하면 IRQ 스레드를 깨웁니다. 이어서 인터럽트 핸들러가 IRQ_WAKE_THREAD를 반환하면 호출하는 irq_wake_thread() 함수를 분석합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c 
1 void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
2 {
...
3 atomic_inc(&desc->threads_active);
4 wake_up_process(action->thread);
5 }

4 번째 코드는 다음 동작을 수행합니다. 

      wake_up_process() 함수를 호출해서 IRQ 스레드를 깨운다. 

우리는 "wake_up_process() 함수를 호출하면 지정한 프로세스를 깨운다." 고 알고 있습니다.

struct irqaction 구조체 thread 필드는 IRQ 스레드의 태스크 디스크립터를 저장합니다.
소스 분석으로 이 내용을 잘 이해 못할 수 있으니 86번 인터럽트에 해당하는 인터럽트 디스크립터를 세부 필드를 같이 분석해볼까요?   

01 (struct irq_desc*)0xB008B300 
...
02  (struct irqaction *) action = 0xBB4E6E40  
03      (irq_handler_t) handler = 0x8061EC00 = bcm2835_mmc_irq,
...
04      (irq_handler_t) thread_fn = 0x8061DCC4 = bcm2835_mmc_thread_irq,
05      (struct task_struct *) thread = 0xBB516CC0
  
01번째 줄을 보면 인터럽트 디스크립터 주소가 0xB008B300임을 알 수 있습니다. 이어서 02번째 줄에 struct irq_desc 구조체 action 필드가 보입니다.  여기서 action 필드의 타입은 struct irqaction입니다. 

03~05번째 줄은 struct irqaction 구조체 필드 정보입니다. 이 중 05번째 줄을 보면 thread 필드가 있고 해당 구조체는 struct task_struct 입니다. 
 
 위에서 본 인터럽트 디스크립터로 __irq_wake_thread() 함수를 호출해 IRQ 스레드를 깨우면 다음과 같이 동작하게 됩니다.
 
     wake_up_process() 함수 인자로 struct task_struct 구조체인 0xBB516CC0 
    주소를 전달한다.


wake_up_process() 함수 선언부를 확인해봐도 태스크 디스크립터를 인자로 받는다는 사실을 알 수 있습니다.
extern int wake_up_process(struct task_struct *tsk);


다음 IRQ 스레드 전체 흐름도를 보면서 이번 소절에 배운 내용을 정리해볼까요?
 
 
   [그림 6.6] IRQ 스레드 전체 실행 흐름도에서 IRQ 스레드를 깨우는 부분

위 그림 가운데에서 검은색으로된 부분이 IRQ 스레드를 깨우는 과정입니다.

인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환한다.
irq_wake_thread() 함수를 호출해 IRQ 스레드를 깨운다.

다음 소절에서는 IRQ 스레드를 깨우면 어떤 흐름으로 IRQ 스레드가 실행하는지 살펴보겠습니다.
 

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


# Reference 인터럽트 후반부 처리








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




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


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2





핑백

덧글

댓글 입력 영역