타이머 (Timers)
jiffies, hrtimer, clocksource, clockevent, timer wheel, tickless 커널 등 Linux 커널의 시간 관리 체계를 설명합니다.
이 문서는 시간 측정(clocksource)과 이벤트 발생(clockevent), 그리고 커널 타이머 API(timer wheel, hrtimer)가 어떻게 결합되어 스케줄러(Scheduler)·네트워크·스토리지 경로의 지연(Latency) 특성을 결정하는지까지 다룹니다. 또한 NO_HZ 계열 설정, vDSO 기반 시간 읽기, delayed_work 선택 기준, watchdog과 lockup 탐지 로그 해석을 포함해 "정확도·전력·오버헤드(Overhead)" 사이의 균형을 시스템 목적에 맞게 조정하는 실무 관점을 제공합니다.
핵심 요약
- jiffies — 부팅 이후 경과한 타이머 틱 수. HZ(보통 250)가 초당 틱 수를 결정합니다.
- timer_list — jiffies 기반 저해상도 타이머. timer wheel 알고리즘으로 O(1) 삽입/삭제합니다.
- hrtimer — 나노초 해상도의 고해상도 타이머(hrtimer). Red-Black 트리로 관리됩니다.
- clocksource — 시간 측정 하드웨어(TSC, HPET 등)를 추상화하는 프레임워크입니다.
- clockevent — 미래 시점에 인터럽트를 발생시키는 하드웨어 타이머 추상화입니다.
- NO_HZ(tickless) — idle CPU에서 불필요한 타이머 인터럽트를 생략하여 전력을 절약합니다.
- delayed_work — workqueue에 지연 실행 작업을 예약하는 API. 프로세스(Process) 컨텍스트에서 실행됩니다.
- watchdog — softlockup(20초)/hardlockup(10초) 감지기. 커널 행 상태를 자동 탐지합니다.
- vDSO — clock_gettime()을 syscall 없이 실행하는 가상 DSO. 수 나노초의 오버헤드로 시간을 읽습니다.
단계별 이해
- 타이머 틱 — 타이머 하드웨어가 HZ 주기로 인터럽트를 발생시키고, 커널이 jiffies를 증가시킵니다.
cat /proc/timer_list로 현재 활성 타이머를 확인할 수 있습니다. - 저해상도 타이머 —
mod_timer()로 jiffies 기반 타이머를 설정합니다. 밀리초 단위 정확도.네트워크 타임아웃, 디바이스 폴링(Polling) 등에 사용됩니다.
- 고해상도 타이머 —
hrtimer_start()로 나노초 정밀 타이머를 설정합니다.POSIX 타이머,
nanosleep(), 스케줄러 타임슬라이스(Time Slice)에 사용됩니다. - tickless 확인 —
grep CONFIG_NO_HZ /boot/config-$(uname -r)로 tickless 설정을 확인합니다.idle 상태에서 타이머 인터럽트를 생략하여 C-state 깊은 절전에 진입합니다.
- delayed_work 사용 —
schedule_delayed_work(&dwork, msecs_to_jiffies(100))로 100ms 후 실행을 예약합니다.timer_list와 달리 프로세스 컨텍스트(kworker 스레드(Thread))에서 실행되므로 슬립(Sleep)/잠금(Lock) 사용이 가능합니다.
- watchdog 설정 확인 —
cat /proc/sys/kernel/watchdog_thresh로 임계값(기본 10초)을 확인합니다.softlockup은 스케줄러 양보(Yield) 없음, hardlockup은 NMI로 인터럽트 없음을 감지합니다.
- 타이머 디버깅(Debugging) —
cat /proc/timer_list | head -60으로 만료 시각과 콜백(Callback) 함수를 확인합니다.cyclictest -m -n -p99 -l 10000으로 타이머 지터를 마이크로초 단위로 측정합니다. - vDSO 타이밍 —
clock_gettime(CLOCK_MONOTONIC, &ts)호출은 syscall 없이 vDSO로 처리됩니다.strace ./my_prog를 실행하면 clock_gettime이 시스템 콜(System Call) 목록에 나타나지 않는 것을 확인할 수 있습니다.
타이머 서브시스템의 정의와 역할
타이머 서브시스템은 커널이 시간의 흐름을 인식하고 미래 시점에 작업을 예약할 수 있게 하는 핵심 인프라입니다. 이 서브시스템이 없으면 프로세스 스케줄링, 네트워크 타임아웃, 디바이스 폴링, 슬립 등 시간 기반 동작이 모두 불가능합니다.
타이머 서브시스템은 크게 세 가지 계층으로 구성됩니다:
- 하드웨어 계층: TSC(Time Stamp Counter), HPET, Local APIC Timer 등 물리적 클럭 소스가 시간 측정과 인터럽트 생성을 담당합니다.
- 프레임워크 계층:
clocksource(시간 읽기)와clockevent(미래 인터럽트 예약)가 하드웨어를 추상화하여 플랫폼 독립적 인터페이스를 제공합니다. - 타이머 API 계층: 커널 코드가 실제로 사용하는
timer_list(저해상도)와hrtimer(고해상도) API입니다.
핵심 설계 원리: 커널 타이머는 "정확한 시점에 실행"이 아니라 "지정된 시점 이후 가능한 빨리 실행"을 보장합니다. 하드웨어 인터럽트 지연, softirq 스케줄링 등으로 수 마이크로초~밀리초의 지터(jitter)가 발생할 수 있습니다.
jiffies와 HZ
jiffies는 시스템 부팅 이후 발생한 타이머 틱(tick) 횟수를 저장하는 전역 변수입니다. HZ는 초당 틱 수를 나타내며, 일반적으로 x86에서는 250 (CONFIG_HZ_250)으로 설정됩니다. HZ 값의 선택은 타이머 해상도와 시스템 오버헤드 사이의 트레이드오프입니다 — HZ가 높을수록 타이머 정밀도가 향상되지만, 매 틱마다 인터럽트를 처리하므로 CPU 오버헤드가 증가합니다.
| HZ 값 | CONFIG 옵션 | 타이머 해상도 | 인터럽트 빈도 | CPU 오버헤드 | 주요 사용 사례 |
|---|---|---|---|---|---|
| 100 | CONFIG_HZ_100 |
10ms | 초당 100회 | 낮음 | 서버 워크로드, 배치 처리 |
| 250 | CONFIG_HZ_250 |
4ms | 초당 250회 | 중간 | 일반 데스크톱, 균형잡힌 설정 (기본) |
| 300 | CONFIG_HZ_300 |
3.33ms | 초당 300회 | 중간 | 멀티미디어 워크로드 (60Hz 모니터 호환) |
| 1000 | CONFIG_HZ_1000 |
1ms | 초당 1000회 | 높음 | 저지연 데스크톱, 게이밍, 오디오 처리 |
#include <linux/jiffies.h>
/* 현재 jiffies 값 */
unsigned long j = jiffies;
/* 시간 비교 (wraparound 안전) */
if (time_after(jiffies, timeout))
pr_info("timeout expired\\n");
/* jiffies ↔ 시간 변환 */
unsigned long ms = jiffies_to_msecs(j);
unsigned long j2 = msecs_to_jiffies(500); /* 500ms */
/* 64-bit jiffies (overflow 방지) */
u64 j64 = get_jiffies_64();
Timer Wheel (저해상도 타이머)
전통적인 커널 타이머는 struct timer_list를 사용하며, Timer Wheel 자료구조로 관리됩니다. 만료 시 softirq 컨텍스트(TIMER_SOFTIRQ)에서 콜백이 실행됩니다.
Timer Wheel 동작 원리
Timer Wheel은 계층적 해시 테이블(Hash Table)의 원리로 동작합니다. 핵심 아이디어는 "가까운 미래의 타이머는 정밀하게, 먼 미래의 타이머는 대략적으로 관리"하는 것입니다:
- 삽입(O(1)): 만료 시간의 비트 패턴에 따라 적절한 레벨과 슬롯을 즉시 결정합니다. 만료까지 남은 틱 수의 상위 비트가 레벨을, 하위 비트가 슬롯 인덱스를 결정합니다.
- 만료 처리: 매 틱마다 Level 0의 현재 슬롯만 확인합니다. Level 0이 한 바퀴 돌면(64 틱) Level 1의 한 슬롯에 있는 타이머들을 Level 0으로 재배치(cascade)합니다.
- 효율성: 대부분의 타이머가 Level 0에서 만료되므로, 상위 레벨의 cascade는 드물게 발생합니다. 이는 네트워크 타임아웃처럼 자주 설정/취소되는 타이머에 최적입니다.
설계 배경: 초기 커널(~1.x)은 정렬된 연결 리스트로 타이머를 관리하여 O(n) 삽입이 병목(Bottleneck)이 되었습니다. Linux 2.4에서 cascading timer wheel(TVR 256슬롯 + TVN 64슬롯×4)이 도입되어 O(1) 삽입/삭제를 달성했지만, 레벨 간 cascade storm으로 간헐적 지연 스파이크가 발생했습니다. 현재 커널(4.8+)은 Thomas Gleixner의 재설계로 5레벨×64슬롯 granularity 기반 구조를 사용하며, cascade를 완전히 제거하는 대신 상위 레벨에 의도적 양자화 오차를 도입했습니다. 자세한 버전별 오차 비교는 호출 시간 오차 분석을 참고하세요.
#include <linux/timer.h>
static struct timer_list my_timer;
static void my_timer_callback(struct timer_list *t)
{
pr_info("timer expired at jiffies=%lu\\n", jiffies);
/* Reschedule for periodic timer */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
}
/* 초기화 및 시작 */
timer_setup(&my_timer, my_timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
/* 해제 (동기적, 콜백 완료 대기) */
del_timer_sync(&my_timer);
코드 설명
include/linux/timer.h와 kernel/time/timer.c에 정의된 저해상도 타이머 API의 기본 사용 패턴입니다.
- timer_setup()
struct timer_list를 초기화하고 콜백 함수를 등록합니다. 세 번째 인자는 플래그로,0이면 일반 타이머,TIMER_DEFERRABLE이면 idle 시 지연 가능합니다. - mod_timer()타이머의 만료 시각을 설정하거나 변경합니다. 아직 활성화되지 않은 타이머도 활성화합니다. 내부에서
__mod_timer()→calc_wheel_index()를 호출하여 Timer Wheel의 적절한 레벨과 슬롯에 삽입합니다. - msecs_to_jiffies()밀리초를 jiffies 단위로 변환합니다. HZ=250일 때 1000ms → 250 jiffies가 됩니다.
- 콜백 내 mod_timer()콜백 함수 안에서
mod_timer()를 다시 호출하면 주기적 타이머 패턴이 됩니다. 커널에는 별도의 주기적 타이머 API가 없으므로 이 방식을 사용합니다. - del_timer_sync()타이머를 삭제하고, 다른 CPU에서 콜백이 실행 중이면 완료될 때까지 대기합니다. 모듈 해제 시 반드시 사용해야 합니다. 단순
del_timer()는 콜백 완료를 보장하지 않으므로 use-after-free 위험이 있습니다.
Timer Wheel 5단계 내부 구조
커널 4.8+의 Timer Wheel은 5단계 계층(base[0]~base[4])으로 구성됩니다. 각 레벨은 64개 슬롯을 가지며, 상위 레벨로 갈수록 시간 해상도(granularity)가 8배씩 거칠어집니다. 이 설계를 통해 총 232 틱(HZ=250 기준 약 198일)까지 커버합니다.
Timer Wheel 슬롯(Bucket) 내부 구조
각 레벨의 64개 슬롯은 hlist_head로 구현되며, 동일 슬롯에 여러 타이머가 해시 리스트로 연결됩니다. 타이머 삽입 시 hlist_add_head()로 O(1) 삽입, 만료 시 hlist_move_list()로 전체 슬롯을 O(1)에 수집합니다.
| Level | Granularity (ticks) | Granularity (시간) | 커버 범위 (ticks) | 커버 범위 (시간) | Slots |
|---|---|---|---|---|---|
| 0 | 1 | 4ms | 0 ~ 63 | 0 ~ 252ms | 64 |
| 1 | 8 | 32ms | 64 ~ 511 | 256ms ~ 2.05s | 64 |
| 2 | 64 | 256ms | 512 ~ 4,095 | 2.05s ~ 16.4s | 64 |
| 3 | 512 | 2.05s | 4,096 ~ 32,767 | 16.4s ~ 2.2min | 64 |
| 4 | 4,096 | 16.4s | 32,768 ~ 262,143 | 2.2min ~ 17.5min | 64 |
Granularity 계산과 슬롯 결정
타이머가 삽입될 때 커널은 만료까지 남은 틱 수(delta)의 최상위 비트(MSB)를 검사하여 레벨을 결정합니다. 레벨 결정 후 해당 레벨의 granularity로 양자화된 슬롯 인덱스에 타이머를 삽입합니다:
/* kernel/time/timer.c - calc_wheel_index() 핵심 로직 */
static unsigned calc_wheel_index(unsigned long expires,
unsigned long clk,
unsigned long *bucket_expiry)
{
unsigned long delta = expires - clk;
unsigned int idx;
if (delta < LVL_START(1)) {
/* Level 0: delta < 64 ticks */
idx = calc_index(expires, 0, bucket_expiry);
} else if (delta < LVL_START(2)) {
/* Level 1: 64 <= delta < 512 */
idx = calc_index(expires, 1, bucket_expiry);
} else if (delta < LVL_START(3)) {
/* Level 2: 512 <= delta < 4096 */
idx = calc_index(expires, 2, bucket_expiry);
} else if (delta < LVL_START(4)) {
/* Level 3: 4096 <= delta < 32768 */
idx = calc_index(expires, 3, bucket_expiry);
} else {
/* Level 4: 32768 <= delta */
idx = calc_index(expires, 4, bucket_expiry);
}
return idx;
}
/* 매크로 정의 (간략화) */
#define LVL_BITS 6 /* 64 slots per level */
#define LVL_SIZE (1 << LVL_BITS) /* 64 */
#define LVL_SHIFT(n) ((n) * LVL_CLK_SHIFT) /* n * 3 (8배씩 증가) */
#define LVL_START(n) (LVL_SIZE << LVL_SHIFT(n))
/* LVL_START(1)=64, LVL_START(2)=512, LVL_START(3)=4096, LVL_START(4)=32768 */
코드 설명
kernel/time/timer.c의 calc_wheel_index()는 타이머 삽입 시 Timer Wheel의 레벨과 슬롯을 결정하는 핵심 함수입니다.
- delta 계산
expires - clk로 현재 시점(base->clk)부터 만료까지 남은 틱 수를 구합니다. 이 값의 크기에 따라 레벨이 결정됩니다. - LVL_START(n) 비교각 레벨의 경계값과 비교하여 레벨을 선택합니다. Level 0은 0~63 ticks, Level 1은 64~511, Level 2는 512~4095, Level 3은 4096~32767, Level 4는 32768 이상을 담당합니다.
- calc_index()선택된 레벨 내에서 실제 슬롯 인덱스를 계산합니다. 만료 시각을 해당 레벨의 granularity로 나누어(양자화) 64개 슬롯 중 하나에 매핑합니다.
- LVL_CLK_SHIFT = 3레벨당 3비트 시프트(2^3 = 8배)로 granularity가 증가합니다. Level 0은 1 tick, Level 1은 8 ticks, Level 2는 64 ticks 단위입니다. 이 지수적 스케일링으로 총 2^32 ticks(약 198일@HZ=250)까지 커버합니다.
- bucket_expiry출력 파라미터로, 슬롯의 양자화된 만료 시각을 반환합니다. 상위 레벨일수록 양자화 오차가 커지므로, 먼 미래의 타이머는 정밀도가 떨어집니다.
calc_index() 내부 구현: 비트 시프트에 의한 양자화
calc_wheel_index()가 레벨을 결정한 후 호출하는 calc_index()는 실제로 슬롯 인덱스와 양자화된 만료 시각(bucket_expiry)을 계산하는 핵심 함수입니다. 이 함수의 동작을 이해하면 양자화 오차가 어디서, 얼마나, 왜 발생하는지 정확히 파악할 수 있습니다.
/* kernel/time/timer.c — calc_index() 실제 구현 (Linux 4.8+) */
#define LVL_CLK_SHIFT 3
#define LVL_SHIFT(n) ((n) * LVL_CLK_SHIFT) /* 0, 3, 6, 9, 12 */
#define LVL_GRAN(n) (1UL << LVL_SHIFT(n)) /* 1, 8, 64, 512, 4096 */
#define LVL_MASK (LVL_SIZE - 1) /* 0x3F = 63 */
#define LVL_OFS(n) ((n) * LVL_SIZE) /* 0, 64, 128, 192, 256 */
static inline unsigned calc_index(
unsigned long expires, /* 원래 만료 시각 (jiffies) */
unsigned int lvl, /* 선택된 레벨 (0~4) */
unsigned long *bucket_expiry) /* [출력] 양자화된 만료 시각 */
{
/*
* expires를 해당 레벨의 granularity로 오른쪽 시프트합니다.
* 이것은 expires ÷ granularity 정수 나눗셈과 동일합니다.
* 시프트 후 하위 6비트(& LVL_MASK)가 슬롯 인덱스가 됩니다.
*
* 예: Level 1 (LVL_SHIFT=3, granularity=8)
* expires = 100 → 100 >> 3 = 12 → 12 & 0x3F = 12
* 슬롯 인덱스 = 64 + 12 = 76 (LVL_OFS(1) + 12)
*/
unsigned long shifted = expires >> LVL_SHIFT(lvl);
/*
* bucket_expiry = 시프트된 값을 다시 왼쪽 시프트하여 복원합니다.
* 이 과정에서 하위 비트가 잘려나가 양자화가 발생합니다.
*
* 예: expires=100, Level 1
* shifted = 100 >> 3 = 12
* bucket_expiry = 12 << 3 = 96 (원래 100이 96으로 절삭!)
* 양자화 오차 = 100 - 96 = 4 ticks (16ms@250Hz, 4ms@1000Hz)
*/
*bucket_expiry = shifted << LVL_SHIFT(lvl);
/* 전체 벡터 배열 내 절대 인덱스 반환 */
return LVL_OFS(lvl) + (shifted & LVL_MASK);
}
코드 설명
kernel/time/timer.c의 calc_index()는 Timer Wheel에서 양자화가 실제로 발생하는 지점입니다. 오른쪽 시프트(÷granularity)와 왼쪽 시프트(×granularity)의 비대칭이 하위 비트 손실(양자화 오차)을 만들어냅니다.
- LVL_SHIFT(n) = n × 3각 레벨의 시프트 양입니다. Level 0은 0비트(시프트 없음 = 1 tick 정밀도), Level 1은 3비트(÷8), Level 2는 6비트(÷64), Level 3은 9비트(÷512), Level 4는 12비트(÷4096)입니다. 레벨당 3비트씩 증가하므로 granularity는 8배씩 거칠어집니다.
- LVL_GRAN(n) = 1 << LVL_SHIFT(n)각 레벨의 granularity(양자화 단위) 값입니다. Level 0=1, Level 1=8, Level 2=64, Level 3=512, Level 4=4096 ticks입니다.
- expires >> LVL_SHIFT(lvl)만료 시각을 granularity로 나누는 핵심 연산입니다. 오른쪽 시프트는 정수 나눗셈이므로 나머지가 버려집니다. 이 "버려지는 나머지"가 양자화 오차의 정체입니다. 예를 들어 Level 1(shift=3)에서 expires=100이면 100>>3=12이고, 하위 3비트(100 & 0x7 = 4)가 손실됩니다.
- *bucket_expiry = shifted << LVL_SHIFT(lvl)시프트된 값을 다시 복원하여 이 슬롯의 "양자화된 만료 시각"을 계산합니다. 오른쪽 시프트에서 버려진 하위 비트는 0으로 채워지므로,
bucket_expiry ≤ expires가 항상 성립합니다. 즉 이 슬롯은bucket_expiry시점에 검사되며, 그 시점에서expires ≤ bucket_expiry + granularity인 모든 타이머가 함께 만료 처리됩니다. - shifted & LVL_MASK시프트된 값의 하위 6비트를 추출하여 64개 슬롯 중 하나를 선택합니다.
LVL_OFS(lvl)을 더해 전체 320개(WHEEL_SIZE) 벡터 배열 내 절대 인덱스로 변환합니다.
아래 수치 예제로 양자화 과정을 구체적으로 추적합니다. clk = 1000(현재 base->clk) 시점에서 다양한 expires 값이 어떤 레벨, 어떤 슬롯에 배치되고, 양자화 오차가 얼마인지 확인합니다.
| expires | delta | Level | shifted (expires >> LVL_SHIFT) |
slot (shifted & 0x3F) |
bucket_expiry (shifted << LVL_SHIFT) |
양자화 오차 (expires − bucket_expiry) |
오차 시간 (@250Hz / @1000Hz) |
|---|---|---|---|---|---|---|---|
| 1030 | 30 | 0 (shift=0) | 1030 >> 0 = 1030 | 1030 & 63 = 6 | 1030 << 0 = 1030 | 0 ticks | 0ms / 0ms |
| 1100 | 100 | 1 (shift=3) | 1100 >> 3 = 137 | 137 & 63 = 9 | 137 << 3 = 1096 | 4 ticks | 16ms / 4ms |
| 1200 | 200 | 1 (shift=3) | 1200 >> 3 = 150 | 150 & 63 = 22 | 150 << 3 = 1200 | 0 ticks | 0ms / 0ms |
| 2345 | 1345 | 2 (shift=6) | 2345 >> 6 = 36 | 36 & 63 = 36 | 36 << 6 = 2304 | 41 ticks | 164ms / 41ms |
| 6000 | 5000 | 3 (shift=9) | 6000 >> 9 = 11 | 11 & 63 = 11 | 11 << 9 = 5632 | 368 ticks | 1.47s / 368ms |
| 10000 | 9000 | 3 (shift=9) | 10000 >> 9 = 19 | 19 & 63 = 19 | 19 << 9 = 9728 | 272 ticks | 1.09s / 272ms |
| 50000 | 49000 | 4 (shift=12) | 50000 >> 12 = 12 | 12 & 63 = 12 | 12 << 12 = 49152 | 848 ticks | 3.39s / 848ms |
양자화 공식 정리: 임의의 expires와 레벨 n에 대해 양자화 오차는 다음과 같이 계산됩니다.
- granularity = 2LVL_SHIFT(n) = 23n = {1, 8, 64, 512, 4096} ticks
- bucket_expiry = (expires >> LVL_SHIFT(n)) << LVL_SHIFT(n) = expires − (expires mod granularity)
- 양자화 오차 = expires mod granularity = expires & (granularity − 1), 범위: 0 ~ granularity − 1 ticks
- 핵심 성질: bucket_expiry ≤ expires < bucket_expiry + granularity. 동일 슬롯에 배치된 타이머들은 bucket_expiry 시점에서 한꺼번에 만료 검사되므로, 같은 슬롯 내 타이머들 사이에는 최대 granularity − 1 ticks의 원래 만료 시각 차이가 존재합니다.
Cascade 동작: 상위 레벨의 타이머가 해당 레벨의 granularity 경계에 도달하면, 하위 레벨로 재배치(cascade)됩니다. 예를 들어 Level 1의 타이머가 64 ticks 이내로 남으면 Level 0으로 이동합니다. 이 과정은 __run_timers()에서 collect_expired_timers() 호출 시 자동으로 수행됩니다. 대부분의 네트워크 타임아웃 타이머는 설정 후 취소되므로 cascade가 실제로 발생하는 빈도는 낮습니다.
Timer Wheel 호출 시간 오차 분석
Timer Wheel 타이머는 mod_timer()로 지정한 만료 시각과 실제 콜백 호출 시점 사이에 다양한 원인의 오차(Jitter)가 발생합니다. 커널 타이머의 근본 원칙은 "지정된 시점에 정확히 실행"이 아니라 "지정된 시점 이후 가능한 빨리 실행"이므로, 콜백은 항상 요청 시각보다 늦게 호출됩니다. 이 섹션에서는 오차를 유발하는 각 원인과 레벨별 최대 오차를 종합적으로 분석합니다.
| 오차 원인 | 최소 오차 | 최대 오차 (HZ=250, 4ms/tick) | 최대 오차 (HZ=1000, 1ms/tick) | 발생 조건 |
|---|---|---|---|---|
| 양자화 오차 (Level 0) | 0 | 0ms (0 ticks) | 0ms (0 ticks) | 항상 (granularity = 1 tick) |
| 양자화 오차 (Level 1) | 0 | 28ms (7 ticks) | 7ms (7 ticks) | delta ≥ 64 ticks |
| 양자화 오차 (Level 2) | 0 | 252ms (63 ticks) | 63ms (63 ticks) | delta ≥ 512 ticks |
| 양자화 오차 (Level 3) | 0 | ~2.04s (511 ticks) | ~511ms (511 ticks) | delta ≥ 4,096 ticks |
| 양자화 오차 (Level 4) | 0 | ~16.4s (4,095 ticks) | ~4.1s (4,095 ticks) | delta ≥ 32,768 ticks |
| 틱 처리 지연 (softirq) | ~0 | ~2ms (일반) / ~10ms+ (부하 시) | 항상 | |
| ksoftirqd 위임 지연 | 0 | 수십 ms | softirq 10회 재시도 초과 시 | |
| NO_HZ idle 지연 | 0 | 1 tick (4ms) | 1 tick (1ms) | CPU가 idle 상태 |
| TIMER_DEFERRABLE 지연 | 0 | 무제한 (다음 non-deferrable 이벤트까지) | TIMER_DEFERRABLE 플래그 설정 시 | |
| PREEMPT_RT 스케줄링 지연 | ~0 | 수 ms (우선순위 의존) | CONFIG_PREEMPT_RT 활성화 시 | |
(a) 양자화 오차(Quantization Error): calc_index()는 만료 시각을 오른쪽 비트 시프트(expires >> LVL_SHIFT(n))로 해당 레벨의 granularity 단위로 절삭(truncate)한 뒤, 왼쪽 시프트로 복원하여 bucket_expiry를 생성합니다. 이 과정에서 하위 LVL_SHIFT(n) 비트가 손실되어 양자화 오차가 발생합니다(상세 비트 연산 과정 참조). bucket_expiry ≤ expires가 항상 성립하므로 타이머가 일찍 실행되는 일은 없습니다. 최대 양자화 오차는 granularity - 1 ticks입니다. Level 0은 granularity가 1 tick이므로 양자화 오차가 0이지만, Level 4에서는 granularity가 4,096 ticks이므로 최대 4,095 ticks까지 오차가 발생합니다. 시간으로 환산하면 HZ=250에서는 ~16.4s, HZ=1000에서는 ~4.1s입니다. HZ가 높을수록 1 tick의 절대 시간이 짧아지므로 동일 레벨에서의 양자화 오차가 비례하여 감소합니다.
(b) softirq 처리 지연: Timer Wheel 콜백은 TIMER_SOFTIRQ 컨텍스트에서 실행됩니다. 타이머 틱 하드 인터럽트가 raise_softirq(TIMER_SOFTIRQ)를 호출한 후, 실제 __run_timers()가 실행되기까지 지연이 발생합니다. 일반적으로 수 마이크로초 수준이지만, 다른 softirq 핸들러(NET_RX_SOFTIRQ 등)가 먼저 실행되면 밀리초 단위로 늘어날 수 있습니다. 특히 __do_softirq()가 MAX_SOFTIRQ_TIME(2ms) 또는 MAX_SOFTIRQ_RESTART(10회) 한도를 초과하면, 나머지 처리를 ksoftirqd 커널 스레드에 위임합니다. ksoftirqd는 SCHED_OTHER 정책(nice 0)으로 동작하므로, CPU 부하가 높은 상황에서 수십 밀리초까지 스케줄링이 지연될 수 있습니다.
(c) NO_HZ/tickless 영향: NO_HZ 모드에서 idle CPU는 틱 인터럽트를 억제합니다. tick_nohz_idle_stop_tick()이 next_expiry 시점에 clockevent를 프로그래밍하므로, 정상적으로는 타이머 만료 시 CPU가 깨어납니다. 그러나 마지막 틱과 idle 진입 사이의 경합(Race) 구간에서 타이머가 삽입되면, 다른 CPU가 trigger_dyntick_cpu()로 IPI를 보내기 전까지 최대 1 tick(HZ=250에서 4ms, HZ=1000에서 1ms)의 추가 지연이 발생할 수 있습니다.
(d) TIMER_DEFERRABLE 추가 지연: TIMER_DEFERRABLE 플래그가 설정된 타이머는 BASE_DEF에 삽입되며, idle CPU를 깨우지 않습니다. CPU가 계속 idle 상태이면 다음 non-deferrable 이벤트(다른 타이머, 외부 인터럽트 등)가 발생할 때까지 콜백이 무기한 지연됩니다. idle 상태가 드문 서버에서는 영향이 적지만, 유휴 시간이 긴 임베디드 시스템에서는 수 분 이상 지연될 수 있습니다.
(e) PREEMPT_RT 스케줄링 지연: CONFIG_PREEMPT_RT 커널에서는 softirq가 전용 ksoftirqd/N 커널 스레드에서 항상 실행됩니다(일반 커널처럼 인터럽트 반환 경로에서 실행되지 않습니다). 이 스레드는 기본적으로 SCHED_OTHER 정책이므로, 높은 우선순위의 RT 태스크에 의해 선점되면 타이머 콜백 실행이 수 밀리초 이상 지연될 수 있습니다. chrt 명령으로 ksoftirqd의 우선순위를 높이면 이 지연을 줄일 수 있습니다.
(f) timer_slack_ns 영향: /proc/<pid>/timer_slack_ns는 유저스페이스 타이머(nanosleep(), select(), poll() 등)에 추가 여유 시간(slack)을 부여합니다. 커널 내부의 timer_list에는 직접 적용되지 않지만, hrtimer 기반의 유저스페이스 인터페이스에서 의도적 그룹핑(batching)을 유발하여 전력 효율을 높입니다. 기본값은 비-RT 프로세스에서 약 50µs이며, prctl(PR_SET_TIMERSLACK)으로 조정 가능합니다.
| Level | Granularity | 양자화 오차 (최대) | softirq 지연 (전형적 최대) | 합산 최대 오차 (일반 커널) | 합산 최대 오차 (PREEMPT_RT) |
|---|---|---|---|---|---|
| 0 | 1 tick = 4ms | 0ms | ~2ms | ~2ms | ~5ms |
| 1 | 8 ticks = 32ms | 28ms | ~2ms | ~30ms | ~33ms |
| 2 | 64 ticks = 256ms | 252ms | ~2ms | ~254ms | ~257ms |
| 3 | 512 ticks = 2.05s | ~2.04s | ~2ms | ~2.05s | ~2.05s |
| 4 | 4,096 ticks = 16.4s | ~16.4s | ~2ms | ~16.4s | ~16.4s |
| Level | Granularity | 커버 범위 (시간) | 양자화 오차 (최대) | softirq 지연 (전형적 최대) | 합산 최대 오차 (일반 커널) | 합산 최대 오차 (PREEMPT_RT) |
|---|---|---|---|---|---|---|
| 0 | 1 tick = 1ms | 0 ~ 63ms | 0ms | ~2ms | ~2ms | ~5ms |
| 1 | 8 ticks = 8ms | 64ms ~ 511ms | 7ms | ~2ms | ~9ms | ~12ms |
| 2 | 64 ticks = 64ms | 512ms ~ 4.1s | 63ms | ~2ms | ~65ms | ~68ms |
| 3 | 512 ticks = 512ms | 4.1s ~ 32.8s | ~511ms | ~2ms | ~513ms | ~516ms |
| 4 | 4,096 ticks = 4.1s | 32.8s ~ 4.4min | ~4.1s | ~2ms | ~4.1s | ~4.1s |
HZ=250 vs HZ=1000 비교: HZ=1000은 1 tick이 1ms이므로 모든 레벨에서 양자화 오차가 HZ=250 대비 4배 감소합니다. Level 1 기준 최대 양자화 오차는 28ms(HZ=250) → 7ms(HZ=1000)로 줄어들며, Level 4에서는 ~16.4s → ~4.1s로 크게 개선됩니다. 다만 HZ=1000은 초당 틱 인터럽트가 4배 증가하므로 CPU 오버헤드가 함께 증가합니다. 또한 양자화 오차는 줄어들지만, softirq 지연(~2ms)이나 ksoftirqd 위임 지연은 HZ 값과 무관하게 동일하므로, Level 0처럼 양자화 오차가 0인 경우에는 HZ를 높여도 실질적인 최대 오차 개선 효과가 제한적입니다.
커널 버전별 Timer Wheel 오차 특성 변화: Timer Wheel의 내부 구조는 커널 역사에서 여러 차례 대폭 변경되었으며, 각 세대마다 오차의 성격과 크기가 다릅니다. 아래 표는 주요 커널 버전 구간별 Timer Wheel 구현 차이와 그에 따른 오차 특성을 요약합니다.
| 커널 버전 | Timer Wheel 구조 | 양자화 오차 | Cascade 오차 | 주요 변경 사항 |
|---|---|---|---|---|
| 2.6.16 ~ 4.7 | 5단계 Cascading Wheel (TVR: 256슬롯 + TVN: 64슬롯×4) |
0 (전 레벨 1 tick 해상도) | 있음 (cascade storm 발생) | 전통적 cascading timer wheel. 모든 레벨에서 1 tick 정밀도를 유지하지만, 하위 레벨 래핑 시 상위 슬롯의 타이머를 일괄 재배치(cascade)하는 과정에서 지연 스파이크가 발생합니다. |
| 4.8+ (2016) | 5단계 Granularity Wheel (5×64슬롯, LVL_CLK_SHIFT=3) |
있음 (레벨별 8배 증가) | 없음 (cascade 제거) | Thomas Gleixner의 전면 재설계. 상위 레벨에 의도적 양자화를 도입하여 cascade를 완전히 제거했습니다. 양자화 오차가 새로운 주요 오차 원인이 되었지만, cascade storm에 의한 예측 불가능한 지연 스파이크가 사라졌습니다. |
| 4.15+ (2018) | 동일 (5×64) | 동일 | 없음 | timer_setup() API 도입으로 init_timer()/setup_timer() 대체. 콜백 시그니처가 struct timer_list *로 통일되어 container_of() 패턴을 강제합니다. 오차 특성 자체는 변화 없습니다. |
| 5.4+ (2019) | 동일 (5×64) | 동일 | 없음 | hrtimer에 HRTIMER_MODE_SOFT 지원 추가. PREEMPT_RT에서 softirq가 항상 ksoftirqd 스레드에서 실행되는 구조가 안정화되어, RT 커널의 타이머 지연 특성이 명확해졌습니다. |
| 6.2+ (2023) | 동일 (5×64) | 동일 | 없음 | timer_shutdown() / timer_shutdown_sync() 도입. 타이머를 영구 비활성화하여 콜백 내 mod_timer() 재등록을 방지합니다. 해제 경로의 안전성이 향상되었지만 오차 특성은 변화 없습니다. |
| 6.8+ (2024) | 동일 (5×64) + tmigr 계층 | 동일 | 없음 | Timer Migration Group(tmigr) 도입. CPU를 계층적 그룹으로 묶어 idle CPU의 타이머 마이그레이션을 효율화합니다. 대규모 NUMA 시스템에서 마이그레이션 지연이 감소하여 NO_HZ idle 상태의 타이머 처리 지연이 개선됩니다. |
Pre-4.8 Cascade Storm vs 4.8+ 양자화 오차: 커널 4.8 이전의 cascading timer wheel에서는 모든 레벨에서 1 tick 정밀도를 유지하므로 양자화 오차가 0이었습니다. 그러나 Level 0이 한 바퀴(256 ticks) 도는 시점마다, 상위 레벨의 한 슬롯에 있는 모든 타이머를 하위 레벨로 일괄 재배치하는 cascade가 발생했습니다. 이 cascade 처리 시간은 해당 슬롯의 타이머 수에 비례(O(n))하므로, 타이머가 많은 시스템(네트워크 서버 등)에서 수백 µs ~ 수 ms의 예측 불가능한 지연 스파이크를 유발했습니다. 4.8+ 재설계는 이 cascade를 완전히 제거하는 대신, 상위 레벨에 의도적으로 양자화 오차를 도입하여 "예측 불가능한 간헐적 스파이크"를 "예측 가능한 고정 오차"로 교환한 설계입니다.
/* Pre-4.8 Cascading Timer Wheel 구조 (kernel/timer.c, Linux 2.6.x ~ 4.7) */
#define TVN_BITS 6 /* 64 slots per upper level */
#define TVR_BITS 8 /* 256 slots in level 0 */
#define TVN_SIZE (1 << TVN_BITS) /* 64 */
#define TVR_SIZE (1 << TVR_BITS) /* 256 */
struct tvec_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies; /* 현재 처리 중인 jiffies */
struct tvec_root tv1; /* Level 0: 256 slots (0~255 ticks) */
struct tvec tv2; /* Level 1: 64 slots (256~16383 ticks) */
struct tvec tv3; /* Level 2: 64 slots (~1M ticks) */
struct tvec tv4; /* Level 3: 64 slots (~67M ticks) */
struct tvec tv5; /* Level 4: 64 slots (~4G ticks) */
} ____cacheline_aligned;
/* cascade(): 상위 레벨 슬롯의 모든 타이머를 하위 레벨로 재배치 */
static int cascade(struct tvec_base *base,
struct tvec *tv, int index)
{
struct list_head *head, *curr;
head = tv->vec + index;
/* 이 슬롯의 모든 타이머를 순회하며 재삽입 — O(n) */
while (!list_empty(head)) {
struct timer_list *timer;
timer = list_first_entry(head, struct timer_list, entry);
__internal_add_timer(base, timer); /* 하위 레벨에 재삽입 */
}
return index;
}
코드 설명
커널 4.7 이전의 전통적 cascading timer wheel 구조입니다. kernel/timer.c에 정의되어 있었으며, 4.8에서 완전히 대체되었습니다.
- TVR_BITS=8, TVN_BITS=6Level 0(tv1)은 256개 슬롯으로 0~255 ticks를 1 tick 해상도로 커버합니다. 상위 레벨(tv2~tv5)은 각 64개 슬롯입니다. 현재 4.8+ 설계(LVL_BITS=6, 모든 레벨 64슬롯)와 달리 Level 0이 4배 넓습니다.
- tv1 ~ tv55단계 wheel 구조입니다. 모든 레벨에서 타이머는 정확한 jiffies 값으로 관리되므로 양자화 오차가 없습니다. 다만 상위 레벨의 타이머는 만료 전에 하위 레벨로 cascade되어야 합니다.
- cascade()tv1이 256 ticks를 한 바퀴 돌 때마다 tv2의 현재 슬롯에 있는 모든 타이머를 tv1로 재배치합니다. 마찬가지로 tv2가 한 바퀴 돌면 tv3에서 cascade가 발생합니다. 이 과정은 해당 슬롯의 타이머 수에 비례하는 O(n) 연산이며, 이것이 "cascade storm"의 원인입니다.
- __internal_add_timer()cascade된 타이머를 새로운 만료 시각에 맞춰 적절한 하위 레벨에 재삽입합니다. 한 슬롯에 수천 개의 타이머가 있으면 이 재삽입 루프가 밀리초 단위 지연을 유발합니다.
| 오차 특성 | Pre-4.8 (Cascading Wheel) | 4.8+ (Granularity Wheel) |
|---|---|---|
| 양자화 오차 | 0 (모든 레벨 1 tick 해상도) | Level별 0 ~ ~16.4s@250Hz / ~4.1s@1000Hz |
| Cascade storm | Level 0 래핑(매 256 ticks ≈ 1.02s@250Hz)마다 발생. 최악 수 ms 지연 스파이크 | 없음 (cascade 제거) |
| 오차 예측 가능성 | 낮음 — cascade 시점과 슬롯 내 타이머 수에 의존 | 높음 — 레벨별 granularity로 최대 오차가 결정적 |
| Level 0 슬롯 수 | 256 (TVR_SIZE) | 64 (LVL_SIZE) |
| Level 0 커버 범위 | 0 ~ 255 ticks (1.02s@250Hz) | 0 ~ 63 ticks (252ms@250Hz) |
| 총 커버 범위 | 232 ticks (~198일@250Hz) | 232 ticks (~198일@250Hz) |
| softirq 지연 | ~2ms (일반), cascade 시 추가 지연 | ~2ms (일반), 일정함 |
| 타이머 삽입 복잡도 | O(1) | O(1) |
| 만료 처리 최악 복잡도 | O(n) — cascade 시 n = 슬롯 내 타이머 수 | O(1) — cascade 없음 |
실무 가이드:
- 1ms 미만 정밀도가 필요하면
timer_list대신hrtimer를 사용하세요. hrtimer는 나노초 해상도로 하드웨어 클럭 이벤트에 직접 프로그래밍하므로 양자화 오차가 없습니다. - 지연 민감 타이머에는
TIMER_DEFERRABLE플래그를 설정하지 마세요. 이 플래그는 전력 절약이 목적이며, 콜백 호출이 무기한 지연될 수 있습니다. - 실제 jitter 측정에는
cyclictest도구를 사용하세요:cyclictest -m -p90 -i1000 -l10000으로 1ms 주기의 최소/평균/최대 지연을 측정할 수 있습니다. - PREEMPT_RT 환경에서는
chrt -f 50 $(pidof ksoftirqd/0)처럼ksoftirqd스레드의 RT 우선순위를 높여 타이머 지연을 줄일 수 있습니다. - 상위 레벨(Level 2~4)의 양자화 오차가 지배적이므로, 수 초 이상의 타이머에서는 softirq 지연보다 양자화 오차가 주요 오차 원인입니다.
- HZ=1000을 사용하면 양자화 오차가 4배 줄어들지만, 틱 인터럽트 오버헤드가 증가합니다. 오디오/게이밍 등 저지연이 중요한 워크로드에서는
CONFIG_HZ_1000이 효과적이며, 서버 워크로드에서는CONFIG_HZ_250이 오버헤드 대비 적합합니다. - 커널 4.8 미만을 사용하는 레거시 시스템에서는 양자화 오차가 없지만, 타이머가 많은 환경에서 cascade storm에 의한 간헐적 지연 스파이크에 주의해야 합니다. 가능하면 커널 4.8 이상으로 업그레이드하여 예측 가능한 오차 특성을 확보하는 것이 권장됩니다.
- 커널 6.8+의 tmigr는 대규모 시스템에서 타이머 마이그레이션 효율을 크게 개선합니다. NUMA 시스템에서 NO_HZ 관련 타이머 지연이 문제라면 6.8 이상을 권장합니다.
struct timer_base 구현 분석
Timer Wheel의 핵심 자료구조인 struct timer_base는 Per-CPU로 할당되며, 모든 타이머 관리 상태를 캡슐화합니다. 커널 6.x에서의 실제 정의를 살펴봅니다.
/* kernel/time/timer.c — struct timer_base (Linux 6.x) */
struct timer_base {
raw_spinlock_t lock; /* Per-CPU lock (IRQ 비활성화 필요) */
struct timer_list *running_timer; /* 현재 콜백 실행 중인 타이머 */
unsigned long clk; /* 현재 처리 중인 jiffies (≤ jiffies) */
unsigned long next_expiry; /* 다음 만료 시각 (빠른 경로 최적화) */
unsigned int cpu; /* 소속 CPU 번호 */
bool next_expiry_recalc; /* next_expiry 재계산 필요 플래그 */
bool is_idle; /* CPU idle 상태 여부 */
bool timers_pending; /* 만료 대기 타이머 존재 */
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
/* 5레벨×64슬롯=320비트 비트맵
* 비트 1 = 해당 슬롯에 타이머 존재
* find_next_bit()로 O(1) 만료 탐색 */
struct hlist_head vectors[WHEEL_SIZE];
/* WHEEL_SIZE = 5×64 = 320 해시 버킷
* 각 슬롯은 hlist(해시 리스트)로 타이머 연결 */
} ____cacheline_aligned;
/* Per-CPU 변수 선언 */
static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);
/* NR_BASES = 2: BASE_STD(일반) + BASE_DEF(deferrable) */
코드 설명
kernel/time/timer.c에 정의된 Timer Wheel의 핵심 Per-CPU 구조체입니다. 타이머 삽입, 삭제, 만료 처리의 모든 상태를 관리합니다.
- raw_spinlock_t lockPer-CPU lock으로, 타이머 조작 시 IRQ를 비활성화한 상태에서 획득합니다.
raw_spinlock이므로 PREEMPT_RT 환경에서도 spinning lock으로 유지됩니다. 콜백 실행 중에는 lock을 해제하여 다른 타이머 조작이 가능합니다. - running_timer현재 콜백이 실행 중인 타이머를 가리킵니다.
del_timer_sync()가 이 필드를 확인하여, 현재 실행 중인 타이머의 완료를 대기합니다. 콜백 완료 후 NULL로 리셋됩니다. - clk
__run_timers()에서 처리 중인 현재 시점입니다. 실제jiffies보다 뒤처질 수 있으며, 루프에서 한 틱씩 전진하면서 만료 타이머를 수집합니다. - next_expiry가장 빨리 만료되는 타이머의 시각입니다.
__run_timers()진입 시 이 값과 jiffies를 비교하여, 만료 타이머가 없으면 lock 획득 없이 빠르게 반환합니다. NO_HZ에서 다음 wakeup 시점 계산에도 사용됩니다. - pending_map320비트 비트맵으로, 각 비트가 하나의 wheel 슬롯에 대응합니다. 타이머가 존재하는 슬롯의 비트만 1이므로,
find_next_bit()으로 만료된 타이머가 있는 슬롯을 O(1)에 찾습니다. 전체 320개 슬롯을 순회하지 않아도 됩니다. - NR_BASES = 2
BASE_STD(일반 타이머)와BASE_DEF(deferrable 타이머) 두 종류의 base를 유지합니다.TIMER_DEFERRABLE플래그가 설정된 타이머는BASE_DEF에 삽입되며, idle 상태에서 CPU를 깨우지 않습니다.
enqueue_timer() 내부 구현
타이머가 Timer Wheel에 삽입되는 실제 경로를 분석합니다. mod_timer() → __mod_timer() → enqueue_timer() 호출 체인으로 진행됩니다.
/* kernel/time/timer.c — enqueue_timer() (Linux 6.x, 간략화) */
static void enqueue_timer(struct timer_base *base,
struct timer_list *timer,
unsigned int idx,
unsigned long bucket_expiry)
{
/* 1. 해당 슬롯의 해시 리스트에 타이머 추가 */
hlist_add_head(&timer->entry, base->vectors + idx);
/* 2. pending_map 비트맵 갱신 — 해당 슬롯 비트 ON */
__set_bit(idx, base->pending_map);
/* 3. next_expiry 갱신 — 더 빠른 만료가 추가되면 업데이트 */
if (time_before(bucket_expiry, base->next_expiry)) {
base->next_expiry = bucket_expiry;
base->next_expiry_recalc = false;
base->timers_pending = true;
/* 4. idle 상태의 다른 CPU를 깨움 (NO_HZ 환경) */
trigger_dyntick_cpu(base, timer);
}
}
/* __mod_timer() — 타이머 삽입/변경의 핵심 경로 */
static inline int __mod_timer(struct timer_list *timer,
unsigned long expires, unsigned int options)
{
struct timer_base *base;
unsigned int idx;
unsigned long clk, bucket_expiry;
/* 이미 활성화된 타이머면 기존 슬롯에서 제거 */
if (timer_pending(timer))
detach_timer(timer, true); /* hlist에서 제거 + pending_map 갱신 */
base = lock_timer_base(timer, &flags);
clk = base->clk;
/* 레벨과 슬롯 인덱스 계산 */
idx = calc_wheel_index(expires, clk, &bucket_expiry);
timer->expires = expires;
/* wheel에 삽입 */
enqueue_timer(base, timer, idx, bucket_expiry);
raw_spin_unlock_irqrestore(&base->lock, flags);
return 0;
}
코드 설명
kernel/time/timer.c의 enqueue_timer()와 __mod_timer()는 타이머를 Timer Wheel에 삽입하는 핵심 경로입니다.
- hlist_add_head()계산된 슬롯 인덱스(
idx)의 해시 리스트 헤드에 타이머를 추가합니다. 동일 슬롯에 여러 타이머가 있을 수 있으며, 만료 시 해당 슬롯의 모든 타이머가 수집됩니다. - __set_bit(idx, pending_map)해당 슬롯에 타이머가 존재함을 비트맵에 기록합니다.
collect_expired_timers()에서find_next_bit()으로 만료 슬롯을 빠르게 탐색하는 데 사용됩니다. - next_expiry 갱신새 타이머의 만료가 현재
next_expiry보다 빠르면 갱신합니다. 이 값은__run_timers()의 조기 반환 최적화와 NO_HZ에서 다음 wakeup 시점 계산에 사용됩니다. - trigger_dyntick_cpu()NO_HZ 환경에서 idle CPU에 타이머를 추가하면, 해당 CPU를 깨워 새 만료 시점에 맞춰 clockevent를 재프로그래밍해야 합니다. 이 함수가 IPI(Inter-Processor Interrupt)를 보내 idle CPU를 깨웁니다.
- detach_timer()이미 활성화된 타이머를 다시
mod_timer()하면, 기존 슬롯에서 먼저 제거(hlist_del + pending_map 비트 정리)한 후 새 슬롯에 재삽입합니다. 이 과정은 원자적으로 lock 내에서 수행됩니다.
고해상도 타이머 (hrtimer)
hrtimer는 나노초 단위의 정밀한 타이머입니다. Timer Wheel 대신 red-black tree로 관리되며, 하드웨어 클럭 이벤트에 직접 프로그래밍합니다.
hrtimer 동작 원리
hrtimer가 Timer Wheel과 근본적으로 다른 점은 틱에 의존하지 않습니다는 것입니다:
- 자료구조: 모든 활성 hrtimer를 만료 시간 순으로 red-black tree에 정렬합니다. 가장 빨리 만료되는 타이머가 항상 leftmost 노드에 위치합니다.
- 하드웨어 프로그래밍: leftmost 타이머의 만료 시간을
clockevent디바이스에 직접 설정합니다. 해당 시점에 하드웨어 인터럽트가 발생하여 콜백을 실행합니다. - 재프로그래밍: 새 hrtimer가 삽입되어 leftmost가 바뀌면, clockevent를 즉시 재프로그래밍합니다.
이 방식은 Timer Wheel의 틱 해상도(1/HZ초 = 4ms@250Hz) 제약을 극복하여, 하드웨어가 지원하는 한 나노초 수준의 정밀도를 달성합니다. POSIX 타이머, nanosleep(), 스케줄러의 bandwidth throttling 등이 hrtimer를 기반으로 합니다.
#include <linux/hrtimer.h>
#include <linux/ktime.h>
static struct hrtimer my_hrtimer;
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
pr_info("hrtimer fired!\\n");
/* Periodic: restart after 10ms */
hrtimer_forward_now(timer, ms_to_ktime(10));
return HRTIMER_RESTART;
/* One-shot: don't restart */
/* return HRTIMER_NORESTART; */
}
/* 초기화 및 시작 */
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = my_hrtimer_callback;
hrtimer_start(&my_hrtimer, ms_to_ktime(10), HRTIMER_MODE_REL);
/* 취소 */
hrtimer_cancel(&my_hrtimer);
코드 설명
include/linux/hrtimer.h와 kernel/time/hrtimer.c에 정의된 고해상도 타이머 기본 사용 패턴입니다.
- hrtimer_init()
struct hrtimer를 초기화합니다. 클럭 베이스(CLOCK_MONOTONIC)와 모드(HRTIMER_MODE_REL: 상대 시간)를 지정합니다. 내부에서 per-CPUhrtimer_cpu_base의 해당 클럭 베이스에 연결합니다. - function 콜백콜백은
enum hrtimer_restart를 반환합니다.HRTIMER_RESTART를 반환하면 타이머가 재활성화되고,HRTIMER_NORESTART면 일회성으로 종료됩니다. - hrtimer_forward_now()현재 시각 기준으로 다음 만료 시각을 재설정합니다. 주기적 hrtimer 패턴에서 사용하며, 내부에서
hrtimer_forward()→ktime_add()로 만료 시각을 갱신합니다. - hrtimer_start()타이머를 활성화하여 timerqueue(Red-Black tree)에 삽입합니다. 삽입된 타이머가 leftmost(가장 이른 만료)이면
tick_program_event()로 clockevent 하드웨어를 즉시 재프로그래밍합니다. - hrtimer_cancel()타이머를 취소하고 콜백 실행 중이면 완료까지 대기합니다. 호출 체인:
hrtimer_cancel()→hrtimer_try_to_cancel()→__remove_hrtimer(). IRQ 컨텍스트에서는hrtimer_try_to_cancel()을 직접 사용해야 합니다.
hrtimer 삽입 및 clockevent 재프로그래밍 내부
hrtimer_start() 호출 시 내부에서 실행되는 핵심 함수인 enqueue_hrtimer()와 hrtimer_reprogram()을 분석합니다. 이 두 함수가 hrtimer의 나노초 정밀도를 실현하는 핵심 메커니즘입니다.
/* kernel/time/hrtimer.c — enqueue_hrtimer() (Linux 6.x, 간략화) */
static void enqueue_hrtimer(struct hrtimer *timer,
struct hrtimer_clock_base *base,
enum hrtimer_mode mode)
{
/* timerqueue(red-black tree)에 만료 시간순으로 삽입 */
bool leftmost = timerqueue_add(&base->active, &timer->node);
/* timerqueue_add():
* 1. rb_add_cached()로 RB-tree에 삽입
* 2. 삽입된 노드가 leftmost(가장 이른 만료)이면 true 반환
* 3. leftmost 캐시 자동 갱신 (O(1) 접근 보장) */
/* 타이머 상태를 ENQUEUED로 전환 */
timer->state = HRTIMER_STATE_ENQUEUED;
/* leftmost가 변경되었으면 clockevent 재프로그래밍 필요 */
if (leftmost)
base->cpu_base->softirq_expires_next = timer->node.expires;
}
/* kernel/time/hrtimer.c — hrtimer_reprogram() (Linux 6.x, 간략화) */
static void hrtimer_reprogram(struct hrtimer *timer,
bool reprogram)
{
struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
struct hrtimer_clock_base *base = timer->base;
ktime_t expires = ktime_sub(
hrtimer_get_expires(timer), base->offset);
/* 절대 시각 → monotonic 기준 변환 */
/* 이미 만료된 시간이면 재프로그래밍 불필요 */
if (ktime_before(expires, ktime_get()))
return;
/* 현재 설정된 다음 만료보다 더 이른 경우에만 재프로그래밍 */
if (expires < cpu_base->expires_next) {
cpu_base->expires_next = expires;
/* clockevent 하드웨어에 새 만료 시각 프로그래밍 */
tick_program_event(expires, 0);
/* → clock_event_device->set_next_event()
* x86: APIC_TMICT 레지스터에 delta cycle 기록
* ARM64: CNTP_TVAL_EL0에 delta cycle 기록 */
}
}
/* hrtimer_start() → __hrtimer_start_range_ns() 핵심 흐름 */
static void __hrtimer_start_range_ns(struct hrtimer *timer,
ktime_t tim,
u64 delta_ns,
const enum hrtimer_mode mode,
struct hrtimer_clock_base *base)
{
/* 이미 활성화된 타이머면 먼저 제거 */
remove_hrtimer(timer, base, true, false);
/* soft range 설정 (timer slack) */
hrtimer_set_expires_range_ns(timer, tim, delta_ns);
/* RB-tree에 삽입 */
enqueue_hrtimer(timer, base, mode);
/* leftmost 변경 시 clockevent 재프로그래밍 */
hrtimer_reprogram(timer, true);
}
코드 설명
kernel/time/hrtimer.c의 hrtimer 삽입 경로입니다. hrtimer_start() → __hrtimer_start_range_ns() → enqueue_hrtimer() + hrtimer_reprogram() 순서로 실행됩니다.
- timerqueue_add()Red-Black tree에 만료 시간순으로 삽입합니다. 내부에서
rb_add_cached()를 사용하여 leftmost 노드(가장 이른 만료)를 O(1)에 캐싱합니다. 삽입된 노드가 새 leftmost이면true를 반환합니다. - leftmost 변경 감지새로 삽입된 hrtimer가 가장 이른 만료 시각이면, clockevent 하드웨어를 즉시 재프로그래밍해야 합니다. 이전 leftmost보다 더 늦은 타이머가 삽입되면 재프로그래밍이 필요 없습니다.
- tick_program_event()clockevent 프레임워크를 통해 하드웨어 타이머에 새 만료 시각을 설정합니다. ktime(나노초)을 cycle 단위로 변환(
mult/shift)한 후 아키텍처별 레지스터에 기록합니다. x86에서는 LAPIC 타이머의APIC_TMICT레지스터, ARM64에서는CNTP_TVAL_EL0레지스터를 프로그래밍합니다. - expires_next 비교현재 프로그래밍된 다음 만료 시각(
cpu_base->expires_next)보다 새 타이머가 더 이를 때만 재프로그래밍합니다. 불필요한 하드웨어 접근을 최소화하여 오버헤드를 줄입니다. - remove_hrtimer()이미 활성화된 hrtimer를 다시
hrtimer_start()하면, 기존 RB-tree 노드를 먼저 제거한 후 새 만료 시각으로 재삽입합니다. Timer Wheel의detach_timer()와 같은 역할입니다.
hrtimer 레드블랙 트리(Red-Black Tree)와 timerqueue 구조
hrtimer는 내부적으로 struct timerqueue_head를 사용하여 Red-Black 트리에 타이머를 정렬합니다. timerqueue는 일반 rbtree에 leftmost 캐싱을 추가한 특수 자료구조로, 다음 만료 타이머를 O(1)에 접근할 수 있습니다.
/* include/linux/timerqueue.h - timerqueue 핵심 구조체 */
struct timerqueue_node {
struct rb_node node; /* RB-tree 노드 */
ktime_t expires; /* 만료 시각 (나노초) */
};
struct timerqueue_head {
struct rb_root_cached rb_root; /* leftmost 캐싱된 RB-root */
};
/* include/linux/hrtimer.h - hrtimer 핵심 구조체 */
struct hrtimer {
struct timerqueue_node node; /* timerqueue 노드 내장 */
ktime_t _softexpires; /* 소프트 만료 (range 하한) */
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
u8 state; /* HRTIMER_STATE_INACTIVE/ENQUEUED/CALLBACK */
u8 is_rel; /* 상대 시간 여부 */
u8 is_soft; /* softirq 모드 여부 */
u8 is_hard; /* hardirq 모드 여부 */
};
/* hrtimer_clock_base: 클럭 베이스별 타이머 트리 */
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base;
unsigned int index; /* HRTIMER_BASE_MONOTONIC 등 */
clockid_t clockid; /* CLOCK_MONOTONIC 등 */
seqcount_raw_spinlock_t seq;
struct hrtimer *running; /* 현재 실행 중인 타이머 */
struct timerqueue_head active; /* 활성 타이머 RB-tree */
ktime_t (*get_time)(void); /* 시간 읽기 함수 */
ktime_t offset; /* 클럭 오프셋 */
};
코드 설명
include/linux/timerqueue.h와 include/linux/hrtimer.h에 정의된 hrtimer 핵심 자료구조입니다.
- timerqueue_nodeRed-Black tree 노드(
rb_node)와 만료 시각(expires, 나노초)을 포함합니다.hrtimer내에 내장되어 timerqueue에 삽입됩니다. - timerqueue_head
rb_root_cached는 일반 RB-tree에 leftmost 노드 캐싱을 추가한 구조입니다.timerqueue_getnext()로 가장 이른 만료 타이머를 O(1)에 접근합니다. - struct hrtimer고해상도 타이머의 핵심 구조체입니다.
_softexpires는 range-based 만료의 하한으로, 실제 만료(node.expires)와의 차이가 timer slack(그룹핑 범위)입니다. - is_soft / is_hard콜백 실행 컨텍스트를 결정합니다.
is_hard=1이면 하드 IRQ 컨텍스트(hrtimer_interrupt())에서,is_soft=1이면 softirq 컨텍스트(HRTIMER_SOFTIRQ)에서 실행됩니다. - hrtimer_clock_base클럭 베이스별(MONOTONIC, REALTIME, BOOTTIME, TAI) 독립적인 timerqueue를 유지합니다.
get_time()은 해당 클럭의 현재 시각을 반환하는 함수 포인터이고,offset은 클럭 조정값입니다. - running / seq
running은 현재 콜백 실행 중인 타이머를 가리켜hrtimer_cancel()의 동기 대기를 지원합니다.seq는 seqcount로, 타이머 상태 변경의 원자성을 보장합니다.
hrtimer 클럭 베이스 4종
hrtimer는 4종의 클럭 베이스를 지원합니다. 각 클럭 베이스는 독립적인 timerqueue(RB-tree)를 유지하며, 사용 목적에 따라 적절한 클럭을 선택해야 합니다.
| 클럭 베이스 | clockid | NTP 조정 | suspend 포함 | 윤초 | get_time() | 주요 사용 사례 |
|---|---|---|---|---|---|---|
| MONOTONIC | CLOCK_MONOTONIC |
없음 (주파수만) | 미포함 | 해당없음 | ktime_get() |
스케줄러, nanosleep, 경과 시간 측정 |
| REALTIME | CLOCK_REALTIME |
있음 (시간 점프) | 포함 | 영향 받음 | ktime_get_real() |
POSIX timer_create, 파일 타임스탬프 |
| BOOTTIME | CLOCK_BOOTTIME |
없음 | 포함 | 해당없음 | ktime_get_boottime() |
Android 알람, 세션 타임아웃 |
| TAI | CLOCK_TAI |
있음 | 포함 | 없음 (원자시) | ktime_get_clocktai() |
PTP (IEEE 1588), 금융 타임스탬프 |
- CLOCK_REALTIME 타이머의 시간 점프 위험 — NTP가 시계를 앞으로 또는 뒤로 조정하면 REALTIME 기반 타이머가 즉시 만료되거나 지연될 수 있습니다. 경과 시간 측정에는 MONOTONIC을 사용하세요.
- suspend 포함 여부 — MONOTONIC은 suspend 동안 멈추지만 BOOTTIME은 계속 흐릅니다. suspend를 넘겨야 하는 타이머(예: 알람)에는 BOOTTIME을 사용하세요.
- hard vs soft 모드 — 커널 5.4+에서 hrtimer는
HRTIMER_MODE_SOFT를 지원합니다. soft hrtimer는 softirq에서 실행되어 IRQ context 제약이 일부 완화됩니다.
CLOCK_BOOTTIME과 CLOCK_MONOTONIC의 차이와 선택 기준
CLOCK_BOOTTIME과 CLOCK_MONOTONIC은 모두 단조 증가 클럭이지만, suspend/resume 시간 포함 여부에서 결정적으로 다릅니다. 이 차이를 무시하면 suspend 후 타이머가 의도와 다르게 동작하는 미묘한 버그가 발생합니다.
| 시나리오 | CLOCK_MONOTONIC | CLOCK_BOOTTIME | 올바른 선택 |
|---|---|---|---|
| 30분 후 알람 (suspend 중에도 울려야 함) |
suspend 중 시간 정지 → 알람이 suspend 해제 후에야 울림 | suspend 시간 포함 → 정확히 30분 후 울림 (CLOCK_BOOTTIME_ALARM으로 웨이크업 가능) | BOOTTIME |
| 네트워크 RTT 측정 | 패킷 전송~수신 사이 순수 경과 시간 정확히 측정 | suspend 발생 시 RTT에 suspend 시간이 포함되어 왜곡 | MONOTONIC |
| 세션 타임아웃 (2시간) | suspend 1시간 후 resume → 아직 1시간 남음 (의도와 다를 수 있음) | suspend 1시간 포함 → 정확히 2시간 후 세션 만료 | BOOTTIME |
| 스케줄러 타임슬라이스 | CPU가 실제로 프로세스를 실행한 시간만 측정 | suspend 시간이 타임슬라이스에 포함되어 부적절 | MONOTONIC |
| 시스템 업타임 추적 | suspend 시간 제외 → 실제 가동 시간보다 짧게 표시 | 부팅 이후 전체 경과 시간 정확히 반영 | BOOTTIME |
/* CLOCK_BOOTTIME vs CLOCK_MONOTONIC 커널 내부 차이 */
/* MONOTONIC: ktime_get() — suspend 시간 미포함 */
ktime_t now_mono = ktime_get();
/* 내부: tk->tkr_mono.base + cycles_to_ns(read_clocksource())
* suspend 중에는 tkr_mono.base가 증가하지 않음 */
/* BOOTTIME: ktime_get_boottime() — suspend 시간 포함 */
ktime_t now_boot = ktime_get_boottime();
/* 내부: ktime_get() + tk->offs_boot
* offs_boot는 resume 시 suspend 경과 시간만큼 증가
* timekeeping_resume() → tk_update_sleep_time() */
/* 커널 드라이버에서 세션 타임아웃 구현 */
struct my_session {
ktime_t start_time;
ktime_t timeout;
};
void session_start(struct my_session *s)
{
/* BOOTTIME 사용: suspend 중에도 시간 경과를 반영 */
s->start_time = ktime_get_boottime();
s->timeout = ktime_add_ms(s->start_time, 7200000); /* 2시간 */
}
bool session_expired(struct my_session *s)
{
return ktime_after(ktime_get_boottime(), s->timeout);
}
/* hrtimer에서 BOOTTIME 클럭 사용 */
hrtimer_init(&my_timer, CLOCK_BOOTTIME, HRTIMER_MODE_REL);
/* → suspend 시간을 포함한 타이머, suspend 후에도 정확한 시점에 만료 */
/* timerfd에서 BOOTTIME 사용 (유저스페이스) */
int tfd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);
/* → suspend를 넘기는 타이머 FD */
코드 설명
CLOCK_BOOTTIME과 CLOCK_MONOTONIC의 커널 내부 구현 차이와 실전 사용 예제입니다.
- ktime_get() vs ktime_get_boottime()
ktime_get()는tkr_mono.base를 기준으로 시간을 반환하며 suspend 중에는 정지합니다.ktime_get_boottime()는ktime_get()에offs_boot(누적 suspend 시간)를 더하여 반환합니다.offs_boot는timekeeping_resume()에서 suspend 경과 시간만큼 갱신됩니다. - 세션 타임아웃 패턴세션 타임아웃은 "실제 시간이 경과했는지"를 판단하는 것이므로 BOOTTIME이 적합합니다. MONOTONIC을 사용하면 suspend 동안 세션이 연장되는 버그가 발생합니다.
- hrtimer에서 BOOTTIME
hrtimer_init()에CLOCK_BOOTTIME을 전달하면hrtimer_clock_base[HRTIMER_BASE_BOOTTIME]에 연결됩니다. suspend 이후에도 정확한 시점에 타이머가 만료됩니다.
CLOCK_BOOTTIME_ALARM: CLOCK_BOOTTIME_ALARM과 CLOCK_REALTIME_ALARM은 특별한 클럭 ID로, suspend 상태에서 타이머가 만료되면 시스템을 자동으로 웨이크업합니다. 일반 CLOCK_BOOTTIME은 suspend 시간을 포함하여 경과 시간을 계산하지만 시스템을 깨우지는 않습니다. Android의 AlarmManager가 CLOCK_BOOTTIME_ALARM을 사용하여 절전 중에도 알람을 울리는 대표적인 사례입니다. 커널 내부에서는 alarmtimer 서브시스템(kernel/time/alarmtimer.c)이 RTC 하드웨어를 프로그래밍하여 웨이크업을 구현합니다.
clocksource와 clockevent
clocksource는 시간 측정용 하드웨어 클럭 추상화이고, clockevent는 미래 시점에 인터럽트를 발생시키는 하드웨어 타이머 추상화입니다. 대표적인 clocksource로 TSC, HPET, ACPI PM Timer, ARM Arch Timer가 있으며, clockevent는 LAPIC Timer, HPET, ARM Arch Timer가 담당합니다.
struct clocksource 핵심 필드
struct clocksource(include/linux/clocksource.h)는 하드웨어 카운터를 나노초로 변환하는 모든 정보를 캡슐화합니다.
/* include/linux/clocksource.h */
struct clocksource {
u64 (*read)(struct clocksource *cs); /* HW 카운터 읽기 */
u64 mask; /* 카운터 비트마스크 (예: TSC=~0ULL, HPET=0xFFFFFFFF) */
u32 mult; /* cycle→ns 곱셈 인수 */
u32 shift; /* cycle→ns 비트 시프트 */
u32 max_idle_ns; /* idle 시 최대 허용 ns (overflow 방지) */
u32 maxadj; /* NTP 최대 주파수 조정 범위 */
int rating; /* 품질 등급 (높을수록 우선) */
unsigned long flags; /* CLOCK_SOURCE_IS_CONTINUOUS 등 */
const char *name;
struct list_head list; /* clocksource_list 연결 */
};
/* cycle → ns 변환: ns = (cycles * mult) >> shift
* mult/shift는 clocksource_register_hz()가 자동 계산 */
코드 설명
include/linux/clocksource.h에 정의된 시간 측정 프레임워크의 핵심 구조체입니다.
- read()하드웨어 카운터의 현재 cycle 값을 반환합니다. TSC:
rdtsc명령어, ARM:cntvct_el0레지스터, HPET: MMIO 레지스터 읽기. timekeeping이 매 tick마다 호출합니다. - mult / shift정수 연산으로 cycle→ns 변환하는 인수입니다.
clocks_calc_mult_shift()가 HW 주파수로부터 자동 계산하며, 부동소수점 없이 나노초 정밀도를 달성합니다. - mask카운터의 유효 비트 범위. cycle 차이 계산 시 overflow를 안전하게 처리합니다. 64비트 TSC는
~0ULL, 32비트 HPET은0xFFFFFFFF입니다. - rating클럭 품질 점수. 커널은 항상 가장 높은 rating의 clocksource를 자동 선택합니다.
| 등급 | rating 범위 | 설명 | 대표 클럭 |
|---|---|---|---|
| Perfect | 400+ | HW 보장 최고 정밀도 | TSC invariant (400) |
| Desired | 300~399 | 고품질, 대부분 적합 | ARM Arch Timer (350) |
| Good | 200~299 | 사용 가능, 더 나은 대안 존재 시 대체 | HPET (250), ACPI PM (200) |
| Base | 100~199 | 기본 폴백 소스 | PIT (110) |
| Undesirable | 1~99 | 최후 수단 | jiffies (1) |
struct clock_event_device 핵심 필드
/* include/linux/clockchips.h */
struct clock_event_device {
void (*event_handler)(struct clock_event_device *);
/* 핸들러: tick_handle_periodic 또는 hrtimer_interrupt */
int (*set_next_event)(unsigned long evt,
struct clock_event_device *);
/* delta cycle 단위로 다음 인터럽트 설정 */
int (*set_next_ktime)(ktime_t expires,
struct clock_event_device *);
/* 절대 ktime으로 설정 (옵션) */
unsigned int features; /* PERIODIC | ONESHOT | C3STOP 등 */
int rating; /* clocksource와 같은 스케일 */
u32 mult, shift; /* ns→cycle 변환 (clocksource 반대 방향) */
ktime_t min_delta_ticks; /* 프로그래밍 최소 간격 */
ktime_t max_delta_ticks; /* 프로그래밍 최대 간격 */
const char *name;
int irq;
enum clock_event_state state_use_accessors;
/* DETACHED, SHUTDOWN, PERIODIC, ONESHOT */
};
#define CLOCK_EVT_FEAT_PERIODIC 0x01 /* 주기적 모드 */
#define CLOCK_EVT_FEAT_ONESHOT 0x02 /* 원샷 모드 (hrtimer 필수) */
#define CLOCK_EVT_FEAT_KTIME 0x04 /* set_next_ktime() 지원 */
#define CLOCK_EVT_FEAT_C3STOP 0x08 /* C3+에서 정지 → broadcast 필요 */
코드 설명
include/linux/clockchips.h에 정의된 클럭 이벤트 디바이스 구조체입니다.
- event_handler현재 동작 모드의 핸들러입니다. 저해상도:
tick_handle_periodic(), 고해상도 전환 후:hrtimer_interrupt().tick_setup_sched_timer()에서 전환됩니다. - set_next_event()delta cycle 단위로 다음 인터럽트를 프로그래밍합니다. LAPIC: APIC_TMICT 레지스터 기록, ARM: CNTP_TVAL_EL0 설정. clockevent가 ns→cycle 변환 후 호출합니다.
- CLOCK_EVT_FEAT_ONESHOT이 플래그가 없으면 hrtimer 고해상도 모드를 사용할 수 없습니다. 대부분의 현대 하드웨어는 ONESHOT을 지원합니다.
- CLOCK_EVT_FEAT_C3STOP깊은 C-state에서 타이머가 멈추므로 tick broadcast 메커니즘이 필요합니다. 외부 타이머(HPET 등)가 대신 broadcast 인터럽트를 발생시킵니다.
clocksource 등록과 선택 흐름
/* kernel/time/clocksource.c — 등록 핵심 경로 */
int clocksource_register_hz(struct clocksource *cs, u32 hz)
{
return __clocksource_register_scale(cs, 1, hz);
}
static int __clocksource_register_scale(struct clocksource *cs,
u32 scale, u32 freq)
{
clocks_calc_mult_shift(&cs->mult, &cs->shift,
freq, NSEC_PER_SEC / scale, 3600);
cs->max_idle_ns = clocksource_max_deferment(cs);
clocksource_enqueue(cs); /* rating순 정렬 리스트에 삽입 */
clocksource_enqueue_watchdog(cs); /* watchdog 검증 등록 */
clocksource_select(); /* 최적 clocksource 선택 */
}
/* 최적 선택: clocksource_list는 rating 내림차순
* 리스트 헤드 = 최고 rating → timekeeping_notify() → tk_core 교체 */
/* clockevent 등록 */
void clockevents_register_device(struct clock_event_device *dev)
{
list_add(&dev->list, &clockevent_devices);
tick_check_new_device(dev);
/* → tick_setup_device() → ONESHOT 지원 시 hrtimer 전환 */
}
# 현재 clocksource/clockevent 확인
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# tsc
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
# tsc hpet acpi_pm
# clocksource 수동 변경 (디버깅용)
echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource
# 초기화 로그 확인
dmesg | grep -iE "clocksource|clockevent|tsc|hpet"
clocksource/clockevent 프레임워크 상세 — watchdog, fallback 전략, 하드웨어 클럭(TSC/HPET/ACPI PM)별 비교는 ktime / Clock — Clocksource 프레임워크에서 자세히 다룹니다.
지연 함수 (Delay Functions)
커널은 컨텍스트에 따라 다양한 지연 함수를 제공합니다. 인터럽트 컨텍스트에서는 busy-wait(udelay(), ndelay())만 사용 가능하고, 프로세스 컨텍스트에서는 슬립 기반(usleep_range(), msleep())을 권장합니다. 잘못된 선택은 CPU 낭비(busy-wait 과용), 스케줄링 미작동(인터럽트 컨텍스트에서 sleep), 우선순위 역전 등의 문제를 유발합니다.
지연 함수 비교표
| 함수 | 범위 | 내부 메커니즘 | 컨텍스트 | 정밀도 | 방식 |
|---|---|---|---|---|---|
ndelay(ns) |
나노초 단위 | __const_udelay() → loops_per_jiffy 기반 루프 |
인터럽트/프로세스 모두 | 낮음 (수십 ns 오차) | Busy-wait |
udelay(us) |
마이크로초 단위, 최대 ~2000 µs 권장 | __const_udelay() / TSC 기반 루프 |
인터럽트/프로세스 모두 | 낮음~중간 | Busy-wait |
mdelay(ms) |
밀리초 단위 | udelay() 반복 호출 |
인터럽트/프로세스 모두 | 낮음 | Busy-wait |
usleep_range(min, max) |
10 µs ~ 20 ms | do_usleep_range() → hrtimer_nanosleep() |
프로세스 컨텍스트만 | 높음 (hrtimer) | Sleep |
msleep(ms) |
1 ms 이상 | schedule_timeout_uninterruptible() → timer_list |
프로세스 컨텍스트만 | 중간 (jiffy 단위) | Sleep (인터럽트 불가) |
msleep_interruptible(ms) |
1 ms 이상 | schedule_timeout_interruptible() → timer_list |
프로세스 컨텍스트만 | 중간 (jiffy 단위) | Sleep (시그널 인터럽트 가능) |
ssleep(s) |
초 단위 | msleep(ms * 1000) 래퍼 |
프로세스 컨텍스트만 | 낮음 (jiffy 단위) | Sleep (인터럽트 불가) |
fsleep(us) |
나노초~초 전 범위 | 범위에 따라 udelay / usleep_range / msleep 자동 선택 | 범위에 따라 다름 | 범위에 따라 다름 | 통합 API |
udelay() 내부 구현
udelay()는 컴파일 타임 상수 여부에 따라 경로가 달라집니다. 상수 인자일 때는 __const_udelay()를 통해 루프 횟수를 컴파일 타임에 결정하고, 동적 값일 때는 __udelay()를 호출합니다. 두 경로 모두 내부적으로 loops_per_jiffy를 기준으로 보정된 루프를 실행하며, TSC(Time Stamp Counter)를 지원하는 아키텍처에서는 더 정밀한 TSC 기반 구현으로 대체될 수 있습니다.
/* include/linux/delay.h (요약) */
#define udelay(n) \
(__builtin_constant_p(n) \
? ((n) > 20000 ? __bad_udelay() \
: __const_udelay((n) * 0x10c7ul)) \
: __udelay(n))
/* arch/x86/lib/delay.c (요약) */
void __const_udelay(unsigned long xloops)
{
unsigned long lpj = this_cpu_read(cpu_info.loops_per_jiffy);
__delay(xloops * lpj >> 32); /* 보정된 루프 실행 */
}
중요 제약: udelay()에 2000 µs를 초과하는 값을 전달하면 오버플로가 발생할 수 있습니다. 긴 busy-wait이 필요한 경우에는 mdelay()를 사용해야 합니다.
usleep_range() 내부 경로
usleep_range(min, max)는 프로세스 컨텍스트 전용 슬립 함수입니다. min/max 범위를 지정하는 이유는 hrtimer 만료 시점의 타이밍을 커널이 유연하게 조절하여 타이머 뭉침(timer coalescing)을 줄이고 에너지 효율을 높이기 위해서입니다.
/* kernel/time/sleep_timeout.c (요약) */
void usleep_range(unsigned long min, unsigned long max)
{
do_usleep_range(min, max, TASK_UNINTERRUPTIBLE);
}
static void do_usleep_range(u64 min, u64 max, unsigned int state)
{
ktime_t kmin = ns_to_ktime(min * NSEC_PER_USEC);
ktime_t kmax = ns_to_ktime(max * NSEC_PER_USEC);
hrtimer_nanosleep(kmin, kmax, state, CLOCK_MONOTONIC);
}
msleep() 내부 경로
msleep()은 jiffy 단위 정밀도로 동작하는 저수준 슬립입니다. 내부적으로 schedule_timeout_uninterruptible()을 호출하여 태스크를 TASK_UNINTERRUPTIBLE 상태로 전환하고 timer_list 기반 타이머를 설정합니다. 시그널로 깨어날 필요가 있다면 msleep_interruptible()을 사용해야 합니다.
/* kernel/time/sleep_timeout.c (요약) */
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}
fsleep() 통합 API
fsleep()은 리눅스 5.10에 도입된 통합 지연 API입니다. 지연 시간 범위에 따라 최적의 내부 함수를 자동으로 선택하므로, 호출자가 컨텍스트와 범위를 직접 판단할 필요 없이 단일 인터페이스를 사용할 수 있습니다.
/* include/linux/delay.h */
static inline void fsleep(unsigned long usecs)
{
if (usecs <= 10) /* < 10 µs: busy-wait */
udelay(usecs);
else if (usecs <= 20000) /* 10 µs ~ 20 ms: hrtimer sleep */
usleep_range(usecs, usecs * 2);
else /* > 20 ms: jiffy-based sleep */
msleep(DIV_ROUND_UP(usecs, 1000));
}
지연 함수 선택 결정 트리
컨텍스트별 사용 예
#include <linux/delay.h>
/* 인터럽트 핸들러: busy-wait만 허용 */
static irqreturn_t my_irq_handler(int irq, void *dev)
{
ndelay(500); /* 500 ns 대기 (레지스터 안정화) */
udelay(10); /* 10 µs busy-wait */
return IRQ_HANDLED;
}
/* 커널 스레드: sleep 가능 */
static int my_kthread(void *data)
{
while (!kthread_should_stop()) {
/* 정밀한 짧은 대기: hrtimer 기반 */
usleep_range(1000, 1100); /* 1~1.1 ms */
/* 긴 대기: jiffy 기반, 인터럽트 불가 */
msleep(100); /* 100 ms */
/* 통합 API: 범위 자동 선택 */
fsleep(5000); /* 5000 µs → usleep_range 경로 */
}
return 0;
}
/* 드라이버 probe: 시그널 인터럽트 가능한 슬립 */
static int my_probe(struct platform_device *pdev)
{
unsigned long remaining;
remaining = msleep_interruptible(200);
if (remaining) {
/* 시그널로 깨어남: 남은 시간 = remaining ms */
return -EINTR;
}
return 0;
}
각 지연 함수의 내부 구현, 컨텍스트별 선택 가이드, fsleep() 통합 API, 정밀도 비교표는 ktime / Clock — 지연 함수에서 상세히 다룹니다.
Tickless 커널 (NO_HZ)
전통적인 커널은 매 tick(1/HZ초)마다 타이머 인터럽트를 발생시켰지만, tickless 커널은 불필요한 tick을 제거하여 전력 소모를 줄입니다.
Tickless 동작 원리
Tickless의 핵심 원리는 "다음에 해야 할 일이 없으면 깨우지 않습니다"입니다:
- CPU가 idle에 진입하기 전, 다음으로 만료될 타이머(Timer Wheel + hrtimer)의 시간을 확인합니다.
- 그 시간까지 주기적 틱 인터럽트를 중단하고, 대신 하나의 oneshot clockevent만 해당 시점에 프로그래밍합니다.
- CPU는 깊은 C-state(저전력 상태)에 진입하여 전력을 절약합니다.
- 타이머 만료 시점에 clockevent 인터럽트가 CPU를 깨우고, 밀린 jiffies를 한꺼번에 보정합니다.
| 모드 | CONFIG 옵션 | 틱 중단 조건 | 전력 절감 | 지연 시간 | 주요 사용 사례 |
|---|---|---|---|---|---|
| NO_HZ_OFF | CONFIG_HZ_PERIODIC |
틱 항상 활성 | 없음 | 예측 가능 | 레거시 시스템, 디버깅 |
| NO_HZ_IDLE | CONFIG_NO_HZ_IDLE |
CPU idle 시 | 중간~높음 | 낮음 | 일반 서버/데스크톱 (기본 설정) |
| NO_HZ_FULL | CONFIG_NO_HZ_FULL |
단일 태스크(Task) 실행 시 | 매우 높음 | 매우 낮음 (지터 최소) | HPC, 실시간(Real-time), 저지연 워크로드 |
nohz_full= 커널 부트 파라미터로 tick 중단할 CPU 지정 필요 (예: nohz_full=1-7). CPU 0은 일반적으로 housekeeping으로 유지됩니다. RCU 콜백 오프로딩(rcu_nocbs=)도 함께 설정해야 완전한 tick 중단이 가능합니다.
tick_nohz_idle_stop_tick() 구현 분석
Tickless 커널의 핵심인 tick_nohz_idle_stop_tick()은 CPU가 idle에 진입할 때 호출되어, 주기 tick을 중단하고 다음 타이머 만료 시점에만 인터럽트를 프로그래밍합니다.
/* kernel/time/tick-sched.c — tick_nohz_idle_stop_tick() (Linux 6.x, 간략화) */
static void tick_nohz_idle_stop_tick(void)
{
struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
ktime_t expires;
int cpu = smp_processor_id();
/* 1. 다음 만료 타이머 시각 조회 (Timer Wheel + hrtimer 통합) */
expires = tick_nohz_next_event(ts, cpu);
/* tick_nohz_next_event() 내부:
* - timer_base->next_expiry (Timer Wheel 최소 만료)
* - hrtimer_get_next_event() (hrtimer RB-tree leftmost)
* - 두 값 중 더 이른 쪽을 반환 */
/* 2. 다음 이벤트까지 시간이 충분하지 않으면 tick 유지 */
if (ktime_before(expires,
ktime_add_ns(ktime_get(), TICK_NSEC))) {
/* 1 tick 미만 남음 → 주기 tick 유지가 더 효율적 */
return;
}
/* 3. 주기 tick 중단 — oneshot 모드 전환 */
ts->tick_stopped = 1;
ts->idle_sleeptime_seq++;
/* 4. clockevent를 다음 만료 시각으로 프로그래밍 */
tick_program_event(expires, 1);
/* → set_next_event()로 HW 타이머에 만료 시각 설정
* expires가 KTIME_MAX면 clockevent 정지 (완전 무tick) */
/* 5. 통계 갱신 */
ts->idle_expires = expires;
ts->idle_entrytime = ktime_get();
}
/* idle 복귀 시 — tick_nohz_idle_restart_tick() */
static void tick_nohz_idle_restart_tick(struct tick_sched *ts,
ktime_t now)
{
/* jiffies를 idle 기간만큼 보정 */
tick_nohz_update_jiffies(now);
/* 주기 tick 복원 */
ts->tick_stopped = 0;
hrtimer_cancel(&ts->sched_timer);
tick_setup_sched_timer(false);
/* 스케줄러 tick hrtimer 재설정 → 주기적 tick 복원 */
}
코드 설명
kernel/time/tick-sched.c의 tickless 핵심 구현입니다. idle 진입 경로(cpuidle_idle_call())에서 호출됩니다.
- tick_nohz_next_event()Timer Wheel(
timer_base->next_expiry)과 hrtimer(hrtimer_get_next_event())의 다음 만료 시각을 모두 조회하여 더 이른 쪽을 반환합니다. 만료될 타이머가 전혀 없으면KTIME_MAX를 반환하여 clockevent를 완전히 정지시킵니다. - TICK_NSEC 비교다음 이벤트까지 1 tick(HZ=250이면 4ms) 미만이면 tick을 중단하지 않습니다. tick 중단/복원의 오버헤드가 절약 효과보다 클 수 있기 때문입니다. 이 임계값은 전력/지연 트레이드오프의 핵심입니다.
- tick_program_event(expires, 1)두 번째 인자
1은 force 플래그입니다. clockevent를 oneshot 모드로 전환하고, 정확히expires시각에 인터럽트가 발생하도록 프로그래밍합니다. 이 인터럽트가 CPU를 깨워 타이머 콜백을 실행합니다. - tick_nohz_update_jiffies()idle에서 깨어난 후, 건너뛴 기간만큼 jiffies를 한꺼번에 보정합니다. 예를 들어 100ms 동안 idle이었으면 jiffies를 25(HZ=250 기준) 증가시킵니다.
- tick_setup_sched_timer()idle 복귀 시 스케줄러의 주기적 tick hrtimer를 다시 설정합니다. 이 hrtimer가
hrtimer_interrupt()를 통해 매 tick마다scheduler_tick()을 호출하게 됩니다.
NO_HZ idle 진입/탈출 시 타이머 재프로그래밍 흐름
NO_HZ idle 진입과 탈출 과정에서 clockevent 하드웨어의 재프로그래밍이 어떤 순서로 이루어지는지 전체 흐름을 분석합니다. 이 과정은 타이머 정확도와 전력 효율의 핵심 경로입니다.
idle 진입/탈출의 오버헤드: tick 중단과 복원에는 clockevent 하드웨어 프로그래밍, jiffies 보정, sched_timer 재설정 등의 비용이 발생합니다. 일반적으로 수 마이크로초 수준이지만, 매우 짧은 idle(수십 µs)에서는 이 오버헤드가 절약 효과를 상쇄할 수 있습니다. 이 때문에 tick_nohz_idle_stop_tick()은 다음 이벤트까지 1 tick 미만이면 tick 중단을 건너뛰는 최적화를 적용합니다. cpuidle 거버너(Governor)도 예상 idle 시간이 짧으면 얕은 C-state를 선택하여 탈출 지연을 최소화합니다.
ktime API
ktime_t는 나노초 단위의 시간을 표현하는 64비트 통일 타입입니다. ktime_get()(monotonic), ktime_get_real()(wall clock), ktime_get_boottime()(suspend 포함) 등 다양한 시간 읽기 함수와 산술 연산 매크로(Macro)를 제공합니다.
ktime_t 내부 표현
ktime_t는 단순한 64비트 부호 있는 정수(signed 64-bit integer)로, 각 클럭 기준에 따라 에포크(epoch)로부터의 나노초 값을 저장합니다.
/* include/linux/ktime.h */
typedef s64 ktime_t;
/* 클럭 기준별 에포크 */
/* CLOCK_MONOTONIC : 시스템 부팅 이후 경과 나노초 (suspend 제외) */
/* CLOCK_REALTIME : 1970-01-01 00:00:00 UTC 이후 경과 나노초 */
/* CLOCK_BOOTTIME : 시스템 부팅 이후 경과 나노초 (suspend 포함) */
/* CLOCK_TAI : TAI 기준 나노초 (윤초 보정 없음) */
/* 유효 범위: ±292년 (s64 나노초) */
KTIME_MAX = 9223372036854775807LL /* s64 최대값 */
KTIME_MIN = -9223372036854775808LL /* s64 최소값 */
KTIME_SEC_MAX = (KTIME_MAX / NSEC_PER_SEC)
시간 읽기 함수 내부 경로
각 ktime_get_*() 함수는 공통적으로 하드웨어 클럭소스(clocksource)를 읽은 뒤 mult/shift 변환을 통해 나노초로 환산하고, 클럭 종류에 따라 오프셋(offset)을 더합니다.
/* kernel/time/timekeeping.c — 단순화된 내부 경로 */
/* 1단계: 하드웨어 카운터 읽기 + mult/shift 변환 */
static u64 timekeeping_get_ns(const struct tk_read_base *tkr)
{
u64 delta = timekeeping_get_delta(tkr); /* clocksource.read() */
return timekeeping_delta_to_ns(tkr, delta); /* (delta * mult) >> shift */
}
/* ktime_get(): CLOCK_MONOTONIC (오프셋 없음) */
ktime_t ktime_get(void)
{
return ktime_add_ns(tk->ktime_sec,
timekeeping_get_ns(&tk->tkr_mono));
}
/* ktime_get_real(): CLOCK_REALTIME = monotonic + wall_to_monotonic */
ktime_t ktime_get_real(void)
{
return ktime_add(ktime_get(), tk->offs_real);
}
/* ktime_get_boottime(): CLOCK_BOOTTIME = monotonic + suspend 누적 */
ktime_t ktime_get_boottime(void)
{
return ktime_add(ktime_get(), tk->offs_boot);
}
/* ktime_get_raw(): NTP/adjtime 보정 없이 raw 하드웨어 값 반환 */
ktime_t ktime_get_raw(void)
{
return ktime_add_ns(tk->ktime_sec_raw,
timekeeping_get_ns(&tk->tkr_raw));
}
변환 함수 및 매크로
커널은 ktime_t와 다른 시간 단위 간 변환을 위한 인라인(inline) 함수를 제공합니다.
| 함수 / 매크로 | 방향 | 설명 |
|---|---|---|
ktime_to_ns(kt) | ktime_t → s64 | 나노초(ns) 값으로 변환 |
ktime_to_us(kt) | ktime_t → s64 | 마이크로초(µs)로 변환 (ns / 1,000) |
ktime_to_ms(kt) | ktime_t → s64 | 밀리초(ms)로 변환 (ns / 1,000,000) |
ktime_to_timespec64(kt) | ktime_t → timespec64 | 초/나노초 구조체로 변환 |
ktime_to_timeval(kt) | ktime_t → timeval | 초/마이크로초 구조체로 변환 (레거시) |
ns_to_ktime(ns) | s64 → ktime_t | 나노초 정수를 ktime_t로 변환 |
ms_to_ktime(ms) | s64 → ktime_t | 밀리초를 ktime_t로 변환 |
ktime_set(sec, ns) | (s64, long) → ktime_t | 초 + 나노초로 ktime_t 생성 |
timespec64_to_ktime(ts) | timespec64 → ktime_t | timespec64 구조체를 ktime_t로 변환 |
산술 및 비교 연산
ktime_t는 s64 별칭이므로 단순 덧셈·뺄셈도 동작하지만, 커널은 가독성과 오버플로 방지를 위해 전용 헬퍼(helper) 함수를 제공합니다.
/* 산술 연산 */
ktime_t ktime_add(ktime_t kt1, ktime_t kt2); /* kt1 + kt2 */
ktime_t ktime_sub(ktime_t lhs, ktime_t rhs); /* lhs - rhs */
ktime_t ktime_add_ns(ktime_t kt, u64 ns); /* kt + ns */
ktime_t ktime_sub_ns(ktime_t kt, u64 ns); /* kt - ns */
ktime_t ktime_add_us(ktime_t kt, u64 us); /* kt + us*1,000 */
ktime_t ktime_add_ms(ktime_t kt, u64 ms); /* kt + ms*1,000,000 */
ktime_t ktime_add_safe(ktime_t lhs, ktime_t rhs); /* 오버플로 시 KTIME_MAX 반환 */
/* 비교 연산 */
bool ktime_before(ktime_t cmp1, ktime_t cmp2); /* cmp1 < cmp2 이면 true */
bool ktime_after(ktime_t cmp1, ktime_t cmp2); /* cmp1 > cmp2 이면 true */
int ktime_compare(ktime_t cmp1, ktime_t cmp2); /* -1 / 0 / 1 반환 */
bool ktime_equal(ktime_t cmp1, ktime_t cmp2); /* cmp1 == cmp2 이면 true */
경과 시간 측정 패턴
커널 코드에서 특정 구간의 수행 시간을 나노초 단위로 측정할 때 가장 많이 쓰이는 패턴입니다.
#include <linux/ktime.h>
#include <linux/timekeeping.h>
static void measure_elapsed_example(void)
{
ktime_t start, end;
s64 elapsed_ns, elapsed_us, elapsed_ms;
/* 시작 시각 캡처 (CLOCK_MONOTONIC 기반) */
start = ktime_get();
/* 측정 대상 작업 */
do_some_work();
/* 종료 시각 캡처 */
end = ktime_get();
/* 경과 시간 계산 */
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
elapsed_us = ktime_to_us(ktime_sub(end, start));
elapsed_ms = ktime_to_ms(ktime_sub(end, start));
pr_info("elapsed: %lld ns / %lld us / %lld ms\n",
elapsed_ns, elapsed_us, elapsed_ms);
}
/* 더 간결한 패턴: ktime_get_ns() 사용 */
static void measure_ns_example(void)
{
u64 start_ns = ktime_get_ns(); /* ktime_to_ns(ktime_get()) 래퍼 */
do_some_work();
pr_info("elapsed: %llu ns\n", ktime_get_ns() - start_ns);
}
/* 벽시계 기준 측정이 필요한 경우 */
static void measure_realtime_example(void)
{
ktime_t t1 = ktime_get_real();
do_some_work();
pr_info("wall elapsed: %lld ns\n",
ktime_to_ns(ktime_sub(ktime_get_real(), t1)));
}
ktime_t 전체 함수 레퍼런스, 변환 매크로, ns/us/ms 변환 헬퍼, 경과 시간 측정 패턴은 ktime / Clock — ktime_t 함수 레퍼런스에서 상세히 다룹니다.
POSIX 타이머 (유저스페이스)
커널은 유저스페이스 POSIX 타이머를 hrtimer 기반으로 구현합니다. timer_create() 시스템 콜로 생성한 타이머는 커널 내부에서 struct k_itimer로 관리되며, 만료 시 시그널(Signal)을 통해 사용자 프로세스에 통지합니다.
POSIX 클럭 ID
/* 주요 클럭 ID (include/uapi/linux/time.h) */
CLOCK_REALTIME /* 벽시계 (NTP 조정 가능, 점프 가능) */
CLOCK_MONOTONIC /* 단조 증가 (NTP 주파수만 조정) */
CLOCK_BOOTTIME /* MONOTONIC + suspend 시간 포함 */
CLOCK_PROCESS_CPUTIME_ID /* 프로세스 CPU 시간 */
CLOCK_THREAD_CPUTIME_ID /* 스레드 CPU 시간 */
CLOCK_REALTIME_ALARM /* REALTIME + suspend 시 웨이크업 */
CLOCK_BOOTTIME_ALARM /* BOOTTIME + suspend 시 웨이크업 */
CLOCK_TAI /* 국제원자시 (윤초 없음) */
struct k_itimer와 커널 내부 경로
유저스페이스의 timer_create() 호출은 커널에서 struct k_itimer를 할당하고, 내부적으로 hrtimer를 초기화하여 연결합니다.
/* kernel/time/posix-timers.c — 커널 내부 구조 */
struct k_itimer {
struct list_head list; /* 프로세스의 타이머 리스트 */
timer_t it_id; /* 유저스페이스 timer_t ID */
int it_clock; /* CLOCK_REALTIME 등 */
int it_sigev_notify; /* SIGEV_SIGNAL, SIGEV_THREAD_ID 등 */
struct signal_struct *it_signal; /* 시그널 전달 대상 프로세스 */
union {
struct {
struct hrtimer timer; /* 핵심: 내장된 hrtimer */
ktime_t interval; /* 주기적 타이머 간격 */
} real;
/* CPU 타이머용 union 멤버도 존재 */
} it;
int it_overrun; /* 누적 overrun 횟수 */
int it_overrun_last; /* 이전 overrun 값 */
};
/* timer_create() syscall → do_timer_create() 핵심 흐름 */
static int do_timer_create(clockid_t which_clock,
struct sigevent *event, timer_t *id)
{
struct k_itimer *new_timer = alloc_posix_timer();
new_timer->it_clock = which_clock;
new_timer->it_sigev_notify = event->sigev_notify;
/* hrtimer 초기화 — clockid에 맞는 클럭 베이스에 연결 */
hrtimer_init(&new_timer->it.real.timer, which_clock, HRTIMER_MODE_ABS);
new_timer->it.real.timer.function = posix_timer_fn;
/* ... ID 할당, 프로세스 타이머 리스트에 추가 */
}
/* posix_timer_fn() — hrtimer 만료 콜백 */
static enum hrtimer_restart posix_timer_fn(struct hrtimer *timer)
{
struct k_itimer *timr = container_of(timer,
struct k_itimer, it.real.timer);
/* 시그널 전달 (SIGEV_SIGNAL: kill_pid_info 등) */
posix_timer_event(timr);
/* 주기적 타이머: interval이 0이 아니면 재시작 */
if (timr->it.real.interval) {
timr->it_overrun += hrtimer_forward_now(timer,
timr->it.real.interval);
return HRTIMER_RESTART;
}
return HRTIMER_NORESTART;
}
코드 설명
kernel/time/posix-timers.c에 정의된 POSIX 타이머의 커널 내부 구현입니다.
- k_itimer유저스페이스 POSIX 타이머의 커널 표현입니다.
it.real.timer에 hrtimer가 내장되어 있으며, hrtimer 프레임워크를 통해 만료 처리됩니다. - do_timer_create()
timer_create()시스템 콜의 커널 진입점입니다. k_itimer를 할당하고 hrtimer를 초기화합니다. clockid에 따라 적절한 hrtimer 클럭 베이스에 연결됩니다. - posix_timer_fn()hrtimer 만료 시 호출되는 콜백입니다.
posix_timer_event()로 대상 프로세스에 시그널을 전달합니다. 주기적 타이머는hrtimer_forward_now()로 다음 만료를 설정하고 overrun 카운트를 누적합니다. - it_overrun시그널이 아직 pending 상태에서 타이머가 다시 만료된 횟수입니다.
timer_getoverrun()시스템 콜로 확인할 수 있으며, 실시간 시스템에서 타이머 누락을 감지하는 데 사용됩니다.
| 통지 방법 | sigev_notify 값 | 동작 | 사용 사례 |
|---|---|---|---|
| SIGEV_SIGNAL | SIGEV_SIGNAL | 지정 시그널(기본 SIGALRM)을 프로세스에 전달 | 전통적 타이머, 간단한 주기 작업 |
| SIGEV_THREAD_ID | SIGEV_THREAD_ID | 특정 스레드에 시그널 전달 | 멀티스레드 서버의 특정 워커에 통지 |
| SIGEV_NONE | SIGEV_NONE | 시그널 없음, timer_gettime()으로 폴링 | 오버헤드 최소화, 능동적 상태 확인 |
| SIGEV_THREAD | SIGEV_THREAD | glibc가 내부 스레드를 생성하여 콜백 실행 | 시그널 핸들러 대신 일반 함수 호출 원할 때 |
timerfd — 파일 디스크립터 기반 타이머
timerfd는 시그널 대신 파일 디스크립터(FD)를 통해 타이머 만료를 통지하는 현대적 API입니다. epoll()/select()/poll()로 다른 I/O 이벤트와 함께 대기할 수 있어 이벤트 루프(Event Loop) 기반 아키텍처에 적합합니다.
/* 유저스페이스 timerfd 사용 예제 */
#include <sys/timerfd.h>
#include <sys/epoll.h>
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
struct itimerspec its = {
.it_value = { .tv_sec = 1, .tv_nsec = 0 }, /* 1초 후 첫 만료 */
.it_interval = { .tv_sec = 0, .tv_nsec = 500000000 } /* 500ms 주기 */
};
timerfd_settime(tfd, 0, &its, NULL);
/* epoll에 등록하여 다른 FD와 함께 대기 */
struct epoll_event ev = { .events = EPOLLIN, .data.fd = tfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
/* 만료 시 read()로 overrun 카운트 읽기 */
uint64_t expirations;
read(tfd, &expirations, sizeof(expirations));
/* expirations = 1 (정상), > 1이면 overrun 발생 */
/* fs/timerfd.c — timerfd 커널측 핵심 구현 (Linux 6.x, 간략화) */
struct timerfd_ctx {
union {
struct hrtimer tmr; /* 내장 hrtimer */
struct alarm alarm; /* CLOCK_REALTIME_ALARM용 */
} t;
ktime_t tintv; /* 주기 간격 (0이면 one-shot) */
ktime_t moffs; /* REALTIME 오프셋 보정 */
wait_queue_head_t wqh; /* epoll/read 대기 큐 */
u64 ticks; /* 누적 만료 횟수 (overrun) */
int clockid; /* CLOCK_MONOTONIC 등 */
unsigned settime_flags; /* TFD_TIMER_ABSTIME 등 */
};
/* hrtimer 만료 콜백 — timerfd의 핵심 */
static enum hrtimer_restart timerfd_tmrproc(struct hrtimer *hrtimer)
{
struct timerfd_ctx *ctx = container_of(hrtimer,
struct timerfd_ctx, t.tmr);
ctx->ticks++; /* overrun 카운트 증가 */
/* wait queue에서 대기 중인 epoll/read를 깨움 */
wake_up_locked_poll(&ctx->wqh, EPOLLIN);
/* 주기적 타이머면 자동 재시작 */
if (ctx->tintv)
return HRTIMER_RESTART;
return HRTIMER_NORESTART;
}
/* read() 시스템 콜 → timerfd_read() */
static ssize_t timerfd_read(struct file *file,
char __user *buf,
size_t count, loff_t *ppos)
{
struct timerfd_ctx *ctx = file->private_data;
u64 ticks;
/* 만료될 때까지 대기 (nonblock이면 -EAGAIN) */
if (!ctx->ticks) {
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(ctx->wqh, ctx->ticks);
}
/* overrun 카운트 읽고 리셋 */
ticks = ctx->ticks;
ctx->ticks = 0;
/* 주기적: 다음 만료 시각 forward */
if (ctx->tintv)
hrtimer_forward_now(&ctx->t.tmr, ctx->tintv);
/* 8바이트 uint64_t로 overrun 카운트 반환 */
return put_user(ticks, (u64 __user *)buf) ? -EFAULT :
sizeof(u64);
}
코드 설명
fs/timerfd.c의 timerfd 커널측 핵심 구현입니다. hrtimer와 wait queue를 결합하여 파일 디스크립터 기반 타이머 통지를 구현합니다.
- timerfd_ctxtimerfd의 커널측 상태입니다.
anon_inode_getfd()로 생성된 익명 파일의private_data에 저장됩니다. 내부에 hrtimer가 내장되어 있으며,ticks필드가read()시 반환할 overrun 카운트입니다. - timerfd_tmrproc()hrtimer 만료 시 호출되는 콜백입니다.
ticks를 증가시키고wake_up_locked_poll()로 wait queue에서 대기 중인epoll_wait()/read()를 깨웁니다.EPOLLIN이벤트를 전달하여 timerfd가 읽기 가능해졌음을 알립니다. - ticks와 overrun
read()호출 사이에 타이머가 여러 번 만료되면ticks가 누적됩니다.read()는 이 값을 8바이트uint64_t로 반환하고 0으로 리셋합니다. 반환값이 1보다 크면 주기를 따라가지 못하고 있는 의미입니다. - hrtimer_forward_now()주기적 timerfd에서
read()시 다음 만료 시각을 재설정합니다. 이는 POSIX 타이머의posix_timer_fn()에서hrtimer_forward()를 호출하는 것과 같은 메커니즘입니다. - epoll 통합timerfd는 일반 파일 디스크립터이므로
epoll_ctl()로 감시 대상에 추가할 수 있습니다. 네트워크 소켓, 시그널fd, 이벤트fd와 함께 하나의 이벤트 루프에서 통합 처리가 가능합니다. 이것이 timerfd의 핵심 장점입니다.
POSIX timer vs timerfd 선택: 이벤트 루프 아키텍처(epoll 기반)에서는 timerfd가 시그널 안전성 문제를 피할 수 있어 권장됩니다. 시그널 핸들러의 비동기 안전(async-signal-safe) 제약이 없으므로 콜백에서 임의의 코드를 실행할 수 있습니다. 반면, 전통적인 시그널 기반 아키텍처에서는 POSIX timer가 여전히 유효합니다.
타이머 마이그레이션과 그룹핑
전력 효율을 위해 커널은 여러 타이머를 같은 시점에 만료되도록 그룹핑합니다:
/* 타이머를 "느슨하게" 설정하면 커널이 그룹핑 가능 */
mod_timer(&timer, jiffies + msecs_to_jiffies(1000));
/* timer_setup_on_stack: 스택 기반 타이머 (짧은 수명) */
/* sysctl 파라미터 */
/* /proc/sys/kernel/timer_migration = 1 */
/* idle CPU의 타이머를 busy CPU로 마이그레이션 */
/* → idle CPU가 더 오래 슬립 가능 (전력 절약) */
10ms 이상의 지연에는 msleep(), 10us~10ms에는 usleep_range(), 10us 미만에는 udelay()를 사용하세요. mdelay()는 CPU를 오래 점유하므로 가급적 사용하지 마세요.
매 틱(Tick) 처리 체인
타이머 인터럽트(tick)가 발생하면 커널은 tick_handle_periodic() 또는 hrtimer_interrupt()를 통해 여러 서브시스템에 시간 경과를 알립니다. 이 과정에서 jiffies 갱신, 프로세스 CPU 시간 계정, 스케줄러 시간 슬라이스 검사, 저해상도 타이머 처리 등이 순차적으로 수행됩니다.
/* kernel/time/tick-common.c — periodic 모드 핸들러 */
void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id();
ktime_t next = dev->next_event;
tick_periodic(cpu);
/* oneshot 모드: 다음 tick을 직접 프로그래밍 */
if (dev->state_use_accessors == CLOCK_EVT_STATE_ONESHOT) {
next = ktime_add_ns(next, TICK_NSEC);
clockevents_program_event(dev, next, 0);
}
}
/* tick_periodic() 내부 */
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
raw_spin_lock(&jiffies_lock);
do_timer(1); /* jiffies_64++, timekeeping_advance() */
raw_spin_unlock(&jiffies_lock);
update_wall_time(); /* tk_core 벽시계 갱신 */
}
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
/* update_process_times() — 매 tick의 핵심 */
void update_process_times(int user_tick)
{
struct task_struct *p = current;
/* 1. CPU 시간 계정: user/system/irq/softirq 시간 누적 */
account_process_tick(p, user_tick);
/* 2. 저해상도 타이머 + hrtimer softirq 트리거 */
run_local_timers();
/* 3. RCU grace period 추적 */
rcu_sched_clock_irq(user_tick);
/* 4. 스케줄러 틱: vruntime 갱신, 선점 검사 */
scheduler_tick();
/* 5. perf 이벤트 업데이트 */
if (IS_ENABLED(CONFIG_PERF_EVENTS))
perf_event_task_tick();
}
코드 설명
매 타이머 인터럽트(tick)마다 실행되는 처리 체인입니다. 타이머 서브시스템의 핵심 구동 루프에 해당합니다.
- tick_handle_periodic()clockevent의
event_handler로 설정된 저해상도 모드 핸들러입니다.tick_periodic()을 호출한 후, oneshot 모드에서는 다음 tick 시점을 직접 clockevent에 프로그래밍합니다. - do_timer()전역
jiffies_64를 증가시키고,timekeeping_advance()를 호출하여 clocksource 카운터를 읽어 나노초 시간을 갱신합니다. 이 작업은tick_do_timer_cpu로 지정된 하나의 CPU에서만 수행됩니다. - account_process_tick()현재 프로세스의 CPU 사용 시간을 user/system/irq/softirq 카테고리로 분류하여 누적합니다.
/proc/stat과top명령이 보여주는 CPU 사용률의 원천 데이터입니다. - run_local_timers()만료된 타이머가 있으면
raise_softirq(TIMER_SOFTIRQ)를 호출합니다. 또한 저해상도 모드에서는hrtimer_run_queues()로 hrtimer도 처리합니다. - scheduler_tick()CFS 스케줄러의
vruntime을 갱신하고, 현재 태스크의 시간 슬라이스 소진 여부를 검사합니다. 소진 시resched_curr()로 선점 플래그를 설정합니다. 주기적 부하 균형(load balancing) 트리거도 여기서 수행됩니다.
tick_do_timer_cpu: 커널은 하나의 CPU만 do_timer()(jiffies 갱신, timekeeping)를 수행하도록 지정합니다. 이는 jiffies_lock 경쟁을 제거하고 중복 갱신을 방지합니다. NO_HZ 모드에서 해당 CPU가 idle에 진입하면 다른 CPU로 역할이 이전됩니다.
Timer 콜백 실행 흐름
저해상도 타이머(timer_list)와 고해상도 타이머(hrtimer)는 각각 다른 경로로 콜백을 실행합니다. 저해상도 타이머는 TIMER_SOFTIRQ를 통해, 고해상도 타이머는 hrtimer_interrupt()를 통해 처리됩니다.
__run_timers() 흐름 (저해상도)
/* kernel/time/timer.c - __run_timers() 핵심 로직 (간략화) */
static inline void __run_timers(struct timer_base *base)
{
struct hlist_head heads[LVL_DEPTH];
int levels;
if (time_before(jiffies, base->next_expiry))
return; /* 아직 만료된 타이머 없음 */
raw_spin_lock_irq(&base->lock);
/* base->clk를 jiffies까지 전진시키며 만료 타이머 수집 */
while (time_after_eq(jiffies, base->clk) &&
(levels = collect_expired_timers(base, heads))) {
/* 상위 레벨 타이머를 하위로 cascade + 만료 타이머 수집 */
base->clk++;
expire_timers(base, heads);
}
base->running_timer = NULL;
raw_spin_unlock_irq(&base->lock);
}
/* expire_timers: 수집된 타이머의 콜백 실행 */
static void expire_timers(struct timer_base *base,
struct hlist_head *head)
{
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;
fn = timer->function;
raw_spin_unlock_irq(&base->lock);
fn(timer); /* 콜백 실행 (lock 해제 상태) */
raw_spin_lock_irq(&base->lock);
}
}
코드 설명
kernel/time/timer.c의 __run_timers()와 expire_timers()는 저해상도 타이머 만료 처리의 핵심 함수입니다. TIMER_SOFTIRQ → run_timer_softirq() → __run_timers() 호출 체인으로 실행됩니다.
- time_before() 조기 반환현재 jiffies가
base->next_expiry보다 이전이면 만료된 타이머가 없으므로 즉시 반환합니다. 이 최적화로 대부분의 틱에서 불필요한 lock 획득을 피합니다. - raw_spin_lock_irq()per-CPU
timer_base의 raw spinlock을 IRQ 비활성화 상태로 획득합니다. 타이머 콜백 중에는 lock을 해제하여 다른 타이머 조작이 가능하게 합니다. - collect_expired_timers()
base->clk를 현재 jiffies까지 한 틱씩 전진하면서, 각 틱에서 만료된 타이머를 수집합니다. 동시에 상위 레벨 타이머의 cascade(하위 레벨 재배치)도 수행합니다. - expire_timers()수집된 타이머 목록을 순회하며 콜백을 실행합니다. lock을 해제한 상태에서 콜백을 호출하므로, 콜백 내에서
mod_timer()등 타이머 API를 안전하게 사용할 수 있습니다. - running_timer
base->running_timer에 현재 실행 중인 타이머를 기록합니다.del_timer_sync()가 이 필드를 확인하여 콜백 완료 대기 여부를 결정합니다.
collect_expired_timers()와 pending_map 비트맵 최적화
__run_timers()가 만료 타이머를 수집할 때, 320개 슬롯을 모두 순회하면 비효율적입니다. 커널은 pending_map 비트맵과 find_next_bit()을 사용하여, 타이머가 존재하는 슬롯만 O(1)에 찾아 처리합니다.
/* kernel/time/timer.c — collect_expired_timers() (Linux 6.x, 간략화) */
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;
/* 현재 clk에서 각 레벨의 해당 슬롯 검사 */
for (i = 0; i < LVL_DEPTH; i++) {
/* clk에서 이 레벨의 슬롯 인덱스 계산 */
idx = (clk & LVL_MASK) + i * LVL_SIZE;
/* LVL_MASK = 0x3F (64-1), LVL_SIZE = 64
* Level 0: idx = clk & 0x3F (0~63)
* Level 1: idx = 64 + (clk>>3 & 0x3F) (64~127)
* Level 2: idx = 128 + (clk>>6 & 0x3F) (128~191) ... */
/* pending_map에서 해당 슬롯에 타이머 존재 확인 */
if (!test_bit(idx, base->pending_map))
continue; /* 이 슬롯 비어있음 → 스킵 */
vec = base->vectors + idx;
/* 해당 슬롯의 모든 타이머를 수집 리스트로 이동 */
hlist_move_list(vec, heads + levels);
levels++;
/* 슬롯이 비었으면 pending_map 비트 클리어 */
__clear_bit(idx, base->pending_map);
clk >>= LVL_CLK_SHIFT; /* 상위 레벨로 (÷8) */
}
return levels;
}
/* next_expiry 계산 — find_next_bit()으로 O(1) 탐색 */
static unsigned long __next_timer_interrupt(
struct timer_base *base)
{
unsigned long clk, next = NEXT_TIMER_MAX_DELTA;
unsigned int idx, bit;
int lvl;
clk = base->clk;
for (lvl = 0; lvl < LVL_DEPTH; lvl++, clk >>= LVL_CLK_SHIFT) {
unsigned int pos = clk & LVL_MASK;
unsigned int start = pos + lvl * LVL_SIZE;
/* 이 레벨에서 다음 비트가 세팅된 슬롯 탐색 */
bit = find_next_bit(base->pending_map,
start + LVL_SIZE, start);
/* find_next_bit(): 비트맵에서 start부터
* 가장 가까운 1-비트 위치를 O(word_size)에 반환
* → 320개 슬롯 순회 대신 비트 연산으로 탐색 */
if (bit < start + LVL_SIZE) {
/* 이 레벨에서 타이머 발견 → 만료 시각 계산 */
unsigned long expires = clk + (bit - start);
if (time_before(expires, next))
next = expires;
}
}
return next;
}
코드 설명
kernel/time/timer.c의 만료 타이머 수집과 다음 만료 시각 탐색 구현입니다. pending_map 비트맵이 핵심 최적화입니다.
- pending_map 비트 검사
test_bit(idx, pending_map)으로 해당 슬롯에 타이머가 존재하는지 O(1)에 확인합니다. 비트가 0이면 슬롯이 비어있으므로 hlist 순회 없이 즉시 건너뜁니다. 대부분의 슬롯이 비어있으므로 이 최적화로 불필요한 메모리 접근을 크게 줄입니다. - hlist_move_list()만료된 슬롯의 전체 hlist를 수집 리스트(
heads[])로 O(1)에 이동합니다. 개별 타이머를 하나씩 옮기지 않고 리스트 헤드만 교체하므로 매우 효율적입니다. - LVL_CLK_SHIFT상위 레벨로 이동할 때
clk를 3비트 오른쪽 시프트합니다(÷8). 이는 Level 1의 granularity가 Level 0의 8배인 것에 대응합니다. 각 레벨의 해당 슬롯을 한 번에 계산할 수 있습니다. - find_next_bit()320비트 비트맵에서 지정 범위 내 가장 가까운 1-비트를 CPU 워드 크기 단위로 탐색합니다. 64비트 시스템에서는 최대 5개 워드만 검사하면 되므로, 320개 슬롯을 순회하는 것보다 훨씬 빠릅니다. 이것이 NO_HZ에서 다음 wakeup 시점을 빠르게 계산하는 핵심 메커니즘입니다.
- __clear_bit()슬롯의 모든 타이머를 수집한 후 해당 비트를 클리어합니다. 새 타이머가 삽입되면
enqueue_timer()에서 다시__set_bit()으로 설정합니다.
hrtimer_interrupt() 흐름 (고해상도)
hrtimer_interrupt()는 clockevent 인터럽트 핸들러(Handler)에서 직접 호출됩니다. 하드 IRQ 컨텍스트에서 만료된 hrtimer의 콜백을 실행하며, 처리 후 다음 만료 시각으로 clockevent를 재프로그래밍합니다.
/* kernel/time/hrtimer.c - hrtimer_interrupt() 핵심 로직 (간략화) */
void hrtimer_interrupt(struct clock_event_device *dev)
{
struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
ktime_t now = ktime_get();
ktime_t expires_next;
int i;
cpu_base->in_hrtirq = 1;
/* 모든 클럭 베이스 순회 */
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
struct hrtimer_clock_base *base = &cpu_base->clock_base[i];
struct timerqueue_node *node;
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer = container_of(node, struct hrtimer, node);
if (node->expires > now)
break; /* 아직 만료 안 됨 */
__remove_hrtimer(timer, base, HRTIMER_STATE_INACTIVE, 0);
__run_hrtimer(cpu_base, base, timer, &basenow, flags);
}
}
/* 다음 만료 시각 계산 후 clockevent 재프로그래밍 */
expires_next = hrtimer_update_next_event(cpu_base);
tick_program_event(expires_next, 1);
cpu_base->in_hrtirq = 0;
}
/* __run_hrtimer: 개별 hrtimer 콜백 실행 */
static void __run_hrtimer(struct hrtimer_cpu_base *cpu_base,
struct hrtimer_clock_base *base,
struct hrtimer *timer, ...)
{
enum hrtimer_restart (*fn)(struct hrtimer *);
fn = timer->function;
base->running = timer;
raw_write_seqcount_barrier(&base->seq);
/* 콜백 실행 (RESTART면 다시 enqueue) */
if (fn(timer) == HRTIMER_RESTART)
enqueue_hrtimer(timer, base, HRTIMER_MODE_ABS);
base->running = NULL;
}
코드 설명
kernel/time/hrtimer.c의 hrtimer_interrupt()는 clockevent 하드웨어 인터럽트에서 직접 호출되어 만료된 고해상도 타이머를 처리합니다. 하드 IRQ 컨텍스트에서 실행됩니다.
- this_cpu_ptr(&hrtimer_bases)현재 CPU의
hrtimer_cpu_base를 가져옵니다. hrtimer는 per-CPU 구조이므로 lock 없이 접근 가능하며,in_hrtirq플래그로 재진입을 방지합니다. - 클럭 베이스 순회
HRTIMER_MAX_CLOCK_BASES(8개: MONOTONIC, REALTIME, BOOTTIME, TAI x hard/soft)를 순회하며 각 베이스의 timerqueue에서 만료된 타이머를 처리합니다. - timerqueue_getnext()leftmost 캐싱된 RB-tree에서 가장 이른 만료 타이머를 O(1)에 가져옵니다.
node->expires > now이면 아직 만료되지 않았으므로 루프를 종료합니다. - __run_hrtimer()개별 타이머의 콜백을 실행합니다. 반환값이
HRTIMER_RESTART이면enqueue_hrtimer()로 RB-tree에 재삽입합니다.raw_write_seqcount_barrier()로 타이머 상태 변경의 가시성을 보장합니다. - hrtimer_update_next_event()모든 클럭 베이스에서 가장 이른 다음 만료 시각을 계산합니다. 이 값으로
tick_program_event()가 clockevent 하드웨어를 재프로그래밍하여 다음 인터럽트 시점을 설정합니다.
Timer Softirq (TIMER_SOFTIRQ) 처리 상세
TIMER_SOFTIRQ는 softirq 벡터 0번으로, 저해상도 타이머의 만료를 처리합니다. 이 softirq는 매 tick마다 run_local_timers()에서 raise되며, do_softirq() 경로에서 run_timer_softirq()로 처리됩니다.
/* kernel/time/timer.c - softirq 등록 */
void __init init_timers(void)
{
init_timer_cpus();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
/* run_timer_softirq: TIMER_SOFTIRQ 핸들러 */
static void run_timer_softirq(struct softirq_action *h)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
__run_timers(base);
/* deferrable 타이머도 처리 (idle이 아닐 때만) */
if (!tick_nohz_full_cpu(smp_processor_id())) {
base = this_cpu_ptr(&timer_bases[BASE_DEF]);
__run_timers(base);
}
}
/* run_local_timers: 매 tick마다 호출 */
void run_local_timers(void)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
hrtimer_run_queues(); /* 저해상도 모드일 때 hrtimer도 처리 */
if (time_after_eq(jiffies, base->next_expiry) ||
time_after_eq(jiffies, this_cpu_read(timer_bases[BASE_DEF].next_expiry)))
raise_softirq(TIMER_SOFTIRQ);
}
코드 설명
kernel/time/timer.c에 정의된 저해상도 타이머의 softirq 처리 경로입니다. 호출 체인: 타이머 인터럽트 → run_local_timers() → raise_softirq(TIMER_SOFTIRQ) → run_timer_softirq() → __run_timers().
- init_timers()부팅 시
start_kernel()에서 호출되어 per-CPUtimer_base를 초기화하고,open_softirq()로TIMER_SOFTIRQ벡터에run_timer_softirq핸들러를 등록합니다. - run_timer_softirq()
TIMER_SOFTIRQ핸들러입니다.BASE_STD(일반 타이머)를 먼저 처리한 후,NO_HZ_FULLCPU가 아니면BASE_DEF(deferrable 타이머)도 처리합니다. - tick_nohz_full_cpu()현재 CPU가
NO_HZ_FULL모드인지 확인합니다.NO_HZ_FULLCPU에서는 deferrable 타이머 처리를 건너뛰어 불필요한 틱을 최소화합니다. - run_local_timers()매 타이머 틱마다 호출됩니다.
hrtimer_run_queues()로 저해상도 모드의 hrtimer를 처리한 후, 만료된 타이머가 있으면raise_softirq(TIMER_SOFTIRQ)로 softirq를 트리거합니다. - next_expiry 비교
BASE_STD와BASE_DEF모두의next_expiry를 확인하여, 어느 한쪽이라도 만료되었으면 softirq를 발생시킵니다. 이 조건문으로 불필요한 softirq 발생을 억제합니다.
BASE_STD vs BASE_DEF: 커널은 타이머를 두 가지 베이스로 분류합니다. BASE_STD(standard)는 일반 타이머, BASE_DEF(deferrable)는 지연 가능한 타이머입니다. deferrable 타이머는 CPU가 idle 상태일 때 처리를 미루어 전력을 절약합니다. TIMER_DEFERRABLE 플래그로 지정합니다.
RTC (Real-Time Clock) 서브시스템
RTC는 시스템 전원이 꺼져 있을 때도 배터리로 유지되는 하드웨어 시계입니다. 커널은 부팅 시 RTC에서 시간을 읽어 시스템 시계를 초기화하고, RTC 알람으로 시스템을 깨울 수 있습니다.
RTC 아키텍처
| RTC 유형 | 인터페이스 | 정밀도 | 커널 드라이버 |
|---|---|---|---|
| CMOS RTC | I/O 포트 0x70/0x71 (x86) | 1초 | drivers/rtc/rtc-cmos.c |
| I2C RTC | I2C 버스 (DS1307, PCF8523 등) | 1초 | drivers/rtc/rtc-ds1307.c |
| SoC 내장 RTC | MMIO (SoC 레지스터(Register)) | 서브초 가능 | drivers/rtc/rtc-* (벤더별) |
| PL031 (ARM) | MMIO (AMBA PrimeCell) | 1초 | drivers/rtc/rtc-pl031.c |
RTC 드라이버 구현
#include <linux/rtc.h>
/* RTC 오퍼레이션 구조체 */
static const struct rtc_class_ops my_rtc_ops = {
.read_time = my_read_time, /* 현재 시각 읽기 */
.set_time = my_set_time, /* 시각 설정 */
.read_alarm = my_read_alarm, /* 알람 읽기 */
.set_alarm = my_set_alarm, /* 알람 설정 */
.alarm_irq_enable = my_alarm_irq_enable,
};
/* 시간 읽기 콜백 */
static int my_read_time(struct device *dev, struct rtc_time *tm)
{
/* H/W 레지스터에서 BCD 또는 바이너리로 읽기 */
tm->tm_sec = readl(base + RTC_SEC);
tm->tm_min = readl(base + RTC_MIN);
tm->tm_hour = readl(base + RTC_HOUR);
tm->tm_mday = readl(base + RTC_DAY);
tm->tm_mon = readl(base + RTC_MON) - 1; /* 0-based */
tm->tm_year = readl(base + RTC_YEAR) - 1900;
return 0;
}
/* RTC 디바이스 등록 */
struct rtc_device *rtc = devm_rtc_device_register(&pdev->dev,
"my-rtc", &my_rtc_ops, THIS_MODULE);
/* 또는 현대적 API (5.x+) */
rtc = devm_rtc_allocate_device(&pdev->dev);
rtc->ops = &my_rtc_ops;
rtc->range_min = RTC_TIMESTAMP_BEGIN_2000;
rtc->range_max = RTC_TIMESTAMP_END_2099;
devm_rtc_register_device(rtc);
유저 공간 RTC 관리
# RTC 시간 읽기
hwclock --show # /dev/rtc0에서 읽기
cat /sys/class/rtc/rtc0/time # sysfs에서 읽기
cat /proc/driver/rtc # CMOS RTC 상세 정보 (x86)
# RTC에 시스템 시간 쓰기
hwclock --systohc # 시스템 시간 → RTC
hwclock --hctosys # RTC → 시스템 시간 (부팅 시)
# UTC vs Local Time (주의!)
hwclock --systohc --utc # RTC를 UTC로 설정 (Linux 권장)
hwclock --systohc --localtime # 로컬 시간 (Windows 듀얼부팅 시)
timedatectl set-local-rtc 0 # systemd에서 UTC 모드 설정
# RTC 알람 설정 (시스템 웨이크업)
echo +60 > /sys/class/rtc/rtc0/wakealarm # 60초 후 깨우기
echo 0 > /sys/class/rtc/rtc0/wakealarm # 알람 해제
rtcwake -m mem -s 300 # suspend 후 300초 뒤 깨우기
CMOS RTC 레지스터 상세
CMOS RTC 전체 레지스터 맵
| 인덱스 | 이름 | R/W | 설명 |
|---|---|---|---|
| 0x00 | Seconds | R/W | 현재 초 (0-59, BCD 또는 바이너리) |
| 0x01 | Seconds Alarm | R/W | 알람 초 (0xC0~0xFF = don't care) |
| 0x02 | Minutes | R/W | 현재 분 (0-59) |
| 0x03 | Minutes Alarm | R/W | 알람 분 |
| 0x04 | Hours | R/W | 현재 시 (12H: 1-12+PM bit, 24H: 0-23) |
| 0x05 | Hours Alarm | R/W | 알람 시 |
| 0x06 | Day of Week | R/W | 요일 (1=일요일, 7=토요일) |
| 0x07 | Day of Month | R/W | 일 (1-31) |
| 0x08 | Month | R/W | 월 (1-12) |
| 0x09 | Year | R/W | 연도 하위 2자리 (00-99) |
| 0x0A | Status Register A | R/W | UIP, divider, rate select |
| 0x0B | Status Register B | R/W | SET, PIE, AIE, UIE, SQWE, DM, 24/12 |
| 0x0C | Status Register C | R/O | IRQ 플래그 (읽으면 클리어됨) |
| 0x0D | Status Register D | R/O | VRT (Valid RAM and Time) |
| 0x32 | Century | R/W | 세기 (19/20, ACPI FADT에 오프셋(Offset) 정의) |
Status Register A 비트 필드 (0x0A)
| 비트 | 필드 | 설명 |
|---|---|---|
| [7] | UIP (Update In Progress) | 1=RTC가 시간 레지스터 업데이트 중 (~244μs). 이 동안 시간 레지스터를 읽으면 안 됨 |
| [6:4] | DV (Divider) | 오실레이터 분주비. 010=32.768kHz(기본). 11x=리셋, 110=분주기 리셋 |
| [3:0] | RS (Rate Select) | Periodic Interrupt 주파수. 0000=없음, 0011=8192Hz, 0110=1024Hz(기본), 1111=2Hz |
Status Register B 비트 필드 (0x0B)
| 비트 | 필드 | R/W | 설명 |
|---|---|---|---|
| [7] | SET | R/W | 1=시간 업데이트 억제 (시간 설정 시 사용). 0=정상 카운트 |
| [6] | PIE (Periodic Interrupt Enable) | R/W | 1=RS 주파수로 IRQ8 발생 |
| [5] | AIE (Alarm Interrupt Enable) | R/W | 1=알람 시각 도달 시 IRQ8 발생 |
| [4] | UIE (Update-ended Interrupt Enable) | R/W | 1=매초 업데이트 완료 시 IRQ8 발생 |
| [3] | SQWE (Square Wave Enable) | R/W | 1=SQW 출력 핀에 구형파 생성 (레거시) |
| [2] | DM (Data Mode) | R/W | 0=BCD, 1=Binary. 시간 레지스터의 데이터 형식 |
| [1] | 24/12 | R/W | 0=12시간 모드, 1=24시간 모드 |
| [0] | DSE (Daylight Saving Enable) | R/W | 1=DST 자동 전환 (실제 사용 안 함) |
CMOS RTC 인터럽트 종류
/* drivers/rtc/rtc-cmos.c — CMOS RTC 인터럽트 핸들러 */
/*
* CMOS RTC는 IRQ 8을 통해 3가지 인터럽트를 생성합니다:
* 1. Periodic Interrupt (PIE): RS 비트로 설정된 주파수 (2-8192 Hz)
* 2. Alarm Interrupt (AIE): 설정된 알람 시각 도달
* 3. Update-ended Interrupt (UIE): 매초 시간 업데이트 완료
*
* Status Register C를 읽어 어떤 인터럽트인지 확인합니다.
* (읽으면 플래그가 자동 클리어됨)
*/
static irqreturn_t cmos_interrupt(int irq, void *p)
{
struct cmos_rtc *cmos = p;
u8 irqstat;
spin_lock(&rtc_lock);
/* Status C 읽기: IRQ 원인 확인 + 플래그 클리어 */
irqstat = CMOS_READ(RTC_INTR_FLAGS); /* 0x0C */
irqstat &= (CMOS_READ(RTC_CONTROL) /* 0x0B */
& (RTC_PIE|RTC_AIE|RTC_UIE));
spin_unlock(&rtc_lock);
if (irqstat) {
/* rtc-core에 이벤트 보고 */
rtc_update_irq(cmos->rtc, 1, irqstat);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
/* RTC 시간 읽기 — UIP 비트 확인 필수 */
static int cmos_read_time(struct device *dev, struct rtc_time *t)
{
unsigned char ctrl;
spin_lock_irq(&rtc_lock);
/* UIP=1이면 업데이트 중 — 최대 244μs 대기 */
while (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
cpu_relax();
t->tm_sec = CMOS_READ(RTC_SECONDS); /* 0x00 */
t->tm_min = CMOS_READ(RTC_MINUTES); /* 0x02 */
t->tm_hour = CMOS_READ(RTC_HOURS); /* 0x04 */
t->tm_mday = CMOS_READ(RTC_DAY_OF_MONTH); /* 0x07 */
t->tm_mon = CMOS_READ(RTC_MONTH); /* 0x08 */
t->tm_year = CMOS_READ(RTC_YEAR); /* 0x09 */
ctrl = CMOS_READ(RTC_CONTROL); /* 0x0B */
spin_unlock_irq(&rtc_lock);
/* BCD → 바이너리 변환 (DM 비트 확인) */
if (!(ctrl & RTC_DM_BINARY)) {
t->tm_sec = bcd2bin(t->tm_sec);
t->tm_min = bcd2bin(t->tm_min);
t->tm_hour = bcd2bin(t->tm_hour);
t->tm_mday = bcd2bin(t->tm_mday);
t->tm_mon = bcd2bin(t->tm_mon);
t->tm_year = bcd2bin(t->tm_year);
}
t->tm_mon--; /* 커널: 0-based month */
t->tm_year += (t->tm_year < 70) ? 100 : 0; /* 2000+ 보정 */
return 0;
}
NTP ↔ RTC 동기화
/* kernel/time/ntp.c — NTP → RTC 주기적 동기화 */
/*
* CONFIG_RTC_SYSTOHC 설정 시 커널이 11분마다
* 시스템 시간을 RTC에 기록하여 동기화합니다.
*
* 이 메커니즘은 NTP로 보정된 정확한 시스템 시간을
* RTC에 반영하여, 다음 부팅 시 시간 오차를 최소화합니다.
*/
static void sync_hw_clock(struct work_struct *work)
{
/* RTC 동기화 조건:
* 1. NTP가 시간을 동기화한 상태 (STA_UNSYNC 해제)
* 2. 잔여 보정량이 0.5초 이내
* 3. 초 값이 정확한 시점 (0.5초 경계) */
struct timespec64 now;
ktime_get_real_ts64(&now);
/* 0.5초 경계에서 RTC에 쓰기 (정수 초 정확도 보장) */
if (now.tv_nsec >= (NSEC_PER_SEC >> 1))
now.tv_sec++;
struct rtc_time tm;
rtc_time64_to_tm(now.tv_sec, &tm);
rtc_set_time(rtc, &tm);
}
/* 11분 주기 타이머 — sync_cmos_clock() */
/* schedule_delayed_work(&sync_work, 660 * HZ); */
RTC sysfs 인터페이스
# /sys/class/rtc/rtc0/ 전체 파일 목록
$ ls /sys/class/rtc/rtc0/
date # 현재 날짜 (YYYY-MM-DD)
hctosys # 부팅 시 이 RTC에서 시간을 읽었는지 (1/0)
max_user_freq # 유저 공간 최대 periodic 주파수 (기본: 64)
name # RTC 이름 (예: "rtc_cmos")
offset # 보정 오프셋 (ppb 단위)
since_epoch # Unix epoch 이후 초
time # 현재 시각 (HH:MM:SS)
wakealarm # 웨이크업 알람 (epoch 또는 +N초)
# /proc/driver/rtc 출력 해석 (x86 CMOS RTC 전용)
$ cat /proc/driver/rtc
rtc_time : 14:30:25 # 현재 RTC 시간
rtc_date : 2026-02-26 # 현재 RTC 날짜
rtc_epoch : 1900 # epoch 기준 연도
alarm : 00:00:00 # 알람 시각
alarm_IRQ : no # 알람 IRQ 활성 여부
alrm_date : 2026-02-26 # 알람 날짜
update_IRQ : no # 매초 업데이트 IRQ
periodic_IRQ : no # periodic IRQ
periodic_freq : 1024 # periodic 주파수 (Hz)
batt_status : okay # 배터리 상태 (VRT 비트)
24hr : yes # 24시간 모드
BCD : yes # BCD 모드
RTC 주의사항
- Y2038 문제 — 32비트 time_t를 사용하는 구형 RTC는 2038년에 오버플로. 커널은
rtc_time64_to_tm()으로 64비트 전환 완료. 드라이버에서range_min/range_max명시 필요 - BCD vs 바이너리 — 일부 RTC는 BCD 인코딩.
bcd2bin()/bin2bcd()로 변환. 잘못된 변환은 날짜 오류 - 레지스터 읽기 경합(Contention) — RTC 레지스터 읽기 중 초가 변경되면 불일치 데이터 반환. Update-In-Progress(UIP) 비트 확인 또는 두 번 읽어서 비교
- 배터리 고갈 — CMOS 배터리(CR2032) 소진 시 시간 초기화. 부팅 시 NTP 동기화로 보상하지만, NTP 없는 임베디드 환경에서 문제
- UTC/로컬 타임 혼동 — Linux는 RTC를 UTC로, Windows는 로컬 타임으로 가정. 듀얼부팅 시 시간이 틀어지는 원인
- RTC 알람과 suspend — S3(suspend-to-RAM)에서 RTC 알람으로 깨울 수 있지만, 모든 RTC가 알람 IRQ를 지원하지는 않음.
/sys/class/rtc/rtc0/wakealarm지원 여부 확인 - NTP 드리프트 보상 — RTC는 수십 ppm의 오차 가능(월 수 초).
adjtimex로 커널이 NTP와 RTC 간 주기적 보정
delayed_work — 지연 실행 작업
delayed_work는 timer_list와 workqueue를 결합한 API입니다.
timer_list로 지정한 시간 후에 workqueue(kworker 스레드)에 작업을 등록하므로,
타이머 콜백과 달리 프로세스 컨텍스트에서 실행됩니다.
슬립, mutex 잠금, 메모리 할당(GFP_KERNEL) 등이 모두 허용됩니다.
timer_list vs delayed_work 선택 기준:
콜백에서 슬립/잠금이 필요하거나 실행 시간이 긴 경우 → delayed_work
나노초 정밀도가 필요하거나 인터럽트 컨텍스트에서 실행해야 하는 경우 → hrtimer
단순 타임아웃(슬립 불필요) → timer_list
#include <linux/workqueue.h>
#include <linux/jiffies.h>
struct my_dev {
struct delayed_work poll_work; /* delayed_work 선언 */
void __iomem *base;
};
/* workqueue 핸들러 — 프로세스 컨텍스트에서 실행 */
static void my_poll_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_dev *dev = container_of(dwork, struct my_dev, poll_work);
/* 프로세스 컨텍스트: mutex, kmalloc(GFP_KERNEL), msleep() 가능 */
u32 status = readl(dev->base + STATUS_REG);
if (status & BUSY_BIT) {
/* 아직 바쁨: 100ms 후 재시도 */
schedule_delayed_work(&dev->poll_work, msecs_to_jiffies(100));
} else {
pr_info("device ready\n");
}
}
/* 초기화 */
INIT_DELAYED_WORK(&dev->poll_work, my_poll_handler);
/* 200ms 후 실행 예약 */
schedule_delayed_work(&dev->poll_work, msecs_to_jiffies(200));
/* 특정 workqueue에 예약 (기본 system_wq 대신) */
queue_delayed_work(my_wq, &dev->poll_work, msecs_to_jiffies(200));
/* 취소 — 동기적으로 진행 중인 작업 완료까지 대기 */
cancel_delayed_work_sync(&dev->poll_work);
/* 재예약 (pending이면 취소 후 재등록) */
mod_delayed_work(system_wq, &dev->poll_work, msecs_to_jiffies(500));
| 특성 | timer_list | hrtimer | delayed_work |
|---|---|---|---|
| 실행 컨텍스트 | softirq (TIMER_SOFTIRQ) | hardirq / softirq | kworker 스레드 (프로세스) |
| 시간 해상도 | 1/HZ (≥4ms@250Hz) | 나노초 | 1/HZ (jiffies 기반) |
| 슬립 가능 | ❌ | ❌ | ✅ |
| mutex/semaphore | ❌ | ❌ | ✅ |
| kmalloc(GFP_KERNEL) | ❌ | ❌ | ✅ |
| CPU 친화성 | Per-CPU (timer wheel) | Per-CPU (hrtimer_cpu_base) | WQ 정책에 따름 |
| 취소 API | del_timer_sync() |
hrtimer_cancel() |
cancel_delayed_work_sync() |
| 주요 사용 사례 | 네트워크 타임아웃, 폴링 | nanosleep, POSIX 타이머 | 디바이스 폴링, I/O 완료 처리 |
schedule_delayed_work() 내부: 내부적으로 timer_list를 설정하고, 타이머 만료 시 queue_work()로 workqueue에 work를 등록합니다.
즉, 두 단계로 동작합니다: ① timer_list 만료 → ② kworker에서 핸들러 실행.
실제 실행 시각은 타이머 만료 후 kworker 스케줄링까지의 지연이 추가됩니다.
커널 Watchdog — Lockup 감지
커널 watchdog은 CPU가 장기간 응답하지 않는 lockup 상태를 자동으로 감지합니다. softlockup과 hardlockup 두 종류가 있으며, 각각 다른 타이머 메커니즘을 사용합니다.
Softlockup vs Hardlockup
| 종류 | 감지 조건 | 감지 시간 | 감지 메커니즘 | 커널 반응 | CONFIG 옵션 |
|---|---|---|---|---|---|
| Softlockup | CPU가 스케줄러에 제어를 오래 양보하지 않음 | watchdog_thresh 초 (기본 20초) | hrtimer + watchdog kthread | 경고 메시지 출력 (panic 옵션 가능) | CONFIG_SOFTLOCKUP_DETECTOR |
| Hardlockup | CPU가 인터럽트(hrtimer 포함)도 처리하지 않음 | watchdog_thresh/2 초 (기본 10초) | NMI watchdog (PMU 이벤트) | 패닉 또는 KDB/KGDB 진입 | CONFIG_HARDLOCKUP_DETECTOR |
# watchdog 설정 확인
cat /proc/sys/kernel/watchdog # 1=활성화
cat /proc/sys/kernel/watchdog_thresh # 임계값 (기본 10, softlockup=2배=20초)
cat /proc/sys/kernel/softlockup_panic # 1이면 softlockup 시 패닉
cat /proc/sys/kernel/hardlockup_panic # 1이면 hardlockup 시 패닉
# watchdog 비활성화 (테스트/디버깅용)
echo 0 > /proc/sys/kernel/watchdog
# 임계값 변경 (20초 → 30초로 완화)
echo 15 > /proc/sys/kernel/watchdog_thresh # softlockup=30초, hardlockup=15초
# softlockup 로그 (dmesg 출력 예시)
# [12345.678] watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [my_task:4567]
# [12345.679] Modules linked in: ...
# [12345.680] CPU: 3 PID: 4567 Comm: my_task Not tainted 6.1.0 #1
# 커널 부트 파라미터로 watchdog 설정
# nosoftlockup — softlockup 감지 비활성화
# nohlt — halt 명령 대신 idle 루프 (전력 절약 비활성화)
- 긴 임계 섹션 — spinlock을 오래 잡거나 preemption을 비활성화한 상태로 무거운 연산을 수행하면 softlockup 발생.
cond_resched()로 스케줄러에 제어를 양보해야 합니다. - 인터럽트 비활성화 —
local_irq_disable()상태로 오래 실행하면 hardlockup 발생. 인터럽트 비활성화 구간은 최소화해야 합니다. - PREEMPT_RT — 실시간 커널에서는 스핀락(Spinlock)도 슬립 가능 뮤텍스(Mutex)로 교체되어 softlockup 위험이 감소합니다.
- 가상 머신 — VM 환경에서는 하이퍼바이저(Hypervisor)가 vCPU를 선점(Preemption)할 때 softlockup 오탐이 발생할 수 있습니다. watchdog_thresh를 높이거나 비활성화하기도 합니다.
watchdog_timer_fn() 구현 분석
Softlockup 감지의 핵심인 watchdog_timer_fn()은 Per-CPU hrtimer 콜백으로, 주기적으로 watchdog/N kthread가 정상 스케줄링되는지 확인합니다.
/* kernel/watchdog.c — watchdog_timer_fn() (Linux 6.x, 간략화) */
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
unsigned long touch_ts, now;
struct rq *rq = this_rq();
int duration;
int softlockup_all_cpu_backtrace =
sysctl_softlockup_all_cpu_backtrace;
/* 1. hardlockup 감지용 카운터 증가
* NMI watchdog가 이 카운터를 모니터링 */
watchdog_interrupt_count();
/* __this_cpu_inc(hrtimer_interrupts);
* NMI 핸들러에서 이 값이 변하지 않으면 hardlockup으로 판단 */
/* 2. watchdog kthread의 마지막 실행 시각 조회 */
touch_ts = __this_cpu_read(watchdog_touch_ts);
now = get_timestamp(); /* sched_clock() 기반 */
/* 3. watchdog kthread가 스케줄링되었으면 → 정상 */
if (touch_ts == SOFTLOCKUP_RESET) {
__this_cpu_write(watchdog_touch_ts, now);
goto reprogram;
}
/* 4. 마지막 실행 후 경과 시간 계산 */
duration = now - touch_ts;
/* 5. watchdog_thresh * 2 초 초과 → softlockup 감지 */
if (duration >= get_softlockup_thresh()) {
/* watchdog_thresh 기본=10, softlockup 임계=20초 */
pr_emerg("BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
smp_processor_id(), duration,
current->comm, task_pid_nr(current));
/* 스택 트레이스 출력 */
dump_stack();
/* softlockup_panic이 설정되었으면 패닉 */
if (softlockup_panic)
panic("softlockup: hung tasks");
}
reprogram:
/* 6. hrtimer 재프로그래밍 (watchdog_thresh/5 간격) */
hrtimer_forward_now(hrtimer,
ns_to_ktime(sample_period));
/* sample_period = watchdog_thresh * (NSEC_PER_SEC / 5)
* watchdog_thresh=10이면 2초 간격 */
return HRTIMER_RESTART;
}
/* watchdog kthread — 스케줄링되면 타임스탬프 갱신 */
static int watchdog_kthread_fn(void *data)
{
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
/* hrtimer 콜백이 깨워줌 → 타임스탬프 갱신 */
__this_cpu_write(watchdog_touch_ts, SOFTLOCKUP_RESET);
schedule();
}
return 0;
}
코드 설명
kernel/watchdog.c의 softlockup 감지 메커니즘 구현입니다. hrtimer + kthread 조합으로 CPU 스케줄링 정상 동작을 모니터링합니다.
- watchdog_interrupt_count()Per-CPU 변수
hrtimer_interrupts를 1 증가시킵니다. NMI watchdog(hardlockup 감지)이 이 값을 모니터링하여, 값이 변하지 않으면 hrtimer 인터럽트도 처리되지 않는 hardlockup으로 판단합니다. - watchdog_touch_ts
watchdog/Nkthread가 마지막으로 스케줄링된 시각입니다. kthread가 정상 실행되면SOFTLOCKUP_RESET으로 설정하고, hrtimer 콜백에서 현재 시각으로 갱신합니다. 이 값이 오래 갱신되지 않으면 kthread가 스케줄링되지 못하고 있는 의미입니다. - get_softlockup_thresh()
watchdog_thresh * 2를 반환합니다. 기본값watchdog_thresh=10이므로 softlockup 임계값은 20초입니다. kthread가 20초 이상 스케줄링되지 못하면 softlockup으로 감지됩니다. - sample_periodhrtimer의 주기는
watchdog_thresh / 5초입니다(기본 2초). 임계값(20초)보다 훨씬 짧은 주기로 검사하므로, 감지 지연이 최소화됩니다. - 감지 원리 요약hrtimer 콜백은 hard IRQ 컨텍스트에서 실행되므로, 커널이 spinlock을 잡고 있어도 실행됩니다. 하지만
watchdog/Nkthread는 스케줄러를 통해 실행되므로, CPU가 선점 불가 상태에 갇히면 kthread가 실행되지 못합니다. 이 차이를 이용하여 softlockup(스케줄링 멈춤)을 감지합니다.
타이머 디버깅 및 분석
타이머 서브시스템 문제(지터, 지연, 불필요한 wake-up 등)를 진단하는 도구와 기법을 설명합니다.
/proc/timer_list 출력 해석 가이드
/proc/timer_list는 모든 CPU의 활성 hrtimer와 timer_list를 덤프(Dump)합니다. 출력을 정확히 해석하면 타이머 지연, 불필요한 wakeup, 만료 타이밍 문제를 진단할 수 있습니다.
# 전체 타이머 목록 보기
cat /proc/timer_list
# 출력 예시:
# cpu: 0
# clock 0:
# .base: 0xffff888003400000
# .index: 0
# .resolution: 1 nsecs
# .get_time: ktime_get
# active timers:
# #0: <0xffff888012345678>, tick_sched_timer, S:01 ...
# # expires at 5000000000-5000000000 nsecs [in 4000000 to 4000000 nsecs]
# 특정 콜백 함수 이름 검색
grep "tick_sched_timer" /proc/timer_list
# 가장 빨리 만료될 타이머 확인
awk '/expires at/ {print NR": "$0}' /proc/timer_list | head -20
# timer_list의 상세 jiffies 정보
grep -A5 "^jiffies" /proc/timer_list
/proc/timer_list 출력 필드 상세 해석
출력은 크게 3개 섹션으로 구분됩니다: hrtimer(클럭 베이스별), Tick Device(clockevent), 전역 timekeeping 정보. 각 필드의 의미를 정확히 파악해야 실전 디버깅에 활용할 수 있습니다.
| 필드 | 예시 | 의미 | 진단 활용 |
|---|---|---|---|
cpu: N |
cpu: 0 |
해당 CPU 번호 | 특정 CPU의 타이머 부하 확인 |
clock N: |
clock 0: |
클럭 베이스 인덱스 (0=MONOTONIC, 1=REALTIME, 2=BOOTTIME, 3=TAI) | 어떤 클럭에 타이머가 집중되는지 확인 |
.index |
.index: 0 |
hrtimer_clock_base 내부 인덱스 | 클럭 베이스 식별 |
.resolution |
.resolution: 1 nsecs |
고해상도 모드 활성 시 1ns, 저해상도 시 tick 단위 | hrtimer 고해상도 전환 확인 |
.get_time |
.get_time: ktime_get |
이 클럭 베이스의 시간 읽기 함수 | 클럭 소스 확인 |
#N: <addr> |
#0: <0xffff888012345678> |
활성 hrtimer의 순번과 커널 메모리 주소 | 타이머 객체 추적 |
callback_fn |
tick_sched_timer |
만료 시 호출될 콜백 함수명 | 어떤 서브시스템이 타이머를 사용하는지 식별 |
S:XX |
S:01 |
타이머 상태: 01=ENQUEUED (활성), 00=INACTIVE | 비활성 타이머 누적 확인 |
expires at A-B nsecs |
expires at 5000000000-5000000000 nsecs |
A=소프트 만료(_softexpires), B=하드 만료(node.expires) | A < B이면 timer slack 범위 확인 |
[in X to Y nsecs] |
[in 4000000 to 4000000 nsecs] |
현재 시각 기준 만료까지 남은 나노초 | 가장 임박한 타이머 식별 |
# 실전 진단 스크립트: CPU별 활성 타이머 수 집계
awk '/^cpu:/ {cpu=$2} /^ #[0-9]/ {count[cpu]++} END {for(c in count) print "CPU "c": "count[c]" active timers"}' /proc/timer_list
# 가장 자주 나타나는 콜백 함수 TOP 10
grep -oP '(?<=, )[a-z_]+(?=,)' /proc/timer_list | sort | uniq -c | sort -rn | head -10
# 1ms 이내에 만료될 hrtimer 찾기
awk '/\[in [0-9]+ to/ {
match($0, /\[in ([0-9]+)/, a);
if (a[1] < 1000000) print $0
}' /proc/timer_list
# Tick Device 섹션 해석
# Tick Device: mode: 1 (1=ONESHOT, 0=PERIODIC)
# Per CPU device: N
# Clock Event Device: lapic-deadline (clockevent 디바이스명)
# event_handler: hrtimer_interrupt (현재 핸들러)
# → hrtimer_interrupt이면 고해상도 모드 활성
# → tick_handle_periodic이면 저해상도 모드
grep "event_handler" /proc/timer_list
# 모든 CPU가 hrtimer_interrupt를 사용해야 고해상도 모드가 정상 동작
# 전역 타이밍 정보
# jiffies: 4300012345 — 현재 jiffies 값
# last_jiffies_update: 5000000000 — 마지막 jiffies 갱신 시각(ns)
grep "^jiffies" /proc/timer_list
/proc/timer_list 활용 팁:
- idle wakeup 원인 추적:
tick_sched_timer외에 불필요한 활성 타이머가 있으면 idle 시간이 줄어듭니다.PowerTOP과 함께 확인하여 불필요한 주기적 타이머를 식별하세요. - softexpires 범위 확인:
expires at A-B에서 A < B이면 timer slack이 적용된 것입니다. 범위가 넓을수록 커널이 타이머를 묶어 처리(coalescing)하여 전력을 절약합니다. - 고해상도 미전환 진단:
event_handler: tick_handle_periodic이 표시되면 해당 CPU가 고해상도 모드로 전환되지 않은 것입니다. clockevent가 ONESHOT을 미지원하거나highres=off부트 파라미터가 원인일 수 있습니다.
ftrace 타이머 이벤트
# ftrace로 타이머 이벤트 추적
cd /sys/kernel/debug/tracing
# timer 관련 이벤트 목록 확인
ls events/timer/
# timer_cancel timer_expire_entry timer_expire_exit
# timer_init timer_start
# hrtimer_cancel hrtimer_expire_entry hrtimer_expire_exit
# hrtimer_init hrtimer_start
# hrtimer 이벤트만 추적 (고해상도 타이머)
echo 1 > events/timer/hrtimer_expire_entry/enable
echo 1 > events/timer/hrtimer_expire_exit/enable
echo 1 > tracing_on
sleep 1
echo 0 > tracing_on
cat trace | head -50
# 특정 프로세스의 타이머 이벤트만 추적
echo $PID > set_ftrace_pid
echo 1 > events/timer/enable
# hrtimer 실행 지연 히스토그램
echo 1 > events/timer/hrtimer_expire_entry/enable
echo "lat" > trace_clock
cat trace | awk '/hrtimer_expire_entry/ {print $NF}' | sort -n | uniq -c
cyclictest — 타이머 지터 측정
cyclictest는 rt-tests 패키지의 도구로, nanosleep()을 이용한 실제 타이머 지터를
마이크로초 단위로 측정합니다. 실시간 시스템 튜닝의 기준 도구입니다.
# cyclictest 설치
apt install rt-tests # Debian/Ubuntu
dnf install rt-tests # Fedora/RHEL
# 기본 테스트: 실시간 우선순위 99, 1만 회 반복, 1ms 주기
cyclictest -m -n -p99 -i1000 -l10000
# 멀티 CPU 전체 테스트 (각 CPU별 스레드)
cyclictest -m -n -p99 -i1000 -l100000 -t $(nproc)
# 출력 예시:
# T: 0 (12345) P:99 I:1000 C: 10000 Min: 3 Act: 5 Avg: 5 Max: 42
# → Min/Avg/Max 지터(us): 최소 3us, 평균 5us, 최대 42us
# 히스토그램 모드 (지터 분포 파일 저장)
cyclictest -m -n -p99 -i1000 -l1000000 -h 200 > /tmp/cyclictest.hist
# 히스토그램 출력 (지터 분포 확인)
python3 - <<'EOF'
import sys
data = open('/tmp/cyclictest.hist').readlines()
for line in data[1:20]:
us, cnt = line.split()[:2]
print(f"{int(us):4d}us: {'#' * int(cnt)}")
EOF
perf로 타이머 인터럽트 분석
# 타이머 인터럽트 통계
perf stat -e irq:irq_handler_entry/name=timer/ sleep 5
# 타이머 소프트IRQ 비율 확인
perf stat -e softirq:softirq_entry/vec=1/ sleep 5
# TIMER_SOFTIRQ = 0, NET_TX = 1 ... vec=0이 TIMER_SOFTIRQ
# CPU별 타이머 인터럽트 횟수 (실시간)
watch -n1 'awk "/LOC:/ {for(i=2;i<=NF;i++) printf \"CPU%d: %d\\n\", i-2, \$i}" /proc/interrupts'
# /proc/interrupts로 Local Timer 인터럽트 확인
grep -E "^(LOC|TIM)" /proc/interrupts
# 타이머 관련 소프트IRQ 통계
cat /proc/softirqs | grep TIMER
타이머 지터 최소화 팁:
① isolcpus= + nohz_full= 커널 파라미터로 특정 CPU를 타이머/인터럽트에서 격리(Isolation)
② irqbalance 비활성화 후 수동으로 IRQ 친화성 설정
③ PREEMPT_RT 패치(Patch) 커널 사용으로 인터럽트 핸들러를 스레드화
④ BIOS에서 C-state 제한 또는 비활성화 (intel_idle.max_cstate=1)
⑤ CPU 주파수 스케일링(Frequency Scaling) 비활성화 (cpupower frequency-set -g performance)
vDSO — 빠른 시간 읽기
vDSO(Virtual Dynamic Shared Object)는 clock_gettime(), gettimeofday() 등의 시간 관련 시스템 콜을 커널 진입 없이 사용자 공간(User Space)에서 직접 실행할 수 있게 하는 메커니즘입니다. 커널이 관리하는 vvar 페이지(Page)를 읽어 수 나노초 수준의 오버헤드로 시간을 읽습니다.
vdso_data 구조체와 vvar 페이지
커널은 struct vdso_data를 vvar 페이지에 매핑하여 유저 공간이 읽을 수 있게 합니다. 이 구조체에는 시간 변환에 필요한 모든 파라미터가 담겨 있습니다.
/* include/vdso/datapage.h */
struct vdso_timestamp {
u64 sec; /* 초 단위 기준 시각 */
u64 nsec; /* 나노초 보정값 */
};
struct vdso_data {
u32 seq; /* seqcount — 홀수이면 갱신 중 */
s32 clock_mode; /* VDSO_CLOCKMODE_TSC 등, 음수면 fallback */
u64 cycle_last; /* 마지막 업데이트 시점의 TSC/카운터 값 */
u64 mask; /* clocksource 비트마스크 */
u32 mult; /* cycle→ns 곱셈 인수 */
u32 shift; /* cycle→ns 비트 시프트 */
struct vdso_timestamp basetime[VDSO_BASES]; /* 클럭별 기준 시각 */
/* VDSO_BASES: REALTIME, MONOTONIC, MONOTONIC_RAW,
* BOOTTIME, TAI, MONOTONIC_COARSE, REALTIME_COARSE */
};
vDSO clock_gettime() fast path
/* lib/vdso/gettimeofday.c — vDSO clock_gettime 핵심 로직 (간략화) */
static int __cvdso_clock_gettime_common(
const struct vdso_data *vd, clockid_t clock,
struct __kernel_timespec *ts)
{
u32 seq;
u64 cycles, ns;
do {
seq = vdso_read_begin(vd); /* seq 읽기 (짝수 확인) */
if (vd->clock_mode == VDSO_CLOCKMODE_NONE)
return -1; /* syscall 폴백 */
cycles = __arch_get_hw_counter(vd->clock_mode);
/* rdtsc 또는 cntvct */
ns = vd->basetime[clock].nsec;
ns += (cycles - vd->cycle_last) * vd->mult;
ns >>= vd->shift;
} while (vdso_read_retry(vd, seq));
/* seq 변경 시 재시도 (커널이 갱신 중) */
ts->tv_sec = vd->basetime[clock].sec + ns / NSEC_PER_SEC;
ts->tv_nsec = ns % NSEC_PER_SEC;
return 0;
}
/* 커널 측: 매 tick마다 vdso_data 갱신 */
/* kernel/time/vsyscall.c */
void update_vsyscall(struct timekeeper *tk)
{
struct vdso_data *vdata = __arch_get_k_vdso_data();
vdso_write_begin(vdata); /* seq를 홀수로 → 갱신 중 */
vdata->cycle_last = tk->tkr_mono.cycle_last;
vdata->mult = tk->tkr_mono.mult;
vdata->shift = tk->tkr_mono.shift;
vdata->basetime[CLOCK_MONOTONIC].sec = ...;
vdata->basetime[CLOCK_MONOTONIC].nsec = ...;
/* REALTIME, BOOTTIME 등 모든 클럭 기준 갱신 */
vdso_write_end(vdata); /* seq를 짝수로 → 갱신 완료 */
}
코드 설명
vDSO의 핵심 동작 원리입니다. 유저 공간 코드가 syscall 없이 HW 카운터를 직접 읽어 시간을 계산합니다.
- seqcount 루프
vdso_read_begin()/vdso_read_retry()는 seqlock의 읽기 측입니다. 커널이 vdso_data를 갱신하는 동안(seq가 홀수) 읽은 데이터는 불일치할 수 있으므로, seq가 변경되면 처음부터 재시도합니다. 이 메커니즘은 락(Lock) 없이 일관된 데이터를 보장합니다. - __arch_get_hw_counter()아키텍처별 HW 카운터 읽기입니다. x86에서는
rdtsc명령어, ARM64에서는mrs cntvct_el0명령어를 실행합니다. 유저 공간에서 직접 실행 가능한 명령어만 사용합니다. - ns 계산현재 cycle과 마지막 업데이트 시점의 cycle 차이에 mult를 곱하고 shift만큼 오른쪽 시프트하여 나노초를 구합니다. basetime과 합산하면 최종 시각입니다.
- update_vsyscall()커널의
timekeeping_advance()가 매 tick마다 호출합니다. seqcount의 write side로, 갱신 중에는 seq를 홀수로 만들어 유저 측이 재시도하게 합니다. - clock_mode < 0현재 clocksource가 vDSO를 지원하지 않으면(예: HPET은 MMIO 접근 필요)
VDSO_CLOCKMODE_NONE이 설정되고, vDSO 함수는 -1을 반환하여 일반 syscall로 폴백합니다.
| vDSO 함수 | 대응 syscall | 지원 clockid | 폴백 조건 |
|---|---|---|---|
__vdso_clock_gettime | clock_gettime() | REALTIME, MONOTONIC, BOOTTIME, TAI, *_COARSE | clock_mode == NONE, 미지원 clockid |
__vdso_gettimeofday | gettimeofday() | REALTIME | clock_mode == NONE |
__vdso_clock_getres | clock_getres() | 모든 clockid | 없음 (항상 vDSO로 처리) |
__vdso_time | time() | REALTIME (초 단위) | 없음 |
# vDSO 활성화 확인
cat /proc/self/maps | grep vdso
# 7ffe9a5fe000-7ffe9a600000 r-xp ... [vdso]
# 7ffe9a5fc000-7ffe9a5fe000 r--p ... [vvar]
# vDSO 비활성화 (디버깅용, syscall 오버헤드 비교)
# vdso=0 커널 부트 파라미터 또는:
echo 0 > /proc/sys/abi/vsyscall32 # 32비트 호환
# 성능 벤치마크
perf stat -e 'syscalls:sys_enter_clock_gettime' -- ./my_app
# vDSO 활성 시 syscall 카운트 ≈ 0, 비활성 시 수백만
vDSO의 아키텍처별 구현 차이, 성능 벤치마크, 비활성화 시나리오 상세는 ktime / Clock — vDSO에서 자세히 다룹니다.
NO_HZ_IDLE vs NO_HZ_FULL 상세 비교
Tickless 커널의 두 가지 주요 모드는 적용 범위와 동작 특성이 크게 다릅니다. NO_HZ_IDLE은 idle CPU에서만 tick을 중단하지만, NO_HZ_FULL은 단일 태스크가 실행 중인 CPU에서도 tick을 중단하여 사용자 공간 작업에 대한 커널 간섭을 최소화합니다.
| 특성 | NO_HZ_IDLE | NO_HZ_FULL |
|---|---|---|
| CONFIG 옵션 | CONFIG_NO_HZ_IDLE |
CONFIG_NO_HZ_FULL |
| tick 중단 조건 | CPU가 idle 상태일 때 | CPU에 단일 runnable 태스크만 있을 때 |
| 부트 파라미터 | 불필요 (기본 활성) | nohz_full=1-7 (CPU 지정 필수) |
| RCU 콜백 | 해당 CPU에서 처리 | rcu_nocbs=로 오프로딩 필요 |
| housekeeping CPU | 불필요 | CPU 0 (최소 1개) 유지 필수 |
| 잔여 tick 빈도 | 0 (완전 중단) | ~1Hz (커널 유지보수용) |
| 스케줄러 통계 | idle이므로 불필요 | vtime으로 대체 (context tracking) |
| 주요 이점 | 전력 절약 (C-state 진입) | 지터 최소화 + 전력 절약 |
| 오버헤드 | 매우 낮음 | syscall 진입/탈출 시 tick 전환 비용 |
| 주요 사용 사례 | 일반 서버, 데스크톱 | HPC, 실시간, 저지연 거래 시스템 |
# NO_HZ 설정 확인
grep CONFIG_NO_HZ /boot/config-$(uname -r)
# CONFIG_NO_HZ_IDLE=y (기본: idle 시 tick 중단)
# CONFIG_NO_HZ_FULL=y (선택: 단일 태스크 시에도 중단)
# nohz_full 활성화 CPU 확인
cat /sys/devices/system/cpu/nohz_full
# 1-7 (CPU 1~7이 NO_HZ_FULL 대상)
# NO_HZ_FULL 커널 부트 파라미터 예시
# nohz_full=1-7 rcu_nocbs=1-7 isolcpus=nohz,domain,managed_irq,1-7
# → CPU 1-7: tick 중단 + RCU 오프로딩 + 스케줄링 도메인 격리
# → CPU 0: housekeeping (tick 유지, RCU 처리, IRQ 처리)
# 런타임에 tick 상태 확인
cat /proc/timer_list | grep "jiffies:"
# 각 CPU의 현재 jiffies 값과 next_expiry 확인
# NO_HZ 통계 확인
cat /proc/stat | head -1
# CPU idle 비율로 tick 절약 효과 간접 확인
타이머 정확도와 오버헤드
타이머 서브시스템의 선택은 정확도(accuracy), 지연(latency), 오버헤드(overhead) 세 축 사이의 트레이드오프입니다. 워크로드 특성에 따라 적절한 타이머 메커니즘을 선택해야 합니다.
| 메커니즘 | 시간 단위 | 최소 해상도 | 전형적 지터 | 삽입 비용 | 만료 처리 비용 | 적합한 용도 |
|---|---|---|---|---|---|---|
| jiffies (timer_list) | tick (1/HZ) | 4ms (HZ=250) | 1~10ms | O(1) | O(1) amortized | 네트워크 타임아웃, 폴링 |
| hrtimer (hard) | 나노초 | ~1us (HW 종속) | 1~50us | O(log n) | O(log n) | nanosleep, POSIX 타이머 |
| hrtimer (soft) | 나노초 | ~1us | 10~100us | O(log n) | O(log n) | 스케줄러 bandwidth |
| delayed_work | tick (1/HZ) | 4ms (HZ=250) | 1~50ms | O(1) | kworker 스케줄링 | I/O 폴링, 상태 점검 |
| udelay()/ndelay() | us/ns | ~100ns | ~0 (busy-wait) | 없음 | CPU 점유 | 하드웨어 초기화 대기 |
| usleep_range() | 마이크로초 | ~10us | 10~200us | hrtimer 삽입 | 스케줄러 호출 | 디바이스 드라이버 지연 |
hrtimer 지터 요인: hrtimer의 나노초 해상도에도 불구하고 실제 지터는 다음 요인들에 의해 증가합니다: (1) 인터럽트 비활성화 구간 (local_irq_disable), (2) 높은 우선순위(Priority) 인터럽트의 선점, (3) SMI (System Management Interrupt) — BIOS가 발생시키는 비마스크 인터럽트, (4) C-state 탈출 지연 (깊은 C-state에서 수십~수백 us), (5) CPU 주파수 전환 지연. 실시간 시스템에서는 이 요인들을 모두 제어해야 합니다.
Timer Migration 계층 구조
커널은 idle CPU의 타이머를 busy CPU로 마이그레이션하여 idle CPU가 더 오래 슬립할 수 있게 합니다. 이 메커니즘은 CONFIG_NO_HZ_COMMON에서 활성화되며, Per-CPU timer_base와 그룹 계층 구조를 통해 관리됩니다.
/* Timer migration 관련 API 및 플래그 */
/* Pinned timer: 마이그레이션 불가 */
timer_setup(&my_timer, callback, TIMER_PINNED);
/* → 이 타이머는 항상 등록된 CPU에서만 실행 */
/* 일반 timer: 마이그레이션 가능 */
timer_setup(&my_timer, callback, 0);
/* → idle 시 busy CPU로 마이그레이션 가능 */
/* Deferrable timer: idle 시 처리 보류 */
timer_setup(&my_timer, callback, TIMER_DEFERRABLE);
/* → CPU가 idle이면 처리를 미루어 슬립 시간 연장 */
/* Deferrable + Pinned: 특정 CPU에서만, idle 시 보류 */
timer_setup(&my_timer, callback, TIMER_DEFERRABLE | TIMER_PINNED);
/* 특정 CPU에 타이머 추가 */
add_timer_on(&my_timer, smp_processor_id());
/* sysctl: 마이그레이션 활성/비활성 */
/* /proc/sys/kernel/timer_migration = 1 (활성) */
/* /proc/sys/kernel/timer_migration = 0 (비활성) */
Timer Migration Group (tmigr): 커널 6.8+에서 도입된 Timer Migration 계층은 CPU를 그룹으로 묶어 마이그레이션을 효율적으로 관리합니다. 전체 CPU를 순회하는 대신, 계층적 그룹 구조에서 가장 적합한 타겟 CPU를 빠르게 선택합니다. 이는 대규모 NUMA 시스템에서 타이머 마이그레이션의 확장성을 크게 개선합니다.
타이머 마이그레이션의 NUMA 영향
NUMA(Non-Uniform Memory Access) 시스템에서 타이머 마이그레이션은 단순한 CPU 간 이동 이상의 성능 영향을 미칩니다. 타이머 콜백이 접근하는 데이터가 원래 CPU의 로컬 메모리에 있을 때, 다른 NUMA 노드의 CPU로 마이그레이션되면 원격 메모리 접근(Remote Memory Access)이 발생하여 지연이 크게 증가합니다.
| 항목 | 같은 NUMA 노드 | 다른 NUMA 노드 | 영향 |
|---|---|---|---|
| 메모리 접근 지연 | ~80ns (로컬) | ~200ns (원격) | 콜백 실행 시간 증가 |
| L3 캐시 | 공유 (히트 가능) | 비공유 (콜드 미스) | 캐시 워밍업 필요 |
| 인터커넥트 대역폭 | 사용 안 함 | QPI/UPI 대역폭 소비 | 다른 트래픽과 경합 |
| IPI 지연 | ~1µs | ~2-5µs | 마이그레이션 알림 지연 |
/* NUMA-aware 타이머 사용 패턴 */
/* ✅ Per-CPU 데이터와 함께 PINNED 타이머 사용 */
struct my_percpu_data {
struct timer_list timer;
unsigned long local_counter; /* 이 CPU의 로컬 메모리에 할당 */
};
static DEFINE_PER_CPU(struct my_percpu_data, pcpu_data);
static void percpu_timer_cb(struct timer_list *t)
{
struct my_percpu_data *data = from_timer(data, t, timer);
data->local_counter++; /* 로컬 메모리 접근 → 빠름 */
mod_timer(&data->timer, jiffies + HZ);
}
void init_percpu_timer(void)
{
int cpu;
for_each_online_cpu(cpu) {
struct my_percpu_data *data = per_cpu_ptr(&pcpu_data, cpu);
/* PINNED: 이 CPU에서만 실행, 마이그레이션 방지 */
timer_setup(&data->timer, percpu_timer_cb, TIMER_PINNED);
add_timer_on(&data->timer, cpu);
}
}
/* ❌ NUMA 비친화적 패턴: 원격 데이터를 접근하는 마이그레이션 가능 타이머 */
struct my_dev {
struct timer_list timer; /* 마이그레이션 가능 (non-pinned) */
char *big_buffer; /* Node 0에 할당된 대용량 버퍼 */
};
static void bad_timer_cb(struct timer_list *t)
{
struct my_dev *dev = from_timer(dev, t, timer);
/* 타이머가 Node 1으로 마이그레이션되면 big_buffer 접근이
* 원격 메모리 접근이 되어 지연이 2~3배 증가 */
memset(dev->big_buffer, 0, PAGE_SIZE * 256); /* 느림! */
}
코드 설명
NUMA 환경에서 타이머 마이그레이션의 영향을 최소화하는 패턴과 안티패턴입니다.
- TIMER_PINNED + DEFINE_PER_CPUPer-CPU 데이터와 함께 사용하는 타이머는 반드시
TIMER_PINNED로 고정해야 합니다. Per-CPU 변수는 해당 CPU의 NUMA 노드 로컬 메모리에 할당되므로, 다른 CPU로 마이그레이션되면 원격 메모리 접근이 발생합니다. - add_timer_on()특정 CPU에 타이머를 추가합니다.
TIMER_PINNED와 함께 사용하면 해당 CPU가 idle이 되어도 마이그레이션되지 않습니다. watchdog 타이머, Per-CPU 통계 수집 등에 적합합니다. - 원격 메모리 접근 문제마이그레이션 가능한 타이머가 대용량 버퍼를 접근하면, 원격 NUMA 노드에서 실행될 때 QPI/UPI 인터커넥트를 통한 원격 접근이 발생합니다. 특히
memset(),memcpy()같은 대량 메모리 연산에서 성능 저하가 극대화됩니다.
tmigr의 NUMA 인식: 커널 6.8+의 Timer Migration Group(tmigr)은 CPU 토폴로지를 인식하여 같은 NUMA 노드 내의 busy CPU를 우선적으로 마이그레이션 대상으로 선택합니다. 그러나 같은 노드의 모든 CPU가 idle인 경우에는 다른 노드의 CPU로 마이그레이션될 수 있습니다. 대규모 NUMA 시스템(4소켓 이상)에서는 timer_migration=0으로 마이그레이션을 비활성화하고 각 CPU가 자체 타이머를 처리하도록 하는 것이 지연 측면에서 유리할 수 있습니다. 다만 이 경우 idle CPU가 타이머 만료마다 깨어나므로 전력 소모가 증가합니다.
타이머 디버깅
/proc/timer_stats (레거시)
/proc/timer_stats는 커널 4.10 이전에 제공되던 타이머 통계 인터페이스입니다. 활성화하면 어떤 프로세스가 어떤 타이머를 몇 번 발생시켰는지 추적합니다. 커널 4.11+에서는 CONFIG_TIMER_STATS가 제거되었으므로 ftrace 기반 대안을 사용해야 합니다.
# (커널 4.10 이하) /proc/timer_stats 사용
echo 1 > /proc/timer_stats # 수집 시작
sleep 10 # 10초간 수집
echo 0 > /proc/timer_stats # 수집 중지
cat /proc/timer_stats
# 출력 예시:
# 1234, 5, my_module my_timer_callback (my_start_fn)
# → PID 1234가 my_timer_callback을 5번 트리거
# (커널 4.11+) ftrace 대안: timer_start/timer_expire 추적
cd /sys/kernel/debug/tracing
echo 1 > events/timer/timer_start/enable
echo 1 > events/timer/timer_expire_entry/enable
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace | grep -E "(timer_start|timer_expire)" | head -30
# 타이머별 발생 횟수 집계
cat trace | grep timer_start | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
ftrace로 hrtimer 지연 측정
# hrtimer 만료 지연 측정 (예정 시각 vs 실제 실행 시각)
cd /sys/kernel/debug/tracing
# function_graph tracer로 hrtimer_interrupt 실행 시간 측정
echo function_graph > current_tracer
echo hrtimer_interrupt > set_graph_function
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on
cat trace | head -50
# 출력 예시:
# 0) | hrtimer_interrupt() {
# 0) 0.234 us | __hrtimer_get_next_event();
# 0) | __run_hrtimer() {
# 0) 0.567 us | tick_sched_timer();
# 0) 1.123 us | }
# 0) 2.345 us | }
# trace_printk으로 커널 모듈에서 직접 지연 측정
# ktime_t start = ktime_get();
# /* ... 작업 ... */
# s64 delta_ns = ktime_to_ns(ktime_sub(ktime_get(), start));
# trace_printk("timer latency: %lld ns\n", delta_ns);
PowerTOP으로 불필요한 타이머 찾기
# PowerTOP: 불필요한 wakeup을 유발하는 타이머 식별
powertop --time=20
# "Timer Stats" 탭에서 초당 wakeup 횟수별로 정렬
# 높은 빈도의 타이머가 전력 소모의 주범
# PowerTOP CSV 리포트 생성
powertop --csv=report.csv --time=30
# turbostat: CPU C-state 거주 시간과 타이머의 상관관계
turbostat --interval 5
# C1/C3/C6 비율이 낮으면 타이머가 깊은 C-state 진입을 방해
일반적인 타이머 실수와 주의사항
타이머 서브시스템은 동시성, 컨텍스트 제약, 메모리 관리(Memory Management)와 밀접하게 관련되어 있어 다양한 실수가 발생합니다. 아래는 실무에서 자주 발생하는 버그 패턴과 올바른 사용법입니다.
Use-After-Free 패턴
/* ❌ 잘못된 예: 구조체 해제 후 타이머가 남아 있음 */
struct my_dev {
struct timer_list timer;
int data;
};
void remove_device(struct my_dev *dev)
{
del_timer(&dev->timer); /* ❌ 비동기: 콜백이 이미 실행 중일 수 있음 */
kfree(dev); /* ❌ 콜백이 dev->data에 접근하면 UAF! */
}
/* ✅ 올바른 예: 동기적 취소 후 해제 */
void remove_device(struct my_dev *dev)
{
del_timer_sync(&dev->timer); /* ✅ 콜백 실행 완료까지 대기 */
kfree(dev); /* ✅ 안전하게 해제 */
}
/* hrtimer도 동일한 원칙 */
hrtimer_cancel(&dev->hrtimer); /* ✅ 동기적 취소 */
kfree(dev);
컨텍스트 혼동
/* ❌ timer_list 콜백에서 슬립 시도 */
static void bad_timer_cb(struct timer_list *t)
{
msleep(100); /* ❌ softirq 컨텍스트에서 슬립 불가! */
mutex_lock(&my_mutex); /* ❌ 슬립 가능 잠금 불가! */
}
/* ✅ 슬립이 필요하면 delayed_work 사용 */
static void good_work_handler(struct work_struct *work)
{
msleep(100); /* ✅ 프로세스 컨텍스트에서 가능 */
mutex_lock(&my_mutex); /* ✅ 가능 */
mutex_unlock(&my_mutex);
}
timer_list vs hrtimer 선택 오류
| 요구사항 | 권장 API | 잘못된 선택 | 이유 |
|---|---|---|---|
| 네트워크 타임아웃 (1초) | timer_list |
hrtimer | ms 정확도 충분, hrtimer는 불필요한 오버헤드 |
| POSIX nanosleep 구현 | hrtimer |
timer_list | 나노초 정밀도 필요, 4ms 해상도 부족 |
| 디바이스 폴링 (100ms) | delayed_work |
timer_list | 폴링에서 I/O/잠금 필요 시 프로세스 컨텍스트 필수 |
| 하드웨어 레지스터 대기 | udelay() / readl_poll_timeout() |
timer_list / hrtimer | 마이크로초 단위 busy-wait가 적절 |
| 대량 타임아웃 (수천 개) | timer_list |
hrtimer | O(1) 삽입 vs O(log n), 메모리 효율 |
| 스케줄러 bandwidth 제어 | hrtimer (soft) |
timer_list | ms 이하 정밀도 필요, 소프트 모드로 오버헤드 절감 |
기타 주의사항
- jiffies 랩어라운드 — 32비트 시스템에서 jiffies는 약 497일(HZ=100) 후 오버플로. 반드시
time_after(),time_before()매크로를 사용하여 비교해야 합니다. 직접 비교(jiffies > timeout)는 버그의 원인입니다. - mod_timer() 재진입 —
mod_timer()는 동일 타이머가 pending이면 취소 후 재등록합니다. 콜백 내에서mod_timer()를 호출하는 것은 안전합니다 (주기적 타이머 패턴). - del_timer_sync()와 데드락 — 타이머 콜백 자체에서
del_timer_sync()를 호출하면 자기 자신을 대기하므로 데드락. 콜백에서는del_timer()만 사용하거나, 별도 플래그로 재등록을 방지하세요. - 스택 기반 타이머 —
timer_setup_on_stack()으로 설정한 타이머는 함수 반환 전에destroy_timer_on_stack()을 호출해야 합니다. CONFIG_DEBUG_OBJECTS가 이를 검사합니다. - 모듈 언로드 순서 — 모듈의
exit함수에서 모든 타이머를del_timer_sync()/hrtimer_cancel()로 취소한 후에 구조체(Struct)를 해제해야 합니다. - TIMER_IRQSAFE — timer_list에
TIMER_IRQSAFE플래그를 설정하면 콜백이 hard IRQ 컨텍스트에서도 안전하게 실행됩니다. 이 플래그 없이는 softirq에서만 실행됩니다.
타이머 콜백 재진입(Reentrancy) 문제
SMP 환경에서 주기적 타이머의 콜백 재진입은 미묘한 동시성 버그를 유발합니다. mod_timer()로 주기적 타이머를 구현할 때, 이전 콜백이 아직 다른 CPU에서 실행 중인 상태에서 새 콜백이 시작되는 시나리오를 고려해야 합니다.
/* ❌ 위험: 콜백 재진입 가능한 주기적 타이머 */
static struct timer_list my_timer;
static int shared_counter = 0; /* 공유 데이터: 동시 접근 가능 */
static void risky_callback(struct timer_list *t)
{
/* 문제: 이 콜백이 CPU 0에서 실행 중일 때,
* mod_timer()가 다음 만료를 짧게 설정하면
* CPU 1에서 동일 콜백이 동시에 실행될 수 있습니다.
*
* timer_list 콜백은 재진입 안전하지 않습니다:
* - 단일 타이머의 동시 실행은 발생하지 않음 (하나의 timer_list는
* 한 번에 한 CPU에서만 pending)
* - 그러나 콜백 내에서 mod_timer()를 호출하면,
* 현재 콜백 실행이 끝나기 전에 타이머가 재등록되어
* 다른 CPU의 다음 softirq에서 실행될 수 있습니다 */
shared_counter++; /* ❌ 원자적이지 않은 연산 */
mod_timer(&my_timer, jiffies + 1); /* 1 tick 후 재등록 */
/* mod_timer() 후 즉시 반환, 아래 코드 실행 중
* 다른 CPU에서 이미 새 콜백이 시작될 수 있음 */
do_slow_work(); /* 시간이 오래 걸리는 작업 */
}
/* ✅ 안전: 콜백 완료 후 재등록 */
static void safe_callback(struct timer_list *t)
{
spin_lock(&my_lock);
shared_counter++;
spin_unlock(&my_lock);
do_slow_work();
/* ✅ 모든 작업 완료 후 마지막에 재등록 */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(100));
}
/* ✅ 더 안전: timer_shutdown()으로 재등록 방지 (커널 6.2+) */
static bool shutting_down = false;
static void shutdown_safe_callback(struct timer_list *t)
{
if (shutting_down)
return; /* 재등록하지 않음 */
/* ... 작업 수행 ... */
mod_timer(&my_timer, jiffies + HZ);
}
void cleanup(void)
{
shutting_down = true;
timer_shutdown_sync(&my_timer); /* ✅ 영구 비활성화 */
/* timer_shutdown_sync() 이후에는 mod_timer()가 무시됨 */
}
코드 설명
타이머 콜백의 재진입 문제와 안전한 주기적 타이머 패턴입니다.
- 단일 타이머 동시 실행 불가하나의
timer_list구조체는 Timer Wheel에 한 번만 등록될 수 있으며,expire_timers()에서 제거 후 콜백을 실행합니다. 따라서 동일 타이머의 콜백이 두 CPU에서 동시 실행되는 상황은 기본적으로 발생하지 않습니다. - mod_timer() 후 재진입 가능성콜백 내에서
mod_timer()를 호출하면 타이머가 다시 Timer Wheel에 등록됩니다. 만약 만료 시간이 매우 짧으면(1 tick), 현재 콜백이 끝나기 전에 다음 softirq 사이클에서 새로운 만료가 발생할 수 있습니다. 다만base->running_timer체크로 동일 타이머의 동시 실행은 방지됩니다. - 공유 데이터 보호콜백이 공유 데이터를 접근하면 적절한 동기화(spinlock, atomic 연산)가 필요합니다. softirq 컨텍스트에서 실행되므로
spin_lock()(BH 안전 버전 불필요)을 사용할 수 있습니다. - timer_shutdown_sync()커널 6.2+에서 제공하는 API로, 타이머를 영구적으로 비활성화합니다. 이후
mod_timer()호출은 아무 효과가 없습니다. 모듈 해제 시 콜백 내mod_timer()재등록을 완벽하게 방지합니다.
타이머 관련 데드락 패턴
/* ❌ 데드락 패턴 1: 콜백에서 del_timer_sync() 호출 */
static void deadlock_callback(struct timer_list *t)
{
struct my_dev *dev = from_timer(dev, t, timer);
/* del_timer_sync()는 콜백 완료를 대기하지만,
* 우리가 바로 그 콜백 안에 있으므로 영원히 대기 → 데드락! */
del_timer_sync(&dev->other_timer); /* ❌ 다른 타이머라도 문제 가능 */
/* 같은 CPU의 같은 base lock을 획득하려고 하면 데드락 */
}
/* ✅ 올바른 패턴: 콜백에서는 del_timer()만 사용 */
static void safe_callback_v2(struct timer_list *t)
{
struct my_dev *dev = from_timer(dev, t, timer);
del_timer(&dev->other_timer); /* ✅ 비동기 삭제 (대기 없음) */
}
/* ❌ 데드락 패턴 2: lock 순서 역전 */
static DEFINE_SPINLOCK(my_lock);
static struct timer_list my_timer;
/* 경로 A: 프로세스 컨텍스트 */
void process_path(void)
{
spin_lock_bh(&my_lock); /* (1) my_lock 획득 */
del_timer_sync(&my_timer); /* (2) base->lock 획득 시도
* → timer_base lock 대기 */
spin_unlock_bh(&my_lock);
}
/* 경로 B: 타이머 콜백 (softirq) */
static void timer_callback(struct timer_list *t)
{
/* __run_timers()가 이미 base->lock 보유 상태에서 콜백 실행 */
spin_lock(&my_lock); /* (3) my_lock 획득 시도
* 경로 A가 my_lock 보유 + base->lock 대기
* 경로 B가 base->lock 보유 + my_lock 대기
* → ABBA 데드락! */
spin_unlock(&my_lock);
}
/* ✅ 올바른 해결: lock 순서 통일 또는 try_to_del_timer_sync() 사용 */
void safe_process_path(void)
{
del_timer_sync(&my_timer); /* ✅ lock 없이 먼저 타이머 취소 */
spin_lock_bh(&my_lock);
/* ... 안전하게 작업 ... */
spin_unlock_bh(&my_lock);
}
코드 설명
타이머 서브시스템에서 발생하는 대표적인 데드락 패턴과 해결 방법입니다.
- 데드락 패턴 1
del_timer_sync()는base->running_timer가 대상 타이머를 가리키면 콜백 완료까지 spin-wait합니다. 콜백 자체에서 호출하면 자기 완료를 대기하므로 무한 루프에 빠집니다. 다른 타이머의del_timer_sync()도 같은timer_base의 lock을 필요로 하므로 동일한 데드락이 발생할 수 있습니다. - 데드락 패턴 2 (ABBA)프로세스 컨텍스트에서
my_lock → base->lock순서로, 타이머 콜백에서base->lock → my_lock순서로 lock을 획득하면 ABBA 데드락이 발생합니다.expire_timers()는 콜백 호출 전에base->lock을 해제하지만,del_timer_sync()의 내부 spin-wait 로직이 간접적으로 이 데드락을 유발할 수 있습니다. - 해결: lock 순서 통일lock과
del_timer_sync()를 동시에 사용해야 하면, 항상del_timer_sync()를 lock 바깥에서 먼저 호출합니다. 타이머가 완전히 취소된 후 lock을 획득하면 ABBA 데드락을 방지할 수 있습니다.
PREEMPT_RT 환경의 타이머 동작 차이
PREEMPT_RT(실시간) 커널에서는 타이머 서브시스템의 동작이 상당히 달라집니다. softirq가 스레드화되고, spinlock이 mutex로 교체되며, hrtimer의 hard/soft 모드 구분이 중요해집니다. 실시간 시스템에서 타이머를 사용할 때 이러한 차이를 이해해야 예측 가능한 지연 시간을 달성할 수 있습니다.
| 항목 | 일반 커널 (PREEMPT_VOLUNTARY) | PREEMPT_RT 커널 |
|---|---|---|
| timer_list 콜백 실행 | TIMER_SOFTIRQ (softirq 컨텍스트) | ksoftirqd/N 스레드 (프로세스 컨텍스트) |
| hrtimer 콜백 (기본) | hard IRQ 컨텍스트 | softirq 스레드 (HRTIMER_MODE_SOFT 기본) |
| hrtimer hard 모드 | 별도 플래그 불필요 | HRTIMER_MODE_HARD 명시 필요 |
| timer_base lock | raw_spinlock_t (IRQ off spinning) |
raw_spinlock_t (RT에서도 spinning 유지) |
| softirq 처리 | IRQ 리턴 시 인라인 실행 | ksoftirqd/N 커널 스레드에서 실행 |
| softlockup 위험 | spinlock 장기 보유 시 발생 가능 | spinlock이 mutex로 교체되어 위험 감소 |
| 타이머 지연 결정성 | 가변적 (softirq 부하 의존) | 예측 가능 (우선순위 기반 스케줄링) |
/* PREEMPT_RT 환경에서의 hrtimer 모드 선택 */
/* 1. 기본 모드 — RT에서는 softirq 스레드에서 실행됨 */
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
/* → RT 커널: 콜백이 ksoftirqd/N에서 실행
* → 다른 RT 태스크에 의해 선점(Preemption) 가능
* → 대부분의 드라이버 타이머에 적합 */
/* 2. Hard 모드 — RT에서도 hard IRQ 컨텍스트에서 실행 필요 시 */
hrtimer_init(&timer, CLOCK_MONOTONIC,
HRTIMER_MODE_REL | HRTIMER_MODE_HARD);
/* → RT 커널: 콜백이 실제 hard IRQ 컨텍스트에서 실행
* → 선점 불가 — 최소 지연 보장
* → 사용 사례: watchdog, NMI 관련, 시간 동기화
* → 주의: 콜백에서 mutex/sleeping 절대 금지 */
/* 3. Soft 모드 명시 — 일반 커널에서도 softirq 스레드 실행 */
hrtimer_init(&timer, CLOCK_MONOTONIC,
HRTIMER_MODE_REL | HRTIMER_MODE_SOFT);
/* → 일반/RT 모두: softirq 컨텍스트에서 실행
* → RT에서 ksoftirqd 스레드의 우선순위로 스케줄링됨
* → 정밀도가 약간 감소하나 시스템 응답성 향상 */
코드 설명
PREEMPT_RT 커널에서 hrtimer 모드 선택 시 주의사항입니다.
- HRTIMER_MODE_HARDRT 커널에서 진짜 hard IRQ 컨텍스트 실행이 필요할 때만 사용합니다. watchdog(
watchdog_timer_fn), 스케줄러 tick(sched_timer), NTP 보정처럼 선점되면 안 되는 경우에 해당합니다. 콜백 내에서raw_spinlock만 사용 가능하며,mutex,kmalloc(GFP_KERNEL), sleep 계열 함수는 사용할 수 없습니다. - HRTIMER_MODE_SOFTRT 커널에서 기본 hrtimer 모드입니다. 콜백이
ksoftirqd스레드에서 실행되므로 다른 RT 태스크에 의해 선점될 수 있습니다. 이로 인해 타이머 콜백의 지연이 약간 증가하지만, 고우선순위 RT 태스크의 응답성이 보장됩니다. - timer_list와 RTtimer_list의
timer_base->lock은raw_spinlock_t이므로 RT에서도 spinning입니다. 하지만 TIMER_SOFTIRQ 자체가ksoftirqd스레드에서 실행되므로, 타이머 콜백의 실행 컨텍스트는 프로세스 컨텍스트가 됩니다.
- 대부분의 드라이버 — 기본 모드(SOFT)를 사용하세요. RT에서 자동으로 softirq 스레드에서 실행됩니다.
- HARD 모드 필요 조건 — 타이머 콜백이 선점되면 시스템 안정성에 영향을 미치는 경우에만 사용합니다(watchdog, scheduling tick 등).
- del_timer_sync() 주의 — RT에서
del_timer_sync()는 타이머 콜백이ksoftirqd에서 완료될 때까지 대기합니다. 콜백이 호출자보다 낮은 우선순위라면 우선순위 역전(Priority Inversion)이 발생할 수 있습니다. - cyclictest로 검증 — RT 환경에서 타이머 지터를
cyclictest --mlockall -t -p99로 측정하여 요구사항을 충족하는지 확인하세요.
NO_HZ_FULL 내부 동작
NO_HZ_FULL은 단순 절전 기능이 아니라 격리 CPU의 주기 tick 제거를 통해 지터를 줄이는 메커니즘입니다. 다만 tick을 끄기 위해서는 RCU, timer, workqueue, unbound kthread를 housekeeping CPU로 오프로딩해야 하며, 그렇지 않으면 예상치 못한 인터럽트로 지터가 증가합니다.
# NO_HZ_FULL 실무 설정 예시
GRUB_CMDLINE_LINUX="nohz_full=1-7 rcu_nocbs=1-7 isolcpus=domain,managed_irq,1-7 irqaffinity=0"
# 부팅 후 확인
grep NO_HZ /boot/config-$(uname -r)
cat /sys/devices/system/cpu/nohz_full
cat /proc/cmdline
# 인터럽트가 CPU0(housekeeping)로 몰렸는지 확인
watch -n1 'grep -E "LOC|RES|CAL|TLB|NET_RX" /proc/interrupts'
Timer Slack과 Coalescing
타이머 정확도를 조금 양보하면 wakeup 횟수를 크게 줄일 수 있습니다. 커널은 timer slack으로 타이머 만료를 근접 시점에 묶어(coalescing) 전력 효율을 높입니다.
/* timer slack API: 정확도 대신 wakeup 절감 */
#include <linux/sched.h>
#include <linux/timer.h>
/* 현재 태스크의 slack 설정 (나노초) */
current->timer_slack_ns = 20 * 1000 * 1000; /* 20ms */
/* hrtimer 범위 예약: [expires, expires+delta] 범위 내 만료 허용 */
hrtimer_start_range_ns(&timer,
ms_to_ktime(100), /* 목표 만료 */
20 * 1000 * 1000, /* 허용 오차 */
HRTIMER_MODE_REL);
/* 유저 공간에서는 prctl(PR_SET_TIMERSLACK, ns) 사용 */
timekeeping과 NTP 보정 경로
타이머 정확도는 단순히 하드웨어 클럭 성능만으로 결정되지 않습니다. timekeeping 서브시스템은 clocksource 카운터를 ns로 변환하고, NTP PLL/FLL 보정을 적용해 장기 오차를 줄입니다.
/* 시간 읽기와 변환 흐름 요약 */
#include <linux/timekeeping.h>
/* monotonic 시간 읽기 */
ktime_t now = ktime_get();
/* ns 단위 */
u64 ns = ktime_to_ns(now);
/* realtime 읽기 (NTP/관리자 설정 반영) */
struct timespec64 ts;
ktime_get_real_ts64(&ts);
/* boottime: suspend 기간 포함 */
ktime_get_boottime_ts64(&ts);
타이머 트러블슈팅 플레이북
지연/지터 문제는 "타이머가 느린가"보다 "어떤 경로에서 늦어지는가"를 분리해야 해결됩니다. 아래 순서로 계측하면 원인을 빠르게 좁힐 수 있습니다.
# 1) 타이머/인터럽트 기본 상태
cat /proc/timer_list | head -80
grep -E "LOC|RES|CAL|TLB|TIMER" /proc/interrupts
# 2) 지터 측정
cyclictest -m -n -p99 -i 1000 -l 200000
# 3) timer/hrtimer tracepoint
echo 1 > /sys/kernel/debug/tracing/events/timer/timer_start/enable
echo 1 > /sys/kernel/debug/tracing/events/timer/hrtimer_start/enable
echo 1 > /sys/kernel/debug/tracing/events/timer/hrtimer_expire_entry/enable
sleep 3
cat /sys/kernel/debug/tracing/trace | tail -n 120
# 4) 전력/웨이크업 확인
powertop --time=10 --html
hrtimer backlog 제어와 우선순위 역전(Priority Inversion)
지연 문제에서 자주 놓치는 부분은 "타이머 만료 시각"보다 "콜백이 실제 실행되는 순서"입니다. softirq backlog가 길거나 callback 내부에서 긴 작업을 수행하면, 높은 중요도의 hrtimer도 후순위로 밀릴 수 있습니다.
/* 안티패턴: hrtimer callback에서 장시간 처리 */
enum hrtimer_restart bad_timer_fn(struct hrtimer *t)
{
/* 금지: 긴 루프/슬립/복잡한 락 경합 */
do_heavy_work();
return HRTIMER_RESTART;
}
/* 권장: 최소 작업 후 deferred 처리 */
enum hrtimer_restart good_timer_fn(struct hrtimer *t)
{
queue_work(system_unbound_wq, &ctx->work);
hrtimer_forward_now(t, interval);
return HRTIMER_RESTART;
}
운영 정책: 지연 목표와 전력 목표의 균형
타이머 튜닝은 "최저 지연"과 "최저 전력"을 동시에 만족시키기 어렵습니다. 서비스 SLO를 먼저 정하고, 타이머 정책을 등급별로 분리 운영하면 회귀를 줄일 수 있습니다.
| 서비스 등급 | 권장 타이머 정책 | 중점 지표 |
|---|---|---|
| 초저지연 (RT/제어) | hrtimer 중심, slack 최소화, 전용 CPU | p99/p999 latency, irq off time |
| 일반 서버 | timer coalescing 적극 사용, NO_HZ_IDLE | throughput/wakeup/s |
| 배치/백그라운드 | slack 확대, delayed_work 병합 | 전력/열/총 처리량(Throughput) |
Tick Broadcast와 Deep Idle 복귀 지연
tickless 시스템에서 일부 CPU가 deep idle(C-state 깊은 단계)로 내려가면, 로컬 timer event 대신 broadcast 장치를 통한 깨움 경로가 사용됩니다. 이 경로는 전력 효율에는 유리하지만, 특정 패턴에서 깨움 지연 편차를 키울 수 있습니다.
# tick/nohz/idle 상태 확인
cat /proc/timer_list | grep -E 'broadcast|tick|expires_next' -n
cat /sys/devices/system/cpu/cpuidle/current_driver
cat /sys/devices/system/cpu/cpuidle/current_governor_ro
# 지연 측정과 함께 C-state 영향 비교
cyclictest -m -n -p99 -i 1000 -l 200000
powertop --time=10 --html
timer_list API 함수 상세 레퍼런스
이 섹션에서는 커널 저해상도 타이머(struct timer_list) API의 모든 함수를 상세히 다룹니다. 각 함수의 내부 동작, 반환값, 사용 시 주의사항을 포함합니다.
timer_setup() 상세
timer_setup()은 struct timer_list를 초기화하는 현재 표준 API입니다. 커널 4.15에서 기존 init_timer()와 setup_timer()를 대체하며 도입되었습니다. 새로운 콜백 시그니처(struct timer_list * 인자)를 사용하여 container_of() 패턴을 강제합니다.
/* 함수 시그니처 */
void timer_setup(struct timer_list *timer,
void (*callback)(struct timer_list *),
unsigned int flags);
사용 가능한 플래그(Flags):
0— 기본값. 표준 타이머 동작.TIMER_DEFERRABLE— 유휴(Idle) 상태에서 지연 허용. 전력 절약에 유리.TIMER_PINNED— 현재 CPU에 고정. 다른 CPU로 마이그레이션(Migration) 금지.TIMER_IRQSAFE— 하드 IRQ 컨텍스트에서 안전. 내부적으로raw_spinlock사용.
/* 기본 사용 예제 */
#include <linux/timer.h>
struct my_device {
struct timer_list poll_timer;
void __iomem *regs;
int status;
};
static void my_timer_callback(struct timer_list *t)
{
struct my_device *dev = from_timer(dev, t, poll_timer);
dev->status = readl(dev->regs + STATUS_REG);
if (dev->status & NEED_POLL)
mod_timer(&dev->poll_timer, jiffies + msecs_to_jiffies(100));
}
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
timer_setup(&dev->poll_timer, my_timer_callback, 0);
mod_timer(&dev->poll_timer, jiffies + msecs_to_jiffies(100));
return 0;
}
add_timer() / mod_timer() 상세
add_timer()는 mod_timer(timer, timer->expires)의 편의 래퍼(Convenience Wrapper)입니다. 실질적으로 모든 타이머 등록/변경은 mod_timer()를 통해 이루어집니다.
/* 함수 시그니처 */
int mod_timer(struct timer_list *timer, unsigned long expires);
int mod_timer_pending(struct timer_list *timer, unsigned long expires);
int timer_reduce(struct timer_list *timer, unsigned long expires);
void add_timer(struct timer_list *timer);
mod_timer()의 내부 동작은 타이머의 현재 상태에 따라 분기합니다:
- 타이머가 대기 중이 아닌 경우: 타이머 휠(Timer Wheel)에 새로 등록(Enqueue)합니다.
- 타이머가 대기 중이고 expires가 동일한 경우: 아무 작업도 하지 않습니다(최적화).
- 타이머가 대기 중이고 expires가 다른 경우: 기존 위치에서 제거(Dequeue) 후 새 위치에 재등록합니다.
- 반환값: 타이머가 이전에 대기 중이었으면
1, 아니면0
관련 변형 함수:
mod_timer_pending(timer, expires): 타이머가 대기 중인 경우에만 변경합니다. 대기 중이 아니면0을 반환하고 아무 작업도 하지 않습니다.timer_reduce(timer, expires): 만료 시각을 앞당기기만 합니다(뒤로 미루지 않음). 데드라인 레이싱(Deadline Racing) 패턴에 유용합니다.
/* 주기적 타이머 패턴 */
static void periodic_callback(struct timer_list *t)
{
struct my_device *dev = from_timer(dev, t, my_timer);
do_periodic_work(dev);
/* 다음 주기 등록 — mod_timer가 re-enqueue 처리 */
mod_timer(&dev->my_timer, jiffies + HZ);
}
/* timer_reduce: 데드라인 레이싱 패턴 */
static void new_request(struct my_device *dev, unsigned long deadline)
{
/* 현재 설정된 만료보다 이른 경우에만 갱신 */
timer_reduce(&dev->timeout_timer, deadline);
}
del_timer() / del_timer_sync() / try_to_del_timer_sync() 비교
타이머 취소 함수는 사용 컨텍스트에 따라 올바르게 선택해야 합니다. 잘못된 함수 사용은 교착 상태(Deadlock)나 use-after-free를 유발합니다.
| 함수 | 동작 | 반환값 | 콜백 실행 중일 때 | 사용 가능 컨텍스트 |
|---|---|---|---|---|
del_timer() |
비동기 취소 | 1: 대기 중이었음, 0: 아님 | 기다리지 않음 (콜백 계속 실행) | 모든 컨텍스트 |
del_timer_sync() |
동기 취소 | 1: 대기 중이었음, 0: 아님 | 콜백 완료까지 대기 | 프로세스 컨텍스트 (IRQ 비활성 금지) |
try_to_del_timer_sync() |
비차단 동기 시도 | -1: 콜백 실행 중, 0: 대기 아님, 1: 취소 성공 | 실패 반환 (-1) | 모든 컨텍스트 |
/* 안전한 모듈 정리 패턴 */
static void my_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
/* 동기 취소: 콜백 완료까지 대기 */
del_timer_sync(&dev->poll_timer);
/* 이 시점에서 타이머가 절대 실행 중이 아님을 보장 */
kfree(dev);
}
timer_pending() / timer_shutdown() / timer_shutdown_sync()
timer_pending()은 타이머가 휠에 등록되어 있는지 확인합니다. 스레드 안전(Thread-Safe)하지만 반환 직후 상태가 변경될 수 있으므로 참고용으로만 사용합니다.
커널 6.2에서 도입된 timer_shutdown() 계열은 타이머를 비활성화하고 재등록을 영구적으로 방지합니다. 콜백을 NULL로 설정하므로 이후 mod_timer() 호출 시 경고가 발생합니다.
/* 커널 6.2+ shutdown API */
int timer_shutdown(struct timer_list *timer); /* 비동기 */
int timer_shutdown_sync(struct timer_list *timer); /* 동기 */
/* 모듈 exit에서 권장 패턴 (6.2+) */
static void my_exit(void)
{
/* shutdown_sync: 콜백 완료 대기 + 재등록 방지 */
timer_shutdown_sync(&my_timer);
/* 이 시점 이후 mod_timer()는 WARN 발생 */
}
/* 상태 확인 */
if (timer_pending(&dev->timer))
pr_info("timer is queued in wheel\n");
timer_setup_on_stack() / destroy_timer_on_stack()
스택 할당(Stack-Allocated) 타이머를 위한 API입니다. CONFIG_DEBUG_OBJECTS가 활성화된 경우 디버그 오브젝트 추적기(Debug Object Tracker)가 이를 관리합니다. 함수 반환 전에 반드시 destroy_timer_on_stack()을 호출해야 합니다.
/* 스택 타이머 사용 패턴 */
static void wait_with_timeout(unsigned long timeout_jiffies)
{
struct timer_list timeout_timer;
timer_setup_on_stack(&timeout_timer, timeout_handler, 0);
mod_timer(&timeout_timer, jiffies + timeout_jiffies);
/* ... 대기 로직 ... */
wait_event(wq, condition || timer_expired);
del_timer_sync(&timeout_timer);
destroy_timer_on_stack(&timeout_timer); /* 반드시 호출! */
}
타이머 플래그 상세
| 플래그 | 값 | 효과 | 사용 사례 |
|---|---|---|---|
TIMER_DEFERRABLE |
0x00080000 |
유휴 시 처리 지연. BASE_DEF 베이스에 등록. tick 생략 가능. |
캐시 정리, 통계 수집 등 정밀도 불필요 작업 |
TIMER_PINNED |
0x00100000 |
CPU 마이그레이션 금지. 현재 CPU에 고정. | per-CPU 자원 접근, 캐시 친화 작업 |
TIMER_IRQSAFE |
0x00200000 |
콜백을 하드 IRQ 컨텍스트에서 안전하게 실행. raw_spinlock 사용. |
IRQ 핸들러에서 타이머 조작 필요 시 |
hrtimer API 함수 상세 레퍼런스
고해상도 타이머(High-Resolution Timer, hrtimer)는 나노초(Nanosecond) 단위 정밀도를 제공하며, 레드-블랙 트리(Red-Black Tree) 기반으로 관리됩니다. 이 섹션에서는 hrtimer의 모든 주요 API를 상세히 다룹니다.
hrtimer_init() 상세
void hrtimer_init(struct hrtimer *timer,
clockid_t clock_id,
enum hrtimer_mode mode);
사용 가능한 클록 ID(Clock ID):
CLOCK_MONOTONIC— 부팅 이후 단조 증가. 절전(Suspend) 시간 제외. 가장 일반적.CLOCK_REALTIME— 실제 시각(Wall Clock). NTP 조정 영향 받음. 사용자 공간 인터페이스용.CLOCK_BOOTTIME— MONOTONIC + 절전 시간 포함. 하드웨어 타임아웃에 적합.CLOCK_TAI— 국제원자시(International Atomic Time). 윤초(Leap Second) 영향 없음.
모드(Mode) 플래그:
HRTIMER_MODE_ABS— 절대 시각(Absolute Time) 기준HRTIMER_MODE_REL— 상대 시간(Relative Time) 기준 (내부에서 절대 시각으로 변환)HRTIMER_MODE_PINNED— 현재 CPU 고정HRTIMER_MODE_SOFT— softirq 컨텍스트에서 실행 (5.4+)HRTIMER_MODE_HARD— PREEMPT_RT에서도 hardirq 강제- 조합 가능:
HRTIMER_MODE_REL_PINNED_SOFT등
hrtimer_start() / hrtimer_start_range_ns() 상세
void hrtimer_start(struct hrtimer *timer, ktime_t tim,
const enum hrtimer_mode mode);
void hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
u64 range_ns,
const enum hrtimer_mode mode);
range_ns 파라미터는 허용 슬랙(Slack)을 나노초로 지정합니다. 0이면 정확한 시점에 만료되고, 양수이면 해당 범위 내에서 다른 타이머와 통합(Coalescing)할 수 있어 전력 소비를 줄입니다. hrtimer_start()는 range_ns = 0으로 호출하는 래퍼입니다.
내부 흐름:
- 이미 등록되어 있으면 RB-트리에서 제거
- REL 모드이면 현재 시각을 더해 절대 시각으로 변환
_softexpires = tim,node.expires = tim + range_ns설정enqueue_hrtimer()로 RB-트리에 삽입- 새 타이머가 가장 왼쪽(Leftmost) 노드이면
hrtimer_reprogram()→tick_program_event()로 클록이벤트 재프로그래밍
/* 원샷(One-shot) hrtimer */
static enum hrtimer_restart my_hrt_callback(struct hrtimer *timer)
{
struct my_device *dev = container_of(timer, struct my_device, hr_timer);
do_work(dev);
return HRTIMER_NORESTART; /* 반복 안 함 */
}
/* 주기적(Periodic) hrtimer */
static enum hrtimer_restart periodic_callback(struct hrtimer *timer)
{
struct my_device *dev = container_of(timer, struct my_device, hr_timer);
do_periodic_work(dev);
hrtimer_forward_now(timer, ms_to_ktime(10));
return HRTIMER_RESTART;
}
/* range 기반 — 1ms ± 100us 슬랙 허용 */
hrtimer_start_range_ns(&dev->timer, ms_to_ktime(1),
100 * NSEC_PER_USEC,
HRTIMER_MODE_REL);
hrtimer_cancel() / hrtimer_try_to_cancel() 상세
int hrtimer_cancel(struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);
hrtimer_cancel()은 콜백이 완료될 때까지 차단(Block)합니다. 내부적으로 hrtimer_try_to_cancel()을 반복 호출하며, 반환값이 -1(콜백 실행 중)인 동안 cpu_relax()로 스핀(Spin)합니다.
| 함수 | 차단 여부 | 반환값 | 용도 |
|---|---|---|---|
hrtimer_cancel() |
차단 (콜백 완료 대기) | 1: 활성이었음, 0: 비활성 | 모듈 제거, 안전한 정리 |
hrtimer_try_to_cancel() |
비차단 | -1: 콜백 실행 중, 0: 비활성, 1: 취소 성공 | 비동기 취소 시도, 폴링 |
hrtimer_forward() / hrtimer_forward_now() 상세
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
u64 hrtimer_forward_now(struct hrtimer *timer, ktime_t interval);
hrtimer_forward()는 타이머의 만료 시각을 now를 지나도록 interval 단위로 전진시킵니다. 오버런 횟수(Overrun Count)를 반환합니다. 반드시 콜백 내에서만 호출해야 합니다.
hrtimer_forward_now()는 hrtimer_forward(timer, hrtimer_cb_get_time(timer), interval)과 동일합니다.
hrtimer_forward() 계열은 콜백 내에서만 호출해야 합니다. 임의의 컨텍스트에서 호출하면 경쟁 조건(Race Condition)이 발생할 수 있습니다.
hrtimer 상태 조회 함수
| 함수 | INACTIVE | ENQUEUED | CALLBACK 실행 중 | 설명 |
|---|---|---|---|---|
hrtimer_active() |
false | true | true | RB-트리 등록 또는 콜백 실행 중이면 true |
hrtimer_is_queued() |
false | true | false | RB-트리에 등록된 경우만 true |
hrtimer_callback_running() |
false | false | true | 콜백이 현재 실행 중인 경우만 true |
hrtimer 모드 플래그 상세
hrtimer_sleeper — 슬립/웨이크업(Sleep/Wakeup) 통합
struct hrtimer_sleeper는 hrtimer와 task_struct 포인터를 결합하여, 타이머 만료 시 자동으로 태스크를 깨우는 구조체입니다. nanosleep() 시스템 호출의 핵심 구현체입니다.
struct hrtimer_sleeper {
struct hrtimer timer;
struct task_struct *task; /* 깨울 태스크 */
};
void hrtimer_init_sleeper(struct hrtimer_sleeper *sl,
clockid_t clock_id,
enum hrtimer_mode mode);
int hrtimer_nanosleep(ktime_t rqtp,
const enum hrtimer_mode mode,
const clockid_t clockid);
schedule_timeout() 상세
schedule_timeout()은 현재 태스크를 최대 timeout 지피(Jiffies) 동안 슬립시킵니다. 호출 전에 반드시 태스크 상태를 설정해야 합니다.
signed long schedule_timeout(signed long timeout);
태스크 상태별 동작:
TASK_INTERRUPTIBLE— 시그널(Signal)에 의해 조기 깨움 가능. 잔여 지피 반환.TASK_UNINTERRUPTIBLE— 타이머만으로 깨움. 항상0반환.TASK_KILLABLE— 치명적 시그널(Fatal Signal)에만 반응.SIGKILL등.
특수 값: schedule_timeout(MAX_SCHEDULE_TIMEOUT)은 타이머를 생성하지 않고 무기한 슬립합니다.
편의 래퍼 함수:
/* 태스크 상태를 내부에서 설정하는 래퍼 */
signed long schedule_timeout_interruptible(signed long timeout);
signed long schedule_timeout_uninterruptible(signed long timeout);
signed long schedule_timeout_killable(signed long timeout);
/* msleep: schedule_timeout_uninterruptible의 밀리초 래퍼 */
void msleep(unsigned int msecs);
/* = schedule_timeout_uninterruptible(msecs_to_jiffies(msecs)) */
/* msleep_interruptible: 잔여 밀리초 반환 */
unsigned long msleep_interruptible(unsigned int msecs);
지연 함수 완전 레퍼런스
리눅스 커널에서 지연(Delay)이 필요할 때 올바른 함수를 선택하는 것이 중요합니다. 지연 시간과 실행 컨텍스트에 따라 적절한 API가 다릅니다.
Busy-wait 지연 함수
바쁜 대기(Busy-Wait) 지연은 CPU를 점유하면서 정확한 지연을 제공합니다. 모든 컨텍스트(IRQ, softirq, 프로세스)에서 사용 가능합니다.
void udelay(unsigned long usecs); /* 마이크로초 바쁜 대기 */
void ndelay(unsigned long nsecs); /* 나노초 바쁜 대기 */
void mdelay(unsigned long msecs); /* 밀리초 바쁜 대기 — 가능하면 사용 금지 */
내부 동작: __const_udelay()를 통해 부팅 시 보정된(Calibrated) loops_per_jiffy 값을 곱하여 지연 루프(Delay Loop) 또는 TSC 기반 대기를 수행합니다.
udelay()의 안전 최대값은 대부분의 플랫폼에서 약 1000us입니다. 더 큰 값은 곱셈 오버플로(Overflow)를 유발할 수 있습니다. mdelay()는 CPU를 독점하므로 msleep()이 가능한 환경에서는 사용을 피하세요.
Sleep 기반 지연 함수
/* hrtimer 기반 — 정밀, 프로세스 컨텍스트 전용 */
void usleep_range(unsigned long min, unsigned long max);
/* timer_list 기반 — 프로세스 컨텍스트 전용 */
void msleep(unsigned int msecs); /* 비인터럽트 */
unsigned long msleep_interruptible(unsigned int msecs); /* 잔여 ms 반환 */
void ssleep(unsigned int seconds); /* = msleep(s * 1000) */
usleep_range(min, max)는 내부에서 hrtimer_sleeper를 생성하고 hrtimer_start_range_ns()로 타이머를 시작한 뒤 schedule()로 슬립합니다. [min, max] 범위 내에서 다른 웨이크업과 통합하여 전력을 절약합니다.
msleep()은 timer_list 기반이므로 지피 단위 정밀도입니다. 실제 슬립 시간은 최대 1 tick(보통 1ms ~ 10ms) 더 길 수 있습니다.
fsleep() — 통합 지연 API
커널 5.8에서 도입된 fsleep()(Flexible Sleep)은 지정된 지연 시간에 따라 최적의 함수를 자동 선택합니다:
void fsleep(unsigned long usecs);
/* 내부 동작:
* usecs < 10 → udelay(usecs)
* usecs < 20000 → usleep_range(usecs, 2 * usecs)
* usecs >= 20000 → msleep(usecs / 1000)
*/
/* 마이그레이션 전: 복잡한 조건 분기 */
if (delay_us < 10)
udelay(delay_us);
else if (delay_us < 20000)
usleep_range(delay_us, delay_us * 2);
else
msleep(delay_us / 1000);
/* 마이그레이션 후: 한 줄 */
fsleep(delay_us);
readl_poll_timeout() — 하드웨어 레지스터 폴링(Polling)
하드웨어가 준비 상태가 될 때까지 레지스터를 반복 읽는 매크로입니다. 내부적으로 읽기 간 usleep_range()를 사용합니다.
/* 프로세스 컨텍스트 전용 */
int readl_poll_timeout(addr, val, cond, delay_us, timeout_us);
/* 원자적 컨텍스트 전용 (udelay 사용) */
int readl_poll_timeout_atomic(addr, val, cond, delay_us, timeout_us);
/* 범용 버전: 임의의 읽기 연산 사용 */
int read_poll_timeout(op, val, cond, sleep_us, timeout_us,
sleep_before, args...);
/* 사용 예: 하드웨어 준비 비트 대기 */
u32 val;
int ret;
/* 10us 간격으로 폴링, 최대 100ms 대기 */
ret = readl_poll_timeout(dev->regs + STATUS_REG, val,
val & STATUS_READY,
10, /* delay_us */
100000); /* timeout_us */
if (ret)
dev_err(dev, "hardware timeout\n");
delayed_work API 완전 레퍼런스
struct delayed_work는 타이머(timer_list)와 워크큐(Workqueue)를 결합한 2단계 지연 실행 메커니즘(Two-Phase Deferred Execution)입니다. 타이머 만료 시 워크를 워크큐에 등록하고, kworker 스레드가 프로세스 컨텍스트에서 핸들러를 실행합니다.
내부 동작 흐름
함수별 상세
| 함수 / 매크로 | 설명 | 반환값 |
|---|---|---|
INIT_DELAYED_WORK(&dwork, handler) |
초기화 (런타임). 정적 선언은 DECLARE_DELAYED_WORK 사용. |
— |
schedule_delayed_work(&dwork, delay) |
system_wq에 등록. = queue_delayed_work(system_wq, ...) |
true: 새로 큐잉, false: 이미 큐잉됨 |
queue_delayed_work(wq, &dwork, delay) |
지정 워크큐에 등록. | true: 새로 큐잉, false: 이미 큐잉됨 |
mod_delayed_work(wq, &dwork, delay) |
대기 중이면 취소 후 새 delay로 재등록. 원자적 연산(Atomic). | true: 이전에 대기 중, false: 아님 |
cancel_delayed_work(&dwork) |
비동기 취소. 실행 중인 핸들러를 기다리지 않음. | true: 대기 중이었음, false: 아님 |
cancel_delayed_work_sync(&dwork) |
동기 취소. 실행 중인 핸들러 완료까지 대기. | true: 대기 중이었음, false: 아님 |
delayed_work_pending(&dwork) |
타이머 또는 워크가 대기 중인지 확인. | bool |
flush_delayed_work(&dwork) |
실행 중인 핸들러 완료를 기다림. 취소하지 않음. | true: 워크가 있었음 |
to_delayed_work(work) |
work_struct → delayed_work 포인터 변환. 핸들러 내에서 사용. |
struct delayed_work * |
clockevent 프로그래밍 API 상세
클록이벤트 장치(Clock Event Device)는 하드웨어 타이머 인터럽트를 커널 타이머 프레임워크에 연결하는 추상화 계층(Abstraction Layer)입니다. 각 CPU에 하나의 tick 장치가 할당되며, hrtimer와 타이머 휠의 만료 이벤트를 하드웨어로 프로그래밍합니다.
struct clock_event_device 상세
등록 흐름
/* 주요 등록 API */
void clockevents_config_and_register(struct clock_event_device *dev,
u32 freq,
unsigned long min_delta,
unsigned long max_delta);
/* 이미 mult/shift 설정된 경우 */
void clockevents_register_device(struct clock_event_device *dev);
tick_program_event() 상세
void tick_program_event(ktime_t expires, int force);
현재 CPU의 클록이벤트 장치에 다음 이벤트를 프로그래밍합니다. hrtimer_interrupt()가 만료된 타이머를 모두 처리한 후 다음 만료 시각으로 이 함수를 호출합니다.
force = 1:expires가 과거 시점이어도 프로그래밍 (즉시 발생)force = 0: 과거 시점이면 프로그래밍 실패 시 재시도 루프- 내부:
clockevents_program_event()→ 장치의set_next_ktime()또는set_next_event()호출
clockevent 모드 전환
클록이벤트 장치의 상태 전환(State Transition):
| 상태 | 설명 | 전환 조건 |
|---|---|---|
CLOCK_EVT_STATE_DETACHED |
초기 상태. 미연결. | 등록 전 |
CLOCK_EVT_STATE_PERIODIC |
주기적 인터럽트 생성. 고정 HZ 주기. | hrtimer 미활성, NO_HZ 미설정 |
CLOCK_EVT_STATE_ONESHOT |
단발 인터럽트. hrtimer/tickless에 필수. | hrtimer 활성화 또는 NO_HZ 설정 |
CLOCK_EVT_STATE_SHUTDOWN |
비활성. 인터럽트 중지. | CPU offline 또는 deep idle |
jiffies 유틸리티 함수 레퍼런스
jiffies 변환(Conversion) 및 비교(Comparison) 함수의 전체 레퍼런스입니다.
변환 함수 표
| 함수 | 변환 방향 | 설명 |
|---|---|---|
jiffies_to_msecs(j) |
jiffies → ms | 밀리초로 변환 |
jiffies_to_usecs(j) |
jiffies → us | 마이크로초로 변환 |
jiffies_to_nsecs(j) |
jiffies → ns | 나노초로 변환 |
msecs_to_jiffies(m) |
ms → jiffies | 밀리초에서 변환. 가장 자주 사용. |
usecs_to_jiffies(u) |
us → jiffies | 마이크로초에서 변환 |
nsecs_to_jiffies(n) |
ns → jiffies | 나노초에서 변환 |
jiffies_to_timespec64(j, &ts) |
jiffies → timespec64 | 초/나노초 구조체로 변환 |
timespec64_to_jiffies(&ts) |
timespec64 → jiffies | 구조체에서 jiffies로 |
jiffies_to_clock_t(j) |
jiffies → clock_t (USER_HZ) | /proc 출력용 변환 |
clock_t_to_jiffies(c) |
clock_t → jiffies | 사용자 공간 값에서 변환 |
jiffies64_to_msecs(j) |
u64 jiffies → ms | 64비트 안전 밀리초 변환 |
jiffies64_to_nsecs(j) |
u64 jiffies → ns | 64비트 안전 나노초 변환 |
nsecs_to_jiffies64(n) |
ns → u64 jiffies | 64비트 안전 역변환 |
get_jiffies_64() |
— | 64비트 jiffies 원자적 읽기 (32비트 아키텍처에서 안전) |
비교 매크로 표
| 매크로 | 비교 | 설명 |
|---|---|---|
time_after(a, b) |
a > b | 래핑(Wraparound) 안전 비교 |
time_before(a, b) |
a < b | 래핑 안전 비교 |
time_after_eq(a, b) |
a >= b | 같거나 이후 |
time_before_eq(a, b) |
a <= b | 같거나 이전 |
time_in_range(a, b, c) |
b <= a <= c | 범위 내 확인 (닫힌 구간) |
time_in_range_open(a, b, c) |
b <= a < c | 범위 내 확인 (반개 구간) |
time_is_before_jiffies(a) |
a < jiffies | 타임아웃(Timeout) 만료 확인 |
time_is_after_jiffies(a) |
a > jiffies | 아직 미만료 확인 |
time_is_before_eq_jiffies(a) |
a <= jiffies | 만료 또는 정확히 현재 |
time_is_after_eq_jiffies(a) |
a >= jiffies | 미만료 또는 정확히 현재 |
64비트 버전: time_after64(a, b), time_before64(a, b), time_after_eq64(a, b), time_before_eq64(a, b), time_in_range64(a, b, c) 등이 있으며, u64 값에 대해 동일한 래핑 안전 비교를 수행합니다.
unsigned long jiffies는 HZ=1000에서 약 49.7일, HZ=100에서 약 497일 후 래핑됩니다. 반드시 time_after() / time_before() 매크로를 사용하고, 직접 비교(if (jiffies > deadline))는 사용하지 마세요. 장기 타임아웃은 64비트 버전을 사용하세요.
/* 올바른 타임아웃 확인 */
unsigned long deadline = jiffies + msecs_to_jiffies(5000);
/* ✅ 올바름: time_after 사용 (래핑 안전) */
if (time_after(jiffies, deadline)) {
pr_err("timeout!\n");
return -ETIMEDOUT;
}
/* ❌ 잘못됨: 직접 비교 (래핑 시 오동작) */
if (jiffies > deadline) { /* 위험! jiffies 래핑 시 실패 */
pr_err("timeout!\n");
return -ETIMEDOUT;
}
/* time_is_before_jiffies를 사용한 간결한 패턴 */
if (time_is_before_jiffies(deadline)) {
/* deadline이 jiffies보다 이전 = 만료됨 */
return -ETIMEDOUT;
}
부팅 시 타이머 초기화 시퀀스
리눅스 커널의 타이머 서브시스템은 부팅 과정에서 정해진 순서대로 초기화됩니다. start_kernel()에서 시작하여 아키텍처 독립적인 초기화와 아키텍처별 초기화가 순차적으로 진행되며, 각 단계에서 clocksource 등록, clockevent 설정, hrtimer 프레임워크 활성화가 이루어집니다.
| 순서 | 함수 | 설명 |
|---|---|---|
| 1 | tick_init() | tick 서브시스템 자료구조 초기화, broadcast 프레임워크 설정 |
| 2 | init_timers() | Timer Wheel (저해상도 타이머) per-CPU 기반 초기화, TIMER_SOFTIRQ 등록 |
| 3 | hrtimers_init() | hrtimer per-CPU clock base 초기화, softirq 핸들러 등록 |
| 4 | timekeeping_init() | jiffies clocksource를 기본 등록, wall time 초기값 설정 (RTC에서 읽기) |
| 5 | time_init() | 아키텍처별 타이머 하드웨어 초기화 (TSC, HPET, Generic Timer 등) |
| 6 | late_time_init() | x86에서 HPET/TSC clocksource 최종 등록, calibration 완료 |
/* init/main.c - start_kernel() 내 타이머 초기화 순서 */
void start_kernel(void)
{
...
tick_init(); /* tick 서브시스템 초기화 */
init_timers(); /* Timer Wheel 초기화 */
hrtimers_init(); /* hrtimer 프레임워크 초기화 */
softirq_init(); /* softirq 인프라 (TIMER_SOFTIRQ 포함) */
timekeeping_init(); /* timekeeping + jiffies clocksource */
time_init(); /* arch별 타이머 HW 초기화 */
...
if (late_time_init)
late_time_init(); /* x86: HPET/TSC 최종 등록 */
...
}
/* arch/x86/kernel/time.c */
void __init time_init(void)
{
late_time_init = x86_late_time_init;
}
static __initdata void (*x86_late_time_init)(void);
static void __init x86_late_time_init(void)
{
x86_init.timers.timer_init(); /* hpet_time_init() 또는 setup_pit_timer() */
tsc_init(); /* TSC calibration 및 clocksource 등록 */
}
/* arch/arm64/kernel/time.c */
void __init time_init(void)
{
timer_probe(); /* DT에서 timer 노드 탐색 → arch_timer_of_register() */
tick_setup_hrtimer_broadcast();
}
timekeeping_init()이 hrtimers_init() 이후에 호출되어야 하며, 아키텍처별 time_init()은 반드시 공통 프레임워크가 준비된 후에 실행됩니다. 순서를 바꾸면 NULL 포인터 역참조나 부팅 실패가 발생할 수 있습니다.
저해상도 → 고해상도 전환
커널은 부팅 초기에 주기적(periodic) 모드의 저해상도 타이머로 시작합니다. 이후 one-shot 가능한 clockevent 장치가 등록되면 hrtimer 프레임워크가 활성화되어 고해상도 모드로 전환됩니다. 이 전환은 동적으로 이루어지며, tick_check_new_device()가 핵심 전환 트리거입니다.
/* kernel/time/tick-common.c - 새 clockevent 장치 감지 */
void tick_check_new_device(struct clock_event_device *newdev)
{
struct clock_event_device *curdev;
struct tick_device *td;
int cpu;
cpu = smp_processor_id();
td = &per_cpu(tick_cpu_device, cpu);
curdev = td->evtdev;
/* one-shot 가능 여부 확인 */
if (!tick_check_percpu(curdev, newdev, cpu))
goto out;
if (!tick_check_preferred(curdev, newdev))
goto out;
/* 교체 결정: 더 나은 장치로 전환 */
tick_install_replacement(newdev);
...
}
/* kernel/time/hrtimer.c - 고해상도 모드 전환 */
static void hrtimer_switch_to_hres(void)
{
struct hrtimer_cpu_base *base = this_cpu_ptr(&hrtimer_bases);
if (tick_init_highres()) { /* clockevent를 oneshot으로 전환 */
return;
}
base->hres_active = 1; /* 고해상도 모드 활성화 플래그 */
hrtimer_resolution = HIGH_RES_NSEC;
tick_setup_sched_timer(); /* tick_sched_timer를 hrtimer로 등록 */
retrigger_next_event(NULL);
}
late_time_init() 후 처음 clockevent 장치가 등록될 때 발생합니다. /proc/timer_list에서 .hres_active 필드가 1이면 해당 CPU가 고해상도 모드임을 의미합니다. CONFIG_HIGH_RES_TIMERS=n이면 전환이 발생하지 않습니다.
아키텍처별 타이머 하드웨어 상세 비교
각 CPU 아키텍처는 고유한 타이머 하드웨어를 제공합니다. x86은 TSC, HPET, APIC Timer, PM Timer 등 다양한 소스가 공존하며, ARM은 통합된 Generic Timer를, RISC-V는 SBI 기반 타이머 인터페이스를 사용합니다.
| 아키텍처 | Clocksource | Rating | 해상도 | Clockevent | One-shot | 절전 영향 |
|---|---|---|---|---|---|---|
| x86 | TSC | 300 | ~1ns | Local APIC Timer | O (TSC-deadline) | C-state에서 APIC 정지 가능 |
| x86 | HPET | 250 | ~100ns | HPET comparator | O | 항상 동작 |
| x86 | ACPI PM Timer | 200 | ~280ns | - | - | 항상 동작 (I/O 느림) |
| ARM64 | arch_sys_counter | 400 | ~10-20ns | arch_timer | O | WFI/WFE와 무관하게 동작 |
| RISC-V | riscv_clocksource | 400 | DT 의존 | riscv_timer | O (Sstc) | SBI ecall 오버헤드 |
/* x86: TSC clocksource 등록 (arch/x86/kernel/tsc.c) */
static struct clocksource clocksource_tsc = {
.name = "tsc",
.rating = 300,
.read = read_tsc,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS | CLOCK_SOURCE_MUST_VERIFY,
};
/* ARM64: Generic Timer clocksource (drivers/clocksource/arm_arch_timer.c) */
static struct clocksource clocksource_counter = {
.name = "arch_sys_counter",
.rating = 400,
.read = arch_counter_read,
.mask = CLOCKSOURCE_MASK(56),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/* RISC-V: Timer 초기화 (drivers/clocksource/timer-riscv.c) */
static int __init riscv_timer_init_dt(struct device_node *n)
{
int cpuid, error;
cpuid = riscv_of_processor_hartid(n, &error);
if (cpuid != smp_processor_id())
return 0;
pr_info("Timer frequency %lu Hz\n", riscv_timebase);
clocksource_register_hz(&riscv_clocksource, riscv_timebase);
...
}
tsc_reliable, tsc_clocksource_reliable 등의 플래그로 이를 검증하며, 불안정하면 자동으로 HPET이나 PM Timer로 폴백합니다. dmesg | grep -i tsc로 TSC 상태를 확인할 수 있습니다.
pending_map 비트맵 최적화
Timer Wheel은 5단계 × 64슬롯 = 총 320개의 슬롯을 관리합니다. 다음에 만료될 타이머를 찾기 위해 모든 슬롯을 순회하면 비효율적이므로, pending_map 비트맵을 사용하여 타이머가 등록된 슬롯만 빠르게 탐색합니다.
/* kernel/time/timer.c - pending_map 관련 핵심 코드 */
/* 타이머 등록 시 비트맵 설정 */
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); /* 해당 슬롯 비트 1로 설정 */
}
/* 타이머 제거 시 비트맵 갱신 */
static void dequeue_timer(struct timer_base *base,
struct timer_list *timer)
{
unsigned int idx = timer->flags & TIMER_ARRAYMASK;
hlist_del_init(&timer->entry);
if (hlist_empty(base->vectors + idx))
__clear_bit(idx, base->pending_map); /* 슬롯이 비면 0으로 */
}
/* 다음 만료 타이머 찾기 - 비트맵 스캔 */
static unsigned long __next_timer_interrupt(struct timer_base *base)
{
unsigned long clk, next, adj;
unsigned int lvl, offset;
next = base->clk + NEXT_TIMER_MAX_DELTA;
clk = base->clk;
for (lvl = 0; lvl < LVL_DEPTH; lvl++, offset += LVL_SIZE) {
int pos = find_next_bit(base->pending_map,
offset + LVL_SIZE, offset);
if (pos < offset + LVL_SIZE) {
unsigned long tmp = clk + (unsigned long)(pos - offset)
<< LVL_SHIFT(lvl);
if (time_before(tmp, next))
next = tmp;
}
}
return next;
}
find_next_bit()은 x86에서 bsf(Bit Scan Forward), ARM에서 clz(Count Leading Zeros) 명령어로 컴파일됩니다. 이로 인해 64개 슬롯에서 다음 활성 비트를 찾는 작업이 단일 CPU 명령어로 완료됩니다. 320슬롯 전체를 5개의 64-bit 워드로 표현하므로, 최악의 경우에도 5번의 비트 스캔으로 충분합니다.
hrtimer Soft 모드 실행 경로
커널 5.4부터 hrtimer에 HRTIMER_MODE_SOFT 모드가 추가되었습니다. 기존 hrtimer 콜백은 하드 인터럽트 컨텍스트(hardirq)에서 실행되지만, soft 모드 타이머는 softirq 컨텍스트에서 실행됩니다. 이로써 더 긴 콜백 처리가 가능하고, hardirq 시간을 줄여 시스템 응답성을 개선할 수 있습니다.
/* include/linux/hrtimer.h - 모드 정의 */
enum hrtimer_mode {
HRTIMER_MODE_ABS = 0x00, /* 절대 시간, hard */
HRTIMER_MODE_REL = 0x01, /* 상대 시간, hard */
HRTIMER_MODE_PINNED = 0x02, /* 현재 CPU 고정 */
HRTIMER_MODE_SOFT = 0x04, /* softirq 컨텍스트 실행 */
HRTIMER_MODE_HARD = 0x08, /* hardirq 컨텍스트 (PREEMPT_RT) */
/* 조합 모드 */
HRTIMER_MODE_ABS_SOFT = HRTIMER_MODE_ABS | HRTIMER_MODE_SOFT,
HRTIMER_MODE_REL_SOFT = HRTIMER_MODE_REL | HRTIMER_MODE_SOFT,
HRTIMER_MODE_ABS_HARD = HRTIMER_MODE_ABS | HRTIMER_MODE_HARD,
...
};
/* kernel/time/hrtimer.c - soft hrtimer 처리 */
static void hrtimer_run_softirq(struct softirq_action *h)
{
struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
unsigned long flags;
ktime_t now;
hrtimer_cpu_base_lock_expiry(cpu_base);
raw_spin_lock_irqsave(&cpu_base->lock, flags);
now = hrtimer_update_base(cpu_base);
__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_SOFT);
raw_spin_unlock_irqrestore(&cpu_base->lock, flags);
hrtimer_cpu_base_unlock_expiry(cpu_base);
}
/* CFS bandwidth throttling - soft hrtimer 사용 예 */
void init_cfs_bandwidth(struct cfs_bandwidth *cfs_b)
{
hrtimer_init(&cfs_b->period_timer, CLOCK_MONOTONIC,
HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_SOFT); /* Soft 모드! */
cfs_b->period_timer.function = sched_cfs_period_timer;
hrtimer_init(&cfs_b->slack_timer, CLOCK_MONOTONIC,
HRTIMER_MODE_REL | HRTIMER_MODE_SOFT);
cfs_b->slack_timer.function = sched_cfs_slack_timer;
}
| 특성 | Hard 모드 | Soft 모드 |
|---|---|---|
| 실행 컨텍스트 | hardirq (인터럽트 비활성) | softirq (인터럽트 활성) |
| 최대 지연 | 최소 (나노초 수준) | softirq 스케줄링 지연 추가 |
| 콜백 제약 | sleep/mutex 불가, 짧게 | 더 긴 처리 가능 |
| 선점 가능 | 불가 | 가능 (PREEMPT_RT에서) |
| 사용 예 | sched_clock, perf, watchdog | CFS bandwidth, nanosleep, POSIX timer |
| timerqueue | clock_base별 별도 관리 | clock_base별 별도 관리 |
| PREEMPT_RT 동작 | 진짜 hardirq 유지 | ksoftirqd 스레드에서 실행 |
PREEMPT_RT 패치 적용 시, 일반 hrtimer(mode 미지정)는 자동으로 soft 모드로 전환됩니다. 진짜 hardirq 컨텍스트에서 실행해야 하는 타이머만 HRTIMER_MODE_HARD를 명시적으로 지정해야 합니다. 이는 RT 커널에서 인터럽트 지연 시간을 최소화하기 위한 설계입니다.
Timer와 스케줄러 상호작용
타이머 서브시스템과 프로세스 스케줄러는 긴밀하게 연결되어 있습니다. 고해상도 모드에서 tick_sched_timer() hrtimer가 스케줄러의 심장 박동 역할을 하며, CFS 스케줄러는 hrtimer를 사용하여 대역폭 제한(bandwidth throttling)을 구현합니다.
| 상호작용 지점 | 타이머 측 | 스케줄러 측 | 역할 |
|---|---|---|---|
| 스케줄러 틱 | tick_sched_timer (hrtimer) | scheduler_tick() | 타임슬라이스 갱신, 선점 검사 |
| CFS bandwidth | period_timer (hrtimer soft) | sched_cfs_period_timer() | CPU quota 리필 및 throttle 해제 |
| sched_clock | TSC/clocksource | sched_clock() | 나노초 정밀도 실행 시간 측정 |
| 선점 타이머 | 타이머 인터럽트 | check_preempt_tick() | 현재 태스크의 최소 실행 보장 후 선점 |
| NO_HZ_FULL | tick 정지/재시작 | can_stop_full_tick() | 단일 태스크 실행 시 틱 생략 |
| 지연 실행 | schedule_timeout() | schedule() | 태스크를 지정 시간 후 깨움 |
/* kernel/time/tick-sched.c - 스케줄러 틱 hrtimer 콜백 */
static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
{
struct tick_sched *ts = container_of(timer, struct tick_sched,
sched_timer);
struct pt_regs *regs = get_irq_regs();
ktime_t now = ktime_get();
tick_sched_do_timer(ts, now); /* do_timer(): jiffies 갱신 */
if (regs)
tick_sched_handle(ts, regs); /* scheduler_tick() 포함 */
/* 다음 틱 재설정 (tick_period 간격) */
hrtimer_forward(timer, now, TICK_NSEC);
return HRTIMER_RESTART;
}
/* kernel/sched/fair.c - check_preempt_tick: 선점 판단 */
static void check_preempt_tick(struct cfs_rq *cfs_rq,
struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
/* 할당된 타임슬라이스를 초과했는가? */
if (delta_exec > ideal_runtime) {
resched_curr(rq_of(cfs_rq)); /* TIF_NEED_RESCHED 설정 */
return;
}
...
}
코드 설명
kernel/time/tick-sched.c의 tick_sched_timer()와 kernel/sched/fair.c의 check_preempt_tick()은 타이머 서브시스템과 스케줄러의 핵심 상호작용 지점입니다.
- tick_sched_timer()hrtimer 콜백으로 등록된 스케줄러 틱 함수입니다.
TICK_NSEC(HZ=250일 때 4ms) 주기로 반복 실행되며, 저해상도 타이머의 주기적 틱을 hrtimer 기반으로 에뮬레이션합니다. - tick_sched_do_timer()전역 jiffies 카운터를 갱신하는
do_timer()를 호출합니다. 멀티코어에서 하나의 CPU만 이 역할을 담당합니다(tick_do_timer_cpu). - tick_sched_handle()
scheduler_tick()을 호출하여 현재 태스크의 실행 시간을 갱신하고, CFS의 vruntime을 업데이트합니다. 이를 통해check_preempt_tick()에서 선점 판단이 이루어집니다. - check_preempt_tick()
sched_slice()로 계산된 이상적 실행 시간(ideal_runtime)과 실제 실행 시간(delta_exec)을 비교합니다. 초과하면resched_curr()로TIF_NEED_RESCHED플래그를 설정하여 선점을 예약합니다. - hrtimer_forward()
HRTIMER_RESTART를 반환하기 전에 다음 틱 시점으로 만료 시각을 전진합니다. 이 함수는 놓친 틱 수를 계산하여 정확한 다음 틱 경계에 맞춥니다.
NO_HZ_FULL 모드에서 CPU에 실행 가능한 태스크가 하나뿐이면 tick_sched_timer를 정지하여 불필요한 인터럽트를 제거합니다. 이때 scheduler_tick()이 호출되지 않으므로, 다른 태스크가 깨어날 때 IPI(Inter-Processor Interrupt)로 틱을 재개해야 합니다.
실전 디바이스 드라이버 타이머 패턴 총정리
디바이스 드라이버에서 타이머를 사용하는 대표적인 패턴들을 정리합니다. 각 패턴은 실제 커널 드라이버에서 자주 사용되는 검증된 방식입니다.
패턴 1: 지수 백오프 폴링 (mod_timer)
struct my_device {
struct timer_list poll_timer;
unsigned long poll_interval; /* 현재 폴링 간격 (jiffies) */
unsigned long max_interval; /* 최대 간격 */
};
static void my_poll_callback(struct timer_list *t)
{
struct my_device *dev = from_timer(dev, t, poll_timer);
u32 status = readl(dev->regs + STATUS_REG);
if (status & READY_BIT) {
/* 성공: 작업 처리 후 간격 리셋 */
handle_ready(dev);
dev->poll_interval = msecs_to_jiffies(10); /* 초기 간격으로 */
} else {
/* 미준비: 간격 2배 증가 (최대 제한) */
dev->poll_interval = min(dev->poll_interval * 2,
dev->max_interval);
}
mod_timer(&dev->poll_timer, jiffies + dev->poll_interval);
}
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev = ...;
dev->poll_interval = msecs_to_jiffies(10);
dev->max_interval = msecs_to_jiffies(1000); /* 최대 1초 */
timer_setup(&dev->poll_timer, my_poll_callback, 0);
mod_timer(&dev->poll_timer, jiffies + dev->poll_interval);
return 0;
}
패턴 2: 주기적 hrtimer 샘플링 (ADC 패턴)
struct adc_sampler {
struct hrtimer sample_timer;
ktime_t interval; /* 샘플링 간격 */
u16 *buffer;
int buf_idx;
};
static enum hrtimer_restart adc_sample_callback(struct hrtimer *hr)
{
struct adc_sampler *adc = container_of(hr, struct adc_sampler,
sample_timer);
/* ADC 레지스터 읽기 (짧은 작업만!) */
adc->buffer[adc->buf_idx++] = readw(adc->regs + ADC_DATA);
if (adc->buf_idx >= ADC_BUF_SIZE) {
/* 버퍼 가득 → 워크큐에서 후처리 */
schedule_work(&adc->process_work);
adc->buf_idx = 0;
}
hrtimer_forward_now(hr, adc->interval);
return HRTIMER_RESTART; /* 주기적 반복 */
}
/* 100us 간격 샘플링 시작 */
hrtimer_init(&adc->sample_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
adc->sample_timer.function = adc_sample_callback;
adc->interval = ktime_set(0, 100 * NSEC_PER_USEC); /* 100us */
hrtimer_start(&adc->sample_timer, adc->interval, HRTIMER_MODE_REL);
패턴 3: 워치독 타임아웃
struct hw_watchdog {
struct timer_list wd_timer;
unsigned long timeout_jiffies;
bool hw_alive;
};
static void watchdog_expired(struct timer_list *t)
{
struct hw_watchdog *wd = from_timer(wd, t, wd_timer);
dev_err(wd->dev, "Hardware watchdog timeout! Resetting...\n");
wd->hw_alive = false;
writel(HW_RESET_CMD, wd->regs + CTRL_REG); /* HW 리셋 */
}
/* 워치독 킥 — 정상 동작 시 주기적으로 호출 */
static void watchdog_kick(struct hw_watchdog *wd)
{
mod_timer(&wd->wd_timer, jiffies + wd->timeout_jiffies);
}
/* 인터럽트 핸들러에서 킥 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct hw_watchdog *wd = data;
handle_interrupt(wd);
watchdog_kick(wd); /* 인터럽트 발생 = HW 정상 → 타이머 리셋 */
return IRQ_HANDLED;
}
패턴 4: delayed_work를 이용한 지연 초기화
static void deferred_init_work(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device,
init_work.work);
/* 느린 HW 초기화 (프로세스 컨텍스트에서 안전하게) */
firmware_load(dev); /* sleep 가능 */
calibrate_hardware(dev); /* 시간 소요 작업 */
dev->initialized = true;
}
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev = ...;
INIT_DELAYED_WORK(&dev->init_work, deferred_init_work);
/* 500ms 후 초기화 시작 (probe 완료를 블록하지 않음) */
schedule_delayed_work(&dev->init_work, msecs_to_jiffies(500));
return 0;
}
패턴 5: 안전한 종료 (remove 함수)
static void my_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
/* 1. 먼저 재등록 방지 플래그 설정 */
dev->shutting_down = true;
/* 2. 모든 타이머 동기적으로 취소
* del_timer_sync는 콜백이 실행 중이면 완료까지 대기 */
del_timer_sync(&dev->poll_timer);
del_timer_sync(&dev->wd_timer);
hrtimer_cancel(&dev->sample_timer);
/* 3. delayed_work도 동기 취소 */
cancel_delayed_work_sync(&dev->init_work);
/* 4. 이제 안전하게 리소스 해제 */
kfree(dev->buffer);
...
}
패턴 6: readl_poll_timeout (레지스터 폴링)
#include <linux/iopoll.h>
static int wait_for_hw_ready(struct my_device *dev)
{
u32 val;
int ret;
/* STATUS_REG를 10us 간격으로 폴링, 최대 10ms 대기
* val에 읽은 값 저장, READY_BIT이 설정되면 성공 반환 */
ret = readl_poll_timeout(dev->regs + STATUS_REG, val,
val & READY_BIT,
10, /* sleep_us: 폴링 간격 (us) */
10000); /* timeout_us: 최대 대기 (us) */
if (ret) {
dev_err(dev->dev, "HW not ready (timeout)\n");
return ret; /* -ETIMEDOUT */
}
/* 인터럽트 컨텍스트에서는 _atomic 버전 사용 */
ret = readl_poll_timeout_atomic(dev->regs + STATUS_REG, val,
val & DONE_BIT, 1, 100);
return ret;
}
del_timer_sync(), hrtimer_cancel(), cancel_delayed_work_sync()를 사용하여 동기적으로 취소하세요. _sync 접미사가 없는 버전은 콜백 완료를 보장하지 않습니다.
완전한 타이머 데모 커널 모듈 예제
timer_list(지수 백오프), hrtimer(정밀 주기 샘플링), delayed_work(초기화 지연)를 하나의 모듈에서 통합 사용하는 완전한 예제입니다. insmod/rmmod로 동작을 확인할 수 있으며, 안전한 종료 패턴을 포함합니다.
/* timer_demo.c — timer_list + hrtimer + delayed_work 통합 데모 모듈
* Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
* Usage: insmod timer_demo.ko poll_ms=50 sample_us=200
* dmesg -w 로 동작 확인 후 rmmod timer_demo */
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/hrtimer.h>
#include <linux/workqueue.h>
#include <linux/jiffies.h>
#include <linux/ktime.h>
static unsigned int poll_ms = 100;
module_param(poll_ms, uint, 0644);
MODULE_PARM_DESC(poll_ms, "timer_list 초기 폴링 간격 (ms)");
static unsigned int sample_us = 500;
module_param(sample_us, uint, 0644);
MODULE_PARM_DESC(sample_us, "hrtimer 샘플링 간격 (us)");
struct timer_demo {
/* 패턴 1: 지수 백오프 폴링 (timer_list) */
struct timer_list poll_timer;
unsigned long poll_interval;
unsigned long max_interval;
atomic_t poll_count;
/* 패턴 2: 정밀 주기 샘플링 (hrtimer) */
struct hrtimer sample_timer;
ktime_t sample_interval;
ktime_t last_sample_time;
atomic_t sample_count;
s64 max_jitter_ns; /* 최대 지터 기록 */
/* 패턴 3: 지연 초기화 (delayed_work) */
struct delayed_work init_work;
bool initialized;
/* 종료 제어 */
bool shutting_down;
};
static struct timer_demo demo;
/* === 패턴 1: 지수 백오프 폴링 (softirq 컨텍스트) === */
static void poll_timer_callback(struct timer_list *t)
{
struct timer_demo *d = from_timer(d, t, poll_timer);
int cnt = atomic_inc_return(&d->poll_count);
if (d->shutting_down)
return; /* 종료 중이면 재등록하지 않음 */
/* 매 5번째 폴링에서 "성공" 시뮬레이션 → 간격 리셋 */
if (cnt % 5 == 0) {
pr_info("[poll] #%d success, interval reset to %ums\n",
cnt, poll_ms);
d->poll_interval = msecs_to_jiffies(poll_ms);
} else {
/* 지수 백오프: 간격 2배, 최대 제한 */
d->poll_interval = min(d->poll_interval * 2,
d->max_interval);
pr_info("[poll] #%d backoff, next in %ums\n",
cnt, jiffies_to_msecs(d->poll_interval));
}
mod_timer(&d->poll_timer, jiffies + d->poll_interval);
}
/* === 패턴 2: hrtimer 정밀 주기 샘플링 (hard IRQ 컨텍스트) === */
static enum hrtimer_restart sample_timer_callback(
struct hrtimer *hr)
{
struct timer_demo *d = container_of(hr, struct timer_demo,
sample_timer);
ktime_t now = ktime_get();
s64 elapsed_ns, expected_ns, jitter_ns;
int cnt;
if (d->shutting_down)
return HRTIMER_NORESTART;
cnt = atomic_inc_return(&d->sample_count);
/* 실제 콜백 간격과 기대 간격의 차이(지터) 측정 */
if (d->last_sample_time) {
elapsed_ns = ktime_to_ns(ktime_sub(now,
d->last_sample_time));
expected_ns = ktime_to_ns(d->sample_interval);
jitter_ns = elapsed_ns - expected_ns;
if (abs(jitter_ns) > d->max_jitter_ns)
d->max_jitter_ns = abs(jitter_ns);
/* 매 1000번째 샘플마다 통계 출력 */
if (cnt % 1000 == 0)
pr_info("[sample] #%d jitter=%lldns max=%lldns\n",
cnt, jitter_ns, d->max_jitter_ns);
}
d->last_sample_time = now;
hrtimer_forward_now(hr, d->sample_interval);
return HRTIMER_RESTART;
}
/* === 패턴 3: delayed_work 지연 초기화 (프로세스 컨텍스트) === */
static void deferred_init_handler(struct work_struct *work)
{
struct timer_demo *d = container_of(work, struct timer_demo,
init_work.work);
pr_info("[init] deferred initialization (PID=%d)\n",
current->pid);
/* 프로세스 컨텍스트: msleep(), mutex, kmalloc 모두 가능 */
msleep(50); /* 느린 HW 초기화 시뮬레이션 */
d->initialized = true;
pr_info("[init] initialization complete\n");
}
/* === 모듈 초기화 === */
static int __init timer_demo_init(void)
{
pr_info("timer_demo: loading (poll=%ums, sample=%uus)\n",
poll_ms, sample_us);
memset(&demo, 0, sizeof(demo));
/* 1. timer_list 지수 백오프 설정 */
demo.poll_interval = msecs_to_jiffies(poll_ms);
demo.max_interval = msecs_to_jiffies(2000); /* 최대 2초 */
timer_setup(&demo.poll_timer, poll_timer_callback, 0);
mod_timer(&demo.poll_timer, jiffies + demo.poll_interval);
/* 2. hrtimer 정밀 샘플링 설정 */
demo.sample_interval = ktime_set(0,
sample_us * NSEC_PER_USEC);
hrtimer_init(&demo.sample_timer,
CLOCK_MONOTONIC, HRTIMER_MODE_REL);
demo.sample_timer.function = sample_timer_callback;
hrtimer_start(&demo.sample_timer,
demo.sample_interval, HRTIMER_MODE_REL);
/* 3. delayed_work 지연 초기화 (500ms 후) */
INIT_DELAYED_WORK(&demo.init_work, deferred_init_handler);
schedule_delayed_work(&demo.init_work,
msecs_to_jiffies(500));
pr_info("timer_demo: all timers started\n");
return 0;
}
/* === 모듈 해제 — 안전한 종료 패턴 === */
static void __exit timer_demo_exit(void)
{
pr_info("timer_demo: unloading...\n");
/* 1. 재등록 방지 플래그 설정 */
demo.shutting_down = true;
/* 2. 모든 타이머 동기적 취소
* 순서: timer_list → hrtimer → delayed_work */
del_timer_sync(&demo.poll_timer);
/* 콜백 실행 중이면 완료까지 대기 */
hrtimer_cancel(&demo.sample_timer);
/* HRTIMER_RESTART 반환해도 다시 활성화되지 않음 */
cancel_delayed_work_sync(&demo.init_work);
/* kworker에서 실행 중이면 완료까지 대기 */
pr_info("timer_demo: polls=%d samples=%d max_jitter=%lldns\n",
atomic_read(&demo.poll_count),
atomic_read(&demo.sample_count),
demo.max_jitter_ns);
pr_info("timer_demo: unloaded safely\n");
}
module_init(timer_demo_init);
module_exit(timer_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Timer subsystem demo: timer_list + hrtimer + delayed_work");
코드 설명
세 가지 타이머 패턴을 하나의 모듈에서 통합 사용하는 완전한 예제입니다.
- module_param()
insmod timer_demo.ko poll_ms=50 sample_us=200으로 런타임에 파라미터를 지정할 수 있습니다./sys/module/timer_demo/parameters/에서 읽기/쓰기도 가능합니다(권한 0644). - from_timer()
container_of()의 타이머 전용 래퍼입니다.struct timer_list포인터에서 감싸는 구조체의 포인터를 안전하게 추출합니다. 커널 4.15+에서 도입된 패턴입니다. - 지터 측정hrtimer 콜백에서 이전 콜백과의 실제 간격을 측정하여 기대 간격과의 차이(jitter)를 계산합니다.
max_jitter_ns에 최대 지터를 기록하여 시스템의 타이머 정밀도를 평가할 수 있습니다. 일반 커널에서는 수 마이크로초, PREEMPT_RT에서는 수십 마이크로초 이내를 기대할 수 있습니다. - shutting_down 플래그모듈 해제 시 타이머 콜백이 자기 자신을 재등록하지 않도록 방지합니다.
del_timer_sync()는 현재 실행 중인 콜백의 완료를 대기하지만, 콜백이mod_timer()로 재등록하면 새 타이머는 취소되지 않습니다. 이 플래그로 재등록을 차단합니다. - 취소 순서타이머 간 의존 관계가 없으면 순서는 자유입니다. 그러나 일반적으로 빠른 주기의 타이머(hrtimer)를 먼저 취소하고, 느린 주기의 타이머(delayed_work)를 나중에 취소하는 것이 안전합니다. 핵심은 모든 타이머를
_sync버전으로 취소하여 콜백 완료를 보장하는 것입니다.
# Makefile — 빌드 방법
obj-m += timer_demo.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
# 테스트 절차
make
sudo insmod timer_demo.ko poll_ms=50 sample_us=200
dmesg -w # 로그 실시간 관찰
sleep 5
sudo rmmod timer_demo
dmesg | tail -20 # 최종 통계 확인
# 기대 출력 예시:
# [ +0.500] timer_demo: all timers started
# [ +1.000] [init] deferred initialization (PID=123)
# [ +1.050] [init] initialization complete
# [ +1.050] [poll] #1 backoff, next in 100ms
# [ +1.150] [poll] #2 backoff, next in 200ms
# [ +1.350] [poll] #3 backoff, next in 400ms
# [ +5.000] [sample] #1000 jitter=1523ns max=8762ns
# [ +5.001] timer_demo: polls=12 samples=10000 max_jitter=8762ns
# [ +5.001] timer_demo: unloaded safely
실전 패턴: 네트워크 드라이버 링크 상태 모니터링
실제 네트워크 드라이버(e1000e, igb, ixgbe 등)에서 사용하는 링크 상태 모니터링 패턴입니다. timer_setup()으로 초기화하고 mod_timer()로 주기적 폴링을 수행하며, probe()/remove()/suspend()/resume() 생명주기 전체를 안전하게 관리합니다.
/* 실전 패턴: PCI 네트워크 드라이버 링크 상태 모니터링
* (e1000e/igb 스타일 단순화 예제) */
#include <linux/netdevice.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#define LINK_CHECK_INTERVAL (2 * HZ) /* 2초 주기 */
struct my_nic_adapter {
struct net_device *netdev;
struct pci_dev *pdev;
void __iomem *hw_addr; /* MMIO 레지스터 베이스 */
struct timer_list link_timer; /* 링크 상태 모니터링 타이머 */
bool link_up; /* 현재 링크 상태 */
unsigned int link_speed; /* 현재 속도 (Mbps) */
unsigned long flags;
#define FLAG_DOWN 0 /* 인터페이스 비활성화됨 */
#define FLAG_SUSPENDED 1 /* PM suspend 상태 */
};
/* 링크 상태 확인 (레지스터 읽기) */
static bool my_nic_check_link(struct my_nic_adapter *adapter)
{
u32 status = readl(adapter->hw_addr + 0x08); /* STATUS reg */
return !!(status & 0x2); /* Link Up 비트 */
}
/* 타이머 콜백: softirq 컨텍스트에서 실행 */
static void my_nic_link_timer_cb(struct timer_list *t)
{
struct my_nic_adapter *adapter =
from_timer(adapter, t, link_timer);
struct net_device *netdev = adapter->netdev;
bool link_now;
/* 종료/suspend 중이면 재등록하지 않음 */
if (test_bit(FLAG_DOWN, &adapter->flags) ||
test_bit(FLAG_SUSPENDED, &adapter->flags))
return;
link_now = my_nic_check_link(adapter);
/* 링크 상태 변경 감지 → netdev에 통지 */
if (link_now && !adapter->link_up) {
netdev_info(netdev, "link up at %u Mbps\n",
adapter->link_speed);
netif_carrier_on(netdev); /* 네트워크 스택에 링크 업 통지 */
adapter->link_up = true;
} else if (!link_now && adapter->link_up) {
netdev_info(netdev, "link down\n");
netif_carrier_off(netdev); /* 링크 다운 통지 */
adapter->link_up = false;
}
/* ✅ 콜백 마지막에 재등록 (주기적 타이머 패턴) */
mod_timer(&adapter->link_timer,
jiffies + LINK_CHECK_INTERVAL);
}
/* probe(): PCI 디바이스 탐지 시 호출 */
static int my_nic_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct my_nic_adapter *adapter;
struct net_device *netdev;
/* ... PCI 초기화, netdev 할당 ... */
/* ✅ 타이머 초기화 (아직 시작하지 않음) */
timer_setup(&adapter->link_timer,
my_nic_link_timer_cb, 0);
/* netdev 등록 후 open()에서 타이머 시작 */
return register_netdev(netdev);
}
/* open(): ifconfig up 또는 ip link set up 시 호출 */
static int my_nic_open(struct net_device *netdev)
{
struct my_nic_adapter *adapter = netdev_priv(netdev);
clear_bit(FLAG_DOWN, &adapter->flags);
/* ✅ 타이머 시작: 첫 만료 시각 설정 */
mod_timer(&adapter->link_timer,
jiffies + LINK_CHECK_INTERVAL);
return 0;
}
/* close(): ifconfig down 시 호출 */
static int my_nic_close(struct net_device *netdev)
{
struct my_nic_adapter *adapter = netdev_priv(netdev);
/* ✅ 플래그 먼저 설정 → 콜백 재등록 방지 */
set_bit(FLAG_DOWN, &adapter->flags);
/* ✅ 동기적 취소: 콜백 완료 대기 */
del_timer_sync(&adapter->link_timer);
return 0;
}
/* remove(): PCI 디바이스 제거 시 호출 */
static void my_nic_remove(struct pci_dev *pdev)
{
struct my_nic_adapter *adapter = pci_get_drvdata(pdev);
set_bit(FLAG_DOWN, &adapter->flags);
del_timer_sync(&adapter->link_timer);
unregister_netdev(adapter->netdev);
/* ... PCI 리소스 해제 ... */
}
/* suspend(): PM suspend 시 호출 */
static int my_nic_suspend(struct device *dev)
{
struct my_nic_adapter *adapter = dev_get_drvdata(dev);
set_bit(FLAG_SUSPENDED, &adapter->flags);
del_timer_sync(&adapter->link_timer);
/* ✅ suspend 시 타이머 중단 — resume에서 재시작 */
return 0;
}
/* resume(): PM resume 시 호출 */
static int my_nic_resume(struct device *dev)
{
struct my_nic_adapter *adapter = dev_get_drvdata(dev);
clear_bit(FLAG_SUSPENDED, &adapter->flags);
if (!test_bit(FLAG_DOWN, &adapter->flags))
mod_timer(&adapter->link_timer,
jiffies + LINK_CHECK_INTERVAL);
/* ✅ resume 시 인터페이스가 up이면 타이머 재시작 */
return 0;
}
코드 설명
PCI 네트워크 드라이버의 전체 생명주기에서 timer_setup()/mod_timer()/del_timer_sync()를 올바르게 사용하는 패턴입니다. e1000e, igb 등 실제 Intel 네트워크 드라이버의 링크 모니터링 로직을 단순화한 것입니다.
- timer_setup() in probe()
probe()에서 타이머를 초기화하지만 아직 시작하지 않습니다.timer_setup()은struct timer_list를 안전한 초기 상태로 설정합니다. 세 번째 인자0은 기본 플래그(마이그레이션 허용)입니다. - mod_timer() in open()인터페이스가 활성화될 때(
ifconfig up) 처음으로 타이머를 시작합니다.mod_timer()는 비활성 타이머도 활성화하므로 별도의add_timer()호출이 불필요합니다. - 콜백 마지막 mod_timer()콜백 함수의 마지막에
mod_timer()를 호출하여 다음 실행을 예약합니다. 콜백 앞부분에서 호출하면 실행 중인 작업이 끝나기 전에 다음 콜백이 시작될 수 있습니다. - FLAG_DOWN 플래그 패턴
close()/remove()에서 먼저 FLAG_DOWN을 설정한 후del_timer_sync()를 호출합니다. 이 순서가 중요합니다: (1) 플래그 설정 → (2) 현재 실행 중인 콜백 완료 대기 → (3) 콜백이 FLAG_DOWN을 확인하여 재등록하지 않음. 순서가 반대이면del_timer_sync()후 콜백이 재등록될 수 있습니다. - suspend/resume 처리PM suspend 시 타이머를 중단하고, resume 시 인터페이스 상태를 확인하여 조건부로 재시작합니다. suspend 중에 타이머가 만료되어 접근 불가능한 하드웨어 레지스터를 읽는 것을 방지합니다.
타이머 관련 CONFIG 옵션 레퍼런스
커널 빌드 시 타이머 동작에 영향을 미치는 주요 CONFIG 옵션들을 정리합니다. 이 설정들은 타이머 해상도, 틱 동작, 디버깅 기능을 제어합니다.
| CONFIG 옵션 | 기본값 | 설명 | 영향 |
|---|---|---|---|
CONFIG_HZ | 250 | 틱 주파수 (초당 인터럽트 횟수) | 높을수록 반응성 증가, 오버헤드 증가 |
CONFIG_HZ_100 | n | HZ=100, 서버 최적화 | 10ms 틱 해상도, 최소 오버헤드 |
CONFIG_HZ_250 | y | HZ=250, 범용 (많은 배포판 기본) | 4ms 틱 해상도 |
CONFIG_HZ_300 | n | HZ=300, 멀티미디어/PAL/NTSC | 3.33ms, 영상 프레임률과 정수배 |
CONFIG_HZ_1000 | n | HZ=1000, 저지연 데스크톱 | 1ms 틱 해상도, 높은 오버헤드 |
CONFIG_NO_HZ_IDLE | y | idle 상태에서 틱 중단 | 절전 효과 극대화, 대부분의 배포판 기본 |
CONFIG_NO_HZ_FULL | n | 실행 태스크 1개일 때도 틱 중단 | HPC, 실시간 워크로드에 유용 |
CONFIG_HZ_PERIODIC | n | 항상 주기적 틱 발생 | 레거시 호환, 절전 없음 |
CONFIG_HIGH_RES_TIMERS | y | 나노초 해상도 hrtimer 활성화 | 비활성화하면 모든 타이머가 HZ 해상도 |
CONFIG_HPET_TIMER | y (x86) | HPET clocksource/clockevent 지원 | x86 전용, TSC 폴백으로 사용 |
CONFIG_HPET_EMULATE_RTC | y (x86) | HPET으로 RTC 에뮬레이션 | 레거시 RTC 인터럽트 호환 |
CONFIG_X86_TSC | y (x86) | TSC (Time Stamp Counter) 사용 | x86 필수, rdtsc 명령어 활성화 |
CONFIG_TIMER_STATS | - | 커널 4.11에서 제거됨 | /proc/timer_stats로 타이머 추적 (보안 위험으로 제거) |
CONFIG_DEBUG_OBJECTS_TIMERS | n | 타이머 객체 라이프사이클 추적 | use-after-free, 이중 초기화 감지 |
CONFIG_SOFTLOCKUP_DETECTOR | y | soft lockup 감지 (10+초 선점 불가) | watchdog/N 커널 스레드 사용 |
CONFIG_HARDLOCKUP_DETECTOR | n | hard lockup 감지 (인터럽트 비활성 고착) | NMI perf 이벤트 기반, x86 전용 |
CONFIG_PREEMPT_RT | n | 완전 선점형 실시간 커널 | hrtimer soft 기본, spinlock→mutex 변환 |
/* 런타임에 현재 설정 확인하기 */
/* 1. HZ 값 확인 */
printk("HZ = %d, tick = %lu ns\n", HZ, TICK_NSEC);
/* 2. 고해상도 모드 활성화 여부 */
printk("hres_active = %d\n",
this_cpu_ptr(&hrtimer_bases)->hres_active);
/* 3. NO_HZ 모드 확인 (커널 명령줄) */
/* nohz=on/off, nohz_full=1-3 */
/* 4. /proc/timer_list로 전체 상태 확인 */
/* cat /proc/timer_list | grep -E "(hres_active|nohz_mode|Tick Device)" */
/* 5. sysfs로 clocksource 확인 */
/* cat /sys/devices/system/clocksource/clocksource0/current_clocksource */
/* cat /sys/devices/system/clocksource/clocksource0/available_clocksource */
/proc/timer_stats는 타이머를 등록한 프로세스의 PID와 함수명을 노출했는데, 이는 보안상 커널 주소 공간 배치(KASLR) 무력화에 악용될 수 있었습니다. 커널 4.11(2017)에서 제거되었으며, 동일 기능은 perf나 trace-cmd의 timer:timer_start tracepoint로 대체합니다.
- 서버/클라우드:
HZ_250+NO_HZ_IDLE+HIGH_RES_TIMERS— 절전과 성능의 균형 - 데스크톱/게이밍:
HZ_1000+NO_HZ_IDLE+HIGH_RES_TIMERS— 최소 입력 지연 - 실시간/HPC:
HZ_1000+NO_HZ_FULL+PREEMPT_RT— 결정적 지연 보장 - 임베디드/IoT:
HZ_100+NO_HZ_IDLE— 최소 전력 소비
참고 자료
- docs.kernel.org — Timers — 커널 공식 타이머 서브시스템 문서 인덱스입니다.
- docs.kernel.org — hrtimers — subsystem for high-resolution kernel timers — 고해상도 타이머 설계와 구현을 설명하는 공식 문서입니다.
- docs.kernel.org — NO_HZ: Reducing Scheduling-Clock Ticks — NO_HZ_IDLE, NO_HZ_FULL 구성 옵션과 동작 원리를 다루는 공식 문서입니다.
- docs.kernel.org — Timekeeping — clocksource, timekeeping 프레임워크에 대한 커널 공식 문서입니다.
- docs.kernel.org — Timer Stats — 타이머 통계 수집과 디버깅 인터페이스를 설명하는 문서입니다.
- docs.kernel.org — ktime accessors — ktime_get() 계열 API 사용 가이드 공식 문서입니다.
- kernel/time/timer.c — Bootlin Elixir — Timer Wheel 핵심 구현 소스 코드입니다. __run_timers(), mod_timer() 등이 정의되어 있습니다.
- kernel/time/hrtimer.c — Bootlin Elixir — hrtimer 서브시스템 핵심 구현입니다. hrtimer_start(), hrtimer_interrupt() 등이 포함됩니다.
- kernel/time/jiffies.c — Bootlin Elixir — jiffies 기반 clocksource 구현 소스 코드입니다.
- kernel/time/clocksource.c — Bootlin Elixir — clocksource 등록·선택·watchdog 메커니즘 구현입니다.
- kernel/time/clockevents.c — Bootlin Elixir — clock_event_device 관리 및 이벤트 프레임워크 구현입니다.
- kernel/time/tick-sched.c — Bootlin Elixir — tick_nohz_idle_enter/exit 등 tickless(NO_HZ) 핵심 로직입니다.
- kernel/time/tick-broadcast.c — Bootlin Elixir — tick broadcast 프레임워크 구현으로, deep idle 상태에서의 타이머 전달을 처리합니다.
- kernel/time/posix-timers.c — Bootlin Elixir — POSIX timer_create/timer_settime 시스템 콜 커널 측 구현입니다.
- kernel/time/timekeeping.c — Bootlin Elixir — ktime_get(), do_gettimeofday() 등 시간 관리 핵심 구현입니다.
- include/linux/timer.h — Bootlin Elixir — timer_list 구조체와 타이머 API 매크로/함수 선언부입니다.
- include/linux/hrtimer.h — Bootlin Elixir — hrtimer 구조체와 API 선언부입니다.
- include/linux/jiffies.h — Bootlin Elixir — jiffies 관련 매크로와 변환 함수 선언부입니다.
- LWN: A new approach to kernel timers (2005) — Thomas Gleixner의 hrtimer 프레임워크 최초 제안을 다룬 기사입니다.
- LWN: The high-resolution timer API (2006) — hrtimer API 설계와 사용법을 상세히 설명하는 기사입니다.
- LWN: Reinventing the timer wheel (2015) — Thomas Gleixner의 Timer Wheel 재설계(계층형 해시 휠)를 다룬 기사입니다.
- LWN: (Nearly) full tickless operation in 3.10 (2013) — NO_HZ_FULL 도입 배경과 구현을 설명하는 기사입니다.
- LWN: Clockevents and dyntick (2007) — clockevent 프레임워크와 dynamic tick의 관계를 설명하는 기사입니다.
- LWN: Dynamic tick for the x86 (2005) — x86 아키텍처에서 dynamic tick(NO_HZ) 최초 구현에 관한 기사입니다.
- LWN: Deferrable timers (2016) — TIMER_DEFERRABLE 플래그와 전력 절감 메커니즘을 다룬 기사입니다.
- LWN: Timer slack (2011) — timer_slack_ns와 타이머 병합(coalescing) 메커니즘을 설명하는 기사입니다.
- LWN: vDSO — virtual dynamic shared object (2010) — vDSO를 통한 고속 시간 조회 메커니즘에 관한 기사입니다.
- LWN: The tick broadcast framework (2017) — tick broadcast와 deep idle 상태 관리를 다룬 기사입니다.
- docs.kernel.org — Delaying and Scheduling Routines — msleep, usleep_range, schedule_timeout 등 지연 함수 API 공식 문서입니다.
- man timer_create(2) — POSIX per-process 타이머 생성 시스템 콜 매뉴얼입니다.
- man timerfd_create(2) — timerfd 인터페이스를 통한 파일 디스크립터 기반 타이머 매뉴얼입니다.
- man time(7) — 리눅스 시간 관련 개념(CLOCK_REALTIME, CLOCK_MONOTONIC 등)에 대한 개요 매뉴얼입니다.
- man clock_gettime(2) — clock_gettime/clock_settime 시스템 콜 매뉴얼로, vDSO 가속 대상 함수입니다.
- eLinux: Kernel Timer Systems — 커널 타이머 시스템에 대한 종합 개요와 발전 이력을 정리한 문서입니다.
- Thomas Gleixner — Timers and time management in the kernel (LPC 2017) — 커널 타이머 메인테이너 Thomas Gleixner의 발표로, hrtimer와 tickless 아키텍처를 설명합니다.
관련 문서
타이머와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.