타이머 (Timers)

jiffies, hrtimer, clocksource, clockevent, timer wheel, tickless 커널 등 Linux 커널의 시간 관리 체계를 설명합니다.

이 문서는 시간 측정(clocksource)과 이벤트 발생(clockevent), 그리고 커널 타이머 API(timer wheel, hrtimer)가 어떻게 결합되어 스케줄러·네트워크·스토리지 경로의 지연 특성을 결정하는지까지 다룹니다. 또한 NO_HZ 계열 설정, vDSO 기반 시간 읽기, delayed_work 선택 기준, watchdog과 lockup 탐지 로그 해석을 포함해 "정확도·전력·오버헤드" 사이의 균형을 시스템 목적에 맞게 조정하는 실무 관점을 제공합니다.

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

핵심 요약

  • jiffies — 부팅 이후 경과한 타이머 틱 수. HZ(보통 250)가 초당 틱 수를 결정합니다.
  • timer_list — jiffies 기반 저해상도 타이머. timer wheel 알고리즘으로 O(1) 삽입/삭제합니다.
  • hrtimer — 나노초 해상도의 고해상도 타이머. Red-Black 트리로 관리됩니다.
  • clocksource — 시간 측정 하드웨어(TSC, HPET 등)를 추상화하는 프레임워크입니다.
  • clockevent — 미래 시점에 인터럽트를 발생시키는 하드웨어 타이머 추상화입니다.
  • NO_HZ(tickless) — idle CPU에서 불필요한 타이머 인터럽트를 생략하여 전력을 절약합니다.
  • delayed_work — workqueue에 지연 실행 작업을 예약하는 API. 프로세스 컨텍스트에서 실행됩니다.
  • watchdog — softlockup(20초)/hardlockup(10초) 감지기. 커널 행 상태를 자동 탐지합니다.
  • vDSO — clock_gettime()을 syscall 없이 실행하는 가상 DSO. 수 나노초의 오버헤드로 시간을 읽습니다.

단계별 이해

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

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

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

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

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

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

  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 스레드)에서 실행되므로 슬립/잠금 사용이 가능합니다.

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

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

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

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

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

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

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

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

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

타이머 서브시스템 계층 구조 하드웨어 계층 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은 계층적 해시 테이블의 원리로 동작합니다. 핵심 아이디어는 "가까운 미래의 타이머는 정밀하게, 먼 미래의 타이머는 대략적으로 관리"하는 것입니다:

ℹ️

설계 배경: 초기 커널(~2.6.16)은 단일 수준의 정렬된 리스트를 사용했으나, 타이머 수가 증가하면서 O(n) 삽입이 병목이 되었습니다. 계층적 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);

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 */
ℹ️

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

hrtimer 레드블랙 트리와 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;              /* 클럭 오프셋 */
};

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가 담당합니다.

clocksource/clockevent 프레임워크 상세 — 등록 API, rating 시스템, watchdog, fallback 전략, 하드웨어 클럭(TSC/HPET/ACPI PM)별 비교는 ktime / Clock 심화 — Clocksource 프레임워크에서 자세히 다룹니다.

지연 함수 (Delay Functions)

커널은 컨텍스트에 따라 다양한 지연 함수를 제공합니다. 인터럽트 컨텍스트에서는 busy-wait(udelay(), ndelay())만 사용 가능하고, 프로세스 컨텍스트에서는 슬립 기반(usleep_range(), msleep())을 권장합니다.

각 지연 함수의 내부 구현, 컨텍스트별 선택 가이드, 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 단일 태스크 실행 시 매우 높음 매우 낮음 (지터 최소) HPC, 실시간, 저지연 워크로드
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 포함) 등 다양한 시간 읽기 함수와 산술 연산 매크로를 제공합니다.

ktime_t 전체 함수 레퍼런스, 변환 매크로, ns/us/ms 변환 헬퍼, 경과 시간 측정 패턴은 ktime / Clock 심화 — ktime_t 함수 레퍼런스에서 상세히 다룹니다.

POSIX 타이머 (유저스페이스)

커널은 유저스페이스 POSIX 타이머를 hrtimer 기반으로 구현합니다:

/* 유저스페이스: timer_create, timer_settime */
/* 커널 내부: posix-timers.c → hrtimer */

/* 주요 클럭 ID */
CLOCK_REALTIME       /* 벽시계 (NTP 조정 가능) */
CLOCK_MONOTONIC      /* 단조 증가 (NTP 조정 없음) */
CLOCK_BOOTTIME       /* MONOTONIC + suspend 시간 */
CLOCK_PROCESS_CPUTIME_ID  /* 프로세스 CPU 시간 */
CLOCK_THREAD_CPUTIME_ID   /* 스레드 CPU 시간 */

타이머 마이그레이션과 그룹핑

전력 효율을 위해 커널은 여러 타이머를 같은 시점에 만료되도록 그룹핑합니다:

/* 타이머를 "느슨하게" 설정하면 커널이 그룹핑 가능 */
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를 오래 점유하므로 가급적 사용하지 마세요.

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);
    }
}

hrtimer_interrupt() 흐름 (고해상도)

hrtimer_interrupt()는 clockevent 인터럽트 핸들러에서 직접 호출됩니다. 하드 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;
}

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);
}
💡

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 레지스터) 서브초 가능 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에 오프셋 정의)

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()로 변환. 잘못된 변환은 날짜 오류
  • 레지스터 읽기 경합 — 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 — 실시간 커널에서는 스핀락도 슬립 가능 뮤텍스로 교체되어 softlockup 위험이 감소합니다.
  • 가상 머신 — VM 환경에서는 하이퍼바이저가 vCPU를 선점할 때 softlockup 오탐이 발생할 수 있습니다. watchdog_thresh를 높이거나 비활성화하기도 합니다.

타이머 디버깅 및 분석

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

/proc/timer_list

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

# 전체 타이머 목록 보기
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를 타이머/인터럽트에서 격리
irqbalance 비활성화 후 수동으로 IRQ 친화성 설정
③ PREEMPT_RT 패치 커널 사용으로 인터럽트 핸들러를 스레드화
④ BIOS에서 C-state 제한 또는 비활성화 (intel_idle.max_cstate=1)
⑤ CPU 주파수 스케일링 비활성화 (cpupower frequency-set -g performance)

vDSO — 빠른 시간 읽기

vDSO(Virtual Dynamic Shared Object)는 clock_gettime(), gettimeofday() 등의 시간 관련 시스템 콜을 커널 진입 없이 사용자 공간에서 직접 실행할 수 있게 하는 메커니즘입니다. 커널이 관리하는 vvar 페이지를 읽어 수 나노초 수준의 오버헤드로 시간을 읽습니다.

vDSO의 내부 동작 원리(vvar 페이지, seqcount, TSC 변환), 성능 벤치마크, 비활성화 시나리오, 아키텍처별 구현 차이는 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) 높은 우선순위 인터럽트의 선점, (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 진입을 방해

일반적인 타이머 실수와 주의사항

타이머 서브시스템은 동시성, 컨텍스트 제약, 메모리 관리와 밀접하게 관련되어 있어 다양한 실수가 발생합니다. 아래는 실무에서 자주 발생하는 버그 패턴과 올바른 사용법입니다.

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()로 취소한 후에 구조체를 해제해야 합니다.
  • 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 제어와 우선순위 역전

지연 문제에서 자주 놓치는 부분은 "타이머 만료 시각"보다 "콜백이 실제 실행되는 순서"입니다. 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 병합전력/열/총 처리량

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

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