Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] 커널타이머 - 동적 타이머 실행(__mod_timer) 코드 분석 [라즈베리파이] 타이머관리

이제 add_timer()와 mod_timer() 함수들의 구현부를 살펴 보겠습니다.

add_timer() 함수 구현부를 보면 mod_timer() 함수를 호출하니 기능적으로 add_timer() 함수와 mod_timer() 함수는 차이가 없습니다.
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}

int mod_timer(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, false);
}

mod_timer() 함수는 __mod_timer() 함수를 호출하니 동적 타이머를 실행 함수는 __mod_timer()임을 알 수 있습니다.

여기서 __mod_timer() 함수를 제대로 분석하지 않았는데 이 함수를 핵심 함수라고 유추했습니다. 그 이유는 이 함수 앞에 __이란 기호가 붙었기 때문입니다. __가 붙은 함수는 커널 함수 내에서만 호출하면 다른 커널 서브 시스템에서 호출하지 못합니다. 

리눅스 커널에서 가장 유명한 schedule() 함수를 예를 들어 봅시다.
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;

sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}

schedule() 함수에서 __schedule() 함수를 호출합니다. 실제 세부 스케쥴링을 관련 세부 동작은 __schedule() 함수에서 수행합니다.

다시 동적 타이머 실행 분석으로 돌아가서 __mod_timer() 함수를 봅시다. 주석문을 뺀 함수 코드는 다음과 같습니다.
1 static inline int
2 __mod_timer(struct timer_list *timer, unsigned long expires, bool pending_only)
3 {
4 struct timer_base *base, *new_base;
5 unsigned int idx = UINT_MAX;
6 unsigned long clk = 0, flags;
7 int ret = 0;
8
9 BUG_ON(!timer->function);
10
11 if (timer_pending(timer)) {
12 if (timer->expires == expires)
13 return 1;
14 base = lock_timer_base(timer, &flags);
15 forward_timer_base(base);
16
17 clk = base->clk;
18 idx = calc_wheel_index(expires, clk);
19 if (idx == timer_get_idx(timer)) {
20 timer->expires = expires;
21 ret = 1;
22 goto out_unlock;
23 }
24 } else {
25 base = lock_timer_base(timer, &flags);
26 forward_timer_base(base);
27 }
28
29 ret = detach_if_pending(timer, base, false);
30 if (!ret && pending_only)
31 goto out_unlock;
32
33 new_base = get_target_base(base, timer->flags);
34
35 if (base != new_base) {
36 if (likely(base->running_timer != timer)) {
37 /* See the comment in lock_timer_base() */
38 timer->flags |= TIMER_MIGRATING;
39
40 spin_unlock(&base->lock);
41 base = new_base;
42 spin_lock(&base->lock);
43 WRITE_ONCE(timer->flags,
44    (timer->flags & ~TIMER_BASEMASK) | base->cpu);
45 forward_timer_base(base);
46 }
47 }
48
49 debug_activate(timer, expires);
50
51 timer->expires = expires;
52 if (idx != UINT_MAX && clk == base->clk) {
53 enqueue_timer(base, timer, idx);
54 trigger_dyntick_cpu(base, timer);
55 } else {
56 internal_add_timer(base, timer);
57 }

우선 __mod_timer() 함수에 전달하는 인자의 의미를 알아보겠습니다.
timer: struct timer_list 구조체 타입인 timer란 인자는 로컬 타이머에서 지정한 값들을 담고 있습니다.
expires: 만료될 HZ 단위 시간 정보를 저장하고 있습니다.
pending_only: __mode_timer에서 false로 지정하므로 0입니다.

해당 인자들의 타입은 다음 코드를 보면 알 수 있습니다.
1 static inline int
2 __mod_timer(struct timer_list *timer, unsigned long expires, bool pending_only)

이제 코드 분석으로 들어갑니다. 먼저 9번째 줄 코드를 보겠습니다.
9 BUG_ON(!timer->function);

struct timer_list 멤버 변수에 타이머 핸들러 함수가 NULL이면 BUG_ON() 매크로 함수가 실행해서 커널 패닉을 유발합니다. 디바이스 드라이버에서 동적 타이머 핸들러를 struct timer_list.function에 지정하지 않을 경우 실행합니다. 이 코드로 디바이스 드라이버에서 동적 타이머 핸들러를 반드시 설정하고 타이머를 실행해야 한다는 점을 알 수 있습니다.

다음 11줄 코드를 보겠습니다.
11 if (timer_pending(timer)) {
12 if (timer->expires == expires)
13 return 1;

timer_pending() 함수는 struct timer_list 멤버 변수 중 entry.pprev 값이 NULL이 아니면 1을 리턴합니다.
static inline int timer_pending(const struct timer_list * timer)
{
return timer->entry.pprev != NULL;
}

커널 타이머 관점으로 이 코드의 의미를 분석하면 이미 동적 타이머를 실행한 경우 1을 리턴합니다. 동적 타이머를 실행하면 timer->entry.pprev 포인터는 per-cpu타입 timer_base 변수의 벡터 해시 테이블 주소를 가르키고 있기 때문입니다.

이제부터 11~23번 줄 코드를 봅시다. 
11 if (timer_pending(timer)) {
12 if (timer->expires == expires)
13 return 1;
14 base = lock_timer_base(timer, &flags);
15 forward_timer_base(base);
16
17 clk = base->clk;
18 idx = calc_wheel_index(expires, clk);
19 if (idx == timer_get_idx(timer)) {
20 timer->expires = expires;
21 ret = 1;
22 goto out_unlock;
23 }

11줄 코드는 timer_pending() 함수가 1을 리턴하는지 점검해서 동적 타이머를 이미 실행하는지 확인합니다.

여기서 잠깐 12~23번 줄 코드가 실행하는 시나리오를 그려 보겠습니다. 12~23줄 코드가 다양한 예외 조건을 만족하기 위한 코드이기 때문입니다.

현재 시각을 나타내는 jiffies 값이 1000이고 동적 타이머 만료 시각을 2초 뒤인 1200(1000 + 2*Hz)으로 설정했다고 가정합시다.

첫 번째 상황을 생각해봅시다. 1초 후 jiffies는 1100이 됩니다. 이때 만료 시각을 1200값으로 중복해서 다시 동적 타이머 시각을 설정 후 mod_timer()를 호출합니다. 이 경우 12~13줄 코드를 실행해서 함수를 빠져나옵니다. 12번 줄은 같은 만료 시각으로 중복해서 동적 타이머를 실행하는지 점검하기 때문입니다.

두 번째 상황입니다. 1초 후에 만료 시각을 3초 뒤로 설정하고 싶은 경우입니다. 지금 시각이 HZ 단위로 jiffies는 1100이므로 만료 시각은 1400(jiffies + 3*HZ)으로 설정해서 mod_timer()을 설정해야 합니다. 

다시 설정한 동적 타이머 만료 시각이 기존에 설정한 만료 시각보다 크면 __mod_timer()를 계속 실행해서 동적 타이머를 설정합니다. 반대로 재설정한 만료시각과 기존에 설정한 만료시각 차이가 크지 않으면 19~21번 줄 코드를 실행한 후 out_unlock 레이블로 점프해서 함수 실행을 종료합니다.

이제 14번 코드부터 분석하겠습니다.
14 base = lock_timer_base(timer, &flags);

14줄 코드는 lock_timer_base() 함수를 호출해서 struct timer_base 구조체 포인터 타입 변수인 base에 리턴값을 받습니다. 

lock_timer_base() 함수를 호출하면 get_timer_base() -> get_timer_cpu_base() 순서로 함수를 호출해서 per-cpu 오프셋을 적용한 timer_base 주소를 읽어 옵니다. 이때 처음 동적 타이머를 초기화할 때 실행한 CPU 번호인 struct timer_list.flags 값 기준으로 per-cpu timer_bases 전역 변수 주소를 base이란 지역 변수로 읽어 옵니다. 해당 코드는 다음과 같습니다.
1 static inline struct timer_base *get_timer_cpu_base(u32 tflags, u32 cpu)
2{
3 struct timer_base *base = per_cpu_ptr(&timer_bases[BASE_STD], cpu);

timer_bases이란 전역변수는 per-cpu 타입입니다. timer_bases 전역변수가 위치한 주소 기준으로 cpu 개수만큼 struct time_base 구조체 공간이 있습니다. 라즈베리파이는 CPU 개수가 4개이니 4개의 struct time_base 구조체 공간이 있습니다.

위 get_timer_cpu_base() 함수의 3줄 코드를 실행하면 현재 실행 중인 CPU번호에 해당하는 per-cpu timer_bases 주소 공간을 알 수 있습니다.

15줄 코드를 봅시다. 
15 forward_timer_base(base);

forward_timer_base() 함수를 호출해서 struct timer_base.clk 멤버를 현재 시각인 jiffies 값으로 저장합니다. struct timer_base.clk 멤버 변수는 커널 시스템 타이머 실행 여부를 판단하는 중요한 정보를 담고 있습니다.

17줄은 struct timer_base.clk 멤버 변수 값을 clk로 저장합니다.
17 clk = base->clk;

18줄은 calc_wheel_index() 함수를 호출해서 커널 타이머 벡터 인덱스를 읽습니다. 
18 idx = calc_wheel_index(expires, clk);

커널 타이머 해시 벡터는 동적 시간을 만료 시각 기준으로 동적 타이머 64(1 << 6) 개를 8개씩 묶어서 관리합니다. 이 커널 타이머 해시 벡터 인덱스의 범위는 0부터 512입니다.

다음은 timer_bases 전역 변수의 선언부입니다. 14줄 코드를 보면 WHEEL_SIZE(512) 크기 배열을 볼 수 있습니다. WHEEL_SIZE 매크로가 커널 타이머 해시 벡터 인덱스 크기입니다.
[kernel/time/timer.c]
1 static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);
3 struct timer_base {
4 raw_spinlock_t lock;
5 struct timer_list *running_timer;
6 unsigned long clk;
7 unsigned long next_expiry;
8 unsigned int cpu;
9 bool migration_enabled;
10 bool nohz_active;
11 bool is_idle;
12 bool must_forward_clk;
13 DECLARE_BITMAP(pending_map, WHEEL_SIZE);
14 struct hlist_head vectors[WHEEL_SIZE];
15} ____cacheline_aligned;

struct timer_base.vectors란 멤버는 512개의 배열임을 알 수 있습니다. 512개의 배열을 8개씩 나눠서 관리하는 겁니다.

이제 19줄을 봅시다.
19 if (idx == timer_get_idx(timer)) {
20 timer->expires = expires;
21 ret = 1;
22 goto out_unlock;
23 }

현재 시각 기준으로 계산한 커널 타이머 해시 벡터 인텍스인 idx와 이미 동적 타이머를 실행했을 때 설정했던 커널 타이머 해시 벡터 인텍스를 비교해서 같으면 20줄 코드를 실행합니다. 재설정하는 동적 타이머 만료 시각과 기존에 설정한 만료 시각 차이가 크지 않을 경우 19번 줄 코드 조건을 만족합니다. 동적 타이머 만료 시각을 다시 설정하고 out_unlock 레이블을 이동해서 __mod_timer() 함수를 빠져나옵니다.

다음 24줄 코드를 봅시다.
11 if (timer_pending(timer)) {
24 } else {
25 base = lock_timer_base(timer, &flags);
26 forward_timer_base(base);
27 }

11번 줄 코드와 같이 timer_pending()이란 함수가 0을 리턴했을 때 else문입니다. 동적 타이머를 처음 실행하거나 동적 타이머를 이미 취소한 후 다시 실행할 때 25~26줄 코드가 실행됩니다. 동적 타이머를 초기화했을 때 CPU 번호가 저장된 struct timer_list.flags 멤버 변수 기준으로 per-cpu 타입 timer_base 전역 변수 주소를 읽어 base이란 지역 변수로 가져옵니다.

26 줄 코드는 forward_timer_base() 함수를 호출해서 struct timer_base.clk 멤버를 현재 시각인 jiffies 값으로 저장합니다.

다음 29~33줄 코드를 봅시다.
29 ret = detach_if_pending(timer, base, false);
30 if (!ret && pending_only)
31 goto out_unlock;
32
33 new_base = get_target_base(base, timer->flags);


29줄 코드는 detach_if_pending() 함수를 실행해서 이미 timer_base 타이머 해시 벡터인 timer_base.vector[512]에 지정한 링크를 끊습니다. detach_if_pending() 함수는 이미 동적 타이머를 실행했을 경우 동작합니다. 참고로  timer_base 타이머 해시 벡터 자료 구조는 다음 구조체 멤버와 같습니다.
struct timer_base.vectors[WHEEL_SIZE];

 struct timer_base {
raw_spinlock_t lock;
struct timer_list *running_timer;
...
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
struct hlist_head vectors[WHEEL_SIZE];
} ____cacheline_aligned;

get_target_base() 함수는 get_timer_this_cpu_base() 함수 혹은 get_timer_cpu_base() 함수에 접근합니다.
static inline struct timer_base *
get_target_base(struct timer_base *base, unsigned tflags)
{
#ifdef CONFIG_SMP
if ((tflags & TIMER_PINNED) || !base->migration_enabled)
return get_timer_this_cpu_base(tflags);
return get_timer_cpu_base(tflags, get_nohz_timer_target());
#else
return get_timer_this_cpu_base(tflags);
#endif
}

static inline struct timer_base *get_timer_this_cpu_base(u32 tflags)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

코드 흐름을 살펴보면 get_timer_this_cpu_base() 함수를 통해 timer_bases이란 per-cpu 타입 변수를 가져옵니다. 참고로 this_cpu_ptr() 함수로 per-cpu 타입 변수에 접근하면 현재 실행 중인 CPU 번호에 해당하는 per-cpu 변수 포인터를 가져 옵니다.

다시 35줄 코드 분석으로 돌아오겠습니다.
35 new_base = get_target_base(base, timer->flags);

get_target_base() 함수를 호출하면 처음 동적 타이머를 설정했을 때 CPU 번호 기준으로  per-cpu 타입 timer_bases 변수의 주소를 읽어 옵니다. 여기서 동적 타이머를 초기화할 때 CPU 번호는 timer->flags 값으로 가져옵니다.

계속 struct timer_list 구조체인 timer->flags 기준으로 per-cpu timer_base 주소를 읽어오는 이유를 알아봅시다.

do_init_timer () 함수에서 로컬 타이머를 초기화할 때 timer->flags 멤버에 현재 코드가 돌고 있는 CPU번호를 다음 5줄 코드와 같이 지정했습니다.
1 static void do_init_timer(struct timer_list *timer, unsigned int flags,
2   const char *name, struct lock_class_key *key)
3 {
4 timer->entry.pprev = NULL;
5 timer->flags = flags | raw_smp_processor_id();
6 lockdep_init_map(&timer->lockdep_map, name, key, 0);
7}

이 값을 바탕으로 per-cpu 오프셋을 결정하는 것입니다. init_timer() 함수를 호출하는 코드가 CPU1에서 실행 중 이었다면 per-cpu1에 해당하는 per-cpu timer_base 변수의 CPU1 공간 주소를 포인터 변수로 가져옵니다. 처음 동적 타이머를 초기화할 때 별 의미 없는 코드로 보였지만 코드 전체를 읽고 나니 중요한 루틴임을 알 수 있습니다.

다음은 로컬 타이머를 타이머에 큐잉하는 코드입니다.
11 if (timer_pending(timer)) {
12 if (timer->expires == expires)
13 return 1;
14 base = lock_timer_base(timer, &flags);
15 forward_timer_base(base);
16
17 clk = base->clk;
18 idx = calc_wheel_index(expires, clk);
24 } else {
25 base = lock_timer_base(timer, &flags);
26 forward_timer_base(base);
27 }
51 timer->expires = expires;
52 if (idx != UINT_MAX && clk == base->clk) {
53 enqueue_timer(base, timer, idx);
54 trigger_dyntick_cpu(base, timer);
55 } else {
56 internal_add_timer(base, timer);
57 }

51~57 줄 코드 이해를 돕기 위해 11~27번 줄 코드를 같이 봅시다.

이미 동적 타이머를 실행한 상태에서 __mod_timer() 함수를 실행하면 11~23줄 코드를 실행합니다. 이후 53~54번 줄 코드를 실행합니다.

11~23번 코드에서 이미 커널 타이머 해시 벡터 인텍스를 계산과 예외 처리를 했기 때문입니다. enqueue_timer() 함수를 호출해서 동적 타이머를 struct timer_base.vector 배열에 등록하는 과정만 남았습니다.

처음 동적 타이머를 실행한 경우 56줄 코드를 실행해서 internal_add_timer() 함수를 호출합니다. 다음 코드를 보면 internal_add_timer() 함수는 __internal_add_timer() 함수를 호출합니다.
static void
internal_add_timer(struct timer_base *base, struct timer_list *timer)
{
__internal_add_timer(base, timer);
trigger_dyntick_cpu(base, timer);
}

__internal_add_timer() 함수를 보면 HZ 단위 타이머 만료 시간과 현재 시각 정보인 base->clk 기준으로 calc_wheel_index () 함수를 호출해서 타이머 해시 벡터 인덱스를 얻어 옵니다.
static void
__internal_add_timer(struct timer_base *base, struct timer_list *timer)
{
unsigned int idx;

idx = calc_wheel_index(timer->expires, base->clk);
enqueue_timer(base, timer, idx);
}

이후 enqueue_timer() 함수를 호출합니다.

다음은 if~else 문에서 공통으로 접근하는 enqueue_timer() 함수를 살펴볼 차례입니다. 
1 static void enqueue_timer(struct timer_base *base, struct timer_list *timer,
2   unsigned int idx)
3 {
4 hlist_add_head(&timer->entry, base->vectors + idx);
5 __set_bit(idx, base->pending_map);
6 timer_set_idx(timer, idx);
7}

이 함수에 전달되는 첫 번째 인자인 struct timer_base *base는 per-cpu 타입 timer_bases 전역 변수에서 현재 구동 중인 CPU에 해당하는 오프셋을 적용한 주소를 저장하고 있습니다.

두 번째 인자인 struct timer_list *timer는 이번에 설정한 로컬 타이머 포인터입니다. 

4번 줄 코드는 base->vectors 멤버인 타이머 해시 벡터 테이블 시작 주소에 타이머 해시 벡터 인덱스를 더합니다. struct timer_base 구조체 멤버인 vectors 시작 주소 기준으로 idx만큼 위치한 주소에 timer->entry 주소를 저장하는 겁니다. 또한 struct timer_base 구조체 pending_map 멤버에 pending 비트를 설정합니다.

여기까지 동적 타이머를 설정하면 처리하는 데이터와 관련 자료구조를 살펴봤습니다. 다음에 이어서 동적 타이머 만료 시간에 동적 타이머를 처리하는 동작을 살펴보겠습니다.




핑백

덧글

댓글 입력 영역