Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

91258
1323
114593


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

6.4 IRQ 스레드는 누가 언제 실행할까?

이번 절에서는 IRQ 스레드가 어떤 과정으로 실행하는지 살펴보겠습니다. 
IRQ 스레드는 크게 다음 단계로 실행합니다.
 1. 인터럽트 핸들러에서 IRQ_WAKE_THREAD 반환
 2. IRQ 스레드 깨움
 3. IRQ 스레드 핸들러 실행

IRQ 스레드를 실행하는 출발점은 인터럽트 핸들러가 IRQ_WAKE_THREAD 를 반환하는 시점입니다. 이 부분부터 IRQ 스레드를 어떤 과정으로 깨우는지 세부 동작을 점검하겠습니다.

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

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

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

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

IRQ 스레드 전체 흐름도는 다음과 같습니다.
 
[그림 6.5] IRQ 스레드 전체 실행 흐름도

이 흐름을 더 구체적으로 알기 위해 라즈비안 인터럽트 핸들러 코드와 함께 커널 코드를 분석하겠습니다. 

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

IRQ 스레드 실행의 출발점인 __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)
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)

2 번째 줄 코드를 보면 desc->action 멤버를 act에 저장합니다.

7번째 줄 코드를 전처리 코드에서 보면 다음과 같습니다. 전처리 코드를 보면 매크로를 풀어 실제 어떤 코드를 실행하는지 알려주니 빨리 코드를 읽을 수 있어 좋습니다.
[/kernel/irq/.tmp_handle.i]
 for (action = desc->action; action; action = action->next) {
irqreturn_t res;

다음 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인 경우 __irq_wake_thread() 함수를 호출해 IRQ 스레드를 깨웁니다. 

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

그런데 왜 이런 조건문을 추가했을까요? 이는 IRQ 스레드를 등록하지 않았는데 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환할 경우를 위한 예외 처리 코드입니다. 또는 IRQ 스레드를 생성할 때 IRQ 스레드 핸들러 함수를 제대로 등록했는지 확인하는 것입니다. 만약 IRQ 스레드 핸들 함수가 NULL이면 다음과 같이 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 스레드 핸들 함수를 지정하지 않았다는 의미입니다.

디바이스 드라이버 개발자가 실수로 request_threaded_irq() 함수를 호출할 때 IRQ 스레드 핸들 함수를 지정하지 않을 수 있습니다. 그러면 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 하면 생성하지도 않은 IRQ 스레드를 깨우려고 시도할 것입니다. 이전 소절에서 배웠다시피 IRQ 스레드 핸들러 함수를 등록해야 IRQ Thread를 생성하기 때문입니다. 이런 실수를 막기 위한 예외 처리 코드입니다.

이렇게 리눅스 커널 코드에서 예외 처리 루틴을 보면 지나치치 말고 주의 깊게 읽을 필요가 있습니다. 이렇게 유익한 정보를 얻을 수 있습니다.

IRQ_WAKE_THREAD 를 반환하는 인터럽트 핸들러 분석하기

이번엔 92번 인터럽트 핸들러인 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_WAKE_THREAD를 반환합니다. 

bcm2835_mmc_irq() 함수 가장 마지막 줄인 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_HANDLED을 반환하면 됩니다. IRQ 스레드를 깨우지 않고 인터럽트 핸들링을 마무리합니다.

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

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

이어서 인터럽트 핸들러가 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() 함수에 전달하는 action->thread인자는 다음과 같이IRQ 스레드의 태스크 디스크립터 주소입니다.
(struct irq_desc *) (struct irq_desc*)0xB008B300 
...
  (struct irqaction *) action = 0xBB4E6E40  
      (irq_handler_t) handler = 0x8061EC00 = bcm2835_mmc_irq,
...
      (irq_handler_t) thread_fn = 0x8061DCC4 = bcm2835_mmc_thread_irq,
      (struct task_struct *) thread = 0xBB516CC0

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

이 함수까지 인터럽트 컨텍스트 도중 처리하는 코드입니다. wake_up_process() 함수를 호출해서 IRQ 스레드를 깨우면 스케줄링을 실행하면 프로세스 레벨에서 IRQ 스레드가 실행합니다. 

다음은 IRQ 스레드 전체 흐름도입니다.
 
   [그림 6.6] IRQ 스레드 전체 실행 흐름도에서 IRQ 스레드를 깨우는 부분

위 흐름도에서 검은색으로된 부분이 이번 소절에서 살펴본 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 서비스 실행 횟수 확인




핑백

덧글

댓글 입력 영역