Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

16192
888
89789


[리눅스커널][인터럽트] 인터럽트 컨택스트: in_interrupt() 함수 코드분석 5장. 인터럽트 핸들링

5.2.3 in_interrupt() 함수란

현재 실행 중인 코드가 인터럽트 컨택스트 구간인지 어떻게 알 수 있을까요? in_interrupt() 함수가 이 정보를 알려줍니다. 

리눅스 커널이 실행할 때 수많은 함수가 각자 서로를 호출합니다. 프로세스 컨택스트 혹은 인터럽트 컨택스트, 즉 인터럽트 처리하는 도중 호출될 수 있습니다. 만약 현재 실행하는 코드가 인터럽트를 처리 중이면 더 빨리 실행을 끝내야 합니다. 인터럽트 서비스 루틴은 실행 중인 프로세스를 멈추고 동작하기 때문입니다. 그래서 디바이스 드라이버 코드에서 인터럽트 컨택스트 조건에서만 신속하게 코드를 실행시키고 싶을 때가 있습니다. 

in_interrupt() 함수를 써서 만든 패치 코드 소개

in_interrupt() 함수를 써서 만든 다음 패치를 함께 살펴 봅시다. 
diff --git a/drivers/mmc/core/block.c b/drivers/mmc/core/block.c
index df9320c..0eb54dc 100644
--- a/drivers/mmc/core/block.c
+++ b/drivers/mmc/core/block.c
@@ -361,7 +361,7 @@ static struct mmc_blk_ioc_data *mmc_blk_ioctl_copy_from_user(
01        struct mmc_blk_ioc_data *idata;
02        int err;
03
04 -       idata = kmalloc(sizeof(*idata), GFP_KERNEL);
05 +       idata = kmalloc(sizeof(*idata), in_interrupt() ? GFP_ATOMIC : GFP_KERNEL);
06        if (!idata) {
07                err = -ENOMEM;
08                goto out;

참고로 – 기호는 원래 코드이고 + 기호는 추가하는 코드입니다. 

위 패치는 인터럽트 컨택스트 조건에서 다른 동작을 하는 코드입니다.

05 번째 줄 코드를 봅시다. 
05 +       idata = kmalloc(sizeof(*idata), in_interrupt() ? GFP_ATOMIC : GFP_KERNEL);

리눅스 커널에서 자주 볼 수 있는 3항 연산자 코드입니다.

in_interrupt() 함수가 true를 반환하면 GFP_ATOMIC 플래그, 반대의 경우 GFP_KERNEL 플래그로 kmalloc() 함수를 호출합니다.

in_interrupt() 함수가 true를 반환하는 경우 인터럽트 처리 중입니다. gfp_flag를 GFP_ATOMIC으로 설정하고 메모리를 할당합니다. GFP_ATOMIC 옵션으로 kmalloc() 함수를 호출하면 스케줄링 없이 메모리 할당합니다. 

반대로 in_interrupt() 함수가 false를 반환하면 현재 코드가 프로세스 컨택스트에서 수행 중이니 GFP_KERNEL 옵션으로 메모리를 할당합니다. 참고로 GFP_ATOMIC 옵션으로 메모리를 할당하면 프로세스는 휴면하지 않고 메모리를 할당하고, GFP_KERNEL 옵션인 경우 메모리 할당 실패 시 휴면할 수 있습니다. 
보통 인터럽트 컨택스트에서 GFP_ATOMIC 플래그로 kmalloc() 함수를 호출해 메모리를 할당합니다.

요약하면 인터럽트 처리 도중에는 빨리 메모리를 할당하는 코드입니다.

in_interrupt() 함수 관련 리눅스 커널 패치 소개

이번에는 최근에 반영된 리눅스 커널 패치 코드를 소개합니다.
[https://lore.kernel.org/patchwork/patch/835607/]
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index 7c38e850a8fc..685049a9048d 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -1349,7 +1349,7 @@  void *zs_map_object(struct zs_pool *pool, unsigned long handle,
  * pools/users, we can't allow mapping in interrupt context
  * because it can corrupt another users mappings.
  */
- WARN_ON_ONCE(in_interrupt());
+ BUG_ON(in_interrupt());
 
  /* From now on, migration cannot move the object */
  pin_tag(handle);

위 코드를 잠깐 보면 in_interrupt() 매크로가 TRUE를 반환하면 실행하는 매크로 함수를 BUG_ON() 함수로 변경했습니다. BUG_ON() 매크로를 호출하면 커널 패닉을 유발합니다. zs_map_object() 함수가 인터럽트 컨택스트에서 실행하면 커널 패닉을 유발시켜 문제를 유발한 코드를 수정하겠다는 의도입니다.

zs_map_object() 함수가 인터럽트 컨택스트에서 호출하면 zsmalloc() 으로 할당한 버퍼가 오염되는 문제가 확인돼 코드가 바뀐 것입니다.

리눅스 커널에서 치명적인 시스템 오동작을 유발할 수 있는 에러 조건을 점검해 커널 패닉을 유발하는 코드가 많습니다. 시스템이 다양한 오동작을 하기 전에 문제 발생 시점에 커널 패닉으로 코어 덤프를 받아 근본 원인을 해결하며는 의도입니다.

in_interrupt() 함수 코드 분석하기

이어서 in_interrupt() 함수 코드를 살펴 봅시다. 
아래 in_interrupt() 함수를 보면 실제 irq_count() 매크로로 매핑되어 preempt_count() 매크로로 처리하는 값과 HARDIRQ_MASK | SOFTIRQ_MASK 비트 마스크와 OR 비트 연산을 수행합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define in_interrupt()  (irq_count())
    
 #define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
      | NMI_MASK))  
 
 SOFTIRQ_MASK: 0xff00, SOFTIRQ_OFFSET: 0x100 
 HARDIRQ_MASK: 0xf0000, HARDIRQ_OFFSET: 0x10000.

HARDIRQ_OFFSET(0x10000) 플래그는 인터럽트를 처리 중이란 정보를 표현하는 플래그입니다. HARDIRQ_MASK 플래그는 HARDIRQ_OFFSET(0x10000)이란 비트를 가져오려는 비트 마스크입니다. 

만약 0x210200을 HARDIRQ_MASK(0x10000)와 AND 비트 연산을 하면 결괏값은 0x10000이 됩니다. 0x10000는 HARDIRQ_OFFSET 이니 인터럽트를 처리 중이란 의미입니다.

리눅스 커널 수 많은 코드에서 비트 연산을 볼 수 있습니다. ARM 프로세스 입장에서 비트 연산자는 실행 속도도 빠릅니다.

이번에 preempt_count() 함수 코드를 조금 더 살펴 봅시다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/asm-generic/preempt.h]
01 static __always_inline int preempt_count(void)
02 {
03 return READ_ONCE(current_thread_info()->preempt_count);
04 }

3 번째 줄 코드를 보면 current_thread_info()->preempt_count 값을 반환합니다. 

이어서 current_thread_info() 함수 코드를 살펴봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h]
01 register unsigned long current_stack_pointer asm ("sp");  
02
03 static inline struct thread_info *current_thread_info(void)
04 {
05 return (struct thread_info *)
06 (current_stack_pointer & ~(THREAD_SIZE - 1));  
07 }

01 번째 줄 코드를 보겠습니다. 스택 주소를 current_stack_pointer 변수로 읽는 동작입니다. current_stack_pointer를 register 타입으로 선언했는데 current_stack_pointer 변수를 ARM 레지스터를 써서 처리하라는 의미입니다.

“asm(“sp”)” 는 인라인 어셈블리 코드로 현재 스택 주소를 의미합니다.
 
C 문법에 없는 코드인데, 리눅스 커널에선 아주 자주 호출되는 코드는 인라인 어셈블리를 씁니다. C 코드에서 어셈블리 코드를 쓰는 것입니다.

우리가 보통 선언해 쓰는 지역 변수와 register 타입으로 선언된 지역 변수의 차이점은 뭘까요? register 타입으로 지역 변수를 설정하면 더 적은 어셈블리 코드로 해당 코드를 수행할 수 있습니다. current_thread_info란 인라인 함수가 아주 많이 호출되기 때문입니다. 

다음 05~06 번째 줄 코드를 보겠습니다.
05 return (struct thread_info *)
06 (current_stack_pointer & ~(THREAD_SIZE - 1));  

현재 실행 중인 프로세스의 스택 최상단 주소를 계산합니다. 이 주소에 프로세스 실행 정보가 담긴 struct thread_info 구조체가 있습니다.

요약하면 현재 실행 중인 함수에서 스택 주소을 얻어 스택 최상단 주소를 계산해 struct thread_info 구조체 preempt_count 필드에서 얻어오는 값입니다.
 
[그림 5.7] in_interrupt() 함수 동작 원리

위 그림을 보면 스택 최상단 주소에 struct thread_info 구조체 필드가 있습니다. 커널 프로세스마다 스택 최상단 주소에 이 구조체 데이터를 저장합니다. 

정리하면 in_interrupt()란 매크로는 현재 프로세스의 스택 메모리 공간에 저장된 struct thread_info 구조체의 preempt_count 필드값을 읽습니다. 

이제까지 설명해 드린 내용을 실제 디바이스에서 확인해 봅시다.
다음은 Trace32 프로그램으로 확인한 struct thread_info 구조체 필드 값입니다. 
01 (struct thread_info*)(0xA359B908 & ~0x1fff) = 0xA359A000 -> (
02     flags = 0x2,
03     preempt_count = 0x00010002, //  HARDIRQ_OFFSET
04     addr_limit = 0xBF000000,
05     task = 0xA0B5EA40,   
06     exec_domain = 0xA1A1AF1C,
07     cpu = 0x0,
08     cpu_domain = 0x15,

03번째 줄을 보면 preempt_count가 0x00010002입니다. 이 값으로 현재 코드가 인터럽트 컨택스트에서 실행 중임을 알 수 있습니다. 0x00010002 값과 0x10000 (HARDIRQ_OFFSET)을 AND 비트 연산한 결과가 1이기 때문입니다. 여기서 HARDIRQ_OFFSET 란 비트는 “현재 인터럽트 처리 중”임을 표시합니다.
 
인터럽트 처리 시작을 설정하는 HARDIRQ_OFFSET 란 비트는 어느 함수에서 설정할까요? 
__irq_enter() 함수에서 HARDIRQ_OFFSET 비트를 설정합니다.
        
[그림 5.8] 인터럽트 컨택스트를 설정하는 함수 흐름

__handle_domain_irq() 함수를 열어봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/irqdesc.c]
1 int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
2 bool lookup, struct pt_regs *regs)
3 {
4 struct pt_regs *old_regs = set_irq_regs(regs);
5 unsigned int irq = hwirq;
6 int ret = 0;
7
8 irq_enter();
9
...
10 if (unlikely(!irq || irq >= nr_irqs)) {
11 ack_bad_irq(irq);
12 ret = -EINVAL;
13 } else {
14 generic_handle_irq(irq);
15 }
16
17 irq_exit();
18 set_irq_regs(old_regs);
19 return ret;
20 }

먼저 8번째 줄 코드를 보겠습니다.
__handle_domain_irq() 함수에서 인터럽트 핸들러를 호출하기 전 irq_enter()이란 함수를 호출합니다. 함수 이름대로 “인터럽트 처리 시작”을 나타내는 표시입니다. 

irq_enter() 함수보다 irq_context_enter()로 함수 이름을 바꾸면 함수 동작을 빨리 짐작할 수 있습니다. 

irq_enter() 함수는 결국 __irq_enter() 함수를 호출하는데 __irq_enter() 매크로 함수를 살펴보면 current_thread_info()->preempt_count 필드에 HARDIRQ_OFFSET 비트를 더합니다. 

이제 “인터럽트 처리 시작”이란 의미의 비트를 설정하는 것입니다.

irq_enter() 함수는 __irq_enter() 함수를 호출합니다. 

__irq_enter() 매크로 함수 구현부를 같이 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/hardirq.h] 
1 #define __irq_enter()     \
2  do {      \
3   account_irq_enter_time(current); \
4   preempt_count_add(HARDIRQ_OFFSET); \  
5   trace_hardirq_enter();   \
6  } while (0)

4 번째 줄 코드를 보겠습니다.
4   preempt_count_add(HARDIRQ_OFFSET); \  

preempt_count_add() 함수를 호출하면 current_thread_info()->preempt_count필드에 HARDIRQ_OFFSET 비트를 더합니다. 이제 “인터럽트 처리 시작”이란 의미의 비트를 설정하는 것입니다.

이후 해당 인터럽트 핸들러를 호출해서 인터럽트 처리르 마무리하고 다음 함수가 호출됩니다. 이번에 다음 코드 흐름에서 __irq_exit() 이란 함수를 봅시다. 이 함수에서 struct thread_info 구조체 preempt_count 필드에서 HARDIRQ_OFFSET 비트를 해제합니다.
  
[그림 5.9] 인터럽트 컨택스트 해제 함수 흐름

__irq_exit() 함수를 분석하면서 인터럽트 컨택스트 해제 과정을 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/hardirq.h] 
01 #define __irq_exit()     \
02  do {      \
03   trace_hardirq_exit();   \
04   account_irq_exit_time(current);  \
05   preempt_count_sub(HARDIRQ_OFFSET); \
06  } while (0) 

05 번째 줄 코드를 실행하면 current_thread_info()->preempt_count멤버에서 HARDIRQ_OFFSET 비트를 뺍니다. 

여기까지 HARDIRQ_OFFSET 플래그 비트를 설정하고 해제하는 흐름을 알아봤습니다. 이 비트는 뭘 의미할까요? HARDIRQ_OFFSET 비트(0x10000)은 현재 코드가 인터럽트를 처리 중인지를 알려 줍니다. 이 비트를 설정하면 “현재는 인터럽트 처리 중”, 해제하면 “현재 인터럽트 처리 중이 아님”을 알려주는 것입니다. 

코드 분석으로 파악한 in_interrupt() 함수를 정리해보겠습니다.
  - 실행 중인 프로세스 스택 최상단 주소에 위치한 struct thread_info 구조체 preempt_count 필드임
  - 인터럽트 핸들링을 하기 직전 __irq_enter() 함수에서 HARDIRQ_OFFSET를 더함
  - 인터럽트 핸들링을 하고 난 후 __irq_exit() 함수에서 HARDIRQ_OFFSET를 뺌 

리눅스 커널에서 쓰는 in_interrupt() 함수 코드를 분석했습니다. 이어서 라즈베리파이에서 in_interrupt() 함수 동작을 확인하겠습니다.

라즈베리파이에서 in_interrupt() 함수 동작 확인하기

in_interrupt() 함수에 대한 이해를 돕기 위해 실습을 해봅시다.
커널 소스를 수정해서 라즈비안에서 in_interrupt() 함수가 반환하는 값을 확인하겠습니다. 

인터럽트 핸들러에 아래와 같이 코드를 반영하고 로그를 받아보면 in_interrupt() 함수 반환값을 볼 수 있습니다.
diff --git a/drivers/mmc/host/bcm2835-sdhost.c b/drivers/mmc/host/bcm2835-sdhost.c
index 273b1be05..5f57b3dab 100644
--- a/drivers/mmc/host/bcm2835-sdhost.c
+++ b/drivers/mmc/host/bcm2835-sdhost.c
@@ -1472,6 +1472,16 @@ static irqreturn_t bcm2835_sdhost_irq(int irq, void *dev_id)
01 struct bcm2835_host *host = dev_id;
02 u32 intmask;
03 
04 + void *stack;
05 + struct thread_info *current_thread;
06 +
07 + stack = current->stack;   
08 + current_thread = (struct thread_info*)stack;  
09 +
10 + printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n", 
11 + (unsigned int)in_interrupt(), (unsignedint)current_thread->preempt_count, (long unsigned int)stack);  
12 +
13 +
14 spin_lock(&host->lock);
15 
16 intmask = bcm2835_sdhost_read(host, SDHSTS);

패치 코드를 설명하기 전 패치 코드 입력 방법을 소개합니다.

다음은 bcm2835_sdhost_irq() 함수 오리지널 코드입니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/mmc/host/bcm2835-sdhost.c] 
static irqreturn_t bcm2835_sdhost_irq(int irq, void *dev_id)
{
irqreturn_t result = IRQ_NONE;
struct bcm2835_host *host = dev_id;
u32 intmask;

/* 패치 코드를 입력하세요. */
spin_lock(&host->lock);

다음 패치 코드를 /* 패치 코드를 입력하세요. */ 부분에 입력하면 됩니다.
04 + void *stack;
05 + struct thread_info *current_thread;
06 +
07 + stack = current->stack;   
08 + current_thread = (struct thread_info*)stack;  
09 +
10 + printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n", 
11 + (unsigned int)in_interrupt(), (unsignedint)current_thread->preempt_count, (long unsigned int)stack);  
12 +

이제 패치 코드를 리뷰해 봅시다.

먼저 7 번째 줄 코드를 보겠습니다.
07 + stack = current->stack;

현재 실행 중인 프로세스의 태스크 디스크립터 정보를 얻어옵니다.

실행 중인 코드에서 태스크 디스크립터 정보를 얻기 위해서 current란 매크로를 씁니다. (이 세부 동작은 4장 프로세스에서 다루니 참고하세요.) struct task_struct 구조체 stack 필드에 스택 최상단 주소가 저장돼 있습니다. 스택 최상단 주소로 preempt_count 필드에 접근할 수 있습니다.

current 리눅스 커널에서 자주 씁니다. 세부 동작은 4장 프로세스에서 다루니 참고하세요.

다음 08 번째 줄 코드입니다.
08 + current_thread = (struct thread_info*)stack;  

스택 최상단 주소에 struct thread_info 구조체 필드 정보가 있습니다. 이 구조체로 캐스팅합니다.

이번에 패치 코드에서 가장 중요한 부분입니다.
10 + printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n", 
11 + (unsigned int)in_interrupt(), (unsignedint)current_thread->preempt_count, (long unsigned int)stack);  

in_interrupt() 함수 반환값과 struct thread_info 구조체 preempt_count 필드값을 출력합니다.

위 로그를 작성하고 컴파일하면 문제없이 커널 빌드가 될 것입니다. 이후 커널 이미지를 라즈베리파이에 설치한 후 리부팅합시다. 

다음은 위 패치 코드가 실행해 출력한 커널 로그입니다.
01 [0.911605] Indeed it is in host mode hprt0 = 00021501
02 [1.001692] mmc1: new high speed SDIO card at address 0001
03 [1.037804] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
04 [1.039271] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
05 [1.041839] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
06 [1.042911] mmc0: host does not supp000,preempt_count = 0x00010000, stack=0x80c00000 
08 [1.046503] mmc0: new high speed SDHC card at address aaaa
09 [1.046995] mmcblk0: mmc0:aaaa SB16G 14.8 GiB May 13 13:07:23 raspberrypi keort reading read-only switch, assuming write-enable
07 [1.044570] [+] in_interrupt: 0x00010rnel:

03번째 줄 로그에서 preempt_count 값은 0x00010000이고 in_interrupt 값은 0x00010000입니다. preempt_count은 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 preempt_count 필드에 저장된 값입니다. 이 preempt_count 필드와  HARDIRQ_OFFSET(0x10000) 플래그와 AND 비트 연산한 결과가 0x00010000입니다.
preempt_count & HARDIRQ_OFFSET(0x10000)

 “in_interrupt: 0x00010000” 이 로그의 의미는 “현재 인터럽트 처리 중”이라는 것이며 현재 코드가 인터럽트 컨택스트라는 사실을 알 수 있습니다. 

다음에는 인터럽트 컨택스트에서 실행 시간이 오래 걸리는 코드를 입력하면 어떤 일이 발생하는지 확인해 보겠습니다.

# Reference (인터럽트 처리)





핑백

덧글

  • Daniel 2018/12/14 11:41 # 답글

    만약 어떤 값이 0x210200인데 이 값에 HARDIRQ_MASK(0x10000)와 AND 비트 연산을 하면 결괏값은 0x10000이 됩니다.
    -> 만약 어떤 값이 0x210200인데 이 값에 HARDIRQ_OFFSET(0x10000)와 AND 비트 연산을 하면 결괏값은 0x10000이 됩니다
    로 변경해야 맞지 않은지 문의드립니다.
  • Guillermo 2018/12/14 14:11 #

    좋은 지적 감사드리고, 참 꼼꼼히 글을 읽어주셔서 감사합니다.

    HARDIRQ_MASK는 0xf0000이고 HARDIRQ_OFFSET는 0x10000이며 각각 이진수는 다음과 같습니다.

    HARDIRQ_MASK: 11110000000000000000
    HARDIRQ_OFFSET: 00010000000000000000

    그런데 인터럽트 컨택스트 도중에 IPI(Inter Process Interrupt)가 발생할 수 있고 이 경우 preempt_count() 값은
    00110000000000000000 이 될 수 있습니다.

    따라서 다음과 같은 코드로 인터럽트 컨택스트를 식별하는 것으로 보입니다.
    #define irq_count()(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK
    | NMI_MASK))

    #define in_interrupt()(irq_count())

    알려주신데로, HARDIRQ_OFFSET와 AND 연산을 하면 인터럽트 컨택스트를 제대로 식별할 수 있습니다만,
    코드 구현 내용을 따라 보니 HARDIRQ_MASK으로 언급한 것입니다.
  • Daniel 2018/12/14 14:25 #

    저는 변수명과 값이 매칭되지 않은 것을 확인차 문의드리건데
    자세한 답변 덕분에 또 하나 배워갑니다.
    감사합니다.
  • Guillermo 2018/12/14 14:27 #

    헉~~ 좋은 지적 감사합니다.
    HARDIRQ_MASK(0xf0000) 이렇게 해야 겠네요. ^^
댓글 입력 영역