IRQ 스레드란
리눅스 커널을 익히는 과정에서 만나는 걸림돌 중 하나가 어려운 용어입니다. 어려운 개념을 낯선 용어로 설명하니 이해하기 어렵죠. IRQ Thread의 의미를 알기 전에 IRQ란 용어부터 알아볼까요? IRQ는 Interrupt Request의 약자로 하드웨어에서 발생한 인터럽트를 처리 한다는 의미입니다. 조금 더 구체적으로 인터럽트 발생 후 인터럽트 핸들러까지 처리하는 흐름입니다. 

IRQ Thread란 뭘까요? 인터럽트 핸들러에서는 처리하면 오래 걸리는 일을 수행하는 프로세스입니다. 인터럽트 후반부 처리를 위한 인터럽트 처리 전용 프로세스입니다. 리눅스 커널에서는 IRQ Thread를 irq_thread라고도 합니다. 리눅스 커널 커뮤니티에서는 threaded IRQ 방식이라고도 합니다. 용어는 달라도 같은 의미입니다. 

IRQ Thread 기법은 인터럽트 후반부 처리를 IRQ Thread에서 수행하는 방식을 의미합니다. 이제 용어를 간단히 정리했으니 IRQ Thread에 대해 조금 더 알아볼게요. 

IRQ 스레드 확인하기
라즈베리안에서는 IRQ Thread를 어떻게 확인할 수 있을까요? ps -ely 명령어를 입력하면 프로세스 목록을 볼 수 있습니다. 
root@raspberrypi:/home/pi/dev_raspberrian# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  6012  6750 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
//...
S     0    64     2  0  70 -10     0     0 down_i ?        00:00:00 SMIO
S     0    65     2  0   9   -     0     0 irq_th ?        00:00:00 irq/92-mmc1
I     0    66     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:3

위 목록에서 irq/92-mmc1가 보이죠. 이 프로세스가 IRQ Thread입니다. 커널에서 지어준 이름인데 다음 규칙에 따라 이름을 짓습니다.
"irq/인터럽트 번호-인터럽트 이름"

irq/92-mmc1프로세스는 위 규칙에 따라 어떻게 해석하면 될까요? mmc1이란 이름의 92번째 인터럽트를 처리하는 IRQ Thread라 할 수 있습니다. 라즈베리안에서는 위와 같이 1개의 IRQ Thread만 볼 수 있는데, 다른 리눅스 시스템에서는 보통 10개 이상 IRQ Thread를 확인할 수 있습니다.

이번에는 다른 리눅스 시스템에서 IRQ Thread를 확인해볼까요? 
다음 로그는 안드로이드를 탑재한 Z5 compact 제품(Snapdragon 810)의 리눅스 개발자 커뮤니티에서 공유된 로그입니다.
1  root      14    2     0      0     worker_thr 0000000000 S kworker/1:0H
2  root      15    2     0      0     smpboot_th 0000000000 S migration/2
3  root      16    2     0      0     smpboot_th 0000000000 S ksoftirqd/2
//...
4  root      36    2     0      0     rescuer_th 0000000000 S rpm-smd
5  root      38    2     0      0     irq_thread 0000000000 S irq/48-cpr
6  root      39    2     0      0     irq_thread 0000000000 S irq/51-cpr
//...
7  root      199   2     0      0     irq_thread 0000000000 S irq/212-msm_dwc
//...
8  root      65    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
9  root      66    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
10 root      67    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
11 root      68    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
12 root      69    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
13 root      70    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
14 root      71    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
15 root      72    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
16 root      73    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
17 root      74    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
18 root      75    2     0      0     irq_thread 0000000000 S irq/261-msm_iom

ps 명령어로 출력한 전체 프로세스 중 IRQ Thread만 가려냈는데요. irq_thread 타입으로 볼드체로 표시한 프로세스가 IRQ Thread입니다. IRQ Thread가 14개나 있네요.

위 로그 중에 “irq/212-msm_dwc”란 IRQ Thread를 해석하면 msm_dwc란 이름의 212번째 인터럽트를 처리하는 IRQ Thread라고 볼 수 있겠죠. 
7 root      199   2     0      0     irq_thread 0000000000 S irq/212-msm_dwc

이렇게 상용 리눅스 시스템의 IRQ Thread 개수를 소개한 이유는 여러분이 IRQ Thread는 1개 밖에 없다고 착각할 수 있기 때문입니다. 왜냐면, 라즈베리안에서 IRQ Thread가 1개 밖에 없으니까요. 

이렇게 라즈베리안에 비해 상용 안드로이드 리눅스 디바이스에서는 IRQ Thread가 더 많은 것 같습니다. IRQ Thread가 더 많으면 더 좋은 시스템일까요? 그렇지는 않습니다. 이는 시스템에 인터럽트를 어떻게 설계하고 구성했는지에 따라 다릅니다. 

어떤 인터럽트가 발생하면 인터럽트 핸들러 이후에 실행되는 IRQ Thread가 있다는 것은 뭘 의미할까요? 해당 인터럽트가 발생하면 소프트웨어적으로 할 일이 많다고 봐야겠죠. 인터럽트가 발생했을 때 바로 해야 하는 일은 인터럽트 핸들러에서 처리하고, 조금 후 프로세스 레벨에서 해도 되는 일은 IRQ Thread에서 수행하는 것입니다.

IRQ Thread를 생성하기 위해서는 어떻게 해야 하죠? 방법은 간단합니다. 인터럽트 핸들러를 설정하는 request_irq 함수 대신 request_threaded_irq 을 호출하면 됩니다.

request_threaded_irq 함수의 원형은 다음과 같은데요. 세 번째 파라미터인 thread_fn에 IRQ Thread가 실행되면 호출할 핸들러만 지정해주면 됩니다. 그러면 커널에서 인터럽트 이름과 번호 정보를 바탕으로 IRQ Thread를 생성해 줍니다.
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
     irq_handler_t thread_fn,
     unsigned long flags, const char *name, void *dev);

여기서 Top Half는 인터럽트 핸들러인 handler 그리고 Bottom Half는 IRQ Thread가 수행되면 수행하는 핸들러 함수인 thread_fn 라고 할 수 있겠죠.

실제 request_threaded_irq를 호출하는 코드를 보면서 이 내용을 함께 알아볼까요? 다음 코드는 다른 리눅스 시스템에서 USB 드라이버 설정 코드입니다. 
1 static int dwc3_gadget_start(struct usb_gadget *g,
struct usb_gadget_driver *driver)
3 {
4 struct dwc3 *dwc = gadget_to_dwc(g);
5 unsigned long flags;
6 int ret = 0;
7 int irq;
8
9 irq = dwc->irq_gadget;
10 ret = request_threaded_irq(irq, dwc3_interruptdwc3_thread_interrupt,
11 IRQF_SHARED, "dwc3", dwc->ev_buf);

인터럽트 핸들러를 설정할 때 썼던 request_irq 함수와 유사해 보이네요. 그런데 request_threaded_irq 함수도 비슷하게 인터럽트 핸들러를 등록하는 것 같습니다. request_irq를 호출할 때 같은 인자를 채우는데, request_threaded_irq는 IRQ Thread 핸들러인 dwc3_thread_interrupt 함수를 추가하는군요.

request_threaded_irq 함수 호출로 해당 인터럽트에 대한 전용 IRQ Thread가 생성됩니다. 리눅스 커널에서 이 IRQ Thread 이름을 어떻게 지을까요? 위 인터럽트 번호가 47이면  "irq/47-dwc3"이 되겠죠.

인터럽트 발생 후 dwc3_interrupt란 인터럽트 핸들러에서 인터럽트에 대한 처리를 한 다음 "irq/47-dwc3" IRQ Thread를 깨울지 결정합니다. 이후 "irq/47-dwc3" IRQ Thread가 깨어나면 dwc3_thread_interrupt 함수가 호출되는 구조죠.

해당 코드를 볼까요? dwc 인터럽트가 발생하면 dwc3_interrupt란 인터럽트 핸들러가 실행되겠죠? 다음 dwc3_interrupt 함수를 보면 바로 dwc3_check_event_buf 함수를 호출하는군요. 
static irqreturn_t dwc3_interrupt(int irq, void *_evt)
{
struct dwc3_event_buffer *evt = _evt;
return dwc3_check_event_buf(evt);
}

dwc3_check_event_buf 함수 구현부는 다음과 같습니다.
1 static irqreturn_t dwc3_check_event_buf(struct dwc3_event_buffer *evt)
2 {
3 struct dwc3 *dwc = evt->dwc;
4 u32 amount;
5 u32 count;
6 u32 reg;
7
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }
//...
14 if (amount < count)
15 memcpy(evt->cache, evt->buf, count - amount);
16
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

위 함수를 눈여겨보면 시스템 상태에 따라 IRQ_HANDLED와 IRQ_WAKE_THREAD를 리턴합니다. 인터럽트가 발생한 후 일을 더 할 필요가 없을 때는 다음 코드와 같이 IRQ_HANDLED를 리턴합니다.
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }

특정 조건으로 IRQ Thread가 해당 인터럽트에 대한 처리를 더 수행해야 할 때는 IRQ_WAKE_THREAD를 리턴합니다.
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

이후 IRQ Thread가 깨어난 후 IRQ Thread 핸들러인 dwc3_thread_interrupt가 인터럽트 핸들러에서 바로 하지 못한 일을 수행합니다. 

여기까지 라즈베리안에서 실행하는 드라이버 코드는 아니지만, 다른 시스템 드라이버에서는 어떤 방식으로 request_threaded_irq을 호출했는지 간단히 리뷰했습니다. 다음 시간에는 라즈베리안에서 IRQ Thread가 어떻게 실행하는지 더 자세히 알아볼까요?


+ Recent posts