타이머 (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)의 원리로 동작합니다. 핵심 아이디어는 "가까운 미래의 타이머는 정밀하게, 먼 미래의 타이머는 대략적으로 관리"하는 것입니다:

ℹ️

설계 배경: 초기 커널(~2.6.16)은 단일 수준의 정렬된 리스트를 사용했으나, 타이머 수가 증가하면서 O(n) 삽입이 병목(Bottleneck)이 되었습니다. 계층적 Timer Wheel은 O(1) 삽입/삭제를 달성하며, 현재 커널(4.8+)은 4레벨 구조로 최대 약 12일(HZ=250 기준)까지 커버합니다.

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 각 레벨 세부 사양 (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출력 파라미터로, 슬롯의 양자화된 만료 시각을 반환합니다. 상위 레벨일수록 양자화 오차가 커지므로, 먼 미래의 타이머는 정밀도가 떨어집니다.
ℹ️

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

고해상도 타이머 (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 레드블랙 트리(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 제약이 일부 완화됩니다.

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 중단이 가능합니다.

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_create() → anon_inode_getfd() + hrtimer_init()
 * timerfd_settime() → hrtimer_start()
 * 만료 시 → timerfd_tmrproc() → wake_up_locked_poll()
 *   → timerfd의 wait queue에서 대기 중인 epoll/read를 깨움
 * read() → timerfd_read() → hrtimer_forward() → overrun 카운트 반환 */
💡

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()가 이 필드를 확인하여 콜백 완료 대기 여부를 결정합니다.

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를 높이거나 비활성화하기도 합니다.

타이머 디버깅 및 분석

타이머 서브시스템 문제(지터, 지연, 불필요한 wake-up 등)를 진단하는 도구와 기법을 설명합니다.

/proc/timer_list

/proc/timer_list는 모든 CPU의 활성 hrtimer와 timer_list를 덤프(Dump)합니다.

# 전체 타이머 목록 보기
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

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 시스템에서 타이머 마이그레이션의 확장성을 크게 개선합니다.

타이머 디버깅

/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에서만 실행됩니다.

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 접미사가 없는 버전은 콜백 완료를 보장하지 않습니다.

타이머 관련 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 — 최소 전력 소비

참고 자료

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