타이머 (Timers)
jiffies, hrtimer, clocksource, clockevent, timer wheel, tickless 커널 등 Linux 커널의 시간 관리 체계를 설명합니다.
핵심 요약
- jiffies — 부팅 이후 경과한 타이머 틱 수. HZ(보통 1000)가 초당 틱 수를 결정합니다.
- timer_list — jiffies 기반 저해상도 타이머. timer wheel 알고리즘으로 관리됩니다.
- hrtimer — 나노초 해상도의 고해상도 타이머. Red-Black 트리로 관리됩니다.
- clocksource — 시간 측정 하드웨어(TSC, HPET 등)를 추상화하는 프레임워크입니다.
- NO_HZ(tickless) — idle CPU에서 불필요한 타이머 인터럽트를 생략하여 전력을 절약합니다.
단계별 이해
- 타이머 틱 — 타이머 하드웨어가 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 깊은 절전에 진입합니다.
타이머 서브시스템의 정의와 역할
타이머 서브시스템은 커널이 시간의 흐름을 인식하고 미래 시점에 작업을 예약할 수 있게 하는 핵심 인프라입니다. 이 서브시스템이 없으면 프로세스 스케줄링, 네트워크 타임아웃, 디바이스 폴링, 슬립 등 시간 기반 동작이 모두 불가능합니다.
타이머 서브시스템은 크게 세 가지 계층으로 구성됩니다:
- 하드웨어 계층: 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 오버헤드가 증가합니다.
#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);
고해상도 타이머 (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);
clocksource와 clockevent
clocksource는 시간 측정을 위한 하드웨어 클럭 추상화이고, clockevent는 미래 시점에 인터럽트를 발생시키는 하드웨어 타이머 추상화입니다. 대표적인 clocksource로는 TSC(x86, ~GHz), HPET(14MHz), ACPI PM Timer(3.58MHz), ARM arch timer 등이 있으며, clockevent는 Local APIC Timer(Per-CPU tick), HPET, ARM arch timer 등이 담당합니다.
clocksource/clockevent 프레임워크, 하드웨어 클럭별 상세 비교, clocksource 등록 API, rating 시스템 등은 ktime / Clock 심화 — clocksource 프레임워크에서 자세히 다룹니다.
지연 함수 (Delay Functions)
커널은 다양한 지연 함수를 제공합니다. 인터럽트 컨텍스트에서는 busy-wait(udelay(), ndelay())만 사용 가능하고, 프로세스 컨텍스트에서는 슬립 기반(usleep_range(), msleep())을 사용해야 합니다. 10ms 이상은 msleep(), 10us~10ms는 usleep_range(), 10us 미만은 udelay()를 권장합니다.
각 지연 함수의 내부 구현, 정밀도 비교, 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_IDLE: CPU가 idle 상태일 때 tick 중단 (기본값). 대부분의 서버/데스크탑에 적합합니다.
- CONFIG_NO_HZ_FULL: 실행 중인 태스크가 하나일 때도 tick 중단. 이는 사용자 공간 코드의 실행을 커널 인터럽트 없이 지속시켜 HPC, 실시간 워크로드에서 지터를 최소화합니다. 단, housekeeping CPU(최소 1개)는 항상 틱을 유지해야 합니다.
ktime API
ktime_t는 나노초 단위의 시간을 표현하는 통일된 타입입니다. ktime_get()(monotonic), ktime_get_real()(wall clock), ktime_get_boottime()(suspend 포함) 등으로 현재 시간을 얻고, ktime_add()/ktime_sub()/ktime_us_delta() 등으로 산술 연산합니다.
ktime_t 전체 함수 레퍼런스, 변환 매크로, 경과 시간 측정 패턴, clockevent 프레임워크(PERIODIC/ONESHOT 모드, 디바이스 등록) 등은 ktime / Clock 심화에서 상세히 다룹니다.
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를 오래 점유하므로 가급적 사용하지 마세요.
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초 뒤 깨우기
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 간 주기적 보정