Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

75261
1501
219117


[리눅스커널] 시간관리: 등록된 동적 타이머 실행 단계 코드 분석 8. 커널 타이머 관리

이제 동적 타이머 핸들러를 실행하는 마지막 단계 함수들을 분석할 차례입니다. 
__collect_expired_timers() 
expire_timers()   
call_timer_fn()

먼저 __collect_expired_timers() 함수를 분석해볼까요?

__collect_expired_timers() 함수 분석 

collect_expired_timers() 함수를 보겠습니다.

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/timer.c]
static int collect_expired_timers(struct timer_base *base,
  struct hlist_head *heads)
{
... return __collect_expired_timers(base, heads);
}

위 코드에서 보이듯 collect_expired_timers() 함수는__collect_expired_timers() 함수를 그대로 호출합니다.

이어서 __collect_expired_timers() 함수를 분석하겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/timer.c]
1 static int __collect_expired_timers(struct timer_base *base,
2     struct hlist_head *heads)
3{
4 unsigned long clk = base->clk;
5 struct hlist_head *vec;
6 int i, levels = 0;
7 unsigned int idx;
8
9 for (i = 0; i < LVL_DEPTH; i++) {
10 idx = (clk & LVL_MASK) + i * LVL_SIZE;
11
12 if (__test_and_clear_bit(idx, base->pending_map)) {
13 vec = base->vectors + idx;
14 hlist_move_list(vec, heads++);
15 levels++;
16 }
...
17 }
18 return levels;
}

먼저 12번째 줄 코드를 보겠습니다.

base->pending_map 비트맵 배열의 idx번째 플래그에 접근해 설정된 동적 타이머가 있는지 점검합니다. __test_and_clear_bit() 함수는 idx번째 비트에 pending_map 비트 맵이 있으면 해당 비트를 0으로 바꾸는 기능입니다. 커널 타이머 해시 테이블에 동적 타이머가 등록됐는지 여부는 pending_map 비트맵으로 알 수 있습니다.

13번째 코드에서는 타이머 벡터 해시 인덱스를 vec로 얻어 오고 14번째 줄 코드에서는 이 결과를 heads란 인자에 저장합니다.

15번째 줄에서는 level이라는 동적 변수를 1만큼 증감합니다. level은 현재 커널 타이머 해시 테이블에 등록된 동적 타이머 개수를 의미합니다.

expire_timers() 함수 분석 

이제 동적 타이머 핸들러를 호출하는 단계까지 왔습니다. 다음 expire_timers() 함수를 분석하겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/timer.c]
01 static void expire_timers(struct timer_base *base, struct hlist_head *head)
02 {
03 while (!hlist_empty(head)) {
04 struct timer_list *timer;
05 void (*fn)(struct timer_list *);
06
07 timer = hlist_entry(head->first, struct timer_list, entry);
08
09 base->running_timer = timer;
10 detach_timer(timer, true);
11
12 fn = timer->function;
13
14 if (timer->flags & TIMER_IRQSAFE) {
15 raw_spin_unlock(&base->lock);
16 call_timer_fn(timer, fn);
17 raw_spin_lock(&base->lock);
18 } else {
19 raw_spin_unlock_irq(&base->lock);
20 call_timer_fn(timer, fn);
21 raw_spin_lock_irq(&base->lock);
22 }
23 }
24 }

expire_timers() 함수 핵심 동작은 다음과 같습니다.

만료된 동적 타이머의 struct timer_list 구조체 주소를 로딩
call_timer_fn() 함수 호출

이 점을 기억하고 코드를 분석하겠습니다.

8번째 줄 코드를 보겠습니다.

07 timer = hlist_entry(head->first, struct timer_list, entry);

struct hlist_head 구조체 필드 중 first에 접근 해 struct timer_list 구조체 주소를 timer에 저장합니다.  

이어서 09~10번째 줄 코드를 보겠습니다.

09 base->running_timer = timer;
10 detach_timer(timer, true);

struct timer_base->running_time에 실행하려는 동적 타이머를 지정합니다. 10번째 줄 코드는 detach_timer() 함수를 호출해 struct timer_list->entry 필드를 NULL로 바꿉니다. 이는 동적 타이머를 비활성화하는 동작입니다. 

이 코드로 다음과 같은 사실을 알 수 있습니다. 

    동적 타이머가 만료할 때 커널 시스템 타이머는 동적 타이머를 해제한다.

mod_timer() 함수를 호출해 동적 타이머를 등록한 후 주기적으로 동적 타이머를 구동시키려면 어떻게 해야 할까요? 

    동적 타이머가 만료할 시점에 동적 타이머를 다시 등록해야 합니다.

이어서 12 번째 줄 코드를 보겠습니다.

12 fn = timer->function;

timer->function은 동적 타이머 핸들러 함수 주소를 저장합니다. 이 주소를 fn에 저장합니다.

이어서 14~22 번째 줄 코드를 보겠습니다.

14 if (timer->flags & TIMER_IRQSAFE) {
15 raw_spin_unlock(&base->lock);
16 call_timer_fn(timer, fn);
17 raw_spin_lock(&base->lock);
18 } else {
19 raw_spin_unlock_irq(&base->lock);
20 call_timer_fn(timer, fn);
21 raw_spin_lock_irq(&base->lock);
22 }

위 코드의 핵심은 동적 타이머를 호출하는 call_timer_fn() 함수를 호출하는 루틴입니다. 한 가지 차이점은 스핀락을 걸 때의 조건입니다.

14~22번째 구간 코드는 if~else문입니다. 먼저 if~else 문 조건을 체크해볼까요?

14~17번째 줄: 스핀락을 걸고 동적 타이머 핸들러 함수 호출
19~21번째 줄: 인터럽트를 비활성화하면서 스핀락을 걸고 동적 타이머 핸들러 함수 호출

먼저 14번째 줄 코드를 보겠습니다.
timer->flags가 TIMER_IRQSAFE이면 스핀락을 걸 때 인터럽트를 비활성화하지 않습니다. 인터럽트 발생으로 동기화 문제가 없으리라 판단해 raw_spin_unlock() 함수를 호출해 call_timer_fn() 함수 동기화를 보장합니다. 

19~21번째 줄 코드는 else문 코드입니다. timer->flags가 TIMER_IRQSAFE가 아닐 때 실행합니다. 동적 타이머 핸들러 함수를 실행할 때 인터럽트 발생하면 동기화 문제가 생길 수는 조건입니다. call_timer_fn() 함수를 호출할 때 인터럽트 비활성화하면서 스핀락을 걸어 주는 동작입니다. 

이렇게 call_timer_fn() 함수는 자주 호출되므로 반드시 동기화를 수행해야 합니다.
 
call_timer_fn() 함수 분석

동적 타이머 핸들러 함수를 직접 호출하는 call_timer_fn() 함수 코드를 보겠습니다.
 
https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/timer.c 
01 static void call_timer_fn(struct timer_list *timer, void (*fn)(struct timer_list *))
03{
04 int count = preempt_count();
...
05 trace_timer_expire_entry(timer);
06 fn(timer);
07 trace_timer_expire_exit(timer);

06번째 줄 코드에서 동적 타이머 핸들러 함수 주소를 가르키는 함수 포인터인 fn를 호출합니다. 이 코드는 다음과 같은 의미입니다. 
  
    동적 타이머 핸들러 함수를 호출합니다. 

이렇게 함수 포인터를 써서 동적 타이머 핸들러를 호출하는 방식을 콜백 함수 호출이라고 말합니다.


그렇다면 call_timer_fn() 함수에 전달하는 2번째 인자인 fn는 어느 함수에서 설정했을까요? 이 의문을 풀려면 expire_timer() 함수를 볼 필요가 있습니다.   

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/timer.c]
01 static void expire_timers(struct timer_base *base, struct hlist_head *head)
02 {
03 while (!hlist_empty(head)) {
04 struct timer_list *timer;
05 void (*fn)(struct timer_list *);
...
06
07 fn = timer->function;
08 if (timer->flags & TIMER_IRQSAFE) {
09 raw_spin_unlock(&base->lock);
10 call_timer_fn(timer, fn);
11 raw_spin_lock(&base->lock);
12 } else {
13 raw_spin_unlock_irq(&base->lock);
14 call_timer_fn(timer, fn);
15 raw_spin_lock_irq(&base->lock);
16 }

위 expire_timers() 함수 07번째 줄 코드를 볼까요?
동적 타이머 자료구조인 struct timer_list 구조체 function 필드에 저장된 동적 타이머 핸들러를 fn에 저장합니다. 다음 10번째와 14번째 줄 코드와 같이 call_timer_fn() 함수 2번째 인자로 fn을 전달합니다.

이어서 05번과 07번째 줄 코드를 보겠습니다.  

05 trace_timer_expire_entry(timer);
06 fn(data);
07 trace_timer_expire_exit(timer);

05번과 07번째 줄 코드는 동적 타이머 동작을 출력하는 timer_expire_entry, timer_expire_exit 이벤트 ftrace를 출력해줍니다. 

“echo 1 > /sys/kernel/debug/tracing/event/timer/timer_expire_entry/enable"
“echo 1 > /sys/kernel/debug/tracing/event/timer/timer_expire_exit/enable"

위 명령어를 입력하면 timer_expire_entry와 timer_expire_exit 이벤트를 킬 수 있습니다.

이렇게 ftrace timer 이벤트를 킨 상태에서 위 05번과 07번째 줄 코드를 실행하면 다음과 같은 ftrace 로그를 볼 수 있습니다.

1 <idle>-0 [002] d.s. 187.040020: timer_expire_entry: timer=ba372d80 function=delayed_work_timer_fn now=4294956001          
2 <idle>-0 [002] dns.  187.040031: sched_wakeup: comm=kworker/2:1 pid=31 prio=120 target_cpu=002
3 <idle>-0     [002] dns.   187.040033: timer_expire_exit: timer=ba372d80

위에서 보이는 1번째와 3번째 ftrace 로그를 해석해볼까요? 

타이머 핸들러는 delayed_work_timer_fn() 함수이고 현재 jiffies는 4294956001이다.  
1번째 줄 로그를 보면 187.040020초이고 3번째 줄 로그는 187.040033초에 실행됐습니다. delayed_work_timer_fn() 함수 실행 시간은 다음 계산식으로 0.013밀리 초입니다.
        0.013 =  187.040033 - 187.040020 

이렇게 ftrace로 timer_expire_entry와 timer_expire_exit 타이머 이벤트를 키면 동적 타이머 핸들러 실행 시간을 알 수 있습니다. 혹시 여러분이 드라이버에서 새롭게 동적 타이머 핸들러 함수를 구현했으면 위 ftrace 이벤트를 키고 핸들러 함수 실행 시간을 확인해봅시다.

여기까지 동적 타이머를 처리하는 커널 코드 분석으로 다음 내용을 배웠습니다. 
동적 타이머 초기화
동적 타이머 등록
커널 시스템 타이머가 만료할 동적 타이머를 처리

다음 절에서는 라즈베리파이로 동적 타이머를 디버깅하는 실습을 진행합니다. 
코드를 입력하고 ftrace 로그를 분석하면 분석한 내용을 더 빨리 이해할 수 있니 꼭 따라해 봤으면 좋겠습니다.

* 강의 동영상도 있으니 같이 들으시면 좋습니다.




#커널 시간관리 목차
커널 타이머 관리 주요 개념 소개
jiffies란
커널 타이머 제어
동적 타이머 초기화
동적 타이머 등록하기
동적 타이머는 누가 언제 실행하나?
라즈베리파이 커널 타이머 실습 및 로그 분석

"혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답글 올려드리겠습니다!"


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2


 


핑백

덧글

  • 최재국 2020/09/05 15:27 # 삭제 답글

    timer_list의 구조체를 보니까 동적타이머 함수의 매개변수로 넘겨주는 필드(unsigned long data)가 없어졌는데

    static void expire_timers(struct timer_base *base, struct hlist_head *head) 함수의 call_timer_fn(timer, fn) 까지는 맞는거 같은데

    static void call_timer_fn(struct timer_list *timer, void (*fn)(unsigned long), unsigned long data) 함수에서 호출하는 fn(data)은 unsigned long data가 있네요 ..

    제가 뭘 잘못알고 있는건가요??
  • AustinKim 2020/09/09 11:43 #

    블로그의 글을 정말 꼼꼼히 읽으시는 것 같습니다. ^^

    확인해보니, 블로그에 소스 코드의 내용이 예전 4.14 버전으로 기재돼 있네요.
    call_timer_fn() 함수의 구현부는 다음과 같습니다.

    https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c
    static void call_timer_fn(struct timer_list *timer, void (*fn)(struct timer_list *))
    {
    int count = preempt_count();
    ...
    trace_timer_expire_entry(timer);
    fn(timer);
    trace_timer_expire_exit(timer);
  • ym0914 2021/01/12 08:46 # 삭제 답글

    expire_timers() 를 보면

    14if (timer->flags & TIMER_IRQSAFE) {
    15raw_spin_unlock(&base->lock);
    16call_timer_fn(timer, fn);
    17raw_spin_lock(&base->lock);
    18} else {
    19raw_spin_unlock_irq(&base->lock);
    20call_timer_fn(timer, fn);
    21raw_spin_lock_irq(&base->lock);
    22}

    raw_spin_unlock 후 call_timer_fn() 를 호출하는데요, spinlock 을 해제하는 unlock 을 호출 후 call_timer_fn() 을 호출하는 이유가 있을까요?
  • AustinKim 2021/01/13 12:10 #

    문의주신 루틴은 아래 링크의 패치 중 하나인데요.
    https://lore.kernel.org/patchwork/patch/690072/

    리눅스 커널의 최고 수준의 개발자(탑3)인 'Thomas Gleixner' 분의 커밋입니다.

    위와 같이 스핀락을 다시 풀어주는 이유를 말씀드리자면, 굉장히 긴데요. 간단히 요약하면;

    expire_timers() 함수를 호출하기 전에 __run_timers() 함수에서 &base->lock 주소로 이미 스핀락을 걸고 있습니다.
    그런데 동적 타이머 핸들러에서는 각각 드라이버에서 처리할 코드가 수행된 후, add_timer() 함수나 add_timer_on() 함수를 호출해 다시 동적 타이머를 설정하는 경우가 많습니다. 이 시점에 jiffies나 struct timer_base의 필드 값이 업데이트 되어야 정밀히 타이머를 제어할 수 있습니다.

    비슷한 의도로 작성된 패치는 아래 링크와 같으니 참고하시면 좋겠습니다.
    https://lore.kernel.org/patchwork/patch/614818/

    감사합니다.
  • ym0914 2021/01/15 14:28 # 삭제 답글

    히스토리가 따로 있었군요.어려운 내용이네요..ㅠㅠ
    바쁘실텐데 찾아봐주셔서 감사합니다.
    좋은 주말 되세요!!
댓글 입력 영역