Timer -9- (Timekeeping)

 

Timekeeping

 

리눅스에서 다루는 각 시간의 기초 값을 4개로 나누어 관리한다.

 

다음 그림은 Timekeeping이 어떻게 유지 관리되는지 모습을 보여준다.

 

시간 구조체 타입 비교

기존 커널이 사용하였던 timespec 구조체로된 xtime 변수는 이제 커널의 timekeeping 컴포넌트 내부 구조에서만 사용된다.

 

아래 그림은 각 시간 구조체 타입을 비교하였다.

 

Timekeeping APIs

Get & Set timeofday

  • do_gettimeofday()
  • do_settimeofday64()
  • do_sys_settimeofday()

 

Kernel time accessors

  • get_seconds()
  • current_kernel_time()

 

timespec 기반 인터페이스

  • getrawmonotonic64()
  • ktime_get_ts64()
  • ktime_get_seconds()
  • ktime_get_real_seconds()
  • __getnstimeofday64()
  • getnstimeofday64()
  • getboottime64()
  • do_settimeofday()
  • __getnstimeofday()
  • getnstimeofday()
  • ktime_get_ts()
  • ktime_get_real_ts()
  • getrawmonotonic()
  • getboottime()

 

ktime_t 기반 인터페이스

  • ktime_get()
  • ktime_get_with_offset()
  • ktime_mono_to_any()
  • ktime_get_raw()
  • ktime_get_real()
  • ktime_get_boottime()
  • ktime_get_clocktai()
  • ktime_mono_to_real()

 

ktime -> ns 변환

  • ktime_get_ns()
  • ktime_get_real_ns()
  • ktime_get_boot_ns()
  • ktime_get_raw_ns()
  • ktime_get_mono_fast_ns()

 

ktime 기반의 하나를 timespec 인터페이스로 반환

  • get_monotonic_boottime()
  • get_monotonic_boottime64()
  • timekeeping_clocktai()

 

RTC

  • timekeeping_inject_sleeptime64

 

PPS

  • getnstime_raw_and_real()

 

Persisten 클럭 관련 인터페이스

  • has_persistent_clock()
  • read_persistent_clock()
  • read_boot_clock()
  • update_persistent_clock()

 

ToD 클럭 설정

do_settimeofday64()

kernel/time/timekeeping.c

/**
 * do_settimeofday64 - Sets the time of day.
 * @ts:     pointer to the timespec64 variable containing the new time
 *
 * Sets the time of day to the new time and update NTP and notify hrtimers
 */
int do_settimeofday64(const struct timespec64 *ts)
{
        struct timekeeper *tk = &tk_core.timekeeper;
        struct timespec64 ts_delta, xt;
        unsigned long flags;

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

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

        timekeeping_forward_now(tk);

        xt = tk_xtime(tk);
        ts_delta.tv_sec = ts->tv_sec - xt.tv_sec;
        ts_delta.tv_nsec = ts->tv_nsec - xt.tv_nsec;

        tk_set_wall_to_mono(tk, timespec64_sub(tk->wall_to_monotonic, ts_delta));

        tk_set_xtime(tk, ts);

        timekeeping_update(tk, TK_CLEAR_NTP | TK_MIRROR | TK_CLOCK_WAS_SET);

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

        /* signal hrtimers about time change */
        clock_was_set();

        return 0;
}
EXPORT_SYMBOL(do_settimeofday64);

요청한 시각을 Timekeeping에 설정한다.

  • 코드 라인 13~14에서 요청한 시각이 유효하지 않은 값이면 처리를 중단하고 -EINVAL 값을 반환한다.
  • 코드 라인 19에서 Timekeeper 클럭 소스를 읽어 갱신한다. (last_cycle, xtime, raw_time)
  • 코드 라인 21~23에서 요청한 시각과 현재 시각의 차 delta를 구한다.
  • 코드 라인 25에서 realtime 값을 monotonic으로 변환을 할 수 있도록 wall_to_monotonic에 delta 값을 증가시킨다.
  • 코드 라인 27에서 요청한 시각을 xtime에 설정한다.
  • 코드 라인 29에서 timekeeping을 갱신한다.
  • 코드 라인 35에서 클럭이 변경된 경우 모든 cpu에 next이벤트가 발생하도록 프로그램하고 cancel_list에 있는 타이머를 취소하도록 호출된다.

 

다음 그림은 date 명령을 통해 시각이 설정되는 과정을 보여준다.

 

Timekeeper 카운터 갱신(xtime & raw_time)

timekeeping_forward_now()

kernel/time/timekeeping.c

/**
 * timekeeping_forward_now - update clock to the current time
 *
 * Forward the current clock to update its state since the last call to
 * update_wall_time(). This is useful before significant clock changes,
 * as it avoids having to deal with this time offset explicitly.
 */
static void timekeeping_forward_now(struct timekeeper *tk)
{
        struct clocksource *clock = tk->tkr.clock;
        cycle_t cycle_now, delta;
        s64 nsec;

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

        tk->tkr.xtime_nsec += delta * tk->tkr.mult;

        /* If arch requires, add in get_arch_timeoffset() */
        tk->tkr.xtime_nsec += (u64)arch_gettimeoffset() << tk->tkr.shift;

        tk_normalize_xtime(tk);

        nsec = clocksource_cyc2ns(delta, clock->mult, clock->shift);
        timespec64_add_ns(&tk->raw_time, nsec);
}

Timekeeper 클럭 소스를 읽은 값에서 마지막에 저장해둔 cycle_last 값을 뺀 delta 사이클을 구하여 현재 시각인 xtime과 raw_time에 더해 갱신한다.

  • 코드 라인 14~16에서 Timekeeper 클럭 소스의 cycle 값을 읽어 기존 cycle_last 값을 뺀 delta 사이클을 구한다. 방금 읽은 값은 cycle_last에 저장해둔다.
  • 코드 라인 18에서 xtime_nsec에 delta 값에 mult를 곱해 저장한다.
  • 코드 라인 21에서 old 스타일 gettimeoffset 값에 좌로 shift하여 xtime_nsec에 더한다.
    •  nohz 및 high-res 타이머를 사용하지 못하는 기존 시스템에서 CONFIG_ARCH_USES_GETTIMEOFFSET 커널 옵션을 사용할 때 적용된다.
  •  코드 라인 23에서 xime_nsec 값이 1초를 넘어가면 1초 단위로 루프를 돌며 xime_sec++ 한다. delta 값이 매우 큰 경우 1초 단위로 반복하며 시간을 조정하여 시스템 에러를 최소화한다.
  • 코드 라인 25~26에서 raw_time에 delta에 해당하는 나노초를 더한다.

 

다음 그림은 timekeeper 카운터를 읽어 delta cycle을 xtime 및 raw_time에 더해 현재 시각을 갱신하는 과정을 보여준다.

 

Time Normalization

tk_normalize_xtime()

kernel/time/timekeeping.c

static inline void tk_normalize_xtime(struct timekeeper *tk)
{
        while (tk->tkr.xtime_nsec >= ((u64)NSEC_PER_SEC << tk->tkr.shift)) {
                tk->tkr.xtime_nsec -= (u64)NSEC_PER_SEC << tk->tkr.shift;
                tk->xtime_sec++;
        }
}

xtime에서 나노초가 1초를 초과한 경우 1초 단위로 루프를 돌며 조정하여 한 번에 급격한 시간 변동이 되지 않도록 한다.

 

tk_set_wall_to_mono()

kernel/time/timekeeping.c

static void tk_set_wall_to_mono(struct timekeeper *tk, struct timespec64 wtm)
{
        struct timespec64 tmp;

        /*
         * Verify consistency of: offset_real = -wall_to_monotonic
         * before modifying anything
         */
        set_normalized_timespec64(&tmp, -tk->wall_to_monotonic.tv_sec,
                                        -tk->wall_to_monotonic.tv_nsec);
        WARN_ON_ONCE(tk->offs_real.tv64 != timespec64_to_ktime(tmp).tv64);
        tk->wall_to_monotonic = wtm;
        set_normalized_timespec64(&tmp, -wtm.tv_sec, -wtm.tv_nsec);
        tk->offs_real = timespec64_to_ktime(tmp);
        tk->offs_tai = ktime_add(tk->offs_real, ktime_set(tk->tai_offset, 0));
}

realtime 값을 monotonic으로 변환을 할 수 있도록 wall_to_monotonic 값을 설정한다.

  • monotonic = realtime + wall_to_monotonic 형태이므로 wall_to_monotonic 값은 보통 음수가 대입된다.

 

  • 코드 라인 9~11에서 -wall_to_monotonic 값을 읽어 offs_real 값과 다른 경우 경고 메시지를 출력한다.
  • 코드 라인 12에서 인수로 받은 시간을 wall_to_monotonic에 기록하고 나노초의 범위가 0~1G 이내에 있도록 조정한다.
    • realtime + wall_to_monotonic = monotonic 이므로 wall_to_monotonic에는 보통 음수 값이 담긴다.
  • 코드 라인 13에서  인수로 받은 시간을 음수로 바꾼 timespce64 타입의 시간을 ktime 형태로 바꾸어 offs_real에 대입한다.
    • realtime – offs_real = monotonic 이므로 off_real 값은 양수 값이 담긴다.
  • 코드 라인 14~15에서 offs_real 값과 tai_offset을 더해 offs_tai에 저장한다.

 

다음 그림은 “2017년 1월 1일 10시 20분 30초”로 시간 설정을 하는 경우 wall_to_monotonic 값의 변화를 보여준다.

 

Wall 타임 갱신

update_wall_time()

kernel/time/timekeeping.c

/**
 * update_wall_time - Uses the current clocksource to increment the wall time
 *
 */
void update_wall_time(void)
{
        struct timekeeper *real_tk = &tk_core.timekeeper;
        struct timekeeper *tk = &shadow_timekeeper;
        cycle_t offset;
        int shift = 0, maxshift;
        unsigned int clock_set = 0;
        unsigned long flags;

        raw_spin_lock_irqsave(&timekeeper_lock, flags);

        /* Make sure we're fully resumed: */
        if (unlikely(timekeeping_suspended))
                goto out;

#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
        offset = real_tk->cycle_interval;
#else
        offset = clocksource_delta(tk->tkr.read(tk->tkr.clock),
                                   tk->tkr.cycle_last, tk->tkr.mask);
#endif

        /* Check if there's really nothing to do */
        if (offset < real_tk->cycle_interval)
                goto out;

        /*
         * With NO_HZ we may have to accumulate many cycle_intervals
         * (think "ticks") worth of time at once. To do this efficiently,
         * we calculate the largest doubling multiple of cycle_intervals
         * that is smaller than the offset.  We then accumulate that
         * chunk in one go, and then try to consume the next smaller
         * doubled multiple.
         */
        shift = ilog2(offset) - ilog2(tk->cycle_interval);
        shift = max(0, shift);
        /* Bound shift to one less than what overflows tick_length */
        maxshift = (64 - (ilog2(ntp_tick_length())+1)) - 1;
        shift = min(shift, maxshift);
        while (offset >= tk->cycle_interval) {
                offset = logarithmic_accumulation(tk, offset, shift,
                                                        &clock_set);
                if (offset < tk->cycle_interval<<shift)
                        shift--;
        }

        /* correct the clock when NTP error is too big */
        timekeeping_adjust(tk, offset);

스케쥴 틱 간격으로 호출되어 timekeeping을 갱신한다.

  • 코드 라인 17~18에서 낮은 확률로 PM suspend된 경우 처리를 진행하지 않고 빠져나간다.
  • 코드 라인 20~25에서 마지막에 읽은 cycle과 현재 읽은 cycle과의 차이를 산출하여 offset에 대입한다.
  • 코드 라인 28~29에서 offset이 cycle_interval보다 작은 경우 처리할 필요가 없으므로 함수를 빠져나간다.
  • 코드 라인 39~49에서 offset cycle과 cycle_interval의 차를 사용하여 적절한 shift 값을 구한다.
  • 코드 라인 52에서 NTP 에러가 너무 큰 경우 교정한다.

 

        /*
         * XXX This can be killed once everyone converts
         * to the new update_vsyscall.
         */
        old_vsyscall_fixup(tk);

        /*
         * Finally, make sure that after the rounding
         * xtime_nsec isn't larger than NSEC_PER_SEC
         */
        clock_set |= accumulate_nsecs_to_secs(tk);

        write_seqcount_begin(&tk_core.seq);
        /*
         * Update the real timekeeper.
         *
         * We could avoid this memcpy by switching pointers, but that
         * requires changes to all other timekeeper usage sites as
         * well, i.e. move the timekeeper pointer getter into the
         * spinlocked/seqcount protected sections. And we trade this
         * memcpy under the tk_core.seq against one before we start
         * updating.
         */
        memcpy(real_tk, tk, sizeof(*tk));
        timekeeping_update(real_tk, clock_set);
        write_seqcount_end(&tk_core.seq);
out:
        raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
        if (clock_set)
                /* Have to call _delayed version, since in irq context*/
                clock_was_set_delayed();
}
  • 코드 라인 5에서 old vsyscall에 대한 fixup을 수행한다.
  • 코드 라인 11에서 xtime 나노초가 1초를 초과한 경우 이 값을 xtime 초에 반영한다. 또한 leap second 처리를 위한 NTP 코드를 호출한다. leap second가 발생한 경우 clock_set에 TK_CLOCK_WAS_SET(1 << 2) 비트가 추가된다.
  • 코드 라인 13~26에서 shadow timekeeper를 timekeeper에 복사한 후 timekeeping을 갱신한다.
  • 코드 라인 29~31에서 clock_set가 설정된 경우 워크큐를 통해 스케쥴하여 clock_was_set() 코드를 수행하여 모든 cpu에 다음 이벤트가 발생하도록 프로그램하고 cancel_list에 있는 타이머를 취소하도록 호출된다.

 

clocksource_delta()

kernel/time/timekeeping_internal.h

static inline cycle_t clocksource_delta(cycle_t now, cycle_t last, cycle_t mask)
{
        return (now - last) & mask;
}

현재 cycle과 기존 cycle과의 차이를 mask하여 반환한다.

 

logarithmic_accumulation()

참고: time: Implement logarithmic time accumulation

kernel/time/timekeeping.c

/**
 * logarithmic_accumulation - shifted accumulation of cycles
 *
 * This functions accumulates a shifted interval of cycles into
 * into a shifted interval nanoseconds. Allows for O(log) accumulation
 * loop.
 *
 * Returns the unconsumed cycles.
 */
static cycle_t logarithmic_accumulation(struct timekeeper *tk, cycle_t offset,
                                                u32 shift,
                                                unsigned int *clock_set)
{
        cycle_t interval = tk->cycle_interval << shift;
        u64 raw_nsecs;

        /* If the offset is smaller then a shifted interval, do nothing */
        if (offset < interval)
                return offset;

        /* Accumulate one shifted interval */
        offset -= interval;
        tk->tkr.cycle_last += interval;

        tk->tkr.xtime_nsec += tk->xtime_interval << shift;
        *clock_set |= accumulate_nsecs_to_secs(tk);

        /* Accumulate raw time */
        raw_nsecs = (u64)tk->raw_interval << shift;
        raw_nsecs += tk->raw_time.tv_nsec;
        if (raw_nsecs >= NSEC_PER_SEC) {
                u64 raw_secs = raw_nsecs;
                raw_nsecs = do_div(raw_secs, NSEC_PER_SEC);
                tk->raw_time.tv_sec += raw_secs;
        }
        tk->raw_time.tv_nsec = raw_nsecs;

        /* Accumulate error between NTP and clock interval */
        tk->ntp_error += tk->ntp_tick << shift;
        tk->ntp_error -= (tk->xtime_interval + tk->xtime_remainder) <<
                                                (tk->ntp_error_shift + shift);

        return offset;
}

시프트된 cycle의 인터벌을 시프트된 인터벌 나노초로 산술한다. offset이 인터벌보다 큰 경우에만 동작한다. 또한 적용되지 않고 남은 offset 값은 반환된다.

  • 코드 라인 14에서 1 NTP 인터벌에 대한 ns << shift 하여 interval에 대입한다.
  • 코드 라인 22~23에서 offset 값에서 시프트된 interval을 빼고, cycle_last에는 추가한다.
  • 코드 라인 25에서 xtime_nsec에 xtime_interrval << shift를 더한다.
  • 코드 라인 26에서 xtime 나노초가 1초를 초과한 경우 이 값을 xtime 초에 반영한다. 또한 leap second 처리를 위한 NTP 코드를 호출한다.
  • 코드 라인 29~36에서 raw_time에 시프트된 raw_interval을 추가한다.
  • 코드 라인 39~41에서 ntp_error에 시프트된 (xtime_interval + xtime_remainder)를 빼고 시프트된 ntp_tick 을 추가한다.

 

accumulate_nsecs_to_secs()

kernel/time/timekeeping.c

/**
 * accumulate_nsecs_to_secs - Accumulates nsecs into secs
 *
 * Helper function that accumulates a the nsecs greater then a second
 * from the xtime_nsec field to the xtime_secs field.
 * It also calls into the NTP code to handle leapsecond processing.
 *
 */
static inline unsigned int accumulate_nsecs_to_secs(struct timekeeper *tk)
{
        u64 nsecps = (u64)NSEC_PER_SEC << tk->tkr.shift;
        unsigned int clock_set = 0;

        while (tk->tkr.xtime_nsec >= nsecps) {
                int leap;

                tk->tkr.xtime_nsec -= nsecps;
                tk->xtime_sec++;

                /* Figure out if its a leap sec and apply if needed */
                leap = second_overflow(tk->xtime_sec);
                if (unlikely(leap)) {
                        struct timespec64 ts;

                        tk->xtime_sec += leap;

                        ts.tv_sec = leap;
                        ts.tv_nsec = 0;
                        tk_set_wall_to_mono(tk,
                                timespec64_sub(tk->wall_to_monotonic, ts));

                        __timekeeping_set_tai_offset(tk, tk->tai_offset - leap);

                        clock_set = TK_CLOCK_WAS_SET;
                }
        }
        return clock_set;
}

xtime 나노초가 1초를 초과한 경우 이 값을 xtime 초에 반영한다. 또한 leap second 처리를 위한 NTP 코드를 호출한다.

  • 코드 라인 11에서 1초에 해당하는 나노초에 좌측 shift를 적용한 값을 nsecps에 대입한다.
    • 예) 1E9 << shift
  • 코드 라인 14~18에서 xtime_nsec가 1초를 초과한 경우 xtime_nsec와 xtime_sec에 대해 1초를 조정한다.
  • 코드 라인 21에서 xtime_sec에 변화가 있는 경우 NTP 코드를 호출하고 leap second를 알아온다.
  • 코드 라인 22~35에서 leaf 값이 있는 경우 wtm 값을 조절하고 tai_offset 값도 조절한다.
  • 코드 라인 37에서 leaf 조정이 없었던 경우 0을 반환하고 조정이 있었던 경우 TK_CLOCK_WAS_SET 값을 반환한다.

 

NTP 조정

second_overflow()

kernel/time/ntp.c

/*
 * this routine handles the overflow of the microsecond field
 *
 * The tricky bits of code to handle the accurate clock support
 * were provided by Dave Mills (Mills@UDEL.EDU) of NTP fame.
 * They were originally developed for SUN and DEC kernels.
 * All the kudos should go to Dave for this stuff.
 *
 * Also handles leap second processing, and returns leap offset
 */
int second_overflow(unsigned long secs)
{
        s64 delta;
        int leap = 0;

        /*
         * Leap second processing. If in leap-insert state at the end of the
         * day, the system clock is set back one second; if in leap-delete
         * state, the system clock is set ahead one second.
         */
        switch (time_state) {
        case TIME_OK:
                if (time_status & STA_INS)
                        time_state = TIME_INS;
                else if (time_status & STA_DEL)
                        time_state = TIME_DEL;
                break;
        case TIME_INS:
                if (!(time_status & STA_INS))
                        time_state = TIME_OK;
                else if (secs % 86400 == 0) {
                        leap = -1;
                        time_state = TIME_OOP;
                        printk(KERN_NOTICE
                                "Clock: inserting leap second 23:59:60 UTC\n");
                }
                break;
        case TIME_DEL:
                if (!(time_status & STA_DEL))
                        time_state = TIME_OK;
                else if ((secs + 1) % 86400 == 0) {
                        leap = 1;
                        time_state = TIME_WAIT;
                        printk(KERN_NOTICE
                                "Clock: deleting leap second 23:59:59 UTC\n");
                }
                break;
        case TIME_OOP:
                time_state = TIME_WAIT;
                break;

        case TIME_WAIT:
                if (!(time_status & (STA_INS | STA_DEL)))
                        time_state = TIME_OK;
                break;
        }

 

        /* Bump the maxerror field */
        time_maxerror += MAXFREQ / NSEC_PER_USEC;
        if (time_maxerror > NTP_PHASE_LIMIT) {
                time_maxerror = NTP_PHASE_LIMIT;
                time_status |= STA_UNSYNC;
        }

        /* Compute the phase adjustment for the next second */
        tick_length      = tick_length_base;

        delta            = ntp_offset_chunk(time_offset);
        time_offset     -= delta;
        tick_length     += delta;

        /* Check PPS signal */
        pps_dec_valid();

        if (!time_adjust)
                goto out;

        if (time_adjust > MAX_TICKADJ) {
                time_adjust -= MAX_TICKADJ;
                tick_length += MAX_TICKADJ_SCALED;
                goto out;
        }

        if (time_adjust < -MAX_TICKADJ) {
                time_adjust += MAX_TICKADJ;
                tick_length -= MAX_TICKADJ_SCALED;
                goto out;
        }

        tick_length += (s64)(time_adjust * NSEC_PER_USEC / NTP_INTERVAL_FREQ)
                                                         << NTP_SCALE_SHIFT;
        time_adjust = 0;

out:
        return leap;
}

 

Timekeeping 조정

참고: timekeeping: Rework frequency adjustments to work better w/ nohz

timekeeping_adjust()

kernel/time/timekeeping.c

/*
 * Adjust the timekeeper's multiplier to the correct frequency
 * and also to reduce the accumulated error value.
 */
static void timekeeping_adjust(struct timekeeper *tk, s64 offset)
{
        /* Correct for the current frequency error */
        timekeeping_freqadjust(tk, offset);

        /* Next make a small adjustment to fix any cumulative error */
        if (!tk->ntp_err_mult && (tk->ntp_error > 0)) {
                tk->ntp_err_mult = 1;
                timekeeping_apply_adjustment(tk, offset, 0, 0);
        } else if (tk->ntp_err_mult && (tk->ntp_error <= 0)) {
                /* Undo any existing error adjustment */
                timekeeping_apply_adjustment(tk, offset, 1, 0);
                tk->ntp_err_mult = 0;
        }

        if (unlikely(tk->tkr.clock->maxadj &&
                (abs(tk->tkr.mult - tk->tkr.clock->mult)
                        > tk->tkr.clock->maxadj))) {
                printk_once(KERN_WARNING
                        "Adjusting %s more than 11%% (%ld vs %ld)\n",
                        tk->tkr.clock->name, (long)tk->tkr.mult,
                        (long)tk->tkr.clock->mult + tk->tkr.clock->maxadj);
        }

        /*
         * It may be possible that when we entered this function, xtime_nsec
         * was very small.  Further, if we're slightly speeding the clocksource
         * in the code above, its possible the required corrective factor to
         * xtime_nsec could cause it to underflow.
         *
         * Now, since we already accumulated the second, cannot simply roll
         * the accumulated second back, since the NTP subsystem has been
         * notified via second_overflow. So instead we push xtime_nsec forward
         * by the amount we underflowed, and add that amount into the error.
         *
         * We'll correct this error next time through this function, when
         * xtime_nsec is not as small.
         */
        if (unlikely((s64)tk->tkr.xtime_nsec < 0)) {
                s64 neg = -(s64)tk->tkr.xtime_nsec;
                tk->tkr.xtime_nsec = 0;
                tk->ntp_error += neg << tk->ntp_error_shift;
        }
}

Timekeeper가 올바른 주파수를 갖도록 mult를 변경하고 에러 값 역시 줄이게 한다.

  • 코드 라인 8에서 NTP에 지정된 주파수와 일치시키는 데 필요한 mult 조정을 산출한다.
  • 코드 라인 11~13에서 ntp_error가 양수이고 ntp_err_mult가 0이면 ntp_err_mult를 1로 변경하고 배율 변경없이 +offset만큼 교정한다.
  • 코드 라인 14~18에서 ntp_error가 음수이고 ntp_err_mult가 1이면 ntp_err_mult를 0으로 변경하고 배율 변경없이 -offset만큼 교정한다.
  • 코드 라인 20~27에서 mult 값이 클럭의 maxadj 배율을 벗어나면 경고 메시지를 출력한다.
  • 코드 라인 43~47에서 xime_nsec가 0보다 작은 경우 xtime_nsec 값을 0으로 변경하고 ntp_error에 그 작은 값 만큼을 양수로 변환하여 더한다. ntp_error에 더할 때엔 ntp_error_shift를 적용하여 더해야한다.

 

timekeeping_freqadjust()

kernel/time/timekeeping.c

/*
 * Calculate the multiplier adjustment needed to match the frequency
 * specified by NTP
 */
static __always_inline void timekeeping_freqadjust(struct timekeeper *tk,
                                                        s64 offset)
{
        s64 interval = tk->cycle_interval;
        s64 xinterval = tk->xtime_interval;
        s64 tick_error;
        bool negative;
        u32 adj;

        /* Remove any current error adj from freq calculation */
        if (tk->ntp_err_mult)
                xinterval -= tk->cycle_interval;

        tk->ntp_tick = ntp_tick_length();

        /* Calculate current error per tick */
        tick_error = ntp_tick_length() >> tk->ntp_error_shift;
        tick_error -= (xinterval + tk->xtime_remainder);

        /* Don't worry about correcting it if its small */
        if (likely((tick_error >= 0) && (tick_error <= interval)))
                return;

        /* preserve the direction of correction */
        negative = (tick_error < 0);

        /* Sort out the magnitude of the correction */
        tick_error = abs(tick_error);
        for (adj = 0; tick_error > interval; adj++)
                tick_error >>= 1;

        /* scale the corrections */
        timekeeping_apply_adjustment(tk, offset, negative, adj);
}

NTP에 지정된 주파수와 일치시키는 데 필요한 mult 조정을 산출한다.

  • 코드 라인 15~16에서 mult 교정에 에러가 있는 경우 xtime 인터벌에서 cycle 인터벌을 뺀다.
  • 코드 라인 18에서 ntp_tick에 ntp 틱 길이를 알아온다.
  • 코드 라인 21~26에서 tick 에러가 없는 경우 함수를 빠져나간다.
    • shift된 ntp_tick_length 값에서 shift를 제거하고 xinterval 값과 xtime_remainder 값을 제외시킨 값이 0 ~ cycle_interval 범위를 벗어난 경우 에러
  • 코드 라인 32~34에서 tick_error가 cycle_interval보다 큰 동안 adj를 증가시키고 tick_error 값을 50%씩 줄인다.
  • 코드 라인 37에서 배율(adj) 적용된 offset으로 교정한다.

 

timekeeping_apply_adjustment()

kernel/time/timekeeping.c

/*
 * Apply a multiplier adjustment to the timekeeper
 */
static __always_inline void timekeeping_apply_adjustment(struct timekeeper *tk,
                                                         s64 offset,
                                                         bool negative,
                                                         int adj_scale)
{
        s64 interval = tk->cycle_interval;
        s32 mult_adj = 1;

        if (negative) {
                mult_adj = -mult_adj;
                interval = -interval;
                offset  = -offset;
        }
        mult_adj <<= adj_scale;
        interval <<= adj_scale;
        offset <<= adj_scale;

adj_scale 배율로 offset 만큼 교정한다. (tkr.mult, tkr.xtime_nsec, xtime_interval, ntp_error 값 갱신)

  • 코드 라인 9~10에서 one NTP 인터벌에 해당하는 ns 값을 interval에 대입하고 mult_adj 값은 1부터 시작한다.
  • 코드 라인 12~16에서 음수(감소) 교정인 경우 mult_adj, interval, offset을 음수로 바꾼다.
  • 코드 라인 17~19에서 mult_adj, interval, offset 값들을 adj_scale 만큼 좌측으로 shift 하여 배율을 적용한다. (x1, x2, x4, x8, x16, …)
.        /*
         * So the following can be confusing.
         *
         * To keep things simple, lets assume mult_adj == 1 for now.
         *
         * When mult_adj != 1, remember that the interval and offset values
         * have been appropriately scaled so the math is the same.
         *
         * The basic idea here is that we're increasing the multiplier
         * by one, this causes the xtime_interval to be incremented by
         * one cycle_interval. This is because:
         *      xtime_interval = cycle_interval * mult
         * So if mult is being incremented by one:
         *      xtime_interval = cycle_interval * (mult + 1)
         * Its the same as:
         *      xtime_interval = (cycle_interval * mult) + cycle_interval
         * Which can be shortened to:
         *      xtime_interval += cycle_interval
         *
         * So offset stores the non-accumulated cycles. Thus the current
         * time (in shifted nanoseconds) is:
         *      now = (offset * adj) + xtime_nsec
         * Now, even though we're adjusting the clock frequency, we have
         * to keep time consistent. In other words, we can't jump back
         * in time, and we also want to avoid jumping forward in time.
         *
         * So given the same offset value, we need the time to be the same
         * both before and after the freq adjustment.
         *      now = (offset * adj_1) + xtime_nsec_1
         *      now = (offset * adj_2) + xtime_nsec_2
         * So:
         *      (offset * adj_1) + xtime_nsec_1 =
         *              (offset * adj_2) + xtime_nsec_2
         * And we know:
         *      adj_2 = adj_1 + 1
         * So:
         *      (offset * adj_1) + xtime_nsec_1 =
         *              (offset * (adj_1+1)) + xtime_nsec_2
         *      (offset * adj_1) + xtime_nsec_1 =
         *              (offset * adj_1) + offset + xtime_nsec_2
         * Canceling the sides:
         *      xtime_nsec_1 = offset + xtime_nsec_2
         * Which gives us:
         *      xtime_nsec_2 = xtime_nsec_1 - offset
         * Which simplfies to:
         *      xtime_nsec -= offset
         *
         * XXX - TODO: Doc ntp_error calculation.
         */

 

        if ((mult_adj > 0) && (tk->tkr.mult + mult_adj < mult_adj)) {
                /* NTP adjustment caused clocksource mult overflow */
                WARN_ON_ONCE(1);
                return;
        }

        tk->tkr.mult += mult_adj;
        tk->xtime_interval += interval;
        tk->tkr.xtime_nsec -= offset;
        tk->ntp_error -= (interval - offset) << tk->ntp_error_shift;
}
  • 코드 라인 1~5에서 양수 교정이고 mult가 시스템이 표현하는 값을 초과하여 overflow된 경우 경고 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 7~8에서 mult 값에 mult_adj를 추가하고 xtime_interval도 interval을 추가한다.
  • 코드 라인 9에서 xtime_nsec 값은 offset을 감소시킨다.
  • 코드 라인 10에서 interval에서 offset을 뺀 ns를 ntp_error에서 감소시킨다.

 

Old vsyscall 교정(fixup)

old_vsyscall_fixup()

kernel/time/timekeeping.c

static inline void old_vsyscall_fixup(struct timekeeper *tk)
{
        s64 remainder;

        /*
        * Store only full nanoseconds into xtime_nsec after rounding
        * it up and add the remainder to the error difference.
        * XXX - This is necessary to avoid small 1ns inconsistnecies caused
        * by truncating the remainder in vsyscalls. However, it causes
        * additional work to be done in timekeeping_adjust(). Once
        * the vsyscall implementations are converted to use xtime_nsec
        * (shifted nanoseconds), and CONFIG_GENERIC_TIME_VSYSCALL_OLD
        * users are removed, this can be killed.
        */
        remainder = tk->tkr.xtime_nsec & ((1ULL << tk->tkr.shift) - 1);
        tk->tkr.xtime_nsec -= remainder;
        tk->tkr.xtime_nsec += 1ULL << tk->tkr.shift;
        tk->ntp_error += remainder << tk->ntp_error_shift;
        tk->ntp_error -= (1ULL << tk->tkr.shift) << tk->ntp_error_shift;
}

xtime_nsec에서 1ns를 증가시키고 ntp_error에서는 1 ns를 감소시킨다. 그런 후 xtime_nsec에서 1ns 이하의 값은 제거하여 ntp_error에 추가한다.

  • 코드 라인 15에서 xtime_nsec에서 나노초 이하의 값을 remainder로 담는다.
  • 코드 라인 16~17에서 xtime 나노초 이하를 제거하고 1ns를 더한다.
  • 코드 라인 18~19에서 1나노초 – remainder 값들을 ntp_error에서 감소시킨다.

 

예) tkr.shift=16, ntp_error_shift=8, ntp_error=0x1000_0000 (16ns) 이라할 때 xtime_nsec=0x1234_5678 값이면 4660.22136 ns와 같다. 여기에서 교정되는 값들을 확인해본다.

  • 먼저 remainder=0x5678 (0.22136 ns) 가 산출된다.
  • 그 다음 xtime_nsec=0x1235_0000 값으로 4661 ns와 같다.
  • ntp_error = 0x1000_0000 – 0xa9_8800 (0x100_0000 – 0x56_7800) = 0xf56_7800 (15.22136 ns) 값이 된다.

 

예) tkr.shift=16, ntp_error_shift=8, ntp_error=0x1000_0000 (16ns) 이라할 때 xtime_nsec=0x1234_0000 값이면 4660 ns와 같다. 이렇게 나노초 이하의 값이 없는 경우 변화되는 값들을 확인해본다.

  • 먼저 remainder=0 이고
  • 그 다음 xtime_nsec=0x1235_0000 값으로 4661 ns와 같다.
  • ntp_error = 0x1000_0000 – 0x100_0000 = 0xf00_0000 (15 ns) 값이 된다.

 

PM Suspend & Resume 통지 등록

timekeeping_init_ops()

kernel/time/timekeeping.c

static int __init timekeeping_init_ops(void)
{
        register_syscore_ops(&timekeeping_syscore_ops);
        return 0;
}
device_initcall(timekeeping_init_ops);

timekeeping 관련한 syscore_ops를 syscore_ops_list에 등록한다.

 

kernel/time/timekeeping.c

/* sysfs resume/suspend bits for timekeeping */
static struct syscore_ops timekeeping_syscore_ops = {
        .resume         = timekeeping_resume,
        .suspend        = timekeeping_suspend,
};

 

register_syscore_ops()

drivers/base/syscore.c

/**
 * register_syscore_ops - Register a set of system core operations.
 * @ops: System core operations to register.
 */
void register_syscore_ops(struct syscore_ops *ops)
{
        mutex_lock(&syscore_ops_lock);
        list_add_tail(&ops->node, &syscore_ops_list);
        mutex_unlock(&syscore_ops_lock);
}
EXPORT_SYMBOL_GPL(register_syscore_ops);

syscore_ops를 syscore_ops_list에 등록한다.

 

구조체

timekeeper 구조체

include/linux/timekeeper_internal.h

/**
 * struct timekeeper - Structure holding internal timekeeping values.
 * @tkr:                The readout base structure
 * @xtime_sec:          Current CLOCK_REALTIME time in seconds
 * @ktime_sec:          Current CLOCK_MONOTONIC time in seconds
 * @wall_to_monotonic:  CLOCK_REALTIME to CLOCK_MONOTONIC offset
 * @offs_real:          Offset clock monotonic -> clock realtime
 * @offs_boot:          Offset clock monotonic -> clock boottime
 * @offs_tai:           Offset clock monotonic -> clock tai
 * @tai_offset:         The current UTC to TAI offset in seconds
 * @base_raw:           Monotonic raw base time in ktime_t format
 * @raw_time:           Monotonic raw base time in timespec64 format
 * @cycle_interval:     Number of clock cycles in one NTP interval
 * @xtime_interval:     Number of clock shifted nano seconds in one NTP
 *                      interval.
 * @xtime_remainder:    Shifted nano seconds left over when rounding
 *                      @cycle_interval
 * @raw_interval:       Raw nano seconds accumulated per NTP interval.
 * @ntp_error:          Difference between accumulated time and NTP time in ntp
 *                      shifted nano seconds.
 * @ntp_error_shift:    Shift conversion between clock shifted nano seconds and
 *                      ntp shifted nano seconds.
 *
 * Note: For timespec(64) based interfaces wall_to_monotonic is what
 * we need to add to xtime (or xtime corrected for sub jiffie times)
 * to get to monotonic time.  Monotonic is pegged at zero at system
 * boot time, so wall_to_monotonic will be negative, however, we will
 * ALWAYS keep the tv_nsec part positive so we can use the usual
 * normalization.
 *
 * wall_to_monotonic is moved after resume from suspend for the
 * monotonic time not to jump. We need to add total_sleep_time to
 * wall_to_monotonic to get the real boot based time offset.
 *
 * wall_to_monotonic is no longer the boot time, getboottime must be
 * used instead.
 */
struct timekeeper {
        struct tk_read_base     tkr;
        u64                     xtime_sec;
        unsigned long           ktime_sec;
        struct timespec64       wall_to_monotonic;
        ktime_t                 offs_real;
        ktime_t                 offs_boot;
        ktime_t                 offs_tai;
        s32                     tai_offset;
        ktime_t                 base_raw;
        struct timespec64       raw_time;

        /* The following members are for timekeeping internal use */
        cycle_t                 cycle_interval;
        u64                     xtime_interval;
        s64                     xtime_remainder;
        u32                     raw_interval;
        /* The ntp_tick_length() value currently being used.
         * This cached copy ensures we consistently apply the tick
         * length for an entire tick, as ntp_tick_length may change
         * mid-tick, and we don't want to apply that new value to
         * the tick in progress.
         */
        u64                     ntp_tick;
        /* Difference between accumulated time and NTP time in ntp
         * shifted nano seconds. */
        s64                     ntp_error;
        u32                     ntp_error_shift;
        u32                     ntp_err_mult;
};
  • tkr
    • 아래 tk_read_base 구조체
  • xtime_sec
    • xtime 초
  • ktime_sec
    • ktime 초
  • wall_to_monotonic
    • real 시각을 monotonic으로 변환하기 위해 더할 숫자로 보통 음수가 담긴다.
    • realtime + wall_to_monotonic = monotonic
  • offs_real
    • monotonic을 사용하여 real 시각을 산출하기 위해 더할 숫자로 보통 양수가 담긴다.
    • monotonic + offs_real = realtime
  • offs_boot
    • monotonic을 사용하여 boot-up 시각을 산출하기 위해 더할 숫자로 보통 양수가 담긴다.
    • bootup 시간은 monotonic과 다르게 suspend 시에도 증가된다.
    • monotonic + offs_boot = boottime
  • offs_tai
    • monotonic을 사용하여 tai(우주천문시계) 시각을 산출하기 위해 더할 숫자로 보통 양수가 담긴다.
    • monotonic + offs_tai = tai
  • tai_offset
    • 현재 UTC -> TAI로 바꾸기 위한 offset 초
  • base_raw
    • ktime_t 포맷의 monotonic raw 기반 시각
  • raw_time
    • timespec64 포맷의 monotonic raw 기반 시각
  • cycle_interval
    • 1 NTP 인터벌에 해당하는 cycle 수
    • update_wall_time() 함수가 호출될 때 클럭 소스로 부터 읽은 카운터 cycle이 이 값을 초과할 경우에만 timekeeping에 반영한다.
    • 예) 100hz 시스템인 경우 1E7 ns에 해당하는 cycle 수가 담긴다.
  • xtime_interval
    • 1 NTP 인터벌에 해당하는 나노초를 좌측 shift 한 값
  • xtime_remainder
    • 1초 – xtime_interval을 하여 남는 나노초보다 작은 수
  • raw_interval
    • 1 NTP 인터벌당 산출된 raw 나노초

 

tk_read_base 구조체

include/linux/timekeeper_internal.h

/**
 * struct tk_read_base - base structure for timekeeping readout
 * @clock:      Current clocksource used for timekeeping.
 * @read:       Read function of @clock
 * @mask:       Bitmask for two's complement subtraction of non 64bit clocks
 * @cycle_last: @clock cycle value at last update
 * @mult:       NTP adjusted multiplier for scaled math conversion
 * @shift:      Shift value for scaled math conversion
 * @xtime_nsec: Shifted (fractional) nano seconds offset for readout
 * @base_mono:  ktime_t (nanoseconds) base time for readout
 *
 * This struct has size 56 byte on 64 bit. Together with a seqcount it
 * occupies a single 64byte cache line.
 *
 * The struct is separate from struct timekeeper as it is also used
 * for a fast NMI safe accessor to clock monotonic.
 */
struct tk_read_base {
        struct clocksource      *clock;
        cycle_t                 (*read)(struct clocksource *cs);
        cycle_t                 mask;
        cycle_t                 cycle_last;
        u32                     mult;
        u32                     shift;
        u64                     xtime_nsec;
        ktime_t                 base_mono;
};
  • *clock
    • Timekeeper 클럭 소스
  • (*read)
    • 클럭 소스의 cycle 읽기 함수
  • mask
    • 읽은 값 중 얻을 값만 가려내기 위한 mask
  • mult/shift
    • 1 cycle 을 mult로 곱하고 우측 shift 하여 ns를 산출
  • xtime_nsec
    • 나노초 << shift 하여 보관된다.
  • base_mono
    • ktime_t 기반 monotonic

 

참고

 

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.