Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


[리눅스커널][인터럽트] 인터럽트 발생은 언제 지연해야 할까? 5장. 인터럽트 핸들링

5.6 인터럽트 발생은 언제 지연해야 할까?

이번 장에서 인터럽트가 발생하면 프로세스는 하던 일을 멈추고 인터럽트 벡터를 실행하여 인터럽트를 처리한다고 배웠습니다. 하지만 다음과 같은 특정 코드 구간에서 인터럽트가 발생하면 안 될 때가 있습니다. 
1. SoC에서 정의한 하드웨어 블록에 정확한 시퀀스(Sequence)을 줘야 할 경우 
2. 시스템이 슬립에 진입하기 직전 핀 먹스 값을 저장 동작
3. 각 디바이스 드라이버가 서스팬드 모드로 진입할 때 디바이스 드라이버에 데이터 시트에서 명시한 대로 정확한 특정 시퀀스를 줘야 할 경우
4. 예외(Exception)이 발생해서 시스템 리셋을 시키기 전

시스템에 정확한 타이밍을 전달해야 하거나 익셉션이 발생해서 시스템을 리셋 시켜야 할 때입니다. 

인터럽트를 잠시 지연해야 하는 상황을 4가지로 소개했는데 사실 1번부터 3번까지는 비슷한 이야기입니다. 프로세서가 슬립에 진입할 때 각 디바이스 드라이버를 서스팬드 시키는 동작을 예를 들어 봅시다. 시스템이 슬립에 진입할 때는 많은 디바이스 드라이버에서 적절한 시퀀스(파형)을 해당 하드웨어에 전달하는 코드를 실행합니다. 이 과정에서 인터럽트가 발생하면 데이터 시트에서 정해진 정확한 시퀀스를 줄 수 없습니다.
 
[그림 5.15] LCD 하드웨어를 서스팬드 시키는 신호

LCD 드라이버가 위와 같이 A란 파형이 하이에서 로우로 떨어질 때 B란 파형이 하이로 유지되어야 해당 LCD가 슬립에 들어간다고 가정하겠습니다. 만약 이런 동작 과정에 위에서 눈금 처진 구간에서 인터럽트가 발생하면 어떤 일이 발생할까요? 정확한 타이밍을 줄 수 없습니다. 이 때 인터럽트 발생을 잠시 지연하기 위해 local_irq_disable() 함수를 호출해야 합니다. 

인터럽트 발생 지연 예시 코드 알아보기

또한 커널에서 GPIO 관련 함수로 특정 주소에 정해진 값을 써주고 파형을 줄 수 있습니다. 예제 코드를 같이 살펴 봅시다. 
01 static int system_codec_suspend(struct snd_soc_codec *codec)
02 {
03 struct cs4271_private *cs4271 = snd_soc_codec_get_drvdata(codec);
04    struct cs4271_platform_data *cs4271plat = codec->dev->platform_data;
05  int ret;
06
07       if (gpio_request(gpio_nreset, "CS4271 Reset"))   
08        gpio_nreset = -EINVAL;
09
10       gpio_direction_output(gpio_nreset, 0);  
11
12 local_irq_disable();
13      udelay(100);   
14
15      gpio_set_value(gpio_nreset, 1);  
16
17      /* Give the codec time to wake up */
18      udelay(100);
19
20 local_irq_enable();
21    cs4271->gpio_nreset = gpio_nreset;
22 }

7 번째 코드에서는 "CS4271 Reset"란 GPIO를 초기화하고 10 번째 줄에서 GPIO를 출력 모드로 바꿉니다. 13 번째 줄에서 100나노초만큼 딜레이를 주고 15 번째 코드에서 해당 GPIO를 HIGH로 올려줍니다. 그리고 다시 100나노초만큼 딜레이를 줍니다. 아래 코드의 의미는 오디오 디바이스에서 정해진 기준에 맞게 신호를 전달합니다.

위 코드에서 이런 신호를 전달하는 구간에 local_irq_disabled() 함수와 local_irq_disabled() 함수를 호출해서 인터럽트 발생 지연시킵니다. 들이 리눅스 커널 코드를 읽다가 local_irq_disabled() 함수를 보면 “뭔가 중요한 제어를 하는 코드이구나” 라고 해석하면 됩니다.

익셉션 발생 후 인터럽트 발생 지연 예시 코드 분석하기

이번에는 4 번째로 든 익셉션이 발생한 후 인터럽트를 지연하는 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/traps.c]
01 asmlinkage void bad_mode(struct pt_regs *regs, int reason)
02 {
03 console_verbose();
04
05 pr_crit("Bad mode in %s handler detected\n", handler[reason]);
06
07 die("Oops - bad mode", regs, 0);
08 local_irq_disable();  
09 panic("bad mode");
10 }

09 번째 줄 코드를 보면 panic() 함수를 호출해 커널 패닉을 유발합니다. 이 코드를 실행하기 전 08 번째 줄에서 local_irq_disable() 함수를 호출해 인터럽트 발생을 지연합니다.

커널 패닉을 유발하기 바로 전 local_irq_disable() 함수 호출로 인터럽트 발생을 지연시킵니다. 왜냐면 시스템 전체에 익셉션이 발생했다고 알리고 시스템 리셋을 시켜야 하기 때문입니다.  

bad_mode() 함수는 언제 호출될까요? 유저 모드에서 권한 없이 메모리 공간에 접근하면 ARM 프로세스가 이를 감지하고 익셉션(Exception)을 발생시킵니다. 이때 bad_mode() 함수가 호출되는데 09 번째 줄과 같이 panic() 함수를 호출해 커널 패닉을 유발합니다.   

대부분 익셉션(Exception)이 발생해 시스템 리셋을 시키기 전에도 인터럽트 발생을 지연시킵니다. 익셉션 중 데이터 어보트(Data Abort)나 프리패치 어보트(Prefetch Abort)가 발생하면 커널은 더 이상 도저히 실행할 수 없는 심각한 오류가 발생했다고 판단합니다. 그래서 시스템 전체에 지금 익셉션이 일어났다고 알리고 커널 패닉을 유발 후 리셋 됩니다. 

local_irq_disable() 함수의 실제 구현부를 알아보기 위해 전처리 파일(linux/arch/arm/kernel/.tmp_traps.i)를 열어 보겠습니다.  
01 void bad_mode(struct pt_regs *regs, int reason)
02 {
03 console_verbose();
04
05 printk("\001" "2" "Bad mode in %s handler detected\n", handler[reason]);
06
07  die("Oops - bad mode", regs, 0);
08 do { arch_local_irq_disable(); trace_hardirqs_off(); } while (0);
09 panic("bad mode");
10 }
 
08 번째 줄 코드와 같이 arch_local_irq_disable() 함수가 보입니다.

arch_local_irq_disable() 함수를 열어서 보면 인라인 어셈블 코드로 "cpsid i" 이란 ARM 명령어를 볼 수 있습니다. 이 명령어를 실행하면 인터럽트 발생을 지연합니다. 
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) void arch_local_irq_disable(void)
 {
  asm volatile(
   "     cpsid i                 @ arch_local_irq_disable"
   :
   :
   : "memory", "cc");
 }
 # 155 "./arch/arm/include/asm/irqflags.h"
 
"cpsid i" 어셈블리 코드가 bad_mode() 함수에서 호출하는지 확인해봅시다.
"objdump -d vmlinux" 란 명령어로 바이너리 유틸리티를 쓰면 어셈블리 코드를 확인할 수 있습니다. 
01 8010c260 <bad_mode>:
02 8010c260:       e1a0c00d        mov     ip, sp
03 8010c264:       e92dd800        push    {fp, ip, lr, pc}
...
04 8010c2a4:       e3a02000        mov     r2, #0
05 8010c2a8:       ebfffe9c        bl      8010bd20 <die>
06 8010c2ac:       f10c0080        cpsid   i  
07 8010c2b0:       eb034511        bl      801dd6fc <trace_hardirqs_off>
08 8010c2b4:       e59f0010        ldr     r0, [pc, #16]   ; 8010c2cc <bad_mode+0x6c>
09 8010c2b8:       eb03db7a        bl      802030a8 <panic>

06 번째 줄 코드를 보면 해당 “cpsid" 명령어를 볼 수 있습니다.  


이장의 앞부분에서 인터럽트 관련 동작은 시스템 전반에 영향을 끼치니 세부 동작은 리눅스 커널 개발자는 반드시 잘 알아야 한다고 했습니다. 그 핵심 원리는 다음과 같습니다.
 - 인터럽트는 언제든 발생해 실행하는 코드를 멈추고 인터럽트 핸들러를 실행할 수 있다.
 - 인터럽트 핸들러는 빨리 실행해야 한다. 

그런데 실수로 인터럽트 핸들러에서 실행 시간이 오래 걸리는 함수를 쓰면 시스템은 어떻게 동작할까요? 또한 인터럽트 발생을 잠시 지연해야 하는 조건에서 인터럽트를 지연하지 않으면 어떻게 될까요? 전체 시스템은 불안정해 질 수 있습니다. 심하면 커널 패닉, 락업 등등의 문제를 만나 고생할 수 있습니다.

하지만 이번 장에 다룬 커널이 인터럽트를 어떻게 처리하는지를 정확히 이해하고 활용하면 더 안정적인 코드를 작성할 수 있습니다.

# Reference (인터럽트 처리)



핑백

덧글

댓글 입력 영역