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 커널의 시간 관리 체계를 소스 코드 수준에서 분석합니다.

관련 표준: IEEE 1588 (PTP 시간 동기화), POSIX.1-2017 (CLOCK_* 시계 정의), Intel SDM (TSC, HPET) — 커널 시간 관리 프레임워크가 참조하는 시간/클럭 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
참고: jiffies, timer_list, hrtimer 기본 사용법, clockevent, tickless 커널은 타이머 (Timers) 페이지를 참조하세요. 이 페이지에서는 시간 측정의 내부 구현, 클럭 하드웨어, 동기화 메커니즘을 심층적으로 다룹니다.
전제 조건: 인터럽트동기화 기법 문서를 먼저 읽으세요. 비동기 이벤트 처리 주제는 문맥 전환과 지연 실행 경로를 정확히 구분해야 하므로, IRQ와 deferred work 경계를 먼저 잡아야 합니다.
일상 비유: ktime/Clock 시스템은 시계탑과 여러 시계와 비슷합니다. clocksource(TSC, HPET)는 정확한 시계탑, timekeeper는 시계탑을 읽어 여러 시계(REALTIME, MONOTONIC 등)를 관리하는 관리자, vDSO는 사람들이 직접 시계를 보는 것(시계탑에 묻지 않고)입니다. NTP/PTP는 다른 도시의 표준 시간과 동기화하는 것과 같습니다.

핵심 요약

  • ktime_t — 나노초 단위의 64비트 시간 값. 커널 타이머 API의 표준 시간 타입입니다.
  • timekeeper — clocksource를 읽어 벽시계 시간(wall clock)과 모노토닉 시간을 유지하는 핵심 구조체입니다.
  • CLOCK_REALTIME / CLOCK_MONOTONIC — 대표적인 두 시계. REALTIME은 벽시계, MONOTONIC은 부팅 후 단조 증가합니다.
  • vDSOgettimeofday() 등을 커널 진입 없이 사용자 공간에서 실행하는 최적화입니다.
  • NTP / PTP — 네트워크를 통해 시스템 시계를 외부 기준 시계와 동기화합니다.

단계별 이해

  1. 클럭 소스 확인cat /sys/devices/system/clocksource/clocksource0/current_clocksource로 현재 사용 중인 클럭 소스를 확인합니다.

    대부분의 x86 시스템에서는 TSC(Time Stamp Counter)가 사용됩니다.

  2. 시간 읽기 — 커널에서 ktime_get()(모노토닉) 또는 ktime_get_real()(벽시계)로 현재 시각을 읽습니다.

    사용자 공간에서는 clock_gettime(CLOCK_MONOTONIC, &ts)를 사용합니다.

  3. 시간 동기화chronyntpd가 NTP 서버와 통신하여 커널의 timekeeper를 보정합니다.

    timedatectl로 현재 NTP 동기화 상태를 확인할 수 있습니다.

Timekeeping 아키텍처 개요

User Space clock_gettime() gettimeofday() time() clock_nanosleep() timerfd vDSO (syscall 없이 직접 읽기) Kernel Space struct timekeeper CLOCK_REALTIME CLOCK_MONOTONIC CLOCK_MONOTONIC_RAW CLOCK_BOOTTIME CLOCK_TAI clocksource 프레임워크 (read 추상화) TSC HPET ACPI PM ARM Arch KVM PV RTC NTP/PTP 보정
Linux 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 내부 표현 (s64 나노초) S 63-bit 나노초 값 (최대 ≈ 9.22 x 10^18 ns ≈ 292.47년) bit 63 62 0 양수: 0 ~ 9,223,372,036,854,775,807 ns (미래 방향) 음수: -9,223,372,036,854,775,808 ns ~ -1 ns (과거 방향) 예: 5초 = 5,000,000,000 ns = 0x0000_0001_2A05_F200
ktime_t 64-bit 내부 표현 — signed 64-bit 나노초로 약 ±292년 범위 표현

오버플로 고려사항

오버플로 주의: 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()MONOTONICOX일반적인 경과 시간 측정
ktime_get_real()REALTIMEOO벽시계 시간 (UTC)
ktime_get_boottime()BOOTTIMEOO부팅 이후 총 경과 시간
ktime_get_clocktai()TAIOO윤초 없는 절대 시간
ktime_get_raw()MONOTONIC_RAWXX하드웨어 클럭 직접 읽기
ktime_get_ts64()MONOTONICOXtimespec64 결과
ktime_get_real_ts64()REALTIMEOOtimespec64 결과
ktime_get_coarse()MONOTONICOX틱 해상도, 매우 빠름
ktime_get_coarse_real()REALTIMEOO틱 해상도, 매우 빠름
ktime_get_coarse_boottime()BOOTTIMEOO틱 해상도, 매우 빠름
ktime_get_fast_ns()MONOTONICOXNMI-safe, seqcount 기반
ktime_get_mono_fast_ns()MONOTONICOXNMI-safe 별칭
ktime_get_raw_fast_ns()MONOTONIC_RAWXXNMI-safe, 원시 클럭
ktime_get_boot_fast_ns()BOOTTIMEOONMI-safe, 부팅 시간
ktime_get_real_fast_ns()REALTIMEOONMI-safe, 벽시계
coarse vs 일반 vs fast: 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_REALTIME1970-01-01 UTCO진행적용벽시계. settimeofday()로 변경 가능. 로그 타임스탬프용
CLOCK_MONOTONIC부팅 시점O (주파수)정지X단조 증가. 경과 시간 측정의 기본 시계
CLOCK_MONOTONIC_RAW부팅 시점X정지XNTP 보정 없는 원시 하드웨어 틱. 하드웨어 벤치마크
CLOCK_MONOTONIC_COARSE부팅 시점O (주파수)정지X틱 해상도(~4ms). 매우 빠름. 대략적 시간용
CLOCK_REALTIME_COARSE1970-01-01 UTCO진행적용틱 해상도 벽시계. 매우 빠름
CLOCK_BOOTTIME부팅 시점O (주파수)진행Xsuspend 시간 포함. 모바일, 네트워크 타임아웃
CLOCK_TAI1970-01-01 TAIO진행X국제원자시. 윤초 없음. PTP, 금융 타임스탬프
CLOCK_PROCESS_CPUTIME_ID프로세스 생성X정지X프로세스 CPU 시간 (user+sys)
CLOCK_THREAD_CPUTIME_ID스레드 생성X정지X스레드 CPU 시간
Clocksource 계층 구조: HW → timekeeper → CLOCK_* Hardware Layer TSC HPET ACPI PM ARM Arch Timer KVM pvclock jiffies struct clocksource (추상화 계층) struct timekeeper (시간 관리자) CLOCK_REALTIME CLOCK_MONOTONIC CLOCK_MONOTONIC_RAW CLOCK_BOOTTIME CLOCK_TAI NTP/PTP
Clocksource 계층 구조 — 하드웨어 클럭 → clocksource 추상화 → timekeeper → 사용자 CLOCK_* 시계

시계 선택 의사결정 테이블

사용 시나리오권장 시계이유주의사항
일반 경과 시간 측정CLOCK_MONOTONICNTP 보정 반영, 단조 증가suspend 시 정지
네트워크 타임아웃CLOCK_BOOTTIMEsuspend 시에도 진행모바일/IoT 필수
로그/감사 타임스탬프CLOCK_REALTIMEUTC 벽시계 시간NTP step으로 역행 가능
하드웨어 벤치마크CLOCK_MONOTONIC_RAWNTP 보정 없는 원시 값장기 측정 시 드리프트
고빈도 대략적 시간CLOCK_MONOTONIC_COARSEvDSO, 카운터 읽기 불필요해상도 1/HZ (~4ms)
PTP/금융 타임스탬프CLOCK_TAI윤초 없는 연속 시간tai_offset 설정 필요
NMI/하드IRQ 컨텍스트ktime_get_fast_ns()NMI-safe, seqcount 기반드물게 불일치 가능
컨테이너 마이그레이션CLOCK_MONOTONIC + timensTime 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 보정 없는 원시값)
CLOCK_* 타임라인 비교 (suspend 구간 포함) 부팅 10초 후 suspend (5초) resume 30초 후 SUSPEND MONO 0 → 10 정지 10 → 20 BOOT 0 → 10 진행(10→15) 15 → 25 REAL T → T+10 진행(T+10→T+15) T+15 → T+25 RAW 0 → 10* 정지 10* → 20* TAI T+37 → T+47 진행 T+52 → T+62 실선 = 진행 중 | 점선 = 정지 또는 suspend 중 진행 | * = NTP 보정 없는 원시값 | T = UTC epoch
CLOCK_* 타임라인 비교 — suspend 구간에서의 시계별 동작 차이. MONOTONIC/RAW는 정지, BOOTTIME/REALTIME/TAI는 계속 진행

시계 선택 가이드

/* 커널 코드에서 시계 선택 기준: */

/* 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 값 */
};
Timekeeper 업데이트 사이클 Tick Interrupt (tick_sched_timer) update_wall_time() 사이클 읽기 + 변환 write_seqcount_begin 1. cycle_now = clock->read() 2. delta = cycle_now - cycle_last 3. nsec += (delta * mult) >> shift 4. NTP 오차 보정 (ntp_tick_adj) 5. cycle_last = cycle_now write_seqcount_end vdso_data 업데이트 Reader ktime_get() read_seqcount seq 불일치 시 retry 틱 주기: 1/HZ (HZ=250 → 4ms 간격)
Timekeeper 업데이트 사이클 — 틱 인터럽트가 update_wall_time()을 호출하여 seqcount 보호 하에 시간 업데이트

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 선택 & 워치독 흐름 clocksource_register() mult/shift 자동 계산, 리스트 추가 clocksource_select() rating 비교 → 최고 선택 override 확인 커맨드라인 clocksource= timekeeping_notify() timekeeper의 clocksource 교체 Clocksource Watchdog (0.5초 주기) watchdog timer 콜백 cs_watchdog_timer_fn() 참조 클럭과 비교 delta = |ref - cs| 계산 drift > 62.5 ppm? 안정: 유지 No mark_unstable() CLOCK_SOURCE_UNSTABLE 설정 Yes 폴백 선택 차순위 clocksource timekeeping_notify 워치독 임계값 MAX_SKEW = 100μs / 0.5s = 200 ppm WATCHDOG_THRESHOLD = 62.5μs (0.0625ms) 검사 주기 = WATCHDOG_INTERVAL (0.5s)
Clocksource 선택/워치독 흐름 — 등록된 clocksource 중 rating이 가장 높은 것을 선택하고, 워치독이 0.5초마다 드리프트를 감시하여 불안정 시 폴백

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 Rating: 300-350 접근: rdtsc 명령 비용: ~20-30ns 해상도: sub-ns vDSO 최적 HPET Rating: 250 접근: MMIO 비용: ~100-300ns 해상도: ~70ns TSC 폴백 ACPI PM Rating: 200 접근: I/O Port 비용: ~500ns-1us 해상도: ~279ns 최후 폴백 ARM Arch Timer Rating: 400 접근: 시스템 레지스터 비용: ~5-20ns ARM 기본 KVM pvclock Rating: 450 접근: 공유 메모리 비용: ~15-25ns VM 최적 jiffies Rating: 1 접근: 전역 변수 비용: ~1ns 최저 순위 기본
하드웨어 클럭소스 비교 카드 — Rating이 높을수록 우선 선택. TSC(x86), ARM Arch Timer(ARM), KVM pvclock(VM)이 각 환경의 최적 선택

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 0x15Intel 6세대+ (Skylake+)정확 (HW 보고)즉시
MSR_PLATFORM_INFOIntel 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
TSC 안정성 판별: 커널은 부팅 시 CPUID 피처 플래그를 확인합니다.
  • constant_tsc: P-state 변경과 무관한 일정 주파수 (Core 2 이후)
  • nonstop_tsc: 깊은 C-state에서도 카운터 정지 안 함 (Nehalem 이후)
  • tsc_known_freq: CPUID로 정확한 주파수 보고 (Skylake 이후)
  • 세 가지 모두 있으면 invariant TSC → rating 350으로 승격, watchdog 면제 가능
TSC 변형 진화 (x86 세대별) Variant TSC Pentium ~ P4 CPUID: (없음) 주파수 변동 시 TSC 속도 변화 Rating: 0 (clocksource 부적합) Constant TSC Core 2 / Athlon64 CPUID: constant_tsc P-state 무관, C-state에서 정지 Rating: 300 Invariant TSC Nehalem / Bulldozer+ CPUID: constant + nonstop_tsc C-state/P-state 모두 무관 Rating: 300 → 350 승격 Known-Freq TSC Skylake+ / Zen+ CPUID 0x15: crystal freq 캘리브레이션 불필요 Rating: 350, watchdog 면제 관련 MSR 레지스터 IA32_TSC (0x10): 64-bit 카운터 값 IA32_TSC_ADJUST (0x3B): 오프셋 보정 IA32_TSC_AUX (0xC0000103): 코어 ID rdtsc EDX:EAX ← TSC 비순서 실행 가능 (비직렬화) 벤치마크에서 부정확할 수 있음 ⚡ ~20 cycles lfence + rdtsc lfence 로 이전 명령 완료 대기 읽기 순서 보장 (직렬화) 커널 rdtsc_ordered() 기본 ⚡ ~25-30 cycles rdtscp EDX:EAX ← TSC, ECX ← AUX 읽기 직렬화 내장 (이전 명령 대기) 코어 ID 동시 반환 (마이그레이션 감지) ⚡ ~25-35 cycles
TSC 변형 진화 — Pentium의 Variant TSC부터 Skylake의 Known-Freq TSC까지, 안정성과 정밀도가 세대별로 향상

TSC 레지스터 상세

MSR주소비트설명접근
IA32_TSC0x1064TSC 카운터 현재 값. wrmsr로 쓰면 값을 설정(특권 명령)rdtsc / rdmsr
IA32_TSC_ADJUST0x3B64TSC 오프셋 보정. TSC에 더해지는 signed 값. VM 마이그레이션, CPU hotplug 시 코어 간 동기화에 사용rdmsr / wrmsr
IA32_TSC_AUX0xC000010332보조 데이터(보통 프로세서/코어 ID). rdtscp가 ECX로 반환rdtscp (ECX)
IA32_TSC_DEADLINE0x6E064TSC-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 8254 내부 구조 Crystal Oscillator 14.31818 MHz ÷ 12 = 1.193182 MHz CLK Intel 8254 PIT Channel 0 (I/O 0x40) 16-bit 다운 카운터 Mode 2: Rate Generator → IRQ0 (시스템 틱 인터럽트) HZ=1000 → 카운트: 1193 → IRQ0 8259A Channel 1 (I/O 0x41) DRAM 리프레시 (레거시) 현대 시스템: 미사용 역사적 의미만 존재 Channel 2 (I/O 0x42) PC 스피커 / 캘리브레이션 Gate: NMI 포트 0x61 bit[0] Out: 포트 0x61 bit[5] (읽기) TSC 캘리브레이션에 사용 스피커 TSC 캘리브레이션 Mode Command (0x43) bit[7:6] = Channel 선택 bit[5:4] = RW 모드 bit[3:1] = 동작 모드 (0~5)
PIT 8254 내부 구조 — 14.31818 MHz 크리스탈에서 ÷12한 1.193182 MHz 클럭으로 3개 채널 구동. Channel 0이 시스템 틱, Channel 2가 TSC 캘리브레이션에 사용

PIT I/O 포트 레지스터

포트이름접근설명
0x40Channel 0 DataR/WChannel 0 카운터 읽기/쓰기. 주 시스템 틱 타이머
0x41Channel 1 DataR/WChannel 1 카운터 (레거시 DRAM 리프레시, 현대 시스템 미사용)
0x42Channel 2 DataR/WChannel 2 카운터. PC 스피커 또는 TSC 캘리브레이션
0x43Mode/CommandW모드 커맨드 레지스터. 채널/RW 모드/동작 모드 설정

Mode Command 레지스터 비트 필드 (포트 0x43)

비트필드설명
[7:6]SC (Select Counter)00/01/10Channel 0/1/2 선택. 11=Read-Back 명령 (8254)
[5:4]RW (Read/Write)00Counter Latch: 현재 카운트 래치
01하위 바이트만 R/W
10상위 바이트만 R/W
11하위 → 상위 순서로 양쪽 R/W
[3:1]Mode000Mode 0: Interrupt on Terminal Count
001Mode 1: HW Retriggerable One-Shot
010Mode 2: Rate Generator (주기적 인터럽트)
011Mode 3: Square Wave Generator
100Mode 4: Software Triggered Strobe
101Mode 5: HW Triggered Strobe
[0]BCD0/10: 16-bit 바이너리, 1: 4-decade BCD

PIT 주파수 유래

1.193182 MHz의 유래: IBM PC 원래 설계에서 14.31818 MHz 마스터 오실레이터를 3으로 나누어 NTSC 컬러 서브캐리어(4.77 MHz)를 만들고, 12로 나누어 PIT 클럭(1.193182 MHz)을 생성했습니다. 14.31818 / 12 = 1.193182 MHz. 이 주파수는 40년 넘게 하위 호환을 위해 유지되고 있습니다.

PIT 6가지 카운터 모드

모드이름출력 파형주요 용도
Mode 0Interrupt on Terminal Count초기 LOW → 카운트 0 도달 시 HIGHone-shot 타이머
Mode 1HW Retriggerable One-ShotGate 상승 에지 시 트리거, TC에서 HIGH외부 트리거 one-shot
Mode 2Rate GeneratorN-1 CLK 동안 HIGH, 1 CLK LOW (반복)시스템 틱 (Channel 0)
Mode 3Square Wave GeneratorN/2 HIGH, N/2 LOW (50% 듀티)스피커 톤 생성
Mode 4Software Triggered Strobe카운트 완료 시 1 CLK LOW 펄스소프트웨어 단발 펄스
Mode 5HW Triggered StrobeGate 트리거 후 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);
}
PIT → APIC Timer → TSC-Deadline 진화: 초기 x86은 PIT가 유일한 시스템 타이머였습니다. SMP 등장과 함께 CPU당 Local APIC Timer가 도입되었고, TSC-deadline 모드(Ivy Bridge+)는 다음 이벤트까지의 TSC 값을 직접 비교하여 나노초 정밀도의 one-shot 인터럽트를 제공합니다. 현대 커널에서 PIT는 부팅 초기 캘리브레이션과 레거시 fallback에서만 사용됩니다.

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 양쪽으로 사용됩니다.

HPET 레지스터 맵 (MMIO Base: 0xFED00000) General Registers 0x000 General Capabilities & ID 64-bit R/O REV_ID | NUM_TIM | COUNT_SIZE | LEG_RT_CAP | VENDOR_ID | PERIOD 0x010 General Configuration 64-bit R/W 0x020 General Interrupt Status 64-bit R/Wc 0x0F0 Main Counter Value 64-bit R/W 최소 10 MHz, 일반 14.318180 MHz free-running up-counter (감소하지 않음) Timer N Registers (N=0..31) 0x100+N×0x20 Timer N Config & Capability INT_TYPE | INT_ENB | PER_INT | PER_CAP | SIZE_CAP INT_ROUTE | 32MODE | FSB_EN | FSB_CAP 0x108+N×0x20 Timer N Comparator Value Main Counter가 이 값에 도달 → 인터럽트 0x110+N×0x20 Timer N FSB Interrupt Route MSI 주소/데이터 (FSB 라우팅 시) 일반 구성 예시 Timer 0: 0x100, 0x108, 0x110 → IRQ 2/0 Timer 1: 0x120, 0x128, 0x130 → IRQ 8 Timer 2: 0x140, 0x148, 0x150 → IRQ N 주소 간격: 0x20 (32바이트) per timer General Configuration (0x010) 핵심 비트 bit[0] ENABLE_CNF 1=카운터 동작 bit[1] LEG_RT_CNF 1=레거시 교체 모드 LEG_RT: T0→IRQ0 (PIT 대체) LEG_RT: T1→IRQ8 (RTC 대체) LEG_RT_CNF=1이면 Timer 0/1의 IRQ가 PIT(IRQ0)/RTC(IRQ8)로 고정됩니다
HPET 레지스터 맵 — Base 0xFED00000에서 General 레지스터(카운터, 설정)와 Timer별 레지스터(비교기, IRQ 라우팅)가 MMIO로 매핑

General Capabilities & ID 레지스터 (0x000)

비트필드설명
[7:0]REV_IDHPET 리비전 (최소 0x01)
[12:8]NUM_TIM_CAP타이머 수 - 1 (예: 2 = 3개 타이머)
[13]COUNT_SIZE_CAP1=64-bit 카운터, 0=32-bit
[14](reserved)예약
[15]LEG_RT_CAP1=Legacy Replacement Route 지원
[31:16]VENDOR_IDPCI 벤더 ID (예: 0x8086 = Intel)
[63:32]COUNTER_CLK_PERIOD메인 카운터 주기 (펨토초 단위). 예: 69,841,279 fs ≈ 14.318 MHz

Timer N Configuration & Capability 레지스터

비트필드R/W설명
[1]INT_TYPE_CNFR/W0=Edge triggered, 1=Level triggered
[2]INT_ENB_CNFR/W1=인터럽트 활성화
[3]TYPE_CNFR/W0=One-shot, 1=Periodic (PER_INT_CAP=1일 때만)
[4]PER_INT_CAPR/O1=Periodic 모드 지원
[5]SIZE_CAPR/O1=64-bit 비교기, 0=32-bit
[6]VAL_SET_CNFR/WPeriodic 모드 시 비교기 값 직접 설정 허용
[8]32MODE_CNFR/W1=64-bit 타이머를 32-bit 모드로 강제
[13:9]INT_ROUTE_CNFR/W인터럽트 라우팅 (IOAPIC 입력 핀 번호)
[14]FSB_EN_CNFR/W1=FSB(MSI) 인터럽트 라우팅 사용
[15]FSB_INT_DEL_CAPR/O1=FSB 인터럽트 전달 지원
[63:32]INT_ROUTE_CAPR/O사용 가능한 IRQ 비트맵 (bit N=1 → IOAPIC pin N 사용 가능)
HPET 인터럽트 전달 흐름 Main Counter 0x0F0 (up-count) == ? 비교 Comparator Value match! I/O APIC INT_ROUTE_CNF 핀 MSI (FSB) FSB_EN_CNF=1 Legacy Route LEG_RT_CNF=1 CPU IRQ handler Legacy Replacement Route (LEG_RT_CNF=1) Timer 0 → IRQ 0 (PIT 대체) periodic clockevent로 사용 INT_ROUTE_CNF 무시, 강제 IRQ 0 Timer 1 → IRQ 8 (RTC 대체) RTC periodic 인터럽트 에뮬레이션 INT_ROUTE_CNF 무시, 강제 IRQ 8 Legacy Route 활성화 시 PIT(8254)와 CMOS RTC의 인터럽트가 비활성화되고 HPET Timer 0/1이 동일한 IRQ 핀을 대신 사용합니다
HPET 인터럽트 전달 — Comparator match 시 I/O APIC, MSI, 또는 Legacy Route 경로로 CPU에 인터럽트 전달. Legacy Replacement 모드에서는 Timer 0이 PIT(IRQ0), Timer 1이 RTC(IRQ8)를 대체

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=forceACPI 테이블에서 HPET을 감지 못해도 강제 활성화
nohpetHPET 완전 비활성화
hpet=disablenohpet과 동일
clocksource=hpetHPET을 clocksource로 강제 지정
HPET 알려진 문제점:
  • 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 Timer32-bit PM Timer
카운터 마스크0x00FFFFFF (16,777,215)0xFFFFFFFF (4,294,967,295)
Wrap 주기~4.69초~1199초 (~20분)
FADT 비트TMR_VAL_EXT = 0TMR_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회 읽기) */
가상화 환경에서의 ACPI PM Timer 장점: ACPI PM Timer는 I/O 포트 접근이므로 하이퍼바이저가 I/O trap으로 정확하게 에뮬레이션할 수 있습니다. TSC는 VM 마이그레이션 시 오프셋 보정이 필요하고, HPET MMIO는 EPT 위반 처리 비용이 크지만, ACPI PM은 단순 I/O 트랩으로 정확한 시간을 반환합니다. 그래서 일부 VM 환경에서는 의도적으로 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
TSCx86~GHz64rdtsc 명령~20-30ns300-350
HPETx8614.3 MHz32/64MMIO~100-300ns250
PITx861.19 MHz16I/O port~1us110
ACPI PMx863.58 MHz24/32I/O port~500ns-1us200
ARM ArchARM1-100 MHz56시스템 레지스터~5-20ns400
KVM pvclockKVM 게스트호스트 TSC64공유 메모리~15-25ns450
jiffies모든HZ64전역 변수~1ns1

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 clocksourceclocksource_register_hz()
clockevent언제 인터럽트를 발생시킬까?struct clock_event_deviceclockevents_config_and_register()
CCF어떤 장치에 몇 MHz를 줄까?struct clk_hwclk_set_rate(), clk_prepare_enable()
CCF 클럭 트리와 소비자 연결 OSC 24MHz PLL0 1200MHz MUX DIV /4 gate-uart gate-spi gate-gpu UART (48MHz) SPI (100MHz) GPU (600MHz) Provider가 클럭 트리를 등록하고, Consumer 드라이버가 이름으로 받아 enable/rate 변경
CCF는 "클럭 생산자-소비자" 관계를 관리하고, clocksource와는 책임이 분리된다

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 결정 경로 (schedutil 기준) CFS/RT 부하 schedutil util 업데이트 target_freq 계산 cpufreq core / policy driver (intel/amd/acpi) MSR/CPPC/MMIO를 통한 실제 P-State 적용
스케줄러-거버너-cpufreq 드라이버-하드웨어 레지스터까지 이어지는 주파수 제어 경로

cpufreq 구성요소와 정책 단위

구성요소역할대표 sysfs
policyXCPU 묶음별 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주기적 샘플링 후 급상승짧은 버스트 워크로드에서 출렁임 가능
실전 팁: timekeeping 자체는 invariant TSC 같은 고정 기준을 사용하므로 cpufreq 변화와 논리적으로 분리됩니다. 하지만 사용자 체감 지연(irq 처리 지연, softirq 실행 시점)은 주파수/전력 상태에 영향을 받으므로 지터 분석 시 cpufreq 정책을 함께 기록해야 합니다.

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 시간 읽기 메커니즘 User Space Application glibc wrapper call linux-vdso.so.1 __vdso_clock_gettime() vdso_data (R/O) rdtsc (HW) User / Kernel 경계 (syscall 불필요!) Kernel Space struct timekeeper update_wall_time() vdso_data 페이지 커널이 R/W 업데이트 매 틱 mmap R/O clocksource (TSC/HPET/...) vDSO: ~20-30ns | syscall 경유: ~100-200ns | HPET syscall: ~600-1000ns
vDSO 시간 읽기 메커니즘 — 커널이 vdso_data 페이지를 사용자 공간에 매핑하여 syscall 없이 시간을 읽는다

vDSO 지원 시계 테이블

시계vDSO 지원HW 읽기대략 비용비고
CLOCK_REALTIMEOrdtsc~25nsTSC clocksource일 때
CLOCK_MONOTONICOrdtsc~25ns가장 많이 사용
CLOCK_REALTIME_COARSEO불필요~5nsvdso_data 캐시만 읽기
CLOCK_MONOTONIC_COARSEO불필요~5ns해상도 1/HZ
CLOCK_BOOTTIMEO (v5.7+)rdtsc~25nssuspend 오프셋 포함
CLOCK_TAIO (v5.7+)rdtsc~25nsTAI 오프셋 포함
CLOCK_MONOTONIC_RAWX~200nssyscall 필수
CLOCK_PROCESS_CPUTIME_IDX~200ns프로세스별 정보 필요
CLOCK_THREAD_CPUTIME_IDX~200ns스레드별 정보 필요
vDSO fallback: HPET이나 ACPI PM이 clocksource일 때는 vDSO가 하드웨어 카운터를 직접 읽을 수 없어 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/PTP 보정 흐름 External Time Sources NTP Server (Stratum 1) PTP Grandmaster GPS Receiver Atomic Clock PPS (1Hz Pulse) User Space chronyd / ntpd ptp4l phc2sys ts2phc (PPS→PHC) Kernel Space adjtimex() syscall PTP clock ioctl NTP subsystem mult 조정 (slew/step) PHC driver HW 클럭 조정 struct timekeeper 보정된 mult/shift 적용 NTP: ms 정밀도 | PTP (SW): us 정밀도 | PTP (HW): sub-us~ns 정밀도
NTP/PTP 보정 흐름 — 외부 시간 소스 → 사용자 공간 데몬 → 커널 adjtimex/PHC → timekeeper 보정

NTP vs PTP 정밀도 비교

특성NTPPTP (SW timestamping)PTP (HW timestamping)
프로토콜RFC 5905 (NTPv4)IEEE 1588-2008/2019IEEE 1588-2008/2019
일반 정밀도1~50 ms10~100 us10~100 ns
LAN 최적 정밀도~100 us~1 us< 100 ns
WAN 정밀도1~10 ms지원 안 함 (일반적)지원 안 함 (일반적)
전용 하드웨어불필요불필요PTP 지원 NIC 필요
네트워크 요구인터넷/LANLANLAN (PTP-aware 스위치 권장)
커널 인터페이스adjtimex()adjtimex() + SO_TIMESTAMPING/dev/ptpN + PHC
대표 데몬chrony, ntpdptp4l + phc2sysptp4l + phc2sys
적합 용도일반 서버, 데스크톱데이터센터 내부금융, 통신, 산업 제어

NTP Stratum 계층

Stratum설명예시일반 정밀도
0기준 클럭 (Reference Clock)원자시계, GPS, CDMAns 수준
1Stratum 0에 직접 연결pool.ntp.org 1차 서버~1 us
2Stratum 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 μs5G 프론트홀 (O-RAN), LTE-TDD, CPRI/eCPRI 동기화
금융 (Finance)< 1 μsMiFID II 거래 타임스탬프, HFT (고빈도 거래)
산업 자동화< 1 μsEtherCAT, PROFINET IRT, TSN (Time-Sensitive Networking)
방송 / AV< 1 μsSMPTE ST 2059, AES67 오디오, 방송 IP 전환
전력 시스템< 1 μsIEC 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 타임스탬프를 전달합니다.

Master Clock Slave Clock Sync t1 t2 Follow_Up (t1) Delay_Req t3 t4 Delay_Resp (t4) Offset = [(t2 - t1) - (t4 - t3)] / 2 Delay = [(t2 - t1) + (t4 - t3)] / 2 * 대칭 경로(symmetric path) 가정
Two-step vs One-step: Two-step 모드에서는 Sync 메시지 전송 후 별도의 Follow_Up 메시지로 정확한 t1 타임스탬프를 전달합니다. One-step 모드에서는 Sync 메시지 자체에 하드웨어가 전송 시점의 타임스탬프를 직접 삽입하므로 Follow_Up이 불필요합니다. One-step은 더 높은 정확도를 제공하지만 하드웨어 지원(on-the-fly timestamp insertion)이 필요합니다.

PTP 클럭 유형

클럭 유형영문설명포트 수
Ordinary Clock (OC)Ordinary Clock단일 PTP 포트를 가진 최종 노드. Master 또는 Slave로 동작1
Boundary Clock (BC)Boundary Clock다수의 PTP 포트를 가지며, 한 포트는 Slave(업스트림), 나머지는 Master(다운스트림)로 동작. 각 포트에서 PTP 도메인 종단2+
Transparent Clock (TC)Transparent ClockPTP 메시지를 전달하면서 체류 시간(residence time)을 correctionField에 누적. PTP 도메인에 참여하지 않음2+
E2E TCEnd-to-End TCSync, Delay_Req 메시지의 correctionField 업데이트-
P2P TCPeer-to-Peer TCSync 메시지의 correctionField에 체류 시간 + 링크 지연 반영-
Grandmaster Clock (GM)Grandmaster ClockPTP 도메인의 최상위 시간 소스. GNSS/원자시계 등 외부 기준 시간에 동기화1+

PTP 네트워크 토폴로지

실제 PTP 배포 환경에서는 Grandmaster Clock이 GNSS 수신기 등으로부터 UTC를 공급받고, Boundary Clock과 Transparent Clock을 거쳐 최종 Ordinary Clock(슬레이브)까지 시간이 전달됩니다.

GNSS Receiver Grandmaster (GM) Transparent Clock (TC) Boundary Clock (BC) Boundary Clock (BC) OC (Slave) OC (Slave) OC (Slave) OC (Slave) TC는 correctionField에 체류 시간을 누적, BC는 각 포트에서 PTP 도메인 종단

Best Master Clock Algorithm (BMCA)

PTP 도메인 내에서 Grandmaster를 선출하는 알고리즘입니다. 각 클럭은 Announce 메시지를 통해 자신의 속성을 광고하고, 모든 참여 클럭이 동일한 비교 기준으로 가장 우수한 클럭을 Grandmaster로 결정합니다. 비교는 다음 순서로 진행됩니다:

우선순위필드설명기본값 / 범위
1priority1관리자가 수동 설정하는 최우선 순위. 값이 작을수록 우선128 (0~255)
2clockClass클럭의 품질 등급. 6=GPS 동기, 7=GPS 홀드오버, 248=기본248
3clockAccuracy클럭 정확도 열거값. 0x21=100ns, 0x22=250ns, 0xFE=불명0xFE
4offsetScaledLogVariance클럭 안정성 지표 (Allan variance 기반)0xFFFF
5priority2동률 시 관리자 설정 보조 순위128 (0~255)
6clockIdentity최종 동률 시 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 E2EIEEE 1588UDP/IPv4, UDP/IPv6, L2E2E범용
Default P2PIEEE 1588UDP/IPv4, UDP/IPv6, L2P2P범용 (풀메시 토폴로지)
gPTPIEEE 802.1ASL2 전용P2PTSN, Automotive Ethernet, AV 브릿지
Telecom (Full)ITU-T G.8275.1L2 (Ethernet)E2E5G 프론트홀, 이동통신 기지국
Telecom (Partial)ITU-T G.8275.2UDP/IPv4, UDP/IPv6E2EPTP 비인식 네트워크 경유
PowerIEEE C37.238L2 (Ethernet)P2P전력 변전소 (IEC 61850)
SMPTESMPTE ST 2059-2L2 / UDPE2E/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/IPv6L2 전용 (EtherType 0x88F7)
Delay 방식E2E 또는 P2PP2P 전용 (Peer Delay)
스코프라우팅 가능링크-로컬 (01:80:C2:00:00:0E)
Best MasterBMCABTCA (Best Time-aware Clock Algorithm, 약간 다름)
Sync 간격가변 (프로파일 의존)125ms 기본 (logSyncInterval = -3)
토폴로지 인식선택적필수 (Signaling TLV로 역할 협상)
주요 적용범용Automotive Ethernet, Pro-AV, 산업 TSN
Automotive Ethernet과 gPTP: 차량 내 이더넷 네트워크(100BASE-T1, 1000BASE-T1)에서 ADAS 센서 퓨전, 카메라 동기, V2X 통신 등은 나노초 수준의 동기화가 필수적입니다. gPTP는 링크-로컬 범위에서 동작하여 라우터 없는 차량 내부 네트워크에 최적화되어 있습니다.

Peer Delay 메커니즘 (P2P)

Peer Delay는 인접 노드 간의 링크 지연을 직접 측정합니다. E2E 방식과 달리, 마스터까지의 전체 경로가 아닌 각 홉(hop)별 지연을 독립적으로 측정하므로 Transparent Clock이나 Boundary Clock 환경에서 더 정확한 결과를 제공합니다.

Node A Node B Pdelay_Req t1 t2 Pdelay_Resp (t2) t3 t4 Pdelay_Resp_Follow_Up (t3) Peer Delay = [(t4 - t1) - (t3 - t2)] / 2 * 각 링크별로 독립 측정, 기본 간격 1초 (logPdelayReqInterval = 0)

하드웨어 타임스탬핑 아키텍처

PTP의 나노초 정확도는 하드웨어 타임스탬핑에 의존합니다. 소프트웨어 타임스탬핑은 커널 네트워크 스택의 지연(수십 마이크로초)을 포함하므로 정밀도가 떨어집니다. NIC 또는 PHY 수준에서 패킷의 실제 송수신 시점을 기록하면 이러한 소프트웨어 지터를 제거할 수 있습니다.

타임스탬핑 위치정확도설명
PHY 타임스탬핑최고 (~ ns)PHY 칩이 선로에 가장 가까운 지점에서 타임스탬프. MAC 지연 제거
MAC 타임스탬핑높음 (수~수십 ns)NIC MAC 블록에서 타임스탬프. PHY 지연이 남지만 소프트웨어보다 월등
소프트웨어 타임스탬핑낮음 (수~수십 μs)커널 드라이버 또는 소켓 계층에서 타임스탬프. 스케줄링 지터 포함
Network (Wire/Fiber) PHY HW Timestamp t_phy MAC HW Timestamp t_mac DMA Kernel Driver skb_hwtstamps() → skb에 HW TS 저장 PHC (PTP Hardware Clock) — /dev/ptpN PHY/MAC 타임스탬핑: PHC 레지스터에서 패킷 송수신 시점을 캡처 → 드라이버가 skb에 기록 One-step: PHY/MAC이 송신 시 패킷 내 timestamp 필드 직접 수정

커널 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 소프트웨어 스택

Userspace Application ptp4l phc2sys ts2phc SO_TIMESTAMPING / clock_gettime() /dev/ptpN (chardev ioctl) CLOCK_REALTIME Kernel Space Socket Layer Network Stack PTP Subsystem timekeeping NIC Driver (skb_hwtstamps, ptp_clock_ops) Hardware NIC / PHY HW Timestamping Engine PHC PTP Hardware Clock GNSS/1PPS

PTP 전송 계층 (IPv4/IPv6/L2)

항목UDP/IPv4UDP/IPv6IEEE 802.3 (L2)
EtherType0x0800 (IP)0x86DD (IPv6)0x88F7 (PTP)
Event 포트UDP 319UDP 319해당 없음
General 포트UDP 320UDP 320해당 없음
E2E 멀티캐스트224.0.1.129FF0x::18101:1B:19:00:00:00
P2P 멀티캐스트224.0.0.107FF02::6B01:80:C2:00:00:0E
라우팅 가능가능가능불가 (L2 스코프)
주요 프로파일Default, G.8275.2DefaultgPTP, G.8275.1, C37.238
Event vs General 메시지: Sync, Delay_Req, Pdelay_Req, Pdelay_Resp는 Event 메시지(UDP 319)로 전송되며 하드웨어 타임스탬핑 대상입니다. Follow_Up, Delay_Resp, Announce, Signaling 등은 General 메시지(UDP 320)로 전송되며 타임스탬핑이 필요하지 않습니다.

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'
PTP 배포 시 주의사항:
  • 비대칭 경로(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의 소프트웨어 타임스탬핑을 사용해야 합니다.
One-step 모드와 TC/BC 호환성: One-step Transparent Clock은 Sync 메시지의 correctionField를 전송 중(on-the-fly)에 수정해야 하므로, 모든 경로 상의 스위치가 이를 지원해야 합니다. 지원하지 않는 스위치가 하나라도 있으면 Two-step으로 전환하거나 해당 스위치를 교체해야 합니다.
성능 최적화 팁:
  • 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)을 유발합니다.

지연 함수 결정 트리 atomic/인터럽트 컨텍스트? Yes No 지연 시간은? <1us ndelay() 1-999us udelay() >1ms mdelay() 최후의 수단! 지연 시간은? <10us udelay() 10us-20ms usleep_range() >20ms msleep() 범위 불확실? → fsleep(us) 자동 선택: <10us=udelay | 10us-20ms=usleep_range | >20ms=msleep busy-wait (CPU 100% 점유) hrtimer sleep (CPU 양보) timer sleep (틱 해상도) v5.8+ fsleep()은 프로세스 컨텍스트에서 범위에 따라 최적 지연 방식을 자동 선택합니다
지연 함수 결정 트리 — 컨텍스트와 지연 시간에 따른 최적 함수 선택 가이드

지연 함수 레퍼런스

함수범위방식컨텍스트정밀도
ndelay(ns)1-999 nsbusy-wait모든 (atomic OK)~ns
udelay(us)1-999 usbusy-wait모든 (atomic OK)~us
mdelay(ms)1+ msbusy-wait (반복 udelay)모든 (atomic OK)~ms
usleep_range(min, max)10+ ushrtimer sleep프로세스만~us
msleep(ms)1+ mstimer sleep프로세스만~1/HZ
msleep_interruptible(ms)1+ mstimer sleep프로세스만~1/HZ
ssleep(s)1+ stimer 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 단위로 점유하므로 시스템 응답성 저하

흔한 지연 함수 실수

실수 1: 인터럽트 컨텍스트에서 msleep() 사용
/* 잘못된 코드 — 인터럽트 핸들러에서 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;
}
실수 2: udelay()에 큰 값 전달
/* 잘못된 코드 — udelay(10000)은 10ms 동안 CPU 점유! */
udelay(10000);  /* 10ms busy-wait → 시스템 응답성 저하 */

/* 올바른 코드 — 프로세스 컨텍스트라면 sleep 사용 */
usleep_range(10000, 12000);  /* CPU 양보하면서 10-12ms 대기 */
실수 3: usleep_range() 범위가 너무 좁음
/* 비효율적 — 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 오프셋 모델 Host Namespace MONOTONIC: base + 0 BOOTTIME: base + 0 Container Namespace MONOTONIC: base + 86400s BOOTTIME: base + 86400s REALTIME은 공유(오프셋 미적용), MONOTONIC_RAW도 공유 목표: 컨테이너 체크포인트/복원 시 경과 시간 연속성 유지 제약: namespace 생성 직후에만 /proc/self/timens_offsets 쓰기 가능
호스트와 컨테이너가 같은 하드웨어 시계를 공유하되, MONOTONIC/BOOTTIME에 namespace별 오프셋을 적용한다

영향 범위와 제약

시계Time Namespace 영향설명
CLOCK_MONOTONICOnamespace 오프셋 적용
CLOCK_BOOTTIMEOsuspend 포함 경과 시간에도 오프셋 적용
CLOCK_REALTIMEX시스템 전체 공유 벽시계
CLOCK_MONOTONIC_RAWX원시 하드웨어 기준 그대로
# 현재 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 확인 → 동기화 상태 확인 → 스케줄링/전력 상태 확인 → 트레이스" 순서로 범위를 좁혀야 재현성과 해결 속도가 올라갑니다.

시간 문제 진단 플로우 1) 증상 분류: 역행, 점프, 지터, 과도한 지연, 동기화 실패 2) clocksource / dmesg 점검 3) NTP/PTP 상태 점검 4) cpufreq/C-state/스케줄링 확인 5) ftrace/perf로 경로 추적 6) 재현 스크립트 고정 + 정책 변경 실험(performance/schedutil 비교)
증상을 바로 튜닝으로 해결하지 말고, 계층별 신호를 수집해 원인을 좁히는 방식이 안정적이다

일반적인 문제와 해결

증상원인진단해결
시간이 갑자기 점프NTP step 보정, settimeofday()dmesg에서 clock set 확인CLOCK_MONOTONIC 사용
TSC unstable 경고CPU 주파수 변동, C-state 문제dmesg | grep -i tsctsc=reliable 또는 nohz=off
clock_gettime() 느림HPET/ACPI PM clocksource 사용clocksource 확인TSC로 변경
suspend 후 타이머 폭발CLOCK_MONOTONIC 기반 타이머BOOTTIME vs MONOTONICCLOCK_BOOTTIME 사용
VM에서 시간 드리프트vCPU 스케줄링 지연chronyc trackingkvm-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~30nsTSC 기반, clock_mode 확인
syscall 경유~100~250ns컨텍스트 전환 + 보안 완화 오버헤드
HPET MMIO 읽기~600~1000nsclocksource fallback 여부
NTP step즉시 점프로그/chrony 이벤트와 시점 상관관계
PTP SW 타임스탬프수 us~수십 usNIC 큐잉/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호스트 오버커밋 시 드리프트 증가
임베디드 SoCCONFIG_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과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.