ktime / Clock
ktime/Clock: clocksource, Common Clock Framework(CCF), timekeeper, CPU 주파수 관리(cpufreq/HWP), vDSO, NTP/PTP 동기화, TSC/HPET 하드웨어 클럭 완벽 가이드.
초점은 "커널이 시간을 어떻게 계산하고 보정하는가"입니다. timekeeper 내부의 누적 오차 보정, CLOCK 계열별 의미 차이, TSC 안정성 조건과 fallback 전략, cpufreq 변동이 타임스탬프 일관성에 주는 영향, NTP/PTP 보정 경로를 하나의 흐름으로 연결해 설명합니다. 결과적으로 시간 관련 버그(시계 역행, 지터 급증, 동기화 불안정)를 진단할 때 어떤 자료를 수집하고 어떤 순서로 가설을 검증해야 하는지까지 실전 절차로 제시합니다.
ktime_t 함수 레퍼런스, clocksource 프레임워크, Common Clock Framework(CCF), timekeeper 내부 구현, 7가지 CLOCK_* 시계, CPU 주파수 관리(cpufreq/DVFS/P-State/HWP), vDSO 최적화, NTP/PTP 동기화, 하드웨어 클럭(TSC/HPET/ACPI PM), 지연(Latency) 함수까지 — Linux 커널의 시간 관리 체계를 소스 코드 수준에서 분석합니다.
핵심 요약
- ktime_t — 나노초 단위의 64비트 시간 값. 커널 타이머 API의 표준 시간 타입입니다.
- timekeeper — clocksource를 읽어 벽시계 시간(wall clock)과 모노토닉 시간을 유지하는 핵심 구조체(Struct)입니다.
- CLOCK_REALTIME / CLOCK_MONOTONIC — 대표적인 두 시계. REALTIME은 벽시계, MONOTONIC은 부팅 후 단조 증가합니다.
- vDSO —
gettimeofday()등을 커널 진입 없이 사용자 공간(User Space)에서 실행하는 최적화입니다. - NTP / PTP — 네트워크를 통해 시스템 시계를 외부 기준 시계와 동기화합니다.
단계별 이해
- 클럭 소스 확인 —
cat /sys/devices/system/clocksource/clocksource0/current_clocksource로 현재 사용 중인 클럭 소스를 확인합니다.대부분의 x86 시스템에서는 TSC(Time Stamp Counter)가 사용됩니다.
- 시간 읽기 — 커널에서
ktime_get()(모노토닉) 또는ktime_get_real()(벽시계)로 현재 시각을 읽습니다.사용자 공간에서는
clock_gettime(CLOCK_MONOTONIC, &ts)를 사용합니다. - 시간 동기화 —
chrony나ntpd가 NTP 서버와 통신하여 커널의 timekeeper를 보정합니다.timedatectl로 현재 NTP 동기화 상태를 확인할 수 있습니다.
Timekeeping 아키텍처 개요
ktime_t 함수 전체 레퍼런스
ktime_t는 나노초 해상도의 시간값을 표현하는 커널의 통일된 시간 타입입니다. 내부적으로 s64 (signed 64-bit) 나노초 값입니다.
/* include/linux/ktime.h */
typedef s64 ktime_t; /* 나노초 단위, signed 64-bit */
/* 표현 가능 범위: ±292년 (2^63 ns ≈ 292.47 years) */
오버플로 고려사항
ktime_t는 ±292년 범위를 가지지만, 산술 연산 시 오버플로가 발생할 수 있습니다. 특히 ktime_add()에서 두 큰 양수 값을 더하면 음수로 wrap될 수 있습니다.
/* 오버플로 안전 패턴 */
/* 위험: 직접 나노초 산술 — 오버플로 가능 */
s64 total_ns = elapsed_ns * count; /* count가 크면 오버플로! */
/* 안전: ktime_add_ns()는 내부적으로 오버플로 검사 없음
* → 호출자가 범위를 보장해야 함 */
if (count > NSEC_PER_SEC * 60 * 60) {
pr_warn("timer interval too large\\n");
return -ERANGE;
}
/* 실제 드라이버 사용 패턴: 구간 측정 + 통계 */
struct my_device {
ktime_t last_event;
u64 total_us; /* 마이크로초 누적 (오버플로 방지) */
u32 event_count;
};
static irqreturn_t my_isr(int irq, void *data)
{
struct my_device *dev = data;
ktime_t now = ktime_get();
if (dev->last_event) {
s64 delta_us = ktime_us_delta(now, dev->last_event);
dev->total_us += delta_us; /* us 단위로 누적 → 오버플로 위험 감소 */
dev->event_count++;
}
dev->last_event = now;
return IRQ_HANDLED;
}
시간 읽기 함수
| 함수 | 시계 기준 | NTP 보정 | suspend 포함 | 용도 |
|---|---|---|---|---|
ktime_get() | MONOTONIC | O | X | 일반적인 경과 시간 측정 |
ktime_get_real() | REALTIME | O | O | 벽시계 시간 (UTC) |
ktime_get_boottime() | BOOTTIME | O | O | 부팅 이후 총 경과 시간 |
ktime_get_clocktai() | TAI | O | O | 윤초 없는 절대 시간 |
ktime_get_raw() | MONOTONIC_RAW | X | X | 하드웨어 클럭 직접 읽기 |
ktime_get_ts64() | MONOTONIC | O | X | timespec64 결과 |
ktime_get_real_ts64() | REALTIME | O | O | timespec64 결과 |
ktime_get_coarse() | MONOTONIC | O | X | 틱 해상도, 매우 빠름 |
ktime_get_coarse_real() | REALTIME | O | O | 틱 해상도, 매우 빠름 |
ktime_get_coarse_boottime() | BOOTTIME | O | O | 틱 해상도, 매우 빠름 |
ktime_get_fast_ns() | MONOTONIC | O | X | NMI-safe, seqcount 기반 |
ktime_get_mono_fast_ns() | MONOTONIC | O | X | NMI-safe 별칭 |
ktime_get_raw_fast_ns() | MONOTONIC_RAW | X | X | NMI-safe, 원시 클럭 |
ktime_get_boot_fast_ns() | BOOTTIME | O | O | NMI-safe, 부팅 시간 |
ktime_get_real_fast_ns() | REALTIME | O | O | NMI-safe, 벽시계 |
ktime_get()은 하드웨어 카운터를 직접 읽어 나노초 해상도를 제공합니다(~20-80ns 비용). ktime_get_coarse()는 마지막 틱에 캐시(Cache)된 값을 반환하여 매우 빠르지만(~1-5ns) 해상도가 1/HZ(4ms@250Hz)입니다. ktime_get_fast_ns()는 NMI/하드IRQ 컨텍스트에서도 안전하게 사용 가능하며, seqcount로 일관성을 보장합니다.
ktime 산술/변환 함수
#include <linux/ktime.h>
/* ===== 생성/변환 ===== */
ktime_t t1 = ns_to_ktime(5000000); /* 5ms = 5,000,000 ns */
ktime_t t2 = ms_to_ktime(100); /* 100ms */
ktime_t t3 = us_to_ktime(500); /* 500us (v6.x+) */
ktime_t t4 = ktime_set(5, 123456789); /* 5초 + 123,456,789ns */
s64 ns = ktime_to_ns(t1); /* → 나노초 */
s64 us = ktime_to_us(t1); /* → 마이크로초 */
s64 ms = ktime_to_ms(t1); /* → 밀리초 */
/* ktime ↔ timespec64 변환 */
struct timespec64 ts = ktime_to_timespec64(t1);
ktime_t kt = timespec64_to_ktime(ts);
/* ===== 산술 연산 ===== */
ktime_t sum = ktime_add(a, b); /* a + b */
ktime_t diff = ktime_sub(a, b); /* a - b */
ktime_t add_ns = ktime_add_ns(a, 1000); /* a + 1000ns */
ktime_t add_us = ktime_add_us(a, 500); /* a + 500us */
ktime_t add_ms = ktime_add_ms(a, 100); /* a + 100ms */
ktime_t sub_ns = ktime_sub_ns(a, 1000); /* a - 1000ns */
/* ===== 비교 연산 ===== */
bool is_after = ktime_after(a, b); /* a > b */
bool is_before = ktime_before(a, b); /* a < b */
int cmp = ktime_compare(a, b); /* -1, 0, 1 */
bool is_zero = ktime_is_null(a); /* a == 0 */
/* ===== 델타 계산 ===== */
s64 delta_ns = ktime_to_ns(ktime_sub(end, start));
s64 delta_us = ktime_us_delta(end, start); /* 마이크로초 차이 */
s64 delta_ms = ktime_ms_delta(end, start); /* 밀리초 차이 */
/* ===== 실용 패턴: 구간 측정 ===== */
ktime_t start = ktime_get();
/* ... 측정할 코드 ... */
s64 elapsed_us = ktime_us_delta(ktime_get(), start);
pr_info("operation took %lld us\\n", elapsed_us);
/* ===== 실용 패턴: 타임아웃 ===== */
ktime_t deadline = ktime_add_ms(ktime_get(), 500); /* 500ms 후 */
while (!condition_met()) {
if (ktime_after(ktime_get(), deadline))
return -ETIMEDOUT;
cpu_relax();
}
ktime 실전 사용 패턴
커널 드라이버에서 ktime API를 사용할 때 자주 등장하는 실전 패턴과, 흔히 저지르는 실수를 정리합니다.
패턴 1: 네트워크 드라이버 패킷 타임아웃 (BOOTTIME 사용)
패킷 에이징(packet aging)에는 ktime_get_boottime()을 사용해야 합니다. ktime_get()(MONOTONIC)은 suspend 동안 정지하므로, 시스템이 절전 후 깨어났을 때 패킷이 실제보다 "젊게" 보이는 문제가 발생합니다.
/* 네트워크 드라이버: suspend를 고려한 패킷 타임아웃 */
struct my_packet {
ktime_t arrival; /* 패킷 도착 시각 */
struct sk_buff *skb;
};
static void enqueue_packet(struct my_packet *pkt, struct sk_buff *skb)
{
pkt->skb = skb;
pkt->arrival = ktime_get_boottime(); /* suspend 포함 시계 */
}
static bool is_packet_expired(struct my_packet *pkt, s64 timeout_ms)
{
ktime_t deadline = ktime_add_ms(pkt->arrival, timeout_ms);
return ktime_after(ktime_get_boottime(), deadline);
}
코드 설명
suspend/resume을 고려한 패킷 타임아웃 패턴입니다.
- ktime_get_boottime()CLOCK_BOOTTIME 기반으로, suspend 동안의 경과 시간도 포함합니다. 시스템이 30분간 절전했다면 그 시간도 패킷 나이에 반영됩니다.
- ktime_after()두 ktime_t 값을 비교하는 인라인 함수입니다. 내부적으로
a > b를 수행하며, 오버플로에 안전합니다.
패턴 2: schedule_hrtimeout_range() 정밀 슬립
프로세스 컨텍스트에서 마이크로초~밀리초 범위의 정밀한 대기가 필요할 때 사용합니다. usleep_range()와 달리 hrtimer 기반이며, 슬랙(slack) 범위 내에서 타이머 합병(coalescing)이 가능합니다.
/* 정밀 슬립: 500~600μs 범위 대기 */
static int wait_for_phy_ready(struct device *dev)
{
ktime_t slack = ns_to_ktime(100 * NSEC_PER_USEC); /* 100μs 슬랙 */
int retries = 10;
while (retries--) {
if (phy_is_ready(dev))
return 0;
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_hrtimeout_range(ns_to_ktime(500 * NSEC_PER_USEC), slack,
HRTIMER_MODE_REL);
}
return -ETIMEDOUT;
}
코드 설명
schedule_hrtimeout_range()는 hrtimer 기반 정밀 슬립으로, 슬랙 범위 내에서 다른 타이머와 합병되어 전력 효율을 높입니다.
- slack (100μs)이 범위 안에서 타이머가 합병될 수 있어, CPU가 불필요하게 깨어나는 횟수를 줄입니다.
- set_current_state(TASK_UNINTERRUPTIBLE)
schedule_hrtimeout_range()호출 전에 태스크 상태를 설정해야 합니다. 시그널로 인한 조기 깨어남이 불필요하면 UNINTERRUPTIBLE을 사용합니다.
패턴 3: ktime_get_raw() 하드웨어 벤치마킹
순수 하드웨어 성능을 측정할 때는 NTP 보정이 없는 ktime_get_raw()를 사용합니다. NTP가 주파수를 slew하는 중이면 ktime_get()으로 측정한 시간이 실제와 최대 500ppm(0.05%) 차이가 날 수 있습니다.
/* 하드웨어 벤치마크: NTP 스큐 없는 순수 측정 */
static void benchmark_dma_transfer(struct device *dev)
{
ktime_t t0, t1;
s64 ns;
t0 = ktime_get_raw();
start_dma_and_wait(dev);
t1 = ktime_get_raw();
ns = ktime_to_ns(ktime_sub(t1, t0));
dev_info(dev, "DMA transfer: %lld ns\n", ns);
}
패턴 4: 잘못된 클럭 기준 혼합 (안티패턴)
서로 다른 시계에서 얻은 ktime_t 값을 비교하면 suspend/resume 시 예측 불가능한 오차가 발생합니다. 또한 jiffies와 ktime을 같은 타임아웃 로직에 혼합하면 해상도 불일치로 미묘한 버그가 생깁니다.
/* ✗ 버그: 서로 다른 시계 기준 비교 */
ktime_t start = ktime_get(); /* MONOTONIC */
/* ... suspend 발생 가능 ... */
ktime_t now = ktime_get_boottime(); /* BOOTTIME (suspend 포함) */
s64 elapsed = ktime_us_delta(now, start); /* 오차! suspend 시간 이중 계산 */
/* ✓ 수정: 같은 시계 기준 사용 */
ktime_t start = ktime_get_boottime();
/* ... */
ktime_t now = ktime_get_boottime();
s64 elapsed = ktime_us_delta(now, start); /* 정확 */
/* ✗ 버그: jiffies와 ktime 혼합 타임아웃 */
unsigned long timeout = jiffies + msecs_to_jiffies(100); /* ~4ms 해상도 */
while (ktime_before(ktime_get(), some_ktime_deadline)) {
if (time_after(jiffies, timeout)) /* 해상도 불일치 */
break;
}
/* ✓ 수정: ktime으로 통일 */
ktime_t deadline = ktime_add_ms(ktime_get(), 100);
while (ktime_before(ktime_get(), deadline)) {
if (condition_met())
break;
cpu_relax();
}
코드 설명
클럭 기준 혼합은 미묘하지만 심각한 버그를 유발합니다.
- MONOTONIC vs BOOTTIME 혼합MONOTONIC은 suspend 동안 정지하고 BOOTTIME은 계속 진행합니다. 혼합하면 suspend 시간만큼 경과 시간이 왜곡됩니다.
- jiffies vs ktime 혼합jiffies는 HZ 의존(보통 ~4ms 해상도)이고 ktime은 나노초 해상도입니다. 같은 타임아웃에 혼합하면 해상도 불일치로 타임아웃이 일찍 또는 늦게 발생합니다.
고급 ktime 유틸리티 함수
기본 산술 외에도 커널은 다양한 고급 ktime 유틸리티 함수를 제공합니다. 실무에서 자주 사용되지만 놓치기 쉬운 함수들을 용도별로 정리합니다.
/* === 1. 안전한 산술 연산 === */
/* ktime_add_safe: 오버플로 방지 덧셈 — hrtimer 내부에서 사용 */
ktime_t ktime_add_safe(const ktime_t lhs, const ktime_t rhs)
{
ktime_t res = ktime_add_unsafe(lhs, rhs);
if (res < lhs || res < rhs)
res = KTIME_MAX; /* 오버플로 → 클램프 */
return res;
}
/* 사용 예: hrtimer 만료 시각 설정 */
ktime_t expires = ktime_add_safe(ktime_get(), interval);
/* === 2. 나눗셈 헬퍼 === */
u64 ktime_divns(const ktime_t kt, s64 divisor);
/* 사용 예: 평균 타이머 간격 */
u64 avg_ns = ktime_divns(total_elapsed, count);
/* === 3. 클럭 간 변환 === */
ktime_t mono = ktime_get();
ktime_t real = ktime_mono_to_real(mono);
/* 내부: return ktime_add(mono, tk->offs_real); */
ktime_t boot = ktime_mono_to_any(mono, TK_OFFS_BOOT);
ktime_t tai = ktime_mono_to_any(mono, TK_OFFS_TAI);
/* === 4. 초 해상도 고속 게터 (클럭소스 읽기 없음, ~1ns) === */
time64_t mono_sec = ktime_get_seconds(); /* MONOTONIC 초 */
time64_t real_sec = ktime_get_real_seconds(); /* REALTIME 초 */
/* 사용 예: 로그 로테이션 — 초 정밀도로 충분 */
if (ktime_get_real_seconds() - last_rotate >= 3600)
rotate_log();
/* === 5. coarse 시리즈 (마지막 틱 캐시, HW 읽기 없음) === */
struct timespec64 ts;
ktime_get_coarse_ts64(&ts); /* MONOTONIC coarse */
ktime_get_coarse_real_ts64(&ts); /* REALTIME coarse */
ktime_get_coarse_boottime_ts64(&ts); /* BOOTTIME coarse */
/* 해상도 = 1/HZ (HZ=250 → 4ms, HZ=1000 → 1ms) */
/* === 6. 편의 래퍼 (_ns 접미사) === */
u64 now_ns = ktime_get_ns(); /* ≡ ktime_to_ns(ktime_get()) */
u64 real_ns = ktime_get_real_ns();
u64 boot_ns = ktime_get_boottime_ns();
u64 tai_ns = ktime_get_clocktai_ns();
u64 raw_ns = ktime_get_raw_ns();
/* tracing에서 간결하게 사용 */
u64 start = ktime_get_ns();
do_work();
trace_printk("elapsed: %llu ns\n", ktime_get_ns() - start);
/* === 7. 해상도 조회 === */
u64 res_ns = ktime_get_resolution_ns();
/* TSC: 1ns, HPET: 수십 ns */
코드 설명
- ktime_add_safe()결과가 피연산자보다 작으면 오버플로로 판단하고
KTIME_MAX를 반환합니다.hrtimer_forward()등 외부 입력이 관여하는 경로에서 사용됩니다. - ktime_divns()32비트 아키텍처에서도 안전하게 64비트 나눗셈을 처리하기 위해 내부적으로
do_div()를 사용합니다. - ktime_mono_to_real()
offs_real오프셋을 더하는 단순 덧셈입니다. NTP 보정이나settimeofday()호출 시 오프셋이 변경되므로, 변환 결과는 호출 시점에 의존합니다. - ktime_get_seconds()
timekeeper의ktime_sec필드를 직접 읽습니다. 하드웨어 접근 없이 ~1ns 비용입니다. 초 이하 정밀도가 불필요한 경로에서ktime_get()대신 사용하면 성능 이점이 큽니다. - coarse 시리즈틱마다 갱신되는 캐시 값을 반환합니다. vDSO에서도
CLOCK_MONOTONIC_COARSE/CLOCK_REALTIME_COARSE로 접근 가능합니다. - _ns() 래퍼
ktime_get()이ktime_t(s64)를 반환하는 반면,ktime_get_ns()는u64나노초를 반환합니다. 내부 경로는 동일하며 반환 타입만 다릅니다.
| 폐기된 함수 | 대체 함수 |
|---|---|
getrawmonotonic64() | ktime_get_raw_ts64() |
get_monotonic_boottime() | ktime_get_boottime_ts64() |
do_gettimeofday() | ktime_get_real_ts64() |
getnstimeofday64() | ktime_get_real_ts64() |
CLOCK_* 시계 체계
Linux 커널은 용도에 따라 여러 종류의 시계를 유지합니다. 각 시계는 기준점(epoch), NTP 보정 여부, suspend 동작이 다릅니다.
| 시계 | 기준점 | NTP 보정 | suspend | 윤초 | 설명 |
|---|---|---|---|---|---|
| CLOCK_REALTIME | 1970-01-01 UTC | O | 진행 | 적용 | 벽시계. settimeofday()로 변경 가능. 로그 타임스탬프용 |
| CLOCK_MONOTONIC | 부팅 시점 | O (주파수) | 정지 | X | 단조 증가. 경과 시간 측정의 기본 시계 |
| CLOCK_MONOTONIC_RAW | 부팅 시점 | X | 정지 | X | NTP 보정 없는 원시 하드웨어 틱. 하드웨어 벤치마크 |
| CLOCK_MONOTONIC_COARSE | 부팅 시점 | O (주파수) | 정지 | X | 틱 해상도(~4ms). 매우 빠름. 대략적 시간용 |
| CLOCK_REALTIME_COARSE | 1970-01-01 UTC | O | 진행 | 적용 | 틱 해상도 벽시계. 매우 빠름 |
| CLOCK_BOOTTIME | 부팅 시점 | O (주파수) | 진행 | X | suspend 시간 포함. 모바일, 네트워크 타임아웃 |
| CLOCK_TAI | 1970-01-01 TAI | O | 진행 | X | 국제원자시. 윤초 없음. PTP, 금융 타임스탬프 |
| CLOCK_PROCESS_CPUTIME_ID | 프로세스(Process) 생성 | X | 정지 | X | 프로세스 CPU 시간 (user+sys) |
| CLOCK_THREAD_CPUTIME_ID | 스레드(Thread) 생성 | X | 정지 | X | 스레드 CPU 시간 |
시계 선택 의사결정 테이블
| 사용 시나리오 | 권장 시계 | 이유 | 주의사항 |
|---|---|---|---|
| 일반 경과 시간 측정 | CLOCK_MONOTONIC | NTP 보정 반영, 단조 증가 | suspend 시 정지 |
| 네트워크 타임아웃 | CLOCK_BOOTTIME | suspend 시에도 진행 | 모바일/IoT 필수 |
| 로그/감사 타임스탬프 | CLOCK_REALTIME | UTC 벽시계 시간 | NTP step으로 역행 가능 |
| 하드웨어 벤치마크 | CLOCK_MONOTONIC_RAW | NTP 보정 없는 원시 값 | 장기 측정 시 드리프트 |
| 고빈도 대략적 시간 | CLOCK_MONOTONIC_COARSE | vDSO, 카운터 읽기 불필요 | 해상도 1/HZ (~4ms) |
| PTP/금융 타임스탬프 | CLOCK_TAI | 윤초 없는 연속 시간 | tai_offset 설정 필요 |
| NMI/하드IRQ 컨텍스트 | ktime_get_fast_ns() | NMI-safe, seqcount 기반 | 드물게 불일치 가능 |
| 컨테이너(Container) 마이그레이션 | CLOCK_MONOTONIC + timens | Time namespace 오프셋(Offset) 적용 | REALTIME은 오프셋 불가 |
시계 간 관계
- 시계 간 관계:
- REALTIME = MONOTONIC + wall_to_monotonic + 윤초
- BOOTTIME = MONOTONIC + total_sleep_time
- TAI = REALTIME + tai_offset (현재 37초)
- RAW = 하드웨어 카운터 × 주파수 변환 (NTP 보정 없음)
- 시간 흐름 예시 (suspend 포함):
- 부팅 10s suspend(5s) 깨어남 20s
- MONOTONIC: 0 → 10 (정지) 10 → 20
- BOOTTIME: 0 → 10 (진행) 15 → 25
- REALTIME: T → T+10 (진행) T+15 → T+25
- RAW: 0 → 10* (정지) 10* → 20*
- (* NTP 보정 없는 원시값)
시계 선택 가이드
/* 커널 코드에서 시계 선택 기준: */
/* 1. 일반적인 경과 시간 → ktime_get() (MONOTONIC) */
ktime_t start = ktime_get();
/* 2. 네트워크 타임아웃, 모바일 알람 → ktime_get_boottime()
* suspend 동안에도 타임아웃이 진행되어야 할 때 */
ktime_t deadline = ktime_add_ms(ktime_get_boottime(), timeout_ms);
/* 3. 로그/감사 타임스탬프 → ktime_get_real() (REALTIME) */
struct timespec64 ts;
ktime_get_real_ts64(&ts);
/* 4. 하드웨어 벤치마크 → ktime_get_raw() (NTP 보정 배제) */
ktime_t hw_start = ktime_get_raw();
/* 5. 대략적 시간 (고빈도 호출) → ktime_get_coarse() */
ktime_t approx = ktime_get_coarse();
/* 6. NMI/하드IRQ 컨텍스트 → ktime_get_fast_ns() */
u64 nmi_ts = ktime_get_mono_fast_ns();
/* 7. PTP/금융 타임스탬프 → ktime_get_clocktai() (윤초 없음) */
ktime_t tai = ktime_get_clocktai();
Timekeeper 내부 구현
struct timekeeper는 커널의 모든 시간 기준을 유지하는 핵심 자료구조입니다. clocksource에서 읽은 하드웨어 카운터를 나노초로 변환하고, NTP 보정을 적용합니다.
/* kernel/time/timekeeping.c */
struct timekeeper {
/* 현재 활성 clocksource와 변환 정보 */
struct tk_read_base tkr_mono; /* MONOTONIC 읽기 기반 */
struct tk_read_base tkr_raw; /* RAW 읽기 기반 */
/* 벽시계 오프셋 */
u64 xtime_sec; /* REALTIME 초 부분 */
unsigned long ktime_sec; /* MONOTONIC 초 (캐시) */
/* 시계 간 오프셋 */
struct timespec64 wall_to_monotonic; /* REALTIME→MONOTONIC */
ktime_t offs_real; /* MONOTONIC→REALTIME 오프셋 */
ktime_t offs_boot; /* MONOTONIC→BOOTTIME 오프셋 */
ktime_t offs_tai; /* MONOTONIC→TAI 오프셋 */
/* NTP 보정 */
s64 ntp_error; /* 누적 NTP 오차 */
u32 ntp_error_shift;
u32 ntp_err_mult;
/* suspend 관련 */
ktime_t total_sleep_time; /* 총 suspend 시간 */
};
/* tk_read_base — clocksource 읽기 최적화 구조 */
struct tk_read_base {
struct clocksource *clock; /* 현재 clocksource */
u64 mask; /* 카운터 비트 마스크 */
u64 cycle_last; /* 마지막 읽은 사이클 값 */
u32 mult; /* 사이클→나노초 곱셈 인수 */
u32 shift; /* 사이클→나노초 시프트 인수 */
u64 xtime_nsec; /* 나노초 누적 (시프트됨) */
ktime_t base; /* 기준 ktime 값 */
};
코드 설명
include/linux/timekeeper_internal.h에 정의된 struct timekeeper는 Linux 시간 관리의 핵심 구조체로, 모든 CLOCK_* 시계의 현재 값을 유지합니다. 전역 인스턴스 tk_core.timekeeper가 seqcount로 보호됩니다.
- tkr_mono / tkr_raw각각 MONOTONIC과 MONOTONIC_RAW 시계의 읽기 기반 정보를 담는
tk_read_base구조체입니다. NTP 보정 여부에 따라mult값이 다릅니다. - xtime_secCLOCK_REALTIME의 초 부분(Unix epoch 기준)입니다. 나노초 부분은
tkr_mono.xtime_nsec에서 오프셋을 더해 계산합니다. - wall_to_monotonicREALTIME에서 MONOTONIC으로의 오프셋입니다.
MONOTONIC = REALTIME + wall_to_monotonic관계가 성립합니다. - offs_real / offs_boot / offs_taiMONOTONIC 기준으로 각 시계(REALTIME, BOOTTIME, TAI)의 오프셋을
ktime_t로 저장합니다.ktime_get_real()등이 이 오프셋을 더해 결과를 반환합니다. - ntp_errorNTP 보정에서 발생하는 누적 오차입니다.
update_wall_time()에서 매 틱마다 이 오차를mult조정에 반영합니다. - total_sleep_timesuspend 동안 경과한 총 시간입니다. CLOCK_BOOTTIME은 이 값을 MONOTONIC에 더하여 계산됩니다.
- tk_read_base.cycle_last마지막으로 읽은 하드웨어 카운터 값입니다. 현재 사이클과의 차분(
delta)을 구해 경과 나노초를 계산합니다. - tk_read_base.mult / shift사이클을 나노초로 변환하는 고정소수점 파라미터입니다. NTP가
tkr_mono.mult를 미세 조정하여 주파수 보정을 수행합니다.
seqcount vs spinlock 비교
| 특성 | seqcount (timekeeper) | spinlock |
|---|---|---|
| 읽기 비용 | 락 없음 (read_seqcount_begin) | 스핀 대기 (경합(Contention) 시) |
| 쓰기 비용 | 카운터 증가만 | 락 획득/해제 |
| 읽기 동시성 | 무제한 병렬 읽기 | 읽기도 배타적 (spinlock의 경우) |
| 일관성 보장 | retry 기반 — 불일치 시 재시도 | 항상 일관된 스냅샷 |
| NMI/하드IRQ 안전 | 읽기 안전 (fast 변형) | 불안전 (데드락 위험) |
| 적합 시나리오 | 읽기 빈도 >> 쓰기 빈도 | 읽기/쓰기 빈도 유사 |
| timekeeper 선택 이유 | ktime_get() 호출 빈도 극히 높음 | — |
시간 읽기 흐름
/* ktime_get() 내부 동작 (간략화): */
ktime_t ktime_get(void)
{
struct timekeeper *tk = &tk_core.timekeeper;
ktime_t base;
u64 delta, nsec;
unsigned int seq;
do {
seq = read_seqcount_begin(&tk_core.seq);
/* 1. 기준 시간 읽기 (마지막 업데이트 시점의 값) */
base = tk->tkr_mono.base;
/* 2. 하드웨어 카운터 읽기 */
u64 cycle_now = tk->tkr_mono.clock->read(tk->tkr_mono.clock);
/* 3. 마지막 읽기 이후 경과한 사이클 계산 */
delta = (cycle_now - tk->tkr_mono.cycle_last) & tk->tkr_mono.mask;
/* 4. 사이클 → 나노초 변환
* nsec = (delta * mult) >> shift
* mult와 shift는 NTP 보정이 반영된 값 */
nsec = delta * tk->tkr_mono.mult;
nsec >>= tk->tkr_mono.shift;
} while (read_seqcount_retry(&tk_core.seq, seq));
/* 5. 기준 시간 + 경과 나노초 = 현재 시간 */
return ktime_add_ns(base, nsec);
}
/*
* seqcount를 사용하는 이유:
* - timekeeper 업데이트(update_wall_time)는 틱 인터럽트에서 수행
* - 읽기(ktime_get)는 아무 컨텍스트에서 호출 가능
* - seqcount로 락 없이 일관된 스냅샷 보장
* - 쓰기 중 읽으면 retry
*/
코드 설명
kernel/time/timekeeping.c의 ktime_get()은 CLOCK_MONOTONIC 시간을 반환하는 가장 일반적인 커널 시간 읽기 함수입니다. seqcount 기반의 락-프리 읽기 패턴을 사용합니다.
- read_seqcount_begin()seqcount의 현재 시퀀스 번호를 읽습니다. 이 값이 홀수이면 쓰기 중이므로 즉시 retry합니다.
- tkr_mono.base마지막
update_wall_time()에서 계산된 기준 시간(ktime_t)입니다. 이 값에 경과 나노초를 더해 현재 시간을 구합니다. - clock->read()활성 clocksource의 하드웨어 카운터를 읽습니다. TSC의 경우
rdtsc_ordered()가 호출되어 ~20ns 내에 완료됩니다. - delta & mask현재 사이클에서 마지막 사이클을 빼고 마스크를 적용합니다. 마스크는 카운터 wrap-around를 올바르게 처리합니다.
- (delta * mult) >> shift사이클 차분을 나노초로 변환하는 고정소수점 연산입니다. 나눗셈 없이 곱셈과 시프트만으로 고정밀 변환을 수행합니다.
- read_seqcount_retry()읽기 도중
update_wall_time()이 timekeeper를 갱신했는지 검사합니다. 시퀀스 번호가 변경되었으면 처음부터 다시 읽습니다. - ktime_add_ns(base, nsec)기준 시간에 경과 나노초를 더하여 최종 MONOTONIC 시간을 반환합니다.
ktime_get_with_offset() 통합 게터 패턴
커널의 다양한 시간 조회 함수 — ktime_get_real(), ktime_get_boottime(), ktime_get_clocktai() — 는 모두 내부적으로 ktime_get_with_offset()이라는 단일 함수를 호출합니다. 이 통합 패턴은 코드 중복을 제거하고, 각 클럭이 단조 시간(Monotonic Time)에 특정 오프셋을 더하는 방식임을 명확히 보여줍니다.
/* include/linux/timekeeper_internal.h */
enum tk_offsets {
TK_OFFS_REAL, /* wall clock = mono + offs_real */
TK_OFFS_BOOT, /* boot time = mono + offs_boot */
TK_OFFS_TAI, /* TAI time = mono + offs_tai */
TK_OFFS_MAX,
};
/* kernel/time/timekeeping.c */
ktime_t ktime_get_with_offset(enum tk_offsets offs)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned int seq;
ktime_t base, *offset = offsets[offs];
u64 nsecs;
do {
seq = read_seqcount_begin(&tk_core.seq);
base = ktime_add(tk->tkr_mono.base, *offset);
nsecs = timekeeping_get_ns(&tk->tkr_mono);
} while (read_seqcount_retry(&tk_core.seq, seq));
return ktime_add_ns(base, nsecs);
}
/* 각 클럭 조회 함수는 단순 래퍼 */
ktime_t ktime_get_real(void) { return ktime_get_with_offset(TK_OFFS_REAL); }
ktime_t ktime_get_boottime(void) { return ktime_get_with_offset(TK_OFFS_BOOT); }
ktime_t ktime_get_clocktai(void) { return ktime_get_with_offset(TK_OFFS_TAI); }
코드 설명
- offsets 배열
tk_offsets열거형 값을 인덱스로 사용하여offs_real,offs_boot,offs_tai포인터에 접근합니다. - base + nsecs 패턴
tkr_mono.base는 마지막 업데이트 시점의 단조 시간이고,timekeeping_get_ns()가 그 이후 경과한 나노초 델타를 계산합니다. 여기에 요청된 오프셋을 더하면 최종 시간이 완성됩니다. - 래퍼 함수
ktime_get_real()등은ktime_get_with_offset()에 적절한 열거형 값을 전달하는 한 줄짜리 래퍼입니다. 시간 조회 로직의 유지보수 지점이 단일화됩니다.
timekeeping_get_ns() 델타 계산 헬퍼
timekeeping_get_ns()는 마지막 타임키퍼 업데이트 이후 경과한 나노초를 계산하는 내부 헬퍼 함수입니다. 클럭소스의 현재 사이클 값을 읽고, cycle_last와의 차이를 나노초로 변환합니다.
/* kernel/time/timekeeping.c */
static inline u64 timekeeping_get_ns(
const struct tk_read_base *tkr)
{
u64 delta, nsec;
/* 클럭소스에서 현재 사이클 읽기 */
u64 now = tk_clock_read(tkr);
/* cycle_last 이후 경과 사이클 계산 (마스크 적용) */
delta = clocksource_delta(now, tkr->cycle_last, tkr->mask);
/* max_cycles 클램핑: 비정상 래핑 방지 */
if (delta > tkr->clock->max_cycles)
delta = tkr->clock->max_cycles;
/* 사이클 → 나노초 변환 (고정 소수점 연산) */
nsec = (delta * tkr->mult) >> tkr->shift;
return nsec;
}
코드 설명
- clocksource_delta()
(now - last) & mask패턴은 카운터 래핑을 자연스럽게 처리합니다. 32비트 카운터가 0xFFFFFFFF → 0x00000003으로 넘어가도 마스크 적용 후 정확히 4 사이클이 됩니다. - max_cycles 클램핑클럭소스가 불안정하거나 읽기 순서가 역전되어 비정상적으로 큰 델타가 발생할 수 있습니다.
max_cycles는 등록 시 계산된 상한값으로, 시간 역전(time warp)을 방지합니다. - 고정 소수점 변환
delta * mult >> shift는 나눗셈 없이 사이클을 나노초로 변환합니다. NTP 보정 시mult가 미세 조정됩니다.
timekeeping_advance() 업데이트 경로
타임키퍼는 틱 인터럽트마다 timekeeping_advance()를 통해 갱신됩니다. 이 함수는 축적된 클럭소스 사이클을 나노초로 변환하고, NTP 보정을 적용하며, 초 단위 오버플로우를 처리합니다. 대수적 축적 루프(logarithmic accumulation)를 사용하여 긴 유휴(long idle) 후에도 효율적으로 시간을 따라잡습니다.
/* 호출 체인: tick_sched_timer → tick_sched_do_timer → timekeeping_advance */
/* kernel/time/timekeeping.c (간소화) */
static void timekeeping_advance(enum timekeeping_adv_mode mode)
{
struct timekeeper *tk = &tk_core.timekeeper;
u64 offset;
int shift;
raw_spin_lock_irqsave(&tk_core.lock, flags);
offset = clocksource_delta(
tk_clock_read(&tk->tkr_mono),
tk->tkr_mono.cycle_last, tk->tkr_mono.mask);
/* 최소 1 tick 이상 축적되지 않으면 리턴 */
if (offset < tk->cycle_interval && mode == TK_ADV_TICK)
goto out;
/* 대수적 축적: shift부터 시작하여 큰 단위로 먼저 축적,
* shift를 줄여가며 나머지를 처리 → O(log n) 반복 */
shift = tk->ntp_error_shift;
while (offset >= tk->cycle_interval) {
offset = logarithmic_accumulation(tk, offset, shift);
if (shift) shift--;
}
/* NTP 보정 적용: mult 미세 조정 */
timekeeping_adjust(tk, offset);
/* xtime_nsec → xtime_sec 승격 (1초 이상 축적 시) */
clock_set |= accumulate_nsecs_to_secs(tk);
write_seqcount_begin(&tk_core.seq);
timekeeping_update_staged(tk, clock_set);
write_seqcount_end(&tk_core.seq);
out:
raw_spin_unlock_irqrestore(&tk_core.lock, flags);
}
/* 초 단위 오버플로우 처리 */
static inline unsigned int accumulate_nsecs_to_secs(struct timekeeper *tk)
{
u64 nsecps = (u64)NSEC_PER_SEC << tk->tkr_mono.shift;
while (tk->tkr_mono.xtime_nsec >= nsecps) {
tk->tkr_mono.xtime_nsec -= nsecps;
tk->xtime_sec++;
if (unlikely(ntp_leap_second(tk)))
clock_set = TK_CLOCK_WAS_SET; /* 윤초 처리 */
}
return clock_set;
}
코드 설명
- 대수적 축적 루프시스템이 오래 유휴 상태였다가 깨어나면 수백 ms의 틱이 밀려 있을 수 있습니다. 1ms씩 반복하는 대신,
shift를 이용해 큰 배수(8ms→4ms→2ms→1ms 순)로 축적하여 O(log n) 반복으로 처리합니다. - timekeeping_adjust()NTP 데몬이 계산한 주파수 보정값을
mult에 반영합니다.ntp_tick_length()가 반환하는 목표 틱 길이와 현재 값의 차이를 기반으로mult를 미세 조정합니다. - accumulate_nsecs_to_secs()
xtime_nsec이 1초를 초과하면xtime_sec를 1 증가시키고 나노초 부분에서 차감합니다. 이 시점에서 윤초 삽입/삭제도 처리됩니다.
부팅 시 Timekeeping 초기화 순서
리눅스 커널의 타임키핑은 부팅 과정에서 단계적으로 초기화됩니다. 초기에는 정밀도가 낮은 jiffies 클럭소스로 시작하여, 하드웨어 클럭소스가 보정(calibration)을 마치면 순차적으로 승격됩니다.
/* kernel/time/timekeeping.c */
void __init timekeeping_init(void)
{
struct timespec64 wall;
struct clocksource *clock;
read_persistent_clock64(&wall); /* RTC 읽기 */
clock = clocksource_default_clock(); /* jiffies */
tk_setup_internals(tk, clock);
tk->xtime_sec = wall.tv_sec;
tk->wall_to_monotonic = timespec64_sub(
(struct timespec64){ 0 }, wall);
tk->offs_real = ktime_set(wall.tv_sec, wall.tv_nsec);
}
/* kernel/time/clocksource.c — 부팅 완료 후 최종 선택 */
static int __init clocksource_done_booting(void)
{
finished_booting = 1;
clocksource_select(); /* rating 기준 최고 소스 선택 */
return 0;
}
fs_initcall(clocksource_done_booting);
코드 설명
- timekeeping_init()
start_kernel()초반에 호출됩니다. jiffies 클럭소스(rating=1)로 시작하고, RTC에서 벽시계 시간을 읽어xtime_sec에 설정합니다. - wall_to_monotonicCLOCK_MONOTONIC 기준점을 결정하는 오프셋으로, 벽시계 시간의 음수값으로 초기화됩니다.
settimeofday()호출 시에도 단조 시간이 영향받지 않도록 함께 조정됩니다. - late_time_init()x86에서
tsc_init()을 호출하여 TSC 주파수를 PIT 또는 HPET 기준으로 보정합니다. 성공하면 TSC가 rating=300으로 등록됩니다. - clocksource_done_booting()
fs_initcall단계에서clocksource_select()를 호출하여 최고 rating 클럭소스를 활성화합니다.
do_settimeofday64() / do_adjtimex() 내부 경로
clock_settime()이나 adjtimex() 시스템 호출로 시간을 변경하면, 커널 내부에서는 do_settimeofday64() 또는 do_adjtimex()가 호출됩니다. 전자는 벽시계 시간을 직접 설정하고, 후자는 NTP 데몬이 주파수 보정과 점진적 시간 조정을 수행할 때 사용됩니다.
/* kernel/time/timekeeping.c */
int do_settimeofday64(const struct timespec64 *ts)
{
struct timekeeper *tk = &tk_core.timekeeper;
struct timespec64 ts_delta, xt;
if (!timespec64_valid_settod(ts))
return -EINVAL;
raw_spin_lock_irqsave(&tk_core.lock, flags);
timekeeping_forward_now(tk); /* 최신 상태로 갱신 */
xt = tk_xtime(tk);
ts_delta = timespec64_sub(*ts, xt);
/* wall_to_monotonic 조정: MONOTONIC은 불변 유지 */
tk->wall_to_monotonic = timespec64_sub(
tk->wall_to_monotonic, ts_delta);
tk->xtime_sec = ts->tv_sec;
tk->tkr_mono.xtime_nsec = (u64)ts->tv_nsec << tk->tkr_mono.shift;
timekeeping_update_otherc_offsets(tk, TK_CLOCK_WAS_SET);
write_seqcount_begin(&tk_core.seq);
timekeeping_update_staged(tk, TK_CLOCK_WAS_SET);
write_seqcount_end(&tk_core.seq);
raw_spin_unlock_irqrestore(&tk_core.lock, flags);
clock_was_set(TK_CLOCK_WAS_SET); /* REALTIME 타이머 재평가 */
return 0;
}
/* kernel/time/timekeeping.c */
int do_adjtimex(struct __kernel_timex *txc)
{
struct timekeeper *tk = &tk_core.timekeeper;
raw_spin_lock_irqsave(&tk_core.lock, flags);
timekeeping_forward_now(tk);
/* NTP 코어로 위임: ntp.c의 __do_adjtimex() */
ret = __do_adjtimex(txc, &ts, &tai);
if (tai != tk->tai_offset) {
tk->tai_offset = tai;
timekeeping_update_otherc_offsets(tk, TK_CLOCK_WAS_SET);
}
write_seqcount_begin(&tk_core.seq);
timekeeping_update_staged(tk, clock_set);
write_seqcount_end(&tk_core.seq);
raw_spin_unlock_irqrestore(&tk_core.lock, flags);
if (clock_set) clock_was_set(clock_set);
return ret;
}
코드 설명
- do_settimeofday64()벽시계 시간을 직접 설정합니다. 핵심은
wall_to_monotonic을 역보정하여CLOCK_MONOTONIC이 영향받지 않도록 하는 것입니다. - TK_CLOCK_WAS_SET벽시계 시간이 불연속적으로 변경되었음을 알립니다.
clock_was_set()은 CLOCK_REALTIME 기반 타이머들을 재평가합니다. - do_adjtimex()NTP 데몬이 사용하는 함수입니다. 실제 보정 계산은
ntp.c의__do_adjtimex()에 위임되며, 주파수/위상 보정과 TAI 오프셋 변경을 처리합니다.
사이클 → 나노초 변환 수학
/*
* 하드웨어 카운터 사이클을 나노초로 변환:
*
* ns = cycles × (10^9 / freq)
*
* 정수 연산으로 구현:
* ns = (cycles × mult) >> shift
*
* mult와 shift 계산:
* mult = (10^9 << shift) / freq
*
* 예: TSC 3.0 GHz, shift=24
* mult = (10^9 × 2^24) / (3 × 10^9)
* = 16777216 / 3 = 5592405
*
* 100 cycles → (100 × 5592405) >> 24
* = 559240500 >> 24 = 33 ns ≈ 33.33ns (정확)
*
* NTP 보정 시 mult 값을 미세 조정하여 주파수 보정
*/
/* clocksource 등록 시 mult/shift 자동 계산 */
clocks_calc_mult_shift(
&cs->mult, /* 출력: 곱셈 인수 */
&cs->shift, /* 출력: 시프트 인수 */
cs->freq, /* 입력: 클럭 주파수 (Hz) */
NSEC_PER_SEC, /* 10^9 */
cs->max_idle_ns /* 최대 유휴 시간 */
);
Suspend/Resume 시간 복원
시스템이 suspend(S3/S2idle)에 진입하면 TSC·HPET 등 대부분의 하드웨어 카운터가 정지합니다. Resume 시 커널은 RTC를 읽어 suspend 동안 경과한 시간(sleep_length)을 계산하고, total_sleep_time과 각 시계 오프셋을 복원합니다. 이 과정이 실패하면 CLOCK_BOOTTIME과 CLOCK_REALTIME이 어긋나는 심각한 버그가 발생합니다.
/* kernel/time/timekeeping.c — timekeeping_resume() 핵심 로직 (간략화) */
static void timekeeping_resume(void)
{
struct timekeeper *tk = &tk_core.timekeeper;
struct timespec64 ts_new, ts_delta;
/* 1. RTC에서 현재 시각 읽기 */
read_persistent_clock64(&ts_new);
/* 2. suspend 동안 경과한 시간 계산 */
ts_delta = timespec64_sub(ts_new, timekeeping_suspend_time);
/* 3. sleep 시간이 유효한지 검증 (음수 또는 너무 큰 값 거부) */
if (ts_delta.tv_sec < 0 || ts_delta.tv_sec > 365 * 24 * 3600)
ts_delta = (struct timespec64){ 0, 0 };
/* 4. 시계 오프셋 업데이트 */
__timekeeping_inject_sleeptime(tk, &ts_delta);
/* ├── total_sleep_time += ts_delta (BOOTTIME용)
* ├── offs_real += ts_delta (REALTIME용)
* ├── offs_boot += ts_delta (BOOTTIME용)
* └── MONOTONIC은 변경 없음 (정지 상태 유지) */
/* 5. clocksource의 cycle_last 재설정 */
tk->tkr_mono.cycle_last = tk->tkr_mono.clock->read(tk->tkr_mono.clock);
tk->tkr_raw.cycle_last = tk->tkr_raw.clock->read(tk->tkr_raw.clock);
/* 6. vdso_data 업데이트 */
update_vsyscall(tk);
/* 7. clockevent 재활성화 */
tick_resume();
}
코드 설명
kernel/time/timekeeping.c의 timekeeping_resume()은 시스템이 suspend에서 깨어날 때 호출되어 모든 시계 오프셋을 복원합니다.
- read_persistent_clock64()RTC(배터리 구동 실시간 클럭)에서 현재 벽시계 시각을 읽습니다. RTC는 suspend 중에도 동작하므로 경과 시간 계산의 기준이 됩니다.
- timespec64_sub(ts_new, suspend_time)resume 시 RTC 시각에서 suspend 진입 시 기록한 RTC 시각을 빼서 sleep 경과 시간(
ts_delta)을 계산합니다. - ts_delta 유효성 검증sleep 시간이 음수이거나 1년을 초과하면 RTC 오류로 판단하고 0으로 처리합니다. RTC 배터리 방전 시 이 경로를 탑니다.
- __timekeeping_inject_sleeptime()
total_sleep_time,offs_real,offs_boot에 sleep 경과 시간을 더합니다. CLOCK_BOOTTIME과 CLOCK_REALTIME이 이 업데이트를 반영하고, CLOCK_MONOTONIC은 변경하지 않습니다. - cycle_last 재설정clocksource의
cycle_last를 현재 카운터 값으로 갱신합니다. TSC가 resume 후 리셋되었을 수 있으므로 이전 값과의 차분이 엉뚱한 결과를 내지 않도록 합니다. - update_vsyscall()vDSO 공유 페이지의 시간 데이터를 갱신하여 사용자 공간에서도 올바른 시간을 읽을 수 있게 합니다.
- tick_resume()clockevent 장치를 재활성화하여 틱 인터럽트가 다시 발생하도록 합니다. 이후 정상적인
update_wall_time()사이클이 재개됩니다.
- RTC 배터리 방전 — RTC가 정확한 시간을 유지하지 못하면
sleep_length가 잘못 계산됩니다.hwclock --show로 확인 - RTC 드라이버 미등록 —
read_persistent_clock64()가 실패하면 sleep 시간을 0으로 처리합니다.ls /dev/rtc*로 확인 - TSC reset — 일부 플랫폼에서 resume 시 TSC가 0으로 리셋될 수 있습니다.
cycle_last재설정이 이를 처리하지만, 구형 커널에서는 시간 역행이 발생할 수 있습니다 - BOOTTIME vs MONOTONIC 혼동 — suspend 후 타이머 폭발 증상은 대부분
CLOCK_MONOTONIC기반 타이머를CLOCK_BOOTTIME으로 변경하면 해결됩니다
Clocksource 프레임워크
clocksource 프레임워크는 하드웨어 타이머를 통일된 인터페이스로 추상화합니다. 시스템에 여러 clocksource가 등록되면 rating이 가장 높은 것이 자동 선택됩니다.
clocksource 구조체
/* include/linux/clocksource.h */
struct clocksource {
u64 (*read)(struct clocksource *cs); /* 카운터 읽기 함수 */
u64 mask; /* 비트 마스크 (예: 0xFFFFFFFF) */
u32 mult; /* 사이클→ns 곱셈 인수 */
u32 shift; /* 사이클→ns 시프트 인수 */
u64 max_idle_ns; /* 최대 유휴 시간 (wrap 방지) */
u32 maxadj; /* NTP 최대 조정 범위 */
int rating; /* 품질 등급 (높을수록 우선) */
const char *name; /* 이름 ("tsc", "hpet", ...) */
unsigned long flags; /* CLOCK_SOURCE_* 플래그 */
int (*enable)(struct clocksource *cs);
void (*disable)(struct clocksource *cs);
void (*suspend)(struct clocksource *cs);
void (*resume)(struct clocksource *cs);
void (*mark_unstable)(struct clocksource *cs);
void (*tick_stable)(struct clocksource *cs);
struct list_head list; /* 등록된 clocksource 리스트 */
...
};
/* rating 기준:
* 1-99: 비적합 (테스트/폴백)
* 100-199: 기본 (jiffies)
* 200-299: 합리적 (ACPI PM)
* 300-399: 양호 (HPET)
* 400-499: 우수 (TSC)
*/
코드 설명
include/linux/clocksource.h에 정의된 struct clocksource는 하드웨어 카운터를 커널 timekeeping 프레임워크에 연결하는 추상화 계층입니다.
- read()하드웨어 카운터의 현재 사이클 값을 반환하는 콜백입니다. TSC는
rdtsc, HPET는 MMIO 읽기를 사용합니다. - mask카운터의 유효 비트 마스크입니다. 예를 들어 32-bit 카운터는
0xFFFFFFFF이며, 사이클 차분 계산 시 wrap-around를 처리합니다. - mult / shift사이클을 나노초로 변환하는 고정소수점 인수입니다.
ns = (cycles * mult) >> shift공식으로 나눗셈 없이 변환합니다. - max_idle_ns카운터가 wrap-around하기 전 허용되는 최대 유휴 시간(나노초)입니다. tickless 커널에서 틱 간격 결정에 사용됩니다.
- rating품질 등급으로, 높을수록 우선 선택됩니다.
clocksource_select()가 리스트에서 가장 높은 rating의 소스를 활성화합니다. - flags
CLOCK_SOURCE_IS_CONTINUOUS(연속 카운터),CLOCK_SOURCE_MUST_VERIFY(watchdog 검증 필요) 등 동작 속성을 지정합니다. - suspend / resume시스템 suspend/resume 시 호출되는 콜백으로, 카운터 상태 저장/복원을 처리합니다.
커스텀 Clocksource 등록
#include <linux/clocksource.h>
#include <linux/io.h>
static void __iomem *timer_base;
static u64 my_clocksource_read(struct clocksource *cs)
{
return (u64)readl(timer_base + MY_TIMER_CNT);
}
static struct clocksource my_clksrc = {
.name = "my-timer",
.rating = 250,
.read = my_clocksource_read,
.mask = CLOCKSOURCE_MASK(32),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
static int __init my_timer_init(void)
{
int ret;
timer_base = ioremap(MY_TIMER_BASE, MY_TIMER_SIZE);
if (!timer_base)
return -ENOMEM;
/* 타이머 하드웨어 초기화: free-running 모드 설정 */
writel(TIMER_ENABLE | TIMER_FREERUN, timer_base + MY_TIMER_CTRL);
/* mult/shift 자동 계산 후 등록 */
ret = clocksource_register_hz(&my_clksrc, 24000000); /* 24MHz */
if (ret) {
iounmap(timer_base);
return ret;
}
pr_info("my-timer: registered clocksource @ 24MHz\\n");
return 0;
}
/*
* clocksource_register_hz() vs clocksource_register_khz():
* - _hz: 정확한 주파수 지정 (오차 최소)
* - _khz: kHz 단위 (큰 주파수에서 오버플로 방지)
*
* 내부적으로 __clocksource_register_scale()이
* clocks_calc_mult_shift()를 호출하여 mult/shift를 계산합니다.
*/
코드 설명
kernel/time/clocksource.c의 등록 API를 사용하여 커스텀 하드웨어 타이머를 clocksource로 등록하는 전형적인 패턴입니다.
- my_clocksource_read()MMIO로 하드웨어 타이머 카운터 레지스터를 읽는 콜백입니다.
readl()은 32-bit MMIO 읽기를 수행합니다. - .rating = 250ACPI PM(200)보다 높고 HPET(300)보다 낮은 등급입니다. 시스템에 더 높은 rating의 clocksource가 없으면 이 소스가 선택됩니다.
- CLOCKSOURCE_MASK(32)32-bit 카운터의 비트 마스크(
0xFFFFFFFF)를 생성합니다. 사이클 차분 계산 시 wrap-around를 올바르게 처리합니다. - CLOCK_SOURCE_IS_CONTINUOUS카운터가 free-running 모드로 연속 증가함을 표시합니다. 이 플래그가 없으면 timekeeping에 사용할 수 없습니다.
- ioremap()물리 주소의 타이머 레지스터를 커널 가상 주소 공간에 매핑합니다. 드라이버 제거 시
iounmap()으로 해제해야 합니다. - clocksource_register_hz()주파수(Hz)를 받아 내부적으로
clocks_calc_mult_shift()를 호출하여 최적의mult/shift값을 자동 계산한 후 등록합니다.
Clocksource 선택/변경
# 현재 clocksource 확인
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
# 사용 가능한 clocksource 목록
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
# clocksource 수동 변경 (디버깅/테스트용)
$ echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource
# 커널 커맨드라인으로 강제 지정
# clocksource=hpet (HPET 강제)
# tsc=reliable (TSC 안정성 신뢰)
# tsc=unstable (TSC 불안정 표시 → 폴백)
Clocksource Watchdog
/*
* clocksource watchdog는 0.5초마다 활성 clocksource를
* 참조 clocksource와 비교하여 드리프트를 감지합니다.
*
* 드리프트 > 62.5 ppm → clocksource를 unstable로 표시 → 폴백
*
* 주로 TSC의 안정성 검증에 사용됩니다.
* 불안정한 TSC (C-state 변경, 주파수 스케일링 등)가 감지되면
* HPET 또는 ACPI PM으로 자동 전환됩니다.
*/
# watchdog 로그
$ dmesg | grep -i clocksource
clocksource: Switched to clocksource tsc
# 또는 불안정 시:
clocksource: timekeeping watchdog on CPU0: Marking clocksource 'tsc'
as unstable because the skew is too large
clocksource: Switched to clocksource hpet
clocksource_select() 내부 동작
/* kernel/time/clocksource.c — clocksource_select() 간략화 */
static void clocksource_select(void)
{
struct clocksource *best, *cs;
/* override가 지정된 경우 이름 매칭으로 찾기 */
if (clocksource_override) {
list_for_each_entry(cs, &clocksource_list, list) {
if (!strcmp(cs->name, clocksource_override->name)) {
best = cs;
goto found;
}
}
}
/* rating이 가장 높은 clocksource 찾기
* 리스트는 rating 내림차순으로 정렬되어 있음 */
best = list_entry(clocksource_list.next,
struct clocksource, list);
found:
if (curr_clocksource != best) {
pr_info("Switched to clocksource %s\n", best->name);
timekeeping_notify(best);
/* timekeeper가 새 clocksource로 전환
* mult/shift/mask 등 재설정 */
}
}
/* sysfs를 통한 clocksource 변경 시에도
* clocksource_override를 설정한 뒤
* clocksource_select()가 호출됩니다.
*
* /sys/devices/system/clocksource/clocksource0/current_clocksource
* 에 쓰면 __clocksource_select() → timekeeping_notify() 경로 */
Clocksource 내부 함수 상세
/* ── clocksource_mmio_init(): 임베디드/ARM MMIO 타이머 등록 헬퍼 ──
* kernel/time/clocksource_mmio.c
*
* MMIO 기반 하드웨어 타이머를 간편하게 clocksource로 등록합니다.
* ioremap + clocksource_register_hz()를 한 번에 처리합니다. */
/* 사전 정의된 MMIO 읽기 콜백들 */
static u64 clocksource_mmio_readl_up(struct clocksource *cs)
{
return (u64)readl_relaxed(to_mmio_clksrc(cs)->reg);
}
static u64 clocksource_mmio_readl_down(struct clocksource *cs)
{
return ~(u64)readl_relaxed(to_mmio_clksrc(cs)->reg) & cs->mask;
}
/* MMIO clocksource 등록 헬퍼 */
int clocksource_mmio_init(void __iomem *base, const char *name,
unsigned long hz, int rating, unsigned bits,
u64 (*read)(struct clocksource *))
{
struct clocksource_mmio *cs;
cs = kzalloc(sizeof(*cs), GFP_KERNEL);
if (!cs)
return -ENOMEM;
cs->reg = base;
cs->clksrc.name = name;
cs->clksrc.rating = rating;
cs->clksrc.read = read;
cs->clksrc.mask = CLOCKSOURCE_MASK(bits);
cs->clksrc.flags = CLOCK_SOURCE_IS_CONTINUOUS;
return clocksource_register_hz(&cs->clksrc, hz);
}
/* DT 기반 ARM 타이머에서의 사용 예시 */
static int __init my_timer_init(struct device_node *np)
{
void __iomem *base = of_iomap(np, 0);
return clocksource_mmio_init(base + TIMER_COUNT_REG,
"my-timer", 24000000, /* 24MHz */
200, /* rating */
32, /* bits */
clocksource_mmio_readl_up);
}
TIMER_OF_DECLARE(my_timer, "vendor,my-timer", my_timer_init);
/* ── clocksource_unregister(): clocksource 등록 해제 ──
* kernel/time/clocksource.c
*
* 모듈형 clocksource 드라이버에서만 극히 드물게 사용됩니다.
* 대부분의 clocksource는 커널 수명 동안 유지됩니다. */
int clocksource_unregister(struct clocksource *cs)
{
mutex_lock(&clocksource_mutex);
if (!list_empty(&cs->list))
list_del_init(&cs->list);
if (cs == curr_clocksource)
clocksource_select(); /* 대체 소스로 재선택 */
clocksource_dequeue_watchdog(cs);
mutex_unlock(&clocksource_mutex);
return 0;
}
/* ── clocksource_mark_unstable(): 수동 불안정 표시 ──
* TSC가 CPU 핫플러그 시나리오에서 동기화를 잃을 때 사용됩니다.
* arch/x86/kernel/tsc.c에서 호출하는 대표적인 API입니다. */
void clocksource_mark_unstable(struct clocksource *cs)
{
unsigned long flags;
raw_spin_lock_irqsave(&watchdog_lock, flags);
if (!(cs->flags & CLOCK_SOURCE_UNSTABLE)) {
cs->flags |= CLOCK_SOURCE_UNSTABLE;
list_del_init(&cs->wd_list);
__clocksource_unstable(cs); /* schedule_work()로 비동기 폴백 */
}
raw_spin_unlock_irqrestore(&watchdog_lock, flags);
}
/* ── CLOCKSOURCE_MASK() 매크로 ──
* include/linux/clocksource.h
*
* 카운터 비트 수에 맞는 마스크를 생성합니다.
* 사이클 차분 계산 시 wrap-around를 올바르게 처리합니다. */
#define CLOCKSOURCE_MASK(bits) \
(u64)((bits) < 64 ? ((1ULL << (bits)) - 1) : ~0ULL)
/* 대표적인 값들:
* CLOCKSOURCE_MASK(32) = 0x00000000FFFFFFFF (32-bit MMIO 타이머)
* CLOCKSOURCE_MASK(56) = 0x00FFFFFFFFFFFFFF (HPET)
* CLOCKSOURCE_MASK(64) = 0xFFFFFFFFFFFFFFFF (TSC) */
코드 설명
Clocksource 서브시스템의 핵심 내부 함수와 매크로를 분석합니다. 대부분 kernel/time/clocksource.c와 include/linux/clocksource.h에 정의되어 있습니다.
- clocksource_mmio_readl_up()카운터 값이 0에서 시작하여 증가하는 up-counting 타이머용 읽기 콜백입니다.
readl_relaxed()로 MMIO 레지스터를 읽어u64로 확장하여 반환합니다. - clocksource_mmio_readl_down()최대값에서 감소하는 down-counting 타이머용입니다. 비트 반전(
~)과 마스크 AND 연산으로 up-counting 형태로 변환합니다. - clocksource_mmio_init()MMIO 기반 clocksource를 한 번의 호출로 등록합니다. 내부에서
kzalloc()으로clocksource_mmio구조체를 할당하고, 콜백·마스크·rating을 설정한 뒤clocksource_register_hz()를 호출합니다. - TIMER_OF_DECLARE()Device Tree의
compatible문자열과 초기화 함수를 연결하는 매크로입니다. 커널 부팅 시 DT에서 매칭되는 노드를 찾아 자동으로 초기화 함수를 호출합니다. - clocksource_unregister()리스트에서 제거 후, 현재 활성 소스였다면
clocksource_select()를 호출하여 차선 소스로 전환합니다. 모듈 제거 시에만 사용되며 실제로 호출되는 경우는 극히 드뭅니다. - clocksource_mark_unstable()watchdog 감시 외에 아키텍처 코드가 직접 불안정 표시를 할 때 사용합니다. x86 TSC에서 CPU 핫플러그 시 코어 간 동기화 보장이 깨지면 이 함수를 호출하여 HPET 등으로 폴백합니다.
- __clocksource_unstable()
schedule_work()를 통해 비동기로clocksource_select()를 호출합니다. IRQ 컨텍스트에서 안전하게 폴백을 수행하기 위한 구조입니다. - CLOCKSOURCE_MASK(bits)
bits < 64이면(1ULL << bits) - 1로 하위 비트만 마스킹하고, 64이면~0ULL(전체 1)을 반환합니다. 카운터 overflow 시 차분 계산이 자연스럽게 wrap-around 됩니다.
clocks_calc_mult_shift() 상세 분석
/* kernel/time/clocksource.c
* clocks_calc_mult_shift() — 주파수를 mult/shift 쌍으로 변환
*
* 알고리즘 핵심:
* nanoseconds = (cycles × mult) >> shift
* mult = (NSEC_PER_SEC << shift) / freq_hz
*
* overflow 없이 가능한 가장 큰 shift를 찾아 정밀도를 극대화합니다. */
void clocks_calc_mult_shift(u32 *mult, u32 *shift,
u32 from, u32 to, u32 maxsec)
{
u64 tmp;
u32 sft, sftacc = 32;
/* from=freq_hz, to=NSEC_PER_SEC(10^9) */
/* maxsec 동안의 최대 사이클 수가
* tmp × mult에서 64-bit overflow를 일으키지 않는
* 최대 shift를 찾습니다 */
tmp = ((u64)maxsec * from) >> 32;
while (tmp) {
tmp >>= 1;
sftacc--;
}
/* shift = 32부터 시작하여 sftacc까지 내려가며
* overflow가 없는 최대 shift를 선택 */
for (sft = 32; sft > 0; sft--) {
tmp = (u64)to << sft;
tmp += from / 2; /* 반올림 */
do_div(tmp, from);
if ((tmp >> sftacc) == 0)
break;
}
*mult = tmp;
*shift = sft;
}
/* ── 실제 계산 예시 ──
*
* 【예시 1】 ARM 타이머 24MHz, maxsec=600
* sftacc: (600 × 24000000) >> 32 = 3 → 비트 수 2 → sftacc = 30
* sft=30: mult = (10^9 << 30) / 24000000 = 44739242
* 검증: 1초 = (24000000 × 44739242) >> 30 ≈ 999999999ns ✓
*
* 【예시 2】 HPET 14.318MHz, maxsec=600
* sftacc: (600 × 14318180) >> 32 = 2 → sftacc = 30
* sft=30: mult = (10^9 << 30) / 14318180 = 75013970
* 검증: 1초 = (14318180 × 75013970) >> 30 ≈ 999999995ns ✓
*
* 【예시 3】 TSC 3.0GHz, maxsec=600
* sftacc: (600 × 3000000000) >> 32 = 418 → 비트 수 9 → sftacc = 23
* sft=23: mult = (10^9 << 23) / 3000000000 = 2796202
* 검증: 1초 = (3000000000 × 2796202) >> 23 ≈ 999999761ns ✓
*
* shift가 작을수록 정밀도가 낮아집니다 (TSC는 고주파 → 낮은 shift).
* shift가 클수록 나노초 변환의 소수점 이하 정밀도가 높아집니다. */
/* clocksource_max_deferment(): mult/shift/mask로부터
* 최대 허용 유휴 시간(max_idle_ns)을 계산합니다.
* NO_HZ 모드에서 틱 없이 잠들 수 있는 최대 시간을 결정합니다. */
static u64 clocksource_max_deferment(struct clocksource *cs)
{
u64 max_nsecs;
/* mask 범위 내에서 mult 곱셈이 overflow하지 않는
* 최대 사이클 수를 역산합니다 */
max_nsecs = clocks_calc_max_nsecs(cs->mult, cs->shift, 0,
cs->mask, NULL);
/* 안전 마진 50% 적용 */
return max_nsecs - (max_nsecs >> 5); /* ×(31/32) ≈ 96.875% */
}
코드 설명
clocks_calc_mult_shift()는 하드웨어 주파수를 나노초 변환 계수(mult/shift)로 바꾸는 핵심 알고리즘입니다. 모든 clocksource와 clock_event_device 등록 시 호출됩니다.
- sftacc 계산
maxsec × freq를 32-bit 오른쪽 시프트하여 상위 비트 수를 구합니다. 이 값이 클수록 사용 가능한 shift 범위가 줄어듭니다.sftacc는mult가 사용할 수 있는 최대 비트 폭입니다. - shift 탐색 루프32부터 내려가며
(NSEC_PER_SEC << sft) / freq가sftacc비트 이내인 최대sft를 찾습니다.from / 2를 더하여 반올림 오차를 최소화합니다. - 24MHz 예시ARM SoC 범용 타이머에서 흔한 주파수입니다. shift=30으로 충분한 정밀도를 확보하며, 1초 변환 오차가 1ns 미만입니다.
- 3.0GHz TSC 예시고주파 TSC는
maxsec × freq가 크므로 shift=23까지 낮아집니다. 변환 오차가 ~239ns/초로 저주파 대비 정밀도가 다소 떨어지지만, TSC 자체의 sub-ns 해상도가 이를 보상합니다. - clocksource_max_deferment()
mask범위와mult곱셈의 64-bit 한계로부터 역산하여 틱 없이 유지할 수 있는 최대 나노초를 계산합니다. NO_HZ_FULL 모드에서 타이머 인터럽트 간격의 상한을 결정합니다. - 안전 마진 96.875%이론적 최대값의 약 97%만 사용합니다. wrap-around 직전의 불안정 구간을 피하기 위한 것으로,
>> 5는/ 32와 동일합니다.
clocksource_delta() 함수
/* include/linux/clocksource.h
* clocksource_delta() — 두 사이클 값의 차분을 안전하게 계산
*
* 기본 수식: (now - last) & mask
* mask를 적용하여 카운터 wrap-around를 자동 처리합니다. */
static inline u64 clocksource_delta(
u64 now, u64 last, u64 mask)
{
u64 ret = (now - last) & mask;
#ifdef CONFIG_CLOCKSOURCE_VALIDATE_LAST_CYCLE
/* 음수 델타 방지: now < last인 경우 0 반환
* suspend/resume 전환 중 clocksource 읽기가
* 이전 값보다 작은 값을 반환할 수 있습니다 */
if (ret & ~(mask >> 1))
return 0;
#endif
/* max_cycles 클램핑: 비정상적으로 큰 델타 차단
* MMIO 읽기 실패(0 반환)나 suspend 중 긴 간격으로
* 인해 발생할 수 있는 시간 점프를 방지합니다 */
if (ret > cs->max_cycles)
return cs->max_cycles;
return ret;
}
/* 사용 위치: timekeeping_get_ns()
* kernel/time/timekeeping.c */
static u64 timekeeping_get_ns(struct tk_read_base *tkr)
{
u64 delta, cycle_now;
cycle_now = tkr->read(tkr->clock);
delta = clocksource_delta(cycle_now, tkr->cycle_last,
tkr->mask);
/* delta × mult >> shift → 나노초 변환 */
return (delta * tkr->mult + tkr->xtime_nsec) >> tkr->shift;
}
/* ── 음수 델타가 발생하는 시나리오 ──
*
* 1. Suspend/Resume 전환: clocksource가 정지했다가 재시작 시
* 카운터가 리셋되어 now < last가 될 수 있습니다.
*
* 2. MMIO 읽기 실패: 하드웨어 오류로 readl()이 0을 반환하면
* now=0, last=이전값 → 음수 델타가 됩니다.
*
* 3. CPU 마이그레이션(TSC): invariant TSC가 아닌 경우
* 코어 간 TSC 값 불일치로 역전이 발생합니다.
*
* max_cycles는 clocksource_max_deferment()에서 계산한
* max_idle_ns에 대응하는 사이클 수입니다.
* 이 값을 초과하는 델타는 clocksource 오류로 간주합니다. */
코드 설명
clocksource_delta()는 timekeeping_get_ns()에서 매번 호출되는 인라인(Inline) 함수로, 시간 계산의 기초 연산을 담당합니다.
- (now - last) & mask부호 없는 정수 감산 후 마스크를 적용합니다. 32-bit 카운터가
0xFFFFFFFF에서0x00000000으로 넘어가도 차분이 올바르게 1이 됩니다. - CONFIG_CLOCKSOURCE_VALIDATE_LAST_CYCLE커널 v4.x에서 도입된 설정입니다.
ret & ~(mask >> 1)은 마스크 범위의 상위 절반에 해당하는 델타를 음수로 판정합니다. 32-bit 카운터에서0x80000000이상의 차분은 역전으로 간주합니다. - max_cycles 클램핑
max_cycles는clocksource_max_deferment()에서 역산한 값으로, 이를 초과하는 델타는 시간 변환 시 overflow를 유발할 수 있어 상한으로 잘라냅니다. - timekeeping_get_ns()
ktime_get()계열 함수의 최종 단계로, 델타 사이클을 나노초로 변환합니다.xtime_nsec는 이전 틱에서 누적된 잔여 나노초(고정소수점)입니다. - MMIO 읽기 실패PCIe 타이머(HPET 등)에서 링크 에러 시
readl()이0xFFFFFFFF또는0x00000000을 반환할 수 있습니다. 이 경우 음수 델타 보호와 max_cycles 클램핑이 시간 역전을 방지합니다.
하드웨어 클럭 상세
TSC (Time Stamp Counter) — x86
/*
* TSC는 x86 프로세서의 64-bit 카운터로,
* 프로세서 클럭 사이클(또는 고정 주파수)마다 증가합니다.
*
* TSC 변형:
* - Variant TSC: 주파수 스케일링에 따라 속도 변동 (구형 CPU)
* - Constant TSC: APIC 버스 주파수로 고정 (Core 2+)
* - Invariant TSC: C-state/주파수와 무관, 항상 일정 (Nehalem+)
* - Nonstop TSC: 깊은 C-state에서도 정지하지 않음
*
* 현대 x86에서 TSC는 가장 빠르고 정확한 clocksource입니다.
*/
/* arch/x86/kernel/tsc.c — TSC clocksource */
static u64 read_tsc(struct clocksource *cs)
{
return (u64)rdtsc_ordered();
/* rdtsc_ordered = lfence + rdtsc 또는 rdtscp
* lfence: 이전 명령어 완료 대기 (순서 보장)
* rdtscp: 읽기 직렬화 + IA32_TSC_AUX(코어 ID) 반환 */
}
static struct clocksource clocksource_tsc = {
.name = "tsc",
.rating = 300, /* invariant TSC는 350으로 승격 */
.read = read_tsc,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS |
CLOCK_SOURCE_MUST_VERIFY,
};
# TSC 상태 확인
$ dmesg | grep -i tsc
tsc: Detected 3000.000 MHz processor
tsc: Detected 3000.000 MHz TSC
tsc: Refined TSC clocksource calibration: 2999.998 MHz
clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x2b3e459bf4c
max_idle_ns: 440795310624 ns
# TSC CPUID 피처 확인
$ grep -o 'constant_tsc\|nonstop_tsc\|tsc_known_freq\|rdtscp' /proc/cpuinfo | sort -u
constant_tsc
nonstop_tsc
rdtscp
tsc_known_freq
TSC 캘리브레이션 방법
커널 부팅 시 TSC의 정확한 주파수를 결정하는 과정이 캘리브레이션입니다. 여러 방법이 순차적으로 시도됩니다.
| 방법 | 사용 조건 | 정밀도 | 소요 시간 |
|---|---|---|---|
| CPUID Leaf 0x15 | Intel 6세대+ (Skylake+) | 정확 (HW 보고) | 즉시 |
| MSR_PLATFORM_INFO | Intel Nehalem+ / AMD | 매우 높음 | 즉시 |
| HPET 참조 캘리브레이션 | HPET 사용 가능 시 | 높음 | ~50ms |
| PIT 참조 캘리브레이션 | HPET 없을 때 | 보통 | ~50ms |
| PM Timer 참조 | ACPI PM Timer 사용 가능 | 보통 | ~50ms |
| Refined 캘리브레이션 | 부팅 후 late_initcall | 매우 높음 | ~100ms |
/* arch/x86/kernel/tsc.c — TSC 캘리브레이션 과정 (간략화) */
/* 1. CPUID Leaf 0x15: Crystal Clock 기반 (가장 정확) */
static unsigned long cpu_khz_from_cpuid(void)
{
unsigned int eax_denominator, ebx_numerator, ecx_hz;
cpuid(0x15, &eax_denominator, &ebx_numerator, &ecx_hz, ...);
/* TSC freq = crystal_freq * (ebx/eax)
* Skylake: crystal = 24MHz, ratio 따라 결정 */
return ecx_hz * ebx_numerator / eax_denominator / 1000;
}
/* 2. PIT 참조: 50ms 동안 TSC 증가량 측정 */
static unsigned long pit_calibrate_tsc(void)
{
u64 tsc_start = rdtsc();
/* PIT 채널 2를 50ms 대기에 사용 */
mfence();
/* ... PIT 카운트다운 대기 ... */
u64 tsc_end = rdtsc();
return (tsc_end - tsc_start) / 50; /* kHz */
}
/* 3. Refined 캘리브레이션 (late_initcall):
* 부팅 후 충분한 시간이 지나면 더 긴 참조 구간으로
* TSC 주파수를 재교정하여 정밀도를 높입니다. */
# dmesg 출력 예시:
# tsc: Detected 3000.000 MHz processor
# tsc: Refined TSC clocksource calibration: 2999.998 MHz
constant_tsc: P-state 변경과 무관한 일정 주파수 (Core 2 이후)nonstop_tsc: 깊은 C-state에서도 카운터 정지 안 함 (Nehalem 이후)tsc_known_freq: CPUID로 정확한 주파수 보고 (Skylake 이후)- 세 가지 모두 있으면 invariant TSC → rating 350으로 승격, watchdog 면제 가능
TSC 레지스터(Register) 상세
| MSR | 주소 | 비트 | 설명 | 접근 |
|---|---|---|---|---|
| IA32_TSC | 0x10 | 64 | TSC 카운터 현재 값. wrmsr로 쓰면 값을 설정(특권 명령) | rdtsc / rdmsr |
| IA32_TSC_ADJUST | 0x3B | 64 | TSC 오프셋 보정. TSC에 더해지는 signed 값. VM 마이그레이션, CPU hotplug 시 코어 간 동기화에 사용 | rdmsr / wrmsr |
| IA32_TSC_AUX | 0xC0000103 | 32 | 보조 데이터(보통 프로세서/코어 ID). rdtscp가 ECX로 반환 | rdtscp (ECX) |
| IA32_TSC_DEADLINE | 0x6E0 | 64 | TSC-deadline 모드의 APIC 타이머 비교값. TSC가 이 값에 도달하면 인터럽트 발생 | wrmsr |
TSC 사이클 → 나노초 변환
/*
* TSC 사이클을 나노초로 변환하는 공식:
*
* ns = (cycles * mult) >> shift
*
* mult/shift는 clocksource 등록 시 clocks_calc_mult_shift()가 계산합니다.
* 예: TSC 3GHz → mult=1431655765, shift=31
* ns = (cycles * 1431655765) >> 31
* ≈ cycles * 0.6667 (= 1/1.5GHz... 아님)
* 실제: cycles / 3GHz = cycles * (1/3) ns
*
* 정수 연산만으로 나눗셈 없이 고정밀 변환이 가능합니다.
*/
/* vDSO에서의 시간 계산 (사용자 공간) */
static u64 vdso_calc_ns(const struct vdso_data *vd)
{
u64 cycles = __arch_get_hw_counter(); /* rdtsc */
u64 delta = (cycles - vd->cycle_last) & vd->mask;
return vd->basetime[clock].nsec +
((delta * vd->mult) >> vd->shift);
}
/* mult/shift 계산 내부 (kernel/time/clocksource.c) */
void clocks_calc_mult_shift(u32 *mult, u32 *shift,
u32 from, u32 to, u32 maxsec)
{
/* from=Hz(TSC주파수), to=NSEC_PER_SEC(1e9)
* maxsec: 오버플로 없이 변환 가능한 최대 초
*
* shift를 최대한 크게 잡아 정밀도를 높이되,
* (cycles * mult)가 64비트를 넘지 않도록 조정 */
for (sft = 32; sft > 0; sft--) {
tmp = (u64)to << sft;
do_div(tmp, from);
if ((tmp >> 32) == 0)
break;
}
*mult = tmp;
*shift = sft;
}
PIT (8254 Programmable Interval Timer)
PIT(Programmable Interval Timer)는 IBM PC 초기부터 사용된 가장 오래된 x86 타이머입니다. Intel 8253/8254 칩(또는 호환 로직)으로 구현되며, 1.193182 MHz 고정 주파수의 오실레이터를 기반으로 동작합니다. 현대 시스템에서는 TSC 캘리브레이션과 레거시 호환 용도로만 사용되지만, 타이머 아키텍처의 기본을 이해하는 데 중요합니다.
PIT I/O 포트 레지스터
| 포트 | 이름 | 접근 | 설명 |
|---|---|---|---|
| 0x40 | Channel 0 Data | R/W | Channel 0 카운터 읽기/쓰기. 주 시스템 틱 타이머 |
| 0x41 | Channel 1 Data | R/W | Channel 1 카운터 (레거시 DRAM 리프레시, 현대 시스템 미사용) |
| 0x42 | Channel 2 Data | R/W | Channel 2 카운터. PC 스피커 또는 TSC 캘리브레이션 |
| 0x43 | Mode/Command | W | 모드 커맨드 레지스터. 채널/RW 모드/동작 모드 설정 |
Mode Command 레지스터 비트 필드 (포트 0x43)
| 비트 | 필드 | 값 | 설명 |
|---|---|---|---|
| [7:6] | SC (Select Counter) | 00/01/10 | Channel 0/1/2 선택. 11=Read-Back 명령 (8254) |
| [5:4] | RW (Read/Write) | 00 | Counter Latch: 현재 카운트 래치 |
| 01 | 하위 바이트만 R/W | ||
| 10 | 상위 바이트만 R/W | ||
| 11 | 하위 → 상위 순서로 양쪽 R/W | ||
| [3:1] | Mode | 000 | Mode 0: Interrupt on Terminal Count |
| 001 | Mode 1: HW Retriggerable One-Shot | ||
| 010 | Mode 2: Rate Generator (주기적 인터럽트) | ||
| 011 | Mode 3: Square Wave Generator | ||
| 100 | Mode 4: Software Triggered Strobe | ||
| 101 | Mode 5: HW Triggered Strobe | ||
| [0] | BCD | 0/1 | 0: 16-bit 바이너리, 1: 4-decade BCD |
PIT 주파수 유래
14.31818 / 12 = 1.193182 MHz. 이 주파수는 40년 넘게 하위 호환을 위해 유지되고 있습니다.
PIT 6가지 카운터 모드
| 모드 | 이름 | 출력 파형 | 주요 용도 |
|---|---|---|---|
| Mode 0 | Interrupt on Terminal Count | 초기 LOW → 카운트 0 도달 시 HIGH | one-shot 타이머 |
| Mode 1 | HW Retriggerable One-Shot | Gate 상승 에지 시 트리거, TC에서 HIGH | 외부 트리거 one-shot |
| Mode 2 | Rate Generator | N-1 CLK 동안 HIGH, 1 CLK LOW (반복) | 시스템 틱 (Channel 0) |
| Mode 3 | Square Wave Generator | N/2 HIGH, N/2 LOW (50% 듀티) | 스피커 톤 생성 |
| Mode 4 | Software Triggered Strobe | 카운트 완료 시 1 CLK LOW 펄스 | 소프트웨어 단발 펄스 |
| Mode 5 | HW Triggered Strobe | Gate 트리거 후 TC에서 1 CLK LOW 펄스 | 하드웨어 트리거 펄스 |
커널에서의 PIT 사용
/* arch/x86/kernel/i8253.c — PIT clockevent 등록 */
#define PIT_TICK_RATE 1193182 /* 1.193182 MHz */
static int pit_set_periodic(struct clock_event_device *evt)
{
/* Channel 0을 Mode 2(Rate Generator)로 설정
* 카운트 값 = PIT_TICK_RATE / HZ
* HZ=1000이면 → 1193 카운트 (838.1μs 주기) */
raw_spin_lock(&i8253_lock);
/* Mode Command: Channel 0, LSB/MSB, Mode 2, Binary */
outb_p(0x34, 0x43); /* 0b00_11_010_0 */
/* 카운트 값 (LSB → MSB) */
outb_p(PIT_LATCH & 0xFF, 0x40);
outb_p(PIT_LATCH >> 8, 0x40);
raw_spin_unlock(&i8253_lock);
return 0;
}
static int pit_shutdown(struct clock_event_device *evt)
{
raw_spin_lock(&i8253_lock);
/* Channel 0, LSB/MSB, Mode 0, Binary — 카운터 정지 */
outb_p(0x30, 0x43);
outb_p(0, 0x40);
outb_p(0, 0x40);
raw_spin_unlock(&i8253_lock);
return 0;
}
static struct clock_event_device i8253_clockevent = {
.name = "pit",
.features = CLOCK_EVT_FEAT_PERIODIC,
.set_state_periodic = pit_set_periodic,
.set_state_shutdown = pit_shutdown,
.rating = 110, /* HPET(250)보다 낮음 */
.irq = 0, /* IRQ0 */
};
/* PIT clocksource (TSC 대비 매우 낮은 우선순위) */
static struct clocksource i8253_cs = {
.name = "pit",
.rating = 110,
.read = pit_read,
.mask = CLOCKSOURCE_MASK(32),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/* TSC 캘리브레이션에서의 PIT Channel 2 사용 */
/* arch/x86/kernel/tsc.c — pit_calibrate_tsc() 내부 */
static unsigned long pit_calibrate_tsc(void)
{
u64 tsc, t1, t2;
unsigned long flags;
local_irq_save(flags);
/* Channel 2를 Mode 0 (one-shot)으로 프로그래밍
* Gate: NMI 포트(0x61) bit[0]으로 제어 */
outb((inb(0x61) & ~0x02) | 0x01, 0x61); /* gate on, speaker off */
/* Channel 2, LSB/MSB, Mode 0, Binary */
outb(0xB0, 0x43); /* 0b10_11_000_0 */
outb(CAL_LATCH & 0xFF, 0x42);
outb(CAL_LATCH >> 8, 0x42);
t1 = rdtsc();
/* PIT 카운트다운 완료(OUT 핀 HIGH) 대기 */
while ((inb(0x61) & 0x20) == 0)
;
t2 = rdtsc();
local_irq_restore(flags);
/* TSC 증가량 / 경과 시간(ms) = TSC kHz */
return (t2 - t1) * PIT_TICK_RATE / (CAL_LATCH * 1000);
}
HPET (High Precision Event Timer)
HPET(High Precision Event Timer)는 Intel이 IA-PC HPET Specification으로 정의한 멀티미디어 타이머입니다. PIT와 RTC를 대체하도록 설계되었으며, 최소 10 MHz(일반적으로 14.318 MHz) 주파수의 메인 카운터와 최대 32개의 비교기(comparator)를 제공합니다. MMIO를 통해 접근하며, TSC 폴백 clocksource와 clockevent 양쪽으로 사용됩니다.
General Capabilities & ID 레지스터 (0x000)
| 비트 | 필드 | 설명 |
|---|---|---|
| [7:0] | REV_ID | HPET 리비전 (최소 0x01) |
| [12:8] | NUM_TIM_CAP | 타이머 수 - 1 (예: 2 = 3개 타이머) |
| [13] | COUNT_SIZE_CAP | 1=64-bit 카운터, 0=32-bit |
| [14] | (reserved) | 예약 |
| [15] | LEG_RT_CAP | 1=Legacy Replacement Route 지원 |
| [31:16] | VENDOR_ID | PCI 벤더 ID (예: 0x8086 = Intel) |
| [63:32] | COUNTER_CLK_PERIOD | 메인 카운터 주기 (펨토초 단위). 예: 69,841,279 fs ≈ 14.318 MHz |
Timer N Configuration & Capability 레지스터
| 비트 | 필드 | R/W | 설명 |
|---|---|---|---|
| [1] | INT_TYPE_CNF | R/W | 0=Edge triggered, 1=Level triggered |
| [2] | INT_ENB_CNF | R/W | 1=인터럽트 활성화 |
| [3] | TYPE_CNF | R/W | 0=One-shot, 1=Periodic (PER_INT_CAP=1일 때만) |
| [4] | PER_INT_CAP | R/O | 1=Periodic 모드 지원 |
| [5] | SIZE_CAP | R/O | 1=64-bit 비교기, 0=32-bit |
| [6] | VAL_SET_CNF | R/W | Periodic 모드 시 비교기 값 직접 설정 허용 |
| [8] | 32MODE_CNF | R/W | 1=64-bit 타이머를 32-bit 모드로 강제 |
| [13:9] | INT_ROUTE_CNF | R/W | 인터럽트 라우팅 (IOAPIC 입력 핀 번호) |
| [14] | FSB_EN_CNF | R/W | 1=FSB(MSI) 인터럽트 라우팅 사용 |
| [15] | FSB_INT_DEL_CAP | R/O | 1=FSB 인터럽트 전달 지원 |
| [63:32] | INT_ROUTE_CAP | R/O | 사용 가능한 IRQ 비트맵 (bit N=1 → IOAPIC pin N 사용 가능) |
HPET Clocksource & Clockevent 이중 역할
/* arch/x86/kernel/hpet.c — HPET clocksource */
static u64 read_hpet(struct clocksource *cs)
{
return (u64)hpet_readl(HPET_COUNTER);
/* MMIO 읽기: 0xFED00000 + 0xF0 (Main Counter) */
}
static struct clocksource clocksource_hpet = {
.name = "hpet",
.rating = 250,
.read = read_hpet,
.mask = HPET_MASK, /* 32-bit: 0xFFFFFFFF */
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/* HPET는 clocksource(시간 측정)와 clockevent(인터럽트 생성)
* 양쪽으로 동시에 사용됩니다:
* - clocksource: Main Counter를 읽어 시간 측정
* - clockevent: Comparator에 미래 값을 설정하여 인터럽트
*
* TSC가 clocksource로 선택되면 HPET는 clockevent 전용,
* TSC 불안정 시에는 두 역할 모두 HPET이 담당합니다. */
/* HPET clockevent 초기화 (간략화) */
static int hpet_set_next_event(unsigned long delta,
struct clock_event_device *evt)
{
u32 cnt = hpet_readl(HPET_COUNTER);
/* Comparator = 현재 카운터 + delta */
hpet_writel(cnt + delta, HPET_Tn_CMP(0));
/* 이미 지나간 경우 체크 (짧은 delta일 때 경합 방지) */
return (s32)(hpet_readl(HPET_COUNTER) - cnt) >= (s32)delta
? -ETIME : 0;
}
static int hpet_set_periodic(struct clock_event_device *evt)
{
unsigned int cfg = hpet_readl(HPET_Tn_CFG(0));
cfg |= HPET_TN_ENABLE | HPET_TN_PERIODIC | HPET_TN_SETVAL;
hpet_writel(cfg, HPET_Tn_CFG(0));
/* periodic 주기 값 설정 */
hpet_writel(hpet_tick, HPET_Tn_CMP(0));
return 0;
}
static struct clock_event_device hpet_clockevent = {
.name = "hpet",
.features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
.set_state_periodic = hpet_set_periodic,
.set_state_oneshot = hpet_set_oneshot,
.set_next_event = hpet_set_next_event,
.rating = 50, /* TSC-deadline(350) / LAPIC(200)보다 낮음 */
.irq = 0,
};
/* HPET 초기화 — ACPI HPET 테이블에서 주소/ID 추출 */
static int __init hpet_enable(void)
{
unsigned int id, cfg;
/* ACPI HPET 테이블에서 베이스 주소 획득 (보통 0xFED00000) */
hpet_virt_address = ioremap(hpet_address, HPET_MMAP_SIZE);
/* General Capabilities 읽기 */
id = hpet_readl(HPET_ID);
hpet_period = hpet_readl(HPET_PERIOD); /* fs 단위 주기 */
/* 주기 유효성 검증: 100ns(10MHz) 이하, 10fs 이상 */
if (hpet_period < HPET_MIN_PERIOD ||
hpet_period > HPET_MAX_PERIOD)
return 0;
/* General Configuration: 전체 활성화 */
cfg = hpet_readl(HPET_CFG);
cfg |= HPET_CFG_ENABLE; /* bit[0] = 1 */
/* cfg |= HPET_CFG_LEGACY; bit[1] = 1 (레거시 교체 시) */
hpet_writel(cfg, HPET_CFG);
pr_info("hpet: %d comparators, %s %d.%06d MHz counter\n",
num_timers, id & HPET_ID_64BIT ? "64-bit" : "32-bit",
(int)(freq / 1000000), (int)(freq % 1000000));
return 1;
}
# dmesg 출력 예시
$ dmesg | grep -i hpet
hpet: HPET id: 0x8086a201 base: 0xfed00000
hpet clockevent registered
hpet0: at MMIO 0xfed00000, IRQs 2, 8, 0
hpet0: 3 comparators, 64-bit 14.318180 MHz counter
HPET 커맨드라인 옵션 & 알려진 문제
| 커맨드라인 | 설명 |
|---|---|
hpet=force | ACPI 테이블에서 HPET을 감지 못해도 강제 활성화 |
nohpet | HPET 완전 비활성화 |
hpet=disable | nohpet과 동일 |
clocksource=hpet | HPET을 clocksource로 강제 지정 |
- MMIO 레이턴시 — HPET 읽기는 MMIO이므로 ~100-300ns 소요. 반복 읽기 시 TSC(~20ns) 대비 5-15배 느림
- vDSO 비호환 — HPET이 clocksource이면 vDSO가 비활성화되어
clock_gettime()이 모두 syscall 경유 - 칩셋 버그 — 일부 AMD/VIA 칩셋에서 HPET 카운터 역행(regression) 보고. 특히 SB600/SB700 계열
- 32-bit 카운터 wrap — 32-bit HPET은 ~5분(14.3MHz 기준)에 wrap.
max_idle_ns가 짧아져 NO_HZ 효율 저하 - SMI 간섭 — System Management Interrupt가 HPET 읽기 사이에 발생하면 긴 레이턴시 발생
ACPI PM Timer
/*
* ACPI Power Management Timer
* - 고정 주파수: 3.579545 MHz (NTSC 컬러 서브캐리어의 3배)
* - 24-bit 또는 32-bit 카운터
* - I/O 포트 접근 (매우 느림: ~500ns-1us)
* - 가상화 환경에서도 안정적
*
* rating이 낮지만(200), 모든 ACPI 시스템에서 사용 가능하여
* 최후의 폴백 clocksource로 활용됩니다.
*/
static u64 acpi_pm_read(struct clocksource *cs)
{
return (u64)inl(pmtmr_ioport) & ACPI_PM_MASK;
/* I/O 포트 읽기: 일반적으로 0x408 */
}
static struct clocksource clocksource_acpi_pm = {
.name = "acpi_pm",
.rating = 200,
.read = acpi_pm_read,
.mask = (u64)ACPI_PM_MASK, /* 24-bit: 0x00FFFFFF */
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
24-bit vs 32-bit ACPI PM Timer
| 속성 | 24-bit PM Timer | 32-bit PM Timer |
|---|---|---|
| 카운터 마스크 | 0x00FFFFFF (16,777,215) | 0xFFFFFFFF (4,294,967,295) |
| Wrap 주기 | ~4.69초 | ~1199초 (~20분) |
| FADT 비트 | TMR_VAL_EXT = 0 | TMR_VAL_EXT = 1 |
| max_idle_ns | ~2.3초 (wrap의 절반) | ~600초 |
| NO_HZ 영향 | 짧은 wrap → idle 틱 강제 | 충분한 idle 시간 허용 |
/* drivers/clocksource/acpi_pm.c — ACPI FADT에서 PM Timer 포트 검출 */
static int __init init_acpi_pm_clocksource(void)
{
/* ACPI FADT 테이블에서 PM Timer I/O 포트 주소 획득
* 일반적으로 0x408 또는 0x508 */
pmtmr_ioport = acpi_gbl_FADT.pm_timer_block;
if (!pmtmr_ioport)
return -ENODEV;
/* 32-bit 확장 여부 확인 (FADT flags) */
if (acpi_gbl_FADT.flags & ACPI_FADT_32BIT_TIMER)
clocksource_acpi_pm.mask = CLOCKSOURCE_MASK(32);
else
clocksource_acpi_pm.mask = CLOCKSOURCE_MASK(24);
/* 3회 읽기 안정화 검증 */
if (verify_pmtmr_rate() != 0) {
pr_warn("PM-Timer has inconsistent readings\n");
return -EINVAL;
}
return clocksource_register_hz(&clocksource_acpi_pm,
PMTMR_TICKS_PER_SEC);
}
/* 3회 읽기 안정화 패턴 — acpi_pm_read_verified() */
static u64 acpi_pm_read_verified(struct clocksource *cs)
{
u32 v1, v2, v3;
/* 일부 칩셋(ICH4 등)에서 PM Timer 읽기 시
* 비트 플립 오류가 발생할 수 있어 3회 읽어서 확인.
* v1==v2 또는 v2==v3이면 그 값이 올바른 것 */
v1 = read_pmtmr();
v2 = read_pmtmr();
v3 = read_pmtmr();
if (v1 == v2 || v2 == v3)
return (u64)v2;
if (v1 == v3)
return (u64)v1;
/* 세 번 모두 다르면 마지막 값 반환 (드문 경우) */
return (u64)v3;
}
/* 칩셋에 따라 일반 read 또는 verified read 사용:
* - 정상 칩셋: acpi_pm_read() (단일 I/O 포트 읽기)
* - 문제 칩셋: acpi_pm_read_verified() (3회 읽기) */
clocksource=acpi_pm을 사용합니다.
ARM Generic Timer
/*
* ARM Architecture Timer (ARMv7+, ARMv8)
* - 시스템 카운터: 모든 CPU에서 공유되는 단일 주파수 카운터
* - 주파수: CNTFRQ_EL0 (일반적으로 1-100 MHz)
* - 64-bit 카운터: CNTVCT_EL0 (가상) 또는 CNTPCT_EL0 (물리)
* - CPU 레지스터 접근: 매우 빠름 (~5-20ns)
* - 타이머 비교기: 각 CPU에 EL1 Physical/Virtual, EL2 타이머
*/
/* drivers/clocksource/arm_arch_timer.c */
static u64 arch_counter_read(struct clocksource *cs)
{
return arch_timer_read_counter();
/* AArch64: mrs x0, cntvct_el0 */
}
static struct clocksource clocksource_counter = {
.name = "arch_sys_counter",
.rating = 400, /* 매우 높은 우선순위 */
.read = arch_counter_read,
.mask = CLOCKSOURCE_MASK(56),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
# ARM 타이머 정보
$ dmesg | grep -i "arch_timer\|clocksource"
arch_timer: cp15 timer(s) running at 24.00MHz (phys)
clocksource: arch_sys_counter: mask: 0xffffffffffffff
max_cycles: 0x588fe9dc0, max_idle_ns: 440795202592 ns
KVM pvclock
/*
* KVM 반가상화 클럭 (paravirtual clock)
* - 게스트 VM이 하이퍼바이저의 시간 정보를 직접 읽음
* - 공유 메모리 페이지를 통해 호스트 TSC 오프셋 전달
* - VM exit 없이 시간 읽기 가능 → 매우 빠름
* - rating: 450 (TSC보다 높음 — VM에서 더 안정적)
*/
static struct clocksource kvm_clock = {
.name = "kvm-clock",
.rating = 450,
.read = kvm_clock_get_cycles,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
# KVM 게스트에서 확인
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
kvm-clock
Clocksource 비교 종합
| 클럭 | 플랫폼 | 주파수 | 비트 | 접근 | 읽기 비용 | Rating |
|---|---|---|---|---|---|---|
| TSC | x86 | ~GHz | 64 | rdtsc 명령 | ~20-30ns | 300-350 |
| HPET | x86 | 14.3 MHz | 32/64 | MMIO | ~100-300ns | 250 |
| PIT | x86 | 1.19 MHz | 16 | I/O port | ~1us | 110 |
| ACPI PM | x86 | 3.58 MHz | 24/32 | I/O port | ~500ns-1us | 200 |
| ARM Arch | ARM | 1-100 MHz | 56 | 시스템 레지스터 | ~5-20ns | 400 |
| KVM pvclock | KVM 게스트 | 호스트 TSC | 64 | 공유 메모리 | ~15-25ns | 450 |
| jiffies | 모든 | HZ | 64 | 전역 변수 | ~1ns | 1 |
RTC (Real-Time Clock)
RTC(Real-Time Clock)는 시스템 전원이 꺼져 있는 동안에도 배터리(보통 CR2032 코인 셀)로 구동되어 벽시계 시간을 유지하는 하드웨어입니다. 커널 부팅 시 RTC에서 현재 시각을 읽어 CLOCK_REALTIME을 초기화하고, 주기적으로(11분 간격) 시스템 시간을 RTC에 역동기화합니다. suspend/resume 시에는 RTC가 경과 시간 복원의 핵심 기준점이 됩니다.
RTC 유형 비교
| 유형 | 인터페이스 | 플랫폼 | 정밀도 | 대표 칩 |
|---|---|---|---|---|
| CMOS RTC | I/O 포트 0x70/0x71 | x86 (MC146818 호환) | ±1초/일 | MC146818, DS12887 |
| I2C RTC | I2C 버스 | 임베디드, SBC | ±2~5 ppm | DS3231 (TCXO, ±2ppm), PCF8563, MCP7940 |
| SPI RTC | SPI 버스 | 산업용, 임베디드 | ±2~5 ppm | DS3234, PCF2123 |
| SoC 내장 RTC | MMIO (SoC 레지스터) | ARM SoC | 칩 의존 | STM32 RTC, iMX SNVS, Allwinner RTC |
Linux RTC 프레임워크
/* include/linux/rtc.h — RTC 장치 드라이버 인터페이스 */
struct rtc_class_ops {
int (*read_time)(struct device *dev, struct rtc_time *tm);
int (*set_time)(struct device *dev, struct rtc_time *tm);
int (*read_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
int (*set_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
int (*alarm_irq_enable)(struct device *dev, unsigned int enabled);
int (*read_offset)(struct device *dev, long *offset);
int (*set_offset)(struct device *dev, long offset);
};
/* RTC 등록 API */
struct rtc_device *devm_rtc_device_register(
struct device *dev, const char *name,
const struct rtc_class_ops *ops,
struct module *owner);
/* struct rtc_time — RTC 시간 표현 */
struct rtc_time {
int tm_sec; /* 초 (0-59) */
int tm_min; /* 분 (0-59) */
int tm_hour; /* 시 (0-23) */
int tm_mday; /* 일 (1-31) */
int tm_mon; /* 월 (0-11) ← 주의: 0-based */
int tm_year; /* 연 (1900 기준) ← 주의: 2024 = 124 */
int tm_wday; /* 요일 (0=일요일) */
int tm_yday; /* 연중 일 (0-365) */
int tm_isdst; /* DST 플래그 */
};
부팅 시 RTC → 시스템 시계 초기화
/*
* CONFIG_RTC_HCTOSYS=y 설정 시,
* late_initcall에서 RTC 시간을 읽어 CLOCK_REALTIME을 초기화합니다.
*
* drivers/rtc/hctosys.c — rtc_hctosys()
*/
static int __init rtc_hctosys(void)
{
struct rtc_time tm;
struct timespec64 tv;
/* /dev/rtc0 (또는 CONFIG_RTC_HCTOSYS_DEVICE 지정 장치)에서 읽기 */
err = rtc_read_time(rtc, &tm);
rtc_tm_to_time64(&tm, &tv.tv_sec);
tv.tv_nsec = 0;
/* CLOCK_REALTIME 설정 */
err = do_settimeofday64(&tv);
dev_info(rtc->dev.parent,
"setting system clock to %ptR UTC\n", &tm);
}
late_initcall(rtc_hctosys);
/*
* CONFIG_RTC_SYSTOHC=y 설정 시,
* NTP 동기화 상태에서 11분(660초)마다 시스템 시간을 RTC에 역기록합니다.
*
* kernel/time/ntp.c — sync_hw_clock()
* 조건: STA_UNSYNC 플래그가 꺼져 있을 때만 (NTP 동기화 완료 상태)
*/
# RTC 시간 확인 (hwclock)
$ hwclock --show
2026-03-21 15:30:00.123456+09:00
# 시스템 시간 → RTC 동기화
$ hwclock --systohc
# RTC → 시스템 시간 동기화
$ hwclock --hctosys
# RTC를 UTC로 유지 (권장)
$ hwclock --systohc --utc
# sysfs를 통한 RTC 확인
$ cat /sys/class/rtc/rtc0/name
rtc_cmos
$ cat /sys/class/rtc/rtc0/date
2026-03-21
$ cat /sys/class/rtc/rtc0/time
06:30:00
$ cat /sys/class/rtc/rtc0/since_epoch
1774079400
# dmesg RTC 관련 로그
$ dmesg | grep -i rtc
rtc_cmos 00:01: RTC can wake from S4
rtc_cmos 00:01: setting system clock to 2026-03-21T06:30:00 UTC
timedatectl set-local-rtc 0으로 UTC 모드를 확인하세요. RTC에 로컬 시간을 사용하면 DST 전환 시 시간이 이중 보정되는 버그가 발생할 수 있습니다.
Common Clock Framework (CCF)
Common Clock Framework(CCF)는 SoC 내부의 PLL, divider, gate, mux를 계층적으로 연결해 각 장치(UART, SPI, GPU, NPU 등)에 필요한 주파수를 공급하는 프레임워크입니다. 이름이 비슷해 혼동되지만 clocksource/timekeeper와 목적이 다릅니다. clocksource는 "현재 시각 계산", CCF는 "클럭 트리 설정과 전력 제어"가 핵심입니다.
| 프레임워크 | 주요 질문 | 핵심 자료구조 | 대표 API |
|---|---|---|---|
| clocksource | 지금 몇 ns인가? | struct clocksource | clocksource_register_hz() |
| clockevent | 언제 인터럽트를 발생시킬까? | struct clock_event_device | clockevents_config_and_register() |
| CCF | 어떤 장치에 몇 MHz를 줄까? | struct clk_hw | clk_set_rate(), clk_prepare_enable() |
CCF 핵심 모델
/* include/linux/clk-provider.h */
struct clk_ops {
int (*prepare)(struct clk_hw *hw);
void (*unprepare)(struct clk_hw *hw);
int (*enable)(struct clk_hw *hw);
void (*disable)(struct clk_hw *hw);
unsigned long (*recalc_rate)(struct clk_hw *hw, unsigned long prate);
long (*round_rate)(struct clk_hw *hw, unsigned long rate, unsigned long *prate);
int (*set_rate)(struct clk_hw *hw, unsigned long rate, unsigned long prate);
u8 (*get_parent)(struct clk_hw *hw);
int (*set_parent)(struct clk_hw *hw, u8 index);
};
/* 소비자 드라이버에서 사용하는 공통 API */
ret = clk_prepare_enable(clk); /* gate on */
ret = clk_set_rate(clk, 50000000); /* 50MHz 요청 */
rate = clk_get_rate(clk);
clk_disable_unprepare(clk); /* gate off */
Device Tree 바인딩과 등록 흐름
/* 클럭 제공자(SoC CRU/CCU) */
cru: clock-controller@ff760000 {
compatible = "vendor,soc-cru";
reg = <0x0 0xff760000 0x0 0x1000>;
#clock-cells = <1>;
};
uart2: serial@ff1a0000 {
compatible = "vendor,uart";
reg = <0x0 0xff1a0000 0x0 0x100>;
clocks = <&cru 42>;
clock-names = "baud";
};
/* provider probe */
static int soc_cru_probe(struct platform_device *pdev)
{
struct clk_hw_onecell_data *onecell;
onecell = devm_kzalloc(&pdev->dev, struct_size(onecell, hws, 128), GFP_KERNEL);
onecell->num = 128;
onecell->hws[42] = clk_hw_register_gate(&pdev->dev, "uart2_gate",
"pll_uart", 0, base + 0x120, 4, 0, &lock);
return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_onecell_get, onecell);
}
CCF 디버깅(Debugging) 체크리스트
# debugfs mount
$ mount -t debugfs none /sys/kernel/debug
# 전체 클럭 트리
$ cat /sys/kernel/debug/clk/clk_summary
# 핵심 필드 확인: enable_cnt / prepare_cnt / rate / accuracy
$ grep -E 'uart|spi|gpu' /sys/kernel/debug/clk/clk_summary
# cpufreq와 CCF를 함께 볼 때
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_driver
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq
CPU 주파수 관리 (cpufreq)
cpufreq는 policy 단위(보통 클러스터/패키지)로 최소/최대 주파수와 거버너를 적용합니다. 스케줄러(Scheduler)는 현재 CPU 부하를 util 값으로 제공하고, 거버너는 이를 바탕으로 목표 주파수를 계산해 드라이버(intel_pstate, amd_pstate, acpi-cpufreq)에 전달합니다.
cpufreq 구성요소와 정책 단위
| 구성요소 | 역할 | 대표 sysfs |
|---|---|---|
policyX | CPU 묶음별 min/max, 거버너 저장 | scaling_min_freq, scaling_max_freq |
| 거버너 | 부하 기반 목표 주파수 계산 | scaling_governor |
| 드라이버 | 하드웨어에 주파수 반영 | scaling_driver |
| 통계 | 전환 횟수/체류 시간 제공 | stats/time_in_state |
# policy와 CPU 매핑 확인
$ cat /sys/devices/system/cpu/cpufreq/policy0/related_cpus
0 1 2 3
# 현재 정책
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_driver
intel_pstate
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
schedutil
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq
거버너와 시간 측정 안정성
| 거버너 | 특성 | 시간 측정 영향 |
|---|---|---|
performance | 최대 주파수 고정 | 지연 편차가 작지만 전력 소모 증가 |
powersave | 낮은 주파수 선호 | 긴 시스템 콜(System Call)/인터럽트 처리 지연 가능 |
schedutil | 스케줄러 util 기반 동적 조절 | 일반 서버 기본값으로 균형이 좋음 |
ondemand | 주기적 샘플링 후 급상승 | 짧은 버스(Bus)트 워크로드에서 출렁임 가능 |
cpufreq 트러블슈팅 절차
# 1) 주파수 드라이버/거버너 확인
$ cpupower frequency-info
# 2) 주파수 전환 추적 (tracefs)
$ echo 1 > /sys/kernel/tracing/events/power/cpu_frequency/enable
$ echo 1 > /sys/kernel/tracing/events/power/cpu_frequency_limits/enable
$ cat /sys/kernel/tracing/trace_pipe
# 3) 정책 고정 실험
$ cpupower frequency-set -g performance
$ sleep 10
$ cpupower frequency-set -g schedutil
vDSO (virtual Dynamic Shared Object)
vDSO는 커널이 사용자 공간에 매핑하는 공유 라이브러리(Shared Library)로, 시스템 콜 없이 시간 읽기를 수행합니다. clock_gettime()의 대부분의 호출이 vDSO를 통해 처리됩니다.
vDSO 지원 시계 테이블
| 시계 | vDSO 지원 | HW 읽기 | 대략 비용 | 비고 |
|---|---|---|---|---|
CLOCK_REALTIME | O | rdtsc | ~25ns | TSC clocksource일 때 |
CLOCK_MONOTONIC | O | rdtsc | ~25ns | 가장 많이 사용 |
CLOCK_REALTIME_COARSE | O | 불필요 | ~5ns | vdso_data 캐시만 읽기 |
CLOCK_MONOTONIC_COARSE | O | 불필요 | ~5ns | 해상도 1/HZ |
CLOCK_BOOTTIME | O (v5.7+) | rdtsc | ~25ns | suspend 오프셋 포함 |
CLOCK_TAI | O (v5.7+) | rdtsc | ~25ns | TAI 오프셋 포함 |
CLOCK_MONOTONIC_RAW | X | — | ~200ns | syscall 필수 |
CLOCK_PROCESS_CPUTIME_ID | X | — | ~200ns | 프로세스별 정보 필요 |
CLOCK_THREAD_CPUTIME_ID | X | — | ~200ns | 스레드별 정보 필요 |
clock_mode = VDSO_CLOCKMODE_NONE으로 설정됩니다. 이 경우 glibc는 일반 syscall로 fallback하여 성능이 크게 저하됩니다. clocksource=tsc를 유지하는 것이 vDSO 성능의 핵심입니다.
vDSO 동작 원리
/*
* vDSO 시간 읽기 흐름:
*
* 1. 커널이 vdso_data 페이지를 사용자 주소 공간에 읽기 전용으로 매핑
* 2. 매 틱마다 커널이 vdso_data를 업데이트
* 3. 사용자 프로세스가 clock_gettime() 호출
* 4. glibc가 vDSO 함수를 호출 (커널 진입 없음)
* 5. vDSO 함수가:
* a. vdso_data의 seqcount 확인
* b. 하드웨어 카운터 직접 읽기 (rdtsc 등)
* c. vdso_data의 mult/shift로 나노초 변환
* d. 결과 반환
*
* 비용: syscall ~100-200ns → vDSO ~20-30ns (x86 TSC)
*/
/* include/vdso/datapage.h */
struct vdso_data {
u32 seq; /* seqcount (업데이트 감지) */
s32 clock_mode; /* vDSO 지원 클럭 모드 */
u64 cycle_last; /* 마지막 사이클 값 */
u64 mask; /* 카운터 마스크 */
u32 mult; /* 사이클→ns 곱셈 인수 */
u32 shift; /* 사이클→ns 시프트 인수 */
struct vdso_timestamp
basetime[VDSO_BASES]; /* 시계별 기준 시간 */
s32 tz_minuteswest; /* 타임존 오프셋 */
s32 tz_dsttime; /* DST 정보 */
u32 hrtimer_res; /* hrtimer 해상도 */
};
# vDSO 확인
$ ldd /bin/ls | grep vdso
linux-vdso.so.1 (0x00007ffd...)
# vDSO가 제공하는 함수
$ objdump -T /lib/modules/$(uname -r)/vdso/vdso64.so 2>/dev/null || \
LD_SHOW_AUXV=1 /bin/true | grep SYSINFO
# __vdso_clock_gettime, __vdso_gettimeofday, __vdso_time,
# __vdso_clock_getres, __vdso_getcpu
코드 설명
include/vdso/datapage.h에 정의된 struct vdso_data와 vDSO의 clock_gettime() 고속 경로입니다. 커널 진입(syscall) 없이 사용자 공간에서 직접 시간을 읽습니다.
- vdso_data 페이지 매핑커널이
vdso_data구조체를 포함하는 물리 페이지를 모든 프로세스의 가상 주소 공간에 읽기 전용으로 매핑합니다.update_vsyscall()이 매 틱마다 이 데이터를 갱신합니다. - seq (seqcount)커널의 timekeeper와 동일한 seqcount 패턴입니다. vDSO 코드가 읽는 동안 커널이 데이터를 갱신하면
seq가 변경되어 retry합니다. - cycle_last / mask / mult / shifttimekeeper의
tk_read_base에서 복사된 값입니다. vDSO 함수가 이 값들로ktime_get()과 동일한 사이클→나노초 변환을 사용자 공간에서 수행합니다. - basetime[VDSO_BASES]각 CLOCK_* 시계(REALTIME, MONOTONIC, BOOTTIME 등)의 기준 시간을 배열로 저장합니다. vDSO가 요청된 clock_id에 맞는 기준 시간을 선택합니다.
- clock_modevDSO가 지원하는 clocksource 모드입니다. TSC 기반(
VDSO_CLOCKMODE_TSC)일 때rdtsc로 직접 읽고, 미지원 모드이면 커널 syscall로 폴백합니다. - 성능 비교일반
clock_gettime()syscall은 ~100-200ns가 소요되지만, vDSO 경로는 커널 진입 오버헤드가 없어 x86 TSC 기준 ~20-30ns로 약 5-7배 빠릅니다.
vDSO 성능 측정
/* clock_gettime 벤치마크 */
#include <time.h>
#include <stdio.h>
int main(void) {
struct timespec ts;
int i;
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < 10000000; i++)
clock_gettime(CLOCK_MONOTONIC, &ts);
clock_gettime(CLOCK_MONOTONIC, &end);
long ns = (end.tv_sec - start.tv_sec) * 1000000000L
+ (end.tv_nsec - start.tv_nsec);
printf("avg: %ld ns/call\\n", ns / 10000000);
/* TSC vDSO: ~20-30ns, HPET syscall: ~600-1000ns */
}
/* 결과 비교 (x86, TSC):
* CLOCK_MONOTONIC: ~25ns (vDSO + rdtsc)
* CLOCK_MONOTONIC_COARSE: ~5ns (vDSO, 카운터 읽기 불필요)
* CLOCK_REALTIME: ~25ns (vDSO + rdtsc)
* CLOCK_REALTIME_COARSE: ~5ns (vDSO)
*
* vDSO 비활성 또는 HPET clocksource 시:
* CLOCK_MONOTONIC: ~600ns (syscall + MMIO)
*/
NTP 시간 동기화
NTP(Network Time Protocol)는 네트워크를 통해 시스템 시계를 외부 기준 시계에 동기화합니다. 커널의 NTP 서브시스템은 adjtimex() 시스템 콜을 통해 시간 보정을 수행합니다.
NTP vs PTP 정밀도 비교
| 특성 | NTP | PTP (SW timestamping) | PTP (HW timestamping) |
|---|---|---|---|
| 프로토콜 | RFC 5905 (NTPv4) | IEEE 1588-2008/2019 | IEEE 1588-2008/2019 |
| 일반 정밀도 | 1~50 ms | 10~100 us | 10~100 ns |
| LAN 최적 정밀도 | ~100 us | ~1 us | < 100 ns |
| WAN 정밀도 | 1~10 ms | 지원 안 함 (일반적) | 지원 안 함 (일반적) |
| 전용 하드웨어 | 불필요 | 불필요 | PTP 지원 NIC 필요 |
| 네트워크 요구 | 인터넷/LAN | LAN | LAN (PTP-aware 스위치 권장) |
| 커널 인터페이스 | adjtimex() | adjtimex() + SO_TIMESTAMPING | /dev/ptpN + PHC |
| 대표 데몬 | chrony, ntpd | ptp4l + phc2sys | ptp4l + phc2sys |
| 적합 용도 | 일반 서버, 데스크톱 | 데이터센터 내부 | 금융, 통신, 산업 제어 |
NTP Stratum 계층
| Stratum | 설명 | 예시 | 일반 정밀도 |
|---|---|---|---|
| 0 | 기준 클럭 (Reference Clock) | 원자시계, GPS, CDMA | ns 수준 |
| 1 | Stratum 0에 직접 연결 | pool.ntp.org 1차 서버 | ~1 us |
| 2 | Stratum 1에서 동기화 | 기업 내부 NTP 서버 | ~1 ms |
| 3~15 | 단계별 전파 | 클라이언트 → 서버 체인 | ~1-50 ms |
| 16 | 동기화 불가 (unsynchronized) | — | — |
커널 NTP 보정 메커니즘
/*
* NTP 보정 동작:
*
* 1. ntpd/chronyd가 NTP 서버에서 시간 오프셋 측정
* 2. adjtimex() 시스템 콜로 커널에 보정 파라미터 전달
* 3. 커널이 clocksource의 mult 값을 미세 조정
* → 클럭 주파수를 가속/감속하여 점진적 보정 (slew)
* 4. 큰 오프셋(> 0.5초)인 경우 시간 점프 (step)
*
* 보정 모드:
* - PLL (Phase-Locked Loop): 위상+주파수 보정, 안정적
* - FLL (Frequency-Locked Loop): 주파수만 보정, 빠른 수렴
*/
# NTP 상태 확인
$ adjtimex --print
mode: 0
offset: -123 us # 현재 시간 오프셋
frequency: -12345678 # 주파수 보정값 (2^-16 ppm)
maxerror: 500000 us
esterror: 100 us
status: 8193 # STA_PLL | STA_NANO
constant: 7 # PLL 시간 상수 (2^n 초)
precision: 1 us
tolerance: 500 ppm
tick: 10000 us # 틱 간격
# chronyc로 NTP 상태 확인
$ chronyc tracking
Reference ID : A.B.C.D (ntp.example.com)
Stratum : 2
Ref time (UTC) : Fri Feb 07 10:30:00 2026
System time : 0.000000123 seconds fast of NTP time
Last offset : -0.000000045 seconds
RMS offset : 0.000000089 seconds
Frequency : 1.234 ppm slow
Residual freq : -0.001 ppm
Root delay : 0.012345678 seconds
adjtimex 구조체
/* include/uapi/linux/timex.h */
struct __kernel_timex {
unsigned int modes; /* 보정 모드 비트맵 */
long long offset; /* 시간 오프셋 (us 또는 ns) */
long long freq; /* 주파수 오프셋 (2^-16 ppm) */
long long maxerror; /* 최대 추정 오차 (us) */
long long esterror; /* 추정 오차 (us) */
int status; /* 상태 플래그 (STA_*) */
long long constant; /* PLL 시간 상수 */
long long precision; /* 클럭 정밀도 (us) */
long long tolerance; /* 클럭 주파수 허용 오차 (ppm) */
struct __kernel_timeval time; /* 현재 시간 */
long long tick; /* 틱 간 us */
long long ppsfreq; /* PPS 주파수 (2^-16 ppm) */
long long jitter; /* PPS 지터 (us) */
int shift; /* PPS 인터벌 (초) */
long long stabil; /* PPS 안정성 (ppm) */
long long jitcnt; /* PPS 지터 초과 횟수 */
long long calcnt; /* PPS 보정 간격 수 */
long long errcnt; /* PPS 보정 오류 수 */
long long stbcnt; /* PPS 안정성 초과 횟수 */
int tai; /* TAI 오프셋 (초) */
};
NTP 커널 내부 보정 경로
사용자 공간의 adjtimex() 시스템 콜이 커널 내부에서 어떤 경로를 거쳐 실제 클럭 주파수를 보정하는지 전체 흐름을 살펴봅니다. 핵심은 NTP 서브시스템이 tick_length를 조정하고, timekeeping 루프가 이를 mult에 반영하는 피드백 구조입니다.
do_adjtimex() → __do_adjtimex() 흐름
do_adjtimex()는 timekeeping.c에서 tk_core.seq 쓰기 잠금을 획득한 뒤, 실제 NTP 상태 머신 처리를 ntp.c의 __do_adjtimex()에 위임합니다.
/* kernel/time/timekeeping.c — 단순화 */
int do_adjtimex(struct __kernel_timex *txc)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned long flags;
int ret;
raw_spin_lock_irqsave(&tk_core.lock, flags);
write_seqcount_begin(&tk_core.seq);
/* NTP 상태 머신 처리 위임 */
ret = __do_adjtimex(txc, &ts, &tai);
/* NTP에서 변경된 tick_length를 timekeeping에 반영 */
timekeeping_update(tk, TK_MIRROR | TK_CLOCK_WAS_SET);
write_seqcount_end(&tk_core.seq);
raw_spin_unlock_irqrestore(&tk_core.lock, flags);
return ret;
}
코드 설명
do_adjtimex()는 사용자 공간에서 NTP 보정 요청이 들어올 때 진입하는 커널 최상위 함수입니다.
- raw_spin_lock_irqsave / write_seqcount_beginseqcount 쓰기 잠금으로 timekeeping 구조체의 일관성을 보장합니다. 읽기 경로(
ktime_get()등)는 잠금 없이 seqcount 재시도로 동작합니다. - __do_adjtimex()ntp.c에 구현된 실제 NTP 상태 머신입니다. freq, offset, status 등을 처리하고 내부 NTP 변수를 갱신합니다.
- timekeeping_update()NTP가 변경한
tick_length를 timekeeping 구조체에 반영합니다.TK_CLOCK_WAS_SET은 시간이 변경되었음을 알리는 플래그입니다.
ntp_update_frequency()
ADJ_FREQUENCY가 설정되면 호출되어 사용자가 지정한 주파수 오프셋을 tick_length 조정 값으로 변환합니다. 핵심 공식은 tick_length = tick_length_base + (freq_offset × tick_nsec) / 106입니다.
/* kernel/time/ntp.c — 단순화 */
static void ntp_update_frequency(void)
{
u64 second_length;
u64 tick_length_base;
/* tick_usec(기본 10000)을 nsec 고정소수점으로 변환 */
tick_length_base = (u64)tick_usec * NSEC_PER_USEC * NTP_SCALE;
/* freq: 2^-16 ppm 단위 → 틱 길이에 반영 */
second_length = tick_length_base;
second_length += (s64)time_freq * tick_nsec_scaled;
second_length += (s64)time_freq >> (NTP_SCALE_SHIFT - 10);
tick_length = second_length;
}
코드 설명
NTP 주파수 보정의 핵심 변환 함수입니다.
- tick_length_base보정 없는 기본 틱 길이입니다.
tick_usec(기본 10000μs = 10ms)를 나노초 고정소수점으로 변환합니다. - time_freq사용자가
adjtimex()의freq필드로 설정한 주파수 오프셋입니다. 2-16 ppm 단위로 표현됩니다. - second_length += time_freq * tick_nsec_scaled주파수 오프셋을 틱 길이의 증감으로 변환합니다. 양수이면 틱이 길어져 시계가 느려지고, 음수이면 빨라집니다.
ntp_tick_length()
ntp_tick_length()는 NTP가 보정한 틱 길이를 반환합니다. timekeeping_advance()가 매 틱마다 이 값을 읽어 mult 조정에 사용하므로, NTP 서브시스템과 timekeeping을 연결하는 핵심 브릿지 함수입니다.
/* kernel/time/ntp.c */
u64 ntp_tick_length(void)
{
return tick_length;
}
/* kernel/time/timekeeping.c — timekeeping_advance()에서 사용 */
static void timekeeping_advance(...)
{
/* 초 경계를 넘을 때마다 NTP 보정된 틱 길이 갱신 */
while (tk->tkr_mono.xtime_nsec >= (u64)NSEC_PER_SEC << tk->tkr_mono.shift) {
/* ... 초 증가 ... */
second_overflow(tk->xtime_sec);
}
/* mult 조정으로 클럭 속도 미세 보정 */
timekeeping_adjust(tk, offset);
}
코드 설명
ntp_tick_length()는 NTP 서브시스템과 timekeeping 사이의 유일한 인터페이스입니다.
- tick_length
ntp_update_frequency()에서 계산된 NTP 보정 틱 길이입니다. 고정소수점 나노초 단위로, 1초의 길이가 NTP 주파수 보정에 따라 약간 길거나 짧아집니다. - second_overflow()초 경계를 넘을 때 호출되어 윤초 처리와 PLL/FLL 위상·주파수 보정을 수행합니다.
- timekeeping_adjust()NTP 오차를
mult에 점진적으로 반영하여 클럭 속도를 조정합니다.
second_overflow()
second_overflow()는 timekeeping_advance()에서 초 경계를 넘을 때마다 호출됩니다. 윤초 삽입/삭제(STA_INS/STA_DEL)를 처리하고, PLL/FLL 알고리즘으로 위상·주파수 오차를 보정합니다.
/* kernel/time/ntp.c — 단순화 */
static void second_overflow(time64_t secs)
{
s64 time_adj;
/* ── 윤초 처리 ── */
switch (time_state) {
case TIME_INS:
if (!(secs % 86400)) { /* 자정 UTC */
time_state = TIME_OOP;
/* 1초 삽입: 23:59:60 표시 */
printk_deferred("Clock: inserting leap second");
}
break;
case TIME_DEL:
if (!((secs + 1) % 86400)) {
time_state = TIME_WAIT;
/* 1초 삭제: 23:59:59 건너뜀 */
printk_deferred("Clock: deleting leap second");
}
break;
}
/* ── PLL/FLL 주파수 보정 ── */
if (time_status & STA_FLL) {
/* FLL: 큰 오프셋에 적합 — 오프셋을 직접 주파수로 변환 */
time_adj = time_offset * FLL_SCALE;
} else {
/* PLL: 작은 오프셋에 적합 — 위상 오차를 시간 상수에 따라 보정 */
time_adj = time_offset >> (time_constant + time_constant);
}
/* 보정 값을 주파수에 누적 */
time_freq += time_adj;
time_freq = min(time_freq, MAXFREQ_SCALED);
time_freq = max(time_freq, -MAXFREQ_SCALED);
/* 갱신된 freq로 tick_length 재계산 */
ntp_update_frequency();
}
코드 설명
second_overflow()는 NTP 보정 루프의 핵심으로, 매초 호출되어 윤초와 주파수 보정을 처리합니다.
- TIME_INS / TIME_DEL윤초 삽입/삭제 상태입니다. UTC 자정에 1초를 추가하거나 삭제합니다. ntpd/chronyd가
STA_INS또는STA_DEL을 설정합니다. - STA_FLL 분기FLL(Frequency Locked Loop)은 큰 오프셋을 빠르게 보정하며, PLL(Phase Locked Loop)은 작은 오프셋을 정밀하게 보정합니다.
time_constant가 6 이하이면 PLL, 초과하면 FLL이 선택됩니다. - time_adj = time_offset >> (time_constant + time_constant)PLL 보정량은 위상 오차를 시간 상수의 제곱으로 나눈 값입니다. 시간 상수가 클수록 보정이 느리고 안정적입니다.
- MAXFREQ_SCALED주파수 보정의 상한(±500ppm)입니다. 이를 초과하는 보정은 잘리며, step 보정이 필요할 수 있음을 의미합니다.
timekeeping_adjust()
timekeeping_adjust()는 NTP 오차(ntp_error)를 clocksource의 mult에 점진적으로 반영합니다. 급격한 mult 변경은 시간 흐름에 지터(jitter)를 유발하므로, 1틱에 최대 ±1씩만 조정하는 점진적 방식을 사용합니다.
/* kernel/time/timekeeping.c — 단순화 */
static void timekeeping_adjust(struct timekeeper *tk, s64 offset)
{
s64 error, interval, adj;
/* NTP tick_length와 실제 tick 길이 차이 누적 */
error = ntp_tick_length() << tk->ntp_error_shift;
error -= (tk->tkr_mono.xtime_nsec + offset) << tk->ntp_error_shift;
tk->ntp_error += error;
/* 오차가 interval의 절반 이상이면 mult ±1 조정 */
interval = tk->cycle_interval << tk->ntp_error_shift;
if (tk->ntp_error > interval >> 1) {
adj = 1; /* 시계가 느림 → mult 증가 */
tk->ntp_error -= interval;
} else if (tk->ntp_error < -(interval >> 1)) {
adj = -1; /* 시계가 빠름 → mult 감소 */
tk->ntp_error += interval;
} else {
return;
}
tk->tkr_mono.mult += adj;
}
코드 설명
timekeeping_adjust()는 NTP 보정을 실제 클럭 속도에 반영하는 마지막 단계입니다.
- ntp_tick_length()NTP가 기대하는 1틱의 길이입니다. 이 값과 실제 틱 길이의 차이가 오차로 누적됩니다.
- ntp_errorNTP 기대 틱 길이와 실제 틱 길이의 누적 오차입니다. 이 값이 임계치를 초과하면
mult를 조정합니다. - mult += adj (±1)한 번에 1만 조정하여 지터를 최소화합니다.
mult가 1 증가하면 1사이클당 나노초가1 / 2^shift만큼 늘어납니다. - interval >> 1오차가 반 interval을 넘어야 조정하는 히스테리시스로, 작은 오차에 의한 불필요한 진동을 방지합니다.
NTP slew vs step 비교
NTP 보정에는 두 가지 방식이 있습니다. slew는 클럭 주파수를 미세 조정하여 점진적으로 시간을 맞추고, step은 시간을 즉시 점프시킵니다. 기본적으로 오프셋이 128ms(chronyd) 또는 0.5초(ntpd)를 초과하면 step이 적용됩니다.
/*
* NTP slew 보정의 커널 내부 동작:
*
* adjtimex(ADJ_FREQUENCY, freq_offset) 호출 시:
* 1. freq_offset은 2^-16 ppm 단위 (65536 = 1 ppm)
* 2. 커널이 clocksource의 mult 값을 조정:
* adjusted_mult = base_mult + (freq_offset * base_mult) / (10^6 * 2^16)
* 3. 매 틱마다 조정된 mult로 ns 계산
* → 클럭이 실제보다 약간 빠르거나 느리게 진행
* 4. 목표 시간에 도달하면 주파수를 원래대로 복원
*
* 예: +500 ppm slew 시
* 1초당 0.5ms 빠르게 진행
* 100ms 오프셋 보정에 ~200초 소요
*/
# chronyd의 step/slew 임계값 설정
# /etc/chrony.conf
makestep 1.0 3
# 의미: 처음 3번의 업데이트에서 오프셋이 1초 이상이면 step,
# 이후에는 항상 slew (시간 역행 방지)
# ntpd의 임계값
# 기본: 0.128초(128ms) 초과 시 step, 이하 시 slew
# -g 옵션: 첫 step 허용 (부팅 직후 큰 오프셋 보정)
# -x 옵션: step 금지 (항상 slew)
# step 발생 감지
$ journalctl -u chronyd | grep -i "system clock"
chronyd: System clock was stepped by -0.123456 seconds
코드 설명
kernel/time/ntp.c와 kernel/time/timekeeping.c가 협력하여 수행하는 NTP slew 보정의 커널 내부 메커니즘입니다.
- adjtimex(ADJ_FREQUENCY)사용자 공간의
ntpd/chronyd가 이 시스템 콜을 통해 주파수 보정값을 커널에 전달합니다.2^-16 ppm단위로 65536이 1 ppm에 해당합니다. - adjusted_mult 계산커널은 clocksource의 기본
mult값에 NTP 주파수 오프셋 비율을 더하여 조정된mult를 산출합니다. 이로써 매 틱의 나노초 변환 결과가 미세하게 달라집니다. - slew 방식
mult를 높이면 시간이 실제보다 빠르게 진행하고, 낮추면 느리게 진행합니다. 예를 들어 +500 ppm slew는 1초당 0.5ms씩 빠르게 진행하여 점진적으로 오프셋을 줄입니다. - step 방식오프셋이 임계값(chronyd 기본 128ms, ntpd 기본 0.5초)을 초과하면
do_settimeofday64()로 시간을 즉시 점프시킵니다. CLOCK_REALTIME이 역행할 수 있어 주의가 필요합니다. - makestep 1.0 3chronyd 설정으로, 처음 3번의 NTP 업데이트에서 오프셋이 1초 이상이면 step을 허용하고, 이후에는 항상 slew만 사용합니다. 서버 환경의 권장 설정입니다.
- 서버/데이터센터 —
makestep 1.0 3: 부팅 초기에만 step 허용, 운영 중에는 slew만 사용 - 금융/HFT —
makestep 0 0+ PTP: step 완전 금지, 나노초 slew만 허용 - 데스크톱/랩톱 —
makestep 1.0 -1: 항상 step 허용 (suspend/resume 후 큰 오프셋 가능) - 컨테이너 — 호스트에서만 NTP, 컨테이너 내부에서는 NTP 데몬 실행 금지 (충돌 방지)
윤초 (Leap Second) 처리
/*
* 윤초 시 CLOCK_REALTIME 동작:
*
* 양의 윤초 (1초 삽입):
* 23:59:59 → 23:59:60 → 00:00:00
* CLOCK_REALTIME이 1초 동안 정지하거나 smear
*
* 음의 윤초 (1초 삭제, 이론적):
* 23:59:58 → 00:00:00 (23:59:59 건너뜀)
*
* 커널 처리 옵션:
* 1. STA_INS/STA_DEL: NTP가 윤초 예고 → 커널이 자정에 처리
* 2. Leap second smearing: 24시간에 걸쳐 1초를 분산 조정
* (Google/Amazon NTP 서버가 제공)
*
* 영향받지 않는 시계:
* - CLOCK_MONOTONIC: 윤초 무관
* - CLOCK_TAI: 윤초 없는 TAI 시간 (REALTIME + tai_offset)
*/
# 현재 TAI 오프셋 확인 (2024년 기준: 37초)
$ adjtimex --print | grep tai
tai: 37
PTP (Precision Time Protocol)
PTP(IEEE 1588v2, 흔히 PTPv2)는 네트워크를 통해 서브 마이크로초~나노초 수준의 시간 동기화를 제공하는 프로토콜입니다. NTP가 밀리초 수준의 정확도를 제공하는 데 비해, PTP는 하드웨어 타임스탬핑을 활용하여 수십 나노초 이내의 동기화를 달성할 수 있습니다. 2002년 IEEE 1588 초판이 발표된 이후, 2008년 IEEE 1588-2008(PTPv2)에서 프로파일 지원, Transparent Clock, Peer Delay 메커니즘이 추가되어 현재 사실상 표준으로 자리잡았습니다. 2019년에는 IEEE 1588-2019가 발표되어 보안 TLV 등이 추가되었습니다.
PTP 적용 분야
| 분야 | 요구 정확도 | 대표 사례 |
|---|---|---|
| 통신 (Telecom) | < 1.5 μs | 5G 프론트홀 (O-RAN), LTE-TDD, CPRI/eCPRI 동기화 |
| 금융 (Finance) | < 1 μs | MiFID II 거래 타임스탬프, HFT (고빈도 거래) |
| 산업 자동화 | < 1 μs | EtherCAT, PROFINET IRT, TSN (Time-Sensitive Networking) |
| 방송 / AV | < 1 μs | SMPTE ST 2059, AES67 오디오, 방송 IP 전환 |
| 전력 시스템 | < 1 μs | IEC 61850 변전소 자동화, Synchrophasor (IEEE C37.118) |
| 데이터센터 | < 100 ns | 분산 데이터베이스 (Spanner), 로그 상관 분석 |
PTP 메시지 교환 (E2E Delay Mechanism)
PTP는 마스터(Master)와 슬레이브(Slave) 사이에 4가지 메시지를 교환하여 오프셋(offset)과 전파 지연(propagation delay)을 계산합니다. Two-step 모드에서 Sync 메시지 전송 후 Follow_Up 메시지로 정확한 t1 타임스탬프를 전달합니다.
PTP 클럭 유형
| 클럭 유형 | 영문 | 설명 | 포트 수 |
|---|---|---|---|
| Ordinary Clock (OC) | Ordinary Clock | 단일 PTP 포트를 가진 최종 노드. Master 또는 Slave로 동작 | 1 |
| Boundary Clock (BC) | Boundary Clock | 다수의 PTP 포트를 가지며, 한 포트는 Slave(업스트림), 나머지는 Master(다운스트림)로 동작. 각 포트에서 PTP 도메인 종단 | 2+ |
| Transparent Clock (TC) | Transparent Clock | PTP 메시지를 전달하면서 체류 시간(residence time)을 correctionField에 누적. PTP 도메인에 참여하지 않음 | 2+ |
| E2E TC | End-to-End TC | Sync, Delay_Req 메시지의 correctionField 업데이트 | - |
| P2P TC | Peer-to-Peer TC | Sync 메시지의 correctionField에 체류 시간 + 링크 지연 반영 | - |
| Grandmaster Clock (GM) | Grandmaster Clock | PTP 도메인의 최상위 시간 소스. GNSS/원자시계 등 외부 기준 시간에 동기화 | 1+ |
PTP 네트워크 토폴로지(Topology)
실제 PTP 배포 환경에서는 Grandmaster Clock이 GNSS 수신기 등으로부터 UTC를 공급받고, Boundary Clock과 Transparent Clock을 거쳐 최종 Ordinary Clock(슬레이브)까지 시간이 전달됩니다.
Best Master Clock Algorithm (BMCA)
PTP 도메인 내에서 Grandmaster를 선출하는 알고리즘입니다. 각 클럭은 Announce 메시지를 통해 자신의 속성을 광고하고, 모든 참여 클럭이 동일한 비교 기준으로 가장 우수한 클럭을 Grandmaster로 결정합니다. 비교는 다음 순서로 진행됩니다:
| 우선순위(Priority) | 필드 | 설명 | 기본값 / 범위 |
|---|---|---|---|
| 1 | priority1 | 관리자가 수동 설정하는 최우선 순위. 값이 작을수록 우선 | 128 (0~255) |
| 2 | clockClass | 클럭의 품질 등급. 6=GPS 동기, 7=GPS 홀드오버, 248=기본 | 248 |
| 3 | clockAccuracy | 클럭 정확도 열거값. 0x21=100ns, 0x22=250ns, 0xFE=불명 | 0xFE |
| 4 | offsetScaledLogVariance | 클럭 안정성 지표 (Allan variance 기반) | 0xFFFF |
| 5 | priority2 | 동률 시 관리자 설정 보조 순위 | 128 (0~255) |
| 6 | clockIdentity | 최종 동률 시 EUI-64 기반 고유 ID로 결정 | MAC 기반 |
/*
* Announce 메시지 핵심 필드 (IEEE 1588-2019 Section 13.5)
*
* Announce 메시지는 기본적으로 1초 간격(logAnnounceInterval=0)으로
* 송신되며, 3회 연속 수신 실패(announceReceiptTimeout=3) 시
* 해당 마스터를 타임아웃 처리합니다.
*
* BMCA 비교 순서:
* 1) priority1 → 2) clockClass → 3) clockAccuracy →
* 4) offsetScaledLogVariance → 5) priority2 → 6) clockIdentity
*/
/* 대표적인 clockClass 값 */
#define PTP_CLOCK_CLASS_PRIMARY_REF 6 /* GPS/GNSS 동기 */
#define PTP_CLOCK_CLASS_PRIMARY_HOLDOVER 7 /* GPS 홀드오버 */
#define PTP_CLOCK_CLASS_DEFAULT 248 /* 기본 (freerun) */
#define PTP_CLOCK_CLASS_SLAVE_ONLY 255 /* 슬레이브 전용 */
PTP 프로파일
PTP는 다양한 산업 분야의 요구에 맞춘 프로파일(profile)을 정의합니다. 각 프로파일은 전송 계층, 지연 측정 방식, 메시지 간격 등을 규격화합니다.
| 프로파일 | 표준 | 전송 계층 | Delay 방식 | 주요 용도 |
|---|---|---|---|---|
| Default E2E | IEEE 1588 | UDP/IPv4, UDP/IPv6, L2 | E2E | 범용 |
| Default P2P | IEEE 1588 | UDP/IPv4, UDP/IPv6, L2 | P2P | 범용 (풀메시 토폴로지) |
| gPTP | IEEE 802.1AS | L2 전용 | P2P | TSN, Automotive Ethernet, AV 브릿지 |
| Telecom (Full) | ITU-T G.8275.1 | L2 (Ethernet) | E2E | 5G 프론트홀, 이동통신 기지국 |
| Telecom (Partial) | ITU-T G.8275.2 | UDP/IPv4, UDP/IPv6 | E2E | PTP 비인식 네트워크 경유 |
| Power | IEEE C37.238 | L2 (Ethernet) | P2P | 전력 변전소 (IEC 61850) |
| SMPTE | SMPTE ST 2059-2 | L2 / UDP | E2E/P2P | 방송 영상/오디오 |
gPTP (IEEE 802.1AS)
gPTP(Generalized PTP)는 IEEE 802.1AS에서 정의한 PTP 프로파일로, TSN(Time-Sensitive Networking) 프레임워크의 핵심 시간 동기화 메커니즘입니다. 표준 PTP와의 주요 차이점은 다음과 같습니다:
| 항목 | 표준 PTP (IEEE 1588) | gPTP (IEEE 802.1AS) |
|---|---|---|
| 전송 계층 | L2, UDP/IPv4, UDP/IPv6 | L2 전용 (EtherType 0x88F7) |
| Delay 방식 | E2E 또는 P2P | P2P 전용 (Peer Delay) |
| 스코프 | 라우팅 가능 | 링크-로컬 (01:80:C2:00:00:0E) |
| Best Master | BMCA | BTCA (Best Time-aware Clock Algorithm, 약간 다름) |
| Sync 간격 | 가변 (프로파일 의존) | 125ms 기본 (logSyncInterval = -3) |
| 토폴로지 인식 | 선택적 | 필수 (Signaling TLV로 역할 협상) |
| 주요 적용 | 범용 | Automotive Ethernet, Pro-AV, 산업 TSN |
Peer Delay 메커니즘 (P2P)
Peer Delay는 인접 노드 간의 링크 지연을 직접 측정합니다. E2E 방식과 달리, 마스터까지의 전체 경로가 아닌 각 홉(hop)별 지연을 독립적으로 측정하므로 Transparent Clock이나 Boundary Clock 환경에서 더 정확한 결과를 제공합니다.
하드웨어 타임스탬핑 아키텍처
PTP의 나노초 정확도는 하드웨어 타임스탬핑에 의존합니다. 소프트웨어 타임스탬핑은 커널 네트워크 스택(Network Stack)의 지연(수십 마이크로초)을 포함하므로 정밀도가 떨어집니다. NIC 또는 PHY 수준에서 패킷(Packet)의 실제 송수신 시점을 기록하면 이러한 소프트웨어 지터를 제거할 수 있습니다.
| 타임스탬핑 위치 | 정확도 | 설명 |
|---|---|---|
| PHY 타임스탬핑 | 최고 (~ ns) | PHY 칩이 선로에 가장 가까운 지점에서 타임스탬프. MAC 지연 제거 |
| MAC 타임스탬핑 | 높음 (수~수십 ns) | NIC MAC 블록에서 타임스탬프. PHY 지연이 남지만 소프트웨어보다 월등 |
| 소프트웨어 타임스탬핑 | 낮음 (수~수십 μs) | 커널 드라이버 또는 소켓(Socket) 계층에서 타임스탬프. 스케줄링 지터 포함 |
커널 PTP API
Linux 커널은 include/linux/ptp_clock_kernel.h에 PTP Hardware Clock 서브시스템을 정의합니다.
NIC 드라이버는 ptp_clock_info 구조체에 콜백(Callback)을 채워 ptp_clock_register()로 등록합니다.
등록된 PHC는 /dev/ptpN 캐릭터 디바이스와 /sys/class/ptp/ptpN/ sysfs 노드로 노출됩니다.
/* include/linux/ptp_clock_kernel.h — 핵심 구조체 */
struct ptp_clock_info {
struct module *owner;
char name[32];
s32 max_adj; /* 최대 주파수 조정 (ppb 단위) */
int n_alarm; /* 알람 채널 수 */
int n_ext_ts; /* 외부 타임스탬프 입력 채널 수 */
int n_per_out; /* 주기적 출력 채널 수 */
int n_pins; /* 프로그래밍 가능 핀 수 */
int pps; /* PPS(Pulse Per Second) 지원 여부 */
struct ptp_pin_desc *pin_config; /* 핀 설정 배열 */
/* ── 주파수 조정 ── */
int (*adjfine)(struct ptp_clock_info *ptp, long scaled_ppm);
/* scaled_ppm: ppb 단위 × 2^16 스케일.
* 예: +1 ppm = +65536, -0.5 ppm = -32768 */
/* ── 시간 점프 ── */
int (*adjtime)(struct ptp_clock_info *ptp, s64 delta);
/* 나노초 단위 시간 오프셋 적용 (양수: 앞으로, 음수: 뒤로) */
/* ── 시간 읽기/쓰기 ── */
int (*gettime64)(struct ptp_clock_info *ptp,
struct timespec64 *ts);
int (*settime64)(struct ptp_clock_info *ptp,
const struct timespec64 *ts);
int (*gettimex64)(struct ptp_clock_info *ptp,
struct timespec64 *ts,
struct ptp_system_timestamp *sts);
/* gettimex64: PHC 시간과 시스템 시간의 교차 타임스탬프 쌍 반환 */
/* ── 교차 타임스탬핑 ── */
int (*getcrosststamp)(struct ptp_clock_info *ptp,
struct system_device_crosststamp *cts);
/* PTM(Precision Time Measurement) 등으로 PHC↔시스템 시간을
* 하드웨어적으로 동시 캡처. phc2sys 대비 더 높은 정확도 */
/* ── 외부 이벤트/주기 출력 제어 ── */
int (*enable)(struct ptp_clock_info *ptp,
struct ptp_clock_request *rq, int on);
/* PTP_CLK_REQ_EXTTS: 외부 타임스탬프 캡처 활성화
* PTP_CLK_REQ_PEROUT: 주기적 펄스 출력 활성화
* PTP_CLK_REQ_PPS: PPS 출력 활성화 */
/* ── 핀 기능 설정 ── */
int (*verify)(struct ptp_clock_info *ptp,
unsigned int pin, enum ptp_pin_function func,
unsigned int chan);
};
/* PHC 등록/해제 API */
struct ptp_clock *ptp_clock_register(struct ptp_clock_info *info,
struct device *parent);
int ptp_clock_unregister(struct ptp_clock *ptp);
int ptp_clock_index(struct ptp_clock *ptp); /* /dev/ptpN의 N 반환 */
/* 이벤트 보고 — 외부 타임스탬프, PPS 등 */
void ptp_clock_event(struct ptp_clock *ptp,
struct ptp_clock_event *event);
/* 이벤트 유형 */
struct ptp_clock_event {
enum ptp_clock_events type; /* PTP_CLOCK_EXTTS, PTP_CLOCK_PPS, ... */
int index; /* 채널 인덱스 */
union {
struct timespec64 timestamp;
struct ptp_clock_time pct;
};
};
PTP 드라이버 구현 예제
실제 NIC 드라이버에서 PTP를 지원하기 위한 최소 골격 코드입니다. Intel ixgbe, igb, ice 드라이버 등이 이 패턴을 따릅니다.
#include <linux/ptp_clock_kernel.h>
#include <linux/net_tstamp.h>
struct my_adapter {
struct ptp_clock *ptp_clock;
struct ptp_clock_info ptp_caps;
spinlock_t tmreg_lock;
struct cyclecounter cc;
struct timecounter tc;
u32 tstamp_config;
};
/* ── adjfine: 주파수 미세 조정 ── */
static int my_ptp_adjfine(struct ptp_clock_info *ptp,
long scaled_ppm)
{
struct my_adapter *adapter =
container_of(ptp, struct my_adapter, ptp_caps);
u64 adj;
bool neg = false;
if (scaled_ppm < 0) {
neg = true;
scaled_ppm = -scaled_ppm;
}
/* NIC 고유 주파수 조정 레지스터에 값 기록 */
adj = (u64)scaled_ppm * adapter->cc.mult;
adj >>= 16; /* scaled_ppm은 ppb × 2^16 */
spin_lock(&adapter->tmreg_lock);
timecounter_read(&adapter->tc);
adapter->cc.mult = neg ?
adapter->cc.mult - (u32)adj :
adapter->cc.mult + (u32)adj;
spin_unlock(&adapter->tmreg_lock);
return 0;
}
/* ── adjtime: 시간 점프 ── */
static int my_ptp_adjtime(struct ptp_clock_info *ptp,
s64 delta)
{
struct my_adapter *adapter =
container_of(ptp, struct my_adapter, ptp_caps);
spin_lock(&adapter->tmreg_lock);
timecounter_adjtime(&adapter->tc, delta);
spin_unlock(&adapter->tmreg_lock);
return 0;
}
/* ── gettime64: PHC 현재 시간 읽기 ── */
static int my_ptp_gettime64(struct ptp_clock_info *ptp,
struct timespec64 *ts)
{
struct my_adapter *adapter =
container_of(ptp, struct my_adapter, ptp_caps);
u64 ns;
spin_lock(&adapter->tmreg_lock);
ns = timecounter_read(&adapter->tc);
spin_unlock(&adapter->tmreg_lock);
*ts = ns_to_timespec64(ns);
return 0;
}
/* ── settime64: PHC 시간 설정 ── */
static int my_ptp_settime64(struct ptp_clock_info *ptp,
const struct timespec64 *ts)
{
struct my_adapter *adapter =
container_of(ptp, struct my_adapter, ptp_caps);
u64 ns = timespec64_to_ns(ts);
spin_lock(&adapter->tmreg_lock);
timecounter_init(&adapter->tc, &adapter->cc, ns);
spin_unlock(&adapter->tmreg_lock);
return 0;
}
/* ── enable: 외부 타임스탬프/주기적 출력 제어 ── */
static int my_ptp_enable(struct ptp_clock_info *ptp,
struct ptp_clock_request *rq, int on)
{
struct my_adapter *adapter =
container_of(ptp, struct my_adapter, ptp_caps);
switch (rq->type) {
case PTP_CLK_REQ_EXTTS:
/* 외부 이벤트(예: 1PPS 입력) 캡처 활성화/비활성화 */
if (on)
my_hw_enable_extts(adapter, rq->extts.index);
else
my_hw_disable_extts(adapter, rq->extts.index);
return 0;
case PTP_CLK_REQ_PEROUT:
/* 주기적 펄스 출력 (예: 10MHz, 1PPS) 활성화/비활성화 */
if (on)
my_hw_enable_perout(adapter, &rq->perout);
else
my_hw_disable_perout(adapter, &rq->perout);
return 0;
case PTP_CLK_REQ_PPS:
/* PPS (Pulse Per Second) 출력 */
return 0;
default:
return -EOPNOTSUPP;
}
}
/* ── PHC 등록 ── */
static void my_ptp_init(struct my_adapter *adapter)
{
adapter->ptp_caps = (struct ptp_clock_info) {
.owner = THIS_MODULE,
.name = "my_nic_phc",
.max_adj = 500000000, /* 500 ppm */
.n_alarm = 0,
.n_ext_ts = 2, /* 외부 TS 2채널 */
.n_per_out = 1, /* 주기 출력 1채널 */
.n_pins = 3,
.pps = 1,
.adjfine = my_ptp_adjfine,
.adjtime = my_ptp_adjtime,
.gettime64 = my_ptp_gettime64,
.settime64 = my_ptp_settime64,
.enable = my_ptp_enable,
};
adapter->ptp_clock = ptp_clock_register(
&adapter->ptp_caps, &adapter->pdev->dev);
if (IS_ERR(adapter->ptp_clock)) {
dev_err(&adapter->pdev->dev,
"ptp_clock_register failed\n");
adapter->ptp_clock = NULL;
}
}
/* ── PHC 해제 ── */
static void my_ptp_remove(struct my_adapter *adapter)
{
if (adapter->ptp_clock) {
ptp_clock_unregister(adapter->ptp_clock);
adapter->ptp_clock = NULL;
}
}
PHC (PTP Hardware Clock)
PTP 지원 NIC는 자체 하드웨어 클럭(PHC)을 내장합니다.
PHC는 패킷의 정확한 송수신 타임스탬프를 하드웨어 수준에서 기록하여
소프트웨어 지연에 의한 오차를 제거합니다. Linux에서 PHC는
/dev/ptpN 장치로 노출됩니다.
# PHC 장치 확인
$ ls /dev/ptp*
/dev/ptp0
# PHC 정보 (ethtool)
$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
hardware-transmit (SOF_TIMESTAMPING_TX_HARDWARE)
hardware-receive (SOF_TIMESTAMPING_RX_HARDWARE)
hardware-raw-clock (SOF_TIMESTAMPING_RAW_HARDWARE)
software-transmit (SOF_TIMESTAMPING_TX_SOFTWARE)
software-receive (SOF_TIMESTAMPING_RX_SOFTWARE)
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
on
Hardware Receive Filter Modes:
all
PTP Userspace 도구
linuxptp 패키지는 PTP 운용에 필요한 핵심 유저스페이스 도구를 제공합니다.
ptp4l — PTP 데몬
마스터/슬레이브 자동 협상(BMCA) 및 시간 동기화를 수행하는 핵심 데몬입니다.
# 기본 슬레이브 모드 실행
$ ptp4l -i eth0 -s -m
ptp4l[5678]: master offset 3 s2 freq -567 path delay 800
# 설정 파일을 사용한 실행
$ ptp4l -f /etc/ptp4l.conf -m
# /etc/ptp4l.conf 예제
[global]
twoStepFlag 1
tx_timestamp_timeout 10
logSyncInterval -3 # 125ms 간격
logAnnounceInterval 1 # 2초 간격
logMinDelayReqInterval 0 # 1초 간격
announceReceiptTimeout 3
priority1 128
priority2 128
domainNumber 0
slaveOnly 0
clock_servo pi # PI 서보 (기본)
pi_proportional_const 0.0 # 자동 조정
pi_integral_const 0.0 # 자동 조정
[eth0]
delay_mechanism E2E # 또는 P2P
network_transport UDPv4 # 또는 L2, UDPv6
phc2sys — PHC ↔ 시스템 클럭 동기화
ptp4l이 PHC를 마스터에 동기화한 후, phc2sys는 PHC의 시간을
시스템 클럭(CLOCK_REALTIME)에 반영합니다.
# PHC → CLOCK_REALTIME 동기화 (-O 0: UTC 오프셋 없음)
$ phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 0 -m
phc2sys[1234]: CLOCK_REALTIME phc offset -5 s2 freq -1234 delay 500
# ptp4l과 자동 연동 (-a: automatic 모드)
$ phc2sys -a -r -m
# -a: ptp4l UDS에서 마스터 포트 정보 자동 획득
# -r: CLOCK_REALTIME을 슬레이브로 설정
# PHC 간 동기화 (멀티 NIC)
$ phc2sys -s /dev/ptp0 -c /dev/ptp1 -O 0 -m
ts2phc — 외부 시간 소스 → PHC 동기화
# GNSS 수신기의 1PPS → PHC 동기화
$ ts2phc -s nmea -c eth0 -m
# NMEA 직렬 데이터 + 1PPS 신호로 PHC 초기화
# /etc/ts2phc.conf 예제
[global]
use_syslog 1
ts2phc.nmea_serialport /dev/ttyS0
ts2phc.pulsewidth 100000000 # 100ms
[eth0]
ts2phc.pin_index 0
pmc — PTP Management Client
# 현재 PTP 상태 조회
$ pmc -u -b 0 'GET CURRENT_DATA_SET'
stepsRemoved 1
offsetFromMaster -3.0
meanPathDelay 800.0
# 마스터 정보 조회
$ pmc -u -b 0 'GET PARENT_DATA_SET'
parentPortIdentity 001122.fffe.334455-1
grandmasterIdentity 001122.fffe.334455
grandmasterClockClass 6
grandmasterPriority1 128
# 포트 상태 조회
$ pmc -u -b 0 'GET PORT_DATA_SET'
portState SLAVE
logSyncInterval -3
delayMechanism E2E
# 실시간 priority1 변경 (Grandmaster 전환 유도)
$ pmc -u -b 0 'SET PRIORITY1 100'
PTP와 Linux 네트워킹 스택
어플리케이션은 SO_TIMESTAMPING 소켓 옵션을 통해 하드웨어/소프트웨어 타임스탬프를 수신합니다.
SIOCSHWTSTAMP ioctl로 NIC의 타임스탬핑 모드를 설정하고, 커널은
struct scm_timestamping을 ancillary data로 전달합니다.
/* SO_TIMESTAMPING 소켓 옵션 — 타임스탬프 요청 */
#include <linux/net_tstamp.h>
int flags = SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RAW_HARDWARE |
SOF_TIMESTAMPING_OPT_CMSG;
setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
&flags, sizeof(flags));
/* SIOCSHWTSTAMP — 하드웨어 타임스탬핑 활성화 */
struct hwtstamp_config cfg = {
.tx_type = HWTSTAMP_TX_ON,
.rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT,
};
struct ifreq ifr;
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
ifr.ifr_data = (void *)&cfg;
ioctl(sock, SIOCSHWTSTAMP, &ifr);
/* 타임스탬프 수신 — recvmsg() ancillary data */
struct msghdr msg = { 0 };
char ctrl[CMSG_SPACE(sizeof(struct scm_timestamping))];
msg.msg_control = ctrl;
msg.msg_controllen = sizeof(ctrl);
recvmsg(sock, &msg, 0);
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm;
cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_type == SO_TIMESTAMPING) {
struct scm_timestamping *ts =
(struct scm_timestamping *)CMSG_DATA(cm);
/*
* ts->ts[0] = 소프트웨어 타임스탬프
* ts->ts[1] = (deprecated, 사용 안 함)
* ts->ts[2] = 하드웨어 타임스탬프 (RAW_HARDWARE)
*/
printf("HW TS: %lld.%09ld\n",
(long long)ts->ts[2].tv_sec,
ts->ts[2].tv_nsec);
}
}
코드 설명
PTP 하드웨어 타임스탬핑을 위한 SO_TIMESTAMPING 소켓 옵션 설정과 타임스탬프 수신 코드입니다. include/uapi/linux/net_tstamp.h에 정의된 플래그를 사용합니다.
- SOF_TIMESTAMPING_TX_HARDWARE송신(TX) 패킷의 하드웨어 타임스탬프를 요청합니다. NIC의 MAC/PHY가 패킷이 물리 매체에 실제 전송되는 순간을 기록합니다.
- SOF_TIMESTAMPING_RX_HARDWARE수신(RX) 패킷의 하드웨어 타임스탬프를 요청합니다. 패킷이 NIC에 도착하는 물리적 순간을 나노초 정밀도로 기록합니다.
- SOF_TIMESTAMPING_RAW_HARDWARE하드웨어 타임스탬프를 원시 형태(PHC 시간 기준)로 반환하도록 요청합니다.
ts->ts[2]에 저장됩니다. - SIOCSHWTSTAMP ioctlNIC 드라이버에 하드웨어 타임스탬핑을 활성화하는 ioctl입니다.
tx_type과rx_filter로 어떤 패킷에 타임스탬프를 찍을지 지정합니다. - HWTSTAMP_FILTER_PTP_V2_EVENTPTP v2 이벤트 메시지(Sync, Delay_Req 등)만 하드웨어 타임스탬핑 대상으로 필터링합니다. 모든 패킷에 타임스탬프를 찍으면 성능에 영향을 줄 수 있습니다.
- recvmsg() ancillary data
SO_TIMESTAMPING타입의 보조 메시지(control message)로 타임스탬프를 수신합니다.scm_timestamping구조체의ts[0]은 소프트웨어,ts[2]은 하드웨어 타임스탬프입니다.
Linux PTP 소프트웨어 스택
PTP 전송 계층 (IPv4/IPv6/L2)
| 항목 | UDP/IPv4 | UDP/IPv6 | IEEE 802.3 (L2) |
|---|---|---|---|
| EtherType | 0x0800 (IP) | 0x86DD (IPv6) | 0x88F7 (PTP) |
| Event 포트 | UDP 319 | UDP 319 | 해당 없음 |
| General 포트 | UDP 320 | UDP 320 | 해당 없음 |
| E2E 멀티캐스트 | 224.0.1.129 | FF0x::181 | 01:1B:19:00:00:00 |
| P2P 멀티캐스트 | 224.0.0.107 | FF02::6B | 01:80:C2:00:00:0E |
| 라우팅 가능 | 가능 | 가능 | 불가 (L2 스코프) |
| 주요 프로파일 | Default, G.8275.2 | Default | gPTP, G.8275.1, C37.238 |
PTP 디버깅 및 모니터링
# ── ethtool: NIC 타임스탬핑 능력 확인 ──
$ ethtool -T eth0
# hardware-transmit/receive가 표시되지 않으면 HW TS 미지원
# ── sysfs: PHC 정보 조회 ──
$ cat /sys/class/ptp/ptp0/clock_name
my_nic_phc
$ cat /sys/class/ptp/ptp0/max_adjustment
500000000
$ cat /sys/class/ptp/ptp0/n_pins
3
$ cat /sys/class/ptp/ptp0/pps_available
1
# ── phc_ctl: PHC 직접 제어 ──
$ phc_ctl /dev/ptp0 get
clock time is 1709000000.123456789
$ phc_ctl /dev/ptp0 cmp # PHC vs 시스템 클럭 비교
offset from CLOCK_REALTIME is -125ns
# ── pmc: PTP 관리 명령 ──
$ pmc -u -b 0 'GET TIME_STATUS_NP'
master_offset -3
ingress_time 1709000000123456789
cumulativeScaledRateOffset +0.000000000
gmPresent true
gmIdentity 001122.fffe.334455
# ── 커널 tracepoint (ftrace) ──
$ echo 1 > /sys/kernel/debug/tracing/events/ptp/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
ptp0: ptp_clock_event: type=EXTTS index=0 t=1709000001.000000123
# ── tcpdump: PTP 패킷 캡처 ──
$ tcpdump -i eth0 -nn 'udp port 319 or udp port 320'
12:00:00.000 IP 10.0.0.1.319 > 224.0.1.129.319: PTPv2 Sync seq=1234
# L2 PTP 캡처
$ tcpdump -i eth0 -nn 'ether proto 0x88f7'
- 비대칭 경로(Asymmetric Path): 업링크/다운링크 지연이 다르면 오프셋 계산에 오차가 발생합니다.
광케이블 길이 차이, 스위치 큐잉 비대칭 등이 원인이며,
delayAsymmetry설정으로 보정해야 합니다. - 스위치/라우터 지연: PTP 비인식(non-PTP-aware) 스위치는 수십~수백 마이크로초의 가변 지연을 추가합니다. 반드시 Boundary Clock 또는 Transparent Clock 기능이 있는 PTP-aware 스위치를 사용하십시오.
- 방화벽(Firewall) 멀티캐스트 차단: PTP는 멀티캐스트(224.0.1.129, 224.0.0.107)를 사용합니다. 방화벽에서 UDP 319/320 및 해당 멀티캐스트 그룹을 허용해야 합니다. L2 모드는 EtherType 0x88F7 허용 필요.
- VLAN 환경: VLAN 태깅이 PHY 타임스탬핑 위치에 영향을 줄 수 있습니다. VLAN 태그 삽입/제거가 타임스탬프 지점 이전/이후인지 확인하십시오.
- 가상화 환경: VM에서 PTP를 사용할 경우 하드웨어 타임스탬핑이 불가능합니다. SR-IOV VF 패스스루 또는 virtio-net의 소프트웨어 타임스탬핑을 사용해야 합니다.
logSyncInterval = -3(125ms)에서 시작하여 안정성 확인 후-4(62.5ms)로 단축tx_timestamp_timeout을 NIC 응답 시간에 맞게 조정 (기본 10ms가 부족할 수 있음)- 서보 필터 파라미터(
pi_proportional_const,pi_integral_const)를 네트워크 지터에 맞게 튜닝 - PHC-to-system 동기화 시
phc2sys -R 100(100Hz 폴링(Polling))으로 반응 속도 향상 - 멀티 NIC 환경에서는
phc2sys로 PHC 간 동기화 후 대표 PHC 하나를 시스템 클럭에 연동
지연 함수 (Delay/Sleep)
커널에서 시간 지연은 컨텍스트에 따라 적절한 함수를 선택해야 합니다. 잘못된 지연 함수 사용은 성능 저하나 시스템 행(hang)을 유발합니다.
지연 함수 레퍼런스
| 함수 | 범위 | 방식 | 컨텍스트 | 정밀도 |
|---|---|---|---|---|
ndelay(ns) | 1-999 ns | busy-wait | 모든 (atomic OK) | ~ns |
udelay(us) | 1-999 us | busy-wait | 모든 (atomic OK) | ~us |
mdelay(ms) | 1+ ms | busy-wait (반복 udelay) | 모든 (atomic OK) | ~ms |
usleep_range(min, max) | 10+ us | hrtimer sleep | 프로세스만 | ~us |
msleep(ms) | 1+ ms | timer sleep | 프로세스만 | ~1/HZ |
msleep_interruptible(ms) | 1+ ms | timer sleep | 프로세스만 | ~1/HZ |
ssleep(s) | 1+ s | timer sleep | 프로세스만 | ~1/HZ |
fsleep(us) | 자동 선택 | 범위에 따라 자동 | 프로세스만 | 최적 |
지연 함수 상세
#include <linux/delay.h>
/* ===== Busy-wait 지연 (인터럽트/atomic 컨텍스트 OK) ===== */
ndelay(500); /* 500 나노초 바쁜 대기 */
udelay(100); /* 100 마이크로초 바쁜 대기 */
mdelay(10); /* 10 밀리초 바쁜 대기 — 가능하면 피하라! */
/* 주의: udelay는 내부적으로 TSC/루프 기반 바쁜 대기.
* 부팅 시 calibrate_delay()로 loops_per_jiffy를 계산하여 교정.
* udelay(1000) 이상은 mdelay(1) 사용 권장 (오버플로 방지) */
/* ===== Sleep 지연 (프로세스 컨텍스트에서만) ===== */
usleep_range(500, 1000); /* 500-1000us 범위 슬립 (hrtimer 기반)
* min~max 범위를 지정하여 타이머 합산(coalescing) 허용
* → 전력 효율적 */
msleep(20); /* 최소 20ms 슬립
* schedule_timeout 기반 — 실제 해상도 1/HZ
* HZ=250이면 최소 4ms 단위 */
msleep_interruptible(100); /* 시그널로 깨어날 수 있는 슬립 */
ssleep(1); /* 1초 슬립 (= msleep(1000)) */
/* ===== fsleep — 범위에 따라 최적 함수 자동 선택 (v5.8+) ===== */
fsleep(500); /* < 10us → udelay(500) */
fsleep(50); /* 10us-20ms → usleep_range(50, 2*50) */
fsleep(50000); /* > 20ms → msleep(50) */
/*
* fsleep 내부 구현:
* if (usecs <= 10)
* udelay(usecs);
* else if (usecs <= 20000)
* usleep_range(usecs, 2 * usecs);
* else
* msleep(DIV_ROUND_UP(usecs, 1000));
*/
- atomic/인터럽트 컨텍스트:
udelay()만 사용 가능.msleep()은 스케줄링이 필요하므로 deadlock 발생 - 10us 미만:
udelay()— hrtimer 오버헤드(Overhead)보다 바쁜 대기(Busy Wait)가 효율적 - 10us~20ms:
usleep_range()— hrtimer 기반으로 CPU를 양보(Yield)하면서 정밀 대기 - 20ms 이상:
msleep()— 틱 기반이지만 충분히 큰 단위에서는 적절 - 범위 불확실:
fsleep()— 자동 선택으로 안전 - mdelay()는 최후의 수단: CPU를 ms 단위로 점유하므로 시스템 응답성 저하
흔한 지연 함수 실수
/* 잘못된 코드 — 인터럽트 핸들러에서 msleep() */
static irqreturn_t bad_isr(int irq, void *data) {
msleep(10); /* BUG! 스케줄링 불가 → BUG_ON() or hang */
return IRQ_HANDLED;
}
/* 올바른 코드 */
static irqreturn_t good_isr(int irq, void *data) {
udelay(100); /* OK: busy-wait는 모든 컨텍스트에서 안전 */
return IRQ_HANDLED;
}
/* 잘못된 코드 — udelay(10000)은 10ms 동안 CPU 점유! */
udelay(10000); /* 10ms busy-wait → 시스템 응답성 저하 */
/* 올바른 코드 — 프로세스 컨텍스트라면 sleep 사용 */
usleep_range(10000, 12000); /* CPU 양보하면서 10-12ms 대기 */
/* 비효율적 — min == max는 타이머 합산(coalescing) 불가 */
usleep_range(1000, 1000);
/* 권장 — 20% 이상 여유를 두어 타이머 합산 허용 */
usleep_range(1000, 1200); /* 전력 효율 향상 */
실제 커널 사용 패턴
/* ===== 패턴 1: 하드웨어 레지스터 폴링 (atomic 컨텍스트) ===== */
static int hw_wait_ready(void __iomem *base, unsigned int timeout_us)
{
unsigned int elapsed = 0;
while (!(readl(base + STATUS_REG) & READY_BIT)) {
if (elapsed >= timeout_us)
return -ETIMEDOUT;
udelay(1);
elapsed++;
}
return 0;
}
/* ===== 패턴 2: readx_poll_timeout() 매크로 (권장) ===== */
#include <linux/iopoll.h>
/* 프로세스 컨텍스트: usleep_range 기반 */
ret = readl_poll_timeout(base + STATUS_REG, val,
val & READY_BIT,
100, /* 100us 폴링 간격 */
10000); /* 10ms 타임아웃 */
/* atomic 컨텍스트: udelay 기반 */
ret = readl_poll_timeout_atomic(base + STATUS_REG, val,
val & READY_BIT,
1, /* 1us 폴링 간격 */
1000); /* 1ms 타임아웃 */
/* ===== 패턴 3: 드라이버 초기화에서 안정 대기 ===== */
static int my_device_init(struct platform_device *pdev)
{
/* 리셋 펄스 → 하드웨어 안정 대기 → 레디 확인 */
writel(RESET_BIT, base + CTRL_REG);
udelay(10); /* 리셋 펄스 유지 (하드웨어 요구) */
writel(0, base + CTRL_REG);
fsleep(1000); /* 1ms 안정 대기 (fsleep이 자동 선택) */
return readl_poll_timeout(base + STATUS_REG, val,
val & READY_BIT, 100, 50000);
}
고급 지연 패턴
패턴 1: readx_poll_timeout() 커스텀 읽기 함수 사용
readl_poll_timeout()은 MMIO 레지스터 전용이지만, readx_poll_timeout()은 임의의 읽기 함수를 사용할 수 있습니다. SPI/I2C 전송처럼 복잡한 폴링 절차에 유용합니다.
/* SPI 장치 상태를 커스텀 함수로 폴링 */
static int spi_read_status(struct spi_device *spi)
{
u8 cmd = CMD_READ_STATUS;
u8 val;
int ret = spi_write_then_read(spi, &cmd, 1, &val, 1);
return ret ? : val;
}
static int wait_spi_flash_ready(struct spi_device *spi)
{
int val;
/* 50μs 간격 폴링, 최대 500ms 대기 */
return readx_poll_timeout(spi_read_status, spi, val,
!(val & STATUS_WIP),
50, /* 50μs sleep */
500000); /* 500ms timeout */
}
코드 설명
readx_poll_timeout()은 매크로로, 첫 인자로 읽기 함수를 받아 반복 호출합니다.
- readx_poll_timeout(op, args, val, cond, sleep_us, timeout_us)
op(args)를 호출해 결과를val에 저장하고,cond이 참이면 0을 반환합니다. 타임아웃 시-ETIMEDOUT을 반환합니다. - spi_read_status()SPI 트랜잭션으로 장치 상태 레지스터를 읽는 커스텀 함수입니다. MMIO
readl()대신 이런 함수를 사용할 수 있는 것이readx_poll_timeout()의 장점입니다.
패턴 2: wait_event_timeout() + ktime 정밀 하이브리드
하드웨어 인터럽트로 깨어나는 이벤트 대기에 wait_event_timeout()을 사용하면서, 실제 소요 시간을 ktime으로 정밀 측정하는 하이브리드 패턴입니다.
/* 인터럽트 기반 대기 + ktime 지연 측정 */
static int wait_hw_completion(struct my_device *dev)
{
ktime_t t0 = ktime_get();
unsigned long left;
s64 latency_us;
/* 인터럽트 핸들러가 dev->done을 설정, 최대 100ms 대기 */
left = wait_event_timeout(dev->wq,
READ_ONCE(dev->done),
msecs_to_jiffies(100));
latency_us = ktime_us_delta(ktime_get(), t0);
if (!left) {
dev_err(dev->dev, "HW timeout after %lld us\n", latency_us);
return -ETIMEDOUT;
}
dev_dbg(dev->dev, "HW done in %lld us\n", latency_us);
return 0;
}
코드 설명
이벤트 기반 대기와 정밀 시간 측정을 결합한 패턴입니다.
- wait_event_timeout()jiffies 기반(~4ms 해상도) 타임아웃이지만, CPU를 양보하며 인터럽트로 즉시 깨어날 수 있어 전력 효율적입니다.
- ktime_us_delta()나노초 해상도 ktime으로 실제 경과 시간을 정밀 측정합니다.
wait_event_timeout()의 반환값(남은 jiffies)보다 훨씬 정확합니다. - 하이브리드 장점대기 자체는
wait_event_timeout()으로 전력 효율적으로 하고, 측정은 ktime으로 정밀하게 하여 두 가지 장점을 모두 취합니다.
Time Namespace
Time namespace(v5.6+)는 컨테이너별 CLOCK_MONOTONIC, CLOCK_BOOTTIME 오프셋을 분리해 "컨테이너가 보는 경과 시간 기준"을 독립적으로 만듭니다. 핵심은 REALTIME을 바꾸지 않고 경과 시간 축에만 offset을 더한다는 점입니다.
영향 범위와 제약
| 시계 | Time Namespace 영향 | 설명 |
|---|---|---|
CLOCK_MONOTONIC | O | namespace 오프셋 적용 |
CLOCK_BOOTTIME | O | suspend 포함 경과 시간에도 오프셋 적용 |
CLOCK_REALTIME | X | 시스템 전체 공유 벽시계 |
CLOCK_MONOTONIC_RAW | X | 원시 하드웨어 기준 그대로 |
# 현재 namespace 오프셋
$ cat /proc/self/timens_offsets
monotonic 0 0
boottime 0 0
# 새 time namespace + pid namespace를 함께 생성
$ unshare --time --pid --fork --mount-proc bash
# 생성 직후에만 오프셋 설정 가능
$ echo "monotonic 86400 0" > /proc/self/timens_offsets
$ echo "boottime 86400 0" > /proc/self/timens_offsets
체크포인트(Checkpoint)/복원 시나리오
/*
* 컨테이너 복원 시 MONOTONIC 연속성 유지 절차(개념):
* 1) 체크포인트 시점의 monotonic/boottime 스냅샷 저장
* 2) 복원 대상 호스트의 현재 monotonic/boottime 읽기
* 3) delta = checkpoint_value - restore_host_value 계산
* 4) timens_offsets에 delta 기록
* 5) 복원된 프로세스는 기존 경과 시간 기준을 그대로 관찰
*/
시간 관련 디버깅
시간 문제는 단일 원인보다 계층 간 상호작용으로 발생하는 경우가 많습니다. 따라서 "증상 → clocksource 확인 → 동기화 상태 확인 → 스케줄링/전력 상태 확인 → 트레이스" 순서로 범위를 좁혀야 재현성과 해결 속도가 올라갑니다.
일반적인 문제와 해결
| 증상 | 원인 | 진단 | 해결 |
|---|---|---|---|
| 시간이 갑자기 점프 | NTP step 보정, settimeofday() | dmesg에서 clock set 확인 | CLOCK_MONOTONIC 사용 |
| TSC unstable 경고 | CPU 주파수 변동, C-state 문제 | dmesg | grep -i tsc | tsc=reliable 또는 nohz=off |
clock_gettime() 느림 | HPET/ACPI PM clocksource 사용 | clocksource 확인 | TSC로 변경 |
| suspend 후 타이머 폭발 | CLOCK_MONOTONIC 기반 타이머 | BOOTTIME vs MONOTONIC | CLOCK_BOOTTIME 사용 |
| VM에서 시간 드리프트 | vCPU 스케줄링 지연 | chronyc tracking | kvm-clock + 게스트 NTP |
| 윤초 시 시스템 이상 | CLOCK_REALTIME 점프 | adjtimex tai 필드 | CLOCK_TAI 또는 smeared NTP |
usleep_range()가 예상보다 오래 | HZ 해상도, 시스템 부하 | ftrace 타이머 트레이싱 | 범위 조정, hrtimer 확인 |
디버깅 명령 모음
# ============ Clocksource 확인 ============
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
# ============ 시간 해상도 ============
$ cat /proc/timer_list | head -20
Timer List Version: v0.9
HRTIMER_MAX_CLOCK_BASES: 8
now at 123456789012345 nsecs
# clock_getres()로 각 시계 해상도 확인
$ python3 -c "
import time
for c in ['CLOCK_REALTIME','CLOCK_MONOTONIC','CLOCK_MONOTONIC_RAW',
'CLOCK_BOOTTIME','CLOCK_MONOTONIC_COARSE']:
r = time.clock_getres(getattr(time, c))
print(f'{c}: {r*1e9:.0f} ns')
"
CLOCK_REALTIME: 1 ns
CLOCK_MONOTONIC: 1 ns
CLOCK_MONOTONIC_RAW: 1 ns
CLOCK_BOOTTIME: 1 ns
CLOCK_MONOTONIC_COARSE: 4000000 ns
# ============ NTP/PTP 상태 ============
$ chronyc tracking # NTP 동기화 상태
$ chronyc sources -v # NTP 소스 목록
$ adjtimex --print # 커널 NTP 파라미터
$ ethtool -T eth0 # PTP 하드웨어 타임스탬프 지원
$ pmc -u -b 0 'GET TIME_STATUS_NP' # PTP 상태
# ============ TSC 진단 (x86) ============
$ dmesg | grep -iE 'tsc|clocksource|calibrat'
$ grep -o 'constant_tsc\|nonstop_tsc\|rdtscp\|tsc_known_freq' /proc/cpuinfo | sort -u
# ============ ftrace로 타이머 트레이싱 ============
$ echo 1 > /sys/kernel/tracing/events/timer/enable
$ echo 1 > /sys/kernel/tracing/events/hrtimer/enable
$ cat /sys/kernel/tracing/trace_pipe
# hrtimer_start: hrtimer=... function=tick_sched_timer expires=...
# hrtimer_expire_entry: hrtimer=... function=tick_sched_timer now=...
# ============ 지연 교정 확인 ============
$ dmesg | grep -i 'calibrat\|loops_per\|bogomips'
Calibrating delay loop (skipped), value calculated using timer frequency..
6000.00 BogoMIPS (lpj=12000000)
# /proc/timer_list — 모든 활성 타이머 덤프
$ cat /proc/timer_list | grep -A5 "clock 0:"
지연 예산 관점 점검
| 계층 | 대표 비용 | 관찰 포인트 |
|---|---|---|
| vDSO 읽기 | ~20~30ns | TSC 기반, clock_mode 확인 |
| syscall 경유 | ~100~250ns | 컨텍스트 전환 + 보안 완화 오버헤드 |
| HPET MMIO 읽기 | ~600~1000ns | clocksource fallback 여부 |
| NTP step | 즉시 점프 | 로그/chrony 이벤트와 시점 상관관계 |
| PTP SW 타임스탬프 | 수 us~수십 us | NIC 큐잉/irq 지연 영향 |
# 짧은 기준 벤치: vDSO / syscall / coarse clock 비교
$ perf stat -r 5 ./clock_bench
# irq 지연 확인
$ trace-cmd record -e irq -e hrtimer -e timer
$ trace-cmd report | less
커널 설정 종합
아래 옵션은 "시간 정확도", "전력 효율", "가상화 호환성"을 함께 고려해 선택해야 합니다. 디버깅 시에는 운영 설정을 유지한 상태와 실험용 설정을 분리해 비교하세요.
| 운영 시나리오 | 권장 방향 | 주의점 |
|---|---|---|
| 일반 서버 | CONFIG_HIGH_RES_TIMERS=y, schedutil, NTP | 과도한 고정 주파수는 전력 증가 |
| 저지연 트레이딩/계측 | TSC 안정성 확보, PTP HW timestamp | 전력 관리 완화 시 발열 증가 |
| 가상화 게스트 | CONFIG_PARAVIRT_CLOCK=y, guest NTP | 호스트 오버커밋 시 드리프트 증가 |
| 임베디드 SoC | CONFIG_COMMON_CLK=y + SoC 클럭 드라이버 | CCF 트리 오설정 시 장치 오동작 |
# ===== 시간/클럭 관련 커널 설정 종합 =====
# -- 기본 시간 관리 --
CONFIG_HZ_250=y # 타이머 틱 주파수 (100/250/300/1000)
CONFIG_HIGH_RES_TIMERS=y # 고해상도 타이머 (hrtimer)
CONFIG_GENERIC_CLOCKEVENTS=y # clockevent 프레임워크
CONFIG_POSIX_TIMERS=y # POSIX 타이머 지원
# -- Tickless 커널 --
CONFIG_NO_HZ_IDLE=y # 유휴 시 틱 생략 (전력 절약)
# CONFIG_NO_HZ_FULL=y # 완전 tickless (고성능/RT)
# -- Clocksource --
CONFIG_X86_TSC=y # TSC 지원 (x86)
CONFIG_HPET=y # HPET 지원
CONFIG_HPET_TIMER=y # HPET clockevent
CONFIG_X86_PM_TIMER=y # ACPI PM Timer
CONFIG_ARM_ARCH_TIMER=y # ARM Generic Timer
CONFIG_PARAVIRT_CLOCK=y # KVM pvclock
CONFIG_KVM_GUEST=y # KVM 게스트 지원
# -- Common Clock Framework (CCF) --
CONFIG_COMMON_CLK=y # CCF 프레임워크
CONFIG_COMMON_CLK_HI3519=y # SoC별 CCF 드라이버 (예시)
CONFIG_CLK_SUNXI_NG=y # Allwinner next-gen CCF
CONFIG_CLK_IMX8MM=y # NXP i.MX8M Mini CCF
CONFIG_CLK_SAMSUNG_EXYNOS=y # Samsung Exynos CCF
# -- vDSO --
CONFIG_GENERIC_VDSO_TIME_NS=y # Time namespace vDSO 지원
# -- NTP --
CONFIG_NTP_PPS=y # PPS (Pulse Per Second) 지원
# -- PTP --
CONFIG_PTP_1588_CLOCK=y # PTP 하드웨어 클럭 프레임워크
CONFIG_PTP_1588_CLOCK_OPTIONAL=y
CONFIG_DP83640_PHY=y # PTP PHY 드라이버 (예시)
# -- Time Namespace --
CONFIG_TIME_NS=y # Time namespace
# -- RTC --
CONFIG_RTC_CLASS=y # RTC 프레임워크
CONFIG_RTC_HCTOSYS=y # 부팅 시 RTC → 시스템 시계
CONFIG_RTC_SYSTOHC=y # 주기적 시스템 → RTC 동기화
# -- CPU 주파수 관리 (cpufreq) --
CONFIG_CPU_FREQ=y # cpufreq 프레임워크
CONFIG_CPU_FREQ_STAT=y # 주파수 전환 통계
CONFIG_CPU_FREQ_DEFAULT_GOV_SCHEDUTIL=y # 기본 거버너: schedutil
CONFIG_CPU_FREQ_GOV_PERFORMANCE=y # performance 거버너
CONFIG_CPU_FREQ_GOV_POWERSAVE=y # powersave 거버너
CONFIG_CPU_FREQ_GOV_USERSPACE=y # userspace 거버너
CONFIG_CPU_FREQ_GOV_ONDEMAND=y # ondemand 거버너
CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y # conservative 거버너
CONFIG_CPU_FREQ_GOV_SCHEDUTIL=y # schedutil 거버너
CONFIG_X86_INTEL_PSTATE=y # Intel P-State 드라이버
CONFIG_X86_AMD_PSTATE=y # AMD P-State 드라이버 (v5.17+)
CONFIG_X86_ACPI_CPUFREQ=y # ACPI cpufreq 드라이버
CONFIG_ENERGY_MODEL=y # EAS 에너지 모델
# -- 디버깅 --
CONFIG_CLOCKSOURCE_WATCHDOG=y # clocksource 안정성 감시
CONFIG_DEBUG_TIMEKEEPING=y # timekeeping 디버그 경고
dmesg | grep -i clocksource, clock_getres(), chronyc tracking을 순서대로 확인해 "클럭 소스 선택 → 해상도 → 동기화 품질"이 함께 개선되는지 확인하세요.
참고자료
커널 공식 문서
- Timekeeping (docs.kernel.org) -- 커널 타임키핑 API 공식 문서입니다
- The Common Clk Framework (docs.kernel.org) -- Common Clock Framework(CCF) 드라이버 API 문서입니다
- Delay and Sleep Mechanisms (docs.kernel.org) -- 커널 지연/슬립 메커니즘 API 레퍼런스입니다
- PTP Hardware Clock Infrastructure (docs.kernel.org) -- PTP 하드웨어 클럭 드라이버 인터페이스 문서입니다
- Timekeeping Virtualization for X86-Based Architectures (docs.kernel.org) -- KVM 환경의 TSC/pvclock 가상화 문서입니다
- Timers Subsystem (docs.kernel.org) -- 타이머 서브시스템 전체 색인입니다
- HPET (docs.kernel.org) -- High Precision Event Timer 커널 지원 문서입니다
- NO_HZ: Reducing Scheduling-Clock Ticks (docs.kernel.org) -- tickless(NO_HZ) 커널 동작 설명 문서입니다
- hrtimers - subsystem for high-resolution kernel timers (docs.kernel.org) -- 고해상도 타이머(hrtimer) 설계 문서입니다
- NTP PPS (docs.kernel.org) -- NTP Pulse-Per-Second 관리자 가이드입니다
- CPU Performance Scaling (docs.kernel.org) -- cpufreq 프레임워크 관리자 가이드입니다
- intel_pstate CPU Performance Scaling Driver (docs.kernel.org) -- Intel P-State/HWP 드라이버 문서입니다
LWN.net 기사
- A new approach to kernel timers (LWN, 2006) -- Thomas Gleixner의 hrtimer 도입 배경을 설명하는 기사입니다
- Clockevents and dyntick (LWN, 2007) -- clockevent 프레임워크와 dynamic tick 설계를 다룹니다
- (Nearly) full tickless operation in 3.10 (LWN, 2013) -- NO_HZ_FULL 구현과 timekeeping 영향을 설명합니다
- Reinventing the timer wheel (LWN, 2015) -- 타이머 휠 재설계와 저해상도 타이머 최적화 기사입니다
- POSIX clocks (LWN, 2010) -- CLOCK_MONOTONIC, CLOCK_BOOTTIME 등 POSIX 시계 인터페이스를 설명합니다
- CLOCK_BOOTTIME (LWN, 2011) -- CLOCK_BOOTTIME 추가 배경과 suspend 시간 포함 논의입니다
- The vDSO on ARM64 (LWN, 2014) -- vDSO를 통한 clock_gettime() 가속 원리를 설명합니다
- Y2038 and the kernel (LWN, 2019) -- 커널 ktime_t 64비트 전환과 2038년 문제 대응을 다룹니다
- The Common Clk Framework (LWN, 2014) -- CCF 설계 철학과 구현 상세를 설명하는 기사입니다
커널 소스 코드
- include/linux/ktime.h -- ktime_t 타입 정의 및 변환 인라인 함수입니다
- kernel/time/timekeeping.c -- timekeeper 핵심 구현 (ktime_get 계열 함수)입니다
- kernel/time/clocksource.c -- clocksource 등록·선택·watchdog 구현입니다
- kernel/time/clockevents.c -- clock_event_device 관리 코어입니다
- kernel/time/hrtimer.c -- 고해상도 타이머(hrtimer) 구현입니다
- kernel/time/ntp.c -- 커널 NTP 시간 보정 구현입니다
- kernel/time/posix-timers.c -- POSIX 타이머 시스템 콜 구현입니다
- kernel/time/posix-clock.c -- POSIX 동적 클럭(PHC 등) 프레임워크입니다
- kernel/time/vsyscall.c -- vDSO 타임키핑 데이터 업데이트 코어입니다
- include/linux/timekeeper_internal.h -- struct timekeeper 내부 구조체 정의입니다
- include/linux/clocksource.h -- struct clocksource 정의 및 매크로입니다
- arch/x86/kernel/tsc.c -- x86 TSC clocksource 구현입니다
- arch/x86/kernel/hpet.c -- x86 HPET clocksource/clockevent 구현입니다
- drivers/clocksource/acpi_pm.c -- ACPI PM Timer clocksource 구현입니다
- drivers/clocksource/arm_arch_timer.c -- ARM Generic Timer clocksource 구현입니다
- drivers/clk/clk.c -- Common Clock Framework 코어 구현입니다
- drivers/ptp/ptp_clock.c -- PTP 하드웨어 클럭 코어 구현입니다
- drivers/cpufreq/cpufreq.c -- cpufreq 프레임워크 코어 구현입니다
규격 및 표준
- Intel 64 and IA-32 Architectures Software Developer's Manual -- Vol. 3B Chapter 18: TSC(Time Stamp Counter) 동작 및 Invariant TSC 규격입니다
- IA-PC HPET Specification (Rev 1.0a) -- High Precision Event Timer 하드웨어 규격입니다
- ACPI Specification 6.5 -- Section 4.8.3.3에서 Power Management Timer(PM Timer) 레지스터를 정의합니다
- Arm Architecture Reference Manual -- Generic Timer 아키텍처(CNTPCT_EL0 등) 규격입니다
- IEEE 1588-2019 (PTP) -- Precision Time Protocol 표준 규격입니다
- RFC 5905 -- NTPv4 -- Network Time Protocol Version 4 표준 규격입니다
- POSIX clock_gettime() (IEEE Std 1003.1) -- CLOCK_REALTIME, CLOCK_MONOTONIC 등 POSIX 시계 인터페이스 표준입니다
기술 블로그 및 발표
- Kernel Timer Systems (eLinux.org) -- 커널 타이머 서브시스템 전반을 개괄하는 위키 문서입니다
- time(7) -- Linux manual page -- 리눅스 시간 관련 시스템 콜과 시계 개념을 정리한 맨 페이지입니다
- clock_gettime(2) -- Linux manual page -- clock_gettime/clock_settime 시스템 콜 맨 페이지입니다
- vdso(7) -- Linux manual page -- vDSO 메커니즘 개요 맨 페이지입니다
- A New Model for the Kernel Timer Subsystem (OLS 2006) -- Thomas Gleixner, Douglas Niehaus의 hrtimer/clockevent 설계 논문입니다
관련 문서
ktime/Clock과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.