ktime / Clock

ktime/Clock: clocksource, Common Clock Framework(CCF), timekeeper, CPU 주파수 관리(cpufreq/HWP), vDSO, NTP/PTP 동기화, TSC/HPET 하드웨어 클럭 완벽 가이드.

초점은 "커널이 시간을 어떻게 계산하고 보정하는가"입니다. timekeeper 내부의 누적 오차 보정, CLOCK 계열별 의미 차이, TSC 안정성 조건과 fallback 전략, cpufreq 변동이 타임스탬프 일관성에 주는 영향, NTP/PTP 보정 경로를 하나의 흐름으로 연결해 설명합니다. 결과적으로 시간 관련 버그(시계 역행, 지터 급증, 동기화 불안정)를 진단할 때 어떤 자료를 수집하고 어떤 순서로 가설을 검증해야 하는지까지 실전 절차로 제시합니다.

ktime_t 함수 레퍼런스, clocksource 프레임워크, Common Clock Framework(CCF), timekeeper 내부 구현, 7가지 CLOCK_* 시계, CPU 주파수 관리(cpufreq/DVFS/P-State/HWP), vDSO 최적화, NTP/PTP 동기화, 하드웨어 클럭(TSC/HPET/ACPI PM), 지연(Latency) 함수까지 — Linux 커널의 시간 관리 체계를 소스 코드 수준에서 분석합니다.

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

핵심 요약

  • ktime_t — 나노초 단위의 64비트 시간 값. 커널 타이머 API의 표준 시간 타입입니다.
  • timekeeper — clocksource를 읽어 벽시계 시간(wall clock)과 모노토닉 시간을 유지하는 핵심 구조체(Struct)입니다.
  • CLOCK_REALTIME / CLOCK_MONOTONIC — 대표적인 두 시계. REALTIME은 벽시계, MONOTONIC은 부팅 후 단조 증가합니다.
  • vDSOgettimeofday() 등을 커널 진입 없이 사용자 공간(User Space)에서 실행하는 최적화입니다.
  • 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()는 마지막 틱에 캐시(Cache)된 값을 반환하여 매우 빠르지만(~1-5ns) 해상도가 1/HZ(4ms@250Hz)입니다. ktime_get_fast_ns()는 NMI/하드IRQ 컨텍스트에서도 안전하게 사용 가능하며, seqcount로 일관성을 보장합니다.

ktime 산술/변환 함수

#include <linux/ktime.h>

/* ===== 생성/변환 ===== */
ktime_t t1 = ns_to_ktime(5000000);         /* 5ms = 5,000,000 ns */
ktime_t t2 = ms_to_ktime(100);              /* 100ms */
ktime_t t3 = us_to_ktime(500);              /* 500us (v6.x+) */
ktime_t t4 = ktime_set(5, 123456789);       /* 5초 + 123,456,789ns */

s64 ns = ktime_to_ns(t1);                   /* → 나노초 */
s64 us = ktime_to_us(t1);                   /* → 마이크로초 */
s64 ms = ktime_to_ms(t1);                   /* → 밀리초 */

/* ktime ↔ timespec64 변환 */
struct timespec64 ts = ktime_to_timespec64(t1);
ktime_t kt = timespec64_to_ktime(ts);

/* ===== 산술 연산 ===== */
ktime_t sum  = ktime_add(a, b);              /* a + b */
ktime_t diff = ktime_sub(a, b);              /* a - b */
ktime_t add_ns = ktime_add_ns(a, 1000);     /* a + 1000ns */
ktime_t add_us = ktime_add_us(a, 500);      /* a + 500us */
ktime_t add_ms = ktime_add_ms(a, 100);      /* a + 100ms */
ktime_t sub_ns = ktime_sub_ns(a, 1000);     /* a - 1000ns */

/* ===== 비교 연산 ===== */
bool is_after  = ktime_after(a, b);          /* a > b */
bool is_before = ktime_before(a, b);         /* a < b */
int  cmp       = ktime_compare(a, b);        /* -1, 0, 1 */
bool is_zero   = ktime_is_null(a);           /* a == 0 */

/* ===== 델타 계산 ===== */
s64 delta_ns = ktime_to_ns(ktime_sub(end, start));
s64 delta_us = ktime_us_delta(end, start);   /* 마이크로초 차이 */
s64 delta_ms = ktime_ms_delta(end, start);   /* 밀리초 차이 */

/* ===== 실용 패턴: 구간 측정 ===== */
ktime_t start = ktime_get();
/* ... 측정할 코드 ... */
s64 elapsed_us = ktime_us_delta(ktime_get(), start);
pr_info("operation took %lld us\\n", elapsed_us);

/* ===== 실용 패턴: 타임아웃 ===== */
ktime_t deadline = ktime_add_ms(ktime_get(), 500); /* 500ms 후 */
while (!condition_met()) {
    if (ktime_after(ktime_get(), deadline))
        return -ETIMEDOUT;
    cpu_relax();
}

ktime 실전 사용 패턴

커널 드라이버에서 ktime API를 사용할 때 자주 등장하는 실전 패턴과, 흔히 저지르는 실수를 정리합니다.

패턴 1: 네트워크 드라이버 패킷 타임아웃 (BOOTTIME 사용)

패킷 에이징(packet aging)에는 ktime_get_boottime()을 사용해야 합니다. ktime_get()(MONOTONIC)은 suspend 동안 정지하므로, 시스템이 절전 후 깨어났을 때 패킷이 실제보다 "젊게" 보이는 문제가 발생합니다.

/* 네트워크 드라이버: suspend를 고려한 패킷 타임아웃 */
struct my_packet {
    ktime_t  arrival;    /* 패킷 도착 시각 */
    struct sk_buff *skb;
};

static void enqueue_packet(struct my_packet *pkt, struct sk_buff *skb)
{
    pkt->skb = skb;
    pkt->arrival = ktime_get_boottime();  /* suspend 포함 시계 */
}

static bool is_packet_expired(struct my_packet *pkt, s64 timeout_ms)
{
    ktime_t deadline = ktime_add_ms(pkt->arrival, timeout_ms);
    return ktime_after(ktime_get_boottime(), deadline);
}
코드 설명

suspend/resume을 고려한 패킷 타임아웃 패턴입니다.

  • ktime_get_boottime()CLOCK_BOOTTIME 기반으로, suspend 동안의 경과 시간도 포함합니다. 시스템이 30분간 절전했다면 그 시간도 패킷 나이에 반영됩니다.
  • ktime_after()두 ktime_t 값을 비교하는 인라인 함수입니다. 내부적으로 a > b를 수행하며, 오버플로에 안전합니다.

패턴 2: schedule_hrtimeout_range() 정밀 슬립

프로세스 컨텍스트에서 마이크로초~밀리초 범위의 정밀한 대기가 필요할 때 사용합니다. usleep_range()와 달리 hrtimer 기반이며, 슬랙(slack) 범위 내에서 타이머 합병(coalescing)이 가능합니다.

/* 정밀 슬립: 500~600μs 범위 대기 */
static int wait_for_phy_ready(struct device *dev)
{
    ktime_t slack = ns_to_ktime(100 * NSEC_PER_USEC); /* 100μs 슬랙 */
    int retries = 10;

    while (retries--) {
        if (phy_is_ready(dev))
            return 0;
        set_current_state(TASK_UNINTERRUPTIBLE);
        schedule_hrtimeout_range(ns_to_ktime(500 * NSEC_PER_USEC), slack,
                                 HRTIMER_MODE_REL);
    }
    return -ETIMEDOUT;
}
코드 설명

schedule_hrtimeout_range()는 hrtimer 기반 정밀 슬립으로, 슬랙 범위 내에서 다른 타이머와 합병되어 전력 효율을 높입니다.

  • slack (100μs)이 범위 안에서 타이머가 합병될 수 있어, CPU가 불필요하게 깨어나는 횟수를 줄입니다.
  • set_current_state(TASK_UNINTERRUPTIBLE)schedule_hrtimeout_range() 호출 전에 태스크 상태를 설정해야 합니다. 시그널로 인한 조기 깨어남이 불필요하면 UNINTERRUPTIBLE을 사용합니다.

패턴 3: ktime_get_raw() 하드웨어 벤치마킹

순수 하드웨어 성능을 측정할 때는 NTP 보정이 없는 ktime_get_raw()를 사용합니다. NTP가 주파수를 slew하는 중이면 ktime_get()으로 측정한 시간이 실제와 최대 500ppm(0.05%) 차이가 날 수 있습니다.

/* 하드웨어 벤치마크: NTP 스큐 없는 순수 측정 */
static void benchmark_dma_transfer(struct device *dev)
{
    ktime_t t0, t1;
    s64 ns;

    t0 = ktime_get_raw();
    start_dma_and_wait(dev);
    t1 = ktime_get_raw();

    ns = ktime_to_ns(ktime_sub(t1, t0));
    dev_info(dev, "DMA transfer: %lld ns\n", ns);
}

패턴 4: 잘못된 클럭 기준 혼합 (안티패턴)

서로 다른 시계에서 얻은 ktime_t 값을 비교하면 suspend/resume 시 예측 불가능한 오차가 발생합니다. 또한 jiffies와 ktime을 같은 타임아웃 로직에 혼합하면 해상도 불일치로 미묘한 버그가 생깁니다.

/* ✗ 버그: 서로 다른 시계 기준 비교 */
ktime_t start = ktime_get();           /* MONOTONIC */
/* ... suspend 발생 가능 ... */
ktime_t now = ktime_get_boottime();    /* BOOTTIME (suspend 포함) */
s64 elapsed = ktime_us_delta(now, start); /* 오차! suspend 시간 이중 계산 */

/* ✓ 수정: 같은 시계 기준 사용 */
ktime_t start = ktime_get_boottime();
/* ... */
ktime_t now = ktime_get_boottime();
s64 elapsed = ktime_us_delta(now, start); /* 정확 */

/* ✗ 버그: jiffies와 ktime 혼합 타임아웃 */
unsigned long timeout = jiffies + msecs_to_jiffies(100); /* ~4ms 해상도 */
while (ktime_before(ktime_get(), some_ktime_deadline)) {
    if (time_after(jiffies, timeout)) /* 해상도 불일치 */
        break;
}

/* ✓ 수정: ktime으로 통일 */
ktime_t deadline = ktime_add_ms(ktime_get(), 100);
while (ktime_before(ktime_get(), deadline)) {
    if (condition_met())
        break;
    cpu_relax();
}
코드 설명

클럭 기준 혼합은 미묘하지만 심각한 버그를 유발합니다.

  • MONOTONIC vs BOOTTIME 혼합MONOTONIC은 suspend 동안 정지하고 BOOTTIME은 계속 진행합니다. 혼합하면 suspend 시간만큼 경과 시간이 왜곡됩니다.
  • jiffies vs ktime 혼합jiffies는 HZ 의존(보통 ~4ms 해상도)이고 ktime은 나노초 해상도입니다. 같은 타임아웃에 혼합하면 해상도 불일치로 타임아웃이 일찍 또는 늦게 발생합니다.

고급 ktime 유틸리티 함수

기본 산술 외에도 커널은 다양한 고급 ktime 유틸리티 함수를 제공합니다. 실무에서 자주 사용되지만 놓치기 쉬운 함수들을 용도별로 정리합니다.

/* === 1. 안전한 산술 연산 === */

/* ktime_add_safe: 오버플로 방지 덧셈 — hrtimer 내부에서 사용 */
ktime_t ktime_add_safe(const ktime_t lhs, const ktime_t rhs)
{
    ktime_t res = ktime_add_unsafe(lhs, rhs);
    if (res < lhs || res < rhs)
        res = KTIME_MAX;  /* 오버플로 → 클램프 */
    return res;
}
/* 사용 예: hrtimer 만료 시각 설정 */
ktime_t expires = ktime_add_safe(ktime_get(), interval);


/* === 2. 나눗셈 헬퍼 === */

u64 ktime_divns(const ktime_t kt, s64 divisor);
/* 사용 예: 평균 타이머 간격 */
u64 avg_ns = ktime_divns(total_elapsed, count);


/* === 3. 클럭 간 변환 === */

ktime_t mono = ktime_get();
ktime_t real = ktime_mono_to_real(mono);
/* 내부: return ktime_add(mono, tk->offs_real); */

ktime_t boot = ktime_mono_to_any(mono, TK_OFFS_BOOT);
ktime_t tai  = ktime_mono_to_any(mono, TK_OFFS_TAI);


/* === 4. 초 해상도 고속 게터 (클럭소스 읽기 없음, ~1ns) === */

time64_t mono_sec = ktime_get_seconds();       /* MONOTONIC 초 */
time64_t real_sec = ktime_get_real_seconds();  /* REALTIME 초  */

/* 사용 예: 로그 로테이션 — 초 정밀도로 충분 */
if (ktime_get_real_seconds() - last_rotate >= 3600)
    rotate_log();


/* === 5. coarse 시리즈 (마지막 틱 캐시, HW 읽기 없음) === */

struct timespec64 ts;
ktime_get_coarse_ts64(&ts);             /* MONOTONIC coarse */
ktime_get_coarse_real_ts64(&ts);        /* REALTIME coarse  */
ktime_get_coarse_boottime_ts64(&ts);    /* BOOTTIME coarse  */
/* 해상도 = 1/HZ (HZ=250 → 4ms, HZ=1000 → 1ms) */


/* === 6. 편의 래퍼 (_ns 접미사) === */

u64 now_ns  = ktime_get_ns();             /* ≡ ktime_to_ns(ktime_get()) */
u64 real_ns = ktime_get_real_ns();
u64 boot_ns = ktime_get_boottime_ns();
u64 tai_ns  = ktime_get_clocktai_ns();
u64 raw_ns  = ktime_get_raw_ns();

/* tracing에서 간결하게 사용 */
u64 start = ktime_get_ns();
do_work();
trace_printk("elapsed: %llu ns\n", ktime_get_ns() - start);


/* === 7. 해상도 조회 === */

u64 res_ns = ktime_get_resolution_ns();
/* TSC: 1ns, HPET: 수십 ns */
코드 설명
  • ktime_add_safe()결과가 피연산자보다 작으면 오버플로로 판단하고 KTIME_MAX를 반환합니다. hrtimer_forward() 등 외부 입력이 관여하는 경로에서 사용됩니다.
  • ktime_divns()32비트 아키텍처에서도 안전하게 64비트 나눗셈을 처리하기 위해 내부적으로 do_div()를 사용합니다.
  • ktime_mono_to_real()offs_real 오프셋을 더하는 단순 덧셈입니다. NTP 보정이나 settimeofday() 호출 시 오프셋이 변경되므로, 변환 결과는 호출 시점에 의존합니다.
  • ktime_get_seconds()timekeeperktime_sec 필드를 직접 읽습니다. 하드웨어 접근 없이 ~1ns 비용입니다. 초 이하 정밀도가 불필요한 경로에서 ktime_get() 대신 사용하면 성능 이점이 큽니다.
  • coarse 시리즈틱마다 갱신되는 캐시 값을 반환합니다. vDSO에서도 CLOCK_MONOTONIC_COARSE / CLOCK_REALTIME_COARSE로 접근 가능합니다.
  • _ns() 래퍼ktime_get()ktime_t(s64)를 반환하는 반면, ktime_get_ns()u64 나노초를 반환합니다. 내부 경로는 동일하며 반환 타입만 다릅니다.
폐기된(Deprecated) 함수 매핑: 커널 v5.x 이후 통합된 ktime API로 전환되면서 레거시 함수들이 제거되었습니다. 이전 드라이버 코드를 포팅할 때 참고하세요.
폐기된 함수대체 함수
getrawmonotonic64()ktime_get_raw_ts64()
get_monotonic_boottime()ktime_get_boottime_ts64()
do_gettimeofday()ktime_get_real_ts64()
getnstimeofday64()ktime_get_real_ts64()

CLOCK_* 시계 체계

Linux 커널은 용도에 따라 여러 종류의 시계를 유지합니다. 각 시계는 기준점(epoch), NTP 보정 여부, suspend 동작이 다릅니다.

시계기준점NTP 보정suspend윤초설명
CLOCK_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프로세스(Process) 생성X정지X프로세스 CPU 시간 (user+sys)
CLOCK_THREAD_CPUTIME_ID스레드(Thread) 생성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 기반드물게 불일치 가능
컨테이너(Container) 마이그레이션CLOCK_MONOTONIC + timensTime namespace 오프셋(Offset) 적용REALTIME은 오프셋 불가

시계 간 관계

설명 요약:
  • 시계 간 관계:
  • REALTIME = MONOTONIC + wall_to_monotonic + 윤초
  • BOOTTIME = MONOTONIC + total_sleep_time
  • TAI = REALTIME + tai_offset (현재 37초)
  • RAW = 하드웨어 카운터 × 주파수 변환 (NTP 보정 없음)
  • 시간 흐름 예시 (suspend 포함):
  • 부팅 10s suspend(5s) 깨어남 20s
  • MONOTONIC: 0 → 10 (정지) 10 → 20
  • BOOTTIME: 0 → 10 (진행) 15 → 25
  • REALTIME: T → T+10 (진행) T+15 → T+25
  • RAW: 0 → 10* (정지) 10* → 20*
  • (* NTP 보정 없는 원시값)
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 값 */
};
코드 설명

include/linux/timekeeper_internal.h에 정의된 struct timekeeper는 Linux 시간 관리의 핵심 구조체로, 모든 CLOCK_* 시계의 현재 값을 유지합니다. 전역 인스턴스 tk_core.timekeeperseqcount로 보호됩니다.

  • tkr_mono / tkr_raw각각 MONOTONIC과 MONOTONIC_RAW 시계의 읽기 기반 정보를 담는 tk_read_base 구조체입니다. NTP 보정 여부에 따라 mult 값이 다릅니다.
  • xtime_secCLOCK_REALTIME의 초 부분(Unix epoch 기준)입니다. 나노초 부분은 tkr_mono.xtime_nsec에서 오프셋을 더해 계산합니다.
  • wall_to_monotonicREALTIME에서 MONOTONIC으로의 오프셋입니다. MONOTONIC = REALTIME + wall_to_monotonic 관계가 성립합니다.
  • offs_real / offs_boot / offs_taiMONOTONIC 기준으로 각 시계(REALTIME, BOOTTIME, TAI)의 오프셋을 ktime_t로 저장합니다. ktime_get_real() 등이 이 오프셋을 더해 결과를 반환합니다.
  • ntp_errorNTP 보정에서 발생하는 누적 오차입니다. update_wall_time()에서 매 틱마다 이 오차를 mult 조정에 반영합니다.
  • total_sleep_timesuspend 동안 경과한 총 시간입니다. CLOCK_BOOTTIME은 이 값을 MONOTONIC에 더하여 계산됩니다.
  • tk_read_base.cycle_last마지막으로 읽은 하드웨어 카운터 값입니다. 현재 사이클과의 차분(delta)을 구해 경과 나노초를 계산합니다.
  • tk_read_base.mult / shift사이클을 나노초로 변환하는 고정소수점 파라미터입니다. NTP가 tkr_mono.mult를 미세 조정하여 주파수 보정을 수행합니다.
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)스핀 대기 (경합(Contention) 시)
쓰기 비용카운터 증가만락 획득/해제
읽기 동시성무제한 병렬 읽기읽기도 배타적 (spinlock의 경우)
일관성 보장retry 기반 — 불일치 시 재시도항상 일관된 스냅샷
NMI/하드IRQ 안전읽기 안전 (fast 변형)불안전 (데드락 위험)
적합 시나리오읽기 빈도 >> 쓰기 빈도읽기/쓰기 빈도 유사
timekeeper 선택 이유ktime_get() 호출 빈도 극히 높음

시간 읽기 흐름

/* ktime_get() 내부 동작 (간략화): */
ktime_t ktime_get(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    ktime_t base;
    u64 delta, nsec;
    unsigned int seq;

    do {
        seq = read_seqcount_begin(&tk_core.seq);

        /* 1. 기준 시간 읽기 (마지막 업데이트 시점의 값) */
        base = tk->tkr_mono.base;

        /* 2. 하드웨어 카운터 읽기 */
        u64 cycle_now = tk->tkr_mono.clock->read(tk->tkr_mono.clock);

        /* 3. 마지막 읽기 이후 경과한 사이클 계산 */
        delta = (cycle_now - tk->tkr_mono.cycle_last) & tk->tkr_mono.mask;

        /* 4. 사이클 → 나노초 변환
         *    nsec = (delta * mult) >> shift
         *    mult와 shift는 NTP 보정이 반영된 값 */
        nsec = delta * tk->tkr_mono.mult;
        nsec >>= tk->tkr_mono.shift;

    } while (read_seqcount_retry(&tk_core.seq, seq));

    /* 5. 기준 시간 + 경과 나노초 = 현재 시간 */
    return ktime_add_ns(base, nsec);
}

/*
 * seqcount를 사용하는 이유:
 * - timekeeper 업데이트(update_wall_time)는 틱 인터럽트에서 수행
 * - 읽기(ktime_get)는 아무 컨텍스트에서 호출 가능
 * - seqcount로 락 없이 일관된 스냅샷 보장
 * - 쓰기 중 읽으면 retry
 */
코드 설명

kernel/time/timekeeping.cktime_get()은 CLOCK_MONOTONIC 시간을 반환하는 가장 일반적인 커널 시간 읽기 함수입니다. seqcount 기반의 락-프리 읽기 패턴을 사용합니다.

  • read_seqcount_begin()seqcount의 현재 시퀀스 번호를 읽습니다. 이 값이 홀수이면 쓰기 중이므로 즉시 retry합니다.
  • tkr_mono.base마지막 update_wall_time()에서 계산된 기준 시간(ktime_t)입니다. 이 값에 경과 나노초를 더해 현재 시간을 구합니다.
  • clock->read()활성 clocksource의 하드웨어 카운터를 읽습니다. TSC의 경우 rdtsc_ordered()가 호출되어 ~20ns 내에 완료됩니다.
  • delta & mask현재 사이클에서 마지막 사이클을 빼고 마스크를 적용합니다. 마스크는 카운터 wrap-around를 올바르게 처리합니다.
  • (delta * mult) >> shift사이클 차분을 나노초로 변환하는 고정소수점 연산입니다. 나눗셈 없이 곱셈과 시프트만으로 고정밀 변환을 수행합니다.
  • read_seqcount_retry()읽기 도중 update_wall_time()이 timekeeper를 갱신했는지 검사합니다. 시퀀스 번호가 변경되었으면 처음부터 다시 읽습니다.
  • ktime_add_ns(base, nsec)기준 시간에 경과 나노초를 더하여 최종 MONOTONIC 시간을 반환합니다.

ktime_get_with_offset() 통합 게터 패턴

커널의 다양한 시간 조회 함수 — ktime_get_real(), ktime_get_boottime(), ktime_get_clocktai() — 는 모두 내부적으로 ktime_get_with_offset()이라는 단일 함수를 호출합니다. 이 통합 패턴은 코드 중복을 제거하고, 각 클럭이 단조 시간(Monotonic Time)에 특정 오프셋을 더하는 방식임을 명확히 보여줍니다.

/* include/linux/timekeeper_internal.h */
enum tk_offsets {
    TK_OFFS_REAL,   /* wall clock = mono + offs_real */
    TK_OFFS_BOOT,   /* boot time  = mono + offs_boot */
    TK_OFFS_TAI,    /* TAI time   = mono + offs_tai  */
    TK_OFFS_MAX,
};

/* kernel/time/timekeeping.c */
ktime_t ktime_get_with_offset(enum tk_offsets offs)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned int seq;
    ktime_t base, *offset = offsets[offs];
    u64 nsecs;

    do {
        seq = read_seqcount_begin(&tk_core.seq);
        base = ktime_add(tk->tkr_mono.base, *offset);
        nsecs = timekeeping_get_ns(&tk->tkr_mono);
    } while (read_seqcount_retry(&tk_core.seq, seq));

    return ktime_add_ns(base, nsecs);
}

/* 각 클럭 조회 함수는 단순 래퍼 */
ktime_t ktime_get_real(void)     { return ktime_get_with_offset(TK_OFFS_REAL); }
ktime_t ktime_get_boottime(void) { return ktime_get_with_offset(TK_OFFS_BOOT); }
ktime_t ktime_get_clocktai(void) { return ktime_get_with_offset(TK_OFFS_TAI);  }
코드 설명
  • offsets 배열tk_offsets 열거형 값을 인덱스로 사용하여 offs_real, offs_boot, offs_tai 포인터에 접근합니다.
  • base + nsecs 패턴tkr_mono.base는 마지막 업데이트 시점의 단조 시간이고, timekeeping_get_ns()가 그 이후 경과한 나노초 델타를 계산합니다. 여기에 요청된 오프셋을 더하면 최종 시간이 완성됩니다.
  • 래퍼 함수ktime_get_real() 등은 ktime_get_with_offset()에 적절한 열거형 값을 전달하는 한 줄짜리 래퍼입니다. 시간 조회 로직의 유지보수 지점이 단일화됩니다.

timekeeping_get_ns() 델타 계산 헬퍼

timekeeping_get_ns()는 마지막 타임키퍼 업데이트 이후 경과한 나노초를 계산하는 내부 헬퍼 함수입니다. 클럭소스의 현재 사이클 값을 읽고, cycle_last와의 차이를 나노초로 변환합니다.

/* kernel/time/timekeeping.c */
static inline u64 timekeeping_get_ns(
    const struct tk_read_base *tkr)
{
    u64 delta, nsec;

    /* 클럭소스에서 현재 사이클 읽기 */
    u64 now = tk_clock_read(tkr);

    /* cycle_last 이후 경과 사이클 계산 (마스크 적용) */
    delta = clocksource_delta(now, tkr->cycle_last, tkr->mask);

    /* max_cycles 클램핑: 비정상 래핑 방지 */
    if (delta > tkr->clock->max_cycles)
        delta = tkr->clock->max_cycles;

    /* 사이클 → 나노초 변환 (고정 소수점 연산) */
    nsec = (delta * tkr->mult) >> tkr->shift;
    return nsec;
}
코드 설명
  • clocksource_delta()(now - last) & mask 패턴은 카운터 래핑을 자연스럽게 처리합니다. 32비트 카운터가 0xFFFFFFFF → 0x00000003으로 넘어가도 마스크 적용 후 정확히 4 사이클이 됩니다.
  • max_cycles 클램핑클럭소스가 불안정하거나 읽기 순서가 역전되어 비정상적으로 큰 델타가 발생할 수 있습니다. max_cycles는 등록 시 계산된 상한값으로, 시간 역전(time warp)을 방지합니다.
  • 고정 소수점 변환delta * mult >> shift는 나눗셈 없이 사이클을 나노초로 변환합니다. NTP 보정 시 mult가 미세 조정됩니다.

timekeeping_advance() 업데이트 경로

타임키퍼는 틱 인터럽트마다 timekeeping_advance()를 통해 갱신됩니다. 이 함수는 축적된 클럭소스 사이클을 나노초로 변환하고, NTP 보정을 적용하며, 초 단위 오버플로우를 처리합니다. 대수적 축적 루프(logarithmic accumulation)를 사용하여 긴 유휴(long idle) 후에도 효율적으로 시간을 따라잡습니다.

/* 호출 체인: tick_sched_timer → tick_sched_do_timer → timekeeping_advance */

/* kernel/time/timekeeping.c (간소화) */
static void timekeeping_advance(enum timekeeping_adv_mode mode)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    u64 offset;
    int shift;

    raw_spin_lock_irqsave(&tk_core.lock, flags);

    offset = clocksource_delta(
        tk_clock_read(&tk->tkr_mono),
        tk->tkr_mono.cycle_last, tk->tkr_mono.mask);

    /* 최소 1 tick 이상 축적되지 않으면 리턴 */
    if (offset < tk->cycle_interval && mode == TK_ADV_TICK)
        goto out;

    /* 대수적 축적: shift부터 시작하여 큰 단위로 먼저 축적,
     * shift를 줄여가며 나머지를 처리 → O(log n) 반복 */
    shift = tk->ntp_error_shift;
    while (offset >= tk->cycle_interval) {
        offset = logarithmic_accumulation(tk, offset, shift);
        if (shift) shift--;
    }

    /* NTP 보정 적용: mult 미세 조정 */
    timekeeping_adjust(tk, offset);

    /* xtime_nsec → xtime_sec 승격 (1초 이상 축적 시) */
    clock_set |= accumulate_nsecs_to_secs(tk);

    write_seqcount_begin(&tk_core.seq);
    timekeeping_update_staged(tk, clock_set);
    write_seqcount_end(&tk_core.seq);
out:
    raw_spin_unlock_irqrestore(&tk_core.lock, flags);
}

/* 초 단위 오버플로우 처리 */
static inline unsigned int accumulate_nsecs_to_secs(struct timekeeper *tk)
{
    u64 nsecps = (u64)NSEC_PER_SEC << tk->tkr_mono.shift;
    while (tk->tkr_mono.xtime_nsec >= nsecps) {
        tk->tkr_mono.xtime_nsec -= nsecps;
        tk->xtime_sec++;
        if (unlikely(ntp_leap_second(tk)))
            clock_set = TK_CLOCK_WAS_SET;  /* 윤초 처리 */
    }
    return clock_set;
}
코드 설명
  • 대수적 축적 루프시스템이 오래 유휴 상태였다가 깨어나면 수백 ms의 틱이 밀려 있을 수 있습니다. 1ms씩 반복하는 대신, shift를 이용해 큰 배수(8ms→4ms→2ms→1ms 순)로 축적하여 O(log n) 반복으로 처리합니다.
  • timekeeping_adjust()NTP 데몬이 계산한 주파수 보정값을 mult에 반영합니다. ntp_tick_length()가 반환하는 목표 틱 길이와 현재 값의 차이를 기반으로 mult를 미세 조정합니다.
  • accumulate_nsecs_to_secs()xtime_nsec이 1초를 초과하면 xtime_sec를 1 증가시키고 나노초 부분에서 차감합니다. 이 시점에서 윤초 삽입/삭제도 처리됩니다.

부팅 시 Timekeeping 초기화 순서

리눅스 커널의 타임키핑은 부팅 과정에서 단계적으로 초기화됩니다. 초기에는 정밀도가 낮은 jiffies 클럭소스로 시작하여, 하드웨어 클럭소스가 보정(calibration)을 마치면 순차적으로 승격됩니다.

시간 timekeeping_init() jiffies 클럭소스 read_persistent_clock64() RTC → xtime_sec late_time_init() TSC 보정 & 등록 clocksource_done_booting() 최종 클럭소스 선택 클럭소스 전환: jiffies (rating=1) TSC (rating=300) Refined TSC (late_initcall) 1. timekeeping_init() — jiffies 기반 시작, RTC 시각 읽기 2. late_time_init() → x86: tsc_init() → TSC 주파수 보정 → clocksource 등록 (rating=300) 3. clocksource_done_booting() (fs_initcall) → clocksource_select() → 최고 rating 소스 활성화
부팅 시 Timekeeping 초기화 순서 — jiffies에서 시작하여 TSC 보정 완료 후 최적 클럭소스로 전환
/* kernel/time/timekeeping.c */
void __init timekeeping_init(void)
{
    struct timespec64 wall;
    struct clocksource *clock;

    read_persistent_clock64(&wall);              /* RTC 읽기 */
    clock = clocksource_default_clock();          /* jiffies */
    tk_setup_internals(tk, clock);

    tk->xtime_sec = wall.tv_sec;
    tk->wall_to_monotonic = timespec64_sub(
        (struct timespec64){ 0 }, wall);
    tk->offs_real = ktime_set(wall.tv_sec, wall.tv_nsec);
}

/* kernel/time/clocksource.c — 부팅 완료 후 최종 선택 */
static int __init clocksource_done_booting(void)
{
    finished_booting = 1;
    clocksource_select();  /* rating 기준 최고 소스 선택 */
    return 0;
}
fs_initcall(clocksource_done_booting);
코드 설명
  • timekeeping_init()start_kernel() 초반에 호출됩니다. jiffies 클럭소스(rating=1)로 시작하고, RTC에서 벽시계 시간을 읽어 xtime_sec에 설정합니다.
  • wall_to_monotonicCLOCK_MONOTONIC 기준점을 결정하는 오프셋으로, 벽시계 시간의 음수값으로 초기화됩니다. settimeofday() 호출 시에도 단조 시간이 영향받지 않도록 함께 조정됩니다.
  • late_time_init()x86에서 tsc_init()을 호출하여 TSC 주파수를 PIT 또는 HPET 기준으로 보정합니다. 성공하면 TSC가 rating=300으로 등록됩니다.
  • clocksource_done_booting()fs_initcall 단계에서 clocksource_select()를 호출하여 최고 rating 클럭소스를 활성화합니다.

do_settimeofday64() / do_adjtimex() 내부 경로

clock_settime()이나 adjtimex() 시스템 호출로 시간을 변경하면, 커널 내부에서는 do_settimeofday64() 또는 do_adjtimex()가 호출됩니다. 전자는 벽시계 시간을 직접 설정하고, 후자는 NTP 데몬이 주파수 보정과 점진적 시간 조정을 수행할 때 사용됩니다.

/* kernel/time/timekeeping.c */
int do_settimeofday64(const struct timespec64 *ts)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    struct timespec64 ts_delta, xt;

    if (!timespec64_valid_settod(ts))
        return -EINVAL;

    raw_spin_lock_irqsave(&tk_core.lock, flags);
    timekeeping_forward_now(tk);  /* 최신 상태로 갱신 */

    xt = tk_xtime(tk);
    ts_delta = timespec64_sub(*ts, xt);

    /* wall_to_monotonic 조정: MONOTONIC은 불변 유지 */
    tk->wall_to_monotonic = timespec64_sub(
        tk->wall_to_monotonic, ts_delta);

    tk->xtime_sec = ts->tv_sec;
    tk->tkr_mono.xtime_nsec = (u64)ts->tv_nsec << tk->tkr_mono.shift;

    timekeeping_update_otherc_offsets(tk, TK_CLOCK_WAS_SET);
    write_seqcount_begin(&tk_core.seq);
    timekeeping_update_staged(tk, TK_CLOCK_WAS_SET);
    write_seqcount_end(&tk_core.seq);
    raw_spin_unlock_irqrestore(&tk_core.lock, flags);

    clock_was_set(TK_CLOCK_WAS_SET);  /* REALTIME 타이머 재평가 */
    return 0;
}

/* kernel/time/timekeeping.c */
int do_adjtimex(struct __kernel_timex *txc)
{
    struct timekeeper *tk = &tk_core.timekeeper;

    raw_spin_lock_irqsave(&tk_core.lock, flags);
    timekeeping_forward_now(tk);

    /* NTP 코어로 위임: ntp.c의 __do_adjtimex() */
    ret = __do_adjtimex(txc, &ts, &tai);

    if (tai != tk->tai_offset) {
        tk->tai_offset = tai;
        timekeeping_update_otherc_offsets(tk, TK_CLOCK_WAS_SET);
    }

    write_seqcount_begin(&tk_core.seq);
    timekeeping_update_staged(tk, clock_set);
    write_seqcount_end(&tk_core.seq);
    raw_spin_unlock_irqrestore(&tk_core.lock, flags);

    if (clock_set) clock_was_set(clock_set);
    return ret;
}
코드 설명
  • do_settimeofday64()벽시계 시간을 직접 설정합니다. 핵심은 wall_to_monotonic을 역보정하여 CLOCK_MONOTONIC이 영향받지 않도록 하는 것입니다.
  • TK_CLOCK_WAS_SET벽시계 시간이 불연속적으로 변경되었음을 알립니다. clock_was_set()은 CLOCK_REALTIME 기반 타이머들을 재평가합니다.
  • do_adjtimex()NTP 데몬이 사용하는 함수입니다. 실제 보정 계산은 ntp.c__do_adjtimex()에 위임되며, 주파수/위상 보정과 TAI 오프셋 변경을 처리합니다.

사이클 → 나노초 변환 수학

/*
 * 하드웨어 카운터 사이클을 나노초로 변환:
 *
 *   ns = cycles × (10^9 / freq)
 *
 * 정수 연산으로 구현:
 *   ns = (cycles × mult) >> shift
 *
 * mult와 shift 계산:
 *   mult = (10^9 << shift) / freq
 *
 * 예: TSC 3.0 GHz, shift=24
 *   mult = (10^9 × 2^24) / (3 × 10^9)
 *        = 16777216 / 3 = 5592405
 *
 *   100 cycles → (100 × 5592405) >> 24
 *             = 559240500 >> 24 = 33 ns ≈ 33.33ns (정확)
 *
 * NTP 보정 시 mult 값을 미세 조정하여 주파수 보정
 */

/* clocksource 등록 시 mult/shift 자동 계산 */
clocks_calc_mult_shift(
    &cs->mult,      /* 출력: 곱셈 인수 */
    &cs->shift,     /* 출력: 시프트 인수 */
    cs->freq,       /* 입력: 클럭 주파수 (Hz) */
    NSEC_PER_SEC,   /* 10^9 */
    cs->max_idle_ns /* 최대 유휴 시간 */
);

Suspend/Resume 시간 복원

시스템이 suspend(S3/S2idle)에 진입하면 TSC·HPET 등 대부분의 하드웨어 카운터가 정지합니다. Resume 시 커널은 RTC를 읽어 suspend 동안 경과한 시간(sleep_length)을 계산하고, total_sleep_time과 각 시계 오프셋을 복원합니다. 이 과정이 실패하면 CLOCK_BOOTTIMECLOCK_REALTIME이 어긋나는 심각한 버그가 발생합니다.

Suspend/Resume 시간 복원 흐름 Suspend 진입 1. timekeeping_suspend() 호출 2. RTC에서 현재 시각 읽기 (suspend_rtc) 3. 모든 clocksource에 suspend() 콜백 호출 → TSC/HPET/카운터 정지 (하드웨어 전원 차단) SLEEP (RTC만 배터리로 동작, 다른 카운터 모두 정지) Resume 4. timekeeping_resume() 호출 5. RTC 다시 읽기 (resume_rtc) 6. sleep_length = resume_rtc - suspend_rtc 7. clocksource resume() + cycle_last 재설정 시계 오프셋 업데이트 (step 8) total_sleep_time += sleep_length → CLOCK_BOOTTIME 갱신 offs_real += sleep_length → CLOCK_REALTIME 갱신 CLOCK_MONOTONIC: 변경 없음 suspend 중 정지, resume 후 이어서 진행 vdso_data 업데이트 사용자 공간 vDSO 데이터 동기화
Suspend/Resume 시간 복원 흐름 — suspend 시 RTC 시각을 기록하고, resume 시 경과 시간(sleep_length)을 계산하여 BOOTTIME과 REALTIME을 복원. MONOTONIC은 정지 상태를 유지
/* kernel/time/timekeeping.c — timekeeping_resume() 핵심 로직 (간략화) */

static void timekeeping_resume(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    struct timespec64 ts_new, ts_delta;

    /* 1. RTC에서 현재 시각 읽기 */
    read_persistent_clock64(&ts_new);

    /* 2. suspend 동안 경과한 시간 계산 */
    ts_delta = timespec64_sub(ts_new, timekeeping_suspend_time);

    /* 3. sleep 시간이 유효한지 검증 (음수 또는 너무 큰 값 거부) */
    if (ts_delta.tv_sec < 0 || ts_delta.tv_sec > 365 * 24 * 3600)
        ts_delta = (struct timespec64){ 0, 0 };

    /* 4. 시계 오프셋 업데이트 */
    __timekeeping_inject_sleeptime(tk, &ts_delta);
    /*   ├── total_sleep_time += ts_delta  (BOOTTIME용)
     *   ├── offs_real += ts_delta          (REALTIME용)
     *   ├── offs_boot += ts_delta          (BOOTTIME용)
     *   └── MONOTONIC은 변경 없음 (정지 상태 유지) */

    /* 5. clocksource의 cycle_last 재설정 */
    tk->tkr_mono.cycle_last = tk->tkr_mono.clock->read(tk->tkr_mono.clock);
    tk->tkr_raw.cycle_last  = tk->tkr_raw.clock->read(tk->tkr_raw.clock);

    /* 6. vdso_data 업데이트 */
    update_vsyscall(tk);

    /* 7. clockevent 재활성화 */
    tick_resume();
}
코드 설명

kernel/time/timekeeping.ctimekeeping_resume()은 시스템이 suspend에서 깨어날 때 호출되어 모든 시계 오프셋을 복원합니다.

  • read_persistent_clock64()RTC(배터리 구동 실시간 클럭)에서 현재 벽시계 시각을 읽습니다. RTC는 suspend 중에도 동작하므로 경과 시간 계산의 기준이 됩니다.
  • timespec64_sub(ts_new, suspend_time)resume 시 RTC 시각에서 suspend 진입 시 기록한 RTC 시각을 빼서 sleep 경과 시간(ts_delta)을 계산합니다.
  • ts_delta 유효성 검증sleep 시간이 음수이거나 1년을 초과하면 RTC 오류로 판단하고 0으로 처리합니다. RTC 배터리 방전 시 이 경로를 탑니다.
  • __timekeeping_inject_sleeptime()total_sleep_time, offs_real, offs_boot에 sleep 경과 시간을 더합니다. CLOCK_BOOTTIME과 CLOCK_REALTIME이 이 업데이트를 반영하고, CLOCK_MONOTONIC은 변경하지 않습니다.
  • cycle_last 재설정clocksource의 cycle_last를 현재 카운터 값으로 갱신합니다. TSC가 resume 후 리셋되었을 수 있으므로 이전 값과의 차분이 엉뚱한 결과를 내지 않도록 합니다.
  • update_vsyscall()vDSO 공유 페이지의 시간 데이터를 갱신하여 사용자 공간에서도 올바른 시간을 읽을 수 있게 합니다.
  • tick_resume()clockevent 장치를 재활성화하여 틱 인터럽트가 다시 발생하도록 합니다. 이후 정상적인 update_wall_time() 사이클이 재개됩니다.
Suspend/Resume 시간 문제 진단:
  • RTC 배터리 방전 — RTC가 정확한 시간을 유지하지 못하면 sleep_length가 잘못 계산됩니다. hwclock --show로 확인
  • RTC 드라이버 미등록read_persistent_clock64()가 실패하면 sleep 시간을 0으로 처리합니다. ls /dev/rtc*로 확인
  • TSC reset — 일부 플랫폼에서 resume 시 TSC가 0으로 리셋될 수 있습니다. cycle_last 재설정이 이를 처리하지만, 구형 커널에서는 시간 역행이 발생할 수 있습니다
  • BOOTTIME vs MONOTONIC 혼동 — suspend 후 타이머 폭발 증상은 대부분 CLOCK_MONOTONIC 기반 타이머를 CLOCK_BOOTTIME으로 변경하면 해결됩니다

Clocksource 프레임워크

clocksource 프레임워크는 하드웨어 타이머를 통일된 인터페이스로 추상화합니다. 시스템에 여러 clocksource가 등록되면 rating이 가장 높은 것이 자동 선택됩니다.

clocksource 구조체

/* include/linux/clocksource.h */
struct clocksource {
    u64   (*read)(struct clocksource *cs);  /* 카운터 읽기 함수 */
    u64   mask;                    /* 비트 마스크 (예: 0xFFFFFFFF) */
    u32   mult;                    /* 사이클→ns 곱셈 인수 */
    u32   shift;                   /* 사이클→ns 시프트 인수 */
    u64   max_idle_ns;             /* 최대 유휴 시간 (wrap 방지) */
    u32   maxadj;                  /* NTP 최대 조정 범위 */
    int   rating;                  /* 품질 등급 (높을수록 우선) */
    const char *name;               /* 이름 ("tsc", "hpet", ...) */
    unsigned long flags;            /* CLOCK_SOURCE_* 플래그 */
    int   (*enable)(struct clocksource *cs);
    void  (*disable)(struct clocksource *cs);
    void  (*suspend)(struct clocksource *cs);
    void  (*resume)(struct clocksource *cs);
    void  (*mark_unstable)(struct clocksource *cs);
    void  (*tick_stable)(struct clocksource *cs);
    struct list_head list;          /* 등록된 clocksource 리스트 */
    ...
};

/* rating 기준:
 *   1-99:    비적합 (테스트/폴백)
 *   100-199: 기본 (jiffies)
 *   200-299: 합리적 (ACPI PM)
 *   300-399: 양호 (HPET)
 *   400-499: 우수 (TSC)
 */
코드 설명

include/linux/clocksource.h에 정의된 struct clocksource는 하드웨어 카운터를 커널 timekeeping 프레임워크에 연결하는 추상화 계층입니다.

  • read()하드웨어 카운터의 현재 사이클 값을 반환하는 콜백입니다. TSC는 rdtsc, HPET는 MMIO 읽기를 사용합니다.
  • mask카운터의 유효 비트 마스크입니다. 예를 들어 32-bit 카운터는 0xFFFFFFFF이며, 사이클 차분 계산 시 wrap-around를 처리합니다.
  • mult / shift사이클을 나노초로 변환하는 고정소수점 인수입니다. ns = (cycles * mult) >> shift 공식으로 나눗셈 없이 변환합니다.
  • max_idle_ns카운터가 wrap-around하기 전 허용되는 최대 유휴 시간(나노초)입니다. tickless 커널에서 틱 간격 결정에 사용됩니다.
  • rating품질 등급으로, 높을수록 우선 선택됩니다. clocksource_select()가 리스트에서 가장 높은 rating의 소스를 활성화합니다.
  • flagsCLOCK_SOURCE_IS_CONTINUOUS(연속 카운터), CLOCK_SOURCE_MUST_VERIFY(watchdog 검증 필요) 등 동작 속성을 지정합니다.
  • suspend / resume시스템 suspend/resume 시 호출되는 콜백으로, 카운터 상태 저장/복원을 처리합니다.

커스텀 Clocksource 등록

#include <linux/clocksource.h>
#include <linux/io.h>

static void __iomem *timer_base;

static u64 my_clocksource_read(struct clocksource *cs)
{
    return (u64)readl(timer_base + MY_TIMER_CNT);
}

static struct clocksource my_clksrc = {
    .name   = "my-timer",
    .rating = 250,
    .read   = my_clocksource_read,
    .mask   = CLOCKSOURCE_MASK(32),
    .flags  = CLOCK_SOURCE_IS_CONTINUOUS,
};

static int __init my_timer_init(void)
{
    int ret;

    timer_base = ioremap(MY_TIMER_BASE, MY_TIMER_SIZE);
    if (!timer_base)
        return -ENOMEM;

    /* 타이머 하드웨어 초기화: free-running 모드 설정 */
    writel(TIMER_ENABLE | TIMER_FREERUN, timer_base + MY_TIMER_CTRL);

    /* mult/shift 자동 계산 후 등록 */
    ret = clocksource_register_hz(&my_clksrc, 24000000); /* 24MHz */
    if (ret) {
        iounmap(timer_base);
        return ret;
    }

    pr_info("my-timer: registered clocksource @ 24MHz\\n");
    return 0;
}

/*
 * clocksource_register_hz() vs clocksource_register_khz():
 *   - _hz: 정확한 주파수 지정 (오차 최소)
 *   - _khz: kHz 단위 (큰 주파수에서 오버플로 방지)
 *
 * 내부적으로 __clocksource_register_scale()이
 * clocks_calc_mult_shift()를 호출하여 mult/shift를 계산합니다.
 */
코드 설명

kernel/time/clocksource.c의 등록 API를 사용하여 커스텀 하드웨어 타이머를 clocksource로 등록하는 전형적인 패턴입니다.

  • my_clocksource_read()MMIO로 하드웨어 타이머 카운터 레지스터를 읽는 콜백입니다. readl()은 32-bit MMIO 읽기를 수행합니다.
  • .rating = 250ACPI PM(200)보다 높고 HPET(300)보다 낮은 등급입니다. 시스템에 더 높은 rating의 clocksource가 없으면 이 소스가 선택됩니다.
  • CLOCKSOURCE_MASK(32)32-bit 카운터의 비트 마스크(0xFFFFFFFF)를 생성합니다. 사이클 차분 계산 시 wrap-around를 올바르게 처리합니다.
  • CLOCK_SOURCE_IS_CONTINUOUS카운터가 free-running 모드로 연속 증가함을 표시합니다. 이 플래그가 없으면 timekeeping에 사용할 수 없습니다.
  • ioremap()물리 주소의 타이머 레지스터를 커널 가상 주소 공간에 매핑합니다. 드라이버 제거 시 iounmap()으로 해제해야 합니다.
  • clocksource_register_hz()주파수(Hz)를 받아 내부적으로 clocks_calc_mult_shift()를 호출하여 최적의 mult/shift 값을 자동 계산한 후 등록합니다.

Clocksource 선택/변경

# 현재 clocksource 확인
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

# 사용 가능한 clocksource 목록
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm

# clocksource 수동 변경 (디버깅/테스트용)
$ echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource

# 커널 커맨드라인으로 강제 지정
# clocksource=hpet    (HPET 강제)
# tsc=reliable         (TSC 안정성 신뢰)
# tsc=unstable         (TSC 불안정 표시 → 폴백)

Clocksource Watchdog

/*
 * clocksource watchdog는 0.5초마다 활성 clocksource를
 * 참조 clocksource와 비교하여 드리프트를 감지합니다.
 *
 * 드리프트 > 62.5 ppm → clocksource를 unstable로 표시 → 폴백
 *
 * 주로 TSC의 안정성 검증에 사용됩니다.
 * 불안정한 TSC (C-state 변경, 주파수 스케일링 등)가 감지되면
 * HPET 또는 ACPI PM으로 자동 전환됩니다.
 */

# watchdog 로그
$ dmesg | grep -i clocksource
clocksource: Switched to clocksource tsc
# 또는 불안정 시:
clocksource: timekeeping watchdog on CPU0: Marking clocksource 'tsc'
    as unstable because the skew is too large
clocksource: Switched to clocksource hpet
Clocksource 선택 & 워치독 흐름 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() 경로 */

Clocksource 내부 함수 상세

/* ── clocksource_mmio_init(): 임베디드/ARM MMIO 타이머 등록 헬퍼 ──
 * kernel/time/clocksource_mmio.c
 *
 * MMIO 기반 하드웨어 타이머를 간편하게 clocksource로 등록합니다.
 * ioremap + clocksource_register_hz()를 한 번에 처리합니다. */

/* 사전 정의된 MMIO 읽기 콜백들 */
static u64 clocksource_mmio_readl_up(struct clocksource *cs)
{
    return (u64)readl_relaxed(to_mmio_clksrc(cs)->reg);
}

static u64 clocksource_mmio_readl_down(struct clocksource *cs)
{
    return ~(u64)readl_relaxed(to_mmio_clksrc(cs)->reg) & cs->mask;
}

/* MMIO clocksource 등록 헬퍼 */
int clocksource_mmio_init(void __iomem *base, const char *name,
                         unsigned long hz, int rating, unsigned bits,
                         u64 (*read)(struct clocksource *))
{
    struct clocksource_mmio *cs;

    cs = kzalloc(sizeof(*cs), GFP_KERNEL);
    if (!cs)
        return -ENOMEM;

    cs->reg = base;
    cs->clksrc.name = name;
    cs->clksrc.rating = rating;
    cs->clksrc.read = read;
    cs->clksrc.mask = CLOCKSOURCE_MASK(bits);
    cs->clksrc.flags = CLOCK_SOURCE_IS_CONTINUOUS;

    return clocksource_register_hz(&cs->clksrc, hz);
}

/* DT 기반 ARM 타이머에서의 사용 예시 */
static int __init my_timer_init(struct device_node *np)
{
    void __iomem *base = of_iomap(np, 0);

    return clocksource_mmio_init(base + TIMER_COUNT_REG,
                                 "my-timer", 24000000,   /* 24MHz */
                                 200,                    /* rating */
                                 32,                     /* bits */
                                 clocksource_mmio_readl_up);
}
TIMER_OF_DECLARE(my_timer, "vendor,my-timer", my_timer_init);

/* ── clocksource_unregister(): clocksource 등록 해제 ──
 * kernel/time/clocksource.c
 *
 * 모듈형 clocksource 드라이버에서만 극히 드물게 사용됩니다.
 * 대부분의 clocksource는 커널 수명 동안 유지됩니다. */
int clocksource_unregister(struct clocksource *cs)
{
    mutex_lock(&clocksource_mutex);
    if (!list_empty(&cs->list))
        list_del_init(&cs->list);
    if (cs == curr_clocksource)
        clocksource_select();   /* 대체 소스로 재선택 */
    clocksource_dequeue_watchdog(cs);
    mutex_unlock(&clocksource_mutex);
    return 0;
}

/* ── clocksource_mark_unstable(): 수동 불안정 표시 ──
 * TSC가 CPU 핫플러그 시나리오에서 동기화를 잃을 때 사용됩니다.
 * arch/x86/kernel/tsc.c에서 호출하는 대표적인 API입니다. */
void clocksource_mark_unstable(struct clocksource *cs)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&watchdog_lock, flags);
    if (!(cs->flags & CLOCK_SOURCE_UNSTABLE)) {
        cs->flags |= CLOCK_SOURCE_UNSTABLE;
        list_del_init(&cs->wd_list);
        __clocksource_unstable(cs);  /* schedule_work()로 비동기 폴백 */
    }
    raw_spin_unlock_irqrestore(&watchdog_lock, flags);
}

/* ── CLOCKSOURCE_MASK() 매크로 ──
 * include/linux/clocksource.h
 *
 * 카운터 비트 수에 맞는 마스크를 생성합니다.
 * 사이클 차분 계산 시 wrap-around를 올바르게 처리합니다. */
#define CLOCKSOURCE_MASK(bits) \
    (u64)((bits) < 64 ? ((1ULL << (bits)) - 1) : ~0ULL)

/* 대표적인 값들:
 * CLOCKSOURCE_MASK(32) = 0x00000000FFFFFFFF  (32-bit MMIO 타이머)
 * CLOCKSOURCE_MASK(56) = 0x00FFFFFFFFFFFFFF  (HPET)
 * CLOCKSOURCE_MASK(64) = 0xFFFFFFFFFFFFFFFF  (TSC) */
코드 설명

Clocksource 서브시스템의 핵심 내부 함수와 매크로를 분석합니다. 대부분 kernel/time/clocksource.cinclude/linux/clocksource.h에 정의되어 있습니다.

  • clocksource_mmio_readl_up()카운터 값이 0에서 시작하여 증가하는 up-counting 타이머용 읽기 콜백입니다. readl_relaxed()로 MMIO 레지스터를 읽어 u64로 확장하여 반환합니다.
  • clocksource_mmio_readl_down()최대값에서 감소하는 down-counting 타이머용입니다. 비트 반전(~)과 마스크 AND 연산으로 up-counting 형태로 변환합니다.
  • clocksource_mmio_init()MMIO 기반 clocksource를 한 번의 호출로 등록합니다. 내부에서 kzalloc()으로 clocksource_mmio 구조체를 할당하고, 콜백·마스크·rating을 설정한 뒤 clocksource_register_hz()를 호출합니다.
  • TIMER_OF_DECLARE()Device Tree의 compatible 문자열과 초기화 함수를 연결하는 매크로입니다. 커널 부팅 시 DT에서 매칭되는 노드를 찾아 자동으로 초기화 함수를 호출합니다.
  • clocksource_unregister()리스트에서 제거 후, 현재 활성 소스였다면 clocksource_select()를 호출하여 차선 소스로 전환합니다. 모듈 제거 시에만 사용되며 실제로 호출되는 경우는 극히 드뭅니다.
  • clocksource_mark_unstable()watchdog 감시 외에 아키텍처 코드가 직접 불안정 표시를 할 때 사용합니다. x86 TSC에서 CPU 핫플러그 시 코어 간 동기화 보장이 깨지면 이 함수를 호출하여 HPET 등으로 폴백합니다.
  • __clocksource_unstable()schedule_work()를 통해 비동기로 clocksource_select()를 호출합니다. IRQ 컨텍스트에서 안전하게 폴백을 수행하기 위한 구조입니다.
  • CLOCKSOURCE_MASK(bits)bits < 64이면 (1ULL << bits) - 1로 하위 비트만 마스킹하고, 64이면 ~0ULL(전체 1)을 반환합니다. 카운터 overflow 시 차분 계산이 자연스럽게 wrap-around 됩니다.

clocks_calc_mult_shift() 상세 분석

/* kernel/time/clocksource.c
 * clocks_calc_mult_shift() — 주파수를 mult/shift 쌍으로 변환
 *
 * 알고리즘 핵심:
 *   nanoseconds = (cycles × mult) >> shift
 *   mult = (NSEC_PER_SEC << shift) / freq_hz
 *
 * overflow 없이 가능한 가장 큰 shift를 찾아 정밀도를 극대화합니다. */

void clocks_calc_mult_shift(u32 *mult, u32 *shift,
                            u32 from, u32 to, u32 maxsec)
{
    u64 tmp;
    u32 sft, sftacc = 32;

    /* from=freq_hz, to=NSEC_PER_SEC(10^9) */

    /* maxsec 동안의 최대 사이클 수가
     * tmp × mult에서 64-bit overflow를 일으키지 않는
     * 최대 shift를 찾습니다 */
    tmp = ((u64)maxsec * from) >> 32;
    while (tmp) {
        tmp >>= 1;
        sftacc--;
    }

    /* shift = 32부터 시작하여 sftacc까지 내려가며
     * overflow가 없는 최대 shift를 선택 */
    for (sft = 32; sft > 0; sft--) {
        tmp = (u64)to << sft;
        tmp += from / 2;      /* 반올림 */
        do_div(tmp, from);
        if ((tmp >> sftacc) == 0)
            break;
    }
    *mult = tmp;
    *shift = sft;
}

/* ── 실제 계산 예시 ──
 *
 * 【예시 1】 ARM 타이머 24MHz, maxsec=600
 *   sftacc: (600 × 24000000) >> 32 = 3 → 비트 수 2 → sftacc = 30
 *   sft=30: mult = (10^9 << 30) / 24000000 = 44739242
 *   검증: 1초 = (24000000 × 44739242) >> 30 ≈ 999999999ns ✓
 *
 * 【예시 2】 HPET 14.318MHz, maxsec=600
 *   sftacc: (600 × 14318180) >> 32 = 2 → sftacc = 30
 *   sft=30: mult = (10^9 << 30) / 14318180 = 75013970
 *   검증: 1초 = (14318180 × 75013970) >> 30 ≈ 999999995ns ✓
 *
 * 【예시 3】 TSC 3.0GHz, maxsec=600
 *   sftacc: (600 × 3000000000) >> 32 = 418 → 비트 수 9 → sftacc = 23
 *   sft=23: mult = (10^9 << 23) / 3000000000 = 2796202
 *   검증: 1초 = (3000000000 × 2796202) >> 23 ≈ 999999761ns ✓
 *
 * shift가 작을수록 정밀도가 낮아집니다 (TSC는 고주파 → 낮은 shift).
 * shift가 클수록 나노초 변환의 소수점 이하 정밀도가 높아집니다. */

/* clocksource_max_deferment(): mult/shift/mask로부터
 * 최대 허용 유휴 시간(max_idle_ns)을 계산합니다.
 * NO_HZ 모드에서 틱 없이 잠들 수 있는 최대 시간을 결정합니다. */
static u64 clocksource_max_deferment(struct clocksource *cs)
{
    u64 max_nsecs;

    /* mask 범위 내에서 mult 곱셈이 overflow하지 않는
     * 최대 사이클 수를 역산합니다 */
    max_nsecs = clocks_calc_max_nsecs(cs->mult, cs->shift, 0,
                                       cs->mask, NULL);
    /* 안전 마진 50% 적용 */
    return max_nsecs - (max_nsecs >> 5);  /* ×(31/32) ≈ 96.875% */
}
코드 설명

clocks_calc_mult_shift()는 하드웨어 주파수를 나노초 변환 계수(mult/shift)로 바꾸는 핵심 알고리즘입니다. 모든 clocksource와 clock_event_device 등록 시 호출됩니다.

  • sftacc 계산maxsec × freq를 32-bit 오른쪽 시프트하여 상위 비트 수를 구합니다. 이 값이 클수록 사용 가능한 shift 범위가 줄어듭니다. sftaccmult가 사용할 수 있는 최대 비트 폭입니다.
  • shift 탐색 루프32부터 내려가며 (NSEC_PER_SEC << sft) / freqsftacc 비트 이내인 최대 sft를 찾습니다. from / 2를 더하여 반올림 오차를 최소화합니다.
  • 24MHz 예시ARM SoC 범용 타이머에서 흔한 주파수입니다. shift=30으로 충분한 정밀도를 확보하며, 1초 변환 오차가 1ns 미만입니다.
  • 3.0GHz TSC 예시고주파 TSC는 maxsec × freq가 크므로 shift=23까지 낮아집니다. 변환 오차가 ~239ns/초로 저주파 대비 정밀도가 다소 떨어지지만, TSC 자체의 sub-ns 해상도가 이를 보상합니다.
  • clocksource_max_deferment()mask 범위와 mult 곱셈의 64-bit 한계로부터 역산하여 틱 없이 유지할 수 있는 최대 나노초를 계산합니다. NO_HZ_FULL 모드에서 타이머 인터럽트 간격의 상한을 결정합니다.
  • 안전 마진 96.875%이론적 최대값의 약 97%만 사용합니다. wrap-around 직전의 불안정 구간을 피하기 위한 것으로, >> 5/ 32와 동일합니다.

clocksource_delta() 함수

/* include/linux/clocksource.h
 * clocksource_delta() — 두 사이클 값의 차분을 안전하게 계산
 *
 * 기본 수식: (now - last) & mask
 * mask를 적용하여 카운터 wrap-around를 자동 처리합니다. */

static inline u64 clocksource_delta(
    u64 now, u64 last, u64 mask)
{
    u64 ret = (now - last) & mask;

#ifdef CONFIG_CLOCKSOURCE_VALIDATE_LAST_CYCLE
    /* 음수 델타 방지: now < last인 경우 0 반환
     * suspend/resume 전환 중 clocksource 읽기가
     * 이전 값보다 작은 값을 반환할 수 있습니다 */
    if (ret & ~(mask >> 1))
        return 0;
#endif

    /* max_cycles 클램핑: 비정상적으로 큰 델타 차단
     * MMIO 읽기 실패(0 반환)나 suspend 중 긴 간격으로
     * 인해 발생할 수 있는 시간 점프를 방지합니다 */
    if (ret > cs->max_cycles)
        return cs->max_cycles;

    return ret;
}

/* 사용 위치: timekeeping_get_ns()
 * kernel/time/timekeeping.c */
static u64 timekeeping_get_ns(struct tk_read_base *tkr)
{
    u64 delta, cycle_now;

    cycle_now = tkr->read(tkr->clock);
    delta = clocksource_delta(cycle_now, tkr->cycle_last,
                              tkr->mask);

    /* delta × mult >> shift → 나노초 변환 */
    return (delta * tkr->mult + tkr->xtime_nsec) >> tkr->shift;
}

/* ── 음수 델타가 발생하는 시나리오 ──
 *
 * 1. Suspend/Resume 전환: clocksource가 정지했다가 재시작 시
 *    카운터가 리셋되어 now < last가 될 수 있습니다.
 *
 * 2. MMIO 읽기 실패: 하드웨어 오류로 readl()이 0을 반환하면
 *    now=0, last=이전값 → 음수 델타가 됩니다.
 *
 * 3. CPU 마이그레이션(TSC): invariant TSC가 아닌 경우
 *    코어 간 TSC 값 불일치로 역전이 발생합니다.
 *
 * max_cycles는 clocksource_max_deferment()에서 계산한
 * max_idle_ns에 대응하는 사이클 수입니다.
 * 이 값을 초과하는 델타는 clocksource 오류로 간주합니다. */
코드 설명

clocksource_delta()timekeeping_get_ns()에서 매번 호출되는 인라인(Inline) 함수로, 시간 계산의 기초 연산을 담당합니다.

  • (now - last) & mask부호 없는 정수 감산 후 마스크를 적용합니다. 32-bit 카운터가 0xFFFFFFFF에서 0x00000000으로 넘어가도 차분이 올바르게 1이 됩니다.
  • CONFIG_CLOCKSOURCE_VALIDATE_LAST_CYCLE커널 v4.x에서 도입된 설정입니다. ret & ~(mask >> 1)은 마스크 범위의 상위 절반에 해당하는 델타를 음수로 판정합니다. 32-bit 카운터에서 0x80000000 이상의 차분은 역전으로 간주합니다.
  • max_cycles 클램핑max_cyclesclocksource_max_deferment()에서 역산한 값으로, 이를 초과하는 델타는 시간 변환 시 overflow를 유발할 수 있어 상한으로 잘라냅니다.
  • timekeeping_get_ns()ktime_get() 계열 함수의 최종 단계로, 델타 사이클을 나노초로 변환합니다. xtime_nsec는 이전 틱에서 누적된 잔여 나노초(고정소수점)입니다.
  • MMIO 읽기 실패PCIe 타이머(HPET 등)에서 링크 에러 시 readl()0xFFFFFFFF 또는 0x00000000을 반환할 수 있습니다. 이 경우 음수 델타 보호와 max_cycles 클램핑이 시간 역전을 방지합니다.

하드웨어 클럭 상세

하드웨어 클럭소스 비교 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 레지스터(Register) 상세

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로 매핑(Mapping)

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회 읽기) */
가상화(Virtualization) 환경에서의 ACPI PM Timer 장점: ACPI PM Timer는 I/O 포트 접근이므로 하이퍼바이저(Hypervisor)가 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

RTC (Real-Time Clock)

RTC(Real-Time Clock)는 시스템 전원이 꺼져 있는 동안에도 배터리(보통 CR2032 코인 셀)로 구동되어 벽시계 시간을 유지하는 하드웨어입니다. 커널 부팅 시 RTC에서 현재 시각을 읽어 CLOCK_REALTIME을 초기화하고, 주기적으로(11분 간격) 시스템 시간을 RTC에 역동기화합니다. suspend/resume 시에는 RTC가 경과 시간 복원의 핵심 기준점이 됩니다.

RTC 역할 개요 배터리 CR2032 RTC Hardware 32.768 kHz 크리스탈 전원 OFF에도 동작 1. 부팅 시 REALTIME 초기화 2. Resume 시 경과 시간 복원 3. 11분마다 역동기화 x86 CMOS RTC 레지스터 (I/O 포트: 0x70 인덱스, 0x71 데이터) 시간/날짜 레지스터 0x00: Seconds (0-59) 0x02: Minutes (0-59) 0x04: Hours (0-23 / 1-12+PM) 0x06: Day of Week (1-7) 0x07: Day of Month (1-31) 0x08: Month (1-12) 0x09: Year (0-99) 0x32: Century (19/20) — 선택적 제어/상태 레지스터 0x0A: Status Register A UIP(bit7): 업데이트 진행 중 0x0B: Status Register B SET(bit7): 업데이트 억제 PIE/AIE/UIE: 인터럽트 enable DM(bit2): 0=BCD, 1=Binary 24/12(bit1): 0=12h, 1=24h 0x0C: Status Register C 인터럽트 플래그 (읽으면 클리어) 0x0D: Status Register D VRT(bit7): 배터리 유효 여부
RTC 역할과 x86 CMOS RTC 레지스터 맵 — 32.768kHz 크리스탈로 구동되며, 부팅·Resume 시 시간 복원과 주기적 역동기화를 담당

RTC 유형 비교

유형인터페이스플랫폼정밀도대표 칩
CMOS RTCI/O 포트 0x70/0x71x86 (MC146818 호환)±1초/일MC146818, DS12887
I2C RTCI2C 버스임베디드, SBC±2~5 ppmDS3231 (TCXO, ±2ppm), PCF8563, MCP7940
SPI RTCSPI 버스산업용, 임베디드±2~5 ppmDS3234, PCF2123
SoC 내장 RTCMMIO (SoC 레지스터)ARM SoC칩 의존STM32 RTC, iMX SNVS, Allwinner RTC

Linux RTC 프레임워크

/* include/linux/rtc.h — RTC 장치 드라이버 인터페이스 */
struct rtc_class_ops {
    int (*read_time)(struct device *dev, struct rtc_time *tm);
    int (*set_time)(struct device *dev, struct rtc_time *tm);
    int (*read_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
    int (*set_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
    int (*alarm_irq_enable)(struct device *dev, unsigned int enabled);
    int (*read_offset)(struct device *dev, long *offset);
    int (*set_offset)(struct device *dev, long offset);
};

/* RTC 등록 API */
struct rtc_device *devm_rtc_device_register(
    struct device *dev, const char *name,
    const struct rtc_class_ops *ops,
    struct module *owner);

/* struct rtc_time — RTC 시간 표현 */
struct rtc_time {
    int tm_sec;    /* 초 (0-59) */
    int tm_min;    /* 분 (0-59) */
    int tm_hour;   /* 시 (0-23) */
    int tm_mday;   /* 일 (1-31) */
    int tm_mon;    /* 월 (0-11) ← 주의: 0-based */
    int tm_year;   /* 연 (1900 기준) ← 주의: 2024 = 124 */
    int tm_wday;   /* 요일 (0=일요일) */
    int tm_yday;   /* 연중 일 (0-365) */
    int tm_isdst;  /* DST 플래그 */
};

부팅 시 RTC → 시스템 시계 초기화

/*
 * CONFIG_RTC_HCTOSYS=y 설정 시,
 * late_initcall에서 RTC 시간을 읽어 CLOCK_REALTIME을 초기화합니다.
 *
 * drivers/rtc/hctosys.c — rtc_hctosys()
 */
static int __init rtc_hctosys(void)
{
    struct rtc_time tm;
    struct timespec64 tv;

    /* /dev/rtc0 (또는 CONFIG_RTC_HCTOSYS_DEVICE 지정 장치)에서 읽기 */
    err = rtc_read_time(rtc, &tm);
    rtc_tm_to_time64(&tm, &tv.tv_sec);
    tv.tv_nsec = 0;

    /* CLOCK_REALTIME 설정 */
    err = do_settimeofday64(&tv);
    dev_info(rtc->dev.parent,
             "setting system clock to %ptR UTC\n", &tm);
}
late_initcall(rtc_hctosys);

/*
 * CONFIG_RTC_SYSTOHC=y 설정 시,
 * NTP 동기화 상태에서 11분(660초)마다 시스템 시간을 RTC에 역기록합니다.
 *
 * kernel/time/ntp.c — sync_hw_clock()
 * 조건: STA_UNSYNC 플래그가 꺼져 있을 때만 (NTP 동기화 완료 상태)
 */
# RTC 시간 확인 (hwclock)
$ hwclock --show
2026-03-21 15:30:00.123456+09:00

# 시스템 시간 → RTC 동기화
$ hwclock --systohc

# RTC → 시스템 시간 동기화
$ hwclock --hctosys

# RTC를 UTC로 유지 (권장)
$ hwclock --systohc --utc

# sysfs를 통한 RTC 확인
$ cat /sys/class/rtc/rtc0/name
rtc_cmos
$ cat /sys/class/rtc/rtc0/date
2026-03-21
$ cat /sys/class/rtc/rtc0/time
06:30:00
$ cat /sys/class/rtc/rtc0/since_epoch
1774079400

# dmesg RTC 관련 로그
$ dmesg | grep -i rtc
rtc_cmos 00:01: RTC can wake from S4
rtc_cmos 00:01: setting system clock to 2026-03-21T06:30:00 UTC
RTC 시간대 주의: Linux는 RTC를 UTC로 유지하는 것이 표준입니다. Windows는 로컬 시간을 사용하므로, 듀얼 부팅 시 시간 충돌이 발생합니다. timedatectl set-local-rtc 0으로 UTC 모드를 확인하세요. RTC에 로컬 시간을 사용하면 DST 전환 시 시간이 이중 보정되는 버그가 발생할 수 있습니다.

Common Clock Framework (CCF)

Common Clock Framework(CCF)는 SoC 내부의 PLL, divider, gate, mux를 계층적으로 연결해 각 장치(UART, SPI, GPU, NPU 등)에 필요한 주파수를 공급하는 프레임워크입니다. 이름이 비슷해 혼동되지만 clocksource/timekeeper와 목적이 다릅니다. clocksource는 "현재 시각 계산", CCF는 "클럭 트리 설정과 전력 제어"가 핵심입니다.

프레임워크주요 질문핵심 자료구조대표 API
clocksource지금 몇 ns인가?struct 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 디버깅(Debugging) 체크리스트

# debugfs mount
$ mount -t debugfs none /sys/kernel/debug

# 전체 클럭 트리
$ cat /sys/kernel/debug/clk/clk_summary

# 핵심 필드 확인: enable_cnt / prepare_cnt / rate / accuracy
$ grep -E 'uart|spi|gpu' /sys/kernel/debug/clk/clk_summary

# cpufreq와 CCF를 함께 볼 때
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_driver
$ cat /sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq

CPU 주파수 관리 (cpufreq)

cpufreq는 policy 단위(보통 클러스터/패키지)로 최소/최대 주파수와 거버너를 적용합니다. 스케줄러(Scheduler)는 현재 CPU 부하를 util 값으로 제공하고, 거버너는 이를 바탕으로 목표 주파수를 계산해 드라이버(intel_pstate, amd_pstate, acpi-cpufreq)에 전달합니다.

cpufreq 결정 경로 (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낮은 주파수 선호긴 시스템 콜(System Call)/인터럽트 처리 지연 가능
schedutil스케줄러 util 기반 동적 조절일반 서버 기본값으로 균형이 좋음
ondemand주기적 샘플링 후 급상승짧은 버스(Bus)트 워크로드에서 출렁임 가능
실전 팁: 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는 커널이 사용자 공간에 매핑하는 공유 라이브러리(Shared Library)로, 시스템 콜 없이 시간 읽기를 수행합니다. 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
코드 설명

include/vdso/datapage.h에 정의된 struct vdso_data와 vDSO의 clock_gettime() 고속 경로입니다. 커널 진입(syscall) 없이 사용자 공간에서 직접 시간을 읽습니다.

  • vdso_data 페이지 매핑커널이 vdso_data 구조체를 포함하는 물리 페이지를 모든 프로세스의 가상 주소 공간에 읽기 전용으로 매핑합니다. update_vsyscall()이 매 틱마다 이 데이터를 갱신합니다.
  • seq (seqcount)커널의 timekeeper와 동일한 seqcount 패턴입니다. vDSO 코드가 읽는 동안 커널이 데이터를 갱신하면 seq가 변경되어 retry합니다.
  • cycle_last / mask / mult / shifttimekeeper의 tk_read_base에서 복사된 값입니다. vDSO 함수가 이 값들로 ktime_get()과 동일한 사이클→나노초 변환을 사용자 공간에서 수행합니다.
  • basetime[VDSO_BASES]각 CLOCK_* 시계(REALTIME, MONOTONIC, BOOTTIME 등)의 기준 시간을 배열로 저장합니다. vDSO가 요청된 clock_id에 맞는 기준 시간을 선택합니다.
  • clock_modevDSO가 지원하는 clocksource 모드입니다. TSC 기반(VDSO_CLOCKMODE_TSC)일 때 rdtsc로 직접 읽고, 미지원 모드이면 커널 syscall로 폴백합니다.
  • 성능 비교일반 clock_gettime() syscall은 ~100-200ns가 소요되지만, vDSO 경로는 커널 진입 오버헤드가 없어 x86 TSC 기준 ~20-30ns로 약 5-7배 빠릅니다.

vDSO 성능 측정

/* clock_gettime 벤치마크 */
#include <time.h>
#include <stdio.h>

int main(void) {
    struct timespec ts;
    int i;
    struct timespec start, end;

    clock_gettime(CLOCK_MONOTONIC, &start);
    for (i = 0; i < 10000000; i++)
        clock_gettime(CLOCK_MONOTONIC, &ts);
    clock_gettime(CLOCK_MONOTONIC, &end);

    long ns = (end.tv_sec - start.tv_sec) * 1000000000L
              + (end.tv_nsec - start.tv_nsec);
    printf("avg: %ld ns/call\\n", ns / 10000000);
    /* TSC vDSO: ~20-30ns, HPET syscall: ~600-1000ns */
}

/* 결과 비교 (x86, TSC):
 * CLOCK_MONOTONIC:       ~25ns  (vDSO + rdtsc)
 * CLOCK_MONOTONIC_COARSE: ~5ns  (vDSO, 카운터 읽기 불필요)
 * CLOCK_REALTIME:        ~25ns  (vDSO + rdtsc)
 * CLOCK_REALTIME_COARSE:  ~5ns  (vDSO)
 *
 * vDSO 비활성 또는 HPET clocksource 시:
 * CLOCK_MONOTONIC:      ~600ns  (syscall + MMIO)
 */

NTP 시간 동기화

NTP(Network Time Protocol)는 네트워크를 통해 시스템 시계를 외부 기준 시계에 동기화합니다. 커널의 NTP 서브시스템은 adjtimex() 시스템 콜을 통해 시간 보정을 수행합니다.

NTP/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 오프셋 (초) */
};

NTP 커널 내부 보정 경로

사용자 공간의 adjtimex() 시스템 콜이 커널 내부에서 어떤 경로를 거쳐 실제 클럭 주파수를 보정하는지 전체 흐름을 살펴봅니다. 핵심은 NTP 서브시스템이 tick_length를 조정하고, timekeeping 루프가 이를 mult에 반영하는 피드백 구조입니다.

do_adjtimex() → __do_adjtimex() 흐름

do_adjtimex()는 timekeeping.c에서 tk_core.seq 쓰기 잠금을 획득한 뒤, 실제 NTP 상태 머신 처리를 ntp.c의 __do_adjtimex()에 위임합니다.

/* kernel/time/timekeeping.c — 단순화 */
int do_adjtimex(struct __kernel_timex *txc)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned long flags;
    int ret;

    raw_spin_lock_irqsave(&tk_core.lock, flags);
    write_seqcount_begin(&tk_core.seq);

    /* NTP 상태 머신 처리 위임 */
    ret = __do_adjtimex(txc, &ts, &tai);

    /* NTP에서 변경된 tick_length를 timekeeping에 반영 */
    timekeeping_update(tk, TK_MIRROR | TK_CLOCK_WAS_SET);

    write_seqcount_end(&tk_core.seq);
    raw_spin_unlock_irqrestore(&tk_core.lock, flags);

    return ret;
}
코드 설명

do_adjtimex()는 사용자 공간에서 NTP 보정 요청이 들어올 때 진입하는 커널 최상위 함수입니다.

  • raw_spin_lock_irqsave / write_seqcount_beginseqcount 쓰기 잠금으로 timekeeping 구조체의 일관성을 보장합니다. 읽기 경로(ktime_get() 등)는 잠금 없이 seqcount 재시도로 동작합니다.
  • __do_adjtimex()ntp.c에 구현된 실제 NTP 상태 머신입니다. freq, offset, status 등을 처리하고 내부 NTP 변수를 갱신합니다.
  • timekeeping_update()NTP가 변경한 tick_length를 timekeeping 구조체에 반영합니다. TK_CLOCK_WAS_SET은 시간이 변경되었음을 알리는 플래그입니다.

ntp_update_frequency()

ADJ_FREQUENCY가 설정되면 호출되어 사용자가 지정한 주파수 오프셋을 tick_length 조정 값으로 변환합니다. 핵심 공식은 tick_length = tick_length_base + (freq_offset × tick_nsec) / 106입니다.

/* kernel/time/ntp.c — 단순화 */
static void ntp_update_frequency(void)
{
    u64 second_length;
    u64 tick_length_base;

    /* tick_usec(기본 10000)을 nsec 고정소수점으로 변환 */
    tick_length_base = (u64)tick_usec * NSEC_PER_USEC * NTP_SCALE;

    /* freq: 2^-16 ppm 단위 → 틱 길이에 반영 */
    second_length  = tick_length_base;
    second_length += (s64)time_freq * tick_nsec_scaled;
    second_length += (s64)time_freq >> (NTP_SCALE_SHIFT - 10);

    tick_length = second_length;
}
코드 설명

NTP 주파수 보정의 핵심 변환 함수입니다.

  • tick_length_base보정 없는 기본 틱 길이입니다. tick_usec(기본 10000μs = 10ms)를 나노초 고정소수점으로 변환합니다.
  • time_freq사용자가 adjtimex()freq 필드로 설정한 주파수 오프셋입니다. 2-16 ppm 단위로 표현됩니다.
  • second_length += time_freq * tick_nsec_scaled주파수 오프셋을 틱 길이의 증감으로 변환합니다. 양수이면 틱이 길어져 시계가 느려지고, 음수이면 빨라집니다.

ntp_tick_length()

ntp_tick_length()는 NTP가 보정한 틱 길이를 반환합니다. timekeeping_advance()가 매 틱마다 이 값을 읽어 mult 조정에 사용하므로, NTP 서브시스템과 timekeeping을 연결하는 핵심 브릿지 함수입니다.

/* kernel/time/ntp.c */
u64 ntp_tick_length(void)
{
    return tick_length;
}

/* kernel/time/timekeeping.c — timekeeping_advance()에서 사용 */
static void timekeeping_advance(...)
{
    /* 초 경계를 넘을 때마다 NTP 보정된 틱 길이 갱신 */
    while (tk->tkr_mono.xtime_nsec >= (u64)NSEC_PER_SEC << tk->tkr_mono.shift) {
        /* ... 초 증가 ... */
        second_overflow(tk->xtime_sec);
    }
    /* mult 조정으로 클럭 속도 미세 보정 */
    timekeeping_adjust(tk, offset);
}
코드 설명

ntp_tick_length()는 NTP 서브시스템과 timekeeping 사이의 유일한 인터페이스입니다.

  • tick_lengthntp_update_frequency()에서 계산된 NTP 보정 틱 길이입니다. 고정소수점 나노초 단위로, 1초의 길이가 NTP 주파수 보정에 따라 약간 길거나 짧아집니다.
  • second_overflow()초 경계를 넘을 때 호출되어 윤초 처리와 PLL/FLL 위상·주파수 보정을 수행합니다.
  • timekeeping_adjust()NTP 오차를 mult에 점진적으로 반영하여 클럭 속도를 조정합니다.

second_overflow()

second_overflow()timekeeping_advance()에서 초 경계를 넘을 때마다 호출됩니다. 윤초 삽입/삭제(STA_INS/STA_DEL)를 처리하고, PLL/FLL 알고리즘으로 위상·주파수 오차를 보정합니다.

/* kernel/time/ntp.c — 단순화 */
static void second_overflow(time64_t secs)
{
    s64 time_adj;

    /* ── 윤초 처리 ── */
    switch (time_state) {
    case TIME_INS:
        if (!(secs % 86400)) {  /* 자정 UTC */
            time_state = TIME_OOP;
            /* 1초 삽입: 23:59:60 표시 */
            printk_deferred("Clock: inserting leap second");
        }
        break;
    case TIME_DEL:
        if (!((secs + 1) % 86400)) {
            time_state = TIME_WAIT;
            /* 1초 삭제: 23:59:59 건너뜀 */
            printk_deferred("Clock: deleting leap second");
        }
        break;
    }

    /* ── PLL/FLL 주파수 보정 ── */
    if (time_status & STA_FLL) {
        /* FLL: 큰 오프셋에 적합 — 오프셋을 직접 주파수로 변환 */
        time_adj = time_offset * FLL_SCALE;
    } else {
        /* PLL: 작은 오프셋에 적합 — 위상 오차를 시간 상수에 따라 보정 */
        time_adj = time_offset >> (time_constant + time_constant);
    }

    /* 보정 값을 주파수에 누적 */
    time_freq += time_adj;
    time_freq = min(time_freq, MAXFREQ_SCALED);
    time_freq = max(time_freq, -MAXFREQ_SCALED);

    /* 갱신된 freq로 tick_length 재계산 */
    ntp_update_frequency();
}
코드 설명

second_overflow()는 NTP 보정 루프의 핵심으로, 매초 호출되어 윤초와 주파수 보정을 처리합니다.

  • TIME_INS / TIME_DEL윤초 삽입/삭제 상태입니다. UTC 자정에 1초를 추가하거나 삭제합니다. ntpd/chronyd가 STA_INS 또는 STA_DEL을 설정합니다.
  • STA_FLL 분기FLL(Frequency Locked Loop)은 큰 오프셋을 빠르게 보정하며, PLL(Phase Locked Loop)은 작은 오프셋을 정밀하게 보정합니다. time_constant가 6 이하이면 PLL, 초과하면 FLL이 선택됩니다.
  • time_adj = time_offset >> (time_constant + time_constant)PLL 보정량은 위상 오차를 시간 상수의 제곱으로 나눈 값입니다. 시간 상수가 클수록 보정이 느리고 안정적입니다.
  • MAXFREQ_SCALED주파수 보정의 상한(±500ppm)입니다. 이를 초과하는 보정은 잘리며, step 보정이 필요할 수 있음을 의미합니다.

timekeeping_adjust()

timekeeping_adjust()는 NTP 오차(ntp_error)를 clocksource의 mult에 점진적으로 반영합니다. 급격한 mult 변경은 시간 흐름에 지터(jitter)를 유발하므로, 1틱에 최대 ±1씩만 조정하는 점진적 방식을 사용합니다.

/* kernel/time/timekeeping.c — 단순화 */
static void timekeeping_adjust(struct timekeeper *tk, s64 offset)
{
    s64 error, interval, adj;

    /* NTP tick_length와 실제 tick 길이 차이 누적 */
    error  = ntp_tick_length() << tk->ntp_error_shift;
    error -= (tk->tkr_mono.xtime_nsec + offset) << tk->ntp_error_shift;
    tk->ntp_error += error;

    /* 오차가 interval의 절반 이상이면 mult ±1 조정 */
    interval = tk->cycle_interval << tk->ntp_error_shift;
    if (tk->ntp_error > interval >> 1) {
        adj = 1;                /* 시계가 느림 → mult 증가 */
        tk->ntp_error -= interval;
    } else if (tk->ntp_error < -(interval >> 1)) {
        adj = -1;               /* 시계가 빠름 → mult 감소 */
        tk->ntp_error += interval;
    } else {
        return;
    }
    tk->tkr_mono.mult += adj;
}
코드 설명

timekeeping_adjust()는 NTP 보정을 실제 클럭 속도에 반영하는 마지막 단계입니다.

  • ntp_tick_length()NTP가 기대하는 1틱의 길이입니다. 이 값과 실제 틱 길이의 차이가 오차로 누적됩니다.
  • ntp_errorNTP 기대 틱 길이와 실제 틱 길이의 누적 오차입니다. 이 값이 임계치를 초과하면 mult를 조정합니다.
  • mult += adj (±1)한 번에 1만 조정하여 지터를 최소화합니다. mult가 1 증가하면 1사이클당 나노초가 1 / 2^shift만큼 늘어납니다.
  • interval >> 1오차가 반 interval을 넘어야 조정하는 히스테리시스로, 작은 오차에 의한 불필요한 진동을 방지합니다.
NTP 커널 내부 보정 피드백 루프 ntpd / chronyd adjtimex() ntp_update_frequency() tick_length timekeeping_advance() second_overflow() timekeeping_adjust() mult 조정 실제 클럭 속도 변경 NTP 오차 측정 점선: 피드백 경로 | 실선: 순방향 보정 경로

NTP slew vs step 비교

NTP 보정에는 두 가지 방식이 있습니다. slew는 클럭 주파수를 미세 조정하여 점진적으로 시간을 맞추고, step은 시간을 즉시 점프시킵니다. 기본적으로 오프셋이 128ms(chronyd) 또는 0.5초(ntpd)를 초과하면 step이 적용됩니다.

NTP slew vs step 보정 비교 Slew (주파수 조정) 시간 → 오프셋 정확한 시간 시스템 시간 offset mult 값을 미세 조정 → 클럭 속도 가감속 Step (즉시 점프) 시간 → 시스템 시간 JUMP! offset settimeofday() 또는 ADJ_SETOFFSET 호출 slew vs step 비교 항목 Slew (점진적 보정) Step (즉시 보정) 동작 방식 clocksource mult 값 조정 settimeofday() 호출 적용 조건 오프셋 < 128ms (chronyd) 오프셋 ≥ 128ms (chronyd) 최대 slew 속도 ±500 ppm (maxerror 기반) 즉시 (제한 없음) 시간 역행 가능 불가 (단조 증가 유지) 가능 (CLOCK_REALTIME) 영향받는 시계 REALTIME + MONOTONIC(주파수) REALTIME만 (MONOTONIC 무관) 위험 낮음 — 점진적 보정 높음 — 로그 순서 역전, 타이머 폭발
NTP slew vs step 비교 — slew는 클럭 주파수를 미세 조정하여 안전하게 수렴, step은 즉시 점프하여 빠르지만 CLOCK_REALTIME 역행 위험
/*
 * NTP slew 보정의 커널 내부 동작:
 *
 * adjtimex(ADJ_FREQUENCY, freq_offset) 호출 시:
 * 1. freq_offset은 2^-16 ppm 단위 (65536 = 1 ppm)
 * 2. 커널이 clocksource의 mult 값을 조정:
 *    adjusted_mult = base_mult + (freq_offset * base_mult) / (10^6 * 2^16)
 * 3. 매 틱마다 조정된 mult로 ns 계산
 *    → 클럭이 실제보다 약간 빠르거나 느리게 진행
 * 4. 목표 시간에 도달하면 주파수를 원래대로 복원
 *
 * 예: +500 ppm slew 시
 *     1초당 0.5ms 빠르게 진행
 *     100ms 오프셋 보정에 ~200초 소요
 */

# chronyd의 step/slew 임계값 설정
# /etc/chrony.conf
makestep 1.0 3
# 의미: 처음 3번의 업데이트에서 오프셋이 1초 이상이면 step,
#        이후에는 항상 slew (시간 역행 방지)

# ntpd의 임계값
# 기본: 0.128초(128ms) 초과 시 step, 이하 시 slew
# -g 옵션: 첫 step 허용 (부팅 직후 큰 오프셋 보정)
# -x 옵션: step 금지 (항상 slew)

# step 발생 감지
$ journalctl -u chronyd | grep -i "system clock"
chronyd: System clock was stepped by -0.123456 seconds
코드 설명

kernel/time/ntp.ckernel/time/timekeeping.c가 협력하여 수행하는 NTP slew 보정의 커널 내부 메커니즘입니다.

  • adjtimex(ADJ_FREQUENCY)사용자 공간의 ntpd/chronyd가 이 시스템 콜을 통해 주파수 보정값을 커널에 전달합니다. 2^-16 ppm 단위로 65536이 1 ppm에 해당합니다.
  • adjusted_mult 계산커널은 clocksource의 기본 mult 값에 NTP 주파수 오프셋 비율을 더하여 조정된 mult를 산출합니다. 이로써 매 틱의 나노초 변환 결과가 미세하게 달라집니다.
  • slew 방식mult를 높이면 시간이 실제보다 빠르게 진행하고, 낮추면 느리게 진행합니다. 예를 들어 +500 ppm slew는 1초당 0.5ms씩 빠르게 진행하여 점진적으로 오프셋을 줄입니다.
  • step 방식오프셋이 임계값(chronyd 기본 128ms, ntpd 기본 0.5초)을 초과하면 do_settimeofday64()로 시간을 즉시 점프시킵니다. CLOCK_REALTIME이 역행할 수 있어 주의가 필요합니다.
  • makestep 1.0 3chronyd 설정으로, 처음 3번의 NTP 업데이트에서 오프셋이 1초 이상이면 step을 허용하고, 이후에는 항상 slew만 사용합니다. 서버 환경의 권장 설정입니다.
실전 권장 전략:
  • 서버/데이터센터makestep 1.0 3: 부팅 초기에만 step 허용, 운영 중에는 slew만 사용
  • 금융/HFTmakestep 0 0 + PTP: step 완전 금지, 나노초 slew만 허용
  • 데스크톱/랩톱makestep 1.0 -1: 항상 step 허용 (suspend/resume 후 큰 오프셋 가능)
  • 컨테이너 — 호스트에서만 NTP, 컨테이너 내부에서는 NTP 데몬 실행 금지 (충돌 방지)

윤초 (Leap Second) 처리

/*
 * 윤초 시 CLOCK_REALTIME 동작:
 *
 * 양의 윤초 (1초 삽입):
 *   23:59:59 → 23:59:60 → 00:00:00
 *   CLOCK_REALTIME이 1초 동안 정지하거나 smear
 *
 * 음의 윤초 (1초 삭제, 이론적):
 *   23:59:58 → 00:00:00 (23:59:59 건너뜀)
 *
 * 커널 처리 옵션:
 * 1. STA_INS/STA_DEL: NTP가 윤초 예고 → 커널이 자정에 처리
 * 2. Leap second smearing: 24시간에 걸쳐 1초를 분산 조정
 *    (Google/Amazon NTP 서버가 제공)
 *
 * 영향받지 않는 시계:
 * - CLOCK_MONOTONIC: 윤초 무관
 * - CLOCK_TAI: 윤초 없는 TAI 시간 (REALTIME + tai_offset)
 */

# 현재 TAI 오프셋 확인 (2024년 기준: 37초)
$ adjtimex --print | grep tai
         tai: 37

PTP (Precision Time Protocol)

PTP(IEEE 1588v2, 흔히 PTPv2)는 네트워크를 통해 서브 마이크로초~나노초 수준의 시간 동기화를 제공하는 프로토콜입니다. NTP가 밀리초 수준의 정확도를 제공하는 데 비해, PTP는 하드웨어 타임스탬핑을 활용하여 수십 나노초 이내의 동기화를 달성할 수 있습니다. 2002년 IEEE 1588 초판이 발표된 이후, 2008년 IEEE 1588-2008(PTPv2)에서 프로파일 지원, Transparent Clock, Peer Delay 메커니즘이 추가되어 현재 사실상 표준으로 자리잡았습니다. 2019년에는 IEEE 1588-2019가 발표되어 보안 TLV 등이 추가되었습니다.

PTP 적용 분야

분야요구 정확도대표 사례
통신 (Telecom)< 1.5 μ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 네트워크 토폴로지(Topology)

실제 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로 결정합니다. 비교는 다음 순서로 진행됩니다:

우선순위(Priority)필드설명기본값 / 범위
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의 나노초 정확도는 하드웨어 타임스탬핑에 의존합니다. 소프트웨어 타임스탬핑은 커널 네트워크 스택(Network Stack)의 지연(수십 마이크로초)을 포함하므로 정밀도가 떨어집니다. NIC 또는 PHY 수준에서 패킷(Packet)의 실제 송수신 시점을 기록하면 이러한 소프트웨어 지터를 제거할 수 있습니다.

타임스탬핑 위치정확도설명
PHY 타임스탬핑최고 (~ ns)PHY 칩이 선로에 가장 가까운 지점에서 타임스탬프. MAC 지연 제거
MAC 타임스탬핑높음 (수~수십 ns)NIC MAC 블록에서 타임스탬프. PHY 지연이 남지만 소프트웨어보다 월등
소프트웨어 타임스탬핑낮음 (수~수십 μs)커널 드라이버 또는 소켓(Socket) 계층에서 타임스탬프. 스케줄링 지터 포함
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 구조체에 콜백(Callback)을 채워 ptp_clock_register()로 등록합니다. 등록된 PHC는 /dev/ptpN 캐릭터 디바이스와 /sys/class/ptp/ptpN/ sysfs 노드로 노출됩니다.

/* include/linux/ptp_clock_kernel.h — 핵심 구조체 */
struct ptp_clock_info {
    struct module *owner;
    char   name[32];
    s32    max_adj;          /* 최대 주파수 조정 (ppb 단위) */
    int    n_alarm;          /* 알람 채널 수 */
    int    n_ext_ts;         /* 외부 타임스탬프 입력 채널 수 */
    int    n_per_out;        /* 주기적 출력 채널 수 */
    int    n_pins;           /* 프로그래밍 가능 핀 수 */
    int    pps;              /* PPS(Pulse Per Second) 지원 여부 */
    struct ptp_pin_desc *pin_config;  /* 핀 설정 배열 */

    /* ── 주파수 조정 ── */
    int  (*adjfine)(struct ptp_clock_info *ptp, long scaled_ppm);
    /*  scaled_ppm: ppb 단위 × 2^16 스케일.
     *  예: +1 ppm = +65536, -0.5 ppm = -32768 */

    /* ── 시간 점프 ── */
    int  (*adjtime)(struct ptp_clock_info *ptp, s64 delta);
    /*  나노초 단위 시간 오프셋 적용 (양수: 앞으로, 음수: 뒤로) */

    /* ── 시간 읽기/쓰기 ── */
    int  (*gettime64)(struct ptp_clock_info *ptp,
                       struct timespec64 *ts);
    int  (*settime64)(struct ptp_clock_info *ptp,
                       const struct timespec64 *ts);
    int  (*gettimex64)(struct ptp_clock_info *ptp,
                        struct timespec64 *ts,
                        struct ptp_system_timestamp *sts);
    /*  gettimex64: PHC 시간과 시스템 시간의 교차 타임스탬프 쌍 반환 */

    /* ── 교차 타임스탬핑 ── */
    int  (*getcrosststamp)(struct ptp_clock_info *ptp,
                            struct system_device_crosststamp *cts);
    /*  PTM(Precision Time Measurement) 등으로 PHC↔시스템 시간을
     *  하드웨어적으로 동시 캡처. phc2sys 대비 더 높은 정확도 */

    /* ── 외부 이벤트/주기 출력 제어 ── */
    int  (*enable)(struct ptp_clock_info *ptp,
                    struct ptp_clock_request *rq, int on);
    /*  PTP_CLK_REQ_EXTTS:  외부 타임스탬프 캡처 활성화
     *  PTP_CLK_REQ_PEROUT: 주기적 펄스 출력 활성화
     *  PTP_CLK_REQ_PPS:    PPS 출력 활성화 */

    /* ── 핀 기능 설정 ── */
    int  (*verify)(struct ptp_clock_info *ptp,
                    unsigned int pin, enum ptp_pin_function func,
                    unsigned int chan);
};
/* PHC 등록/해제 API */
struct ptp_clock *ptp_clock_register(struct ptp_clock_info *info,
                                       struct device *parent);
int  ptp_clock_unregister(struct ptp_clock *ptp);
int  ptp_clock_index(struct ptp_clock *ptp);  /* /dev/ptpN의 N 반환 */

/* 이벤트 보고 — 외부 타임스탬프, PPS 등 */
void ptp_clock_event(struct ptp_clock *ptp,
                      struct ptp_clock_event *event);

/* 이벤트 유형 */
struct ptp_clock_event {
    enum ptp_clock_events type;  /* PTP_CLOCK_EXTTS, PTP_CLOCK_PPS, ... */
    int    index;                 /* 채널 인덱스 */
    union {
        struct timespec64 timestamp;
        struct ptp_clock_time pct;
    };
};

PTP 드라이버 구현 예제

실제 NIC 드라이버에서 PTP를 지원하기 위한 최소 골격 코드입니다. Intel ixgbe, igb, ice 드라이버 등이 이 패턴을 따릅니다.

#include <linux/ptp_clock_kernel.h>
#include <linux/net_tstamp.h>

struct my_adapter {
    struct ptp_clock      *ptp_clock;
    struct ptp_clock_info  ptp_caps;
    spinlock_t             tmreg_lock;
    struct cyclecounter    cc;
    struct timecounter     tc;
    u32                    tstamp_config;
};

/* ── adjfine: 주파수 미세 조정 ── */
static int my_ptp_adjfine(struct ptp_clock_info *ptp,
                            long scaled_ppm)
{
    struct my_adapter *adapter =
        container_of(ptp, struct my_adapter, ptp_caps);
    u64 adj;
    bool neg = false;

    if (scaled_ppm < 0) {
        neg = true;
        scaled_ppm = -scaled_ppm;
    }

    /* NIC 고유 주파수 조정 레지스터에 값 기록 */
    adj = (u64)scaled_ppm * adapter->cc.mult;
    adj >>= 16;  /* scaled_ppm은 ppb × 2^16 */

    spin_lock(&adapter->tmreg_lock);
    timecounter_read(&adapter->tc);
    adapter->cc.mult = neg ?
        adapter->cc.mult - (u32)adj :
        adapter->cc.mult + (u32)adj;
    spin_unlock(&adapter->tmreg_lock);

    return 0;
}

/* ── adjtime: 시간 점프 ── */
static int my_ptp_adjtime(struct ptp_clock_info *ptp,
                            s64 delta)
{
    struct my_adapter *adapter =
        container_of(ptp, struct my_adapter, ptp_caps);

    spin_lock(&adapter->tmreg_lock);
    timecounter_adjtime(&adapter->tc, delta);
    spin_unlock(&adapter->tmreg_lock);

    return 0;
}

/* ── gettime64: PHC 현재 시간 읽기 ── */
static int my_ptp_gettime64(struct ptp_clock_info *ptp,
                              struct timespec64 *ts)
{
    struct my_adapter *adapter =
        container_of(ptp, struct my_adapter, ptp_caps);
    u64 ns;

    spin_lock(&adapter->tmreg_lock);
    ns = timecounter_read(&adapter->tc);
    spin_unlock(&adapter->tmreg_lock);

    *ts = ns_to_timespec64(ns);
    return 0;
}

/* ── settime64: PHC 시간 설정 ── */
static int my_ptp_settime64(struct ptp_clock_info *ptp,
                              const struct timespec64 *ts)
{
    struct my_adapter *adapter =
        container_of(ptp, struct my_adapter, ptp_caps);
    u64 ns = timespec64_to_ns(ts);

    spin_lock(&adapter->tmreg_lock);
    timecounter_init(&adapter->tc, &adapter->cc, ns);
    spin_unlock(&adapter->tmreg_lock);

    return 0;
}

/* ── enable: 외부 타임스탬프/주기적 출력 제어 ── */
static int my_ptp_enable(struct ptp_clock_info *ptp,
                           struct ptp_clock_request *rq, int on)
{
    struct my_adapter *adapter =
        container_of(ptp, struct my_adapter, ptp_caps);

    switch (rq->type) {
    case PTP_CLK_REQ_EXTTS:
        /* 외부 이벤트(예: 1PPS 입력) 캡처 활성화/비활성화 */
        if (on)
            my_hw_enable_extts(adapter, rq->extts.index);
        else
            my_hw_disable_extts(adapter, rq->extts.index);
        return 0;

    case PTP_CLK_REQ_PEROUT:
        /* 주기적 펄스 출력 (예: 10MHz, 1PPS) 활성화/비활성화 */
        if (on)
            my_hw_enable_perout(adapter, &rq->perout);
        else
            my_hw_disable_perout(adapter, &rq->perout);
        return 0;

    case PTP_CLK_REQ_PPS:
        /* PPS (Pulse Per Second) 출력 */
        return 0;

    default:
        return -EOPNOTSUPP;
    }
}

/* ── PHC 등록 ── */
static void my_ptp_init(struct my_adapter *adapter)
{
    adapter->ptp_caps = (struct ptp_clock_info) {
        .owner     = THIS_MODULE,
        .name      = "my_nic_phc",
        .max_adj   = 500000000,   /* 500 ppm */
        .n_alarm   = 0,
        .n_ext_ts  = 2,           /* 외부 TS 2채널 */
        .n_per_out = 1,           /* 주기 출력 1채널 */
        .n_pins    = 3,
        .pps       = 1,
        .adjfine   = my_ptp_adjfine,
        .adjtime   = my_ptp_adjtime,
        .gettime64 = my_ptp_gettime64,
        .settime64 = my_ptp_settime64,
        .enable    = my_ptp_enable,
    };

    adapter->ptp_clock = ptp_clock_register(
        &adapter->ptp_caps, &adapter->pdev->dev);

    if (IS_ERR(adapter->ptp_clock)) {
        dev_err(&adapter->pdev->dev,
                "ptp_clock_register failed\n");
        adapter->ptp_clock = NULL;
    }
}

/* ── PHC 해제 ── */
static void my_ptp_remove(struct my_adapter *adapter)
{
    if (adapter->ptp_clock) {
        ptp_clock_unregister(adapter->ptp_clock);
        adapter->ptp_clock = NULL;
    }
}

PHC (PTP Hardware Clock)

PTP 지원 NIC는 자체 하드웨어 클럭(PHC)을 내장합니다. PHC는 패킷의 정확한 송수신 타임스탬프를 하드웨어 수준에서 기록하여 소프트웨어 지연에 의한 오차를 제거합니다. Linux에서 PHC는 /dev/ptpN 장치로 노출됩니다.

# PHC 장치 확인
$ ls /dev/ptp*
/dev/ptp0

# PHC 정보 (ethtool)
$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
    hardware-transmit     (SOF_TIMESTAMPING_TX_HARDWARE)
    hardware-receive      (SOF_TIMESTAMPING_RX_HARDWARE)
    hardware-raw-clock    (SOF_TIMESTAMPING_RAW_HARDWARE)
    software-transmit     (SOF_TIMESTAMPING_TX_SOFTWARE)
    software-receive      (SOF_TIMESTAMPING_RX_SOFTWARE)
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
    on
Hardware Receive Filter Modes:
    all

PTP Userspace 도구

linuxptp 패키지는 PTP 운용에 필요한 핵심 유저스페이스 도구를 제공합니다.

ptp4l — PTP 데몬

마스터/슬레이브 자동 협상(BMCA) 및 시간 동기화를 수행하는 핵심 데몬입니다.

# 기본 슬레이브 모드 실행
$ ptp4l -i eth0 -s -m
ptp4l[5678]: master offset   3 s2 freq  -567 path delay  800

# 설정 파일을 사용한 실행
$ ptp4l -f /etc/ptp4l.conf -m

# /etc/ptp4l.conf 예제
[global]
twoStepFlag              1
tx_timestamp_timeout     10
logSyncInterval          -3          # 125ms 간격
logAnnounceInterval      1           # 2초 간격
logMinDelayReqInterval   0           # 1초 간격
announceReceiptTimeout   3
priority1                128
priority2                128
domainNumber             0
slaveOnly                0
clock_servo              pi          # PI 서보 (기본)
pi_proportional_const    0.0        # 자동 조정
pi_integral_const        0.0        # 자동 조정

[eth0]
delay_mechanism          E2E         # 또는 P2P
network_transport        UDPv4       # 또는 L2, UDPv6

phc2sys — PHC ↔ 시스템 클럭 동기화

ptp4l이 PHC를 마스터에 동기화한 후, phc2sys는 PHC의 시간을 시스템 클럭(CLOCK_REALTIME)에 반영합니다.

# PHC → CLOCK_REALTIME 동기화 (-O 0: UTC 오프셋 없음)
$ phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 0 -m
phc2sys[1234]: CLOCK_REALTIME phc offset  -5 s2 freq  -1234 delay  500

# ptp4l과 자동 연동 (-a: automatic 모드)
$ phc2sys -a -r -m
# -a: ptp4l UDS에서 마스터 포트 정보 자동 획득
# -r: CLOCK_REALTIME을 슬레이브로 설정

# PHC 간 동기화 (멀티 NIC)
$ phc2sys -s /dev/ptp0 -c /dev/ptp1 -O 0 -m

ts2phc — 외부 시간 소스 → PHC 동기화

# GNSS 수신기의 1PPS → PHC 동기화
$ ts2phc -s nmea -c eth0 -m
# NMEA 직렬 데이터 + 1PPS 신호로 PHC 초기화

# /etc/ts2phc.conf 예제
[global]
use_syslog        1
ts2phc.nmea_serialport  /dev/ttyS0
ts2phc.pulsewidth       100000000   # 100ms
[eth0]
ts2phc.pin_index  0

pmc — PTP Management Client

# 현재 PTP 상태 조회
$ pmc -u -b 0 'GET CURRENT_DATA_SET'
   stepsRemoved     1
   offsetFromMaster -3.0
   meanPathDelay    800.0

# 마스터 정보 조회
$ pmc -u -b 0 'GET PARENT_DATA_SET'
   parentPortIdentity  001122.fffe.334455-1
   grandmasterIdentity 001122.fffe.334455
   grandmasterClockClass 6
   grandmasterPriority1  128

# 포트 상태 조회
$ pmc -u -b 0 'GET PORT_DATA_SET'
   portState        SLAVE
   logSyncInterval  -3
   delayMechanism   E2E

# 실시간 priority1 변경 (Grandmaster 전환 유도)
$ pmc -u -b 0 'SET PRIORITY1 100'

PTP와 Linux 네트워킹 스택

어플리케이션은 SO_TIMESTAMPING 소켓 옵션을 통해 하드웨어/소프트웨어 타임스탬프를 수신합니다. SIOCSHWTSTAMP ioctl로 NIC의 타임스탬핑 모드를 설정하고, 커널은 struct scm_timestamping을 ancillary data로 전달합니다.

/* SO_TIMESTAMPING 소켓 옵션 — 타임스탬프 요청 */
#include <linux/net_tstamp.h>

int flags = SOF_TIMESTAMPING_TX_HARDWARE  |
            SOF_TIMESTAMPING_RX_HARDWARE  |
            SOF_TIMESTAMPING_RAW_HARDWARE |
            SOF_TIMESTAMPING_OPT_CMSG;

setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
           &flags, sizeof(flags));

/* SIOCSHWTSTAMP — 하드웨어 타임스탬핑 활성화 */
struct hwtstamp_config cfg = {
    .tx_type   = HWTSTAMP_TX_ON,
    .rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT,
};
struct ifreq ifr;
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
ifr.ifr_data = (void *)&cfg;
ioctl(sock, SIOCSHWTSTAMP, &ifr);

/* 타임스탬프 수신 — recvmsg() ancillary data */
struct msghdr msg = { 0 };
char ctrl[CMSG_SPACE(sizeof(struct scm_timestamping))];
msg.msg_control = ctrl;
msg.msg_controllen = sizeof(ctrl);

recvmsg(sock, &msg, 0);

struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm;
     cm = CMSG_NXTHDR(&msg, cm)) {
    if (cm->cmsg_type == SO_TIMESTAMPING) {
        struct scm_timestamping *ts =
            (struct scm_timestamping *)CMSG_DATA(cm);
        /*
         * ts->ts[0] = 소프트웨어 타임스탬프
         * ts->ts[1] = (deprecated, 사용 안 함)
         * ts->ts[2] = 하드웨어 타임스탬프 (RAW_HARDWARE)
         */
        printf("HW TS: %lld.%09ld\n",
               (long long)ts->ts[2].tv_sec,
               ts->ts[2].tv_nsec);
    }
}
코드 설명

PTP 하드웨어 타임스탬핑을 위한 SO_TIMESTAMPING 소켓 옵션 설정과 타임스탬프 수신 코드입니다. include/uapi/linux/net_tstamp.h에 정의된 플래그를 사용합니다.

  • SOF_TIMESTAMPING_TX_HARDWARE송신(TX) 패킷의 하드웨어 타임스탬프를 요청합니다. NIC의 MAC/PHY가 패킷이 물리 매체에 실제 전송되는 순간을 기록합니다.
  • SOF_TIMESTAMPING_RX_HARDWARE수신(RX) 패킷의 하드웨어 타임스탬프를 요청합니다. 패킷이 NIC에 도착하는 물리적 순간을 나노초 정밀도로 기록합니다.
  • SOF_TIMESTAMPING_RAW_HARDWARE하드웨어 타임스탬프를 원시 형태(PHC 시간 기준)로 반환하도록 요청합니다. ts->ts[2]에 저장됩니다.
  • SIOCSHWTSTAMP ioctlNIC 드라이버에 하드웨어 타임스탬핑을 활성화하는 ioctl입니다. tx_typerx_filter로 어떤 패킷에 타임스탬프를 찍을지 지정합니다.
  • HWTSTAMP_FILTER_PTP_V2_EVENTPTP v2 이벤트 메시지(Sync, Delay_Req 등)만 하드웨어 타임스탬핑 대상으로 필터링합니다. 모든 패킷에 타임스탬프를 찍으면 성능에 영향을 줄 수 있습니다.
  • recvmsg() ancillary dataSO_TIMESTAMPING 타입의 보조 메시지(control message)로 타임스탬프를 수신합니다. scm_timestamping 구조체의 ts[0]은 소프트웨어, ts[2]은 하드웨어 타임스탬프입니다.

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 스위치를 사용하십시오.
  • 방화벽(Firewall) 멀티캐스트 차단: PTP는 멀티캐스트(224.0.1.129, 224.0.0.107)를 사용합니다. 방화벽에서 UDP 319/320 및 해당 멀티캐스트 그룹을 허용해야 합니다. L2 모드는 EtherType 0x88F7 허용 필요.
  • VLAN 환경: VLAN 태깅이 PHY 타임스탬핑 위치에 영향을 줄 수 있습니다. VLAN 태그 삽입/제거가 타임스탬프 지점 이전/이후인지 확인하십시오.
  • 가상화 환경: VM에서 PTP를 사용할 경우 하드웨어 타임스탬핑이 불가능합니다. SR-IOV VF 패스스루 또는 virtio-net의 소프트웨어 타임스탬핑을 사용해야 합니다.
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 폴링(Polling))으로 반응 속도 향상
  • 멀티 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 오버헤드(Overhead)보다 바쁜 대기(Busy Wait)가 효율적
  • 10us~20ms: usleep_range() — hrtimer 기반으로 CPU를 양보(Yield)하면서 정밀 대기
  • 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);
}

고급 지연 패턴

패턴 1: readx_poll_timeout() 커스텀 읽기 함수 사용

readl_poll_timeout()은 MMIO 레지스터 전용이지만, readx_poll_timeout()은 임의의 읽기 함수를 사용할 수 있습니다. SPI/I2C 전송처럼 복잡한 폴링 절차에 유용합니다.

/* SPI 장치 상태를 커스텀 함수로 폴링 */
static int spi_read_status(struct spi_device *spi)
{
    u8 cmd = CMD_READ_STATUS;
    u8 val;
    int ret = spi_write_then_read(spi, &cmd, 1, &val, 1);
    return ret ? : val;
}

static int wait_spi_flash_ready(struct spi_device *spi)
{
    int val;
    /* 50μs 간격 폴링, 최대 500ms 대기 */
    return readx_poll_timeout(spi_read_status, spi, val,
                               !(val & STATUS_WIP),
                               50,       /* 50μs sleep */
                               500000);  /* 500ms timeout */
}
코드 설명

readx_poll_timeout()은 매크로로, 첫 인자로 읽기 함수를 받아 반복 호출합니다.

  • readx_poll_timeout(op, args, val, cond, sleep_us, timeout_us)op(args)를 호출해 결과를 val에 저장하고, cond이 참이면 0을 반환합니다. 타임아웃 시 -ETIMEDOUT을 반환합니다.
  • spi_read_status()SPI 트랜잭션으로 장치 상태 레지스터를 읽는 커스텀 함수입니다. MMIO readl() 대신 이런 함수를 사용할 수 있는 것이 readx_poll_timeout()의 장점입니다.

패턴 2: wait_event_timeout() + ktime 정밀 하이브리드

하드웨어 인터럽트로 깨어나는 이벤트 대기에 wait_event_timeout()을 사용하면서, 실제 소요 시간을 ktime으로 정밀 측정하는 하이브리드 패턴입니다.

/* 인터럽트 기반 대기 + ktime 지연 측정 */
static int wait_hw_completion(struct my_device *dev)
{
    ktime_t t0 = ktime_get();
    unsigned long left;
    s64 latency_us;

    /* 인터럽트 핸들러가 dev->done을 설정, 최대 100ms 대기 */
    left = wait_event_timeout(dev->wq,
                              READ_ONCE(dev->done),
                              msecs_to_jiffies(100));

    latency_us = ktime_us_delta(ktime_get(), t0);

    if (!left) {
        dev_err(dev->dev, "HW timeout after %lld us\n", latency_us);
        return -ETIMEDOUT;
    }
    dev_dbg(dev->dev, "HW done in %lld us\n", latency_us);
    return 0;
}
코드 설명

이벤트 기반 대기와 정밀 시간 측정을 결합한 패턴입니다.

  • wait_event_timeout()jiffies 기반(~4ms 해상도) 타임아웃이지만, CPU를 양보하며 인터럽트로 즉시 깨어날 수 있어 전력 효율적입니다.
  • ktime_us_delta()나노초 해상도 ktime으로 실제 경과 시간을 정밀 측정합니다. wait_event_timeout()의 반환값(남은 jiffies)보다 훨씬 정확합니다.
  • 하이브리드 장점대기 자체는 wait_event_timeout()으로 전력 효율적으로 하고, 측정은 ktime으로 정밀하게 하여 두 가지 장점을 모두 취합니다.

Time Namespace

Time namespace(v5.6+)는 컨테이너별 CLOCK_MONOTONIC, CLOCK_BOOTTIME 오프셋을 분리해 "컨테이너가 보는 경과 시간 기준"을 독립적으로 만듭니다. 핵심은 REALTIME을 바꾸지 않고 경과 시간 축에만 offset을 더한다는 점입니다.

Time Namespace 오프셋 모델 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

체크포인트(Checkpoint)/복원 시나리오

/*
 * 컨테이너 복원 시 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을 순서대로 확인해 "클럭 소스 선택 → 해상도 → 동기화 품질"이 함께 개선되는지 확인하세요.

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

규격 및 표준

기술 블로그 및 발표

ktime/Clock과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.