Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[Linux][Kernel] softirq overview (1) [Linux][Kernel] IRQ(Interrupt)

이번 시간에는 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 서비스 핸들러 함수를 호출합니다.

이제 Soft IRQ 기법의 하이라이트인 Soft IRQ 서비스를 처리하는 흐름을 살펴볼 차례입니다. Soft IRQ 서비스는 언제 처리할까요? 아래 Soft IRQ 전체 흐름도와 같이 인터럽트 핸들러를 처리하는 인터럽트 서비스 루틴이 끝나는 시점에 Soft IRQ 서비스 처리를 시작합니다. 그래서 Soft IRQ 서비스 처리를 시작하는 코드를 점검하려면 인터럽트 서비스 루틴이 끝나는 코드부터 확인해야 합니다.

이번에 Soft IRQ 전체 흐름도에서 볼드체로 된 부분을 점검할 예정이니 눈여겨보세요.


인터럽트 서비스 루틴이 끝나는 코드가 __handle_domain_irq 함수에서 실행하니 우선 이 함수부터 분석해야 합니다. 같이 볼 코드는 다음과 같습니다. 
1 int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
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 #ifdef CONFIG_IRQ_DOMAIN
11 if (lookup)
12 irq = irq_find_mapping(domain, hwirq);
13 #endif
14
15 /*
16  * Some hardware gives randomly wrong interrupts.  Rather
17  * than crashing, do something sensible.
18  */
19 if (unlikely(!irq || irq >= nr_irqs)) {
20 ack_bad_irq(irq);
21 ret = -EINVAL;
22 } else {
23 generic_handle_irq(irq);
24 }
25
26 irq_exit();  //<<---

다음 23번째 줄 코드에서 호출하는generic_handle_irq 함수 서브 루틴에서 인터럽트 핸들러를 호출합니다.
22 } else {
23 generic_handle_irq(irq);
24 }

예전에 봤던 ftrace 로그를 보면 아래 generic_handle_irq 함수가 보이죠? 이 함수가 호출된 후 62번 인터럽트 핸들러인 dwc_otg_common_irq 함수가 호출됐습니다.
kworker/0:0-27338 [000] 6028.897809: dwc_otg_common_irq <-__handle_irq_event_percpu
kworker/0:0-27338 [000] 6028.897847: <stack trace>
 => handle_irq_event
 => handle_level_irq
 => generic_handle_irq
 => bcm2836_chained_handle_irq
 => generic_handle_irq
 => __handle_domain_irq
 => bcm2836_arm_irqchip_handle_irq
 => __irq_svc

이제 인터럽트 핸들러 처리를 마치고 irq_exit() 함수를 호출합니다. 이 irq_exit() 함수가 Soft IRQ 서비스를 처리하는 시작점입니다. 이제 irq_exit() 함수를 분석하겠습니다.
1 void irq_exit(void)
2 {
3 account_irq_exit_time(current);
4 preempt_count_sub(HARDIRQ_OFFSET);
5 if (!in_interrupt() && local_softirq_pending())
6 invoke_softirq();
7
8 tick_irq_exit();
9 rcu_irq_exit();
10 trace_hardirq_exit(); /* must be last! */
11}

4번째 줄 코드를 보면 preempt_count_sub 함수를 호출합니다. struct thread_info->preeempt_count 변수에서 HARDIRQ_OFFSET를 뺍니다. 이제는 인터럽트 컨택스트가 아니라는 표시를 하는 겁니다.
4 preempt_count_sub(HARDIRQ_OFFSET);
5 if (!in_interrupt() && local_softirq_pending())
6 invoke_softirq();

그다음 5번째 줄 코드에서는 두 가지 조건을 점검합니다.

in_interrupt() 매크로를 호출해서 인터럽트 컨택스트가 아닌지 확인합니다. 4번째 줄 코드인 preempt_count_sub 함수가 처리된 직후 다른 인터럽트가 발생해서 다음 코드와 같이 preempt_count_add(HARDIRQ_OFFSET)를 실행해서 in_interrupt함수를 활성화할 수도 있기 때문입니다. 
#define __irq_enter()     \
  do {      \
   account_irq_enter_time(current); \
   preempt_count_add(HARDIRQ_OFFSET); \   
   trace_hardirq_enter();   \
  } while (0)

인터럽트는 언제 어디든 발생할 수 있으니 예외 처리 조건을 추가한 것입니다.

다음은 local_softirq_pending() 함수를 호출해서 Soft IRQ 서비스를 요청했는지 점검합니다. 전처리 코드로 irq_exit 함수를 확인하면 local_softirq_pending() 함수는 (irq_stat[(current_thread_info()->cpu)].__softirq_pending) 코드로 치환됩니다. 
void irq_exit(void)
{
...
__preempt_count_sub((1UL << ((0 + 8) + 8)));
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))
invoke_softirq();

이제 Soft IRQ 서비스를 실행하는 invoke_softirq() 함수를 점검해 봅시다.
1 static inline void invoke_softirq(void)
2 {
3
4 if (!force_irqthreads) {
5 #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
6 __do_softirq();
7  #else
8 do_softirq_own_stack();
9 #endif
10 } else {
11 wakeup_softirqd();
12 }
13 }

위 코드에서 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 컨피그에 따라 8번째 줄 혹은 10번째 줄 코드가 컴파일되는 것 같습니다. 이럴 때 라즈비안에서 리눅스를 빌드할 때 생성되는 .config 파일을 열어서 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 가 있는지 확인해야 할까요? 그럴 필요 없이 다음과 같이 전처리 코드를 열어보면 실제 컴파일하는 코드를 보여줍니다.
1 static inline void invoke_softirq(void)
2 {
3 if (!force_irqthreads) {
 # 361 "/home/pi/dev_raspberrian/src/rasp_kernel/linux/kernel/softirq.c"
4   do_softirq_own_stack();
5  } else {
6   wakeup_softirqd();
7  }

3번째 줄 코드부터 볼까요? force_irqthreads 변수를 1로 설정했으면 wakeup_softirqd 함수를 호출해서 ksofrirqd 쓰레드를 깨웁니다. 이 변수는 리눅스 커널을 부팅하기 전 부트로더에서 threadirqs를 커맨드라인으로 전달하면 설정하는데 라즈베리안에서는 0으로 설정돼 있습니다.
[kernel/irq/manage.c]
__read_mostly bool force_irqthreads;
static int __init setup_forced_irqthreads(char *arg)
{
force_irqthreads = true;
return 0;
}
early_param("threadirqs", setup_forced_irqthreads);

커맨드 라인이란 부트로더에서 리눅스 커널을 부팅시킬 때 전달하는 아규먼트와 같은 역할을 합니다. 리눅스 커널이 부팅할 때 이 커맨드 라인을 읽어서 커널 시스템 설정을 합니다. 

이번에는 6번째 줄 코드입니다. do_softirq_own_stack 함수는 바로 __do_softirq 함수를 호출합니다.
static void do_softirq_own_stack(void)
{
__do_softirq();
}

invoke_softirq 함수는 복잡해 보였는데 코드를 분석하니 바로 __do_softirq 함수를 호출하는 간단한 코드였습니다. 

이제 Soft IRQ 의 핵심 코드인 __do_softirq 함수를 분석할 차례입니다. 전체 코드는 다음과 같습니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2{
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
4 unsigned long old_flags = current->flags;
5 int max_restart = MAX_SOFTIRQ_RESTART;
6 struct softirq_action *h;
7 bool in_hardirq;
8 __u32 pending;
9 int softirq_bit;
10
//...
11
12 pending = local_softirq_pending();
13 account_irq_enter_time(current);
14
15 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
16 in_hardirq = lockdep_softirq_start();
17
18 restart:
19 /* Reset the pending bitmask before enabling irqs */
20 set_softirq_pending(0);
21
22 local_irq_enable();
23
24 h = softirq_vec;
25
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;
29
30 h += softirq_bit - 1;
31
32 vec_nr = h - softirq_vec;
33 prev_count = preempt_count();
34
35 kstat_incr_softirqs_this_cpu(vec_nr);
36
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);
//...
40 h++;
41 pending >>= softirq_bit;
42 }
50
43 rcu_bh_qs();
44 local_irq_disable();
45
46 pending = local_softirq_pending();
47 if (pending) {
48 if (time_before(jiffies, end) && !need_resched() &&
49     --max_restart)
50 goto restart;
51
52 wakeup_softirqd();
53 }

우선 __do_softirq 함수에 선언된 지역 변수부터 살펴봅니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2{
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
4 unsigned long old_flags = current->flags;
5 int max_restart = MAX_SOFTIRQ_RESTART;

#define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10

MAX_SOFTIRQ_TIME 매크로는 msecs_to_jiffies(2) 코드로 치환됩니다. msecs_to_jiffies 함수는 현재 시각 기준으로 2밀리 초를 jiffies 값으로 바꾸는 함수입니다. 이 값을 현재 시각을 표현하는 jiffies란 변수에 더해서 end란 지역변수에 저장합니다. 현재 시각에서 2밀리 초를 end에 저장합니다. end란 로컬 변수는 이 __do_softirq 함수 실행 시간을 제한하려는 용도로 씁니다.

다음은 MAX_SOFTIRQ_RESTART 을 max_restart 지역 변수에 설정합니다. 이 값은 10입니다.
__do_softirq 함수에서 max_restart 지역 변수를 10으로 설정해 놓고 이 함수 내 restart 레이블을 실행할 때마다 --max_restart; 연산으로 1만큼 뺍니다. 만약 10번 restart 레이블을 실행해서 max_restart 변수가 0이 되면 restart 레이블 실행을 못하고 __do_softriq 함수는 도중에 실행을 종료합니다.

end, max_restart 이 로컬 변수는 __do_softirq 함수 실행에 대한 중요한 제약 조건을 설정하는 코드이니 잘 기억해야 합니다.

다음은 Soft IRQ 서비스 요청을 점검하는 코드입니다.
12 pending = local_softirq_pending();
13 account_irq_enter_time(current);
14
15 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
16 in_hardirq = lockdep_softirq_start();
17

12번 줄 코드를 전처리 코드로 보면 다음과 같이 irq_stat 전역 변수에서 __softirq_pending 값을 pending 지역 변수로 읽어 옵니다.
(irq_stat[(current_thread_info()->cpu)].__softirq_pending);

pending 지역 변수는 로컬 변수로 선언했으므로 __do_softirq 함수를 실행하는 스택 메모리 공간에 있습니다. 전역 변수에 있는 값을 지역 변수로 복사하는 이유는 __do_softirq 함수가 실행하는 동안 irq_stat이란 전역 변수가 업데이트될 수 있기 때문입니다. 그래서 될 수 있는 대로 전역 변수는 지역 변수에 저장해서 처리해야 동기화 문제를 피할 수 있습니다.

이제는 restart 레이블 코드를 볼 차례입니다.
18 restart:
19 /* Reset the pending bitmask before enabling irqs */
20 set_softirq_pending(0);
21
22 local_irq_enable();
23
24 h = softirq_vec;

이번에도 전처리 코드로 분석하겠습니다.
set_softirq_pending 함수는 다음과 같이 치환됩니다. 해당 CPU의 irq_stat 배열 멤버인 __softirq_pending 변수를 0으로 초기화하는 겁니다.
((irq_stat[(current_thread_info()->cpu)].__softirq_pending) = (0));

24번째 줄 코드에서는 Soft IRQ 서비스 핸들러 정보가 담긴 softirq_vec 전역 변수를 h란 지역 변수에 저장합니다.
이제 Soft IRQ 서비스 핸들러를 호출하는 코드입니다.
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;
29
30 h += softirq_bit - 1;
31
32 vec_nr = h - softirq_vec;
33 prev_count = preempt_count();
34
35 kstat_incr_softirqs_this_cpu(vec_nr);
36
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);
//...
40 h++;
41 pending >>= softirq_bit;
42 }

26번째 줄 코드는 while 문을 구동하는 조건이므로 가장 유심히 눈여겨봐야 합니다.
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;

리눅스 커널에서 제공하는 ffs란 라이브러리 함수를 써서 pending 이란 변수에서 Soft IRQ 서비스를 설정한 비트를 찾습니다. 만약 pending 지역변수가 20이면 2진수로는 10100입니다.

ffs(20)을 입력하면 가장 먼저 1이 세팅된 비트 위치인 2를 알려줍니다.
30번째 코드는 softirqbit에서 1만큼 뺍니다. 이 이유는 뭘까요?
30 h += softirq_bit - 1;

irq_stat[cpu].__softirq_pending 변수에 Soft IRQ 서비스 아이디별로 설정된 비트 값이 Soft IRQ 서비스 핸들러 함수를 저장한 softirq_vec 배열 위치보다 1만큼 크기 때문입니다. 1 << 0(HI_SOFTIRQ) 연산 결과가 1 입니다. 이 관계를 그림으로 정리하면 다음과 같습니다.


각 Soft IRQ 서비스 아이디를 설정하는 __softirq_pending 변수의 비트 필드와 이를 실행하는 Soft IRQ 서비스 핸들러를 저장하는 softirq_vec 배열은 아주 밀접한 관계입니다.

38번째 코드는 Soft IRQ 서비스 핸들러를 호출합니다. 위 그림에서 softirq_vec에 표시된 함수를 호출하는 겁니다.
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);

라즈베리안의 경우 softirq_vec 이란 Soft IRQ 벡터에 등록된 함수들은 다음과 같습니다. 아래 함수 중 하나를 호출하는 것입니다.
v.v %t %d %i %y %l softirq_vec 
  (static struct softirq_action [10]) [D:0x80C02080] softirq_vec = (
    [0] = ((void (*)()) [D:0x80C02080] action = 0x80122888 = tasklet_hi_action),
    [1] = ((void (*)()) [D:0x80C02084] action = 0x80181270 = run_timer_softirq),
    [2] = ((void (*)()) [D:0x80C02088] action = 0x80614684 = net_tx_action),
    [3] = ((void (*)()) [D:0x80C0208C] action = 0x80615AB0 = net_rx_action),
    [4] = ((void (*)()) [D:0x80C02090] action = 0x804279B0 = blk_done_softirq),
    [5] = ((void (*)()) [D:0x80C02094] action = 0x0 = ),
    [6] = ((void (*)()) [D:0x80C02098] action = 0x8012299C = tasklet_action),
    [7] = ((void (*)()) [D:0x80C0209C] action = 0x801588EC = run_rebalance_domains),
    [8] = ((void (*)()) [D:0x80C020A0] action = 0x0 = ),
    [9] = ((void (*)()) [D:0x80C020A4] action = 0x8017ABC4 = rcu_process_callbacks))

37번째와 39번째 코드는 ftrace 로그를 출력하는 코드입니다. 

다음 코드는 pending에서 1로 설정된 비트를 없애는 코드입니다. 그래야 다음에 1로 설정된 비트를 읽을 수 있습니다.
40 h++;
41 pending >>= softirq_bit;

이렇게 while loop을 실행해서 pending 된 Soft IRQ 서비스 핸들러 호출을 마치면 다음 코드를 실행합니다. Soft IRQ 에서 가장 중요한 코드이니 조금 더 집중하시고 봅시다.
46 pending = local_softirq_pending();
47 if (pending) {
48 if (time_before(jiffies, end) && !need_resched() &&
49     --max_restart)
50 goto restart;
51
52 wakeup_softirqd();
53 }

46번째 줄 코드를 보면 다시 Soft IRQ 서비스 요청이 있었는지 확인합니다. __do_softirq 함수 첫 부분에서 Soft IRQ 서비스 요청이 있는지 확인했는데 왜 다시 확인할까요? 그 이유는 이 함수가 irq_exit 함수에서 호출될 수도 있지만 ksoftirqd 프로세스가 실행하는 run_ksoftirqd() 함수에서도 __do_softirq() 함수를 호출할 수 있습니다. ksoftirqd 프로세스 입장에서는 __do_softirq 함수 실행 도중 인터럽트가 다시 발생해서 누군가 Soft IRQ 서비스를 요청했을 수 있기 때문에 이 조건을 다시 검사하는 것입니다.

만약 Soft IRQ 서비스 요청이 있으면 48번 코드를 실행합니다. 여기서 restart 레이블을 10번 이상을 실행했는지와 time_before 함수를 써서 __do_softirq 함수 실행 시간이 2ms을 넘어서지는 않았는지 조건을 검사합니다. 

time_before 함수에 전달하는 jiffies 값과 end의 의미는 뭘까요? jiffies는 48번째 코드를 실행할 때 시각을 의미하고 end란 지역 변수에는 __do_softirq 함수가 처음 실행할 때 시각 정보를 담고 있는 jiffies에 2밀리 초를 jiffies로 환산한 값을 더한 값이 있습니다. 해당 코드를 전처리 코드에서 보면 다음과 같습니다.
void  __do_softirq(void)
 {
  unsigned long end = jiffies + msecs_to_jiffies(2);
  unsigned long old_flags = (current_thread_info()->task)->flags;
  int max_restart = 10;

이 jiffies와 end인자를 time_before란 타이머 함수에 전달해서 호출하면 __do_softirq를 실행한 시간이 2밀리 초를 넘지 않았으면 1을 리턴하고 2밀리 초를 넘으면 0을 리턴합니다.

max_restart 지역 변수는 처음에 10으로 설정했고 restart 레이블을 실행할 때마다 1만큼 뺍니다. 그런데 이 값이 0이면 뭘 의미일까요? 10번 restart 레이블을 실행했다는 정보입니다. __do_softirq 함수 실행 시간이 오래 걸리는 것을 방지하기 위한 코드입니다.
 
이번에는 wakeup_softirqd 함수를 볼 차례입니다.
1 static void wakeup_softirqd(void)
2 {
3 /* Interrupts are disabled: no need to stop preemption */
4 struct task_struct *tsk = __this_cpu_read(ksoftirqd);
5
6 if (tsk && tsk->state != TASK_RUNNING)
7 wake_up_process(tsk);
8 }

4번째 줄 코드를 보면 CPU 별로 생성된 ksoftirqd 쓰레드의 태스크 디스크립터를 가져옵니다. 현재 ksoftirqd 프로세스 실행 중인지를 점검한 후 wake_up_process 함수를 호출해서 ksoftirqd 프로세스를 깨웁니다.

이렇게 __do_softirq 함수가 처리 도중 wakeup_softirqd 함수를 호출하면 ksoftirqd 쓰레드를 깨우는데요. 나중에 프로세스 레벨에서 스케줄링 되어 실행됩니다. 

ksoftirqd 쓰레드는 어떻게 실행하는지 점검해야겠죠? 해당 코드는 다음과 같습니다.
1 static void run_ksoftirqd(unsigned int cpu)
2 {
3 local_irq_disable();
4 if (local_softirq_pending()) {
5 __do_softirq();
6 local_irq_enable();
7 cond_resched_rcu_qs();
8 return;
9 }
10 local_irq_enable();
11}

4번째 줄 코드를 보면 역시 local_softirq_pending 함수를 호출해서 Soft IRQ 서비스 요청이 있었는지 점검 후 __do_softirq함수를 호출합니다. 참고로 smp 핫플러그인 쓰레드로 실행하는 ksoftirqd 쓰레드의 속성은 다음과 같습니다.
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};

여기까지 Soft IRQ 서비스를 어떻게 처리하는지 살펴봤습니다. 여러 코드를 넘나들면서 분석을 했는데요. 이렇게 Soft IRQ는 IRQ Thread 기법에 비해 인터럽트 후반부 처리를 더 세밀하게 처리합니다. 하지만 인터럽트 후반부 관점으로 Soft IRQ 동작을 다음과 같이 정리할 수 있습니다.

인터럽트가 발생하면 인터럽트 핸들러에서 Soft IRQ 서비스 요청을 합니다. 요청한 Soft IRQ 서비스는 인터럽트 핸들러 수행이 끝나면 irq_exit -> invoke_softirq -> __do_softirq 흐름으로 __do_softirq 함수에서 Soft IRQ 서비스를 실행합니다. 그런데 __do_softirq 함수 실행 시간이 2ms이상 이거나 10번 이상 Soft IRQ 서비스 핸들러를 호출하면 ksoftirqd 쓰레드를 깨우고 __do_softirq 함수는 바로 실행을 마칩니다. 이후 ksoftirqd 쓰레드가 깨어나면 이전 __do_softirq 함수가 실행 제한 조건에 걸려 처리 못한 Soft IRQ 서비스를 실행합니다.


"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 

# Reference (인터럽트 후반부 처리)




덧글

댓글 입력 영역