Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] Soft IRQ 서비스는 언제 요청하나? [라즈베리파이]인터럽트후반부

이번 시간에는 Soft IRQ 서비스를 어떻게 요청하는지 배워 보겠습니다. 우리는 이전에 Soft IRQ를 등록하는 과정을 배웠습니다. 다음 코드와 같이 open_softirq 함수를 써서 TIMER_SOFTIRQ 타입의 Soft IRQ 서비스를 등록했습니다.
1 void __init init_timers(void)
2 {
3 init_timer_cpus();
4 open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
5 }

그럼 TIMER_SOFTIRQ 타입의 Soft IRQ 서비스를 등록만 하면 핸들러 함수인 run_timer_softirq() 함수가 호출될까요? 그렇지 않습니다. 따로 Soft IRQ 서비스를 요청해야 합니다. 인터럽트 핸들러를 등록하는 과정보다 약간 복잡합니다.

Soft IRQ 서비스를 요청하면 커널은 이 정보를 어디에 저장할까요? 정답은 irq_stat란 전역 변수입니다. irq_stat 전역 변수는 아래와 같이 정의합니다. CPU 개수만큼 배열을 잡죠.
extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

라즈베리파이는 4개의 CPU를 쓰므로 irq_stat 변수를 Trace32 프로그램으로 보면 다음과 같습니다.
v.v %t %d %h %i irq_stat
(static irq_cpustat_t [4]) irq_stat = (
    [0] = (
      (unsigned int) __softirq_pending = 0 = 0x1,
    [1] = (
      (unsigned int) __softirq_pending = 0 = 0x3,
    [2] = (
      (unsigned int) __softirq_pending = 0 = 0x0,
    [3] = (
      (unsigned int) __softirq_pending = 0 = 0x6,

커널은 CPU 별로 지정한 __softirq_pending 변수에 (1 << Soft IRQ 서비스 아이디) 비트 연산 결과를 저장합니다. Soft IRQ 서비스 아이디를 왼쪽 시프트 한 결괏값입니다. 각 CPU 별로 저장한 __softirq_pending 변수를 해석하면 다음과 같습니다. 

각 Soft IRQ 서비스 아이디 값은 다음과 같이 선언돼있습니다. HI_SOFTIRQ 아이디가 0이고, RCU_SOFTIRQ가 9입니다.
(where)
[include/linux/interrupt.h]
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, 
RCU_SOFTIRQ,  
NR_SOFTIRQS
};

Trace32 프로그램으로 확인한 irq_stat 값과 위에서 연산한 결과를 주석문으로 같이 확인해보겠습니다.    
(static irq_cpustat_t [4]) irq_stat = (
    [0] = (
      (unsigned int) __softirq_pending = 0 = 0x1, // HI_SOFTIRQ
    [1] = (
      (unsigned int) __softirq_pending = 0 = 0x2, // HI_SOFTIRQ | TIMER_SOFTIRQ
    [2] = (
      (unsigned int) __softirq_pending = 0 = 0x0,
    [3] = (
      (unsigned int) __softirq_pending = 0 = 0x6, // NET_TX_SOFTIRQ | TIMER_SOFTIRQ

CPU0에는 HI_SOFTIRQ이란 Soft IRQ 서비스를 요청했고, CPU1는 HI_SOFTIRQ와 TIMER_SOFTIRQ Soft IRQ 서비스를 요청한 상태입니다. CPU3에서 요청된 Soft IRQ 서비스는 NET_TX_SOFTIRQ와 TIMER_SOFTIRQ입니다.

그런데 커널에서 Soft IRQ 서비스를 요청하려면 커널이 제공한 함수인 raise_softirq() 혹은 __raise_softirq_irqoff() 를 호출해야 합니다. 위와 같이 (static irq_cpustat_t [4]) irq_stat 전역 변수에 직접 접근해서 값을 쓸 수는 없습니다. 

관련 코드를 함께 분석해 보겠습니다. raise_softirq 함수를 호출하면 raise_softirq_irqoff 함수를 거쳐 __raise_softirq_irqoff 함수를 호출합니다.
[kernel/softirq.c]
void raise_softirq(unsigned int nr)
{
unsigned long flags;

local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}

Soft IRQ 서비스를 요청하려면 raise_softirq 혹은 __raise_softirq_irqoff 함수를 호출해야 하는데 두 함수의 차이점은 뭘까요?

raise_softirq 함수는 local_irq_save와 local_irq_store 함수를 써서 raise_softirq_irqoff 함수 실행 도중 인터럽트 동기화를 수행합니다. 그래서 대부분 Soft IRQ 서비스를 요청할 때 raise_softirq 함수를 씁니다. 다만 이미 인터럽트 발생에 대한 동기화가 진행 중인 상태면 __raise_softirq_irqoff 함수를 써도 상관은 없습니다.

이번에는 __raise_softirq_irqoff 함수를 점검할 차례입니다.
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

입력 인자인 nr을 왼쪽으로 쉬프트한 값을 or_softirq_pending 함수에 전달합니다.

or_softirq_pending 함수 구현부를 보면 매크로이고 irq_stat[4].__softirq_pending 변수에 접근한다는 정보를 알 수 있습니다. 원래 __softirq_pending 변수가 저장한 값과 “|=” 연산을 하니 __softirq_pending 멤버가 저장한 값은 그대로 두는 것이죠. 
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)

extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

매크로 함수 중에 smp_processor_id 함수가 보이는데 이 함수는 현재 프로세스가 돌고 있는 CPU 번호를 알려 줍니다. 코드를 잠깐 보면 smp_processor_id 함수는 current_thread_info()->cpu로 치환됩니다.  in_interrupt 매크로와 비슷한 원리로 구현한 매크로죠.

or_softirq_pending 매크로 함수를 쓰면 다른 코드가 실행된다는 점을 알 수 있습니다.
(irq_stat[cpu].__softirq_pending) |= (1UL << x)

분석한 코드 내용을 종합하면 위 코드 연산 결과는 다음과 같이 알기 쉽게 표현할 수 있습니다.
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

irq_stat[cpu]__softirq_pending |=  (1UL << nr)
irq_stat[cpu]__softirq_pending =  irq_stat[cpu]__softirq_pending + (1UL << nr)

위 코드에서 nr는 Soft IRQ 서비스 아이디이고 cpu는 __raise_softirq_irqoff() 함수를 실행하는 CPU번호를 의미합니다. 

이렇게 or_softirq_pending란 함수를 3단계로 매크로가 매크로를 호출하니 소스를 따라가기 어렵습니다. 이럴 때는 3장에서 소개했듯이 전처리 코드를 보면 효율적으로 커널 코드를 읽을 수 있습니다. 전처리 코드는 매크로를 모두 푼 정보를 담고 있거든요. 전처리 코드를 볼까요? 
1 void __raise_softirq_irqoff(unsigned int nr)
2 {
3  trace_softirq_raise(nr);
4  ((irq_stat[(current_thread_info()->cpu)].__softirq_pending) |= (1UL << nr));
5 }

전처리 코드를 보니 or_softirq_pending(1UL << nr); 코드는 위 4번째 줄 코드으로 치환됩니다. irq_stat[cpu갯수].__softirq_pending 멤버에 Soft IRQ enum 값을 왼쪽으로 쉬프트한 값을 OR 연산으로 저장합니다.

여기까지 Soft IRQ 서비스를 요청하는 코드를 알아 봤습니다. 
Soft IRQ 서비스를 요청했으면 어디선가 이 서비스 요청 알아보고 처리하는 코드가 있을 겁니다. 이제 Soft IRQ 서비스를 요청했는지는 누가 어떻게 점검할까요? 

Soft IRQ 서비스 요청 여부는 인터럽트 핸들러 수행이 끝나면 호출하는 irq_exit()과 ksoftirqd() 쓰레드 핸들 함수인 run_ksoftirqd에서 점검합니다. 이 때 커널에서 제공하는 local_softirq_pending 함수를 호출합니다.

실제 소스 코드를 함께 살펴보겠습니다. 다음 코드를 보면 irq_exit 함수에서 local_softirq_pending 함수를 호출하는 루틴을 알 수 있습니다.
void irq_exit(void)
{
....
if (!in_interrupt() && local_softirq_pending())
  invoke_softirq();

irq_exit 함수에서 2가지 조건을 점검한 후 Soft IRQ 서비스를 실행할지 결정합니다. 첫 번째는 현재 실행 중인 코드가 인터럽트 컨택스트 상태인지와 두 번째로 Soft IRQ 서비스 요청이 있었는지를 점검하는 것이죠.

irq_exit 함수를 전처리 코드에서 보면 local_softirq_pending 함수는 irq_stat[(current_thread_info()->cpu)].__softirq_pending)로 치환됨을 알 수 있습니다.
void irq_exit(void)
{
...
if (!((preempt_count() & ((((1UL << (4))-1) << ((0 + 8) + 8)) | (((1UL << (8))-1) << (0 + 8)) | (((1UL << (1))-1) << (((0 + 8) + 8) + 4))))) && (irq_stat[(current_thread_info()->cpu)].__softirq_pending))

ksoftirqd 쓰레드도 마찬가지입니다. 다음 4번째 줄 코드에서 local_softirq_pending 함수를 호출해서 Soft IRQ 서비스 요청이 있었는지 점검합니다.
1 static void run_ksoftirqd(unsigned int cpu)
2 {
3 local_irq_disable();
4 if (local_softirq_pending()) {
5 __do_softirq();

run_ksoftirqd 함수를 전처리 코드로 열어봐도 (irq_stat[(current_thread_info()->cpu)].__softirq_pending) 코드가 실행함을 알 수 있습니다.
[./kernel/.tmp_softirq.i]
static void run_ksoftirqd(unsigned int cpu)
{
  do { arch_local_irq_disable(); trace_hardirqs_off(); } while (0);
  if ((irq_stat[(current_thread_info()->cpu)].__softirq_pending)) {
   __do_softirq();

이렇게 __raise_softirq_irqoff 함수를 써서 irq_stat[(current_thread_info()->cpu)].__softirq_pending)에 값을 써주면 나중에 이 변수를 읽어서 확인하는 것입니다.

전체 Soft IRQ 구조에서 Soft IRQ 서비스를 요청하는 흐름도는 다음과 같습니다.

위 그림과 같이 __softirq_pending 변수의 비트 필드 순서는 Soft IRQ 벡터에 대응합니다. 즉, __softirq_pending 변수의 두 번째 비트 필드가 1이면 run_timer_softirq 함수가 호출되는 것입니다.

이번 절에 배운 내용을 간단히 정리해 보겠습니다.
인터럽트 핸들러에서 or_softirq_pending()이란 함수를 호출해서 Soft IRQ 서비스를 요청하면, irq_exit()과 run_softirqd() 함수에서 local_softirq_pending이란 함수를 써서 Soft IRQ 요청이 있었는지 점검합니다.

나중에 __do_softirq() 함수가 실행할 때 각 CPU 별로 irq_stat[cpu].__softirq_pending 변수에 접근해서 비트 필드를 점검합니다. 이 정보로 softirq_vec에 접근해서 Soft IRQ 서비스 핸들러 함수를 호출합니다.


핑백

덧글

댓글 입력 영역