<kernel v5.4>
Lowres(Low Resolution) Timer
커널에서 사용하는 jifffies 기반의 타이머 tick을 소프트웨어 기법으로 구현한 타이머이다. 커널이 타이머를 요청할 때 만료 시간을 기재하는데 lowres 타이머는 정확하게 그 만료시점에 깨어나는 것을 보장하지 못하고 요청한 만료 시간의 최대 1/8(12.5%)의 지연된 오차를 가진다. 그 외 특징으로 lowres 타이머는 특정 아키텍처와 무관한 구조이다.
새로운 non-cascading wheel 구조 (v4.8~)
lowres 타이머는 커널 v4.8-rc1 에서 새로운 변화를 보여주면서 lowres 타이머에 대한 전반적인 설계가 변경되어 non-cascading wheel 모델이 소개되었다.
- 타이머 휠에 등록된 타이머들은 cascading 하는 작업이 없어졌으므로 이로 인한 오버헤드가 줄었다.
- fast lookup: 만료 타이머를 찾기 위한 lookup이 빨라졌다.
- slack 관련한 동작과 API들도 더 이상 필요없게되어 삭제되었다.
- 직전 implementation한 로직과 유사하게 요청한 만료 시각에 오차가 발생하는데, 최대 약 1/8(12.5%) 지연된 오차를 가진다.
- 예) 40시간 후에 타이머를 동작시키게 하였지만 4.6시간 더 오래 걸려 타이머가 동작한다.
- 참고:
- timers: Switch to a non-cascading wheel (2016, v4.8-rc1)
- Reinventing the timer wheel | LWN.net
타이머 리스트
admin 권한으로 타이머 리스트를 보려면 다음과 같이 한다. (커널 v4.8~)
$ cat /proc/timer_list Timer List Version: v0.8 HRTIMER_MAX_CLOCK_BASES: 8 now at 79245435304 nsecs cpu: 0 clock 0: .base: (____ptrval____) .index: 0 .resolution: 1 nsecs .get_time: ktime_get .offset: 0 nsecs active timers: #0: <(____ptrval____)>, tick_sched_timer, S:01 # expires at 79260000000-79260000000 nsecs [in 14564696 to 14564696 nsecs] #1: <(____ptrval____)>, it_real_fn, S:01 # expires at 79374561805-79374561805 nsecs [in 129126501 to 129126501 nsecs] #2: <(____ptrval____)>, hrtimer_wakeup, S:01 # expires at 77374835623-79874835623 nsecs [in -1870599681 to 629400319 nsecs] ...
타이머 및 벡터 관리 구조
두 개의 타이머 베이스
다음 그림과 같이 nohz에서 사용하는 타이머 베이스까지 두 개로 나뉘어 관리된다. 스케줄틱이 발생할 때마다 두 개의 타이머 베이스의 만료된 타이머들을 깨워 콜백함수를 호출한다.
다음 그림은 타이머들이 해시 벡터리스트로 구현되어 처리되는 모습을 보여준다. 100hz 시스템의 경우 최대 8 단계, 그 외의 시스템은 최대 9 단계 레벨로 구성된다.
- 각 단계의 단위 틱은 2^(lvl*3) 이다. 즉 레벨 0는 1틱, 레벨 1은 8틱, 레벨 2는 64틱이다.
- 스케줄 틱마다 base->clk의 값도 증가된다. 이 값으로 각 레벨의 해시 인덱스를 산출하고 이 인덱스 값에 따른 타이머들을 깨워 호출한다.
timer_list 구조체
include/linux/timer.h
struct timer_list { /* * All fields that change during normal runtime grouped to the * same cacheline */ struct hlist_node entry; unsigned long expires; void (*function)(struct timer_list *); u32 flags; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
타이머마다 만료 시각과 호출될 콜백 함수 정보가 담겨있다.
- entry
- 동적 타이머들을 타이머 벡터 리스트로 연결 시 사용한다.
- expires
- 타이머가 만료될 미래의 jiffies 시점을 지정한다.
- function
- 타이머 만료 시 실행할 함수의 주소를 저장한다.
- flags
- TIMER_CPUMASK
- 타이머 휠 인덱스 값으로 10비트를 사용하여 저장된다.
- TIMER_MIGRATING
- migration 진행중인 타이머 여부 (타이머 cpu 이동중)
- TIMER_DEFERRABLE
- 지연 타이머 여부
- deferrable 전용 타이머 베이스를 사용한다.
- TIMER_PINNED
- cpu 고정(pinned) 타이머 여부
- TIMER_IRQSAFE
- 타이머가 인터럽트 핸들러에서 사용되지 않음을 나타낸다.
- 인터럽트 latency 성능을 위해 추가되는 플래그로 내부에서 spinlock 시 인터럽트를 disable 하지 않는다.
- TIMER_CPUMASK
timer_base 구조체
tvec_base 구조체명이 timer_base 구조체명으로 변경되었다.
- 참고: timers: Give a few structs and members proper names (2016, v4.8-rc1)
kernel/time/timer.c
struct timer_base { raw_spinlock_t lock; struct timer_list *running_timer; #ifdef CONFIG_PREEMPT_RT spinlock_t expiry_lock; atomic_t timer_waiters; #endif unsigned long clk; unsigned long next_expiry; unsigned int cpu; bool is_idle; bool must_forward_clk; DECLARE_BITMAP(pending_map, WHEEL_SIZE); struct hlist_head vectors[WHEEL_SIZE]; } ____cacheline_aligned;
타이머들이 관리되는 타이머 베이스는 per-cpu로 관리되며, 타이머 리스트는 해시 벡터 휠로 관리된다.
- lock
- 타이머 벡터 리스트 조작시 사용할 lock
- running_timer
- 타이머가 만료되어 현재 함수를 실행중인 타이머를 가리킨다.
- clk
- 처리할 시각(틱)이다. nohz 상태에 따라 동작이 상이하다.
- nohz가 동작하지 않는 경우 이 시각은 현재 시각(jiffies)과 동일한 값이 되도록 매 스케줄 틱마다 증가된다.
- nohz가 동작하는 경우에는 jiffies 보다 지연될 수 있다. cpu가 nohz에서 탈출할 때 지연된 시간만큼 처리한다.
- 지연된 시간만큼 루프를 반복하며 틱을 처리하면 성능이 떨어지므로 nohz optimization 기법을 사용하여 처리하지 않아도 되는 틱은 생략하게 한다.
- 처리할 시각(틱)이다. nohz 상태에 따라 동작이 상이하다.
- next_expiry
- 다음 만료될 타이머의 시각(틱)
- cpu
- 현재 타이머 벡터 관리가 동작하는 cpu id
- is_idle
- 타이머 베이스의 idle 상태
- must_forward_clk
- nohz에서 타이머 베이스의 clk를 forward 한다. (nohz optimization)
- pending_map
- 펜딩 비트맵으로 타이머 휠 인덱스마다 1비트를 사용한다.
- 타이머 휠의 인덱스 비트가 1로 설정되면 타이머가 1 개 이상이 대기중임을 의미한다.
- vectors[]
- 타이머들이 대기하는 타이머 휠 리스트이며, 해시 벡터로 구현되었다.
timer_bases[]
kernel/time/timer.c
static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);
timer_base는 성능향상을 위해 per-cpu를 사용하여 관리한다. nohz를 사용하지 않으면 하나의 타이머 베이스에 관리된다. 그러나 nohz를 사용하는 경우 다음과 같이 두 개의 타이머 휠로 나누어 관리된다.
- BASE_STD
- 기본(standard) 타이머를 위해 사용된다.
- BASE_DEF
- defferable 타이머를 위해 사용된다.
jiffies & 초기값
jiffies 값은 스케줄 틱이 발생할 때 마다 1씩 증가한다. 스케줄 틱은 CONFIG_HZ에 정한 시간에 한 번씩 발생한다.
- 예) 100hz 시스템인 경우 1초에 100번의 스케줄 틱이 발생한다. 따라서 10ms 주기마다 1 tick이 발생하고 jiffies 값도 1씩 증가된다.
INITIAL_JIFFIES
include/linux/jiffies.h
/* * Have the 32 bit jiffies value wrap 5 minutes after boot * so jiffies wrap bugs show up earlier. */
#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))
jiffies 초기값은 32bit 시스템에서 부트 후 약 5분이내에 overflow 될 수 있는 값을 주었다. 64비트 시스템에서는 overflow되려면 어마 어마한 시간이 흘러야 하므로 overflow될 걱정이 없다.
- 초기값
- 32bit 예) HZ=250
- 0xfffe_db08 (-75000)
- 64bit 예) HZ=250
- 0x0000_0000_fffe_db08
- 32bit 예) HZ=250
APIs
주요 API
컴파일 타임에 정적으로 타이머 생성 및 초기화
- DEFINE_TIMER()
런타임에 동적으로 타이머 생성 및 초기화
- timer_setup()
- timer_setup_on_stack()
타이머 베이스에 타이머 추가/변경/삭제
- add_timer()
- mod_timer()
- del_timer()
시간 비교 API
jiffies 값을 읽어 시간을 직접 비교하는 경우 jiffies overflow 되는 시점에서 시간 비교가 의도치 않는 반대의 결과를 얻을 수 있다. 따라서 다음 함수들을 사용하여야 정확한 결과를 나타내게 할 수 있으므로 절대 jiffies 시간을 직접 비교 사용하는 일이 없도록 해야 한다.
- 2개 시간 비교 함수
- time_before(a,b)
- a가 b 보다 먼저 시간값이면 true
- time_after(a,b)
- a가 b 보다 나중 시간값이면 true
- time_before_eq(a,b)
- a 가 b보다 먼저이거나 같은 시간이면 true
- time_after_eq(a,b)
- a 가 b 보다 나중이거나 같은 시간이면 true
- time_before(a,b)
- jiffies와 시간 비교 함수
- time_is_before_jiffies(a)
- a가 jiffies 보다 먼저 시간값이면 true
- time_is_after_jiffies(a)
- a가 jiffies 보다 나중 시간값이면 true
- time_is_before_eq_jiffies(a)
- a 가 jiffies 보다 먼저이거나 같은 시간이면 true
- time_is_after_eq_jiffies(a)
- a 가 jiffies 보다 나중이거나 같은 시간이면 true
- time_is_before_jiffies(a)
관련 API
- msleep()
- msleep_interruptible()
- schedule_timeout_interruptible()
- schedule_timeout_killable()
- schedule_timeout_uninterruptible()
- schedule_timeout_idle()
- schedule_timeout()
타이머 추가/삭제
타이머 추가
add_timer()
kernel/time/timer.c
/** * add_timer - start a timer * @timer: the timer to be added * * The kernel will do a ->function(->data) callback from the * timer interrupt at the ->expires point in the future. The * current time is 'jiffies'. * * The timer's ->expires, ->function (and if the handler uses it, ->data) * fields must be set prior calling this function. * * Timers with an ->expires field in the past will be executed in the next * timer tick. */
void add_timer(struct timer_list *timer) { BUG_ON(timer_pending(timer)); mod_timer(timer, timer->expires); } EXPORT_SYMBOL(add_timer);
동적 타이머를 요청한다.
- mod_timer()를 호출하여 만료 시간을 변경한다.
타이머 삭제
del_timer()
kernel/time/timer.c
/** * del_timer - deactive a timer. * @timer: the timer to be deactivated * * del_timer() deactivates a timer - this works on both active and inactive * timers. * * The function returns whether it has deactivated a pending timer or not. * (ie. del_timer() of an inactive timer returns 0, del_timer() of an * active timer returns 1.) */
int del_timer(struct timer_list *timer) { struct tvec_base *base; unsigned long flags; int ret = 0; debug_assert_init(timer); if (timer_pending(timer)) { base = lock_timer_base(timer, &flags); ret = detach_if_pending(timer, base, true); spin_unlock_irqrestore(&base->lock, flags); } return ret; } EXPORT_SYMBOL(del_timer);
타이머를 타이머 베이스에서 제거하여 비활성화한다.
- 코드 라인 7에서 타이머에서 사용자 트래킹 정보를 클리어한다.
- 코드 라인 9~13에서 타이머 벡터 리스트에 등록되어 대기중인 타이머인 경우 그 리스트에서 제거한다.
- 코드 라인 15에서 타이머가 활성화 상태였었으면 1을 반환하고, 그렇지 않은 경우 0을 반환한다.
펜딩 타이머 제거
detach_if_pending()
kernel/time/timer.c
static int detach_if_pending(struct timer_list *timer, struct timer_base *base, bool clear_pending) { unsigned idx = timer_get_idx(timer); if (!timer_pending(timer)) return 0; if (hlist_is_singular_node(&timer->entry, base->vectors + idx)) __clear_bit(idx, base->pending_map); detach_timer(timer, clear_pending); return 1; }
펜딩된 타이머인 경우 리스트에서 제거한다. 활동(pending) 중인 타이머를 제거한 경우 1을 반환한다.
- 코드 라인 6~7에서 타이머 벡터 리스트에 등록되어 대기하고 있는 타이머가 아닌 경우 이미 deactivate된 경우이므로 0을 반환한다.
- 코드 라인 9~10에서 타이머의 인덱스에 해당하는 타이머 벡터 리스트에 자신 1건만 등록된 경우라면 해당 인덱스의 펜딩맵을 클리어한다.
- 코드 라인 12~13에서 타이머를 타이머 베이스 휠에서 제거하고 1을 반환한다.
타이머 제거
detach_timer()
kernel/time/timer.c
static inline void detach_timer(struct timer_list *timer, bool clear_pending) { struct list_head *entry = &timer->entry; debug_deactivate(timer); __hlist_del(entry); if (clear_pending) entry->pprev = NULL; entry->next = LIST_POISON2; }
타이머 리스트에서 요청 타이머를 제거한다. 만일 clear pending 요청이 있는 경우 타이머 엔트리가 다음 타이머와 연결되지 않도록 분리한다.
타이머 변경
mod_timer()
kernel/time/timer.c
/** * mod_timer - modify a timer's timeout * @timer: the timer to be modified * @expires: new timeout in jiffies * * mod_timer() is a more efficient way to update the expire field of an * active timer (if the timer is inactive it will be activated) * * mod_timer(timer, expires) is equivalent to: * * del_timer(timer); timer->expires = expires; add_timer(timer); * * Note that if there are multiple unserialized concurrent users of the * same timer, then mod_timer() is the only safe way to modify the timeout, * since add_timer() cannot modify an already running timer. * * The function returns whether it has modified a pending timer or not. * (ie. mod_timer() of an inactive timer returns 0, mod_timer() of an * active timer returns 1.) */
int mod_timer(struct timer_list *timer, unsigned long expires) { return __mod_timer(timer, expires, 0); } EXPORT_SYMBOL(mod_timer);
요청한 타이머를 제거하고 인수로 받은 만료 시점(timerout을 적용한 새 jiffies 시점)으로 조절하고 설정한다. inactive된 타이머도 active 시킨다.
__mod_timer()
kernel/time/timer.c
static inline int __mod_timer(struct timer_list *timer, unsigned long expires, unsigned int options) { struct timer_base *base, *new_base; unsigned int idx = UINT_MAX; unsigned long clk = 0, flags; int ret = 0; BUG_ON(!timer->function); /* * This is a common optimization triggered by the networking code - if * the timer is re-modified to have the same timeout or ends up in the * same array bucket then just return: */ if (timer_pending(timer)) { /* * The downside of this optimization is that it can result in * larger granularity than you would get from adding a new * timer with this expiry. */ long diff = timer->expires - expires; if (!diff) return 1; if (options & MOD_TIMER_REDUCE && diff <= 0) return 1; /* * We lock timer base and calculate the bucket index right * here. If the timer ends up in the same bucket, then we * just update the expiry time and avoid the whole * dequeue/enqueue dance. */ base = lock_timer_base(timer, &flags); forward_timer_base(base); if (timer_pending(timer) && (options & MOD_TIMER_REDUCE) && time_before_eq(timer->expires, expires)) { ret = 1; goto out_unlock; } clk = base->clk; idx = calc_wheel_index(expires, clk); /* * Retrieve and compare the array index of the pending * timer. If it matches set the expiry to the new value so a * subsequent call will exit in the expires check above. */ if (idx == timer_get_idx(timer)) { if (!(options & MOD_TIMER_REDUCE)) timer->expires = expires; else if (time_after(timer->expires, expires)) timer->expires = expires; ret = 1; goto out_unlock; } } else { base = lock_timer_base(timer, &flags); forward_timer_base(base); }
요청한 타이머를 제거하고 최종 결정된 만료 시점으로 타이머를 설정한 후 다시 추가 하고 active 시킨다. 타이머가 active된 상태이면 1로 반환된다.
- 코드 라인 16에서 타이머가 expire 되지 않고 아직 타이머 휠에서 기다리고 있는 중이다.
- 코드 라인 22~27에서 새로운 만료 시각(@expires)으로 변경을 하려할 때 차이가 없으면 1을 결과로 함수를 빠져나간다.
- 만일 만료 시각이 앞당겨졌는데 만료 시각을 앞으로 당기지 못하게 한 MOD_TIMER_REDUCE 옵션을 사용한 경우도 결과를 1로 함수를 빠져나간다.
- 코드 라인 35~45에서 타이머가 등록된 타이머 베이스에서 산출한 타이머 휠 인덱스를 알아온다.
- lock을 획득 한 후 타이머 베이스의 시각(clk)을 forward 하도록 갱신하고, 다시 한번 체크한다. 만료 시각이 앞당겨졌는데 만료 시각을 앞으로 당기지 못하게 한 MOD_TIMER_REDUCE 옵션을 사용한 경우도 결과를 1로 함수를 빠져나간다.
- 코드 라인 52~59에서 타이머가 등록된 타이머 베이스에서 해당 타이머 휠 인덱스와 위에서 산출한 휠 인덱스가 동일하면 만료 시각을 갱신하고 결과를 1로 변경하고 함수를 빠져나간다.
- 코드 라인 60~63에서 요청한 타이머가 타이머 휠에 없는 경우 타이머를 위한 타이머 베이스를 준비하고, 타이머 베이스의 시각(clk)을 forward 하도록 갱신한다.
ret = detach_if_pending(timer, base, false); if (!ret && (options & MOD_TIMER_PENDING_ONLY)) goto out_unlock; new_base = get_target_base(base, timer->flags); if (base != new_base) { /* * We are trying to schedule the timer on the new base. * However we can't change timer's base while it is running, * otherwise del_timer_sync() can't detect that the timer's * handler yet has not finished. This also guarantees that the * timer is serialized wrt itself. */ if (likely(base->running_timer != timer)) { /* See the comment in lock_timer_base() */ timer->flags |= TIMER_MIGRATING; raw_spin_unlock(&base->lock); base = new_base; raw_spin_lock(&base->lock); WRITE_ONCE(timer->flags, (timer->flags & ~TIMER_BASEMASK) | base->cpu); forward_timer_base(base); } } debug_timer_activate(timer); timer->expires = expires; /* * If 'idx' was calculated above and the base time did not advance * between calculating 'idx' and possibly switching the base, only * enqueue_timer() and trigger_dyntick_cpu() is required. Otherwise * we need to (re)calculate the wheel index via * internal_add_timer(). */ if (idx != UINT_MAX && clk == base->clk) { enqueue_timer(base, timer, idx); trigger_dyntick_cpu(base, timer); } else { internal_add_timer(base, timer); } out_unlock: raw_spin_unlock_irqrestore(&base->lock, flags); return ret; }
- 코드 라인 1~3에서 pending된 타이머인 경우에 한해 해당 타이머 벡터 리스트에서 제거한다. 실패 시 pending only인 경우 처리를 중단하고 빠져나간다.
- 코드 라인 5~26에서 타이머의 cpu 변경이 필요한 경우 migrating을 한다.
- 코드 라인 30~43에서 타이머의 만료 시점을 갱신하고 적절한 타이머 벡터 리스트에 새로 추가한다.
다음 그림은 타이머를 추가 또는 변경 시 호출되는 __mod_timer() 함수를 통해 타이머 베이스의 벡터 리스트에 타이머가 추가되는 모습을 보여준다.
- 레벨이 올라갈수록 만료 시각에 대한 정확도는 레벨 당 8배 단위로 커지는 Granularity 값만큼 비례하여 떨어진다. 레벨 0의 오차가 8틱 이하였지만, 그 다음 레벨 1은 8배 커진 64틱 이하인것을 확인할 수 있다.
타이머 추가(internal)
internal_add_timer()
kernel/time/timer.c
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()
kernel/time/timer.c
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); }
요청 타이머 베이스에 타이머를 추가한다.
- 코드 라인 5에서 타이머 베이스에서 사용할 타이머 휠의 인덱스 값을 알아온다.
- 코드 라인 6에서 산출한 인덱스의 테이머 벡터 리스트에 타이머를 추가한다.
- vectors[idx] 리스트 <— 타이머 추가
enqueue_timer()
kernel/time/timer.c
/* * Enqueue the timer into the hash bucket, mark it pending in * the bitmap and store the index in the timer flags. */
static void enqueue_timer(struct timer_base *base, struct timer_list *timer, unsigned int idx) { hlist_add_head(&timer->entry, base->vectors + idx); __set_bit(idx, base->pending_map); timer_set_idx(timer, idx); trace_timer_start(timer, timer->expires, timer->flags); }
요청 타이머 베이스에서 인덱스에 해당하는 타이머 벡터 리스트에 타이머를 추가한다.
- 코드 라인 4에서 요청 인덱스에 해당하는 타이머 벡터 리스트에 타이머를 추가한다.
- 코드 라인 5에서 펜딩 맵의 인덱스에 해당하는 비트를 설정한다.
- 코드 라인 6에서 타이머의 플래그 중 10비트 공간을 사용하여 인덱스를 기록한다.
timer_set_idx()
kernel/time/timer.c
static inline void timer_set_idx(struct timer_list *timer, unsigned int idx) { timer->flags = (timer->flags & ~TIMER_ARRAYMASK) | idx << TIMER_ARRAYSHIFT; }
타이머의 플래그 bits[31..22]에 인덱스를 기록한다. (10 비트)
timer_pending()
include/linux/timer.h
/** * timer_pending - is a timer pending? * @timer: the timer in question * * timer_pending will tell whether a given timer is currently pending, * or not. Callers must ensure serialization wrt. other operations done * to this timer, eg. interrupt contexts, or other CPUs on SMP. * * return value: 1 if the timer is pending, 0 if not. */
static inline int timer_pending(const struct timer_list * timer) { return timer->entry.pprev != NULL; }
타이머 벡터 리스트에서 대기중인 타이머인지 여부를 반환한다.
- 타이머 벡터 리스트는 환형 더블리스트이므로 타이머가 혼자 등록되어 있어도 head와 연결되는 구조이다. 따라서 null이 들어가 있는 경우는 환형 더블리스트에서 제거된 경우밖에 없다. 즉 리스트에 존재하면 항상 true이다.
no-hz용 타이머 베이스 시각 forward
forward_timer_base()
kernel/time/timer.c
static inline void forward_timer_base(struct timer_base *base) { #ifdef CONFIG_NO_HZ_COMMON unsigned long jnow; /* * We only forward the base when we are idle or have just come out of * idle (must_forward_clk logic), and have a delta between base clock * and jiffies. In the common case, run_timers will take care of it. */ if (likely(!base->must_forward_clk)) return; jnow = READ_ONCE(jiffies); base->must_forward_clk = base->is_idle; if ((long)(jnow - base->clk) < 2) return; /* * If the next expiry value is > jiffies, then we fast forward to * jiffies otherwise we forward to the next expiry value. */ if (time_after(base->next_expiry, jnow)) base->clk = jnow; else base->clk = base->next_expiry; #endif }
nohz를 위해 현재 요청한 타이머 베이스의 시각을 forward 한다.
- 코드 라인 11~12에서 nohz를 위해 타이머 베이스가 idle 상태인 경우 타이머 베이스를 forward할 수 있게 하였다. 그 경우가 아니면 함수를 빠져나간다. 다음 함수들에서 must_forward_clk 플래그를 1로 만든다.
- timers_prepare_cpu()
- get_next_timer_interrupt()
- 코드 라인 14~17에서 must_forward_clk 값을 idle 상태에서만 사용할 수 있게 지정한다. 타이머 베이스가 현재 틱과 비교하여 2틱 미만인 경우 함수를 빠져나간다.
- 코드 라인 23~26에서 만료 시각이 아직 남아 있는 경우 타이머 베이스의 시각에 현재 시각을 대입한다. 만일 만료 시각이 지났으면 다음 만료 시각으로 갱신한다.
휠 인덱스 산출
calc_wheel_index()
kernel/time/timer.c
static int calc_wheel_index(unsigned long expires, unsigned long clk) { unsigned long delta = expires - clk; unsigned int idx; if (delta < LVL_START(1)) { idx = calc_index(expires, 0); } else if (delta < LVL_START(2)) { idx = calc_index(expires, 1); } else if (delta < LVL_START(3)) { idx = calc_index(expires, 2); } else if (delta < LVL_START(4)) { idx = calc_index(expires, 3); } else if (delta < LVL_START(5)) { idx = calc_index(expires, 4); } else if (delta < LVL_START(6)) { idx = calc_index(expires, 5); } else if (delta < LVL_START(7)) { idx = calc_index(expires, 6); } else if (LVL_DEPTH > 8 && delta < LVL_START(8)) { idx = calc_index(expires, 7); } else if ((long) delta < 0) { idx = clk & LVL_MASK; } else { /* * Force expire obscene large timeouts to expire at the * capacity limit of the wheel. */ if (expires >= WHEEL_TIMEOUT_CUTOFF) expires = WHEEL_TIMEOUT_MAX; idx = calc_index(expires, LVL_DEPTH - 1); } return idx; }
만료 시각과 클럭 베이스의 시각의 차이로 타이머 휠 인덱스 값을 산출한다.
- 코드 라인 3에서 만료 시각과 클럭 베이스의 시각의 차이 틱을 구해 delta에 대입한다.
- 코드 라인 6~7에서 delta 값이 1 레벨 시작 틱 값보다 작은 경우 0 레벨 기준으로 휠 인덱스를 산출한다.
- 코드 라인 8~21에서 delta 값을 사용하여 1~7레벨까지를 기준으로 휠 인덱스를 산출한다.
- 코드 라인 22~23에서 0보다 작은 delta 값에 해당하는 휠 인덱스를 산출한다.
- 코드 라인 24~33에서 범위를 초과하는 delta 값인 경우 최대 값으로 휠 인덱스를 산출한다.
- 코드 라인 34에서 산출한 휠 인덱스를 반환한다.
calc_index()
kernel/time/timer.c
/* * Helper function to calculate the array index for a given expiry * time. */
static inline unsigned calc_index(unsigned expires, unsigned lvl) { expires = (expires + LVL_GRAN(lvl)) >> LVL_SHIFT(lvl); return LVL_OFFS(lvl) + (expires & LVL_MASK); }
만료 시각과 레벨로 타이머 휠 인덱스를 알아온다.
- 예) expires=0x43c0, lvl=2
- new expires = (0x43c0 + 0x40) >> 6 = 0x110
- return 0x80 + 0x10 = 0x90 (144)
매크로 함수들
kernel/time/timer.c
/* * The timer wheel has LVL_DEPTH array levels. Each level provides an array of * LVL_SIZE buckets. Each level is driven by its own clock and therefor each * level has a different granularity. * * The level granularity is: LVL_CLK_DIV ^ lvl * The level clock frequency is: HZ / (LVL_CLK_DIV ^ level) * * The array level of a newly armed timer depends on the relative expiry * time. The farther the expiry time is away the higher the array level and * therefor the granularity becomes. * * Contrary to the original timer wheel implementation, which aims for 'exact' * expiry of the timers, this implementation removes the need for recascading * the timers into the lower array levels. The previous 'classic' timer wheel * implementation of the kernel already violated the 'exact' expiry by adding * slack to the expiry time to provide batched expiration. The granularity * levels provide implicit batching. * * This is an optimization of the original timer wheel implementation for the * majority of the timer wheel use cases: timeouts. The vast majority of * timeout timers (networking, disk I/O ...) are canceled before expiry. If * the timeout expires it indicates that normal operation is disturbed, so it * does not matter much whether the timeout comes with a slight delay. * * The only exception to this are networking timers with a small expiry * time. They rely on the granularity. Those fit into the first wheel level, * which has HZ granularity. * * We don't have cascading anymore. timers with a expiry time above the * capacity of the last wheel level are force expired at the maximum timeout * value of the last wheel level. From data sampling we know that the maximum * value observed is 5 days (network connection tracking), so this should not * be an issue. * * The currently chosen array constants values are a good compromise between * array size and granularity. * * This results in the following granularity and range levels: * * HZ 1000 steps * Level Offset Granularity Range * 0 0 1 ms 0 ms - 63 ms * 1 64 8 ms 64 ms - 511 ms * 2 128 64 ms 512 ms - 4095 ms (512ms - ~4s) * 3 192 512 ms 4096 ms - 32767 ms (~4s - ~32s) * 4 256 4096 ms (~4s) 32768 ms - 262143 ms (~32s - ~4m) * 5 320 32768 ms (~32s) 262144 ms - 2097151 ms (~4m - ~34m) * 6 384 262144 ms (~4m) 2097152 ms - 16777215 ms (~34m - ~4h) * 7 448 2097152 ms (~34m) 16777216 ms - 134217727 ms (~4h - ~1d) * 8 512 16777216 ms (~4h) 134217728 ms - 1073741822 ms (~1d - ~12d) * * HZ 300 * Level Offset Granularity Range * 0 0 3 ms 0 ms - 210 ms * 1 64 26 ms 213 ms - 1703 ms (213ms - ~1s) * 2 128 213 ms 1706 ms - 13650 ms (~1s - ~13s) * 3 192 1706 ms (~1s) 13653 ms - 109223 ms (~13s - ~1m) * 4 256 13653 ms (~13s) 109226 ms - 873810 ms (~1m - ~14m) * 5 320 109226 ms (~1m) 873813 ms - 6990503 ms (~14m - ~1h) * 6 384 873813 ms (~14m) 6990506 ms - 55924050 ms (~1h - ~15h) * 7 448 6990506 ms (~1h) 55924053 ms - 447392423 ms (~15h - ~5d) * 8 512 55924053 ms (~15h) 447392426 ms - 3579139406 ms (~5d - ~41d) * * HZ 250 * Level Offset Granularity Range * 0 0 4 ms 0 ms - 255 ms * 1 64 32 ms 256 ms - 2047 ms (256ms - ~2s) * 2 128 256 ms 2048 ms - 16383 ms (~2s - ~16s) * 3 192 2048 ms (~2s) 16384 ms - 131071 ms (~16s - ~2m) * 4 256 16384 ms (~16s) 131072 ms - 1048575 ms (~2m - ~17m) * 5 320 131072 ms (~2m) 1048576 ms - 8388607 ms (~17m - ~2h) * 6 384 1048576 ms (~17m) 8388608 ms - 67108863 ms (~2h - ~18h) * 7 448 8388608 ms (~2h) 67108864 ms - 536870911 ms (~18h - ~6d) * 8 512 67108864 ms (~18h) 536870912 ms - 4294967288 ms (~6d - ~49d) * * HZ 100 * Level Offset Granularity Range * 0 0 10 ms 0 ms - 630 ms * 1 64 80 ms 640 ms - 5110 ms (640ms - ~5s) * 2 128 640 ms 5120 ms - 40950 ms (~5s - ~40s) * 3 192 5120 ms (~5s) 40960 ms - 327670 ms (~40s - ~5m) * 4 256 40960 ms (~40s) 327680 ms - 2621430 ms (~5m - ~43m) * 5 320 327680 ms (~5m) 2621440 ms - 20971510 ms (~43m - ~5h) * 6 384 2621440 ms (~43m) 20971520 ms - 167772150 ms (~5h - ~1d) * 7 448 20971520 ms (~5h) 167772160 ms - 1342177270 ms (~1d - ~15d) */
LVL_SHIFT() & LVL_GRAN()
kernel/time/timer.c
/* Clock divisor for the next level */ #define LVL_CLK_SHIFT 3 #define LVL_CLK_DIV (1UL << LVL_CLK_SHIFT) #define LVL_CLK_MASK (LVL_CLK_DIV - 1) #define LVL_SHIFT(n) ((n) * LVL_CLK_SHIFT) #define LVL_GRAN(n) (1UL << LVL_SHIFT(n))
- LVL_SHIFT(n)
- 레벨별 비트 수
- =n * 3
- 레벨별 비트 수
- LVL_GRAN(n)
- 레벨별 틱 단위
- =2^(n*3) 틱
- 예) 0 레벨 = 2^0 = 1 틱
- 예) 1 레벨 = 2^3 = 8 틱
- 예) 2 레벨 = 2^6 = 64 틱
- 예) 3 레벨 = 2^6 = 512 틱
- 예) 4 레벨 = 2^6 = 4096 틱
- 예) 5 레벨 = 2^6 = 32768 틱
- 예) 6 레벨 = 2^6 = 262144 틱
- 예) 7 레벨 = 2^21=2097152 틱
- 예) 8 레벨 = 2^24=16777216 틱
- 레벨별 틱 단위
LVL_OFFS()
kernel/time/timer.c
/* Size of each clock level */ #define LVL_BITS 6 #define LVL_SIZE (1UL << LVL_BITS) #define LVL_MASK (LVL_SIZE - 1) #define LVL_OFFS(n) ((n) * LVL_SIZE)
- LVL_OFFS(n)
- 레벨별 벡터 offset
- = n * 64
- 예) 0 레벨 = 0 * 64 = 0
- 예) 1 레벨 = 1 * 64 = 64
- 예) 2 레벨 = 2 * 64 = 128
- 예) 3 레벨 = 3 * 64 = 192
- 예) 4 레벨 = 4 * 64 = 256
- 예) 5 레벨 = 5 * 64 = 320
- 예) 6 레벨 = 6 * 64 = 384
- 예) 7 레벨 = 7 * 64 = 448
- 예) 8 레벨 = 8 * 64 = 512
- 레벨별 벡터 offset
LVL_START()
kernel/time/timer.c
/* * The time start value for each level to select the bucket at enqueue * time. */
#define LVL_START(n) ((LVL_SIZE - 1) << (((n) - 1) * LVL_CLK_SHIFT))
- 레벨별 시작 틱 값
- = 63 << (n-1) * 3
- 예) 레벨 0 = 63 << (0-1) * 3 = 0 틱
- 예) 레벨 1 = 63 << (1-1) * 3 = 63 틱
- 예) 레벨 2 = 63 << (2-1) * 3 = 504 틱
- 예) 레벨 3 = 63 << (3-1) * 3 =4032 틱
- 예) 레벨 4 = 63 << (4-1) * 3 =32256 틱
- 예) 레벨 5 = 63 << (5-1) * 3 = 258048 틱
- 예) 레벨 6 = 63 << (6-1) * 3 = 2064384 틱
- 예) 레벨 7 = 63 << (7-1) * 3 = 16515072 틱
- 예) 레벨 8 = 63 << (8-1) * 3 = 132120576 틱
- = 63 << (n-1) * 3
타이머 베이스
타이머 베이스 락 획득
lock_timer_base()
kernel/time/timer.c
/* * We are using hashed locking: Holding per_cpu(timer_bases[x]).lock means * that all timers which are tied to this base are locked, and the base itself * is locked too. * * So __run_timers/migrate_timers can safely modify all timers which could * be found in the base->vectors array. * * When a timer is migrating then the TIMER_MIGRATING flag is set and we need * to wait until the migration is done. */
static struct timer_base *lock_timer_base(struct timer_list *timer, unsigned long *flags) __acquires(timer->base->lock) { for (;;) { struct timer_base *base; u32 tf; /* * We need to use READ_ONCE() here, otherwise the compiler * might re-read @tf between the check for TIMER_MIGRATING * and spin_lock(). */ tf = READ_ONCE(timer->flags); if (!(tf & TIMER_MIGRATING)) { base = get_timer_base(tf); raw_spin_lock_irqsave(&base->lock, *flags); if (timer->flags == tf) return base; raw_spin_unlock_irqrestore(&base->lock, *flags); } cpu_relax(); } }
타이머가 사용할 현재 cpu의 타이머 베이스 락을 획득한다.
- 만일 타이머가 migration 중이면 TIMER_MIGRATING 플래그를 사용하는데 이 플래그가 없어질 때까지 루프를 반복하며 lock을 잡을 때까지 기다린다.
- 또한 lock을 잡은 후에 플래그가 변경된 경우 루프를 반복한다.
타이머 베이스 알아오기
get_timer_base()
kernel/time/timer.c
static inline struct timer_base *get_timer_base(u32 tflags) { return get_timer_cpu_base(tflags, tflags & TIMER_CPUMASK); }
현재 cpu에 대한 타이머 베이스를 반환한다.
get_target_base()
kernel/time/timer.c
static inline struct timer_base * get_target_base(struct timer_base *base, unsigned tflags) { #if defined(CONFIG_SMP) && defined(CONFIG_NO_HZ_COMMON) if (static_branch_likely(&timers_migration_enabled) && !(tflags & TIMER_PINNED)) return get_timer_cpu_base(tflags, get_nohz_timer_target()); #endif return get_timer_this_cpu_base(tflags); }
동작할 cpu의 타이머 베이스를 반환한다.
- 코드 라인 5~7에서 nohz 시스템에서 이주 가능한 타이머인 경우 현재 cpu가 idle 상태일 때 절전을 위해 타이머가 동작될 인접한 busy cpu를 찾아 해당 cpu의 타이머 베이스를 반환한다.
- 코드 라인 9에서 현재 cpu의 타이머 베이스를 반환한다.
get_timer_cpu_base()
kernel/time/timer.c
static inline struct timer_base *get_timer_cpu_base(u32 tflags, u32 cpu) { struct timer_base *base = per_cpu_ptr(&timer_bases[BASE_STD], cpu); /* * If the timer is deferrable and NO_HZ_COMMON is set then we need * to use the deferrable base. */ if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && (tflags & TIMER_DEFERRABLE)) base = per_cpu_ptr(&timer_bases[BASE_DEF], cpu); return base; }
요청 cpu에 대한 타이머 베이스를 반환한다.
- per-cpu로 관리되는 타이머 베이스는 nohz를 사용하는 경우 두 개로 나뉘어 관리되며 지연 가능한 타이머는 별도의 타이머 베이스를 사용한다.
get_timer_this_cpu_base()
kernel/time/timer.c
static inline struct timer_base *get_timer_this_cpu_base(u32 tflags) { struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]); /* * If the timer is deferrable and NO_HZ_COMMON is set then we need * to use the deferrable base. */ if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && (tflags & TIMER_DEFERRABLE)) base = this_cpu_ptr(&timer_bases[BASE_DEF]); return base; }
현재 cpu에 대한 타이머 베이스를 반환한다.
- per-cpu로 관리되는 타이머 베이스는 nohz를 사용하는 경우 두 개로 나뉘어 관리되며 지연 가능한 타이머는 별도의 타이머 베이스를 사용한다.
nohz 타이머용 cpu
get_nohz_timer_target()
kernel/sched/core.c
/* * In the semi idle case, use the nearest busy CPU for migrating timers * from an idle CPU. This is good for power-savings. * * We don't do similar optimization for completely idle system, as * selecting an idle CPU will add more delays to the timers than intended * (as that CPU's timer base may not be uptodate wrt jiffies etc). */
int get_nohz_timer_target(void) { int i, cpu = smp_processor_id(); struct sched_domain *sd; if (!idle_cpu(cpu) && housekeeping_cpu(cpu, HK_FLAG_TIMER)) return cpu; rcu_read_lock(); for_each_domain(cpu, sd) { for_each_cpu(i, sched_domain_span(sd)) { if (cpu == i) continue; if (!idle_cpu(i) && housekeeping_cpu(i, HK_FLAG_TIMER)) { cpu = i; goto unlock; } } } if (!housekeeping_cpu(cpu, HK_FLAG_TIMER)) cpu = housekeeping_any_cpu(HK_FLAG_TIMER); unlock: rcu_read_unlock(); return cpu; }
절전을 위해 nohz 타이머를 위한 타겟 cpu를 알아온다.
- 코드 라인 6~7에서 로컬 cpu가 busy 상태이면서 타이머에 대한 housekeeping이 가능하면 로컬 cpu를 반환한다.
- 코드 라인 10~20에서 cpu가 속한 스케쥴 domain 수 만큼 순회하고 내부에서 순회중인 스케줄 도메인만큼 cpu를 순회하며 busy 상태이면서 타이머에 대한 housekeeping이 가능한 해당 cpu를 반환한다.
- 코드 라인 22~23에서 찾지 못한 경우이다. cpu가 타이머에 대한 housekeeping이 불가능하면 housekeeping이 가능한 어떠한 cpu라도 찾아낸다.
- 코드 라인 26에서 nohz 타이머로 사용할 cpu를 반환한다.
SOFTIRQ – TIMER 수행
lowres 타이머의 bottom-half로 동작하는 softirq 핸들러를 알아본다.
run_timer_softirq()
kernel/time/timer.c
/* * This function runs timers and the timer-tq in bottom half context. */
static __latent_entropy void run_timer_softirq(struct softirq_action *h) { struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]); __run_timers(base); if (IS_ENABLED(CONFIG_NO_HZ_COMMON)) __run_timers(this_cpu_ptr(&timer_bases[BASE_DEF])); }
로컬 cpu에 타이머 tick이 인입될 때 마다 호출되는 timer softirq에 등록한 함수이다. 만료된 타이머들의 해당 타이머 함수를 호출한다.
- 코드 라인 5에서 스탠다드 타이머 베이스에서 만료된 타이머들의 해당 타이머 함수를 호출한다.
- 코드 라인 6~7에서 deferrable 타이머 베이스에서 만료된 타이머들의 해당 타이머 함수를 호출한다.
__run_timers()
kernel/time/timer.c
/** * __run_timers - run all expired timers (if any) on this CPU. * @base: the timer vector to be processed. */
static inline void __run_timers(struct timer_base *base) { struct hlist_head heads[LVL_DEPTH]; int levels; if (!time_after_eq(jiffies, base->clk)) return; timer_base_lock_expiry(base); raw_spin_lock_irq(&base->lock); /* * timer_base::must_forward_clk must be cleared before running * timers so that any timer functions that call mod_timer() will * not try to forward the base. Idle tracking / clock forwarding * logic is only used with BASE_STD timers. * * The must_forward_clk flag is cleared unconditionally also for * the deferrable base. The deferrable base is not affected by idle * tracking and never forwarded, so clearing the flag is a NOOP. * * The fact that the deferrable base is never forwarded can cause * large variations in granularity for deferrable timers, but they * can be deferred for long periods due to idle anyway. */ base->must_forward_clk = false; while (time_after_eq(jiffies, base->clk)) { levels = collect_expired_timers(base, heads); base->clk++; while (levels--) expire_timers(base, heads + levels); } raw_spin_unlock_irq(&base->lock); timer_base_unlock_expiry(base); }
요청 타이머 베이스에서 만료된 타이머들의 함수를 실행한다.
- 코드 라인 6~7에서 jiffies < base->clk 이다. 가장 가까운 타이머 시각이 아직 처리할 시각이 안되었으므로 함수를 빠져나간다.
- 코드 라인 9~10에서 타이머 함수 호출 루틴이 동시에 호출되는 것을 막기 위해 lock을 획득한다. 또한 타이머 베이스를 조작하기 위한 lock도 획득한다.
- 코드 라인 26에서 타이머 베이스에서 forward 처리를 하지 못하도록 막는다.
- 코드 라인 28~35에서 타이머 베이스의 clk가 만료된 경우 다음과 같이 처리한다.
- 만료된 타이머들을 heads 리스트에 모아온다. levels에는 최고 레벨이 저장된다.
- 다음 루프 처리를 위해 타이머 베이스의 clk을 미리 증가시킨다.
- 레벨별로 만료된 타이머에 해당하는 콜백 함수들을 호출한다.
- 코드 라인 36~37에서 걸었던 lock들을 해제한다.
만료된 타이머들 수집
collect_expired_timers()
kernel/time/timer.c
static int collect_expired_timers(struct timer_base *base, struct hlist_head *heads) { unsigned long now = READ_ONCE(jiffies); /* * NOHZ optimization. After a long idle sleep we need to forward the * base to current jiffies. Avoid a loop by searching the bitfield for * the next expiring timer. */ if ((long)(now - base->clk) > 2) { unsigned long next = __next_timer_interrupt(base); /* * If the next timer is ahead of time forward to current * jiffies, otherwise forward to the next expiry time: */ if (time_after(next, now)) { /* * The call site will increment base->clk and then * terminate the expiry loop immediately. */ base->clk = now; return 0; } base->clk = next; } return __collect_expired_timers(base, heads); }
요청 타이머 베이스에서 만료된 타이머들을 @heads 리스트에 추가한다. levels에는 수집한 최고 레벨 + 1이 저장된다.
- 코드 라인 11~27에서 nohz를 사용하면 타이머 베이스를 매 틱마다 처리하지 못할 수 있다. 이 때 3틱 이상 밀려 있는 상태면 매 틱마다 처리하지 않고 nohz optimization을 수행한다. 이 때 다음 타이머의 만료 시각이 현재 시각을 초과했는지 여부에 따라 다음과 같이 처리한다.
- 아직 만료 시각이 남은 경우 지연된 base->clk을 현재 시각으로 갱신하고, 결과 값 0으로 함수를 빠져나간다.
- 만료 시각이 지난 경우 base->clk에 다음 타이머 만료 시각을 대입하고, 다음 루틴을 계속 처리한다.
- 코드 라인 28에서 만료된 타이머 함수들을 @heds 리스트로 수집한다.
__collect_expired_timers()
kernel/time/timer.c
static int __collect_expired_timers(struct timer_base *base, struct hlist_head *heads) { unsigned long clk = base->clk; struct hlist_head *vec; int i, levels = 0; unsigned int idx; for (i = 0; i < LVL_DEPTH; i++) { idx = (clk & LVL_MASK) + i * LVL_SIZE; if (__test_and_clear_bit(idx, base->pending_map)) { vec = base->vectors + idx; hlist_move_list(vec, heads++); levels++; } /* Is it time to look at the next level? */ if (clk & LVL_CLK_MASK) break; /* Shift clock for the next level granularity */ clk >>= LVL_CLK_SHIFT; } return levels; }
요청 타이머 베이스에서 만료된 타이머들을 @heads 리스트로 수집한다.
- 코드 라인 9~17에서 0~마지막 레벨(100hz 이하에서 8, 초과 시 9)까지 순회하며 타이머 휠 인덱스 값에 해당하는 펜딩 맵에 비트가 설정된 경우 이를 클리어하고, 이에 해당하는 인덱스의 타이머 휠 벡터 리스트를 heads 리스트에 옮기고, 반환할 levels를 1 증가시킨다.
- 코드 라인 19~23에서 다음 레벨을 처리할 필요가 없는 경우 루프를 벗어난다. 그렇지 않고 남은 경우 다음 레벨을 처리하기 위해 clk을 3 비트 쉬프트한다.
- clk 값의 해당 레벨의 6비트 중 하위 3비트 값이 0인 경우 다음 레벨로 넘어간다.
- 코드 라인 24에서 수집한 최고 레벨 + 1 값을 반환한다.
다음 그림은 base->clk 값에 의해 호출되는 레벨별 타이머 벡터 리스트들을 보여준다.
- 레벨별로 쉬프트된 6bit clk 값의 하위 3비트가 0인 경우 다음 레벨도 처리하기 위해 이동한다.
- clk 값이 0x4400 값인 경우 lvl 0 ~ lvl 3까지 이동하며 각 레벨의 6bit clk값 + lvl * 64로 idx를 산출한다. 그런 후 idx에 해당하는 펜딩맵이 설정된 벡터 리스트의 타이머들이 선택된다.
만료된 타이머들의 콜백 함수 호출
expire_timers()
kernel/time/timer.c
static void expire_timers(struct timer_base *base, struct hlist_head *head) { /* * This value is required only for tracing. base->clk was * incremented directly before expire_timers was called. But expiry * is related to the old base->clk value. */ unsigned long baseclk = base->clk - 1; while (!hlist_empty(head)) { struct timer_list *timer; void (*fn)(struct timer_list *); timer = hlist_entry(head->first, struct timer_list, entry); base->running_timer = timer; detach_timer(timer, true); fn = timer->function; if (timer->flags & TIMER_IRQSAFE) { raw_spin_unlock(&base->lock); call_timer_fn(timer, fn, baseclk); base->running_timer = NULL; raw_spin_lock(&base->lock); } else { raw_spin_unlock_irq(&base->lock); call_timer_fn(timer, fn, baseclk); base->running_timer = NULL; timer_sync_wait_running(base); raw_spin_lock_irq(&base->lock); } } }
@head 리스트의 만료된 타이머들의 콜백 함수들을 호출한다.
- 호출하는 동안 base->running_timer에 타이머가 기록되고, 호출이 완료되면 null이 대입된다.
- TIMER_IRQSAFE 플래그를 사용하면 인터럽트를 disable하지 않은 상태에서 spinlock을 사용한다.
타이머 콜백 함수 호출
call_timer_fn()
kernel/time/timer.c
static void call_timer_fn(struct timer_list *timer, void (*fn)(struct timer_list *), unsigned long baseclk) { int count = preempt_count(); #ifdef CONFIG_LOCKDEP /* * It is permissible to free the timer from inside the * function that is called from it, this we need to take into * account for lockdep too. To avoid bogus "held lock freed" * warnings as well as problems when looking into * timer->lockdep_map, make a copy and use that here. */ struct lockdep_map lockdep_map; lockdep_copy_map(&lockdep_map, &timer->lockdep_map); #endif /* * Couple the lock chain with the lock chain at * del_timer_sync() by acquiring the lock_map around the fn() * call here and in del_timer_sync(). */ lock_map_acquire(&lockdep_map); trace_timer_expire_entry(timer, baseclk); fn(timer); trace_timer_expire_exit(timer); lock_map_release(&lockdep_map); if (count != preempt_count()) { WARN_ONCE(1, "timer: %pS preempt leak: %08x -> %08x\n", fn, count, preempt_count()); /* * Restore the preempt count. That gives us a decent * chance to survive and extract information. If the * callback kept a lock held, bad luck, but not worse * than the BUG() we had. */ preempt_count_set(count); } }
인수로 받은 fn은 타이머에 설정된 함수이다. 디버그를 위해 trace 출력등이 사용되었다.
타이머 설정
컴파일 타임에 정적 타이머 생성 및 초기화
include/linux/timer.h
DEFINE_TIMER()
#define DEFINE_TIMER(_name, _function) \ struct timer_list _name = \ __TIMER_INITIALIZER(_function, 0)
컴파일 타임에 타이머를 초기화한다. 인자로 타이머 이름과 콜백 함수를 지정한다.
- 플래그는 사용하지 않고 0을 전달한다.
__TIMER_INITIALIZER()
include/linux/timer.h
#define __TIMER_INITIALIZER(_function, _flags) { \ .entry = { .next = TIMER_ENTRY_STATIC }, \ .function = (_function), \ .flags = (_flags), \ __TIMER_LOCKDEP_MAP_INITIALIZER( \ __FILE__ ":" __stringify(__LINE__)) \ }
런타임에 동적 타이머 생성 및 초기화
timer_setup()
include/linux/timer.h
/** * timer_setup - prepare a timer for first use * @timer: the timer in question * @callback: the function to call when timer expires * @flags: any TIMER_* flags * * Regular timer initialization should use either DEFINE_TIMER() above, * or timer_setup(). For timers on the stack, timer_setup_on_stack() must * be used and must be balanced with a call to destroy_timer_on_stack(). */
#define timer_setup(timer, callback, flags) \ __init_timer((timer), (callback), (flags))
타이머를 사용하기 위해 준비한다. 인자로는 타이머와 콜백 함수 및 플래그를 지정한다.
__init_timer()
include/linux/timer.h
#define __init_timer(_timer, _fn, _flags) \ init_timer_key((_timer), (_fn), (_flags), NULL, NULL)
timer_setup_on_stack()
include/linux/timer.h
#define setup_timer_on_stack(timer, callback, flags) \ __init_timer_on_stack((timer), (callback), (flags))
타이머를 전달 받은 인자들로 초기화한다.
__init_timer_on_stack()
include/linux/timer.h
#define __init_timer_on_stack(_timer, _fn, _flags) \ init_timer_on_stack_key((_timer), (_fn), (_flags), NULL, NULL)
init_timer_on_stack_key()
include/linux/timer.h
static inline void init_timer_on_stack_key(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags, const char *name, struct lock_class_key *key) { init_timer_key(timer, func, flags, name, key); }
init_timer_key()
kernel/time/timer.c
/** * init_timer_key - initialize a timer * @timer: the timer to be initialized * @func: timer callback function * @flags: timer flags * @name: name of the timer * @key: lockdep class key of the fake lock used for tracking timer * sync lock dependencies * * init_timer_key() must be done to a timer prior calling *any* of the * other timer functions. */
void init_timer_key(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags, const char *name, struct lock_class_key *key) { debug_init(timer); do_init_timer(timer, func, flags, name, key); } EXPORT_SYMBOL(init_timer_key);
타이머를 전달 받은 인자들로 초기화한다.
do_init_timer()
kernel/time/timer.c
static void do_init_timer(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags, const char *name, struct lock_class_key *key) { timer->entry.pprev = NULL; timer->function = func; timer->flags = flags | raw_smp_processor_id(); lockdep_init_map(&timer->lockdep_map, name, key, 0); }
타이머를 전달 받은 인자들로 초기화한다.
초기화
init_timers()
kernel/time/timer.c
void __init init_timers(void) { init_timer_cpus(); open_softirq(TIMER_SOFTIRQ, run_timer_softirq); }
타이머를 사용할 수 있도록 초기화한다.
- 코드 라인 3에서 cpu별로 타이머를 사용할 수 있도록 초기화한다.
- 코드 라인 4에서 타이머 softirq를 열고 run_timer_softirq() 함수가 호출되도록 준비한다.
init_timer_cpus()
kernel/time/timer.c
static void __init init_timer_cpus(void) { int cpu; for_each_possible_cpu(cpu) init_timer_cpu(cpu); }
모든 possible cpu에 대해 타이머를 사용할 수 있도록 초기화한다.
init_timer_cpu()
kernel/time/timer.c
static void __init init_timer_cpu(int cpu) { struct timer_base *base; int i; for (i = 0; i < NR_BASES; i++) { base = per_cpu_ptr(&timer_bases[i], cpu); base->cpu = cpu; raw_spin_lock_init(&base->lock); base->clk = jiffies; timer_base_init_expiry_lock(base); } }
요청 cpu에 대해 타이머를 사용할 수 있도록 초기화한다.
기존 커널 관련 (~ 커널 v4.7)
기존 cascading wheel 구조
요청된 타이머의 관리를 위해 cpu 마다 타이머휠이 사용된다. 타이머 휠마다 tv1 ~ tv5까지의 5개 벡터로 나누어 관리하며, 각 벡터는 각각 256, 64, 64, 64, 64개의 리스트로 이루어진다.
- CONFIG_BASE_SMALL 사용 시 커널 사이즈를 최소화하기 위해 리스트들은 각각 64, 16, 16, 16, 16개로 1/4로 줄어든다.
다음 그림은 타이머 벡터 간의 cascade 조건을 보여준다.
예) base->timer_jiffies 값이 0x400_0000인 경우 tv2 -> tv1, tv3 -> tv2, tv4 -> tv3, tv5 -> tv4의 순서로 full cascade 처리된다.
다음 그림은 만료시각이 다른 각종 타이머들을 추가했을 때 타이머휠에 등록된 타이머 상태들을 보여준다.
- expires – base->timer_jiffies 하여 산출된 tick 값에 따라 512(256+64+64+64+64)개의 리스트 중 하나가 선택된다.
- 예) timer_jiffies=100, hz=100, 7초 후에 timer가 호출되게 하려할 때 expires 값과 추가될 타이머 벡터 리스트는?
- 7초면 700 tick이 필요하므로 expires=100+700이 대입되고 tv2.vec[32]에 추가된다.
- 예) timer_jiffies=100, hz=100, 7초 후에 timer가 호출되게 하려할 때 expires 값과 추가될 타이머 벡터 리스트는?
다음 그림은 jiffies값이 35인 시점에 cpu#1에 tick이 발생하고 그 동안 tick이 발생하지 못해 처리 못했던 jiffies 들에 대한 처리를 한꺼번에 처리하도록 한다.
- jiffies=35인 시점에 26~35까지 처리 못했던 만료된 타이머들의 함수들을 처리한다.
다음 그림은 tv2에 있는 두 개의 타이머가 tv1으로 cascade되는 모습을 보여준다.
Slack으로 만료 시간 조정
만료 시간을 조정하여 유사한 만료 시간들끼리 모아 한꺼번에 처리하도록 slack 정렬한다. 타이머 인터럽트가 조금이라도 덜 발생하도록하여 절전과 처리 성능에 도움을 준다.
- 참고: Timer slack | LWN.net
아래 그림은 100hz 시스템에서 slack으로 9 tick을 준 경우와 timeout으로 2311 tick을 준 경우에 대해 만료 값이 어떻게 변화하는지를 보여준다.
커널 v4.7까지에서는 타이머 리스트가 disable 된 상태에 있기 때문에 아래와 같이 inactive 출력된다.
$ cat /proc/timer_stats Timer Stats Version: v0.3 Sample period: 0.000 s Collection: inactive 0 total events
다음과 같이 enable 시킨다.
# echo "1" > /proc/timer_stats
그런 후 출력이 가능하다. (lowres 타이머와 hrtimer가 섞여 있다)
# cat /proc/timer_stats Timer Stats Version: v0.3 Sample period: 4.530 s Collection: active 453, 0 swapper/2 hrtimer_start_range_ns (tick_sched_timer) 38, 0 swapper/0 hrtimer_start_range_ns (tick_sched_timer) 50, 7 rcu_preempt rcu_gp_kthread (process_timeout) 5, 1632 ifplugd hrtimer_start_range_ns (hrtimer_wakeup) 16, 0 swapper/1 hrtimer_start (tick_sched_timer) 36, 0 swapper/0 hrtimer_start (tick_sched_timer) 4, 3992 sshd sk_reset_timer (tcp_write_timer) 17, 0 swapper/1 usb_hcd_poll_rh_status (rh_timer_func) 4, 1582 ifplugd hrtimer_start_range_ns (hrtimer_wakeup) 4, 2230 ntpd hrtimer_start_range_ns (posix_timer_fn) 4, 3467 kworker/u8:2 queue_delayed_work_on (delayed_work_timer_fn) 10, 0 swapper/0 sk_reset_timer (tcp_delack_timer) 3, 26686 kworker/2:1 queue_delayed_work_on (delayed_work_timer_fn) 3, 44 kworker/0:1 queue_delayed_work_on (delayed_work_timer_fn) 1, 2058 thd hrtimer_start_range_ns (hrtimer_wakeup) 25, 0 swapper/1 hrtimer_start_range_ns (tick_sched_timer) 673 total events, 148.565 events/sec
참고
- Timer -1- (Lowres Timer) | 문c – 현재 글
- Timer -2- (HRTimer) | 문c
- Timer -3- (Clock Sources Subsystem) | 문c
- Timer -4- (Clock Sources Watchdog) | 문c
- Timer -5- (Clock Events Subsystem) | 문c
- Timer -6- (Clock Source & Timer Driver) | 문c
- Timer -7- (Sched Clock & Delay Timers) | 문c
- Timer -8- (Timecounter) | 문c
- Timer -9- (Tick Device) | 문c
- Timer -10- (Timekeeping) | 문c
- Timer -11- (Posix Clock & Timers) | 문c
- time_init() | 문c
- sched_clock_postinit() | 문c
- tick_init() | 문c
- timekeeping_init() | 문c
- calibrate_delay() | 문c
안녕하세요! iamroot16차 이파란입니다.
타이머 스터디하다가 발견해서 문의드립니다. 항상 감사드립니다~
add_timer-2c.png
다음 그림은 만료시각이 다른 각종 타이머들을 추가했을 때 타이머휠에 등록된 타이머 상태들을 보여준다.
그림에서 구조체 멤버 함수가 finction 으로 되어있습니다!
add 한 B 컬백 함수가 우하단에 동적 타이머에 C 로 나와있습니다.
감사합니다!
p.s.
E 컬백을 가진 동적 타이머가 1100 으로 expires 가 등록되어있는데, 1000 으로 add 되어있습니다.
감사합니다!
안녕하세요?
요청하신 항목들이 잘못 표시되었군요. 모두 수정하였습니다.
감사합니다.