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), 지연 함수까지 — Linux 커널의 시간 관리 체계를 소스 코드 수준에서 분석합니다.
핵심 요약
- ktime_t — 나노초 단위의 64비트 시간 값. 커널 타이머 API의 표준 시간 타입입니다.
- timekeeper — clocksource를 읽어 벽시계 시간(wall clock)과 모노토닉 시간을 유지하는 핵심 구조체입니다.
- CLOCK_REALTIME / CLOCK_MONOTONIC — 대표적인 두 시계. REALTIME은 벽시계, MONOTONIC은 부팅 후 단조 증가합니다.
- vDSO —
gettimeofday()등을 커널 진입 없이 사용자 공간에서 실행하는 최적화입니다. - 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()는 마지막 틱에 캐시된 값을 반환하여 매우 빠르지만(~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();
}
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 | 프로세스 생성 | X | 정지 | X | 프로세스 CPU 시간 (user+sys) |
| CLOCK_THREAD_CPUTIME_ID | 스레드 생성 | 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 기반 | 드물게 불일치 가능 |
| 컨테이너 마이그레이션 | CLOCK_MONOTONIC + timens | Time namespace 오프셋 적용 | 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 값 */
};
seqcount vs spinlock 비교
| 특성 | seqcount (timekeeper) | spinlock |
|---|---|---|
| 읽기 비용 | 락 없음 (read_seqcount_begin) | 스핀 대기 (경합 시) |
| 쓰기 비용 | 카운터 증가만 | 락 획득/해제 |
| 읽기 동시성 | 무제한 병렬 읽기 | 읽기도 배타적 (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
*/
사이클 → 나노초 변환 수학
/*
* 하드웨어 카운터 사이클을 나노초로 변환:
*
* 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 /* 최대 유휴 시간 */
);
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)
*/
커스텀 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를 계산합니다.
*/
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() 경로 */
하드웨어 클럭 상세
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 레지스터 상세
| 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 |
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 디버깅 체크리스트
# 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 단위(보통 클러스터/패키지)로 최소/최대 주파수와 거버너를 적용합니다. 스케줄러는 현재 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 | 낮은 주파수 선호 | 긴 시스템 콜/인터럽트 처리 지연 가능 |
schedutil | 스케줄러 util 기반 동적 조절 | 일반 서버 기본값으로 균형이 좋음 |
ondemand | 주기적 샘플링 후 급상승 | 짧은 버스트 워크로드에서 출렁임 가능 |
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는 커널이 사용자 공간에 매핑하는 공유 라이브러리로, 시스템 콜 없이 시간 읽기를 수행합니다. 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
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 오프셋 (초) */
};
윤초 (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 네트워크 토폴로지
실제 PTP 배포 환경에서는 Grandmaster Clock이 GNSS 수신기 등으로부터 UTC를 공급받고, Boundary Clock과 Transparent Clock을 거쳐 최종 Ordinary Clock(슬레이브)까지 시간이 전달됩니다.
Best Master Clock Algorithm (BMCA)
PTP 도메인 내에서 Grandmaster를 선출하는 알고리즘입니다. 각 클럭은 Announce 메시지를 통해 자신의 속성을 광고하고, 모든 참여 클럭이 동일한 비교 기준으로 가장 우수한 클럭을 Grandmaster로 결정합니다. 비교는 다음 순서로 진행됩니다:
| 우선순위 | 필드 | 설명 | 기본값 / 범위 |
|---|---|---|---|
| 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의 나노초 정확도는 하드웨어 타임스탬핑에 의존합니다. 소프트웨어 타임스탬핑은 커널 네트워크 스택의 지연(수십 마이크로초)을 포함하므로 정밀도가 떨어집니다. NIC 또는 PHY 수준에서 패킷의 실제 송수신 시점을 기록하면 이러한 소프트웨어 지터를 제거할 수 있습니다.
| 타임스탬핑 위치 | 정확도 | 설명 |
|---|---|---|
| PHY 타임스탬핑 | 최고 (~ ns) | PHY 칩이 선로에 가장 가까운 지점에서 타임스탬프. MAC 지연 제거 |
| MAC 타임스탬핑 | 높음 (수~수십 ns) | NIC MAC 블록에서 타임스탬프. PHY 지연이 남지만 소프트웨어보다 월등 |
| 소프트웨어 타임스탬핑 | 낮음 (수~수십 μs) | 커널 드라이버 또는 소켓 계층에서 타임스탬프. 스케줄링 지터 포함 |
커널 PTP API
Linux 커널은 include/linux/ptp_clock_kernel.h에 PTP Hardware Clock 서브시스템을 정의합니다.
NIC 드라이버는 ptp_clock_info 구조체에 콜백을 채워 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);
}
}
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 스위치를 사용하십시오.
- 방화벽 멀티캐스트 차단: 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 폴링)으로 반응 속도 향상 - 멀티 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 오버헤드보다 바쁜 대기가 효율적 - 10us~20ms:
usleep_range()— hrtimer 기반으로 CPU를 양보하면서 정밀 대기 - 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);
}
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
체크포인트/복원 시나리오
/*
* 컨테이너 복원 시 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을 순서대로 확인해 "클럭 소스 선택 → 해상도 → 동기화 품질"이 함께 개선되는지 확인하세요.
관련 문서
ktime/Clock과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.