softirq_init()

 

softirq_init()

kernel/softirq.c

void __init softirq_init(void)
{
        int cpu;

        for_each_possible_cpu(cpu) {
                per_cpu(tasklet_vec, cpu).tail =
                        &per_cpu(tasklet_vec, cpu).head;
                per_cpu(tasklet_hi_vec, cpu).tail =
                        &per_cpu(tasklet_hi_vec, cpu).head;
        }

        open_softirq(TASKLET_SOFTIRQ, tasklet_action);
        open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

softirq를 사용하기 전에 초기화를 수행한다.

  • 코드 라인 5~10에서 possible cpu 수 만큼 루프를 돌며 tasklet_vec 및 tasklet_hi_vec 리스트를 초기화한다.
  • 코드 라인 12에서 TASKLET_SOFTIRQ용 핸들러 함수를 지정한다.
  • 코드 라인 13에서 HI_SOFTIRQ용 핸들러 함수를 지정한다.

 

참고

Interrupts -5- (Softirq)

 

 

Softirq

리눅스 커널의 interrupt bottom-half 처리기 중 가장 큰 부분으로 동작한다. 기존의 많은 드라이버들이 interrupt bottom-half 처리기로 tasklet을 많이 사용해었는데 tasklet 인터페이스가 그대로 softirq의 한 부분으로 동작하면서 기존 tasklet을 사용하던 드라이버들을 흡수하였다. 향후 rt 커널에서 도입을 먼저하였던 threaded interrupt로 자리를 물려주기 전까지는 당분간 대세를 지속하고 있을 예정이다.

 

 

 

다음 그림은 ksoftirqd가 각 cpu 마다 호출되는 과정을 보여준다.

 

ksoftirqd 스레드 생성

spawn_ksoftirqd()

kernel/softirq.c

static __init int spawn_ksoftirqd(void)
{
        register_cpu_notifier(&cpu_nfb);

        BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

        return 0;
}
early_initcall(spawn_ksoftirqd);

ksoftirqd를 생성한다.

  • 코드 라인 3에서 cpu notify 체인에 아래 cpu_nfb를 등록하여 cpu 상태가 변할 떄 마다 cpu_callback() 함수가 호출되도록 한다.

 

kernel/softirq.c

static struct notifier_block cpu_nfb = {
        .notifier_call = cpu_callback
};

 

  • 코드 라인 5에서 아래 softirq_threads 정보를 기준으로 per-cpu 스레드로 등록하고 모든 online cpu에서 동작하게 한다.

 

kernel/softirq.c

static struct smp_hotplug_thread softirq_threads = {
        .store                  = &ksoftirqd,
        .thread_should_run      = ksoftirqd_should_run,
        .thread_fn              = run_ksoftirqd,
        .thread_comm            = "ksoftirqd/%u",
};

 

cpu off에 따른 tasklet 이주

cpu_callback()

kernel/softirq.c

static int cpu_callback(struct notifier_block *nfb, unsigned long action,
                        void *hcpu)
{               
        switch (action) {
#ifdef CONFIG_HOTPLUG_CPU
        case CPU_DEAD:
        case CPU_DEAD_FROZEN:
                takeover_tasklets((unsigned long)hcpu);
                break;
#endif /* CONFIG_HOTPLUG_CPU */
        }
        return NOTIFY_OK;
}

특정 cpu 상태가 dead 상태로 변화하면 해당 cpu의 tasklets 들을 현재 cpu로 전환시킨다.

 

takeover_tasklets()

kernel/softirq.c

static void takeover_tasklets(unsigned int cpu)
{
        /* CPU is dead, so no lock needed. */
        local_irq_disable();

        /* Find end, append list for that CPU. */
        if (&per_cpu(tasklet_vec, cpu).head != per_cpu(tasklet_vec, cpu).tail) {
                *__this_cpu_read(tasklet_vec.tail) = per_cpu(tasklet_vec, cpu).head;
                this_cpu_write(tasklet_vec.tail, per_cpu(tasklet_vec, cpu).tail);
                per_cpu(tasklet_vec, cpu).head = NULL;
                per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
        }
        raise_softirq_irqoff(TASKLET_SOFTIRQ);
        
        if (&per_cpu(tasklet_hi_vec, cpu).head != per_cpu(tasklet_hi_vec, cpu).tail) {
                *__this_cpu_read(tasklet_hi_vec.tail) = per_cpu(tasklet_hi_vec, cpu).head;
                __this_cpu_write(tasklet_hi_vec.tail, per_cpu(tasklet_hi_vec, cpu).tail);
                per_cpu(tasklet_hi_vec, cpu).head = NULL;
                per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
        }
        raise_softirq_irqoff(HI_SOFTIRQ);
                
        local_irq_enable();
}

특정 cpu 상태가 dead 상태로 변화하면 해당 cpu의 tasklets 들을 현재 cpu로 전환시킨다.

  • 코드 라인 7~13에서 요청 cpu의 tasklet_vec 리스트의 엔트리들을 현재 cpu로 옮기고 tasklet softirq를 요청한다.
  • 코드 라인 15~21에서 요청 cpu의 tasklet_hi_vec 리스트의 엔트리들을 현재 cpu로 옮기고 hi softirq를 요청한다.

 

SMP 핫플러그 스레드 등록

모든 online cpu 마다 동작할 커널 스레드들을 등록한다. 다음 커널 스레드들이 이러한 smp 핫플러그 등록을 사용한다.

  • ksoftirqd
  • migration
  • watchdog
  • rcuc

 

smpboot_register_percpu_thread()

kernel/smpboot.c

/**
 * smpboot_register_percpu_thread - Register a per_cpu thread related to hotplug
 * @plug_thread:        Hotplug thread descriptor
 *
 * Creates and starts the threads on all online cpus.
 */
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread)
{
        unsigned int cpu;               
        int ret = 0;

        get_online_cpus();
        mutex_lock(&smpboot_threads_lock);
        for_each_online_cpu(cpu) {
                ret = __smpboot_create_thread(plug_thread, cpu);
                if (ret) {
                        smpboot_destroy_threads(plug_thread);
                        goto out;       
                }
                smpboot_unpark_thread(plug_thread, cpu);
        }
        list_add(&plug_thread->list, &hotplug_threads);
out:
        mutex_unlock(&smpboot_threads_lock);
        put_online_cpus();
        return ret;
}
EXPORT_SYMBOL_GPL(smpboot_register_percpu_thread);

인수로 받은 plug_thread 정보를 기준으로 per-cpu 스레드로 등록하고 모든 online cpu에서 동작하게 한다.

  • 코드 라인 14~19에서 online cpu 수 만큼 루프를 돌며 smpboot_thread_fn() 함수를 fork하고 ht->thread_comm 이름으로 등록한다.
  • 코드 라인 20에서 스레드를 unpark 한다.
  • 코드 라인 22에서 인수로 받은 스레드를 전역 hotplug_threads 리스트에 등록한다.

 

__smpboot_create_thread()

kernel/smpboot.c

static int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
        struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
        struct smpboot_thread_data *td;

        if (tsk)
                return 0;

        td = kzalloc_node(sizeof(*td), GFP_KERNEL, cpu_to_node(cpu));
        if (!td)
                return -ENOMEM;
        td->cpu = cpu;
        td->ht = ht;

        tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
                                    ht->thread_comm);
        if (IS_ERR(tsk)) {
                kfree(td);
                return PTR_ERR(tsk);
        }
        get_task_struct(tsk);
        *per_cpu_ptr(ht->store, cpu) = tsk;
        if (ht->create) {
                /*
                 * Make sure that the task has actually scheduled out
                 * into park position, before calling the create
                 * callback. At least the migration thread callback
                 * requires that the task is off the runqueue.
                 */
                if (!wait_task_inactive(tsk, TASK_PARKED))
                        WARN_ON(1);
                else
                        ht->create(cpu);
        }
        return 0;
}

online cpu 수 만큼 루프를 돌며 smpboot_thread_fn() 함수를 fork 하여 동작하게 하고 생성된 태스크명은 ht->thread_comm으로 한다. 성공 시엔 0을 반환한다.

  • 코드 라인 4~8에서 요청한 cpu에 해당하는 ht->store 값을 tsk로 가져온다. 이 값이 이미 설정된 경우 함수를 빠져나간다.
  • 코드 라인 10~14에서 smpboot_thread_data 구조체르 할당받고 인수로 받은 cpu와 ht 정보를 대입한다.
  • 코드 라인 16~21에서 smpboot_thread_fn() 함수를 fork 하여 동작하게 하고 생성된 태스크명으로 ht->thread_comm을 사용한다.
  • 코드 라인 22~23에서 생성된 태스크를 ht->store에 대입한다.
  • 코드 라인 24~35에서 ht->create에 함수가 등록된 경우 함수를 호출한다.
    • migration 커널 스레드에서 cpu_stop_create() 함수가 등록되어 사용된다.

 

smpboot_thread_fn()

kernel/smpboot.c

/**
 * smpboot_thread_fn - percpu hotplug thread loop function
 * @data:       thread data pointer
 *
 * Checks for thread stop and park conditions. Calls the necessary
 * setup, cleanup, park and unpark functions for the registered
 * thread.
 *
 * Returns 1 when the thread should exit, 0 otherwise.
 */
static int smpboot_thread_fn(void *data)
{
        struct smpboot_thread_data *td = data;
        struct smp_hotplug_thread *ht = td->ht;

        while (1) {
                set_current_state(TASK_INTERRUPTIBLE);
                preempt_disable();
                if (kthread_should_stop()) {
                        __set_current_state(TASK_RUNNING);
                        preempt_enable();
                        if (ht->cleanup)
                                ht->cleanup(td->cpu, cpu_online(td->cpu));
                        kfree(td);
                        return 0;
                }

                if (kthread_should_park()) {
                        __set_current_state(TASK_RUNNING);
                        preempt_enable();
                        if (ht->park && td->status == HP_THREAD_ACTIVE) {
                                BUG_ON(td->cpu != smp_processor_id());
                                ht->park(td->cpu);
                                td->status = HP_THREAD_PARKED;
                        }
                        kthread_parkme();
                        /* We might have been woken for stop */
                        continue;
                }

                BUG_ON(td->cpu != smp_processor_id());

                /* Check for state change setup */
                switch (td->status) {
                case HP_THREAD_NONE:
                        __set_current_state(TASK_RUNNING);
                        preempt_enable();
                        if (ht->setup)
                                ht->setup(td->cpu);
                        td->status = HP_THREAD_ACTIVE;
                        continue;

                case HP_THREAD_PARKED:
                        __set_current_state(TASK_RUNNING);
                        preempt_enable();
                        if (ht->unpark)
                                ht->unpark(td->cpu);
                        td->status = HP_THREAD_ACTIVE;
                        continue;
                }

                if (!ht->thread_should_run(td->cpu)) {
                        preempt_enable_no_resched();
                        schedule();
                } else {
                        __set_current_state(TASK_RUNNING);
                        preempt_enable();
                        ht->thread_fn(td->cpu);
                }
        }
}

per-cpu 핫플러그 스레드 루프 함수로 무한 루프를 돌며 요청 시 마다 등록된 함수를 호출한다.

  • 코드 라인 17~18에서 현재 태스크를 인터럽트 허용상태로 두고 커널 선점은 막는다.
  • 코드 라인 19~26에서 현재 태스크에 KTHREAD_SHOULD_STOP 플래그가 설정된 경우 스레드의 종료 처리를 한다.
  • 코드 라인 28~39에서 현재 태스크에 KTHREAD_SHOULD_PARK 플래그가 설정된 경우 스레드를 park 상태로 변경시켜 sleep한다. 깨어나는 경우 계속 루프를 돈다.
  • 코드 라인 44~51에서 smp 핫플러그 스레드 상태가 HP_THREAD_NONE인 경우 태스크를 running 상태로 변경하고 커널 선점을 오픈하며 ht->setup 함수를 동작시키고 smp 핫플러그 상태를 active로 변경한다.
  • 코드 라인 53~60에서 smp 핫플러그 스레드 상태가 HP_THREAD_PARKED 상태로 요청한 경우 태스크를 running 상태로 변경하고 커널 선점을 오픈하며 ht->unpark 함수를 동작시키고 smp 핫플러그 상태를 active로 변경한다.
  • 코드 라인 62~64에서 ht->should_run() 함수가 false인 경우 리스케쥴한다. (sleep)
    • ksoftirqd 커널 스레드에서는 ksoftirqd_should_run() 함수를 호출하여 처리할 softirq의 여부를 확인한다.
  • 코드 라인 65~69에서 태스크를 TASK_RUNNING 상태로 바꾸고 선점 enable 한 후 해당 스레드의 처리 함수를 호출한다.
    • ksoftirqd 커널 스레드에서는 run_ksoftirqd() 함수를 호출한다.

 

kernel/smpboot.c

enum {
        HP_THREAD_NONE = 0,
        HP_THREAD_ACTIVE,
        HP_THREAD_PARKED,
};

 

다음 그림은 SMP 핫플러그 스레드 상태별 처리 흐름을 보여준다.

ksoftirqd_should_run()

kernel/softirq.c

static int ksoftirqd_should_run(unsigned int cpu)
{
        return local_softirq_pending();
}

처리할 softirq가 있는지 여부를 알아온다.

 

pending softirq 처리

 

tasklet을 감싼 softirq는 매우 빠르게 처리해야 할 인터럽트에 대해서만 제한적으로 메인 라인 커미터에 의해 하드 코딩되어 유지 관리되고 있다. softirq는 최고 우선 순위의 ksoftirqd 스레드가 각각의 cpu에서 동작된다. 그러나 특정 커널 옵션 및 커널 파라메터를 사용한 경우 특정 조건을 만족하는 경우에 한해 irq context가 끝나자 마자 인터럽트를 복귀하지 않고 곧장 softirq 서비스 루틴을 호출할 수 있다.

  • irq context에서 직접 호출 조건
    •  CONFIG_IRQ_FORCED_THREADING 커널 옵션 사용
    • “thredirqs” 커널 파라메터 사용
  • 그 외 사용 조건
    • 호출 시 최대 2ms를 벗어나는 경우 ksoftirqd에서 나머지 pending softirq를 처리

다음 그림은 softirq 호출 시 각 softirq의 처리 루틴으로 분기되는 과정을 보여준다. 특정 softirq를 호출하기 위해서는 raise_softirq()를 사용한다.

 

run_ksoftirqd()

kernel/softirq.c

static void run_ksoftirqd(unsigned int cpu)
{
        local_irq_disable();
        if (local_softirq_pending()) {
                /*
                 * We can safely run softirq on inline stack, as we are not deep
                 * in the task stack here.
                 */
                __do_softirq();
                local_irq_enable();
                cond_resched_rcu_qs();
                return;
        }
        local_irq_enable();
}

local 인터럽트를 막아둔채로 처리할 softirq가 있으면 해당 softirq 핸들러 함수를 호출한다.

 

local_softirq_pending()

include/linux/irq_cpustat.h

  /* arch independent irq_stat fields */
#define local_softirq_pending() \
        __IRQ_STAT(smp_processor_id(), __softirq_pending)

현재 cpu에 처리할 softirq가 있는지 유무를 알아온다.

 

include/linux/irq_cpustat.h

#ifndef __ARCH_IRQ_STAT
extern irq_cpustat_t irq_stat[];                /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
#endif

 

kernel/softirq.c

#ifndef __ARCH_IRQ_STAT
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
EXPORT_SYMBOL(irq_stat);
#endif

 

do_softirq()

kernel/softirq.c

asmlinkage __visible void do_softirq(void)
{
        __u32 pending;
        unsigned long flags;

        if (in_interrupt())
                return;

        local_irq_save(flags);

        pending = local_softirq_pending();

        if (pending)
                do_softirq_own_stack();

        local_irq_restore(flags);
}


local 인터럽트를 막아둔채로 처리할 softirq가 있으면 해당 softirq 핸들러 함수를 호출한다.

 

do_softirq_own_stack()

include/linux/interrupt.h

static inline void do_softirq_own_stack(void)
{
        __do_softirq();
}

처리할 softirq가 있으면 해당 softirq 핸들러 함수를 호출한다.

 

__do_softirq()

kernel/softirq.c

asmlinkage __visible void __do_softirq(void)
{
        unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
        unsigned long old_flags = current->flags;
        int max_restart = MAX_SOFTIRQ_RESTART;
        struct softirq_action *h;
        bool in_hardirq;
        __u32 pending;
        int softirq_bit;

        /*
         * Mask out PF_MEMALLOC s current task context is borrowed for the
         * softirq. A softirq handled such as network RX might set PF_MEMALLOC
         * again if the socket is related to swap
         */
        current->flags &= ~PF_MEMALLOC;

        pending = local_softirq_pending();
        account_irq_enter_time(current);
                
        __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
        in_hardirq = lockdep_softirq_start();

restart:
        /* Reset the pending bitmask before enabling irqs */
        set_softirq_pending(0);

        local_irq_enable();

        h = softirq_vec;

        while ((softirq_bit = ffs(pending))) {
                unsigned int vec_nr;
                int prev_count;

                h += softirq_bit - 1;

                vec_nr = h - softirq_vec;
                prev_count = preempt_count();

                kstat_incr_softirqs_this_cpu(vec_nr);

                trace_softirq_entry(vec_nr);
                h->action(h);
                trace_softirq_exit(vec_nr);
                if (unlikely(prev_count != preempt_count())) {
                        pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                               vec_nr, softirq_to_name[vec_nr], h->action,
                               prev_count, preempt_count());
                        preempt_count_set(prev_count);
                }
                h++;
                pending >>= softirq_bit;
        }

        rcu_bh_qs();
        local_irq_disable();

        pending = local_softirq_pending();
        if (pending) {
                if (time_before(jiffies, end) && !need_resched() &&
                    --max_restart)
                        goto restart;

                wakeup_softirqd();
        }

        lockdep_softirq_end(in_hardirq);
        account_irq_exit_time(current);
        __local_bh_enable(SOFTIRQ_OFFSET);
        WARN_ON_ONCE(in_interrupt());
        tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

처리할 softirq가 있으면 해당 softirq 핸들러 함수를 호출한다.

  • 코드 라인 3에서 최대 softirq 처리 시간으로 현재 시간으로 부터 2ms 후에 해당하는 jiffies 값을 산출한다.
  • 코드 라인 4에서 현재 태스크의 플래그를 백업해둔다.
  • 코드 라인 5에서 최대 재시도 수를 10번으로 제한한다.
  • 코드 라인 16에서 현재 태스크 플래그에서 PF_MEMALLOC을 제거하여 softirq 처리 핸들러 루틴에서 응급 메모리를 사용하지 못하도록 한다. 단 네트워크 swap을 이용하여야 하는 NET_RX_SOFTIRQ는 예외적으로 해당 핸들러에서 PF_MEMALLOC을 설정한다.
  • 코드 라인 18에서 처리할 softirq를 알아온다.
  • 코드 라인 19에서 CONFIG_IRQ_TIME_ACCOUNTING 커널 옵션을 사용한 경우 트레이스 목적으로 irq 진입 시간을 기록한다.
  • 코드 라인 21에서 커널 선점되지 않도록 preempt_count를 SOFTIRQ_OFFSET 만큼 더한다.
  • 코드 라인 26~28에서 irq를 enable하기 전에 __softirq_pending 플래그를 클리어한 후 irq를 enable 한다.
  • 코드 라인 32~38에서 처리할 softirq 중 가장 우선 순위가 높은 softirq의 벡터번호를 알아온다.
    • vec_nr=0    <- softirq_vec[HI_SOFTIRQ]
  • 코드 라인 41에서 처리할 softirq 통계 카운터를 1 증가 시킨다.
  • 코드 라인 44에서 softirq 핸들러 함수를 호출한다.
  • 코드 라인 46~51에서 softirq 핸들러 함수를 수행하기 전에 읽은 preempt_count 값에 변화가 발생하면 에러 메시지를 출력하고 preempt_count 값을 처음 읽었던 값으로 되돌린다.
  • 코드 라인 52~54에서 다음 순위의 softirq를 처리할 준비를 한다.
  • 코드 라인 56~57에서 rcu의 bottom half 처리를 한 후 irq를 disable 한다.
  • 코드 라인 59~63에서 여전히 pending된 softirq가 있는지 확인하여 여전히 존재하면 다음 조건을 만족하면 다시 restart: 레이블로 이동하여 softirq 처리를 수행하게 한다.
    • 최대 반복 횟수: 10회 이내
    • 최대 처리 시간: 2ms 이내
    • 리스케쥴 요청이 없어야 한다.
  • 코드 라인 64에서 ksoftirqd에서 호출되지 않고 irq context에서 직접 호출한 경우ksoftirqd가 나머지 pending 된 softirq를 처리하도록 깨운다. 다음과 같은 특정 커널 조건에서만 유효하다.
    • CONFIG_IRQ_FORCED_THREADING 커널 옵션 사용
    • “thredirqs” 커널 파라메터 사용
  • 코드 라인 69에서 CONFIG_IRQ_TIME_ACCOUNTING 커널 옵션을 사용한 경우 트레이스 목적으로 irq 퇴출 시간을 기록한다.
  • 코드 라인 70에서 softirq에서 막은 커널 선점을 다시 가능하도록 preempt_count를 SOFTIRQ_OFFSET 만큼 감소시킨다.
  • 코드 라인 71에서 아직도 interrupt 처리중인 경우 경고 메시지를 출력한다.
  • 코드 라인 72에서 현재 태스크 플래그를 원래대로 다시 복구한다.

 

특정 softirq 호출

raise_softirq()

kernel/softirq.c

void raise_softirq(unsigned int nr)
{
        unsigned long flags;

        local_irq_save(flags);
        raise_softirq_irqoff(nr);
        local_irq_restore(flags);
}

요청한 번호의 softirq 서비스를 호출한다.

 

raise_softirq_irqoff()

kernel/softirq.c

/*
 * This function must run with irqs disabled!
 */
inline void raise_softirq_irqoff(unsigned int nr)
{
        __raise_softirq_irqoff(nr);

        /*
         * If we're in an interrupt or softirq, we're done
         * (this also catches softirq-disabled code). We will
         * actually run the softirq once we return from
         * the irq or softirq.
         *
         * Otherwise we wake up ksoftirqd to make sure we
         * schedule the softirq soon.
         */
        if (!in_interrupt())
                wakeup_softirqd();
}

요청한 번호의 softirq 서비스를 호출한다.

  • 코드 라인 6에서 요청한 softirq 번호에 해당하는 펜딩 비트플래그를 설정한다.
  • 코드 라인 17에서 인터럽트 서비중이 아닌 경우 만일 softirqd 스레드가 잠들어있는 경우 깨운다.

 

__raise_softirq_irqoff()

kernel/softirq.c

void __raise_softirq_irqoff(unsigned int nr)
{
        trace_softirq_raise(nr);
        or_softirq_pending(1UL << nr);
}

요청한 softirq 번호에 해당하는 펜딩 비트플래그를 설정한다.

 

#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

 

wakeup_softirqd()

kernel/softirq.c

/*
 * we cannot loop indefinitely here to avoid userspace starvation,
 * but we also don't want to introduce a worst case 1/HZ latency
 * to the pending events, so lets the scheduler to balance
 * the softirq load for us.
 */
static void wakeup_softirqd(void)
{
        /* Interrupts are disabled: no need to stop preemption */
        struct task_struct *tsk = __this_cpu_read(ksoftirqd);

        if (tsk && tsk->state != TASK_RUNNING)
                wake_up_process(tsk);
}

softirqd 스레드가 잠들어있는 경우 깨운다.

 

특정 softirq 핸들러 준비

open_softirq()

kernel/softirq.c

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
        softirq_vec[nr].action = action;
}

요청한 softirq 벡터 번호에 action 핸들러 함수를 대입한다.

 

Timer Softirq

run_timer_softirq()

kernel/time/timer.c

/*
 * This function runs timers and the timer-tq in bottom half context.
 */
static void run_timer_softirq(struct softirq_action *h)
{
        struct tvec_base *base = __this_cpu_read(tvec_bases);

        hrtimer_run_pending();

        if (time_after_eq(jiffies, base->timer_jiffies))
                __run_timers(base);
}

타이머 softirq 처리 루틴이다.

  • 코드 라인 8에서 hrtimer가 가동되었는지 확인하고 틱 oneshot 모드 전환 여부를 확인한 후에 oneshot 모드로 전환한다.
  • 코드 라인 10~11에서 lowres 타이머 휠에서 만료된 타이머가 있는 경우 등록된 콜백 함수를 호출한다.

 

참고

Timer -10- (Posix Clock & Timers)

 

Posix Clock & Timers

유저가 사용할 수 있는 클럭, 타이머, 알람 등을 POSIX syscall 인터페이스를 통해 제공되며 다음의 컴포넌트등이 있다.

  • posix clock & timers
  • alarmtimers
  • itimer

 

다음 그림은 posix clocks & timers와 관련된 컴포넌트들과의 관계도이다.

 

Posix Clock & Timers

4개의 기본 클럭과 coarse, raw 등의 3 개 클럭이 추가되었고 process, thread 클럭이 또한 추가 구현되었다.

  • init_posix_timers() – initcall 호출
  • timer_create()
  • timer_gettime()
  • timer_getoverrun()
  • timer_settime()
  • timer_delete()
  • clock_settime()
  • clock_gettime()
  • clock_adjtime()
  • clock_getres()
  • common_nsleep()
  • clock_nanosleep()

 

Alarmtimers

알람 타이머 드라이버로  posix 클럭 ids에 연동하여 사용되며 RTC(Real Time Clock) 하드웨어와 인터페이스되며 내부에서는 hrtimer를 사용한다.

  • alarmtimer_init() – initcall 호출
  • alarm_init()
  • alarmtimer_enqueue()
  • alarmtimer_dequeue()
  • alarmtimer_fired()
  • alarmtimer_suspend()
  • alarm_start()
  • alarm_handle_timer()

 

itimer

hrtimer를 사용하여 아래와 같은 syscall API들을 제공한다.

  • settimer()
  • gettimer()
  • clock_adjtime()
  • clock_getres()
  • clock_nanosleep()

 

참고

 

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

 

참고

 

timekeeping_init()

 

timekeeping_init()

kernel/time/timekeeping.c

/*
 * timekeeping_init - Initializes the clocksource and common timekeeping values
 */
void __init timekeeping_init(void)
{
        struct timekeeper *tk = &tk_core.timekeeper;
        struct clocksource *clock;
        unsigned long flags;
        struct timespec64 now, boot, tmp;
        struct timespec ts;

        read_persistent_clock(&ts);
        now = timespec_to_timespec64(ts);
        if (!timespec64_valid_strict(&now)) {
                pr_warn("WARNING: Persistent clock returned invalid value!\n"
                        "         Check your CMOS/BIOS settings.\n");
                now.tv_sec = 0;
                now.tv_nsec = 0; 
        } else if (now.tv_sec || now.tv_nsec)
                persistent_clock_exist = true;
 
        read_boot_clock(&ts);
        boot = timespec_to_timespec64(ts);
        if (!timespec64_valid_strict(&boot)) {
                pr_warn("WARNING: Boot clock returned invalid value!\n"
                        "         Check your CMOS/BIOS settings.\n");
                boot.tv_sec = 0;
                boot.tv_nsec = 0;
        }
        
        raw_spin_lock_irqsave(&timekeeper_lock, flags);
        write_seqcount_begin(&tk_core.seq);
        ntp_init();
 
        clock = clocksource_default_clock();
        if (clock->enable)
                clock->enable(clock);
        tk_setup_internals(tk, clock);

        tk_set_xtime(tk, &now);
        tk->raw_time.tv_sec = 0;
        tk->raw_time.tv_nsec = 0;
        tk->base_raw.tv64 = 0;
        if (boot.tv_sec == 0 && boot.tv_nsec == 0)
                boot = tk_xtime(tk);

        set_normalized_timespec64(&tmp, -boot.tv_sec, -boot.tv_nsec);
        tk_set_wall_to_mono(tk, tmp);

        timekeeping_update(tk, TK_MIRROR);

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

persistent 클럭이 있는 경우 그 시간으로 리눅스 시계를 설정하고 없는 경우 0 (1970년)값으로 시계를 설정한다.

  • 코드 라인 12~20에서 persistent 클럭 값을 읽어온다.
    • persistent 클럭 값을 읽어들이는데 현재 powerpc 아키텍처에서 사용하며 그 외의 아키텍처는 timespec 구조체가 0으로 초기화된 것과 동일하다.
    • 32비트 타임 스펙 값을 64비트 타임 스펙 값으로 바꾼다.
    • 유효하지 않은 persistent 클럭 값을 읽어온 경우 경고 메시지와 함께 0으로 초기화하여 사용한다.
    • 읽어온 persistent 값이 음수(1970년 이전)이거나 나노 초에 해당하는 값을 초과하거나 초 값이 최대치를 초과하는 경우가 유효하지 않은 값이다.
    • persistent 클럭 값이 정확히 읽히면 persistent_clock_exist = true로 설정한다.
  • 코드 라인 22~29에서 boot 클럭 값을 읽어온다.
    • 역시 powerpc 아키텍처에서만 사용하며 그 외의 아키텍처는 timespec 구조체가 0으로 초기화된 것과 동일하다.
    • 32비트 타임 스펙 값을 64비트 타임 스펙 값으로 바꾼다.
    • 유효하지 않은 boot 클럭 값을 읽어온 경우 경고 메시지와 함께 0으로 초기화하여 사용한다.
  • 코드 라인 31~32에서 timekeeping 값을 바꾸기 위해 lock을 걸고 write 시퀀스 락도 시작한다.
  • 코드 라인 33에서 ntp 초기화를 수행한다.
  • 코드 라인 35~37에서 디폴트 클럭 소스를 enable 시킨다.
    • rpi2의 경우 디폴트 클럭 소스로 jiffies를 사용하므로 항시 enable 되어 있다.
  • 코드 라인 38에서 클럭 소스를 사용하기 위해 timekeeper 내부 설정을 진행한다.
  • 코드 라인 40에서 persistent 클럭 값을 xtime에 설정한다.
  • 코드 라인 41~43에서 raw_time과 base_raw를 0으로 초기화한다.
  • 코드 라인 44~45에서 현재 시각을 monotonic 클럭으로 변환할 수 있도록 boot 타임을 사용하여 wtm을 설정한다.
  • 코드 라인 47에서 boot 타임을 timespec64 타입으로 변환한 nonotonic 클럭을 설정한다.
  • 코드 라인 49에서 timekeeping 값들을 업데이트 하고 통지등을 수행한다.
  • 코드 라인 51~52에서 lock들을 해제한다.

 

tk_setup_internals()

kernel/time/timekeeping.c

/**
 * tk_setup_internals - Set up internals to use clocksource clock.
 *
 * @tk:         The target timekeeper to setup.
 * @clock:              Pointer to clocksource.
 *
 * Calculates a fixed cycle/nsec interval for a given clocksource/adjustment
 * pair and interval request.
 *
 * Unless you're the timekeeping code, you should not be using this!
 */
static void tk_setup_internals(struct timekeeper *tk, struct clocksource *clock)
{
        cycle_t interval;
        u64 tmp, ntpinterval;
        struct clocksource *old_clock;

        old_clock = tk->tkr.clock;
        tk->tkr.clock = clock;
        tk->tkr.read = clock->read;
        tk->tkr.mask = clock->mask;
        tk->tkr.cycle_last = tk->tkr.read(clock);

        /* Do the ns -> cycle conversion first, using original mult */
        tmp = NTP_INTERVAL_LENGTH;
        tmp <<= clock->shift;
        ntpinterval = tmp;
        tmp += clock->mult/2;
        do_div(tmp, clock->mult);
        if (tmp == 0)
                tmp = 1;

        interval = (cycle_t) tmp;
        tk->cycle_interval = interval;

        /* Go back from cycles -> shifted ns */
        tk->xtime_interval = (u64) interval * clock->mult;
        tk->xtime_remainder = ntpinterval - tk->xtime_interval;
        tk->raw_interval =
                ((u64) interval * clock->mult) >> clock->shift;

         /* if changing clocks, convert xtime_nsec shift units */
        if (old_clock) {
                int shift_change = clock->shift - old_clock->shift;
                if (shift_change < 0)
                        tk->tkr.xtime_nsec >>= -shift_change;
                else
                        tk->tkr.xtime_nsec <<= shift_change;
        }
        tk->tkr.shift = clock->shift;

        tk->ntp_error = 0;
        tk->ntp_error_shift = NTP_SCALE_SHIFT - clock->shift;
        tk->ntp_tick = ntpinterval << tk->ntp_error_shift;

        /*
         * The timekeeper keeps its own mult values for the currently
         * active clocksource. These value will be adjusted via NTP
         * to counteract clock drifting.
         */
        tk->tkr.mult = clock->mult;
        tk->ntp_err_mult = 0;
}

클럭 소스를 사용하기 위해 timekeeper 내부 설정을 진행한다.

  • 코드 라인 19~22에서 timekeeper의 clock을 설정한다.
  • 코드 라인 25~27에서 ntp 인터벌 길이 << shift 하여 ntp_interval 값(ns)을 구한다.
    • 예) ntp 인터벌 길이 = 1E9 / 100hz = 1E7
      • -> ntp_interval = 1E7 << 24 = 0xf42_4000_0000
  • 코드 라인 28~34에서 interval = 1 tick에 해당하는 cycle 수 값을 구해 interval 및 cycle_interval에 대입한다.
    • 예) 0xf42_4000_0000 / mult = 192,000
  • 코드 라인 37에서 1 tick 에 해당하는 cycle 수가 담긴 interval에 mult 값을 곱하여 xtime_interval에 대입한다.
  • 코드 라인 38에서 ntpinterval 에서 xtime_interval을 빼서 xtime_remander에 대입한다.
  • 코드 라인 39~40에서 interval을 ns로 산출 시 소요되는 시간을 구한다. (1 스케쥴 tick (ns)에 가깝다)
    • interval 에 mult를 곱하고 shift 하여 raw_interval 값을 구한다.
  • 코드 라인 43~49에서 기존 clock이 있었던 경우 배율에 따라서 xtime_nsec 값을 컨버팅한다.
  • 코드 라인 52~62에서 ntp 관련 값을 초기화한다.

 

아래 그림은 timekeeper의 클럭 소스로 jiffies 클럭 소스를 사용하다 arch_sys_conter 클럭 소스로 변경되었을 때의 처리 과정을 보여준다.

 

set_normalized_timespec64()

kernel/time/time.c

/**
 * set_normalized_timespec - set timespec sec and nsec parts and normalize
 *
 * @ts:         pointer to timespec variable to be set
 * @sec:        seconds to set
 * @nsec:       nanoseconds to set
 *
 * Set seconds and nanoseconds field of a timespec variable and
 * normalize to the timespec storage format
 *
 * Note: The tv_nsec part is always in the range of
 *      0 <= tv_nsec < NSEC_PER_SEC
 * For negative values only the tv_sec field is negative !
 */
void set_normalized_timespec64(struct timespec64 *ts, time64_t sec, s64 nsec)
{
        while (nsec >= NSEC_PER_SEC) {
                /*
                 * The following asm() prevents the compiler from
                 * optimising this loop into a modulo operation. See
                 * also __iter_div_u64_rem() in include/linux/time.h
                 */
                asm("" : "+rm"(nsec));   
                nsec -= NSEC_PER_SEC;
                ++sec;
        }
        while (nsec < 0) {                      
                asm("" : "+rm"(nsec));
                nsec += NSEC_PER_SEC;
                --sec;
        }                       
        ts->tv_sec = sec;
        ts->tv_nsec = nsec;
}
EXPORT_SYMBOL(set_normalized_timespec64);

범위를 벗어난 나노초 단위를 1초 단위로 조정하여 한 번에 급격한 시간 변동이 되지 않도록 한다. 그런 후 출력 인수 ts에 timespec64 단위로 변환한다.

  • 나노초가 항상 0 ~ 1G에 해당하도록 초 단위를 1씩 증감시키며 맞춘다.

 

Timekeeping 변경

timekeeping_update()

kernel/time/timekeeping.c

/* must hold timekeeper_lock */
static void timekeeping_update(struct timekeeper *tk, unsigned int action)
{
        if (action & TK_CLEAR_NTP) {
                tk->ntp_error = 0;
                ntp_clear();
        }

        tk_update_ktime_data(tk);

        update_vsyscall(tk);
        update_pvclock_gtod(tk, action & TK_CLOCK_WAS_SET);

        if (action & TK_MIRROR)
                memcpy(&shadow_timekeeper, &tk_core.timekeeper,
                       sizeof(tk_core.timekeeper));

        update_fast_timekeeper(&tk->tkr);
}

timekeeping 값들을 업데이트 하고 통지등을 수행한다.

  • 코드 라인 4~7에서 TK_CLEAR_NTP 요청이 있는 경우 ntp를 클리어한다.
  • 코드 라인 9에서 time keeper의 ktime_t 기반의 스칼라 나노초들을 업데이트한다.
  • 코드 라인 11에서 vsyscall을 지원하는 아키텍처를 위해 timekeeper를 업데이트한다.
    • 아직 arm에는 적용되지 않았지만 arm64, x86 등 많은 커널이 유저 스페이스에서  커널 코드나 데이터의 직접 접근을 허용하도록 매핑해준다. 이렇게 사용하면 context swtich로 인한 오버헤드를 줄여 매우 빠르게 유저스페이스에서 커널의 자원을 사용할 수 있는 장점이 있다. 하지만 시큐리티 이슈로 매우 제한적인 사용을 보여준다. arm에서는 유사하게 User Helpers라는 인터페이스가 있으며 VDSO와 매우 유사하다.
    • 참고
  • 코드 라인 12에서 pvclock_gtod_chain 리스트에 등록된 notify block에 등록된 함수를 호출한다.
    • 현재는 x86 아키텍처에서 가상화 지원을 위해 사용된다.
  • 코드 라인 14~16에서 TK_MIRROR가 요청된 경우 timekeeper를 shadow timekeeper로 복사한다.
  • 코드 라인 18에서 tracing 지원을 위해 timekeeper를 fast timekeeper로도 복사한다.

 

tk_update_ktime_data()

kernel/time/timekeeping.c

/*
 * Update the ktime_t based scalar nsec members of the timekeeper
 */
static inline void tk_update_ktime_data(struct timekeeper *tk)
{
        u64 seconds;
        u32 nsec;

        /*
         * The xtime based monotonic readout is:
         *      nsec = (xtime_sec + wtm_sec) * 1e9 + wtm_nsec + now();
         * The ktime based monotonic readout is:
         *      nsec = base_mono + now();
         * ==> base_mono = (xtime_sec + wtm_sec) * 1e9 + wtm_nsec
         */
        seconds = (u64)(tk->xtime_sec + tk->wall_to_monotonic.tv_sec);
        nsec = (u32) tk->wall_to_monotonic.tv_nsec;
        tk->tkr.base_mono = ns_to_ktime(seconds * NSEC_PER_SEC + nsec);

        /* Update the monotonic raw base */
        tk->base_raw = timespec64_to_ktime(tk->raw_time);

        /* 
         * The sum of the nanoseconds portions of xtime and
         * wall_to_monotonic can be greater/equal one second. Take
         * this into account before updating tk->ktime_sec.
         */
        nsec += (u32)(tk->tkr.xtime_nsec >> tk->tkr.shift);
        if (nsec >= NSEC_PER_SEC)
                seconds++;
        tk->ktime_sec = seconds;
}

timekepper의 ktime_t 기반 나노초를 갱신한다.

  • 코드 라인 16~18에서 xtime 과 wtm을 더해서 base_mono에 대입한다.
  • 코드 라인 21에서 raw_time 값을 base_raw 타입으로 변환하여 대입한다.
  • 코드 라인 28~31에서 xtime과 wtm의 초를 더해 ktime_sec에 대입하되 wtime_nsec + (xtime_nsec >> shift) 가 1초를 초과하는 경우 추가로 1을 증가시킨다.

 

아래 그림과 같이 timekeeper의 base_mono, ktime_sec, base_raw 값이 갱신되는 것을 보여준다.

 

Fast Timekeeper

참고: timekeeping: Provide fast and NMI safe access to CLOCK_MONOTONIC

update_fast_timekeeper()

kernel/time/timekeeping.c

/**
 * update_fast_timekeeper - Update the fast and NMI safe monotonic timekeeper.
 * @tkr: Timekeeping readout base from which we take the update
 *
 * We want to use this from any context including NMI and tracing /
 * instrumenting the timekeeping code itself.
 *
 * So we handle this differently than the other timekeeping accessor
 * functions which retry when the sequence count has changed. The
 * update side does:
 *
 * smp_wmb();   <- Ensure that the last base[1] update is visible
 * tkf->seq++;
 * smp_wmb();   <- Ensure that the seqcount update is visible
 * update(tkf->base[0], tkr);
 * smp_wmb();   <- Ensure that the base[0] update is visible
 * tkf->seq++;
 * smp_wmb();   <- Ensure that the seqcount update is visible
 * update(tkf->base[1], tkr);
 *
 * The reader side does:
 *
 * do {
 *      seq = tkf->seq;
 *      smp_rmb();
 *      idx = seq & 0x01;
 *      now = now(tkf->base[idx]);
 *      smp_rmb();
 * } while (seq != tkf->seq)
 *
 * As long as we update base[0] readers are forced off to
 * base[1]. Once base[0] is updated readers are redirected to base[0]
 * and the base[1] update takes place.
 *
 * So if a NMI hits the update of base[0] then it will use base[1]
 * which is still consistent. In the worst case this can result is a
 * slightly wrong timestamp (a few nanoseconds). See
 * @ktime_get_mono_fast_ns.
 */
static void update_fast_timekeeper(struct tk_read_base *tkr)
{
        struct tk_read_base *base = tk_fast_mono.base;

        /* Force readers off to base[1] */
        raw_write_seqcount_latch(&tk_fast_mono.seq);

        /* Update base[0] */
        memcpy(base, tkr, sizeof(*base));

        /* Force readers back to base[0] */
        raw_write_seqcount_latch(&tk_fast_mono.seq);

        /* Update base[1] */
        memcpy(base + 1, base, sizeof(*base));
}

tracing 지원을 위해 timekeeper를 fast timekeeper로 복사한다. 복사 할 때 두 개의 공간에 복사하는데 매우 빠르게 lockless로 사용할 수 있도록 구현되었다.

  • 코드 라인 45에서 시퀀스를 증가시킨다. 이 때 시퀀스 값이 홀수가된다.
  • 코드 라인 48에서 fast timekeeper 첫 번째 공간에 tkr을 복사한다.
  • 코드 라인 51에서 시퀀스를 증가시킨다. 이 때 시퀀스 값이 짝수가된다.
  • 코드 라인 52에서 fast timekeeper 두 번째 공간에 첫 번째 공간을 복사한다.

 

다음 그림은 timekeeper를 fast timekeeper로 업데이트하는 것을 보여준다.

 

ktime_get_mono_fast_ns()

kernel/time/timekeeping.c

/**
 * ktime_get_mono_fast_ns - Fast NMI safe access to clock monotonic
 *
 * This timestamp is not guaranteed to be monotonic across an update.
 * The timestamp is calculated by:
 *
 *      now = base_mono + clock_delta * slope
 *
 * So if the update lowers the slope, readers who are forced to the
 * not yet updated second array are still using the old steeper slope.
 *
 * tmono
 * ^
 * |    o  n
 * |   o n
 * |  u
 * | o
 * |o
 * |12345678---> reader order
 *
 * o = old slope
 * u = update
 * n = new slope
 *
 * So reader 6 will observe time going backwards versus reader 5.
 *
 * While other CPUs are likely to be able observe that, the only way
 * for a CPU local observation is when an NMI hits in the middle of
 * the update. Timestamps taken from that NMI context might be ahead
 * of the following timestamps. Callers need to be aware of that and
 * deal with it.
 */
u64 notrace ktime_get_mono_fast_ns(void)
{
        struct tk_read_base *tkr;
        unsigned int seq;
        u64 now;

        do {
                seq = raw_read_seqcount(&tk_fast_mono.seq);
                tkr = tk_fast_mono.base + (seq & 0x01);
                now = ktime_to_ns(tkr->base_mono) + timekeeping_get_ns(tkr);

        } while (read_seqcount_retry(&tk_fast_mono.seq, seq));
        return now;
}
EXPORT_SYMBOL_GPL(ktime_get_mono_fast_ns);

fast timekeeper로부터 monotonic 클럭 값을 얻어온다. fast timekeeeper는 tracing 목적으로 사용되며 lockless 구현되어 매우 빠르게 monotonic 값을 얻고자 할 때 사용하다.

  • 코드 라인 40에서 시퀀스 값을 읽어온다.
  • 코드 라인 41~42에서 시퀀스가 짝수이면 fast timekeeper의 base[0]에서 홀수이면 base[1]을 사용하여 monotonic 값을 산출한다.
  • 코드 라인 44에서 시퀀스 값에 변화가 있는 경우 다시 반복한다.

 

참고