Timer -2- (HRTimer)

 

HR(High Resolution) Timer

  • hrtimer(High Resolution Timer)는 커널 v2.6.21에서 mainline에 채용되었고 1ns 단위의 고해상도로 관리한다.
    • 기존 오리지날 커널 타이머는 jiffies 기반의 lowres 타이머를 사용하여 구현되었고 HZ기반 tick에 의해 해상도가 수 ms ~ 수십 ms의 낮은 해상도만을 관리할 수 있었다.
    • lowres timer를 사용하여 수 ms ~ 수십ms로 동작하는 스케줄 tick 단위 보다 더 높은 해상도의 타이머가 필요한 경우 사용된다.
  • 사용 가능한 타이머 h/w
    • hrtimer는 high resolution h/w 타이머를 사용하는 것을 기본으로 하지만 low resolution h/w 타이머도 사용할 수 있다.
  • hrtimer의 요청 타임들은 RB 트리기반으로 관리된다.

 

nano 단위로 이용가능한 hrtimer API는 다음과 같다.

  • hrtimer_init()
  • hrtimer_start()
  • hrtimer_start_range_ns()
  • hrtimer_start_expires()
  • hrtimer_restart()
  • hrtimer_cancel()
  • hrtimer_try_to_cancel()
  • hrtimer_forward()

 

hrtimer와 generic time subsystem

hrtimer 통해 다음과 같은 기능들을 수행한다.

  • 리눅스 시간 관리 (가능하면 hrtimer를 사용한다)
    • monotonic(0부터 시작한 nano 단위 타임)
    • realtime (실 세계 시간)
    • boottime(0부터 시작한 nano 단위 타임이며 suspend 시에도 동작하는 시간)
    • taiclock(윤초를 포함하는 천문에 사용하는 우주 시계)
  • 고 정밀도 타이머
    • nano 단위의 정확도로 callback 함수를 수행할 수 있다.
  • 스케쥴 tick
    • 위의 고정밀도 타이머 기능을 사용하여 주기적 또는 oneshot 기반 클럭 이벤트를 사용하여 스케쥴 tick을 제공한다.
  • lowres timer의 기반 클럭
    • jiffies로 동작하는 lowres timer(기존 kernel timer로 불림)에 제공되는 클럭
  • process accounting, profileing, …

 

주변 시스템과의 연동 관게

  • 실제 legacy 코드들은 무척 방만(?)하게 구현되어 셀 수 없이 많은 방법으로 여러 subsystem과 연결되어 있다.
    • 수 백개의 구현 코드들이 재활용 없이 copy & paste로 이쪽 저쪽에서 짜집기되어 있다.
    • 32bit arm embedded 시스템들에 구현된 많은 다양성으로 인해 리눅스의 누구 누구는 거의 포기했다는 말이 있다.
    • 그럼에도 불구하고 common subsystem 등이 계속 정리되어 가고 있고 근래에는 device tree를 통해서 더 표준화 되어 가고 있다.

 

다음 그림은 최근 커널의 Time subsystem 간의 연동 관계를 보여준다.

  • 좌측은 4개의 리눅스 시간을 관리하는 timekeeping을 clock source로 부터 지속적으로 공급받는 것을 보여준다.
  • 우측은 hrtimer의 만료 시간에 인터럽트가 깨어나 clock event 를 통해 tick 디바이스에 공급되고 각 서브시스템으로 전달되는 과정을 보여준다.

 

ktime

hrtimer 값은 다음과 같이 ktime이라는 하나의 signed 64비트 값을 사용한다.

include/linux/ktime.h

/*
 * ktime_t: 
 *
 * A single 64-bit variable is used to store the hrtimers
 * internal representation of time values in scalar nanoseconds. The
 * design plays out best on 64-bit CPUs, where most conversions are
 * NOPs and most arithmetic ktime_t operations are plain arithmetic
 * operations.
 *
 */
union ktime {
        s64     tv64;
};

typedef union ktime ktime_t;            /* Kill this */

 

ktime 로드/설정 관련한 api는 다음과 같다.

  • ktime_get()
  • ktime_get_ns()
  • ktime_get_with_offset()
  • ktime_mono_to_any()
  • ktime_get_raw()
  • ktime_get_raw_ns()
  • ktime_get_real()
  • ktime_get_real_ns()
  • ktime_get_boottime()
  • ktime_get_clocktai()
  • ktime_set()
  • ktime_mono_to_real()
  • do_gettimeofday()
  • do_settimeofday64()
  • do_sys_settimeofday()

 

ktime 설정, 연산, 비교와 관련한 api는 다음과 같다

  • ktime_add()
  • ktime_add_ns()
  • ktime_add_us()
  • ktime_sub()
  • ktime_sub_ns()
  • ktime_sub_us()
  • ktime_equal()
  • ktime_compare()
  • ktime_after()
  • ktime_before()
  • ktime_divns()

 

ktime  변환 관련한 api는 다음과 같다.

  • ktime_to_timespec()
  • ktime_to_timespec_cond()
  • ktime_to_timespec64()
  • ktime_to_timespec64_cond()
  • timespec_to_ktime()
  • timespec64_to_ktime()
  • ktime_to_timeval()
  • timeval_to_ktime()
  • ktime_to_ns()
  • ktime_to_us()
  • ktime_to_ms()
  • ns_to_ktime()
  • ms_to_ktime()
  • ktime_us_delta()
  • ktime_ms_delta()

 

HRTimer subsystem이 관리하는 4가지 CLOCK

  • CLOCK_MONOTINIC
    • 부팅 후 0에서 시작하여 단조롭게 계속 전진하는 것을 보장하며 jiffies tick 수와 유사하다. 단 suspend 된 시간은 포함되지 않는다.
  • CLOCK_REALTIME
    • 실제 클럭을 관리한다. (real world clock)
  • CLOCK_BOOTTIME
    • CLOCK_MONOTONIC과 유사하게 커널이 부팅된 후의 클럭을 관리한다. 다른 점으로 suspend 된 시간도 포함한다.
    • 참고: [RFC] Introduce CLOCK_BOOTTIME | LWN.net
  • CLOCK_TAI
    • 천문학에서 사용하는 우주 표준시
    • UTC(Coordinated Universal Time) 기반의 클럭을 유사하지만 윤초가 추가되어 2016 년 12 월 31 일부터 TAI 클럭은 UTC보다 37초 앞당겨진다. 그 전에는 27초가 앞당겨져 있었다.
    • International Atomic Time | Wikipedia

 

HRTimer Latency

hrtimer들은 1 ns 단위의 고해상도로 동작하지만 리눅스의 hrtimer 인터럽트 처리 루틴이 bottom-half로 구현된 softirq에서 처리되므로 수ns ~ 수백 us(평균: 수십 us)의 latency가 발생함을 주의해야 한다.

  • 참고: KTAS: Analysis of Timer Latency for Embedded Linux Kernel – 다운로드 pdf

 

다음 그림은 위의 참고 자료에 나온 hrtimer에 대한 softirq의 latency를 10,000번 테스트한 결과를 보여준다.

다만 최근 커널 v4.2-rc1 부터 hrtimer는 softirq에서 제거되고 원래 구현으로 원복되었다.

 

RT(RealTime) 리눅스 커널을 사용하는 경우 일반 리눅스 커널보다 더 빠른 latency를 보장받을 수 있다.

  • 참고: Evaluation of Real-time property in Embedded Linux | Hitachi – 다운로드 pdf

 

다음 그림은 위의 참고 자료에 나온 RT 커널과 일반 커널간 인터럽트 response time에 대한 대략적인 latency 비교를 보여준다.

 

HRTimer softirq 핸들러

run_hrtimer_softirq()

kernel/time/hrtimer.c

static void run_hrtimer_softirq(struct softirq_action *h)
{
        hrtimer_peek_ahead_timers();
}

hrtimer용 softirq의 진입함수이다. hrtimer로 요청된 후 만료되어 인터럽트 처리로 넘어온 경우 요청한 hrtimer에 연동된 핸들러 함수를 처리한다.

  • 커널 v4.2-rc1에서는 hrtimer용 softirq를 사용하지 않고 clock_event_device의 tick_device를 통해 hrtimer_interrupt() 함수를 직접 호출한다.

 

hrtimer_peek_ahead_timers()

kernel/time/hrtimer.c

/**
 * hrtimer_peek_ahead_timers -- run soft-expired timers now
 *
 * hrtimer_peek_ahead_timers will peek at the timer queue of
 * the current cpu and check if there are any timers for which
 * the soft expires time has passed. If any such timers exist,
 * they are run immediately and then removed from the timer queue.
 *
 */
void hrtimer_peek_ahead_timers(void)
{
        unsigned long flags;

        local_irq_save(flags);
        __hrtimer_peek_ahead_timers();
        local_irq_restore(flags);
}

hrtimer로 요청된 후 만료되어 인터럽트 처리로 넘어온 경우 요청한 hrtimer에 연동된 핸들러 함수를 처리한다.

 

__hrtimer_peek_ahead_timers()

kernel/time/hrtimer.c

/*
 * local version of hrtimer_peek_ahead_timers() called with interrupts
 * disabled.
 */
static void __hrtimer_peek_ahead_timers(void)
{
        struct tick_device *td;

        if (!hrtimer_hres_active())
                return;

        td = this_cpu_ptr(&tick_cpu_device);
        if (td && td->evtdev)
                hrtimer_interrupt(td->evtdev);
}

hrtimer에 대한 softirq 요청이 발생한 경우 처리할 이벤트 디바이스를 통해 hrtimer 인터럽트를 처리한다.

  • 코드 라인 9~10에서 현재 cpu에 동작중인 hrtimer가 있는지 없으면 처리 없이 함수를 빠져나간다.
  • 코드 라인 12~14에서 tick cpu 디바이스가 이벤트 디바이스가 등록된 경우 hrtimer 인터럽트를 처리한다.

 

hrtimer_hres_active()

kernel/time/hrtimer.c

/*
 * Is the high resolution mode active ?
 */
static inline int hrtimer_hres_active(void) 
{
        return __this_cpu_read(hrtimer_bases.hres_active); 
}

현재 cpu에 동작중인 hrtimer가 있는지 여부를 알아온다. 0=동작중인 hrtimer 없음, 그외=동작중인 hrtimer 있음

 

hrtimer_interrupt()

kernel/time/hrtimer.c

/*
 * High resolution timer interrupt
 * Called with interrupts disabled
 */
void hrtimer_interrupt(struct clock_event_device *dev)
{
        struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
        ktime_t expires_next, now, entry_time, delta;
        int i, retries = 0;

        BUG_ON(!cpu_base->hres_active);
        cpu_base->nr_events++;
        dev->next_event.tv64 = KTIME_MAX;

        raw_spin_lock(&cpu_base->lock);
        entry_time = now = hrtimer_update_base(cpu_base);
retry:
        cpu_base->in_hrtirq = 1;
        /*
         * We set expires_next to KTIME_MAX here with cpu_base->lock
         * held to prevent that a timer is enqueued in our queue via
         * the migration code. This does not affect enqueueing of
         * timers which run their callback and need to be requeued on
         * this CPU.
         */
        cpu_base->expires_next.tv64 = KTIME_MAX;

        for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
                struct hrtimer_clock_base *base;
                struct timerqueue_node *node;
                ktime_t basenow;

                if (!(cpu_base->active_bases & (1 << i)))
                        continue;

                base = cpu_base->clock_base + i;
                basenow = ktime_add(now, base->offset);

                while ((node = timerqueue_getnext(&base->active))) {
                        struct hrtimer *timer;

                        timer = container_of(node, struct hrtimer, node);

                        /*
                         * The immediate goal for using the softexpires is
                         * minimizing wakeups, not running timers at the
                         * earliest interrupt after their soft expiration.
                         * This allows us to avoid using a Priority Search
                         * Tree, which can answer a stabbing querry for
                         * overlapping intervals and instead use the simple
                         * BST we already have.
                         * We don't add extra wakeups by delaying timers that
                         * are right-of a not yet expired timer, because that
                         * timer will have to trigger a wakeup anyway.
                         */
                        if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer))
                                break;

                        __run_hrtimer(timer, &basenow);
                }
        }
        /* Reevaluate the clock bases for the next expiry */
        expires_next = __hrtimer_get_next_event(cpu_base);
        /*
         * Store the new expiry value so the migration code can verify
         * against it.
         */
        cpu_base->expires_next = expires_next;
        cpu_base->in_hrtirq = 0;
        raw_spin_unlock(&cpu_base->lock);

hrtimer 인터럽트 핸들러 루틴으로 이 함수는 softirq에서 호출되었으나 커널 v4.2-rc1에서는 hrtimer용 softirq를 사용하지 않고 clock_event_device의 tick_device를 통해 이 함수를 직접 호출한다.

  • 코드 라인 12에서 nr_events 카운터를 1 증가시킨다.
  • 코드 라인 13에서 다음 이벤트에 KTIME_MAX 값을 대입한다.
  • 코드 라인 16에서 락을 얻은 후 timekeeper를 위한 clocksource로부터 읽은 값으로 real, boot, tail 시간의 offset를 갱신하고 monotonic 시간을 알아와서 entry_time과 now에 대입한다.
  • 코드 라인 18에서 hrtimer irq 처리가 진행중임을 표시한다.
  • 코드 라인 28~34에서 HRTIME_MAX_CLOCK_BASES(4) 수 만큼 루프를 돌며 해당 클럭에 요청된 타이머가 없으면 skip 한다.
    • cpu_base->active_bases는 4개의 비트마스크로 각 비트는 clock 별로 활성화된 hrtimer가 있는 경우에만 1로 설정된다.
  • 코드 라인 36~37에서 처리할 clock base를 선택하고 now에 offset 값을 더해 basenow에 대입한다.
    • 예) realtime 클럭으로 요청된 hrtimer를 처리하는 경우
      • realtime 시간(basenow) = monotonic 시간(now) + realtime offset(base->offset)
  • 코드 라인 39~61에서 처리할 base 클럭에서 다음 처리할 hrtimer 요청을 읽어와서 현재 시간이 타이머의 soft 만료 시간 보다 이전인 경우 처리를 하지 않고 루프를 빠져나간다. 그렇지 않은 경우 만료된 hrtimer를 처리한다. 결국 하나의 인터럽트로 인근에 있는 soft 만료 시간이 지난 타이머들을 같이 처리한다.
  • 코드 라인 63에서 다음 처리할 hrtimer를 구해와서 expires_next에 대입하고 hrtimer의 softirq 처리가 완료되었음을 표시하고 락을 해제한다.

 

        /* Reprogramming necessary ? */
        if (expires_next.tv64 == KTIME_MAX ||
            !tick_program_event(expires_next, 0)) {
                cpu_base->hang_detected = 0;
                return;
        }

        /*
         * The next timer was already expired due to:
         * - tracing
         * - long lasting callbacks
         * - being scheduled away when running in a VM
         *
         * We need to prevent that we loop forever in the hrtimer
         * interrupt routine. We give it 3 attempts to avoid
         * overreacting on some spurious event.
         *
         * Acquire base lock for updating the offsets and retrieving
         * the current time.
         */
        raw_spin_lock(&cpu_base->lock);
        now = hrtimer_update_base(cpu_base);
        cpu_base->nr_retries++;
        if (++retries < 3)
                goto retry;
        /*
         * Give the system a chance to do something else than looping
         * here. We stored the entry time, so we know exactly how long
         * we spent here. We schedule the next event this amount of
         * time away.
         */
        cpu_base->nr_hangs++;
        cpu_base->hang_detected = 1;
        raw_spin_unlock(&cpu_base->lock);
        delta = ktime_sub(now, entry_time);
        if (delta.tv64 > cpu_base->max_hang_time.tv64)
                cpu_base->max_hang_time = delta;
        /*
         * Limit it to a sensible value as we enforce a longer
         * delay. Give the CPU at least 100ms to catch up.
         */
        if (delta.tv64 > 100 * NSEC_PER_MSEC)
                expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
        else
                expires_next = ktime_add(now, delta);
        tick_program_event(expires_next, 1);
        printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",
                    ktime_to_ns(delta));
}
  • 코드 라인 2~6에서 tick을 리프로그래밍 요청한다. 만일 처리할 hrtimer 요청이 없거나 요청이 실패한 경우 hang_detected에 0을 대입하고 함수를 빠져나간다.
    • 틱을 프로그램하는 과정에서 이미 지나간 시간에 대해 요청을 하려는 경우 틱 프로그래밍이 불가능하다.
  • 코드 라인 21~22에서 락을 획득하고 hrtimer를 이용하여 timekepping을 갱신하고 monotonic 시간을 가져와 now에 대입한다.
  • 코드 라인 23~25에서 nr_retries 카운터를 증가시킨다. 2번 재시도 할 수 있도록 retry 레이블로 이동한다. (총 3회 = 재시도 2회 가능)
    • tracing을 하거나 시간 소요가 긴 callback 등으로 인해 이미 타이머가 만료되었을 수 있다. 이러한 경우 곧바로 해당 타이머를 처리한다. 이를 위해 최대 3회까지 시도한다.
  • 코드 라인 32~33에서 nr_hangs 카운터를 증가시키고 hang_detected에 1을 대입한다.
  • 코드 라인 35~37에서 인터럽트 처리를 위해 소모된 시간을 delta(ns 단위)에 담는다. 만일 max_hang_time보다 큰 경우 갱신한다.
  • 코드 라인 42~46에서 현재 monotonic 시간에 delta 시간을 더해 tick을 리프로그램 요청한다. 단 delta가 100ms을 초과하는 경우 delta 대신 100ms을 추가한다.

 

다음 그림은 hrtimer를 사용 시 slack range를 사용하여 인터럽트 발생 횟수를 줄이는 모습을 보여준다.

 

hrtimer_update_base()

kernel/time/hrtimer.c

static inline ktime_t hrtimer_update_base(struct hrtimer_cpu_base *base)
{
        ktime_t *offs_real = &base->clock_base[HRTIMER_BASE_REALTIME].offset;
        ktime_t *offs_boot = &base->clock_base[HRTIMER_BASE_BOOTTIME].offset;
        ktime_t *offs_tai = &base->clock_base[HRTIMER_BASE_TAI].offset;

        return ktime_get_update_offsets_now(offs_real, offs_boot, offs_tai);
}

지정한 hrtimer의 cpu base의 각 클럭들을 monotonic 클럭을 기준으로 offset 갱신한다.

 

hrtimer_bases

/*
 * The timer bases:
 *
 * There are more clockids then hrtimer bases. Thus, we index
 * into the timer bases by the hrtimer_base_type enum. When trying
 * to reach a base using a clockid, hrtimer_clockid_to_base()
 * is used to convert from clockid to the proper hrtimer_base_type.
 */
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{

        .lock = __RAW_SPIN_LOCK_UNLOCKED(hrtimer_bases.lock),
        .clock_base =
        {
                {
                        .index = HRTIMER_BASE_MONOTONIC,
                        .clockid = CLOCK_MONOTONIC,
                        .get_time = &ktime_get,
                        .resolution = KTIME_LOW_RES,
                },
                {
                        .index = HRTIMER_BASE_REALTIME,
                        .clockid = CLOCK_REALTIME,
                        .get_time = &ktime_get_real,
                        .resolution = KTIME_LOW_RES,
                },
                {
                        .index = HRTIMER_BASE_BOOTTIME,
                        .clockid = CLOCK_BOOTTIME,
                        .get_time = &ktime_get_boottime,
                        .resolution = KTIME_LOW_RES,
                },
                {
                        .index = HRTIMER_BASE_TAI,
                        .clockid = CLOCK_TAI,
                        .get_time = &ktime_get_clocktai,
                        .resolution = KTIME_LOW_RES,
                },
        }
};

hrtimer가 사용하는 4개의 clock base가 per-cpu 마다 관리된다.

 

__hrtimer_get_next_event()

kernel/time/hrtimer.c

#if defined(CONFIG_NO_HZ_COMMON) || defined(CONFIG_HIGH_RES_TIMERS)
static ktime_t __hrtimer_get_next_event(struct hrtimer_cpu_base *cpu_base)
{
        struct hrtimer_clock_base *base = cpu_base->clock_base;
        ktime_t expires, expires_next = { .tv64 = KTIME_MAX };
        int i;

        for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++, base++) {
                struct timerqueue_node *next;
                struct hrtimer *timer;

                next = timerqueue_getnext(&base->active);
                if (!next)
                        continue;

                timer = container_of(next, struct hrtimer, node);
                expires = ktime_sub(hrtimer_get_expires(timer), base->offset);
                if (expires.tv64 < expires_next.tv64)
                        expires_next = expires;
        }
        /*
         * clock_was_set() might have changed base->offset of any of
         * the clock bases so the result might be negative. Fix it up
         * to prevent a false positive in clockevents_program_event().
         */
        if (expires_next.tv64 < 0)
                expires_next.tv64 = 0;
        return expires_next;
}
#endif

요청한 cpu_base에 있는 4개의 클럭에서 대기하는 hrtimer 들 중 monotonic 기준 만료 시각이 가장 빠른 hrtimer의 monotonic 만료 시각을 반환한다.

 

다음 그림은 hw 타이머에 프로그램되는 hrtimer를 구하는 모습을 보여준다.

 

HRTimer APIs

hrtimer_init()

kernel/time/hrtimer.c

/**
 * hrtimer_init - initialize a timer to the given clock
 * @timer:      the timer to be initialized
 * @clock_id:   the clock to be used
 * @mode:       timer mode abs/rel
 */
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
                  enum hrtimer_mode mode)
{
        debug_init(timer, clock_id, mode);
        __hrtimer_init(timer, clock_id, mode);
}
EXPORT_SYMBOL_GPL(hrtimer_init);

hrtimer를 주어진 clock 타입으로 초기화한다.

 

 

__hrtimer_init()

kernel/time/hrtimer.c

static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
                           enum hrtimer_mode mode)
{
        struct hrtimer_cpu_base *cpu_base;
        int base;

        memset(timer, 0, sizeof(struct hrtimer));

        cpu_base = raw_cpu_ptr(&hrtimer_bases);

        if (clock_id == CLOCK_REALTIME && mode != HRTIMER_MODE_ABS)
                clock_id = CLOCK_MONOTONIC;

        base = hrtimer_clockid_to_base(clock_id);
        timer->base = &cpu_base->clock_base[base];
        timerqueue_init(&timer->node);

#ifdef CONFIG_TIMER_STATS
        timer->start_site = NULL;
        timer->start_pid = -1;
        memset(timer->start_comm, 0, TASK_COMM_LEN);
#endif
}

hrtimer를 주어진 clock 타입으로 초기화한다.

 

hrtimer_clockid_to_base()

kernel/time/hrtimer.c

static inline int hrtimer_clockid_to_base(clockid_t clock_id)
{
        return hrtimer_clock_to_base_table[clock_id];
}

 

kernel/time/hrtimer.c

static const int hrtimer_clock_to_base_table[MAX_CLOCKS] = {
        [CLOCK_REALTIME]        = HRTIMER_BASE_REALTIME,
        [CLOCK_MONOTONIC]       = HRTIMER_BASE_MONOTONIC,
        [CLOCK_BOOTTIME]        = HRTIMER_BASE_BOOTTIME,
        [CLOCK_TAI]             = HRTIMER_BASE_TAI,
};

clock id에 해당하는 base를 구한다.

 

 

timerqueue_init()

include/linux/timerqueue.h

static inline void timerqueue_init(struct timerqueue_node *node)
{
        RB_CLEAR_NODE(&node->node);
}

RB 트리로 관리하는 타이머큐를 초기화한다.

 

hrtimer_start()

kernel/time/hrtimer.c

/**
 * hrtimer_start - (re)start an hrtimer on the current CPU
 * @timer:      the timer to be added
 * @tim:        expiry time
 * @mode:       expiry mode: absolute (HRTIMER_MODE_ABS) or
 *              relative (HRTIMER_MODE_REL)
 *
 * Returns:
 *  0 on success
 *  1 when the timer was active
 */
int
hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)
{
        return __hrtimer_start_range_ns(timer, tim, 0, mode, 1);
}
EXPORT_SYMBOL_GPL(hrtimer_start);

hrtimer를 요청한 ktime으로 상대 시간 또는 절대 시간 후에 동작하도록 요청한다.

  • HRTIMER_MODE_REL
    • 상대 시간으로 현재 시각으로 부터 요청한 ktime(ns)이 지난 후에 동작
  • HRTIMER_MODE_ABS
    • 절대 시간으로 요청한 ktime에 동작

 

다음은 monotonic 시계를 사용하는 hrtimer를 100ms 후에 my_hrtimer_callback() 함수가 호출되게 사용한 예이다.

 

다음 그림은 monotonic 시계를 사용하는 hrtimer 두 개를 추가하였을 때의 모습을 보여준다.

 

__hrtimer_start_range_ns()

kernel/time/hrtimer.c

int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
                unsigned long delta_ns, const enum hrtimer_mode mode,
                int wakeup)
{
        struct hrtimer_clock_base *base, *new_base;
        unsigned long flags;
        int ret, leftmost;

        base = lock_hrtimer_base(timer, &flags);

        /* Remove an active timer from the queue: */
        ret = remove_hrtimer(timer, base);

        if (mode & HRTIMER_MODE_REL) {
                tim = ktime_add_safe(tim, base->get_time());
                /*
                 * CONFIG_TIME_LOW_RES is a temporary way for architectures
                 * to signal that they simply return xtime in
                 * do_gettimeoffset(). In this case we want to round up by
                 * resolution when starting a relative timer, to avoid short
                 * timeouts. This will go away with the GTOD framework.
                 */
#ifdef CONFIG_TIME_LOW_RES
                tim = ktime_add_safe(tim, base->resolution);
#endif  
        }

        hrtimer_set_expires_range_ns(timer, tim, delta_ns);

        /* Switch the timer base, if necessary: */
        new_base = switch_hrtimer_base(timer, base, mode & HRTIMER_MODE_PINNED);

        timer_stats_hrtimer_set_start_info(timer);

        leftmost = enqueue_hrtimer(timer, new_base);

        if (!leftmost) {
                unlock_hrtimer_base(timer, &flags);
                return ret;
        }

        if (!hrtimer_is_hres_active(timer)) {
                /*
                 * Kick to reschedule the next tick to handle the new timer
                 * on dynticks target.
                 */
                wake_up_nohz_cpu(new_base->cpu_base->cpu);
        } else if (new_base->cpu_base == this_cpu_ptr(&hrtimer_bases) &&
                        hrtimer_reprogram(timer, new_base)) {
                /*      
                 * Only allow reprogramming if the new base is on this CPU.
                 * (it might still be on another CPU if the timer was pending)
                 *
                 * XXX send_remote_softirq() ?
                 */
                if (wakeup) {
                        /*
                         * We need to drop cpu_base->lock to avoid a
                         * lock ordering issue vs. rq->lock.
                         */
                        raw_spin_unlock(&new_base->cpu_base->lock);
                        raise_softirq_irqoff(HRTIMER_SOFTIRQ);
                        local_irq_restore(flags);
                        return ret;
                } else {
                        __raise_softirq_irqoff(HRTIMER_SOFTIRQ);
                }
        }

        unlock_hrtimer_base(timer, &flags);

        return ret;
}
EXPORT_SYMBOL_GPL(__hrtimer_start_range_ns);

hrtimer를 요청한 ktime + delta로 상대 시간 또는 절대 시간 후에 동작하도록 요청한다. slack range가 적용되는 방식인데 실제 타이머의 만료시각은 delta가 적용된 hard 만료 타임을 사용한다. 하지만 다른 타이머 처리 시 soft 만료 시간 범위가 포함되면 다른 타이머에 의해 같이 처리할 수 있도록 slack range를 부여하는 기법이다)

  • 코드 라인 9에서 요청한 hrtimer의 cpu_base의 lock을 건다.
  • 코드 라인 12에서 요청한 hrtimer가 큐에 동작중인 경우 삭제한다.
  • 코드 라인 14~26에서 상대 시간을 요청한 경우 hrtimer가 사용하는 클럭에서 ktime을 얻어와서 요청 시간을 더한다. 만일 CONFIG_TIME_LOW_RES 커널 옵션을 사용하는 경우 base->resolution을 추가한다.
  • 코드 라인 28에서 hrtimer에 expire될 시간을 설정한다.
    • timer->_softexpires에는 time만 저장하고, timer->node.expires에는 time + delta를 저장한다.
  • 코드 라인 31에서 가능(현재 cpu가 hrtimer를  사용할 수 있는 경우)하면 현재 cpu의 클럭을 사용하고 그렇게 하지 못할 경우 다른 cpu의 클럭으로 변경한다.
  • 코드 라인 33에서 디버깅을 위한 타이머 정보를 기록한다.
  • 코드 라인 35에서 generic 타이머 큐(RB 트리로 구현)에 hrtimer를 추가한다.
  • 코드 라인 37~40에서 추가한 hrtimer가 가장 먼저 만료되는 경우가 아니면 처리를 완료하고 함수를 빠져나간다.
  • 코드 라인 42~47에서 hrtimer가 활동 중이 아니면 클럭이 동작하는 cpu를 깨우도록 한다.
    • nohz로 동작하는 cpu의 클럭은 사용하지 않는 경우 타이머 인터럽트가 발생하지 않는다.
  • 코드 라인 48~68에서 hrtimer가 이미 활동 중이면 softirqd를 깨운다. 만일 wakeup=1로 요청한 경우에는 new_base에 대한 spinlock을 풀고 irq 마스크를 원래대로 돌려놓는다.

 

다음 그림은 20us 만료시간(soft)에서 slack 20us를 추가한 40us 만료시간(hard)까지의 범위를 갖는 타이머의 모습을 보여준다.

 

hrtimer_set_expires_range_ns()

include/linux/hrtimer.h

static inline void hrtimer_set_expires_range_ns(struct hrtimer *timer, ktime_t time, unsigned long delta)
{
        timer->_softexpires = time;
        timer->node.expires = ktime_add_safe(time, ns_to_ktime(delta));
}

hrtimer의 만료 시간을 기록한다.

  • timer->_softexpires에는 time만 저장하고, timer->node.expires에는 time + delta를 저장한다.

 

switch_hrtimer_base()

kernel/time/hrtimer.c

/*
 * Switch the timer base to the current CPU when possible.
 */
static inline struct hrtimer_clock_base *
switch_hrtimer_base(struct hrtimer *timer, struct hrtimer_clock_base *base,
                    int pinned)
{
        struct hrtimer_clock_base *new_base;
        struct hrtimer_cpu_base *new_cpu_base;
        int this_cpu = smp_processor_id();
        int cpu = get_nohz_timer_target(pinned);
        int basenum = base->index;

again:
        new_cpu_base = &per_cpu(hrtimer_bases, cpu);
        new_base = &new_cpu_base->clock_base[basenum];

        if (base != new_base) {
                /*
                 * We are trying to move timer to new_base.
                 * However we can't change timer's base while it is running,
                 * so we keep it on the same CPU. No hassle vs. reprogramming
                 * the event source in the high resolution case. The softirq
                 * code will take care of this when the timer function has
                 * completed. There is no conflict as we hold the lock until
                 * the timer is enqueued.
                 */
                if (unlikely(hrtimer_callback_running(timer)))
                        return base;

                /* See the comment in lock_timer_base() */
                timer->base = NULL;
                raw_spin_unlock(&base->cpu_base->lock);
                raw_spin_lock(&new_base->cpu_base->lock);

                if (cpu != this_cpu && hrtimer_check_target(timer, new_base)) {
                        cpu = this_cpu;
                        raw_spin_unlock(&new_base->cpu_base->lock);
                        raw_spin_lock(&base->cpu_base->lock);
                        timer->base = base;
                        goto again;
                }
                timer->base = new_base;
        } else {
                if (cpu != this_cpu && hrtimer_check_target(timer, new_base)) {
                        cpu = this_cpu;
                        goto again;
                }
        }
        return new_base;
}

hrtimer를 위해 현재 cpu의 clock base를 사용하지 못하는 경우에 다른 cpu의 clock base로 변경한다.

  • 코드 라인 11에서 타이머를 이주시키기 위해 대체 cpu로 가장 가까운 cpu 도메인에서 바쁜 cpu를 얻어온다.
  • 코드 라인 15~16에서 새 cpu에 대한 cpu base와 clock base를 지정한다.
  • 코드 라인 18~29에서 클럭 base가 변경된 경우 낮은 확률로 요청한 hrtimer의 callback이 이미 실행중인 경우 원래 clock base를 반환한다.
  • 코드 라인 32~43에서 클럭 base를 변경한다. 만일 새 cpu가 지정되었으면서 현재 타이머의 만료 시간이 새 cpu의 clock base의 다음 타이머의 만료 시간 이전인 경우 그냥 현재 cpu로 재시도한다.
  • 코드 라인 44~49에서 새로운 clock base를 반환한다. 단 cpu가 변경된 경우이면서 현재 타이머의 만료 시간이 새 cpu의 clock base의 다음 타이머의 만료 시간 이전인 경우 그냥 현재 cpu로 재시도한다

 

get_nohz_timer_target()

kernel/sched/core.c

/*
 * In the semi idle case, use the nearest busy cpu for migrating timers
 * from an idle cpu.  This is good for power-savings.
 *
 * We don't do similar optimization for completely idle system, as
 * selecting an idle cpu will add more delays to the timers than intended
 * (as that cpu's timer base may not be uptodate wrt jiffies etc).
 */
int get_nohz_timer_target(int pinned)
{
        int cpu = smp_processor_id();
        int i;
        struct sched_domain *sd;

        if (pinned || !get_sysctl_timer_migration() || !idle_cpu(cpu))
                return cpu;

        rcu_read_lock();
        for_each_domain(cpu, sd) {
                for_each_cpu(i, sched_domain_span(sd)) {
                        if (!idle_cpu(i)) {
                                cpu = i;
                                goto unlock;
                        }
                }
        }
unlock:
        rcu_read_unlock();
        return cpu;
}

타이머를 이주시키기 위해 가장 가까운 cpu 도메인에서 바쁜 cpu를 얻어온다. (저전력을 위해 잠들어 있는 cpu를 깨우지 않게 하는 좋은 방법이다.)

  • 코드 라인 15~16에서 cpu를 고정(pinned)하여야 하거나 타이머 migration을 못하게 설정하였거나 idle cpu가 아닌 경우 현재 cpu를 반환한다.
    • /proc/sys/kernel/timer_migration 파일의 디폴트 값은 1이다.
  • 코드 라인 19~26에서 cpu 도메인 수 만큼 루프를 돌며 그 안에 있는 각 cpu에 대해 idle cpu가 아닌 경우 그 cpu id를 반환한다.

 

hrtimer_callback_running()

include/linux/hrtimer.h

/*
 * Helper function to check, whether the timer is running the callback
 * function             
 */
static inline int hrtimer_callback_running(struct hrtimer *timer)
{               
        return timer->state & HRTIMER_STATE_CALLBACK;
}

hrtimer의 콜백 함수가 실행되고 있는 상태인 경우 true를 반환한다.

 

hrtimer_check_target()

kernel/time/hrtimer.c

/*
 * With HIGHRES=y we do not migrate the timer when it is expiring
 * before the next event on the target cpu because we cannot reprogram
 * the target cpu hardware and we would cause it to fire late.
 *
 * Called with cpu_base->lock of target cpu held.
 */
static int
hrtimer_check_target(struct hrtimer *timer, struct hrtimer_clock_base *new_base)
{
#ifdef CONFIG_HIGH_RES_TIMERS
        ktime_t expires;

        if (!new_base->cpu_base->hres_active)
                return 0;

        expires = ktime_sub(hrtimer_get_expires(timer), new_base->offset);
        return expires.tv64 <= new_base->cpu_base->expires_next.tv64;
#else
        return 0;
#endif
}

요청 타이머의 만료 시간이 새 clock base의 다음 타이머의 만료 시간 이하인 경우 true를 반환한다.

  • 코드 라인 11~15에서 CONFIG_HIGH_RES_TIMERS를 사용하는 경우 hrtimer 모드가 활성화되지 않은 경우 false를 반환한다.
    • hrtimer가 처음 초기화 상태에서는 false이고 oneshot 모드로 전환되면 활성화된다.
    • hrtimer 에 대한 인터럽트가 발생하려면 hrtimer 모드로 전환되어 반드시 활성화되어 있어야 한다.
  • 코드 라인 17에서 기존 요청한 타이머의 만료 시간을 가져와서 새 clock base의 offset을 빼서 새 clock base의 만료 시간으로 컨버팅한다.
  • 코드 라인 18에서 컨버팅한 기존 타이머의 만료 시간이 새 clock base에서 다음 처리할 타이머의 만료 시간 이하이면 true를 반환한다.

 

다음 그림은 요청 hrtimer를 new_base(target clock base)로 이주 가능한지 체크하여 true가 반환되는 경우를 보여준다.

  • 이주할 clock base의 다음 차례 만료될 타이머보다 더 앞으로 끼워 넣지 못하여 true를 반환

 

timer_stats_hrtimer_set_start_info()

kernel/time/hrtimer.c

static inline void timer_stats_hrtimer_set_start_info(struct hrtimer *timer)
{
#ifdef CONFIG_TIMER_STATS
        if (timer->start_site)
                return;
        timer->start_site = __builtin_return_address(0);
        memcpy(timer->start_comm, current->comm, TASK_COMM_LEN);
        timer->start_pid = current->pid;
#endif
}

hrtimer에 디버그 정보를 기록한다.

 

enqueue_hrtimer()

kernel/time/hrtimer.c

/*
 * enqueue_hrtimer - internal function to (re)start a timer
 *
 * The timer is inserted in expiry order. Insertion into the
 * red black tree is O(log(n)). Must hold the base lock.
 *
 * Returns 1 when the new timer is the leftmost timer in the tree.
 */
static int enqueue_hrtimer(struct hrtimer *timer,
                           struct hrtimer_clock_base *base)
{
        debug_activate(timer);

        timerqueue_add(&base->active, &timer->node);
        base->cpu_base->active_bases |= 1 << base->index;

        /*
         * HRTIMER_STATE_ENQUEUED is or'ed to the current state to preserve the
         * state of a possibly running callback.
         */
        timer->state |= HRTIMER_STATE_ENQUEUED;

        return (&timer->node == base->active.next);
}

지정한 clock base의 active 타이머 큐(RB 트리)에 hrtimer를 노드로 추가한다. 추가한 타이머가 타이머큐(RB 트리)에서 가장 먼저 만료 시간이되는 경우 1을 반환한다.

  • 코드 라인 14에서 지정한 clock base의 active 타이머 큐(RB 트리)에 hrtimer를 노드로 추가한다.
  • 코드 라인 15에서 cpu_base->active_bases 마스크에 요청 타이머가 사용한 clock base에 해당하는 비트를 설정하여 사용됨을 표시한다.
    • 해당 cpu base에서 활성화된 hrtimer가 어느 clock base에 있는지 빠른 확인을 위해 사용한다.
  • 코드 라인 21에서 hrtimer에 enque 상태임을 추가한다.
  • 코드 라인 23에서 추가한 타이머가 타이머큐(RB 트리)에서 가장 먼저 만료 시간이되는 경우 1을 반환한다.

 

timerqueue_add()

lib/timerqueue.c

/**
 * timerqueue_add - Adds timer to timerqueue.
 *
 * @head: head of timerqueue
 * @node: timer node to be added
 *
 * Adds the timer node to the timerqueue, sorted by the
 * node's expires value.
 */
void timerqueue_add(struct timerqueue_head *head, struct timerqueue_node *node)
{
        struct rb_node **p = &head->head.rb_node;
        struct rb_node *parent = NULL;
        struct timerqueue_node  *ptr;

        /* Make sure we don't add nodes that are already added */
        WARN_ON_ONCE(!RB_EMPTY_NODE(&node->node));
        
        while (*p) { 
                parent = *p;
                ptr = rb_entry(parent, struct timerqueue_node, node);
                if (node->expires.tv64 < ptr->expires.tv64)
                        p = &(*p)->rb_left;
                else
                        p = &(*p)->rb_right;
        }
        rb_link_node(&node->node, parent, p);
        rb_insert_color(&node->node, &head->head);

        if (!head->next || node->expires.tv64 < head->next->expires.tv64)
                head->next = node;
}
EXPORT_SYMBOL_GPL(timerqueue_add);

타이머 큐(RB 트리)에 노드(hrtimer)를 추가한다.

 

nohz full

wake_up_nohz_cpu()

kernel/sched/core.c

void wake_up_nohz_cpu(int cpu)
{
        if (!wake_up_full_nohz_cpu(cpu))
                wake_up_idle_cpu(cpu);
}

nohz full cpu를 깨우거나 nohz idle 상태의 cpu를 깨운다.

 

wake_up_full_nohz_cpu()

kernel/sched/core.c

static bool wake_up_full_nohz_cpu(int cpu) 
{
        /*
         * We just need the target to call irq_exit() and re-evaluate
         * the next tick. The nohz full kick at least implies that.
         * If needed we can still optimize that later with an
         * empty IRQ.
         */
        if (tick_nohz_full_cpu(cpu)) {
                if (cpu != smp_processor_id() ||
                    tick_nohz_tick_stopped())
                        tick_nohz_full_kick_cpu(cpu);
                return true;
        }

        return false;
}

요청한 cpu가 nohz full로 동작하는 경우 현재 cpu가 아니거나 nohz tick이 멈춘 경우 해당 cpu를 nohz full 모드에서 제거하도록 요청 하고 true를 반환한다.

 

tick_nohz_tick_stopped()

include/linux/tick.h

static inline int tick_nohz_tick_stopped(void)
{
        return __this_cpu_read(tick_cpu_sched.tick_stopped);
}

스케쥴 tick이 멈춘 상태인지 여부를 알아온다.

 

tick_nohz_full_kick_cpu()

kernel/time/tick-sched.c

/*
 * Kick the CPU if it's full dynticks in order to force it to
 * re-evaluate its dependency on the tick and restart it if necessary.
 */
void tick_nohz_full_kick_cpu(int cpu)
{
        if (!tick_nohz_full_cpu(cpu))
                return;

        irq_work_queue_on(&per_cpu(nohz_full_kick_work, cpu), cpu);
}

요청한 cpu가 nohz full로 동작할 때 work queue를 사용하여 해당 cpu를 nohz full 모드에서 제거하게 한다.

 

tick_nohz_full_cpu()

include/linux/tick.h

static inline bool tick_nohz_full_cpu(int cpu)
{
        if (!tick_nohz_full_enabled())
                return false;

        return cpumask_test_cpu(cpu, tick_nohz_full_mask);
}

요청한 cpu가 nohz full로 동작하는지 여부를 반환한다.

 

hrtimer_reprogram()

kernel/time/hrtimer.c

/*
 * Shared reprogramming for clock_realtime and clock_monotonic
 *
 * When a timer is enqueued and expires earlier than the already enqueued
 * timers, we have to check, whether it expires earlier than the timer for
 * which the clock event device was armed.
 *
 * Note, that in case the state has HRTIMER_STATE_CALLBACK set, no reprogramming
 * and no expiry check happens. The timer gets enqueued into the rbtree. The
 * reprogramming and expiry check is done in the hrtimer_interrupt or in the
 * softirq.
 *
 * Called with interrupts disabled and base->cpu_base.lock held
 */
static int hrtimer_reprogram(struct hrtimer *timer,
                             struct hrtimer_clock_base *base) 
{
        struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
        ktime_t expires = ktime_sub(hrtimer_get_expires(timer), base->offset);
        int res;

        WARN_ON_ONCE(hrtimer_get_expires_tv64(timer) < 0);

        /*
         * When the callback is running, we do not reprogram the clock event
         * device. The timer callback is either running on a different CPU or
         * the callback is executed in the hrtimer_interrupt context. The
         * reprogramming is handled either by the softirq, which called the
         * callback or at the end of the hrtimer_interrupt.
         */
        if (hrtimer_callback_running(timer))
                return 0;

        /*
         * CLOCK_REALTIME timer might be requested with an absolute
         * expiry time which is less than base->offset. Nothing wrong
         * about that, just avoid to call into the tick code, which
         * has now objections against negative expiry values.
         */
        if (expires.tv64 < 0)
                return -ETIME;

        if (expires.tv64 >= cpu_base->expires_next.tv64)
                return 0;

        /*
         * When the target cpu of the timer is currently executing
         * hrtimer_interrupt(), then we do not touch the clock event
         * device. hrtimer_interrupt() will reevaluate all clock bases
         * before reprogramming the device.
         */
        if (cpu_base->in_hrtirq)
                return 0;

        /*
         * If a hang was detected in the last timer interrupt then we
         * do not schedule a timer which is earlier than the expiry
         * which we enforced in the hang detection. We want the system
         * to make progress.
         */
        if (cpu_base->hang_detected)
                return 0;

        /*
         * Clockevents returns -ETIME, when the event was in the past.
         */
        res = tick_program_event(expires, 0);
        if (!IS_ERR_VALUE(res))
                cpu_base->expires_next = expires;
        return res;
}

hrtimer가 요청한 clock base 큐에 있거나 현재 동작 중인 경우를 제외하고 다시 프로그램한다.

  • 코드 라인 31~32에서 이미 hrtimer의 콜백 함수가 실행되고 있는 상태인 경우 0을 반환한다.
  • 코드 라인 40~41에서 만료시간이 음수 값이면 -ETIME을 반환한다.
  • 코드 라인 43~44에서 hrtimer의 만료 시간이 다른 hrtimer에 비해 후 순위이면 0을 반환한다.
  • 코드 라인 52~53에서 현재 cpu base의 hrtimer 인터럽트 컨텍스트를 수행중인 경우 0을 반환한다.
  • 코드 라인 61~62에서 현재 cpu base에서 hang이 검출된 경우 0을 반환한다.
  • 코드 라인 67~69에서 tick을 프로그램한다.

 

만료 시간을 forward

hrtimer_forward()

kernel/time/hrtimer.c

/**
 * hrtimer_forward - forward the timer expiry
 * @timer:      hrtimer to forward
 * @now:        forward past this time
 * @interval:   the interval to forward
 *
 * Forward the timer expiry so it will expire in the future.
 * Returns the number of overruns.
 */
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval)
{       
        u64 orun = 1;
        ktime_t delta;

        delta = ktime_sub(now, hrtimer_get_expires(timer));

        if (delta.tv64 < 0)
                return 0;

        if (interval.tv64 < timer->base->resolution.tv64)
                interval.tv64 = timer->base->resolution.tv64; 

        if (unlikely(delta.tv64 >= interval.tv64)) {
                s64 incr = ktime_to_ns(interval);

                orun = ktime_divns(delta, incr);
                hrtimer_add_expires_ns(timer, incr * orun);
                if (hrtimer_get_expires_tv64(timer) > now.tv64)
                        return orun;
                /*
                 * This (and the ktime_add() below) is the
                 * correction for exact:
                 */
                orun++;
        }
        hrtimer_add_expires(timer, interval);
        
        return orun;
}
EXPORT_SYMBOL_GPL(hrtimer_forward);

만료된 타이머에 한해 만료 시각으로부터 interval 간격의 now를 지난 시각을 만료 시각으로 설정한다. (주의: now + interval이 아님)

  • 코드 라인 15~16에서 현재 시각으로부터 기존 만료 시각을 빼서 delta에 대입한다.
    • 양수인 경우 기존 타이머는 만료된 상태임을 알 수 있다.
    • 음수인 경우 기존 타이머가 만료되지 않았음을 알 수 있다.
  • 코드 라인 18~19에서 타이머의 해상도보다 인터벌이 작은 경우 인터벌 값을 타이머의 해상도로 바꾼다.
  • 코드 라인 21~27에서 낮은 확률로 기존 만료된 타이머가 인터벌보다 길게 오래된 경우  orun에 인터벌이 들어갈 횟수를 대입하고 hrtimer의 만료시각을 orun x 인터벌 기간만큼 추가한다. 추가한 시각이 현재 시각을 넘어간 경우 orun을 반환한다.
  • 코드 라인 32~35에서 orun 값을 1 증가시키고 hrtimer를 재설정한 후 orun 값을 반환한다.

 

다음 그림은 hrtimer_forward() 함수로 만료 시각이 변경되지 않는 사례와 변경되는 사례를 보여준다.

 

구조체

hrtimer_cpu_base 구조체

include/linux/hrtimer.h

/*
 * struct hrtimer_cpu_base - the per cpu clock bases
 * @lock:               lock protecting the base and associated clock bases
 *                      and timers
 * @cpu:                cpu number
 * @active_bases:       Bitfield to mark bases with active timers
 * @clock_was_set:      Indicates that clock was set from irq context.
 * @expires_next:       absolute time of the next event which was scheduled
 *                      via clock_set_next_event()
 * @in_hrtirq:          hrtimer_interrupt() is currently executing
 * @hres_active:        State of high resolution mode
 * @hang_detected:      The last hrtimer interrupt detected a hang
 * @nr_events:          Total number of hrtimer interrupt events
 * @nr_retries:         Total number of hrtimer interrupt retries
 * @nr_hangs:           Total number of hrtimer interrupt hangs
 * @max_hang_time:      Maximum time spent in hrtimer_interrupt
 * @clock_base:         array of clock bases for this cpu
 */
struct hrtimer_cpu_base {
        raw_spinlock_t                  lock;
        unsigned int                    cpu;
        unsigned int                    active_bases;
        unsigned int                    clock_was_set;
#ifdef CONFIG_HIGH_RES_TIMERS
        ktime_t                         expires_next;
        int                             in_hrtirq;
        int                             hres_active;
        int                             hang_detected;
        unsigned long                   nr_events;
        unsigned long                   nr_retries;
        unsigned long                   nr_hangs;
        ktime_t                         max_hang_time;
#endif 
        struct hrtimer_clock_base       clock_base[HRTIMER_MAX_CLOCK_BASES];
};
  • lock
    • base와 연결된 clock base와 타이머들을 보호하기 위해 lock을 사용한다.
  •  cpu
    • cpu id가 지정된다.
  • active_bases
    • 활성화된 타이머가 있는 clock base에 해당하는 비트 필드를 운영한다.
    • 예) 0x0f -> 4개 clock base 모두에 활성화된 hrtimer가 있다.
  • clock_was_set
    • irq context에서 설정되었는지 여부
  • expires_next
    • 4개의 클럭 RB 트리에 등록된 hrtimer 들 중 monotonic 기준 클럭으로 가장 먼저 만료되는 타이머의 만료 시각
  • in_hrtirq
    • hrtimer 인터럽트 context가 수행중인 경우 true (hrtimer_interrupt()가 실행중)
  • hres_active
    • high resolution 모드로 동작중인 상태
  • hang_detected
    • hang이 검출된 경우
  • nr_events
    • hrtimer 인터럽트 이벤트 수
  • nr_hangs
    • hang된 수
  • max_hang_time
    • hrtimer 인터럽트 context가 수행된 시간 중 최대 시간
  • clock_base[]
    • 4개의 clock base

 

hrtimer_clock_base 구조체

include/linux/hrtimer.h

/**
 * struct hrtimer_clock_base - the timer base for a specific clock
 * @cpu_base:           per cpu clock base
 * @index:              clock type index for per_cpu support when moving a
 *                      timer to a base on another cpu.
 * @clockid:            clock id for per_cpu support
 * @active:             red black tree root node for the active timers
 * @resolution:         the resolution of the clock, in nanoseconds
 * @get_time:           function to retrieve the current time of the clock
 * @softirq_time:       the time when running the hrtimer queue in the softirq
 * @offset:             offset of this clock to the monotonic base
 */
struct hrtimer_clock_base {
        struct hrtimer_cpu_base *cpu_base;
        int                     index;
        clockid_t               clockid;
        struct timerqueue_head  active;
        ktime_t                 resolution;
        ktime_t                 (*get_time)(void);
        ktime_t                 softirq_time;
        ktime_t                 offset;
};
  • *cpu_base
    • cpu_base를 가리키는 포인터
  • index
    • clock base index (0~3)
  • clockid
    • clock id (0~11)
  • active
    • 활성화된 타이머들이 추가되는 타이머 큐(RB 트리)
  • resolution
    • 현재 clock의 해상도
  • (*get_time)
    • 현재 clock의 시간을 조회하는 함수
  • softirq_time
    • 타이머 큐에서 각 클럭에 대해 만료 시간을 비교하기 위해 내부에서 사용하는 시간
  • offset
    • 현재 clock과 monotonic 클럭과의 offset 시간(ns)
    • 현재 clock이 monotonic인 경우 이 값은 0

 

hrtimer 구조체

include/linux/hrtimer.h

/**
 * struct hrtimer - the basic hrtimer structure
 * @node:       timerqueue node, which also manages node.expires,
 *              the absolute expiry time in the hrtimers internal
 *              representation. The time is related to the clock on
 *              which the timer is based. Is setup by adding
 *              slack to the _softexpires value. For non range timers
 *              identical to _softexpires.
 * @_softexpires: the absolute earliest expiry time of the hrtimer.
 *              The time which was given as expiry time when the timer
 *              was armed.
 * @function:   timer expiry callback function
 * @base:       pointer to the timer base (per cpu and per clock)
 * @state:      state information (See bit values above)
 * @start_pid: timer statistics field to store the pid of the task which
 *              started the timer
 * @start_site: timer statistics field to store the site where the timer
 *              was started
 * @start_comm: timer statistics field to store the name of the process which
 *              started the timer
 *
 * The hrtimer structure must be initialized by hrtimer_init()
 */
struct hrtimer {
        struct timerqueue_node          node;
        ktime_t                         _softexpires;
        enum hrtimer_restart            (*function)(struct hrtimer *);
        struct hrtimer_clock_base       *base;
        unsigned long                   state;
#ifdef CONFIG_TIMER_STATS
        int                             start_pid;
        void                            *start_site;
        char                            start_comm[16];
#endif
};
  • node
    • RB 트리에 연결될 노드로 slack이 적용된 실제 만료시간을 가지고 있다.
  • _softexpires
    • slack이 적용되지 않은 만료 시각(soft 만료 시각)
  • (*function)
    • 타이머 만료 시 호출될 함수
  • *base
    • clock base
  • state
    • 타이머 상태
  • start_pid
    • 태스크의 pid 값 (디버그)
  • start_site
    • 타이머가 시작된 site (디버그)
  • start_comm
    • 타이머가 시작된 프로세스명 (디버그)

 

타이머 정보

다음과 같이 모든 cpu에서 동작하는 hrtimer의 동작상태를 확인할 수 있다.

$ cat /proc/timer_list
Timer List Version: v0.7
HRTIMER_MAX_CLOCK_BASES: 4
now at 6680679880466542 nsecs

cpu: 0
 clock 0:
  .base:       b9b483d0
  .index:      0
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 #0: <b9b48628>, tick_sched_timer, S:01, hrtimer_start, swapper/0/0
 # expires at 6680679900000000-6680679900000000 nsecs [in 19533458 to 19533458 nsecs]
 #1: <b6e19a48>, hrtimer_wakeup, S:01, hrtimer_start_range_ns, ifplugd/1632
 # expires at 6680680247387930-6680680248387924 nsecs [in 366921388 to 367921382 nsecs]
(...생략...)

 

참고

 

답글 남기기

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