Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

3112
737
82113


[리눅스커널]Soft IRQ 서비스는 누가 언제 처리하나?(2/2): __do_softirq() 분석 6장. 인터럽트 후반부 처리

6.8.3 Soft IRQ 서비스 실행

이어서 Soft IRQ 서비스를 실행하는 세부 코드를 살펴보겠습니다.

__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

먼저 3 번째 줄 코드를 분석합시다.
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;

MAX_SOFTIRQ_TIME 매크로는 msecs_to_jiffies(2) 코드로 치환됩니다. msecs_to_jiffies 함수는 현재 시각 기준으로 2밀리 초를 jiffies 기준값으로 변환합니다. 
이 값을 현재 시각을 표현하는 jiffies란 변수에 더해서 end란 지역변수에 저장합니다. 

현재 시각에서 2밀리 초를 end에 저장하는 것입니다. end란 로컬 변수는 이 __do_softirq() 함수 실행 시간을 제한하려는 용도로 씁니다. __do_softirq() 함수에서 Soft IRQ 서비스 핸들러를 실행하는 도중 실행 시각이 end를 초과하면 함수 실행을 종료합니다. 이 코드는 조금 후에 더 분석할 예정입니다.

다음은 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[(current_thread_info()->cpu)].__softirq_pending);

irq_stat 전역 변수에서 __softirq_pending 값을 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() 함수는 다음과 같이 치환됩니다. 
((irq_stat[(current_thread_info()->cpu)].__softirq_pending) = (0));

해당 CPU의 irq_stat 배열 멤버인 __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입니다.
 
[그림 6.13]  ffs 라이브러리 실행 결과

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 입니다. 이 관계를 그림으로 정리하면 다음과 같습니다.
 
  [그림 6.14] __softirq_pending 변수와 softirq_vec 배열과의 관계 

각 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 벡터에 등록된 함수들은 다음과 같습니다. 아래 함수 중 하나를 호출하는 것입니다.
(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 로그를 출력하는 코드입니다. 
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);

ftrace 는 Soft IRQ 동작을 트레이싱할 수 있는 이벤트를 제공합니다.

다음 코드는 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번째 코드를 실행합니다. 여기서 다음 2가지 조건을 점검합니다. 
1. restart 레이블을 10번 이상을 실행했는지
2. time_before() 함수를 써서 __do_softirq() 함수 실행 시간이 2ms을 넘어서지는 않았는지 

time_before() 함수에 전달하는 jiffies 값과 end의 의미는 뭘까요? jiffies는 48번째 코드를 실행할 때 시각 정보를 담고 있습니다. end란 지역 변수는 __do_softirq() 함수가 처음 실행할 때 시각 정보를 담고 있는 jiffies에 2밀리 초를 jiffies로 환산한 값을 더한 값을 저장합니다. 

jiffies와 end인자로 time_before() 타이머 함수를 호출해서 __do_softirq() 함수를 실행한 시간이 2밀리 초를 넘지 않았으면 1, 2밀리 초를 넘으면 0을 반환합니다.

max_restart 지역 변수는 처음에 10으로 설정했고 restart 레이블을 실행할 때마다 1만큼 뺍니다. 그런데 이 값이 0이면 뭘 의미일까요? 10번 restart 레이블을 실행했다는 정보입니다. __do_softirq() 함수 실행 시간이 오래 걸리는 것을 방지하기 위한 코드입니다.

이번 절에서는 Soft IRQ 서비스를 언제 실행하는지 살펴봤습니다.
  1. 인터럽트 핸들링 후 Soft IRQ 서비스 진입
  2. Soft IRQ 서비스 요청 점검
  3. __do_softirq() 함수에서 Soft IRQ 서비스 핸들러 실행
  - 만약 __do_softirq() 실행 시각이 2밀리초를 넘으면 ksoftirq 스레드를 깨움

이어서 __do_softirq() 함수에서 ksoftirq 스레드를 깨우는 과정을 살펴보겠습니다.
 
6.8.4 ksoftirqd 스레드 깨우기

Soft IRQ 서비스 핸들러를 호출하는 __do_softirq() 함수 실행 시간이 길면 ksoftirqd 스레드를 깨웁니다. 

ksoftirqd 스레드를 깨우는 wakeup_softirqd() 함수 분석하기

이어서 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 스레드의 태스크 디스크립터를 가져옵니다. 

다음에 볼 6 번째 줄 코드는 예외 처리를 위한 조건문입니다. 
 - tsk 이란 지역 변수가 태스크 디스크립터 주소를 저장
 - ksoftirqd 스레드가 TASK_RUNNING 상태가 아닌지 점검

이미 ksoftirqd 프로세스 실행 요청을 한 상태면 ksoftirqd 프로세스를 깨우지 않겠다는 의도입니다.

이 조건을 만족하면 wake_up_process() 함수를 호출해서 ksoftirqd 프로세스를 깨웁니다.

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

ksoftirqd 스레드 핸들러인 run_ksoftirqd() 함수 분석하기
이번에 ksoftirqd 스레드를 어떻게 실행하는지 분석합니다. 다음에 볼 함수는 ksoftirqd 스레드 핸들러인 run_ksoftirqd() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
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 쓰레드의 속성은 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
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 스레드 기법에 비해 인터럽트 후반부 처리를 더 세밀하게 처리합니다. 하지만 인터럽트 후반부 관점으로 Soft IRQ 동작을 다음과 같이 정리할 수 있습니다.
 1. 인터럽트가 발생하면 인터럽트 핸들러에서 Soft IRQ 서비스 요청 
 2. 요청한 Soft IRQ 서비스는 인터럽트 핸들러 수행이 끝나면 다음 함수 흐름으로 Soft IRQ 서비스 실행 
; irq_exit -> invoke_softirq -> __do_softirq 
 3. __do_softirq 함수 실행 시간이 2ms이상 이거나 10번 이상 Soft IRQ 서비스 핸들러를 호출하면 ksoftirqd 스레드를 깨우고 __do_softirq 함수는 바로 실행을 마무리 
 4. ksoftirqd 스레드가 깨어나면 이전 __do_softirq 함수가 실행 제한 조건에 걸려 처리 못한 Soft IRQ 서비스를 실행합니다.

다음 소절에서는 Soft IRQ 에서 Bottom Half 임무를 수행하는 ksoftirqd 프로세스에 대해 알아보겠습니다.

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




핑백

덧글

댓글 입력 영역