타이머 (Timers)
jiffies, hrtimer, clocksource, clockevent, timer wheel, tickless 커널 등 Linux 커널의 시간 관리 체계를 설명합니다.
이 문서는 시간 측정(clocksource)과 이벤트 발생(clockevent), 그리고 커널 타이머 API(timer wheel, hrtimer)가 어떻게 결합되어 스케줄러·네트워크·스토리지 경로의 지연 특성을 결정하는지까지 다룹니다. 또한 NO_HZ 계열 설정, vDSO 기반 시간 읽기, delayed_work 선택 기준, watchdog과 lockup 탐지 로그 해석을 포함해 "정확도·전력·오버헤드" 사이의 균형을 시스템 목적에 맞게 조정하는 실무 관점을 제공합니다.
핵심 요약
- 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. 수 나노초의 오버헤드로 시간을 읽습니다.
단계별 이해
- 타이머 틱 — 타이머 하드웨어가 HZ 주기로 인터럽트를 발생시키고, 커널이 jiffies를 증가시킵니다.
cat /proc/timer_list로 현재 활성 타이머를 확인할 수 있습니다. - 저해상도 타이머 —
mod_timer()로 jiffies 기반 타이머를 설정합니다. 밀리초 단위 정확도.네트워크 타임아웃, 디바이스 폴링 등에 사용됩니다.
- 고해상도 타이머 —
hrtimer_start()로 나노초 정밀 타이머를 설정합니다.POSIX 타이머,
nanosleep(), 스케줄러 타임슬라이스에 사용됩니다. - tickless 확인 —
grep CONFIG_NO_HZ /boot/config-$(uname -r)로 tickless 설정을 확인합니다.idle 상태에서 타이머 인터럽트를 생략하여 C-state 깊은 절전에 진입합니다.
- delayed_work 사용 —
schedule_delayed_work(&dwork, msecs_to_jiffies(100))로 100ms 후 실행을 예약합니다.timer_list와 달리 프로세스 컨텍스트(kworker 스레드)에서 실행되므로 슬립/잠금 사용이 가능합니다.
- watchdog 설정 확인 —
cat /proc/sys/kernel/watchdog_thresh로 임계값(기본 10초)을 확인합니다.softlockup은 스케줄러 양보 없음, hardlockup은 NMI로 인터럽트 없음을 감지합니다.
- 타이머 디버깅 —
cat /proc/timer_list | head -60으로 만료 시각과 콜백 함수를 확인합니다.cyclictest -m -n -p99 -l 10000으로 타이머 지터를 마이크로초 단위로 측정합니다. - vDSO 타이밍 —
clock_gettime(CLOCK_MONOTONIC, &ts)호출은 syscall 없이 vDSO로 처리됩니다.strace ./my_prog를 실행하면 clock_gettime이 시스템 콜 목록에 나타나지 않는 것을 확인할 수 있습니다.
타이머 서브시스템의 정의와 역할
타이머 서브시스템은 커널이 시간의 흐름을 인식하고 미래 시점에 작업을 예약할 수 있게 하는 핵심 인프라입니다. 이 서브시스템이 없으면 프로세스 스케줄링, 네트워크 타임아웃, 디바이스 폴링, 슬립 등 시간 기반 동작이 모두 불가능합니다.
타이머 서브시스템은 크게 세 가지 계층으로 구성됩니다:
- 하드웨어 계층: TSC(Time Stamp Counter), HPET, Local APIC Timer 등 물리적 클럭 소스가 시간 측정과 인터럽트 생성을 담당합니다.
- 프레임워크 계층:
clocksource(시간 읽기)와clockevent(미래 인터럽트 예약)가 하드웨어를 추상화하여 플랫폼 독립적 인터페이스를 제공합니다. - 타이머 API 계층: 커널 코드가 실제로 사용하는
timer_list(저해상도)와hrtimer(고해상도) API입니다.
핵심 설계 원리: 커널 타이머는 "정확한 시점에 실행"이 아니라 "지정된 시점 이후 가능한 빨리 실행"을 보장합니다. 하드웨어 인터럽트 지연, softirq 스케줄링 등으로 수 마이크로초~밀리초의 지터(jitter)가 발생할 수 있습니다.
jiffies와 HZ
jiffies는 시스템 부팅 이후 발생한 타이머 틱(tick) 횟수를 저장하는 전역 변수입니다. HZ는 초당 틱 수를 나타내며, 일반적으로 x86에서는 250 (CONFIG_HZ_250)으로 설정됩니다. HZ 값의 선택은 타이머 해상도와 시스템 오버헤드 사이의 트레이드오프입니다 — HZ가 높을수록 타이머 정밀도가 향상되지만, 매 틱마다 인터럽트를 처리하므로 CPU 오버헤드가 증가합니다.
| HZ 값 | CONFIG 옵션 | 타이머 해상도 | 인터럽트 빈도 | CPU 오버헤드 | 주요 사용 사례 |
|---|---|---|---|---|---|
| 100 | CONFIG_HZ_100 |
10ms | 초당 100회 | 낮음 | 서버 워크로드, 배치 처리 |
| 250 | CONFIG_HZ_250 |
4ms | 초당 250회 | 중간 | 일반 데스크톱, 균형잡힌 설정 (기본) |
| 300 | CONFIG_HZ_300 |
3.33ms | 초당 300회 | 중간 | 멀티미디어 워크로드 (60Hz 모니터 호환) |
| 1000 | CONFIG_HZ_1000 |
1ms | 초당 1000회 | 높음 | 저지연 데스크톱, 게이밍, 오디오 처리 |
#include <linux/jiffies.h>
/* 현재 jiffies 값 */
unsigned long j = jiffies;
/* 시간 비교 (wraparound 안전) */
if (time_after(jiffies, timeout))
pr_info("timeout expired\\n");
/* jiffies ↔ 시간 변환 */
unsigned long ms = jiffies_to_msecs(j);
unsigned long j2 = msecs_to_jiffies(500); /* 500ms */
/* 64-bit jiffies (overflow 방지) */
u64 j64 = get_jiffies_64();
Timer Wheel (저해상도 타이머)
전통적인 커널 타이머는 struct timer_list를 사용하며, Timer Wheel 자료구조로 관리됩니다. 만료 시 softirq 컨텍스트(TIMER_SOFTIRQ)에서 콜백이 실행됩니다.
Timer Wheel 동작 원리
Timer Wheel은 계층적 해시 테이블의 원리로 동작합니다. 핵심 아이디어는 "가까운 미래의 타이머는 정밀하게, 먼 미래의 타이머는 대략적으로 관리"하는 것입니다:
- 삽입(O(1)): 만료 시간의 비트 패턴에 따라 적절한 레벨과 슬롯을 즉시 결정합니다. 만료까지 남은 틱 수의 상위 비트가 레벨을, 하위 비트가 슬롯 인덱스를 결정합니다.
- 만료 처리: 매 틱마다 Level 0의 현재 슬롯만 확인합니다. Level 0이 한 바퀴 돌면(64 틱) Level 1의 한 슬롯에 있는 타이머들을 Level 0으로 재배치(cascade)합니다.
- 효율성: 대부분의 타이머가 Level 0에서 만료되므로, 상위 레벨의 cascade는 드물게 발생합니다. 이는 네트워크 타임아웃처럼 자주 설정/취소되는 타이머에 최적입니다.
설계 배경: 초기 커널(~2.6.16)은 단일 수준의 정렬된 리스트를 사용했으나, 타이머 수가 증가하면서 O(n) 삽입이 병목이 되었습니다. 계층적 Timer Wheel은 O(1) 삽입/삭제를 달성하며, 현재 커널(4.8+)은 4레벨 구조로 최대 약 12일(HZ=250 기준)까지 커버합니다.
#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일)까지 커버합니다.
| 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로 관리되며, 하드웨어 클럭 이벤트에 직접 프로그래밍합니다.
hrtimer 동작 원리
hrtimer가 Timer Wheel과 근본적으로 다른 점은 틱에 의존하지 않는다는 것입니다:
- 자료구조: 모든 활성 hrtimer를 만료 시간 순으로 red-black tree에 정렬합니다. 가장 빨리 만료되는 타이머가 항상 leftmost 노드에 위치합니다.
- 하드웨어 프로그래밍: leftmost 타이머의 만료 시간을
clockevent디바이스에 직접 설정합니다. 해당 시점에 하드웨어 인터럽트가 발생하여 콜백을 실행합니다. - 재프로그래밍: 새 hrtimer가 삽입되어 leftmost가 바뀌면, clockevent를 즉시 재프로그래밍합니다.
이 방식은 Timer Wheel의 틱 해상도(1/HZ초 = 4ms@250Hz) 제약을 극복하여, 하드웨어가 지원하는 한 나노초 수준의 정밀도를 달성합니다. POSIX 타이머, nanosleep(), 스케줄러의 bandwidth throttling 등이 hrtimer를 기반으로 합니다.
#include <linux/hrtimer.h>
#include <linux/ktime.h>
static struct hrtimer my_hrtimer;
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
pr_info("hrtimer fired!\\n");
/* Periodic: restart after 10ms */
hrtimer_forward_now(timer, ms_to_ktime(10));
return HRTIMER_RESTART;
/* One-shot: don't restart */
/* return HRTIMER_NORESTART; */
}
/* 초기화 및 시작 */
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = my_hrtimer_callback;
hrtimer_start(&my_hrtimer, ms_to_ktime(10), HRTIMER_MODE_REL);
/* 취소 */
hrtimer_cancel(&my_hrtimer);
hrtimer 레드블랙 트리와 timerqueue 구조
hrtimer는 내부적으로 struct timerqueue_head를 사용하여 Red-Black 트리에 타이머를 정렬합니다. timerqueue는 일반 rbtree에 leftmost 캐싱을 추가한 특수 자료구조로, 다음 만료 타이머를 O(1)에 접근할 수 있습니다.
/* include/linux/timerqueue.h - timerqueue 핵심 구조체 */
struct timerqueue_node {
struct rb_node node; /* RB-tree 노드 */
ktime_t expires; /* 만료 시각 (나노초) */
};
struct timerqueue_head {
struct rb_root_cached rb_root; /* leftmost 캐싱된 RB-root */
};
/* include/linux/hrtimer.h - hrtimer 핵심 구조체 */
struct hrtimer {
struct timerqueue_node node; /* timerqueue 노드 내장 */
ktime_t _softexpires; /* 소프트 만료 (range 하한) */
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
u8 state; /* HRTIMER_STATE_INACTIVE/ENQUEUED/CALLBACK */
u8 is_rel; /* 상대 시간 여부 */
u8 is_soft; /* softirq 모드 여부 */
u8 is_hard; /* hardirq 모드 여부 */
};
/* hrtimer_clock_base: 클럭 베이스별 타이머 트리 */
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base;
unsigned int index; /* HRTIMER_BASE_MONOTONIC 등 */
clockid_t clockid; /* CLOCK_MONOTONIC 등 */
seqcount_raw_spinlock_t seq;
struct hrtimer *running; /* 현재 실행 중인 타이머 */
struct timerqueue_head active; /* 활성 타이머 RB-tree */
ktime_t (*get_time)(void); /* 시간 읽기 함수 */
ktime_t offset; /* 클럭 오프셋 */
};
hrtimer 클럭 베이스 4종
hrtimer는 4종의 클럭 베이스를 지원합니다. 각 클럭 베이스는 독립적인 timerqueue(RB-tree)를 유지하며, 사용 목적에 따라 적절한 클럭을 선택해야 합니다.
| 클럭 베이스 | clockid | NTP 조정 | suspend 포함 | 윤초 | get_time() | 주요 사용 사례 |
|---|---|---|---|---|---|---|
| MONOTONIC | CLOCK_MONOTONIC |
없음 (주파수만) | 미포함 | 해당없음 | ktime_get() |
스케줄러, nanosleep, 경과 시간 측정 |
| REALTIME | CLOCK_REALTIME |
있음 (시간 점프) | 포함 | 영향 받음 | ktime_get_real() |
POSIX timer_create, 파일 타임스탬프 |
| BOOTTIME | CLOCK_BOOTTIME |
없음 | 포함 | 해당없음 | ktime_get_boottime() |
Android 알람, 세션 타임아웃 |
| TAI | CLOCK_TAI |
있음 | 포함 | 없음 (원자시) | ktime_get_clocktai() |
PTP (IEEE 1588), 금융 타임스탬프 |
- CLOCK_REALTIME 타이머의 시간 점프 위험 — NTP가 시계를 앞으로 또는 뒤로 조정하면 REALTIME 기반 타이머가 즉시 만료되거나 지연될 수 있습니다. 경과 시간 측정에는 MONOTONIC을 사용하세요.
- suspend 포함 여부 — MONOTONIC은 suspend 동안 멈추지만 BOOTTIME은 계속 흐릅니다. suspend를 넘겨야 하는 타이머(예: 알람)에는 BOOTTIME을 사용하세요.
- hard vs soft 모드 — 커널 5.4+에서 hrtimer는
HRTIMER_MODE_SOFT를 지원합니다. soft hrtimer는 softirq에서 실행되어 IRQ context 제약이 일부 완화됩니다.
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을 제거하여 전력 소모를 줄입니다.
Tickless 동작 원리
Tickless의 핵심 원리는 "다음에 해야 할 일이 없으면 깨우지 않는다"입니다:
- CPU가 idle에 진입하기 전, 다음으로 만료될 타이머(Timer Wheel + hrtimer)의 시간을 확인합니다.
- 그 시간까지 주기적 틱 인터럽트를 중단하고, 대신 하나의 oneshot clockevent만 해당 시점에 프로그래밍합니다.
- CPU는 깊은 C-state(저전력 상태)에 진입하여 전력을 절약합니다.
- 타이머 만료 시점에 clockevent 인터럽트가 CPU를 깨우고, 밀린 jiffies를 한꺼번에 보정합니다.
| 모드 | CONFIG 옵션 | 틱 중단 조건 | 전력 절감 | 지연 시간 | 주요 사용 사례 |
|---|---|---|---|---|---|
| NO_HZ_OFF | CONFIG_HZ_PERIODIC |
틱 항상 활성 | 없음 | 예측 가능 | 레거시 시스템, 디버깅 |
| NO_HZ_IDLE | CONFIG_NO_HZ_IDLE |
CPU idle 시 | 중간~높음 | 낮음 | 일반 서버/데스크톱 (기본 설정) |
| NO_HZ_FULL | CONFIG_NO_HZ_FULL |
단일 태스크 실행 시 | 매우 높음 | 매우 낮음 (지터 최소) | HPC, 실시간, 저지연 워크로드 |
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() 흐름 (저해상도)
/* 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 드라이버 구현
#include <linux/rtc.h>
/* RTC 오퍼레이션 구조체 */
static const struct rtc_class_ops my_rtc_ops = {
.read_time = my_read_time, /* 현재 시각 읽기 */
.set_time = my_set_time, /* 시각 설정 */
.read_alarm = my_read_alarm, /* 알람 읽기 */
.set_alarm = my_set_alarm, /* 알람 설정 */
.alarm_irq_enable = my_alarm_irq_enable,
};
/* 시간 읽기 콜백 */
static int my_read_time(struct device *dev, struct rtc_time *tm)
{
/* H/W 레지스터에서 BCD 또는 바이너리로 읽기 */
tm->tm_sec = readl(base + RTC_SEC);
tm->tm_min = readl(base + RTC_MIN);
tm->tm_hour = readl(base + RTC_HOUR);
tm->tm_mday = readl(base + RTC_DAY);
tm->tm_mon = readl(base + RTC_MON) - 1; /* 0-based */
tm->tm_year = readl(base + RTC_YEAR) - 1900;
return 0;
}
/* RTC 디바이스 등록 */
struct rtc_device *rtc = devm_rtc_device_register(&pdev->dev,
"my-rtc", &my_rtc_ops, THIS_MODULE);
/* 또는 현대적 API (5.x+) */
rtc = devm_rtc_allocate_device(&pdev->dev);
rtc->ops = &my_rtc_ops;
rtc->range_min = RTC_TIMESTAMP_BEGIN_2000;
rtc->range_max = RTC_TIMESTAMP_END_2099;
devm_rtc_register_device(rtc);
유저 공간 RTC 관리
# RTC 시간 읽기
hwclock --show # /dev/rtc0에서 읽기
cat /sys/class/rtc/rtc0/time # sysfs에서 읽기
cat /proc/driver/rtc # CMOS RTC 상세 정보 (x86)
# RTC에 시스템 시간 쓰기
hwclock --systohc # 시스템 시간 → RTC
hwclock --hctosys # RTC → 시스템 시간 (부팅 시)
# UTC vs Local Time (주의!)
hwclock --systohc --utc # RTC를 UTC로 설정 (Linux 권장)
hwclock --systohc --localtime # 로컬 시간 (Windows 듀얼부팅 시)
timedatectl set-local-rtc 0 # systemd에서 UTC 모드 설정
# RTC 알람 설정 (시스템 웨이크업)
echo +60 > /sys/class/rtc/rtc0/wakealarm # 60초 후 깨우기
echo 0 > /sys/class/rtc/rtc0/wakealarm # 알람 해제
rtcwake -m mem -s 300 # suspend 후 300초 뒤 깨우기
CMOS RTC 레지스터 상세
CMOS RTC 전체 레지스터 맵
| 인덱스 | 이름 | R/W | 설명 |
|---|---|---|---|
| 0x00 | Seconds | R/W | 현재 초 (0-59, BCD 또는 바이너리) |
| 0x01 | Seconds Alarm | R/W | 알람 초 (0xC0~0xFF = don't care) |
| 0x02 | Minutes | R/W | 현재 분 (0-59) |
| 0x03 | Minutes Alarm | R/W | 알람 분 |
| 0x04 | Hours | R/W | 현재 시 (12H: 1-12+PM bit, 24H: 0-23) |
| 0x05 | Hours Alarm | R/W | 알람 시 |
| 0x06 | Day of Week | R/W | 요일 (1=일요일, 7=토요일) |
| 0x07 | Day of Month | R/W | 일 (1-31) |
| 0x08 | Month | R/W | 월 (1-12) |
| 0x09 | Year | R/W | 연도 하위 2자리 (00-99) |
| 0x0A | Status Register A | R/W | UIP, divider, rate select |
| 0x0B | Status Register B | R/W | SET, PIE, AIE, UIE, SQWE, DM, 24/12 |
| 0x0C | Status Register C | R/O | IRQ 플래그 (읽으면 클리어됨) |
| 0x0D | Status Register D | R/O | VRT (Valid RAM and Time) |
| 0x32 | Century | R/W | 세기 (19/20, ACPI FADT에 오프셋 정의) |
Status Register A 비트 필드 (0x0A)
| 비트 | 필드 | 설명 |
|---|---|---|
| [7] | UIP (Update In Progress) | 1=RTC가 시간 레지스터 업데이트 중 (~244μs). 이 동안 시간 레지스터를 읽으면 안 됨 |
| [6:4] | DV (Divider) | 오실레이터 분주비. 010=32.768kHz(기본). 11x=리셋, 110=분주기 리셋 |
| [3:0] | RS (Rate Select) | Periodic Interrupt 주파수. 0000=없음, 0011=8192Hz, 0110=1024Hz(기본), 1111=2Hz |
Status Register B 비트 필드 (0x0B)
| 비트 | 필드 | R/W | 설명 |
|---|---|---|---|
| [7] | SET | R/W | 1=시간 업데이트 억제 (시간 설정 시 사용). 0=정상 카운트 |
| [6] | PIE (Periodic Interrupt Enable) | R/W | 1=RS 주파수로 IRQ8 발생 |
| [5] | AIE (Alarm Interrupt Enable) | R/W | 1=알람 시각 도달 시 IRQ8 발생 |
| [4] | UIE (Update-ended Interrupt Enable) | R/W | 1=매초 업데이트 완료 시 IRQ8 발생 |
| [3] | SQWE (Square Wave Enable) | R/W | 1=SQW 출력 핀에 구형파 생성 (레거시) |
| [2] | DM (Data Mode) | R/W | 0=BCD, 1=Binary. 시간 레지스터의 데이터 형식 |
| [1] | 24/12 | R/W | 0=12시간 모드, 1=24시간 모드 |
| [0] | DSE (Daylight Saving Enable) | R/W | 1=DST 자동 전환 (실제 사용 안 함) |
CMOS RTC 인터럽트 종류
/* drivers/rtc/rtc-cmos.c — CMOS RTC 인터럽트 핸들러 */
/*
* CMOS RTC는 IRQ 8을 통해 3가지 인터럽트를 생성합니다:
* 1. Periodic Interrupt (PIE): RS 비트로 설정된 주파수 (2-8192 Hz)
* 2. Alarm Interrupt (AIE): 설정된 알람 시각 도달
* 3. Update-ended Interrupt (UIE): 매초 시간 업데이트 완료
*
* Status Register C를 읽어 어떤 인터럽트인지 확인합니다.
* (읽으면 플래그가 자동 클리어됨)
*/
static irqreturn_t cmos_interrupt(int irq, void *p)
{
struct cmos_rtc *cmos = p;
u8 irqstat;
spin_lock(&rtc_lock);
/* Status C 읽기: IRQ 원인 확인 + 플래그 클리어 */
irqstat = CMOS_READ(RTC_INTR_FLAGS); /* 0x0C */
irqstat &= (CMOS_READ(RTC_CONTROL) /* 0x0B */
& (RTC_PIE|RTC_AIE|RTC_UIE));
spin_unlock(&rtc_lock);
if (irqstat) {
/* rtc-core에 이벤트 보고 */
rtc_update_irq(cmos->rtc, 1, irqstat);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
/* RTC 시간 읽기 — UIP 비트 확인 필수 */
static int cmos_read_time(struct device *dev, struct rtc_time *t)
{
unsigned char ctrl;
spin_lock_irq(&rtc_lock);
/* UIP=1이면 업데이트 중 — 최대 244μs 대기 */
while (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
cpu_relax();
t->tm_sec = CMOS_READ(RTC_SECONDS); /* 0x00 */
t->tm_min = CMOS_READ(RTC_MINUTES); /* 0x02 */
t->tm_hour = CMOS_READ(RTC_HOURS); /* 0x04 */
t->tm_mday = CMOS_READ(RTC_DAY_OF_MONTH); /* 0x07 */
t->tm_mon = CMOS_READ(RTC_MONTH); /* 0x08 */
t->tm_year = CMOS_READ(RTC_YEAR); /* 0x09 */
ctrl = CMOS_READ(RTC_CONTROL); /* 0x0B */
spin_unlock_irq(&rtc_lock);
/* BCD → 바이너리 변환 (DM 비트 확인) */
if (!(ctrl & RTC_DM_BINARY)) {
t->tm_sec = bcd2bin(t->tm_sec);
t->tm_min = bcd2bin(t->tm_min);
t->tm_hour = bcd2bin(t->tm_hour);
t->tm_mday = bcd2bin(t->tm_mday);
t->tm_mon = bcd2bin(t->tm_mon);
t->tm_year = bcd2bin(t->tm_year);
}
t->tm_mon--; /* 커널: 0-based month */
t->tm_year += (t->tm_year < 70) ? 100 : 0; /* 2000+ 보정 */
return 0;
}
NTP ↔ RTC 동기화
/* kernel/time/ntp.c — NTP → RTC 주기적 동기화 */
/*
* CONFIG_RTC_SYSTOHC 설정 시 커널이 11분마다
* 시스템 시간을 RTC에 기록하여 동기화합니다.
*
* 이 메커니즘은 NTP로 보정된 정확한 시스템 시간을
* RTC에 반영하여, 다음 부팅 시 시간 오차를 최소화합니다.
*/
static void sync_hw_clock(struct work_struct *work)
{
/* RTC 동기화 조건:
* 1. NTP가 시간을 동기화한 상태 (STA_UNSYNC 해제)
* 2. 잔여 보정량이 0.5초 이내
* 3. 초 값이 정확한 시점 (0.5초 경계) */
struct timespec64 now;
ktime_get_real_ts64(&now);
/* 0.5초 경계에서 RTC에 쓰기 (정수 초 정확도 보장) */
if (now.tv_nsec >= (NSEC_PER_SEC >> 1))
now.tv_sec++;
struct rtc_time tm;
rtc_time64_to_tm(now.tv_sec, &tm);
rtc_set_time(rtc, &tm);
}
/* 11분 주기 타이머 — sync_cmos_clock() */
/* schedule_delayed_work(&sync_work, 660 * HZ); */
RTC sysfs 인터페이스
# /sys/class/rtc/rtc0/ 전체 파일 목록
$ ls /sys/class/rtc/rtc0/
date # 현재 날짜 (YYYY-MM-DD)
hctosys # 부팅 시 이 RTC에서 시간을 읽었는지 (1/0)
max_user_freq # 유저 공간 최대 periodic 주파수 (기본: 64)
name # RTC 이름 (예: "rtc_cmos")
offset # 보정 오프셋 (ppb 단위)
since_epoch # Unix epoch 이후 초
time # 현재 시각 (HH:MM:SS)
wakealarm # 웨이크업 알람 (epoch 또는 +N초)
# /proc/driver/rtc 출력 해석 (x86 CMOS RTC 전용)
$ cat /proc/driver/rtc
rtc_time : 14:30:25 # 현재 RTC 시간
rtc_date : 2026-02-26 # 현재 RTC 날짜
rtc_epoch : 1900 # epoch 기준 연도
alarm : 00:00:00 # 알람 시각
alarm_IRQ : no # 알람 IRQ 활성 여부
alrm_date : 2026-02-26 # 알람 날짜
update_IRQ : no # 매초 업데이트 IRQ
periodic_IRQ : no # periodic IRQ
periodic_freq : 1024 # periodic 주파수 (Hz)
batt_status : okay # 배터리 상태 (VRT 비트)
24hr : yes # 24시간 모드
BCD : yes # BCD 모드
RTC 주의사항
- Y2038 문제 — 32비트 time_t를 사용하는 구형 RTC는 2038년에 오버플로. 커널은
rtc_time64_to_tm()으로 64비트 전환 완료. 드라이버에서range_min/range_max명시 필요 - BCD vs 바이너리 — 일부 RTC는 BCD 인코딩.
bcd2bin()/bin2bcd()로 변환. 잘못된 변환은 날짜 오류 - 레지스터 읽기 경합 — RTC 레지스터 읽기 중 초가 변경되면 불일치 데이터 반환. Update-In-Progress(UIP) 비트 확인 또는 두 번 읽어서 비교
- 배터리 고갈 — CMOS 배터리(CR2032) 소진 시 시간 초기화. 부팅 시 NTP 동기화로 보상하지만, NTP 없는 임베디드 환경에서 문제
- UTC/로컬 타임 혼동 — Linux는 RTC를 UTC로, Windows는 로컬 타임으로 가정. 듀얼부팅 시 시간이 틀어지는 원인
- RTC 알람과 suspend — S3(suspend-to-RAM)에서 RTC 알람으로 깨울 수 있지만, 모든 RTC가 알람 IRQ를 지원하지는 않음.
/sys/class/rtc/rtc0/wakealarm지원 여부 확인 - NTP 드리프트 보상 — RTC는 수십 ppm의 오차 가능(월 수 초).
adjtimex로 커널이 NTP와 RTC 간 주기적 보정
delayed_work — 지연 실행 작업
delayed_work는 timer_list와 workqueue를 결합한 API입니다.
timer_list로 지정한 시간 후에 workqueue(kworker 스레드)에 작업을 등록하므로,
타이머 콜백과 달리 프로세스 컨텍스트에서 실행됩니다.
슬립, mutex 잠금, 메모리 할당(GFP_KERNEL) 등이 모두 허용됩니다.
timer_list vs delayed_work 선택 기준:
콜백에서 슬립/잠금이 필요하거나 실행 시간이 긴 경우 → delayed_work
나노초 정밀도가 필요하거나 인터럽트 컨텍스트에서 실행해야 하는 경우 → hrtimer
단순 타임아웃(슬립 불필요) → timer_list
#include <linux/workqueue.h>
#include <linux/jiffies.h>
struct my_dev {
struct delayed_work poll_work; /* delayed_work 선언 */
void __iomem *base;
};
/* workqueue 핸들러 — 프로세스 컨텍스트에서 실행 */
static void my_poll_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_dev *dev = container_of(dwork, struct my_dev, poll_work);
/* 프로세스 컨텍스트: mutex, kmalloc(GFP_KERNEL), msleep() 가능 */
u32 status = readl(dev->base + STATUS_REG);
if (status & BUSY_BIT) {
/* 아직 바쁨: 100ms 후 재시도 */
schedule_delayed_work(&dev->poll_work, msecs_to_jiffies(100));
} else {
pr_info("device ready\n");
}
}
/* 초기화 */
INIT_DELAYED_WORK(&dev->poll_work, my_poll_handler);
/* 200ms 후 실행 예약 */
schedule_delayed_work(&dev->poll_work, msecs_to_jiffies(200));
/* 특정 workqueue에 예약 (기본 system_wq 대신) */
queue_delayed_work(my_wq, &dev->poll_work, msecs_to_jiffies(200));
/* 취소 — 동기적으로 진행 중인 작업 완료까지 대기 */
cancel_delayed_work_sync(&dev->poll_work);
/* 재예약 (pending이면 취소 후 재등록) */
mod_delayed_work(system_wq, &dev->poll_work, msecs_to_jiffies(500));
| 특성 | timer_list | hrtimer | delayed_work |
|---|---|---|---|
| 실행 컨텍스트 | softirq (TIMER_SOFTIRQ) | hardirq / softirq | kworker 스레드 (프로세스) |
| 시간 해상도 | 1/HZ (≥4ms@250Hz) | 나노초 | 1/HZ (jiffies 기반) |
| 슬립 가능 | ❌ | ❌ | ✅ |
| mutex/semaphore | ❌ | ❌ | ✅ |
| kmalloc(GFP_KERNEL) | ❌ | ❌ | ✅ |
| CPU 친화성 | Per-CPU (timer wheel) | Per-CPU (hrtimer_cpu_base) | WQ 정책에 따름 |
| 취소 API | del_timer_sync() |
hrtimer_cancel() |
cancel_delayed_work_sync() |
| 주요 사용 사례 | 네트워크 타임아웃, 폴링 | nanosleep, POSIX 타이머 | 디바이스 폴링, I/O 완료 처리 |
schedule_delayed_work() 내부: 내부적으로 timer_list를 설정하고, 타이머 만료 시 queue_work()로 workqueue에 work를 등록합니다.
즉, 두 단계로 동작합니다: ① timer_list 만료 → ② kworker에서 핸들러 실행.
실제 실행 시각은 타이머 만료 후 kworker 스케줄링까지의 지연이 추가됩니다.
커널 Watchdog — Lockup 감지
커널 watchdog은 CPU가 장기간 응답하지 않는 lockup 상태를 자동으로 감지합니다. softlockup과 hardlockup 두 종류가 있으며, 각각 다른 타이머 메커니즘을 사용합니다.
Softlockup vs Hardlockup
| 종류 | 감지 조건 | 감지 시간 | 감지 메커니즘 | 커널 반응 | CONFIG 옵션 |
|---|---|---|---|---|---|
| Softlockup | CPU가 스케줄러에 제어를 오래 양보하지 않음 | watchdog_thresh 초 (기본 20초) | hrtimer + watchdog kthread | 경고 메시지 출력 (panic 옵션 가능) | CONFIG_SOFTLOCKUP_DETECTOR |
| Hardlockup | CPU가 인터럽트(hrtimer 포함)도 처리하지 않음 | watchdog_thresh/2 초 (기본 10초) | NMI watchdog (PMU 이벤트) | 패닉 또는 KDB/KGDB 진입 | CONFIG_HARDLOCKUP_DETECTOR |
# watchdog 설정 확인
cat /proc/sys/kernel/watchdog # 1=활성화
cat /proc/sys/kernel/watchdog_thresh # 임계값 (기본 10, softlockup=2배=20초)
cat /proc/sys/kernel/softlockup_panic # 1이면 softlockup 시 패닉
cat /proc/sys/kernel/hardlockup_panic # 1이면 hardlockup 시 패닉
# watchdog 비활성화 (테스트/디버깅용)
echo 0 > /proc/sys/kernel/watchdog
# 임계값 변경 (20초 → 30초로 완화)
echo 15 > /proc/sys/kernel/watchdog_thresh # softlockup=30초, hardlockup=15초
# softlockup 로그 (dmesg 출력 예시)
# [12345.678] watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [my_task:4567]
# [12345.679] Modules linked in: ...
# [12345.680] CPU: 3 PID: 4567 Comm: my_task Not tainted 6.1.0 #1
# 커널 부트 파라미터로 watchdog 설정
# nosoftlockup — softlockup 감지 비활성화
# nohlt — halt 명령 대신 idle 루프 (전력 절약 비활성화)
- 긴 임계 섹션 — spinlock을 오래 잡거나 preemption을 비활성화한 상태로 무거운 연산을 수행하면 softlockup 발생.
cond_resched()로 스케줄러에 제어를 양보해야 합니다. - 인터럽트 비활성화 —
local_irq_disable()상태로 오래 실행하면 hardlockup 발생. 인터럽트 비활성화 구간은 최소화해야 합니다. - PREEMPT_RT — 실시간 커널에서는 스핀락도 슬립 가능 뮤텍스로 교체되어 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을 중단하여 사용자 공간 작업에 대한 커널 간섭을 최소화합니다.
| 특성 | 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 관련 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 | 잘못된 선택 | 이유 |
|---|---|---|---|
| 네트워크 타임아웃 (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 실무 설정 예시
GRUB_CMDLINE_LINUX="nohz_full=1-7 rcu_nocbs=1-7 isolcpus=domain,managed_irq,1-7 irqaffinity=0"
# 부팅 후 확인
grep NO_HZ /boot/config-$(uname -r)
cat /sys/devices/system/cpu/nohz_full
cat /proc/cmdline
# 인터럽트가 CPU0(housekeeping)로 몰렸는지 확인
watch -n1 'grep -E "LOC|RES|CAL|TLB|NET_RX" /proc/interrupts'
Timer Slack과 Coalescing
타이머 정확도를 조금 양보하면 wakeup 횟수를 크게 줄일 수 있습니다. 커널은 timer slack으로 타이머 만료를 근접 시점에 묶어(coalescing) 전력 효율을 높입니다.
/* timer slack API: 정확도 대신 wakeup 절감 */
#include <linux/sched.h>
#include <linux/timer.h>
/* 현재 태스크의 slack 설정 (나노초) */
current->timer_slack_ns = 20 * 1000 * 1000; /* 20ms */
/* hrtimer 범위 예약: [expires, expires+delta] 범위 내 만료 허용 */
hrtimer_start_range_ns(&timer,
ms_to_ktime(100), /* 목표 만료 */
20 * 1000 * 1000, /* 허용 오차 */
HRTIMER_MODE_REL);
/* 유저 공간에서는 prctl(PR_SET_TIMERSLACK, ns) 사용 */
timekeeping과 NTP 보정 경로
타이머 정확도는 단순히 하드웨어 클럭 성능만으로 결정되지 않습니다. timekeeping 서브시스템은 clocksource 카운터를 ns로 변환하고, NTP PLL/FLL 보정을 적용해 장기 오차를 줄입니다.
/* 시간 읽기와 변환 흐름 요약 */
#include <linux/timekeeping.h>
/* monotonic 시간 읽기 */
ktime_t now = ktime_get();
/* ns 단위 */
u64 ns = ktime_to_ns(now);
/* realtime 읽기 (NTP/관리자 설정 반영) */
struct timespec64 ts;
ktime_get_real_ts64(&ts);
/* boottime: suspend 기간 포함 */
ktime_get_boottime_ts64(&ts);
타이머 트러블슈팅 플레이북
지연/지터 문제는 "타이머가 느린가"보다 "어떤 경로에서 늦어지는가"를 분리해야 해결됩니다. 아래 순서로 계측하면 원인을 빠르게 좁힐 수 있습니다.
# 1) 타이머/인터럽트 기본 상태
cat /proc/timer_list | head -80
grep -E "LOC|RES|CAL|TLB|TIMER" /proc/interrupts
# 2) 지터 측정
cyclictest -m -n -p99 -i 1000 -l 200000
# 3) timer/hrtimer tracepoint
echo 1 > /sys/kernel/debug/tracing/events/timer/timer_start/enable
echo 1 > /sys/kernel/debug/tracing/events/timer/hrtimer_start/enable
echo 1 > /sys/kernel/debug/tracing/events/timer/hrtimer_expire_entry/enable
sleep 3
cat /sys/kernel/debug/tracing/trace | tail -n 120
# 4) 전력/웨이크업 확인
powertop --time=10 --html
hrtimer backlog 제어와 우선순위 역전
지연 문제에서 자주 놓치는 부분은 "타이머 만료 시각"보다 "콜백이 실제 실행되는 순서"입니다. softirq backlog가 길거나 callback 내부에서 긴 작업을 수행하면, 높은 중요도의 hrtimer도 후순위로 밀릴 수 있습니다.
/* 안티패턴: hrtimer callback에서 장시간 처리 */
enum hrtimer_restart bad_timer_fn(struct hrtimer *t)
{
/* 금지: 긴 루프/슬립/복잡한 락 경합 */
do_heavy_work();
return HRTIMER_RESTART;
}
/* 권장: 최소 작업 후 deferred 처리 */
enum hrtimer_restart good_timer_fn(struct hrtimer *t)
{
queue_work(system_unbound_wq, &ctx->work);
hrtimer_forward_now(t, interval);
return HRTIMER_RESTART;
}
운영 정책: 지연 목표와 전력 목표의 균형
타이머 튜닝은 "최저 지연"과 "최저 전력"을 동시에 만족시키기 어렵습니다. 서비스 SLO를 먼저 정하고, 타이머 정책을 등급별로 분리 운영하면 회귀를 줄일 수 있습니다.
| 서비스 등급 | 권장 타이머 정책 | 중점 지표 |
|---|---|---|
| 초저지연 (RT/제어) | hrtimer 중심, slack 최소화, 전용 CPU | p99/p999 latency, irq off time |
| 일반 서버 | timer coalescing 적극 사용, NO_HZ_IDLE | throughput/wakeup/s |
| 배치/백그라운드 | slack 확대, delayed_work 병합 | 전력/열/총 처리량 |
Tick Broadcast와 Deep Idle 복귀 지연
tickless 시스템에서 일부 CPU가 deep idle(C-state 깊은 단계)로 내려가면, 로컬 timer event 대신 broadcast 장치를 통한 깨움 경로가 사용됩니다. 이 경로는 전력 효율에는 유리하지만, 특정 패턴에서 깨움 지연 편차를 키울 수 있습니다.
# tick/nohz/idle 상태 확인
cat /proc/timer_list | grep -E 'broadcast|tick|expires_next' -n
cat /sys/devices/system/cpu/cpuidle/current_driver
cat /sys/devices/system/cpu/cpuidle/current_governor_ro
# 지연 측정과 함께 C-state 영향 비교
cyclictest -m -n -p99 -i 1000 -l 200000
powertop --time=10 --html
관련 문서
타이머와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.