타이머 (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)" 사이의 균형을 시스템 목적에 맞게 조정하는 실무 관점을 제공합니다.

관련 표준: IEEE 1588 (PTP, 정밀 시간 동기화), ACPI 6.5 (타이머 하드웨어, C-states) — 커널 타이머 서브시스템이 참조하는 시간 동기화 및 하드웨어 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 인터럽트(Interrupt)동기화 기법 문서를 먼저 읽으세요. 비동기 이벤트 처리 주제는 문맥 전환(Context Switch)과 지연 실행 경로를 정확히 구분해야 하므로, IRQ와 deferred work 경계를 먼저 잡아야 합니다.

핵심 요약

  • 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. 수 나노초의 오버헤드로 시간을 읽습니다.

단계별 이해

  1. 타이머 틱 — 타이머 하드웨어가 HZ 주기로 인터럽트를 발생시키고, 커널이 jiffies를 증가시킵니다.

    cat /proc/timer_list로 현재 활성 타이머를 확인할 수 있습니다.

  2. 저해상도 타이머mod_timer()로 jiffies 기반 타이머를 설정합니다. 밀리초 단위 정확도.

    네트워크 타임아웃, 디바이스 폴링(Polling) 등에 사용됩니다.

  3. 고해상도 타이머hrtimer_start()로 나노초 정밀 타이머를 설정합니다.

    POSIX 타이머, nanosleep(), 스케줄러 타임슬라이스(Time Slice)에 사용됩니다.

  4. tickless 확인grep CONFIG_NO_HZ /boot/config-$(uname -r)로 tickless 설정을 확인합니다.

    idle 상태에서 타이머 인터럽트를 생략하여 C-state 깊은 절전에 진입합니다.

  5. delayed_work 사용schedule_delayed_work(&dwork, msecs_to_jiffies(100))로 100ms 후 실행을 예약합니다.

    timer_list와 달리 프로세스 컨텍스트(kworker 스레드(Thread))에서 실행되므로 슬립(Sleep)/잠금(Lock) 사용이 가능합니다.

  6. watchdog 설정 확인cat /proc/sys/kernel/watchdog_thresh로 임계값(기본 10초)을 확인합니다.

    softlockup은 스케줄러 양보(Yield) 없음, hardlockup은 NMI로 인터럽트 없음을 감지합니다.

  7. 타이머 디버깅(Debugging)cat /proc/timer_list | head -60으로 만료 시각과 콜백(Callback) 함수를 확인합니다.

    cyclictest -m -n -p99 -l 10000으로 타이머 지터를 마이크로초 단위로 측정합니다.

  8. vDSO 타이밍clock_gettime(CLOCK_MONOTONIC, &ts) 호출은 syscall 없이 vDSO로 처리됩니다.

    strace ./my_prog를 실행하면 clock_gettime이 시스템 콜(System Call) 목록에 나타나지 않는 것을 확인할 수 있습니다.

타이머 서브시스템의 정의와 역할

타이머 서브시스템은 커널이 시간의 흐름을 인식하고 미래 시점에 작업을 예약할 수 있게 하는 핵심 인프라입니다. 이 서브시스템이 없으면 프로세스 스케줄링, 네트워크 타임아웃, 디바이스 폴링, 슬립 등 시간 기반 동작이 모두 불가능합니다.

타이머 서브시스템은 크게 세 가지 계층으로 구성됩니다:

타이머 서브시스템 계층 구조 하드웨어 계층 TSC, HPET, Local APIC Timer, ARM Arch Timer 프레임워크 계층 (clocksource / clockevent) 시간 측정 추상화 + 미래 인터럽트 프로그래밍 저해상도 타이머 (Timer Wheel) jiffies 단위, softirq 컨텍스트 timer_list, mod_timer() 고해상도 타이머 (hrtimer) 나노초 단위, HW 클럭 직접 프로그래밍 hrtimer, hrtimer_start()
타이머 서브시스템: 하드웨어 → clocksource/clockevent 프레임워크 → Timer Wheel / 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)의 원리로 동작합니다. 핵심 아이디어는 "가까운 미래의 타이머는 정밀하게, 먼 미래의 타이머는 대략적으로 관리"하는 것입니다:

ℹ️

설계 배경: 초기 커널(~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를 완전히 제거하는 대신 상위 레벨에 의도적 양자화 오차를 도입했습니다. 자세한 버전별 오차 비교는 호출 시간 오차 분석을 참고하세요.

Hierarchical Timer Wheel Level 0 64 slots (0~63 ticks) Level 1 64 slots (64~4095) Level 2 64 slots (~262K) Level 3 64 slots (~16M) Level 0 상세 (tick 단위 슬롯): 0 1 2 3 ... 63 current
계층적 Timer Wheel: 가까운 만료 시간은 Level 0, 먼 시간은 상위 레벨에서 관리
#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.hkernel/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 5단계 내부 구조 (kernel/time/timer.c) struct timer_base (Per-CPU) lock (raw_spinlock_t) | running_timer | clk (현재 jiffies) | next_expiry | pending_map base[0] (Level 0) Granularity: 1 tick (4ms@250Hz) | 범위: 0~63 ticks (0~252ms) ... clk [63] 64 slots x 1 tick = 64 ticks 커버 cascade (8x) base[1] (Level 1) Granularity: 8 ticks (32ms) | 범위: 64~511 ticks (256ms~2.05s) 64 slots x 8 ticks = 512 ticks cascade (8x) base[2] (Level 2) Granularity: 64 ticks (256ms) | 범위: 512~4095 ticks (2s~16.4s) 64 slots x 64 ticks = 4096 ticks cascade (8x) base[3] (Level 3) Granularity: 512 ticks (2.05s) | 범위: 4096~32767 ticks (16s~2.2min) 64 slots x 512 ticks cascade (8x) base[4] (Level 4) Granularity: 4096 ticks (16.4s) | 범위: 32768~262143 ticks (2.2min~17.5min) 64 slots x 4096 ticks 전체 커버 범위: 0 ~ 2^(6+3*5) = 2^21 ticks (HZ=250 기준 약 8738초 = ~2.4시간, 확장 시 ~198일)

Timer Wheel 슬롯(Bucket) 내부 구조

각 레벨의 64개 슬롯은 hlist_head로 구현되며, 동일 슬롯에 여러 타이머가 해시 리스트로 연결됩니다. 타이머 삽입 시 hlist_add_head()로 O(1) 삽입, 만료 시 hlist_move_list()로 전체 슬롯을 O(1)에 수집합니다.

Timer Wheel 슬롯(Bucket) 내부 구조: vectors[] + hlist vectors[0..63] (Level 0) [0] NULL [1] Timer A exp:1001 Timer B exp:1001 → NULL [2] [3] Timer C exp:1003 ... [63] pending_map 비트맵 (320 bits) Level 0: 0 1 0 1 ... (64 bits) Level 1: ... (64 bits) Level 2-4: ... (192 bits) 1 = 타이머 존재 삽입 경로 (enqueue_timer) 1. calc_wheel_index(expires, clk) → idx, bucket_expiry 2. hlist_add_head(&timer->entry, &vectors[idx]) 3. __set_bit(idx, pending_map) // O(1) 4. if (bucket_expiry < next_expiry) next_expiry = bucket_expiry; trigger_dyntick_cpu(); // idle CPU 깨움 전체 O(1) — lock 내에서 수행 만료 수집 (collect_expired_timers) 1. 현재 clk에서 각 레벨의 슬롯 인덱스 계산 2. test_bit(idx, pending_map) // O(1) 존재 확인 3. 비트가 1이면: hlist_move_list() → 수집 리스트 4. __clear_bit(idx, pending_map) 5. clk >>= 3 // 상위 레벨로 이동 6. expire_timers(): lock 해제 후 콜백 실행 전체 O(레벨 수) = O(5) — 비트맵 최적화
Timer Wheel의 각 슬롯은 hlist(해시 연결 리스트)이며, pending_map 비트맵으로 비어있지 않은 슬롯을 O(1)에 식별합니다.
Timer Wheel 각 레벨 세부 사양 (HZ=250 기준)
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.ccalc_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.ccalc_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 값이 어떤 레벨, 어떤 슬롯에 배치되고, 양자화 오차가 얼마인지 확인합니다.

calc_index() 양자화 계산 예제 (clk = 1000)
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의 원래 만료 시각 차이가 존재합니다.
calc_index() 양자화 과정: expires=1100, Level 1 (shift=3, granularity=8) 1단계: expires = 1100 이진수: 0 1 0 0 0 1 0 0 0 1 0 0 하위 3비트 = 4 = 0b 010 001 001 100 >> 3 (÷ 8) 2단계: shifted = 1100 >> 3 = 137 이진수: 0 0 1 0 0 0 1 0 0 = 0b 10 001001 하위 6비트 & 0x3F = 9 (slot) 3단계: slot = LVL_OFS(1) + (137 & 63) = 64 + 9 = 73 << 3 (× 8) 4단계: bucket_expiry = 137 << 3 = 1096 이진수: 0 1 0 0 0 1 0 0 0 0 0 0 = 0b 010 001 001 000 0으로 채워짐! 결과: expires=1100 → slot[73], bucket_expiry=1096, 양자화 오차 = 1100 − 1096 = 4 ticks (16ms@250Hz / 4ms@1000Hz)
calc_index() 양자화 과정: expires=1100이 Level 1(shift=3)에서 오른쪽 시프트로 하위 3비트(값=4)가 손실되고, 왼쪽 시프트로 복원 시 0으로 채워져 bucket_expiry=1096이 됩니다.
ℹ️

Cascade 동작: 상위 레벨의 타이머가 해당 레벨의 granularity 경계에 도달하면, 하위 레벨로 재배치(cascade)됩니다. 예를 들어 Level 1의 타이머가 64 ticks 이내로 남으면 Level 0으로 이동합니다. 이 과정은 __run_timers()에서 collect_expired_timers() 호출 시 자동으로 수행됩니다. 대부분의 네트워크 타임아웃 타이머는 설정 후 취소되므로 cascade가 실제로 발생하는 빈도는 낮습니다.

Timer Wheel 호출 시간 오차 분석

Timer Wheel 타이머는 mod_timer()로 지정한 만료 시각과 실제 콜백 호출 시점 사이에 다양한 원인의 오차(Jitter)가 발생합니다. 커널 타이머의 근본 원칙은 "지정된 시점에 정확히 실행"이 아니라 "지정된 시점 이후 가능한 빨리 실행"이므로, 콜백은 항상 요청 시각보다 늦게 호출됩니다. 이 섹션에서는 오차를 유발하는 각 원인과 레벨별 최대 오차를 종합적으로 분석합니다.

Timer Wheel 호출 시간 오차 원인 분석 (HZ=250 / HZ=1000 비교)
오차 원인 최소 오차 최대 오차 (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 커널 스레드에 위임합니다. ksoftirqdSCHED_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)으로 조정 가능합니다.

Timer Wheel 호출 시간 오차 타임라인 (최악의 경우) 시간 mod_timer() 타이머 설정 expires 요청 만료 시각 bucket_expiry 양자화된 만료 tick IRQ 틱 인터럽트 softirq softirq 실행 callback() 실제 콜백 호출 양자화 오차 Level별: 0 ~ 16.4s@250Hz / 0 ~ 4.1s@1000Hz 틱 대기 0 ~ 1 tick (4ms@250Hz / 1ms@1000Hz) softirq 지연 ~0 ~ 수십ms 큐 처리 시간 앞선 콜백 실행 시간 총 오차 = 양자화 + 틱 대기 + softirq 지연 + 큐 처리 TIMER_DEFERRABLE 설정 시: 위 모든 오차에 추가로 idle 탈출 대기 시간이 가산됩니다 (무제한) PREEMPT_RT 커널: softirq가 ksoftirqd 스레드에서 실행 → 스케줄링 지연 추가 (수 ms)
Timer Wheel 타이머의 설정(mod_timer)부터 실제 콜백 호출까지의 지연 구간. 각 구간이 누적되어 총 오차를 형성합니다.
Timer Wheel 레벨별 최대 오차 종합 — HZ=250 (HZ=250 기준, TIMER_DEFERRABLE 미사용)
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
Timer Wheel 레벨별 최대 오차 종합 — HZ=1000 (HZ=1000 기준, TIMER_DEFERRABLE 미사용)
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 구현과 오차 특성 비교
커널 버전 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 vs 4.8+ Timer Wheel 오차 직접 비교 (HZ=250 기준)
오차 특성 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 = 2BASE_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.cenqueue_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로 관리되며, 하드웨어 클럭 이벤트에 직접 프로그래밍합니다.

struct hrtimer_cpu_base active_bases (RB-Tree 루트) expires_next: 다음 만료 시각 Timer C expires: 100ms Timer A expires: 50ms ← leftmost (다음 만료) leftmost (O(1)) Timer D expires: 200ms Timer B exp: 30ms Timer E exp: 80ms 만료 시각이 가장 빠른 타이머(leftmost)를 clockevent에 프로그래밍

hrtimer 동작 원리

hrtimer가 Timer Wheel과 근본적으로 다른 점은 틱에 의존하지 않습니다는 것입니다:

이 방식은 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.hkernel/time/hrtimer.c에 정의된 고해상도 타이머 기본 사용 패턴입니다.

  • hrtimer_init()struct hrtimer를 초기화합니다. 클럭 베이스(CLOCK_MONOTONIC)와 모드(HRTIMER_MODE_REL: 상대 시간)를 지정합니다. 내부에서 per-CPU hrtimer_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)에 접근할 수 있습니다.

hrtimer 레드블랙 트리 + timerqueue 구조 struct hrtimer_clock_base cpu_base | index | clockid | active (timerqueue_head) get_time() | offset struct timerqueue_head rb_root_cached (rbtree + leftmost) 150ns (black) 80ns (red) 300ns (black) 50ns (black) leftmost 120ns (red) 200ns (red) 500ns (black) struct timerqueue_node node (rb_node) | expires (ktime_t) hrtimer 내에 timerqueue_node 내장 struct hrtimer node (timerqueue_node) | _softexpires function | base | state | is_rel
/* 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.hinclude/linux/hrtimer.h에 정의된 hrtimer 핵심 자료구조입니다.

  • timerqueue_nodeRed-Black tree 노드(rb_node)와 만료 시각(expires, 나노초)을 포함합니다. hrtimer 내에 내장되어 timerqueue에 삽입됩니다.
  • timerqueue_headrb_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 / seqrunning은 현재 콜백 실행 중인 타이머를 가리켜 hrtimer_cancel()의 동기 대기를 지원합니다. seq는 seqcount로, 타이머 상태 변경의 원자성을 보장합니다.

hrtimer 클럭 베이스 4종

hrtimer는 4종의 클럭 베이스를 지원합니다. 각 클럭 베이스는 독립적인 timerqueue(RB-tree)를 유지하며, 사용 목적에 따라 적절한 클럭을 선택해야 합니다.

hrtimer 클럭 베이스 4종 비교 hrtimer_cpu_base clock_base[8] (hard + soft) MONOTONIC CLOCK_MONOTONIC NTP 조정 없음 suspend 미포함 스케줄러, nanosleep REALTIME CLOCK_REALTIME NTP 조정 가능 벽시계 (wall clock) POSIX 타이머, cron BOOTTIME CLOCK_BOOTTIME NTP 조정 없음 suspend 시간 포함 Android 알람, 세션 추적 TAI CLOCK_TAI 윤초 없음 국제원자시 PTP, 금융 시간 관계 다이어그램 boot suspend resume now MONOTONIC BOOTTIME NTP 보정으로 점프 가능 REALTIME TAI REALTIME + tai_offset (윤초 보정 없음)
hrtimer 클럭 베이스 4종 상세 비교
클럭 베이스 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_BOOTTIMECLOCK_MONOTONIC은 모두 단조 증가 클럭이지만, suspend/resume 시간 포함 여부에서 결정적으로 다릅니다. 이 차이를 무시하면 suspend 후 타이머가 의도와 다르게 동작하는 미묘한 버그가 발생합니다.

CLOCK_BOOTTIME vs CLOCK_MONOTONIC: suspend 구간의 시간 차이 시간 boot suspend resume now suspend 구간 (4시간) MONO +220s 정지 (0s 경과) +260s 총 480s BOOT +220s +14400s 포함 +260s 총 14880s CLOCK_MONOTONIC 사용 사례 - 경과 시간 측정 (suspend 제외가 맞는 경우) - 스케줄러 타임슬라이스, nanosleep() - 네트워크 RTT 측정, 성능 벤치마크 CLOCK_BOOTTIME 사용 사례 - 세션 타임아웃 (suspend 동안에도 시간 경과 필요) - Android 알람, 웨이크업 타이머 - 시스템 가동 시간(uptime) 추적, 임베디드 워치독
CLOCK_MONOTONIC은 suspend 구간 동안 멈추고, CLOCK_BOOTTIME은 suspend 시간을 포함하여 계속 흐릅니다.
CLOCK_BOOTTIME vs CLOCK_MONOTONIC 시나리오별 동작 비교
시나리오 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_boottimekeeping_resume()에서 suspend 경과 시간만큼 갱신됩니다.
  • 세션 타임아웃 패턴세션 타임아웃은 "실제 시간이 경과했는지"를 판단하는 것이므로 BOOTTIME이 적합합니다. MONOTONIC을 사용하면 suspend 동안 세션이 연장되는 버그가 발생합니다.
  • hrtimer에서 BOOTTIMEhrtimer_init()CLOCK_BOOTTIME을 전달하면 hrtimer_clock_base[HRTIMER_BASE_BOOTTIME]에 연결됩니다. suspend 이후에도 정확한 시점에 타이머가 만료됩니다.
💡

CLOCK_BOOTTIME_ALARM: CLOCK_BOOTTIME_ALARMCLOCK_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를 자동 선택합니다.
clocksource rating 등급 체계
등급rating 범위설명대표 클럭
Perfect400+HW 보장 최고 정밀도TSC invariant (400)
Desired300~399고품질, 대부분 적합ARM Arch Timer (350)
Good200~299사용 가능, 더 나은 대안 존재 시 대체HPET (250), ACPI PM (200)
Base100~199기본 폴백 소스PIT (110)
Undesirable1~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 등록과 선택 흐름

clocksource 등록 → 선택 → timekeeping 연결 드라이버 등록 clocksource_register_hz() mult/shift 계산 clocks_calc_mult_shift() rating순 삽입 → 최적 선택 clocksource_enqueue() → clocksource_select() timekeeping_notify() tk_core.timekeeper에 새 clocksource 반영 clocksource watchdog (0.5초 주기) TSC/jiffies 교차 비교, 오차 ±12.5% 초과 시 UNSTABLE → 폴백 clockevent 등록 흐름 clockevents_register_device() clockevent_devices 리스트에 추가 tick_check_new_device() rating 비교 → 더 높으면 교체 tick_setup_device() event_handler 설정, 모드 전환 ONESHOT 지원 → 고해상도 전환 트리거 hrtimer_switch_to_hres() → handler = hrtimer_interrupt PERIODIC만 → 저해상도 유지 handler = tick_handle_periodic, HZ 주기 인터럽트
/* 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));
}

지연 함수 선택 결정 트리

지연이 필요한가? 인터럽트 컨텍스트? 지연 범위 (busy-wait 전용) ndelay() / udelay() mdelay() (ms 이상, busy-wait) 아니오 지연 시간 범위? < 10 µs udelay() busy-wait 10 µs~20 ms usleep_range(min, max) hrtimer_nanosleep() — 고정밀 sleep > 20 ms msleep() schedule_timeout — jiffy 정밀도 fsleep(usecs) 범위 자동 판단 통합 API (Linux 5.10+)

컨텍스트별 사용 예

#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을 제거하여 전력 소모를 줄입니다.

CPU idle 진입 다음 타이머 만료 시각 검사 (Timer Wheel + hrtimer 통합) 주기적 tick 중단 clockevent를 oneshot 모드로 전환 다음 만료 시각에만 인터럽트 설정 깊은 C-state 진입 전력 절약 (CPU 클럭/전압 감소) 타이머 만료 인터럽트 CPU 깨어남, jiffies 보정 타이머 콜백 실행 → idle 재진입 전통적 커널: HZ마다 틱 (예: 4ms@250Hz) → 전력 낭비 Tickless: 필요 시점만 인터럽트 → 전력 절약

Tickless 동작 원리

Tickless의 핵심 원리는 "다음에 해야 할 일이 없으면 깨우지 않습니다"입니다:

  1. CPU가 idle에 진입하기 전, 다음으로 만료될 타이머(Timer Wheel + hrtimer)의 시간을 확인합니다.
  2. 그 시간까지 주기적 틱 인터럽트를 중단하고, 대신 하나의 oneshot clockevent만 해당 시점에 프로그래밍합니다.
  3. CPU는 깊은 C-state(저전력 상태)에 진입하여 전력을 절약합니다.
  4. 타이머 만료 시점에 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), 저지연 워크로드
NO_HZ_FULL 요구사항: 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 하드웨어의 재프로그래밍이 어떤 순서로 이루어지는지 전체 흐름을 분석합니다. 이 과정은 타이머 정확도와 전력 효율의 핵심 경로입니다.

NO_HZ idle 진입/탈출 타이머 재프로그래밍 흐름 idle 진입 경로 do_idle() / cpuidle_idle_call() tick_nohz_idle_enter() tick_nohz_idle_stop_tick() next_expiry = min(timer_wheel, hrtimer) tick_program_event(next_expiry, 1) clockevent HW 프로그래밍 set_next_event(delta_cycles) → APIC/Arch Timer C-state 진입 (CPU halt) idle 구간 수 ms ~ 수 초 idle 탈출 경로 clockevent 인터럽트 발생! hrtimer_interrupt() / tick handler tick_nohz_idle_exit() tick_nohz_idle_restart_tick() tick_nohz_update_jiffies(now) jiffies 보정 + tick 복원 tick_do_update_jiffies64() + tick_setup_sched_timer() __run_timers() + hrtimer 만료 처리 경합 조건 (Race Condition): idle 진입 직전 타이머 삽입 다른 CPU에서 idle CPU에 새 타이머를 삽입하면: enqueue_timer() → trigger_dyntick_cpu() → 대상 CPU에 IPI 전송 IPI가 idle CPU를 깨워 clockevent를 재프로그래밍합니다. IPI 전달 전에 idle 진입이 완료되면 최대 1 tick 지연이 발생할 수 있습니다. 해결책: timer_base->is_idle 플래그로 idle 상태를 추적하고, idle CPU에 타이머 삽입 시 IPI를 즉시 전송합니다.
NO_HZ idle 진입 시 clockevent를 다음 만료 시각으로 재프로그래밍하고, 탈출 시 jiffies를 보정한 후 주기적 tick을 복원합니다.
ℹ️

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)을 더합니다.

clocksource read() mult / shift 변환 → ns timekeeping_get_ns() ktime_get() CLOCK_MONOTONIC ktime_get_real() CLOCK_REALTIME ktime_get_boottime() CLOCK_BOOTTIME ktime_get_raw() NTP 보정 없음 오프셋 없음(기준 0) + wall_to_monotonic + monotonic_to_boottime raw clocksource 직접
/* 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_ts64나노초(ns) 값으로 변환
ktime_to_us(kt)ktime_ts64마이크로초(µs)로 변환 (ns / 1,000)
ktime_to_ms(kt)ktime_ts64밀리초(ms)로 변환 (ns / 1,000,000)
ktime_to_timespec64(kt)ktime_ttimespec64초/나노초 구조체로 변환
ktime_to_timeval(kt)ktime_ttimeval초/마이크로초 구조체로 변환 (레거시)
ns_to_ktime(ns)s64ktime_t나노초 정수를 ktime_t로 변환
ms_to_ktime(ms)s64ktime_t밀리초를 ktime_t로 변환
ktime_set(sec, ns)(s64, long) → ktime_t초 + 나노초로 ktime_t 생성
timespec64_to_ktime(ts)timespec64ktime_ttimespec64 구조체를 ktime_t로 변환

산술 및 비교 연산

ktime_ts64 별칭이므로 단순 덧셈·뺄셈도 동작하지만, 커널은 가독성과 오버플로 방지를 위해 전용 헬퍼(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를 초기화하여 연결합니다.

POSIX 타이머 라이프사이클: 유저스페이스 → 커널 내부 timer_create() 유저스페이스 syscall do_timer_create() k_itimer 할당 + hrtimer_init timer_settime() → hrtimer_start() 타이머 활성 RB-tree에 삽입됨 hrtimer 만료 posix_timer_fn() 콜백 시그널 전달 __send_signal() → 유저에 SIGALRM 주기적: hrtimer_forward() → 재삽입, overrun 카운트 timer_delete() hrtimer_cancel + k_itimer 해제 현대적 대안: timerfd_create() / timerfd_settime() 파일 디스크립터(FD) 기반 — epoll/select로 대기 가능, 시그널 불필요
/* 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() 시스템 콜로 확인할 수 있으며, 실시간 시스템에서 타이머 누락을 감지하는 데 사용됩니다.
POSIX 타이머 시그널 통지 방법
통지 방법sigev_notify 값동작사용 사례
SIGEV_SIGNALSIGEV_SIGNAL지정 시그널(기본 SIGALRM)을 프로세스에 전달전통적 타이머, 간단한 주기 작업
SIGEV_THREAD_IDSIGEV_THREAD_ID특정 스레드에 시그널 전달멀티스레드 서버의 특정 워커에 통지
SIGEV_NONESIGEV_NONE시그널 없음, timer_gettime()으로 폴링오버헤드 최소화, 능동적 상태 확인
SIGEV_THREADSIGEV_THREADglibc가 내부 스레드를 생성하여 콜백 실행시그널 핸들러 대신 일반 함수 호출 원할 때

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와 overrunread() 호출 사이에 타이머가 여러 번 만료되면 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 시간 계정, 스케줄러 시간 슬라이스 검사, 저해상도 타이머 처리 등이 순차적으로 수행됩니다.

매 틱 처리 체인: tick_periodic() → update_process_times() 타이머 인터럽트 (Local APIC / Arch Timer) HZ 주기 또는 oneshot 만료 tick_periodic() / tick_sched_timer() do_timer() + update_process_times() 호출 do_timer(ticks) jiffies_64 += ticks, timekeeping_advance() update_process_times(user_tick) 프로세스 시간 계정 + 타이머 + 스케줄러 account_process_tick() user/system/irq/softirq 시간 계정 (cputime) run_local_timers() hrtimer_run_queues() raise_softirq(TIMER_SOFTIRQ) scheduler_tick() CFS vruntime 갱신 선점 검사, 부하 균형 트리거 기타: profile_tick() | rcu_sched_clock_irq() | perf_event_task_tick() | posix_cpu_timer_schedule() 프로파일링, RCU grace period 추적, perf 이벤트, POSIX CPU 타이머 처리 timekeeping_advance(): clocksource read → cycle→ns 변환 → tk_core 갱신 → update_vsyscall() → vDSO 페이지 갱신
/* 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/stattop 명령이 보여주는 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() 흐름 (저해상도)

Timer 콜백 실행 흐름: tick → softirq → __run_timers Timer Tick (Local APIC 인터럽트) hrtimer_interrupt() / tick_handle_periodic() tick_sched_handle() update_process_times() 호출 run_local_timers() raise_softirq(TIMER_SOFTIRQ) TIMER_SOFTIRQ 처리 run_timer_softirq() → __run_timers(base) __run_timers(struct timer_base *base) 1. raw_spin_lock_irq(&base->lock) 2. collect_expired_timers() - 만료된 타이머를 리스트로 수집 3. base->clk 갱신 (forward), expire_timers() 호출 expire_timers() base->running_timer = timer; fn = timer->function; timer->function(timer) 사용자 정의 콜백 실행 (softirq context) hard IRQ context softirq context
/* 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_SOFTIRQrun_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_timerbase->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.chrtimer_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-CPU timer_base를 초기화하고, open_softirq()TIMER_SOFTIRQ 벡터에 run_timer_softirq 핸들러를 등록합니다.
  • run_timer_softirq()TIMER_SOFTIRQ 핸들러입니다. BASE_STD(일반 타이머)를 먼저 처리한 후, NO_HZ_FULL CPU가 아니면 BASE_DEF(deferrable 타이머)도 처리합니다.
  • tick_nohz_full_cpu()현재 CPU가 NO_HZ_FULL 모드인지 확인합니다. NO_HZ_FULL CPU에서는 deferrable 타이머 처리를 건너뛰어 불필요한 틱을 최소화합니다.
  • run_local_timers()매 타이머 틱마다 호출됩니다. hrtimer_run_queues()로 저해상도 모드의 hrtimer를 처리한 후, 만료된 타이머가 있으면 raise_softirq(TIMER_SOFTIRQ)로 softirq를 트리거합니다.
  • next_expiry 비교BASE_STDBASE_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 서브시스템 아키텍처 User Space hwclock --systohc --hctosys /dev/rtc0 ioctl: RTC_RD_TIME RTC_SET_TIME sysfs /sys/class/rtc/rtc0/ time, date, wakealarm /proc/driver/rtc CMOS RTC 상세 (x86 전용) rtcwake suspend + alarm wakeup Kernel Space rtc-core (drivers/rtc/class.c) struct rtc_device, rtc_class_ops rtc_read_time() / rtc_set_time() / rtc_set_alarm() Hardware Drivers rtc-cmos.c CMOS RTC (x86) I/O 0x70/0x71 rtc-ds1307.c I2C RTC DS1307/DS3231 rtc-pl031.c PL031 (ARM) MMIO PrimeCell rtc-*.c (기타) SoC 내장 RTC SPI, MMIO, ... rtc_class_ops callbacks
RTC 서브시스템 아키텍처 — User space(hwclock, /dev/rtc0, sysfs) → rtc-core → 하드웨어 드라이버(CMOS, I2C, PL031 등)

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 레지스터 접근 & 맵 I/O 포트 인덱스 접근 방식 Port 0x70 Index Register (W) select Port 0x71 Data Register (R/W) outb(index, 0x70); val = inb(0x71); NMI 비활성화: index | 0x80 (bit[7] = NMI disable) 시간/날짜 레지스터 0x00 Seconds 0x02 Minutes 0x04 Hours 0x06 Day of Week 0x07 Day 0x08 Month 0x09 Year 0x32 Century 0x01 AlmSec 0x03 AlmMin 0x05 AlmHr Status Registers Status A (0x0A) [7] UIP (Update In Progress) [6:4] DV — divider 선택 [3:0] RS — rate 선택 R/W (UIP는 R/O) Status B (0x0B) [7] SET — 업데이트 억제 [6] PIE [5] AIE [4] UIE [3] SQWE [2] DM [1] 24/12 R/W Status C (0x0C) [7] IRQF (IRQ 활성) [6] PF (Periodic Flag) [5] AF (Alarm Flag) R/O (읽으면 클리어) Status D (0x0D) [7] VRT (Valid RAM/Time) 1=배터리 유효 0=배터리 소진(시간 무효) R/O UIP(Update-In-Progress) 비트 확인 흐름 1. Status A 읽기 2. UIP=1? Yes → 대기 No → 시간 레지스터 읽기 UIP=1: RTC가 카운터를 업데이트 중 (~244μs). 이 동안 시간 레지스터 읽기는 불일치 데이터를 반환할 수 있음
CMOS RTC 레지스터 맵 — Port 0x70(Index)/0x71(Data) 방식으로 접근. 시간/날짜 레지스터, 알람 레지스터, Status A~D로 구성

CMOS RTC 전체 레지스터 맵

인덱스이름R/W설명
0x00SecondsR/W현재 초 (0-59, BCD 또는 바이너리)
0x01Seconds AlarmR/W알람 초 (0xC0~0xFF = don't care)
0x02MinutesR/W현재 분 (0-59)
0x03Minutes AlarmR/W알람 분
0x04HoursR/W현재 시 (12H: 1-12+PM bit, 24H: 0-23)
0x05Hours AlarmR/W알람 시
0x06Day of WeekR/W요일 (1=일요일, 7=토요일)
0x07Day of MonthR/W일 (1-31)
0x08MonthR/W월 (1-12)
0x09YearR/W연도 하위 2자리 (00-99)
0x0AStatus Register AR/WUIP, divider, rate select
0x0BStatus Register BR/WSET, PIE, AIE, UIE, SQWE, DM, 24/12
0x0CStatus Register CR/OIRQ 플래그 (읽으면 클리어됨)
0x0DStatus Register DR/OVRT (Valid RAM and Time)
0x32CenturyR/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]SETR/W1=시간 업데이트 억제 (시간 설정 시 사용). 0=정상 카운트
[6]PIE (Periodic Interrupt Enable)R/W1=RS 주파수로 IRQ8 발생
[5]AIE (Alarm Interrupt Enable)R/W1=알람 시각 도달 시 IRQ8 발생
[4]UIE (Update-ended Interrupt Enable)R/W1=매초 업데이트 완료 시 IRQ8 발생
[3]SQWE (Square Wave Enable)R/W1=SQW 출력 핀에 구형파 생성 (레거시)
[2]DM (Data Mode)R/W0=BCD, 1=Binary. 시간 레지스터의 데이터 형식
[1]24/12R/W0=12시간 모드, 1=24시간 모드
[0]DSE (Daylight Saving Enable)R/W1=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 주의사항

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_worktimer_listworkqueue를 결합한 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));
delayed_work vs timer_list vs hrtimer 비교
특성 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 두 종류가 있으며, 각각 다른 타이머 메커니즘을 사용합니다.

커널 Watchdog 감지 메커니즘 Softlockup 감지 CONFIG_SOFTLOCKUP_DETECTOR hrtimer (Per-CPU) watchdog_thresh/5 초마다 타임스탬프 갱신 watchdog/N kthread 스케줄링됨 → 타임스탬프 갱신 타임스탬프 > watchdog_thresh 초 → "soft lockup" 경고/BUG 출력 Hardlockup 감지 CONFIG_HARDLOCKUP_DETECTOR hrtimer (Per-CPU) watchdog_thresh/5 초마다 hrtimer_interrupts 카운터 증가 NMI Watchdog (PMU 이벤트) hrtimer_interrupts 카운터 변화 확인 카운터 미증가 → NMI 핸들러 → "hard lockup" 패닉/KDB 진입
Softlockup: hrtimer + kthread로 스케줄링 멈춤 감지 / Hardlockup: NMI watchdog으로 인터럽트 멈춤 감지

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 루프 (전력 절약 비활성화)
Watchdog 개발 주의사항:
  • 긴 임계 섹션 — 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_tswatchdog/N kthread가 마지막으로 스케줄링된 시각입니다. 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/N kthread는 스케줄러를 통해 실행되므로, 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 정보. 각 필드의 의미를 정확히 파악해야 실전 디버깅에 활용할 수 있습니다.

/proc/timer_list hrtimer 섹션 필드 해석
필드 예시 의미 진단 활용
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

vDSO 데이터 흐름: 커널 → vvar 페이지 → 유저 공간 커널 timekeeping timekeeping_advance() 매 tick마다 tk_core 갱신 update_vsyscall() vdso_write_begin() (seq++) vdso_data 필드 갱신 → seq++ vvar 페이지 (공유 메모리) 커널 → 유저 공간 읽기 전용 매핑 프로세스 주소 공간에 자동 매핑 __vdso_clock_gettime() — 유저 공간 실행 1. seq 읽기 → 2. rdtsc → 3. ns = basetime + (tsc - cycle_last) * mult >> shift 4. seq 재확인 (변경되었으면 1부터 재시도) → 5. 결과 반환 Syscall 폴백 조건 clock_mode < 0 (TSC 미지원 clocksource) 지원 안되는 clockid → 일반 syscall 경로 성능: vDSO ~15-25ns vs syscall ~200-300ns (약 10배 차이) 고빈도 시간 읽기(초당 수백만 회)에서 vDSO는 필수적 — 네트워킹, 데이터베이스, 트레이싱 등
/* 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 지원 함수와 폴백 조건
vDSO 함수대응 syscall지원 clockid폴백 조건
__vdso_clock_gettimeclock_gettime()REALTIME, MONOTONIC, BOOTTIME, TAI, *_COARSEclock_mode == NONE, 미지원 clockid
__vdso_gettimeofdaygettimeofday()REALTIMEclock_mode == NONE
__vdso_clock_getresclock_getres()모든 clockid없음 (항상 vDSO로 처리)
__vdso_timetime()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을 중단하여 사용자 공간 작업에 대한 커널 간섭을 최소화합니다.

Tickless (NO_HZ) 모드 전환 흐름 태스크 실행 중 tick 활성 (주기적 인터럽트) run queue 비어짐 run queue == 1 태스크 NO_HZ_IDLE 모드 진입 tick_nohz_idle_enter() CPU idle + tick 중단 다음 타이머 만료 시각 계산 clockevent oneshot 프로그래밍 cpuidle_enter() C-state 진입 (전력 절약) 인터럽트로 깨어남 → tick 재개 NO_HZ_FULL 모드 진입 tick_nohz_full_cpu() 단일 태스크 실행 + tick 중단 RCU 콜백 오프로딩 (rcu_nocbs= 설정 필요) 스케줄러 통계 갱신 보류 사용자 공간 실행 커널 간섭 최소 (1Hz로 감소) syscall/IRQ 시 tick 재개 IDLE: 전력 절약 | FULL: 저지연 + 전력 절약 (HPC, 실시간)
NO_HZ_IDLE vs NO_HZ_FULL 상세 비교
특성 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 계층 (Per-CPU timer_base, 그룹 마이그레이션) Timer Migration Group (tmigr) 전역 다음 만료 시각 추적 | 그룹 계층 관리 CPU 0 (Busy) timer_base[STD]: T1, T2, T5 timer_base[DEF]: T6, T7 tick 활성, 타이머 처리 가능 CPU 1 (Idle) timer_base[STD]: T3 (pinned) timer_base[DEF]: (비어있음) tick 중단, C-state 진입 CPU 2 (Idle) timer_base[STD]: (비어있음) timer_base[DEF]: (비어있음) tick 중단, 깊은 C-state 비-pinned 타이머 마이그레이션 마이그레이션 가능한 타이머 - 일반 timer_list (non-pinned) - deferrable 타이머 - idle CPU에서 busy CPU로 이동 - 그룹 계층에서 가장 가까운 busy CPU sysctl: /proc/sys/kernel/timer_migration 활성화 시 idle CPU의 슬립 시간 연장 마이그레이션 불가 (Pinned) 타이머 - TIMER_PINNED 플래그 설정 - Per-CPU 전용 타이머 (예: watchdog) - hrtimer (항상 로컬 CPU에서 실행) - 특정 CPU에서만 실행해야 하는 작업 add_timer_on(timer, cpu) 으로 고정 idle 진입 시에도 해당 CPU에서 처리
/* 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 Node 0 CPU 0 (idle) Timer T1 등록 CPU 1 (idle) C-state 진입 로컬 메모리: T1 콜백이 접근하는 데이터 접근 지연: ~80ns NUMA Node 1 CPU 4 (busy) 타이머 처리 가능 CPU 5 (busy) 태스크 실행 중 로컬 메모리: Node 1 데이터 접근 지연: ~80ns T1 마이그레이션 (Node 0 → Node 1) 원격 메모리 접근: ~200ns (2.5배 지연) 로컬 노드 실행 (마이그레이션 없음) 콜백 데이터 접근: ~80ns (로컬) 캐시 히트율: 높음 (L3 공유) 원격 노드로 마이그레이션 콜백 데이터 접근: ~200ns (원격, QPI/UPI 경유) 캐시 미스 증가, L3 캐시 효과 상실
NUMA 환경에서 타이머가 원격 노드로 마이그레이션되면 콜백의 데이터 접근 지연이 2~3배 증가합니다.
NUMA 타이머 마이그레이션 영향 요약
항목 같은 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 선택 가이드
요구사항 권장 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);
}
코드 설명

타이머 서브시스템에서 발생하는 대표적인 데드락 패턴과 해결 방법입니다.

  • 데드락 패턴 1del_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 모드 구분이 중요해집니다. 실시간 시스템에서 타이머를 사용할 때 이러한 차이를 이해해야 예측 가능한 지연 시간을 달성할 수 있습니다.

일반 커널 vs PREEMPT_RT 커널 타이머 동작 비교
항목 일반 커널 (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->lockraw_spinlock_t이므로 RT에서도 spinning입니다. 하지만 TIMER_SOFTIRQ 자체가 ksoftirqd 스레드에서 실행되므로, 타이머 콜백의 실행 컨텍스트는 프로세스 컨텍스트가 됩니다.
PREEMPT_RT 타이머 사용 가이드라인:
  • 대부분의 드라이버 — 기본 모드(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: Housekeeping CPU와 Isolated CPU 분리 Housekeeping CPU (예: CPU0) 주기 tick 유지 (scheduler tick) RCU callback 처리 (rcu_nocbs 대상 수집) unbound workqueue / timer migration ksoftirqd, watchdog, balancing 수행 Isolated CPU (예: CPU1-7) 단일 태스크 실행 시 periodic tick 정지 사용자 태스크 중심 실행 (지터 최소화) 불가피한 IRQ/NMI만 수신 housekeeping 작업 유입 시 지터 증가 callback offload timer/work migration
# 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: 분산 만료를 묶어 wakeup 횟수 축소 슬랙 없음 4회 wakeup 슬랙 20ms 묶음 A 묶음 B 2회 wakeup
/* 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 보정을 적용해 장기 오차를 줄입니다.

clocksource 카운터 → timekeeping 변환 → NTP 보정 TSC/HPET 카운터 cycle 값 읽기 mult/shift 변환 cycle → ns tk_core 누적 monotonic/realtime vDSO export 유저 공간 사용 NTP 보정 루프 (adjtimex/chronyd/ntpd) 오프셋(offset), 지터, 주파수 오차(ppm) 추정 timekeeping freq/phase 보정값 반영 장기적으로 drift 감소, 단기적으로 slew/step 정책 적용
/* 시간 읽기와 변환 흐름 요약 */
#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);

타이머 트러블슈팅 플레이북

지연/지터 문제는 "타이머가 느린가"보다 "어떤 경로에서 늦어지는가"를 분리해야 해결됩니다. 아래 순서로 계측하면 원인을 빠르게 좁힐 수 있습니다.

Timer/HRTimer 지연 분석 단계 1) baseline 수집 cyclictest / perf 2) 경로 분리 irq vs softirq vs kworker 3) 설정 조정 HZ/NO_HZ/affinity 4) 재검증 p95/p99 비교 판정 기준 예시 hrtimer callback 지연 p99 < 100us, softirq backlog 지속 0, watchdog 경고 0건 전력 목표 있는 경우 wakeup/s 감소 + 성능 저하 없음
# 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 backlog 형성 경로 만료 이벤트 다수 도착 hrtimer softirq 큐 적재 callback에서 긴 연산/락 경합 발생 다음 타이머 처리 지연 p99 지터 확대 실시간성 저하 완화 전략 1) callback은 짧게 유지하고 무거운 작업은 workqueue로 이관 2) CPU affinity 분리로 타이머 처리 CPU의 경쟁 완화 3) timer slack/coalescing 정책으로 불필요한 wakeup 감소
/* 안티패턴: 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 최소화, 전용 CPUp99/p999 latency, irq off time
일반 서버timer coalescing 적극 사용, NO_HZ_IDLEthroughput/wakeup/s
배치/백그라운드slack 확대, delayed_work 병합전력/열/총 처리량(Throughput)

Tick Broadcast와 Deep Idle 복귀 지연

tickless 시스템에서 일부 CPU가 deep idle(C-state 깊은 단계)로 내려가면, 로컬 timer event 대신 broadcast 장치를 통한 깨움 경로가 사용됩니다. 이 경로는 전력 효율에는 유리하지만, 특정 패턴에서 깨움 지연 편차를 키울 수 있습니다.

Tick Broadcast 경로와 지연 편차 CPU idle 진입 local tick 정지 broadcast timer 장치가 만료 관리 원격 깨움 이벤트 전달 CPU 복귀 + callback 실행 지연 편차 발생 가능 튜닝 포인트 1) 지연 민감 스레드는 깊은 idle 회피 또는 전용 CPU에 배치 2) cpuidle governor와 tick policy를 함께 조정 3) 에너지 절감 이득 대비 p99 지연 손실을 수치로 비교 4) RT 워크로드는 housekeeping/isolated CPU 설계 병행
# 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):

timer_setup() 내부 흐름 timer_setup() __init_timer() timer->function = callback timer->flags = flags | TIMER_INIT_FLAGS INIT_LIST_HEAD (&timer->entry) debug_object_init() (CONFIG_DEBUG_OBJECTS) 초기화 완료: expires 미설정, 휠 미등록 add_timer() 또는 mod_timer()로 활성화 필요
/* 기본 사용 예제 */
#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()의 내부 동작은 타이머의 현재 상태에 따라 분기합니다:

관련 변형 함수:

mod_timer() 내부 흐름 mod_timer(timer, expires) timer pending? No enqueue Yes expires 같은가? Yes no-op No detach_if_pending get_target_base() calc_wheel_index() enqueue_timer() trigger_dyntick _cpu()
/* 주기적 타이머 패턴 */
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) 모든 컨텍스트
타이머 취소 함수 결정 트리 타이머 콜백 내부? Yes del_timer() No IRQ 비활성 상태? Yes try_to_del_timer _sync() No 콜백 완료 보장 필요? Yes del_timer_sync() No del_timer()
/* 안전한 모듈 정리 패턴 */
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 핸들러에서 타이머 조작 필요 시
타이머 플래그(Timer Flags)별 동작 차이 flags = 0 (표준) BASE_STD 베이스 등록 CPU 마이그레이션 허용 softirq 컨텍스트 콜백 유휴 시 tick 발생 TIMER_DEFERRABLE BASE_DEF 베이스 등록 CPU 마이그레이션 허용 softirq 컨텍스트 콜백 유휴 시 처리 지연 (전력↓) TIMER_PINNED BASE_STD 베이스 등록 현재 CPU 고정 (마이그레이션 금지) softirq 컨텍스트 콜백 유휴 시 tick 발생 TIMER_IRQSAFE BASE_STD 베이스 등록 CPU 마이그레이션 허용 하드 IRQ 안전 (raw_spinlock) 유휴 시 tick 발생 플래그 조합 예시 TIMER_DEFERRABLE | TIMER_PINNED → 현재 CPU 고정 + 유휴 시 지연 TIMER_PINNED | TIMER_IRQSAFE → CPU 고정 + IRQ 안전 TIMER_DEFERRABLE | TIMER_IRQSAFE → 지연 + IRQ 안전 참고: TIMER_DEFERRABLE 타이머는 idle tick 해제 시 BASE_DEF를 건너뛰므로 만료가 지연될 수 있음

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):

모드(Mode) 플래그:

hrtimer_init() 내부 흐름 hrtimer_init(timer, clk, mode) cpu_base = this_cpu_ptr(&hrtimer_bases) clock_base index = clockid_map[clock_id] (SOFT 모드이면 +HRTIMER_MAX_CLOCK_BASES/2) timer->base = &cpu_base->clock_base[idx] state = INACTIVE, is_soft/is_hard 설정, debug_object_init() 초기화 완료 clock_base 인덱스 0: MONOTONIC (hard) 1: REALTIME (hard) 2: BOOTTIME (hard) 3: TAI (hard) 4: MONOTONIC (soft) 5: REALTIME (soft) 6: BOOTTIME (soft) 7: TAI (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으로 호출하는 래퍼입니다.

내부 흐름:

  1. 이미 등록되어 있으면 RB-트리에서 제거
  2. REL 모드이면 현재 시각을 더해 절대 시각으로 변환
  3. _softexpires = tim, node.expires = tim + range_ns 설정
  4. enqueue_hrtimer()로 RB-트리에 삽입
  5. 새 타이머가 가장 왼쪽(Leftmost) 노드이면 hrtimer_reprogram()tick_program_event()로 클록이벤트 재프로그래밍
hrtimer_start() 내부 흐름 hrtimer_start(timer, tim, mode) lock_hrtimer_base(timer) active? Yes remove_hrtimer(timer) _softexpires = tim (REL이면 += ktime_get()) node.expires = _softexpires + range_ns enqueue_hrtimer() (RB-tree) leftmost 변경? Yes 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() 내부 동작 hrtimer_cancel(timer) ret = hrtimer_try_to_cancel(timer) ret >= 0 ? Yes return ret No (-1) cpu_relax() 콜백 완료까지 반복
함수 차단 여부 반환값 용도
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_forward_now() 오버런(Overrun) 시나리오 시간 → T T+I T+2I T+3I T+4I T+5I 콜백 실행 (지연) hrtimer_forward_now(I) overrun = 5, 다음: T+5I 콜백 지연 — 4개 인터벌 놓침 (시스템 부하)

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 모드 플래그 조합 HRTIMER_MODE_ABS 절대 시각 기준 HRTIMER_MODE_REL 상대 시간 (→ ABS 변환) _PINNED CPU 마이그레이션 금지 _SOFT (5.4+) softirq에서 실행 _HARD PREEMPT_RT에서도 hardirq 자주 사용되는 조합 HRTIMER_MODE_REL 가장 일반적. 상대 시간 지정. HRTIMER_MODE_ABS_PINNED per-CPU 타이밍. CPU 고정 절대 시각. HRTIMER_MODE_REL_PINNED_SOFT 상대 + CPU고정 + softirq. 네트워크 타이머에 적합. HRTIMER_MODE_REL_HARD PREEMPT_RT에서 저지연 보장 필요 시. HRTIMER_MODE_REL_PINNED_HARD RT 환경 per-CPU 정밀 타이머. 최고 성능.

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);
nanosleep() → hrtimer_sleeper 흐름 nanosleep() sys_nanosleep hrtimer_nanosleep() hrtimer_init_sleeper(&sl) hrtimer_start(&sl.timer) schedule() [슬립] hrtimer 만료 → wake_up_process(task) 사용자 공간으로 복귀

schedule_timeout() 상세

schedule_timeout()은 현재 태스크를 최대 timeout 지피(Jiffies) 동안 슬립시킵니다. 호출 전에 반드시 태스크 상태를 설정해야 합니다.

signed long schedule_timeout(signed long timeout);

태스크 상태별 동작:

특수 값: schedule_timeout(MAX_SCHEDULE_TIMEOUT)은 타이머를 생성하지 않고 무기한 슬립합니다.

schedule_timeout() 내부 흐름 set_current_state() + schedule_timeout(n) MAX_SCHEDULE_TIMEOUT? Yes schedule() — 무기한 No timer_setup_on_stack(&timer) mod_timer(&timer, expire) schedule() [슬립] 타이머 or 시그널 깨움 del_timer_sync + destroy_on_stack → return 잔여 지피

편의 래퍼 함수:

/* 태스크 상태를 내부에서 설정하는 래퍼 */
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가 다릅니다.

지연 함수(Delay Function) 결정 트리 지연 필요? < 10us udelay() / ndelay() 바쁜 대기, 모든 컨텍스트 10us ~ 20ms usleep_range(min, max) hrtimer 기반, 프로세스만 > 20ms msleep(ms) timer_list 기반, 프로세스만 fsleep(usecs) — 5.8+ 자동 선택: <10us→udelay, ~20ms→usleep_range, >20ms→msleep 프로세스 컨텍스트가 아니면(IRQ/softirq) → udelay()/ndelay()만 사용 가능 모르겠으면 → fsleep() 사용 (커널이 최적 함수 자동 선택)

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 스레드가 프로세스 컨텍스트에서 핸들러를 실행합니다.

내부 동작 흐름

delayed_work 2단계 실행 흐름 Phase 1: 타이머 단계 Phase 2: 워크큐 단계 INIT_DELAYED_WORK schedule_delayed_work(delay) Timer Wheel 등록 (delay 지피 대기) delayed_work_timer_fn → __queue_work(wq, work) kworker 스레드 (프로세스 컨텍스트) handler() 실행 delay 만료 work 큐잉 핸들러 실행

함수별 상세

함수 / 매크로 설명 반환값
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_structdelayed_work 포인터 변환. 핸들러 내에서 사용. struct delayed_work *
delayed_work 취소 API 결정 트리 핸들러 완료 보장 필요? Yes cancel_delayed _work_sync() No IRQ/타이머 컨텍스트? Yes cancel_delayed _work() No flush_delayed_work() (이미 취소, 실행 완료만 대기)

clockevent 프로그래밍 API 상세

클록이벤트 장치(Clock Event Device)는 하드웨어 타이머 인터럽트를 커널 타이머 프레임워크에 연결하는 추상화 계층(Abstraction Layer)입니다. 각 CPU에 하나의 tick 장치가 할당되며, hrtimer와 타이머 휠의 만료 이벤트를 하드웨어로 프로그래밍합니다.

struct clock_event_device 상세

struct clock_event_device 주요 필드 식별 name, rating (품질 점수, 높을수록 우선) features (기능 플래그) CLOCK_EVT_FEAT_PERIODIC | ONESHOT | KTIME | HRTIMER 콜백 함수 set_next_event(delta, dev) — delta 사이클 후 발생 set_next_ktime(ktime, dev) — 절대 ktime 시점 발생 set_state_periodic / set_state_oneshot set_state_shutdown / tick_resume event_handler 인터럽트 발생 시 호출: tick_handle_periodic / hrtimer_interrupt 변환 상수 mult, shift — ns ↔ 하드웨어 사이클 변환 (ns = cycles * mult >> shift) 범위 제한 min_delta_ns / max_delta_ns — 프로그래밍 가능 최소/최대 간격 역할: 하드웨어 타이머 → 커널 타이머 프레임워크 연결 ONESHOT 모드에서 hrtimer_interrupt가 다음 만료 시각을 재프로그래밍 PERIODIC 모드에서 tick_handle_periodic이 HZ 주기로 호출

등록 흐름

/* 주요 등록 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);
clockevent 장치 등록 흐름 드라이버 probe alloc + 콜백 설정 clockevents_config_and_register() clockevents_config(freq, min, max) mult/shift 계산 clockevents_register_device() tick_check_new_device() → per-CPU tick 장치 또는 브로드캐스트 장치로 설치 per-CPU tick device 또는 broadcast device

tick_program_event() 상세

void tick_program_event(ktime_t expires, int force);

현재 CPU의 클록이벤트 장치에 다음 이벤트를 프로그래밍합니다. hrtimer_interrupt()가 만료된 타이머를 모두 처리한 후 다음 만료 시각으로 이 함수를 호출합니다.

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
팁: ONESHOT 모드는 hrtimer와 tickless(NO_HZ) 커널 동작의 전제 조건입니다. 하드웨어가 ONESHOT을 지원하지 않으면 고해상도 타이머가 활성화되지 않습니다.

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 값에 대해 동일한 래핑 안전 비교를 수행합니다.

32비트 jiffies 래핑 주의: 32비트 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 프레임워크 활성화가 이루어집니다.

부팅 시 타이머 초기화 시퀀스 start_kernel() tick_init() init_timers() hrtimers_init() timekeeping_init() time_init() [arch-specific] x86: tsc_init() hpet_time_init() setup_APIC_timer() ARM: timer_probe() arch_timer_of_register() arch_timer_common_init() RISC-V: timer_probe() riscv_timer_init_dt() late_time_init() → clocksource/clockevent 등록 완료
순서함수설명
1tick_init()tick 서브시스템 자료구조 초기화, broadcast 프레임워크 설정
2init_timers()Timer Wheel (저해상도 타이머) per-CPU 기반 초기화, TIMER_SOFTIRQ 등록
3hrtimers_init()hrtimer per-CPU clock base 초기화, softirq 핸들러 등록
4timekeeping_init()jiffies clocksource를 기본 등록, wall time 초기값 설정 (RTC에서 읽기)
5time_init()아키텍처별 타이머 하드웨어 초기화 (TSC, HPET, Generic Timer 등)
6late_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()가 핵심 전환 트리거입니다.

저해상도 → 고해상도 타이머 전환 흐름 Phase 1: 주기적 모드 periodic clockevent HZ 간격 인터럽트 jiffies 기반 타이머 Timer Wheel만 동작 해상도: 1/HZ (최대 10ms) Phase 2: 전환 감지 tick_check_new_device() one-shot capable 감지 tick_install_replacement() hrtimer_switch_to_hres() hrtimer_hres_enabled = 1 Phase 3: 고해상도 모드 one-shot clockevent tick_sched_timer (hrtimer) 나노초 해상도 hrtimer + Timer Wheel 공존 해상도: ~1ns tick_sched_timer() — 고해상도 모드의 스케줄러 틱 hrtimer 만료 tick_sched_handle() update_process_times() scheduler_tick() run_local_timers() TIMER_SOFTIRQ 발생
/* 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 기반 타이머 인터페이스를 사용합니다.

아키텍처별 타이머 하드웨어 비교 x86 / x86_64 TSC (Time Stamp Counter) rdtsc / rdtscp 명령어 Invariant TSC (최신 CPU) Rating: 300 | ~1ns 주의: 멀티소켓 동기화 HPET Memory-mapped counter 3+ comparator channels Rating: 250 | ~100ns Local APIC Timer per-CPU, one-shot/periodic TSC-deadline 모드 지원 clockevent 주력 소스 ACPI PM Timer 24/32-bit, 3.579545 MHz Rating: 200 | 안정적 PIT (i8254) 1.193182 MHz, 레거시 Rating: 110 | 대체됨 ARM / ARM64 ARM Generic Timer System Counter (전역 카운터) CNTP_TVAL / CNTV_TVAL (per-CPU) EL0/EL1/EL2 접근 레벨 CNTFRQ_EL0: 주파수 레지스터 clocksource + clockevent 통합 Rating: 400 | ~10ns Timer 레지스터 구조 CNTPCT_EL0: Physical Count CNTVCT_EL0: Virtual Count CNTVOFF_EL2: VM 오프셋 SoC 타이머 (보조) Broadcom, Samsung, Qualcomm 등 DT(Device Tree) 기반 등록 RISC-V mtime / mtimecmp mtime: 64-bit free-running counter mtimecmp: per-HART comparator MMIO 또는 CSR 접근 주파수: DT timebase-frequency Rating: 400 SBI Timer Interface sbi_set_timer(stime_value) S-mode에서 M-mode 호출 Sstc 확장: stimecmp CSR CSR 접근 csrr rd, time (= mtime 읽기) rdtime 의사 명령어 ■ x86 (다수 소스 공존) ■ ARM (통합 Generic Timer) ■ RISC-V (SBI 기반)
아키텍처ClocksourceRating해상도ClockeventOne-shot절전 영향
x86TSC300~1nsLocal APIC TimerO (TSC-deadline)C-state에서 APIC 정지 가능
x86HPET250~100nsHPET comparatorO항상 동작
x86ACPI PM Timer200~280ns--항상 동작 (I/O 느림)
ARM64arch_sys_counter400~10-20nsarch_timerOWFI/WFE와 무관하게 동작
RISC-Vriscv_clocksource400DT 의존riscv_timerO (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);
    ...
}
x86 TSC 신뢰성 주의: 오래된 CPU나 멀티소켓 시스템에서 TSC가 코어/소켓 간에 동기화되지 않을 수 있습니다. 커널은 tsc_reliable, tsc_clocksource_reliable 등의 플래그로 이를 검증하며, 불안정하면 자동으로 HPET이나 PM Timer로 폴백합니다. dmesg | grep -i tsc로 TSC 상태를 확인할 수 있습니다.

pending_map 비트맵 최적화

Timer Wheel은 5단계 × 64슬롯 = 총 320개의 슬롯을 관리합니다. 다음에 만료될 타이머를 찾기 위해 모든 슬롯을 순회하면 비효율적이므로, pending_map 비트맵을 사용하여 타이머가 등록된 슬롯만 빠르게 탐색합니다.

pending_map 비트맵 최적화 struct timer_base spinlock_t lock; unsigned long clk; unsigned long next_expiry; DECLARE_BITMAP(pending_map, WHEEL_SIZE); /* 320 bits */ struct hlist_head vectors[WHEEL_SIZE]; unsigned int cpu; pending_map[5] (320 bits = 5 × 64) LVL0: ... (64 slots, 1 = 타이머 있음) LVL1: ... (64 slots) LVL2-4: ... (192 slots) __next_timer_interrupt() 스캔 과정 1. clk 기준으로 현재 레벨(LVL0)의 시작 비트 계산 2. find_next_bit(pending_map, end, start)로 다음 1-bit 탐색 3. 해당 레벨에서 없으면 상위 레벨(LVL1→LVL2→...)로 이동 4. 찾은 비트의 만료 시각 = next_expiry에 저장 성능 비교 비트맵 없이: O(320) 최악 비트맵 사용: O(5) 최악 (레벨 수) find_next_bit: CPU 명령어 1개 (bsf/ctz) pending_map bit → vectors[] 슬롯 → hlist (timer_list 체인) bit[3] = 1 vectors[3] timer_list A timer_list B timer_list C
/* 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 시간을 줄여 시스템 응답성을 개선할 수 있습니다.

hrtimer Hard vs Soft 실행 경로 clockevent 인터럽트 발생 hrtimer_interrupt() __hrtimer_run_queues() Hard 모드 (HRTIMER_MODE_*_HARD) HRTIMER_ACTIVE_HARD 큐 확인 __run_hrtimer() — hardirq 컨텍스트 timer->function() 즉시 실행 특징: 최소 지연, irq disabled 상태 제약: sleep 불가, 짧은 콜백만 사용: sched_clock, perf_events Soft 모드 (HRTIMER_MODE_*_SOFT) HRTIMER_ACTIVE_SOFT 큐 확인 raise_softirq(HRTIMER_SOFTIRQ) hrtimer_run_softirq() — softirq 컨텍스트 timer->function() 지연 실행 특징: 약간의 지연, irq enabled 장점: 긴 콜백 가능, preemptible 사용: CFS bandwidth, nanosleep
/* 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, watchdogCFS bandwidth, nanosleep, POSIX timer
timerqueueclock_base별 별도 관리clock_base별 별도 관리
PREEMPT_RT 동작진짜 hardirq 유지ksoftirqd 스레드에서 실행
PREEMPT_RT와 Soft hrtimer: PREEMPT_RT 패치 적용 시, 일반 hrtimer(mode 미지정)는 자동으로 soft 모드로 전환됩니다. 진짜 hardirq 컨텍스트에서 실행해야 하는 타이머만 HRTIMER_MODE_HARD를 명시적으로 지정해야 합니다. 이는 RT 커널에서 인터럽트 지연 시간을 최소화하기 위한 설계입니다.

Timer와 스케줄러 상호작용

타이머 서브시스템과 프로세스 스케줄러는 긴밀하게 연결되어 있습니다. 고해상도 모드에서 tick_sched_timer() hrtimer가 스케줄러의 심장 박동 역할을 하며, CFS 스케줄러는 hrtimer를 사용하여 대역폭 제한(bandwidth throttling)을 구현합니다.

Timer와 스케줄러 상호작용 tick_sched_timer() [hrtimer 콜백] tick_sched_handle() update_process_times() account_process_tick() run_local_timers() raise(TIMER_SOFTIRQ) scheduler_tick() curr->sched_class->task_tick() task_tick_fair() [CFS] check_preempt_tick() resched_curr() → TIF_NEED_RESCHED sched_clock_tick() sched_clock() → TSC 읽기 CFS Bandwidth Throttling period_timer (hrtimer) quota 소진 → cfs_rq throttle period 만료 → quota 리필 unthrottle → 태스크 재실행 HRTIMER_MODE_ABS_SOFT 선점 메커니즘 1. 타이머 인터럽트 발생 2. scheduler_tick() 호출 3. vruntime 비교 4. TIF_NEED_RESCHED 설정 5. 인터럽트 복귀 시 스케줄
상호작용 지점타이머 측스케줄러 측역할
스케줄러 틱tick_sched_timer (hrtimer)scheduler_tick()타임슬라이스 갱신, 선점 검사
CFS bandwidthperiod_timer (hrtimer soft)sched_cfs_period_timer()CPU quota 리필 및 throttle 해제
sched_clockTSC/clocksourcesched_clock()나노초 정밀도 실행 시간 측정
선점 타이머타이머 인터럽트check_preempt_tick()현재 태스크의 최소 실행 보장 후 선점
NO_HZ_FULLtick 정지/재시작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.ctick_sched_timer()kernel/sched/fair.ccheck_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과 스케줄러 틱: NO_HZ_FULL 모드에서 CPU에 실행 가능한 태스크가 하나뿐이면 tick_sched_timer를 정지하여 불필요한 인터럽트를 제거합니다. 이때 scheduler_tick()이 호출되지 않으므로, 다른 태스크가 깨어날 때 IPI(Inter-Processor Interrupt)로 틱을 재개해야 합니다.

실전 디바이스 드라이버 타이머 패턴 총정리

디바이스 드라이버에서 타이머를 사용하는 대표적인 패턴들을 정리합니다. 각 패턴은 실제 커널 드라이버에서 자주 사용되는 검증된 방식입니다.

디바이스 드라이버 타이머 패턴 1. 지수 백오프 폴링 10ms 20ms 40ms mod_timer()로 간격 2배 증가 최대 간격 제한 (max_delay) 용도: HW 상태 폴링 DMA 완료, 링크 상태 등 성공 시 간격 리셋 2. 주기적 hrtimer 샘플링 S S S S hrtimer + HRTIMER_RESTART 정밀 주기 보장 (us 단위) 용도: ADC, 센서 읽기 hrtimer_forward_now() 사용 S = 샘플링 포인트 3. 워치독 타임아웃 timeout 만료 → 리셋 mod_timer()로 리셋 (kick) 정상 동작 시 지속 갱신 용도: HW 행 감지 콜백에서 HW 리셋 수행 timeout 미리셋 = 장애 4. delayed_work 초기화 probe()에서 지연 초기화 예약 schedule_delayed_work() 용도: 부팅 후 느린 HW 초기화 프로세스 컨텍스트 실행 5. 안전한 종료 패턴 remove()에서 모든 타이머 취소 del_timer_sync() / hrtimer_cancel() cancel_delayed_work_sync() 미취소 시 use-after-free! 6. readl_poll_timeout 레지스터 폴링 + 타임아웃 usleep_range() 기반 대기 용도: HW 레디 비트 대기 -ETIMEDOUT 반환 패턴 선택 가이드 ms 이상 주기 → timer_list | us 정밀도 → hrtimer | 프로세스 컨텍스트 필요 → delayed_work 레지스터 폴링 → readl_poll_timeout | HW 감시 → watchdog 패턴 | 적응형 → 지수 백오프

패턴 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;
}
remove()에서 타이머 미취소 시 위험: 드라이버 언로드 후에도 타이머 콜백이 실행되면 이미 해제된 메모리에 접근하여 커널 패닉이 발생합니다. 반드시 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 옵션 관계도 틱 주파수 (HZ) HZ_100 HZ_250 HZ_300 HZ_1000 서버: 100-250 데스크톱: 300-1000 Tickless 모드 HZ_PERIODIC (항상 틱) NO_HZ_IDLE (idle 시 생략) NO_HZ_FULL (단일 태스크도 생략) NO_HZ_IDLE이 대부분의 배포판 기본 고해상도 + HW HIGH_RES_TIMERS HPET_TIMER (x86) X86_TSC (x86) HIGH_RES_TIMERS=y 필수 디버깅 및 감시 DEBUG_OBJECTS_TIMERS SOFTLOCKUP_DETECTOR HARDLOCKUP_DETECTOR TIMER_STATS (4.11 제거) 실시간 (RT) PREEMPT_RT PREEMPT_DYNAMIC RT: hrtimer가 기본 soft 모드 softirq → ksoftirqd 스레드 일반적인 배포판 기본 설정 CONFIG_HZ=250 | NO_HZ_IDLE=y | HIGH_RES_TIMERS=y | SOFTLOCKUP_DETECTOR=y | PREEMPT_DYNAMIC=y
CONFIG 옵션기본값설명영향
CONFIG_HZ250틱 주파수 (초당 인터럽트 횟수)높을수록 반응성 증가, 오버헤드 증가
CONFIG_HZ_100nHZ=100, 서버 최적화10ms 틱 해상도, 최소 오버헤드
CONFIG_HZ_250yHZ=250, 범용 (많은 배포판 기본)4ms 틱 해상도
CONFIG_HZ_300nHZ=300, 멀티미디어/PAL/NTSC3.33ms, 영상 프레임률과 정수배
CONFIG_HZ_1000nHZ=1000, 저지연 데스크톱1ms 틱 해상도, 높은 오버헤드
CONFIG_NO_HZ_IDLEyidle 상태에서 틱 중단절전 효과 극대화, 대부분의 배포판 기본
CONFIG_NO_HZ_FULLn실행 태스크 1개일 때도 틱 중단HPC, 실시간 워크로드에 유용
CONFIG_HZ_PERIODICn항상 주기적 틱 발생레거시 호환, 절전 없음
CONFIG_HIGH_RES_TIMERSy나노초 해상도 hrtimer 활성화비활성화하면 모든 타이머가 HZ 해상도
CONFIG_HPET_TIMERy (x86)HPET clocksource/clockevent 지원x86 전용, TSC 폴백으로 사용
CONFIG_HPET_EMULATE_RTCy (x86)HPET으로 RTC 에뮬레이션레거시 RTC 인터럽트 호환
CONFIG_X86_TSCy (x86)TSC (Time Stamp Counter) 사용x86 필수, rdtsc 명령어 활성화
CONFIG_TIMER_STATS-커널 4.11에서 제거됨/proc/timer_stats로 타이머 추적 (보안 위험으로 제거)
CONFIG_DEBUG_OBJECTS_TIMERSn타이머 객체 라이프사이클 추적use-after-free, 이중 초기화 감지
CONFIG_SOFTLOCKUP_DETECTORysoft lockup 감지 (10+초 선점 불가)watchdog/N 커널 스레드 사용
CONFIG_HARDLOCKUP_DETECTORnhard lockup 감지 (인터럽트 비활성 고착)NMI perf 이벤트 기반, x86 전용
CONFIG_PREEMPT_RTn완전 선점형 실시간 커널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 */
CONFIG_TIMER_STATS 제거 이유: /proc/timer_stats는 타이머를 등록한 프로세스의 PID와 함수명을 노출했는데, 이는 보안상 커널 주소 공간 배치(KASLR) 무력화에 악용될 수 있었습니다. 커널 4.11(2017)에서 제거되었으며, 동일 기능은 perftrace-cmdtimer: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 — 최소 전력 소비

참고 자료

타이머와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.