Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이][리눅스커널] in_interrupt() 함수란 [라즈베리파이][커널]인터럽트

in_interrupt() 함수란
리눅스 커널이 실행할 때 수많은 함수가 각자 서로를 호출합니다. 어떤 함수는 프로세스 컨택스트에서 실행(커널 쓰레드)되는데 대부분 함수는 인터럽트 컨택스트, 즉 인터럽트 처리하는 도중 호출될 수 있습니다. 만약 현재 실행하는 코드가 인터럽트를 처리 중이면 더 빨리 처리해야겠죠? 

인터럽트 서비스 루틴은 실행 중인 프로세스를 멈추고 동작하므로 인터럽트 컨택스트 조건에서만 신속하게 코드를 실행시키고 싶을 때가 있습니다. 그럼 현재 실행 중인 코드가 인터럽트 컨택스트 구간인지 어떻게 알 수 있을까요? in_interrupt()란 함수가 이 정보를 알려줍니다. 

그럼 다음 패치를 함께 살펴 봅시다. 
diff --git a/drivers/md/dm-region-hash.c b/drivers/md/dm-region-hash.c
index b929fd5..1325a8a 100644
--- a/drivers/md/dm-region-hash.c
+++ b/drivers/md/dm-region-hash.c
@@ -289,7 +289,12 @@ static struct dm_region *__rh_alloc(struct dm_region_hash *rh, region_t region)
1  {
2        struct dm_region *reg, *nreg;
3
4 -       nreg = mempool_alloc(rh->region_pool, GFP_ATOMIC);
5 +       gfp_t gfp_flag = GFP_KERNEL;  
6 +       if (in_interrupt()) {   
7 +               gfp_flag = GFP_ATOMIC;
8 +       }
9 +       nreg = mempool_alloc(flush_entry_pool, gfp_flag);
10 +
11         if (unlikely(!nreg))
12                 nreg = kmalloc(sizeof(*nreg), GFP_NOIO | __GFP_NOFAIL);.

참고로 – 기호는 원래 코드이고 + 기호는 추가하는 코드이니 이점 참고하세요. 

위 코드는 __rh_alloc() 함수가 인터럽트 컨택스트 조건에서 다른 동작을 합니다.
먼저 5번 줄 코드를 봅시다. 
5 +       gfp_t gfp_flag = GFP_KERNEL;  

메모리 설정 플래그를 저장하는 지역 변수 gfp_flag를 GFP_KERNEL로 초기화합니다.  
다음은 6번 줄 코드입니다.
6 +       if (in_interrupt()) {   
7 +               gfp_flag = GFP_ATOMIC;
8 +       }

in_interrupt() 함수가 true이면 인터럽트 처리 중이므로 gfp_flag를 GFP_ATOMIC으로 설정하고 메모리를 할당합니다. GFP_ATOMIC 옵션으로 kmalloc() 함수를 호출하면 스케줄링 없이 메모리 할당합니다. 반대로 in_interrupt() 함수가 false로 리턴하면 현재 코드가 프로세스 컨택스트에서 수행 중이니 GFP_KERNEL 옵션으로 메모리를 할당합니다. 참고로 GFP_ATOMIC 옵션으로 메모리를 할당하면 프로세스는 휴면하지 않고 메모리를 할당하고, GFP_KERNEL 옵션인 경우 메모리 할당 실패 시 휴면할 수 있습니다. 

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

이제 in_interrupt() 함수 코드를 살펴 봅시다. 아래 in_interrupt() 함수를 보면 실제 irq_count() 매크로로 매핑되어 preempt_count() 매크로로 처리하는 값과 HARDIRQ_MASK | SOFTIRQ_MASK 비트 마스크와 OR 비트 연산을 수행합니다.
#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(0xF0000)와 AND 비트 연산을 하면 결괏값은 0x10000이 됩니다. 반복해서 설명을 드리면 0x10000는 HARDIRQ_OFFSET 이니 인터럽트를 처리 중이란 의미입니다.

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

이제 preempt_count 코드를 조금 더 살펴 봅시다. 
static __always_inline int preempt_count(void)
{
return READ_ONCE(current_thread_info()->preempt_count);
}

current_thread_info()->preempt_count 값을 리턴하는데 이 코드를 이해하려면 current_thread_info() 코드를 살펴봐야 합니다. current_thread_info() 함수를 보겠습니다.
register unsigned long current_stack_pointer asm ("sp"); //<<-- [1]

static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1)); //<<-- [2]
}

current_thread_info 함수를 눈여겨보면, [1] 코드에서 스택 주소를 sp 지역 변수로 읽어 옵니다. 여기서 “asm(“sp”)” 는 인라인 어셈블리 코드입니다. C 문법에 없는 코드인데, 리눅스 커널에선 아주 자주 호출되는 코드는 인라인 어셈블리를 씁니다. C 코드에서 어셈블리 코드를 쓰는 겁니다.

다시 [1] 코드를 보면 지역 변수를 register 타입으로 선언했습니다. 이는 sp란 지역 변수를 다른 지역 변수를 처리하듯이 스택 공간 말고 ARM 레지스터를 써서 처리하라는 의미입니다. 여기서 다른 지역 변수와 register 타입으로 선언된 지역 변수의 차이점은 뭘까요? 그 이유는 register 타입으로 지역 변수를 설정하면 더 적은 어셈블리 코드로 해당 코드를 수행할 수 있습니다. current_thread_info란 인라인 함수가 아주 많이 호출되기 때문입니다. 

이제 [2]번 코드를 봅시다.
[kernel/arch/arm/include/asm/thread_info.h]
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1)); //<<-- [2]
}

이 코드는 스택 Top 주소를 계산합니다. 이 주소에 프로세스 실행 정보가 담긴 struct thread_info 구조체의 멤버가 저장돼 있습니다.

정리하면 현재 구동 중인 함수 내에서 확인되는 스택 주소를 통해 스택 상단 Top 주소를 얻어 온 후 struct thread_info 구조체의 preempt_count 멤버에서 얻어오는 값입니다. 
위 그림 상단 낮은 주소(Top 주소)로 옆에 struct thread_info란 구조체가 볼 수 있습니다.  커널 프로세스마다 스택 Top 주소에 이 구조체 데이터를 저장합니다. 정리하면 in_interrupt()란 매크로는 현재 프로세스의 스택에 저장된 struct thread_info 구조체의 preempt_count 멤버를 읽습니다. 

이제까지 설명해 드린 내용을 실제 디바이스에서 확인해 봅시다.

아래는 코어 덤프를 로딩해서 Trace32 프로그램으로 확인한 struct thread_info 자료 구조입니다. 
(struct thread_info*)(0xE359B908 & ~0x1fff) = 0xE359A000 -> (
     flags = 0x2,
     preempt_count = 0x00010002, // <<-- HARDIRQ_OFFSET
     addr_limit = 0xBF000000,
     task = 0xD0B5EA40,  // <<--태스크 디스크립터 주소
     exec_domain = 0xC1A1AF1C,
     cpu = 0x0,
     cpu_domain = 0x15,

참고로, 리눅스 커널에서는 커널 패닉이 발생하면 그 시점의 메모리 덤프를 받을 수 있는 기능을 지원합니다. 이 메모리 덤프를 보통 vmcore 혹은 코어 덤프라고 합니다. 

이제 코어 덤프 분석으로 돌아가면, preempt_count값은 0x00010002로 현재 프로세스가 인터럽트 컨택스트에서 실행 중임을 알 수 있습니다. 이 이유는 0x00010002 값과 10000 (HARDIRQ_OFFSET)을 AND 연산하면 결과는 1이기 때문입니다.

여기까지 잘 이해되시나요? 조금 낯선 용어들은 조금 지나면 친숙해지니 끝까지 읽어주세요. 다시 반복하면 HARDIRQ_OFFSET 란 비트는 “현재 인터럽트 처리 중”임을 나타냅니다.
 
인터럽트 처리 시작을 설정하는 HARDIRQ_OFFSET 란 비트는 어느 함수에서 설정할까요? 
코드를 조금 더 살펴보면 __irq_enter 매크로에서 HARDIRQ_OFFSET 비트를 설정하고 있습니다.

위 그림에서 보이는 __handle_domain_irq() 함수를 열어 봅시다.
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
    bool lookup, struct pt_regs *regs)
 {
  struct pt_regs *old_regs = set_irq_regs(regs);
  unsigned int irq = hwirq;
  int ret = 0;
 
  irq_enter();  // <<--
 
 //skip
  if (unlikely(!irq || irq >= nr_irqs)) {
   ack_bad_irq(irq);
   ret = -EINVAL;
  } else {
   generic_handle_irq(irq);
  }
 
  irq_exit();  // <<--
  set_irq_regs(old_regs);
  return ret;
 }

__handle_domain_irq 함수에서 인터럽트 핸들러를 호출하기 전 irq_enter란 함수를 호출합니다. 함수 이름대로 “인터럽트 처리 시작”을 나타내는 표시로 보입니다. 제 생각에 irq_enter보다 irq_context_enter로 함수 이름을 바꾸면 함수 동작을 빨리 짐작할 수 있을 것 같습니다. 해당 코드는 아래 화살표가 가르키고 있으니 잘 살펴보세요.

irq_enter 함수는 결국 __irq_enter 함수를 호출하는데 __irq_enter 매크로 함수를 자세히 살펴보면 current_thread_info()->preempt_count란 멤버에 HARDIRQ_OFFSET 비트를 더합니다. 이제 “인터럽트 처리 시작”이란 의미의 비트를 설정하는 겁니다.
#define __irq_enter()     
  do {     
   account_irq_enter_time(current);
   preempt_count_add(HARDIRQ_OFFSET); //<<-- 
   trace_hardirq_enter();   
  } while (0)

이후 해당 인터럽트 핸들러가 호출되어 인터럽트에 대한 처리를 마친 후에 다음 함수가 호출됩니다. 자, 이제 아래 코드 흐름에서 __irq_exit란 함수가 보이죠? 이 함수에서 struct thread_info.preempt_count 멤버에서 HARDIRQ_OFFSET 비트를 해제합니다. 


바로 __irq_exit() 함수를 보겠습니다. 
#define __irq_exit()     
  do {     
   trace_hardirq_exit();   
   account_irq_exit_time(current); 
   preempt_count_sub(HARDIRQ_OFFSET);
  } while (0) 

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

정리하면 in_interrupt() 함수는 현재 구동 중인 프로세스 스택 Top 주소에 위치한 current_thread_info()->preempt_count 멤버이며 인터럽트 서비스 루틴이 실행되기 전 __irq_enter 함수에서 HARDIRQ_OFFSET를 더하고 인터럽트 서비스 루틴이 종료되면 해제합니다.

라즈베리안(라즈베리파이)에서 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)
  struct bcm2835_host *host = dev_id;
  u32 intmask;
 
+ void *stack;
+ struct thread_info *current_thread;
+
+ stack = current->stack;  // <<--[1]
+ current_thread = (struct thread_info*)stack; // <<--[2]
+
+ printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx n", 
+ (unsigned int)in_interrupt(), (unsignedint)current_thread->preempt_count, (long unsigned int)stack); // [3]
+
+
  spin_lock(&host->lock);
 
  intmask = bcm2835_sdhost_read(host, SDHSTS);

위와 같이 코드를 변경하기 전 bcm2835_sdhost_irq 함수는 다음과 같았습니다. void *stack부터 시작하는 +라고 표시한 코드를 u32 intmask; 와 spin_lock(&host->lock); 코드 사이에 추가하면 됩니다.
[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);

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

[1]: 다음 코드는 현재 실행 중인 프로세스의 태스크 디스크립터 정보를 얻어옵니다.
+ stack = current->stack;

현재 실행 중인 코드에서 태스크 디스크립터 정보를 어떻게 얻어올까요? 이때 current란 매크로를 쓰면 됩니다. 이는 커널에서 제공하는 매크로인데 커널에서 자주 사용합니다. (이 세부 동작은 4장 프로세스에서 다루니 참고하세요.) struct task_struct 구조체에 stack이란 멤버에 스택 Top 주소가 저장돼 있습니다. 이 스택 Top 주소로 preempt_count 멤버 변수에 접근할 수 있습니다.

[2]: 스택 탑(Top) 주소에 struct thread_info 구조체 정보가 있다고 배웠습니다. 이 구조체로 캐스팅합니다.
+ current_thread = (struct thread_info*)stack;

[3]: in_interrupt와 struct thread_info.preempt_count 값을 출력합니다.
+ printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx n", 
+ (unsigned int)in_interrupt(), (unsignedint)current_thread->preempt_count, (long unsigned int)stack);  

위 로그를 작성하고 컴파일하면 문제없이 커널 빌드가 될 겁니다. 이후 커널 이미지를 다운로드 하고 커널 로그를 확인하면 됩니다.

커널 로그를 받아보면, in_interrupt() 함수는 현재 실행 중인 프로세스 스택에 저장된 struct thread_info->preempt_count 값에서 HARDIRQ_OFFSET 비트만 출력하기 위해 HARDIRQ_MASK로 마스킹한 결과를 리턴한다는 점을 알 수 있습니다.
[0.911605] Indeed it is in host mode hprt0 = 00021501
[1.001692] mmc1: new high speed SDIO card at address 0001
[1.037804] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
[1.039271] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
[1.041839] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
[1.042911] mmc0: host does not support reading read-only switch, assuming write-enable
[1.044570] [+] in_interrupt: 0x00010000,preempt_count = 0x00010000, stack=0x80c00000 
[1.046503] mmc0: new high speed SDHC card at address aaaa
[1.046995] mmcblk0: mmc0:aaaa SB16G 14.8 GiB May 13 13:07:23 raspberrypi kernel:

위 로그에서 preempt_count 값은 0x00010000이고 in_interrupt 값은 0x00010000이죠? 0x00010000 값에서 HARDIRQ_OFFSET 비트 값만 뽑아서 출력한 값이 0x00010000값 입니다. “in_interrupt: 0x00010000” 이 로그의 의미는 “현재 인터럽트 처리 중”이라는 것이며 현재 코드가 인터럽트 컨택스트라는 사실을 알려줍니다. 

이렇게 코드 분석 후 패치 코드를 작성해서 실제 시스템에서 출력되는 결괏값을 확인하면 더 오랫동안 머릿속에 남습니다. 

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


핑백

덧글

  • 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) 이렇게 해야 겠네요. ^^
댓글 입력 영역