Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

16192
888
89789


[리눅스커널]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 서비스 실행 횟수 확인




핑백

덧글

  • 방문자1 2019/07/11 17:17 # 삭제 답글

    정말 디테일하게 작성해주셨네요~!!
    덕분에 심도 깊게 봤습니다.
    유익한 자료 감사합니다.
  • AustinKim 2019/07/11 17:50 #

    응원 감사합니다. 블로그 자주 오셔서 유익한 정보 얻어가세요.
댓글 입력 영역