Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

5112
737
82115


[리눅스커널][인터럽트] 인터럽트 핸들러 등록하기 - request_irq() 5장. 인터럽트 핸들링

5.4 인터럽트 핸들러는 어떻게 등록할까?

인터럽트가 발생하면 이를 핸들링하는 인터럽트 핸들러가 호출됩니다. 예를 들면 다음과 같습니다.
 - 컴퓨터에서 키보드 자판을 입력하면 키보드 인터럽트가 발생해 키보드 인터럽트 핸들러가 
 - 휴대폰에서 USB 케이블을 연결하면 USB 인터럽트가 발생해 USB 인터럽트 핸들러가 호출됩니다.

디바이스 드라이버 입장에서 이렇게 인터럽트가 발생해 인터럽트 핸들러가 호출되려면 인터럽트를 제대로 초기화시켜야 합니다.

이번 절에서는 인터럽트 핸들러를 등록하는 방법과 이를 검증하는 실습 패치를 살펴봤습니다. 인터럽트 핸들러를 등록하는 코드는 실전 디바이스 드라이버 개발에서 자주 접할 가능성이 매우 높으니 잘 숙지합시다. 

5.4.1 인터럽트 핸들러 등록 과정 분석하기 

인터럽트 핸들러 등록은 인터럽트를 초기화하는 과정에 포함돼 있습니다.
인터럽트 초기화하려면 호출하는 request_irq() 함수 선언부와 함수 사용법에 대해 살펴보겠습니다.

request_irq() 함수 소개  
request_irq() 함수 선언부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
    const char *name, void *dev)

request_irq() 함수로 인터럽트를 정상 초기화하면 true, 아니면 false를 반환합니다.

다음 인자를 확인합시다.

unsigned int irq;
인터럽트 번호입니다.

irq_handler_t handler;
인터럽트가 발생하면 호출될 인터럽트 핸들러 주소입니다.

unsigned long flags;
인터럽트 속성 플래그입니다.

const char *name;
인터럽트 이름입니다.

void *dev;
인터럽트 핸들러에 전달하는 매개인자입니다. 
보통 디바이스 드라이버를 제어하는 구조체 주소를 전달합니다.
디바이스 드라이버와 인터럽트 핸들러를 연결하는 중요한 인터페이스입니다.

request_irq() 함수를 써서 인터럽트를 초기화하는 라즈비안 코드 분석 

request_irq() 함수 선언부를 소개했으니 라즈비안에서 request_irq() 함수를 써서 인터럽트를 초기화하는 코드를 보겠습니다.
 [https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/usb/host/dwc_otg/dwc_otg_driver.c]
01 static int dwc_otg_driver_probe(
02 #ifdef LM_INTERFACE
03        struct lm_device *_dev
...
04          int retval = 0;
05          dwc_otg_device_t *dwc_otg_device;
06          int devirq;
...          
07 dev_dbg(&_dev->dev, "Calling request_irq(%d)\n", devirq);
08 retval = request_irq(devirq, dwc_otg_common_irq,
09                             IRQF_SHARED,
10                             "dwc_otg", dwc_otg_device);

08 번째 줄 코드를 보면 request_irq() 함수를 호출해 인터럽트를 초기화합니다.
request_irq() 함수로 전달하는 인자는 다음과 같습니다.
 - unsigned int irq = devirq(62);    /* 인터럽트 번호 */
 - irq_handler_t handler = dwc_otg_common_irq;  /* 인터럽트 핸들러 */
 - unsigned long flags = 0x80(IRQF_SHARED);  /* 인터럽트 속성 플래그 */
 - const char *name = "dwc_otg";  /* 인터럽트 이름  */
 - void *dev = dwc_otg_device; /* 인터럽트 핸들러에 전달한 매개인자 */

각 필드에 저장하는 인터럽트 속성은 위 코드 주석문을 참고합시다.

request_irq() 함수와 request_threaded_irq() 함수 분석

위에서 살펴본 dwc_otg_driver_probe() 함수에서 request_irq() 함수에 전달한 인자가 커널 내부에서 어떻게 초기화되는지 궁금하지 않나요? request_irq() 함수 선언부와 전달 인자를 소개했으니 이어서 request_irq() 함수 소스 코드 분석으로 들어갑니다.

request_irq() 함수 코드 구현부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
01 static inline int __must_check
02 request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
03     const char *name, void *dev)
04 {
05 return request_threaded_irq(irq, handler, NULL, flags, name, dev);
06 }

05번째 줄 코드를 보면 request_irq() 함수에 전달된 인자를 실어서 request_threaded_irq() 함수를 호출합니다. request_irq() 함수는 특별한 동작이 없습니다.

request_threaded_irq() 함수 세부 코드 분석 전 함수 주요 동작을 소개합니다.
 - 인터럽트 번호로 인터럽트 디스크립터 가져오기
 - struct irqaction 구조체로 동적 메모리 할당
 - struct irqaction 구조체 필드에 인터럽트 초기화 인자 설정하기
  : 인터럽트 핸들러, 인터럽트 속성, 인터럽트 핸들러 매개인자

이어서 request_threaded_irq() 함수 코드를 분석하겠습니다.
01 int request_threaded_irq(unsigned int irq, irq_handler_t handler,
02 irq_handler_t thread_fn, unsigned long irqflags,
03 const char *devname, void *dev_id)
04 {
05 struct irqaction *action;
06 struct irq_desc *desc;
07 int retval;
...
08 desc = irq_to_desc(irq);
09 if (!desc)
10 return -EINVAL;
...
11 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
12 if (!action)
13 return -ENOMEM;
...
14 action->handler = handler;
15 action->thread_fn = thread_fn;
16 action->flags = irqflags;
17 action->name = devname;
18 action->dev_id = dev_id;

먼저 08 번째 줄 코드입니다.
08 desc = irq_to_desc(irq);
09 if (!desc)
10 return -EINVAL;
정수형 인터럽트 번호를 저장한 irq 인자로 irq_to_desc() 함수를 호출해 인터럽트 디스크립터 주소를 desc 변수에 가져옵니다. 만약 인터럽트 디스크립터 주소가 NULL이면 09~10 번째 줄 코드와 같이 -EINVAL 를 반환합니다. 만약 디바이스 드라이버에서 오타나 실수로 잘못된 인터럽트 번호로 request_irq() 함수를 호출하면 이 부분에서 에러 코드를 반환하며 request_threaded_irq() 함수 실행을 마칩니다.
  
다음 11~13 번째 줄 코드를 보겠습니다.
11 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
12 if (!action)
13 return -ENOMEM;

11 번째 줄 코드는 kzalloc() 함수를 호출해 struct irqaction 구조체 크기로 동적 메모리를 할당 받습니다. 12~13 번째 줄은 동적 메모리 할당을 못 받을 경우 -ENOMEM 에러 코드를 반환하며 함수 실행을 종료합니다.
 
다음은 14~18 번째 줄 코드입니다.
14 action->handler = handler;      /* 인터럽트 핸들러 */
15 action->thread_fn = thread_fn;  /* 스레드 핸들러*/
16 action->flags = irqflags;     /* 인터럽트 속성 플래그*/
17 action->name = devname;       /* 인터럽트 이름*/
18 action->dev_id = dev_id;    /* 인터럽트 핸들러 매개인자*/

request_irq() 함수로 전달된 인자를 struct irqaction 구조체 필드에 저장합니다. 
각 필드에 저장하는 인터럽트 속성은 위 코드 주석문을 참고합시다.

5.4.2 인터럽트 핸들러 초기화 과정 디버깅해보기

우리는 request_irq() 함수로 인터럽트 핸들러를 등록하는 과정을 배웠습니다. 또한 request_threaded_irq() 함수에서 인터럽트 디스크립터에 해당 인터럽트 설정 정보를 저장하는 코드를 살펴봤습니다. 

이번에는 라즈비안에서 리눅스 커널 코드에 디버깅 코드를 추가해 인터럽트 핸들러를 등록하는 코드 흐름을 파악해보겠습니다. 

먼저 패치 코드를 소개합니다.
diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
index 5c0ba5c..a8fca52 100644
--- a/kernel/irq/manage.c
+++ b/kernel/irq/manage.c
@@ -1847,6 +1847,12 @@ int request_threaded_irq(unsigned int irq, irq_handler_t handler,
01        action->name = devname;
02        action->dev_id = dev_id;
03
04 +       if (irq == 62 || irq == 92 ) {
05 +               printk("[+][irq_debug] irq_num: %d, func: %s, line:%d, caller:%pS \n",
06 +                               irq, __func__, __LINE__, (void *)__builtin_return_address(0));
07 +               dump_stack();
08 +       }
09 +
10        retval = irq_chip_pm_get(&desc->irq_data);
11        if (retval < 0) {
12                kfree(action);

+ 기호는 기존 파일에 코드가 추가됐다는 표시입니다. 패치 코드를 설명하기 전 패치 코드 입력 방법을 소개합니다.

다음은 패치 코드를 입력하기 전 request_threaded_irq() 함수 코드입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/manage.c]
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 = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
/* 패치 코드를 입력하세요 */ 
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}

위 코드에서 /* 패치 코드를 입력하세요 */ 라고 표시된 부분에 다음 코드를 입력합시다.
04 +       if (irq == 62 || irq == 92 ) {
05 +               printk("[+][irq_debug] irq_num: %d, func: %s, line:%d, caller:%pS \n",
06 +                               irq, __func__, __LINE__, (void *)__builtin_return_address(0));
07 +               dump_stack();
08 +       }

패치 코드 입력 방법을 살펴봤으니 패치 코드 내용을 소개합니다.

위 패치 코드의 목적은 인터럽트 번호가 62과 92인 인터럽트를 설정할 때 커널 로그로 콜스택을 출력합니다.

먼저 04번째 줄 코드를 보겠습니다.  
04 +       if (irq == 62 || irq == 92 ) {

먼저 irq 변수의 정체를 알아보겠습니다. request_threaded_irq() 함수 첫 번째 인자로 irq가 전달되는데 unsigned int 타입으로 인터럽트 번호를 의미합니다.

다음 조건문 코드를 보겠습니다. 정수형 인터럽트 번호인 irq가 62와 92일 때만 if 문 내 코드를 실행합니다. 이 조건을 추가한 이유는 모든 인터럽트를 디버깅 정보로 출력하면 시스템에 과부하를 줄 수 있기 때문입니다.

다음은 커널 로그를 출력하는 코드입니다.
05 +               printk("[+][irq_debug] irq_num: %d, func: %s, line:%d, caller:%pS \n",
06 +                               irq, __func__, __LINE__, (void *)__builtin_return_address(0));

irq는 인터럽트 번호이고 __func__, __LINE__은 각각 해당 코드의 함수 이름과 코드 라인 정보를 알려주는 매크로입니다. __builtin_return_address(0) 는 GCC 컴파일러가 제공하는 빌트인 매크로로 자기 자신을 호출한 함수 정보를 알려줍니다. 


__func__, __LINE__, __builtin_return_address(0) 매크로는 리눅스 커널에 다양한 방식으로 활용되니 처음 보는 함수를 디버깅할 때 자주 활용합시다.


이번에는 핵심 디버깅 코드입니다. 함수 이름과 같이 현재 코드를 실행한 콜스택 정보를 출력합니다.
16 dump_stack();


위 코드를 입력하고 커널 빌드를 한 후 커널 이미지를 라즈베리파이에 설치합니다.

패치 코드는 부팅 과정에 실행합니다. 
오타로 잘못 코드를 입력하면 라즈베리파이가 부팅을 못할 수 있으니 신경써서 코드를 입력합시다.

라즈베리파이를 리부팅을 시키고 /var/log/kern.log에 저장된 커널 로그를 열어보겠습니다. 

커널 로그를 보면 다음 메시지를 볼 수 있습니다.
01 [0.696159] Dedicated Tx FIFOs mode
02 [0.696436] [+][irq_debug] irq_num: 62, func: request_threaded_irq, line:1777, caller:pcd_init+0x138/0x240 
03 [0.696449] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.30-v7+ #11
04 [0.696456] Hardware name: BCM2835
05 [0.696476] [<8010ffe0>] (unwind_backtrace) from [<8010c21c>] (show_stack+0x20/0x24)
06 [0.696494] [<8010c21c>] (show_stack) from [<8078721c>] (dump_stack+0xc8/0x10c)
07 [0.696514] [<8078721c>] (dump_stack) from [<801780f0>] (request_threaded_irq+0x1a4/0x1a8)
08 [0.696531] [<801780f0>] (request_threaded_irq) from [<805d38e4>] (pcd_init+0x138/0x240)
09 [0.696548] [<805d38e4>] (pcd_init) from [<805c5768>] (dwc_otg_driver_probe+0x728/0x884)
10 [0.696565] [<805c5768>] (dwc_otg_driver_probe) from [<805464f8>] (platform_drv_probe+0x60/0xc0)
11 [0.696583] [<805464f8>] (platform_drv_probe) from [<80544a84>] (driver_probe_device+0x244/0x2f8)
12 [0.696603] [<80544a84>] (driver_probe_device) from [<80544c00>] (__driver_attach+0xc8/0xcc)
13 [0.696621] [<80544c00>] (__driver_attach) from [<80542c0c>] (bus_for_each_dev+0x78/0xac)
14 [0.696640] [<80542c0c>] (bus_for_each_dev) from [<805443a0>] (driver_attach+0x2c/0x30)
15 [0.696659] [<805443a0>] (driver_attach) from [<80543de8>] (bus_add_driver+0x114/0x220)
16 [0.696678] [<80543de8>] (bus_add_driver) from [<805453a4>] (driver_register+0x88/0x104)
17 [0.696696][<805453a4>](driver_register) from [<80546444>](__platform_driver_register+0x50/0x58)
18 [0.696713][<80546444>](__platform_driver_register) from[<80b39a60>] (dwc_otg_driver_init+0x74/0x120)
19 [0.696733] [<80b39a60>] (dwc_otg_driver_init) from [<80101c1c>] (do_one_initcall+0x54/0x17c)
20 [0.696753] [<80101c1c>] (do_one_initcall) from [<80b01058>] (kernel_init_freeable+0x224/0x2b8)
21 [0.696771] [<80b01058>] (kernel_init_freeable) from [<8079c1f0>] (kernel_init+0x18/0x124)
22 [0.696788] [<8079c1f0>] (kernel_init) from [<801080cc>] (ret_from_fork+0x14/0x28)
23 [0.696875] WARN::dwc_otg_hcd_init:1046: FIQ DMA bounce buffers: virt = 0xbad04000 dma = 0xfad04000 len=9024

위 로그는 인터럽트 62번을 설정할 때 콜스택입니다. 
콜스택에서 20 번째 줄 do_one_initcall() 함수와 11 번째 줄 platform_drv_probe() 함수가 보이니 리눅스 커널이 부팅하는 과정 콜스택임을 알 수 있습니다. 이로 부팅 과정에 인터럽트를 핸들러를 설정한다는 사실을 알 수 있습니다.

do_one_initcall() 함수는 커널 부팅 과정에서 한번 호출되며 디바이스 드라이버를 초기화합니다. 

먼저 2 번째 줄 로그를 보겠습니다.
02 [0.696436] [+][irq_debug] irq_num: 62, func: request_threaded_irq, line:1777, caller:pcd_init+0x138/0x240 

"[0.696436]"은 초 단위 타임스탬프입니다. 즉 부팅 후 0.696436초에 이 코드가 실행됐다는 의미입니다. 

커널 로그에서 본 메시지를 정리하면 다음과 같습니다.
1. 62번 인터럽트는 pcd_init() 함수에서 request_threaded_irq() 함수를 호출해 초기화합니다.
02 [0.696436] [+][irq_debug] irq_num: 62, func: request_threaded_irq, line:1777, caller:pcd_init+0x138/0x240

2. 코드를 실행한 프로세스는 pid가 1인 swapper/0 프로세스입니다.
03 [0.696449] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.30-v7+ #11

3. 부팅 과정에서 인터럽트를 초기화합니다. 
11 [0.696583] [<805464f8>] (platform_drv_probe) from [<80544a84>] (driver_probe_device+0x244/0x2f8)
...
20 [0.696753] [<80101c1c>] (do_one_initcall) from [<80b01058>] (kernel_init_freeable+0x224/0x2b8)

커널 로그로 인터럽트 초기화 과정은 시스템이 부팅될 때 이루어진다는 점을 알게 됐습니다.

인터럽트 초기화한 후 인터럽트 디스크립터 확인하기  

이어서 인터럽트를 초기화한 후 인터럽트 디스크립터를 읽어 제대로 인터럽트 초기화가 됐는지 점검하는 실습을 해보겠습니다.

먼저 패치 코드를 소개합니다.
1 static void interrupt_debug_irq_desc(int irq_num)
2 {
3 struct irqaction *action;
4 struct irq_desc *desc;
5
6 desc = irq_to_desc(irq_num);
7
8 if (!desc ) {
9 pr_err("invalid desc at %s line: %d\n", __func__, __LINE__);
10 return;
11 }
12
13 action = desc->action;
14
15 if (!action ) {
16 pr_err("invalid action at %s line:%d \n", __func__, __LINE__);
17 return;
18 }
19
20 printk("[+] irq_desc debug start \n");
21
22 printk("irq num: %d name: %8s \n", action->irq , action->name);
23 printk("dev_id:0x%x \n", (unsigned int)action->dev_id);
24
25      if (action->handler) {
26 printk("interrupt handler: %pF \n", action->handler);
27 }
28
29 printk("[-] irq_desc debug end \n");
30}

패치 코드 분석 전 패치 코드 입력 방법을 소개합니다.

위 코드를 dwc_otg_driver_probe() 함수 윗 부분에 입력합시다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/usb/host/dwc_otg/dwc_otg_driver.c]
01 static int dwc_otg_driver_probe(

01 static int dwc_otg_driver_probe(
02 #ifdef LM_INTERFACE
03        struct lm_device *_dev
04 #elif defined(PCI_INTERFACE)
...
05 {
06 int retval = 0;
07 dwc_otg_device_t *dwc_otg_device;
...
08 DWC_DEBUGPL(DBG_CIL, "registering (common) handler for irq%d\n",
09     devirq);
10 dev_dbg(&_dev->dev, "Calling request_irq(%d)\n", devirq);
11 retval = request_irq(devirq, dwc_otg_common_irq,
12                             IRQF_SHARED,
13                             "dwc_otg", dwc_otg_device);
14 +            interrupt_debug_irq_desc(devirq);

11~13 번째 줄에서는 request_irq() 함수를 호출해 인터럽트를 초기화합니다. 이 함수 다음 14 라인에 interrupt_debug_irq_desc() 함수를 호출합니다. 이 함수를 호출할 때 인터럽트 번호를 담고 있는 devirq 를 인자로 전달합시다.

패치 입력 방법을 소개했으니 패치 코드 내용을 알아봅시다.

우선 이번에 소개한 패치 코드인 interrupt_debug_irq_desc() 함수 6 번째 줄 코드부터 보겠습니다. 
6 desc = irq_to_desc(irq_num);

irq_to_desc() 함수를 써서 세부 인터럽트 정보가 있는 인터럽트 디스크립터를 읽어옵니다. 
여기서 인터럽트 디스크립터 구조체는 struct irq_desc 입니다. 조금 난해한 함수로 보이지만 함수 이름처럼 인터럽트 번호를 받아 해당 인터럽트 디스크립터를 알려줍니다.

이번에는 13 번째 줄 코드입니다.
13 action = desc->action;

인터럽트 디스크립터 struct irq_desc 구조체 action 필드는 인터럽트 핸들러, 인터럽트 번호, 인터럽트 플레그 정보를 저장하고 있습니다. 이 정보를 출력하기 위해 desc->action 이란 포인터를 struct irqaction 구조체인 action이란 지역 변수로 읽어 옵니다.

22번째부터 25번째까지 코드를 볼 차례입니다. 인터럽트에서 가장 중요한 정보를 출력하는 코드이니 조금 더 집중해서 볼 필요가 있습니다. 
22 printk("irq num: %d name: %8s \n", action->irq , action->name);
23 printk("dev_id:0x%x \n", (unsigned int)action->dev_id);
24
25      if (action->handler) {
26 printk("interrupt handler: %pF \n", action->handler);
27 }

인터럽트 이름, 인터럽트 번호, 디바이스 핸들 정보인 action->dev_id 그리고 인터럽트 핸들러를 출력합니다. 참고로 printk() 로 함수 포인터를 심볼 정보로 보고 싶을 때는 %pF로 옵션을 주면 됩니다.

패치 코드를 입력한 후 커널 빌드를 하고 커널 이미지를 라즈비안에 설치합시다.

위에서 소개한 패치 코드를 입력하고 커널을 빌드해 라즈비안에 커널 이미지를 설치합시다. 라즈베리파이를 재부팅 시키고 커널 로그를 열어 보면 다음 메시지를 볼 수 있습니다.
01 [0.496039] [+] irq_desc debug start  
02 [0.496048] irq num: 62 name:  dwc_otg 
03 [0.496055] dev_id:0xb9355c40 
04 [0.496065] interrupt handler: dwc_otg_common_irq+0x0/0x28 
05 [0.496071] [-] irq_desc debug end

02 번째 줄 로그를 보겠습니다.
02 [0.496048] irq num: 62 name:  dwc_otg 

인터럽트 번호는 62이고 이름은 dwc_otg입니다. 인터럽트 핵심 속성 정보입니다.

다음 03~04 번째 줄을 분석하겠습니다.
03 [0.496055] dev_id:0xb9355c40 
04 [0.496065] interrupt handler: dwc_otg_common_irq+0x0/0x28 

03 번째 줄 "dev_id:0xb9355c40" 메시지는 인터럽트 핸들러 매개인자 주소를 의미합니다.
마지막 04 번째 줄 "dwc_otg_common_irq+0x0/0x28" 인터럽트 핸들러 함수 이름입니다.

위 커널 로그로 다음 코드와 같이 인터럽트 이름은 dwc_otg, 62번 인터럽트 번호로 인터럽트를 등록한다는 정보를 알 수 있습니다. 
retval = request_irq(devirq, dwc_otg_common_irq,
                             IRQF_SHARED,
                             "dwc_otg", dwc_otg_device);

이렇게 우리가 알고 싶은 함수에 적절한 디버깅 코드를 입력하면 해당 코드가 어느 시점에 호출하는지 알 수 있습니다. 리눅스 커널을 익힐 때 코드만 보는 것보다 이 방식으로 로그를 추가해서 시스템을 실행하면 더 많은 정보를 얻을 수 있습니다.

여기까지 인터럽트 핸들러를 초기화하는 흐름까지 알아봤습니다. 다음에는 인터럽트 핸들러를 등록할 때 플래그를 어떻게 설정하는지 배워 보겠습니다.


# Reference (인터럽트 처리)

핑백

덧글

댓글 입력 영역