Timer -9- (Tick Device)

<kernel v5.4>

Tick Device Subsystem

Tick Device Subsystem은 CONFIG_HZ 주기에 해당하는 타이머 스케줄 틱을 발생시키고, 이 스케줄 틱을 이용하는 다음과 같은 여러 루틴들을 처리한다.

  • jiffies
    • jiffies 값을 증가시킨다. (jiffies 담당 cpu가 처리하며 nohz를 위해 담당 cpu는 변경될 수 있다.)
  • timer
    • lowres timer wheel을 검색하여 만료된 타이머의 함수를 호출한다.
  • 스케줄러 틱
    • 런큐의 로드 값을 갱신하고, 현재 동작 중인 태스크 스케줄러(cfs, rt, deadline, …)의 (*task_tick)을 호출한다.
  • rcu
    • rcu core를 처리한다.
  • process account
    • cpu 사용 시간을 계량한다.

 

다음 그림은 generic timer sybsystem을 모두 보여준다.

 

틱 디바이스의 모드 운영

주기적으로 틱을 발생하기 위해 다음과 같이 두 가지 모드를 사용한다.

  • hz 기반의 periodic 모드로 운영
    • periodic 또는 oneshot 기능을 가진 클럭 이벤트 디바이스 모두 동작할 수 있다.
    • legacy 하드웨어에서는 low-resolution 타이머가 oneshot을 지원하지 않고 지속적(periodic)으로 틱을 만드는 hw를 호환하여 운영하기 위해  커널은 처음 부트업시 항상 이 모드로 시작한다.
  • nohz 기반의 oneshot 모드로 운영
    • oneshot 기능을 가진 high-resolution 타이머를 가진 클럭 이벤트 디바이스만 운영 가능하다.
    • 틱 모드는 처음에 periodic으로 운영하다가 hrtimer가 준비된 후에 틱 디바이스의 모드를 oneshot 모드로 변경하여 운영한다. 이 과정에서 tick 핸들러가 다음과 같은 순으로 바뀐다.
      • tick_handle_periodic() -> clockevents_handle_noop() -> tick_sched_timer()

 

다음 그림은 tick devices subsystem이 동작하는 과정을 보여준다.

 

Per-cpu Tick Device 및 Tick Broadcast Device

틱을 관리하는 드라이버들로 클럭 이벤트 디바이스의 기능에 따라 각 cpu들은 틱 cpu 디바이스 또는 틱 브로드캐스트 디바이스 둘 중 하나에 연결하여 사용한다.

  • Per-cpu Tick 디바이스 (전역 변수 per-cpu tick_cpu_device)
    • per-cpu 타이머, 절전기능(c3stop) 타이머와 dummy 타이머를 사용한 클럭 이벤트 디바이스들로 사용 가능하다.
  • Tick 브로드캐스트 디바이스 (전역 변수 tick_broadcast_device)
    • per-cpu 타이머, 절전기능(c3stop) 타이머 및 dummy 타이머를 사용하지 않는 클럭 이벤트 디바이스만 사용 가능하다.
    • 틱 브로드캐스트 모드가 oneshot인 경우 클럭 이벤트 디바이스도 oneshot 기능이 준비되어 있어야 한다.
    • 시스템에서 nohz 구현을 위해 Tick 브로드캐스트 디바이스를 사용하지 않는 경우도 있다. (rpi2, rpi3도 사용하지 않는다.)

 

 

c3stop (절전 상태 진입하여 코어, IC 및 타이머 등 파워 다운)

arm 아키텍처에 내장된 타이머 중 CP15 레지스터를 통해 제어되는 경우 cpu가 c3(deep-sleep) 절전 상태로 진입하면 절전을 위해 코어, IC 및 타이머의 power를 다운시킨다.  이렇게 타이머 전원을 다운시키면 해당 타이머에 의해 틱 처리를 하기 위해 스스로 wake-up할 수 없으므로 다른 깨어있는 cpu의 도움을 받아야 한다. arm 아키텍처의 내장 타이머는 기본적으로 c3stop을 지원하는데 cpu가 c3(deep-sleep) 상태로 진입할 때 최소 한 개의 cpu는 지속적으로 tick을 처리하고 c3 상태에 진입한 cpu를 대신해서 틱의 만료시간을 계산하여 알릴 필요가 있다. 이렇게 만료된 cpu를 깨워 틱을 처리하게 하기 위해 틱 브로드캐스트 디바이스를 사용한다.

  • 현재까지 armv7, armv8 아키텍처의 대부분은 core 단위의 idle에서는 wfi를 사용하여 절전활동을 하고, 클러스터에 소속한 모든 core가 idle되는 경우 deep-sleep 상태로 진입한다. suspend와 동일하게 클러스터 내의 파워를 끄는 형태인데 이 때 시스템에 따라 해당 클러스터의 타이머 및 인터럽트 컨트롤러도 같이 전원이 꺼질 수 있다.
  • arm 아키텍처에 내장된 CP15로 제어되는 타이머라 하더라도 특정 시스템은 절전 설계되지 않아 항상 타이머 전원이 on되어 있다. 이러한 경우 c3stop을 false(“always-on”)로 설정하여 내부적으로 스케줄 틱의 관리에 틱 브로드캐스트 디바이스를 사용할 필요 없이 그냥 틱 cpu 디바이스를 사용한다.

 

c3stop 기능을 사용하는 클럭소스

  • x86의 LAPIC 타이머
  • ARM 내장 generic 타이머
    • arm_arch_timer
  • 기타
    • mips-gic-timer
    • clps711x-timer

 

절전을 위한 전원 관리 시스템이 없는 SoC에서는 c3stop 기능을 사용하면 안된다. 이러한 시스템에서 절전을 위해 타이머의 전원을 off 후 on 하는 경우 타이머의 comparison 레지스터의 내용이 유실되는 현상이 벌어진다. 이러한 증상으로 인해 리눅스 커널은 디바이스 트리 스크립트의 타이머 노드에 “always-on;” 속성을 추가하여 c3stop 기능을 사용하지 못하게 막는다. (항상 전원이 켜져서 동작하는 타이머로 인식한다.)

  • rpi2, rpi3: “always-on;” 속성을 사용하여 브로드캐스트 기능을 사용하지 못하게 한다.

 

틱 브로드캐스트

절전을 위해 tick을 발생시키지 않는 nohz-idle을 구현할 때 cpu가 c3(deep-sleep) 상태에 진입되면 타이머 전원도 같이 꺼지는 시스템을 위해 다음과 같은 브로드캐스트 구현이 필요하다.

  • cpu가 처리할 일이 없어 idle 상태로 진입하기 전에 브로드캐스트를 수신하기 위해 해당 cpu의 비트를 설정한다.
    • 브로드캐스트 디바이스의 모드가 periodic인 경우 shutdown 한다.
    • 모든 cpu가 처리할 일이 없어도 최소 하나의 대표 cpu는 브로드캐스트 디바이스가 아니라 틱 디바이스로 동작해야 한다.
  • 대표 cpu는 c3(deep-sleep) 상태의 cpu를 깨우기 위해 broadcast를 한다.
    • c3 상태에 있는 cpu를 깨울 때 IPI(Inter Process Interrupt)를 통해 브로드캐스트하여 해당 cpu를 깨운다.
  • 깨어난 cpu는 더 이상 브로드캐스트를 수신하지 않아도 되므로 해당 cpu의 비트를 클리어한다.
    • 브로드캐스트 디바이스의 모드가 periodic인 경우 다시 프로그램한다.

 

C-State

인텔에서 정의한 절전 상태 (참고: Everything You Need to Know About the CPU C-States Power Saving Modes

  • C0
    • 풀 파워로 인스트럭션을 수행한다.
  • C1
    • 내부 코어 클럭을 정지시킨 halt 상태
    • 외부 클럭과 ACPI는 동작 중으로 인터럽트는 처리할 수 있는 상태
  • C2
    • Stop-Grant & Stop-Clock 상태로 내부 및 외부 클럭이 정지된 상태
    • ACPI는 동작중으로 인터럽트는 처리할 수 있는 상태
  • C3
    • 플러시 캐시. 내부 클럭 및 외부 클럭도 받아들이지 않고 deep sleep 상태
    • ACPI를 통한 인터럽트도 받아들이지 않으며, 특별히 wake-up을 통한 장치를 통해서만 깨울 수 있다.
    • c3stop이라고도 한다.
  • C4/C5
    • 인텔 듀오프로세서에서 멀티 코어 전체가 deep sleep 상태
  • C6
    • 인텔 Core i7에서 모든 코어가 좀 더 deeper sleep 상태

 

다음 그림은 tick 핸들러가 처음 설정되는 모습을 보여준다.

  • arm 아키텍처에 내장된 타이머는 부트업 시 틱 periodic 모드로 출발하므로 tick_handle_periodic() 핸들러가 선택된다.

 

다음 그림은 tick_periodic_handler()가 호출된 후 high-resolution 타이머가 준비되고, nohz oneshot 모드로 전환되어 hrtimer_interrupt() 핸들러가 선택된다. hrtimer_intrerrupt() 내부에서 스케줄 틱 타이머 함수인 tick_sched_timer() 함수가 호출된다.

 

다음 그림은 틱 cpu 디바이스 및 틱 브로드캐스트 디바이스가 사용하는 틱 핸들러들이다.

  • hz/nohz 기반의 high resolution을 사용하는 핸들러는 hrtimer_interrupt이지만 틱 디바이스와 별개로 모든 hrtimer의 만료 시간을 관리한다. 따라서 틱 디바이스만을 대상으로 좁히는 경우 실제 틱 처리에 대한 핸들러는 tick_sched_timer()라고 할 수 있다.

 


틱 cpu 디바이스 또는 틱 브로드캐스트 디바이스 등록

tick_check_new_device()

kernel/time/tick-common.c

/*
 * Check, if the new registered device should be used. Called with
 * clockevents_lock held and interrupts disabled.
 */
void tick_check_new_device(struct clock_event_device *newdev)
{
        struct clock_event_device *curdev;
        struct tick_device *td;
        int cpu;

        cpu = smp_processor_id();
        td = &per_cpu(tick_cpu_device, cpu);
        curdev = td->evtdev;
        
        /* cpu local device ? */
        if (!tick_check_percpu(curdev, newdev, cpu))
                goto out_bc;

        /* Preference decision */
        if (!tick_check_preferred(curdev, newdev))
                goto out_bc;

        if (!try_module_get(newdev->owner))
                return;

        /* 
         * Replace the eventually existing device by the new
         * device. If the current device is the broadcast device, do
         * not give it back to the clockevents layer !
         */
        if (tick_is_broadcast_device(curdev)) {
                clockevents_shutdown(curdev);
                curdev = NULL;
        }
        clockevents_exchange_device(curdev, newdev);
        tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
        if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
                tick_oneshot_notify();
        return;

out_bc:
        /*
         * Can the new device be used as a broadcast device ?
         */
        tick_install_broadcast_device(newdev);
}

현재 cpu의 tick 디바이스로 기존 tick 디바이스보다 새 tick 디바이스가 더 좋은 rating 등급인 경우 변경하여 사용할지 체크한다.

  • 처음 호출 시에는 요청 clock event 디바이스가 tick 디바이스로 사용된다.
  • nohz 구현을 위해 경우에 따라 등록되는 클럭 이벤트 디바이스가 틱 브로드캐스트 디바이스로 동작할 수도 있다.
  • 코드 라인 12~13에서 현재 cpu의 틱 디바이스에 사용중인 clock event 디바이스를 새 디바이스로 변경 가능한지 체크한다. 사용할 수 없으면 out_bc 레이블로 이동한다.
  • 코드 라인 16~17에서 기존 디바이스와 새 디바이스를 비교하여 새 디바이스의 rating이 더 높은 경우가 아니면  out_bc 레이블로 이동한다.
  • 코드 라인 19~20에서 새 clock event 디바이스 모듈에 대한 참조 카운터를 증가시킨다.
  • 코드 라인 27~30에서 기존 디바이스가 브로드 캐스트 디바이스인 경우 shutdown 시킨다.
  • 코드 라인 31~32에서 틱 디바이스에 사용할 새 클럭 이벤트 디바이스로 변경한다.
  • 코드 라인 33~35에서 새 디바이스에 oneshot 기능이 있는 경우 클럭 이벤트 디바이스가 변경되었음을 틱 스케쥴 정보에 async하게 알리고 함수를 빠져나간다.
  • 코드 라인 37~41에서 out_bc 레이블이다. 새 클럭 이벤트 디바이스를 체크하여 브로드 캐스트 디바이스로 지정한다.

 

다음 그림은 각 cpu별로 여러 개의 틱 이벤트 디바이스가 존재하는 경우 best rating 디바이스를 선택하고 그 나머지들은 release 리스트에 두는 모습을 보여준다.

 

틱 디바이스 조건 체크

tick_check_percpu()

kernel/time/tick-common.c

static bool tick_check_percpu(struct clock_event_device *curdev,
                              struct clock_event_device *newdev, int cpu)
{
        if (!cpumask_test_cpu(cpu, newdev->cpumask))
                return false;
        if (cpumask_equal(newdev->cpumask, cpumask_of(cpu)))
                return true;
        /* Check if irq affinity can be set */
        if (newdev->irq >= 0 && !irq_can_set_affinity(newdev->irq))
                return false;
        /* Prefer an existing cpu local device */
        if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
                return false;
        return true;
}

새 클럭 이벤트 디바이스를 tick 디바이스로 설정할 수 있는지 여부를 반환한다.

  • 코드 라인 4~5에서 요청 cpu가 새 클럭 이벤트 디바이스가 허용하는 cpu가 아닌 경우 false를 반환한다.
  • 코드 라인 6~7에서 요청 cpu가 새 디바이스에 per-cpu로 등록된 경우 true를 반환한다.
  • 코드 라인 9~10에서 새 디바이스가 취급하는 irq로 affinity 설정할 수 없으면 false를 반환한다.
    • FEAT_DYNTICK이 부여된 드라이버들
      • armv7 아키텍처에 내장된 generic 타이머의 메모리 mapped 드라이버 – “arm,armv7-timer-mem”
      • “st,nomadik-mtu” 드라이버
  • 코드 라인 12~13에서 요청 cpu가 현재 디바이스에 이미 per-cpu로 등록되어 있는 경우 false를 반환한다.

 

tick_check_preferred()

kernel/time/tick-common.c

static bool tick_check_preferred(struct clock_event_device *curdev,
                                 struct clock_event_device *newdev)
{
        /* Prefer oneshot capable device */
        if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {
                if (curdev && (curdev->features & CLOCK_EVT_FEAT_ONESHOT))
                        return false;
                if (tick_oneshot_mode_active())
                        return false;
        }

        /*
         * Use the higher rated one, but prefer a CPU local device with a lower
         * rating than a non-CPU local device
         */
        return !curdev ||
                newdev->rating > curdev->rating ||
               !cpumask_equal(curdev->cpumask, newdev->cpumask);
}

기존 틱 디바이스와 새 디바이스를 비교하여 새 디바이스의 동작 모드 및 등급(rating)이 더 높은 경우 true를 반환한다. (oneshot -> rating 순)

  • 코드 라인  5~7에서 기존  틱 디바이스는 oneshot 기능이 있는데 새 디바이스가 oneshot 기능이 없는 경우 false를 반환한다. (oneshot 우선)
  • 코드 라인 8~9에서 기존 틱 디바이스가 이미 oneshot 모드로 동작하는데 새 디바이스가 oneshot 기능이 없는 경우 false를 반환한다. (oneshot 우선)
  • 코드 라인 16~18에서 새 디바이스의 rating이 더 높은 경우 true를 반환한다.

 

다음 그림은 새 클럭 이벤트 디바이스가 기존보다 더 구형(periodic) 모드로 동작하거나 rating이 더 낮을 때 선호되지 않는 케이스들을 보여준다.

  • 성공 case:
    • 기존 틱 디바이스로 등록된 클럭 이벤트 디바이스가 하나도 없는 경우
    • 기존 틱 디바이스에 등록된 클럭 이벤트 디바이스의 등급(rating)보다 높고 feature도 서로 같거나 더 높은 oneshot인 경우

 

tick_oneshot_mode_active()

kernel/time/tick-oneshot.c

/**
 * tick_check_oneshot_mode - check whether the system is in oneshot mode
 *
 * returns 1 when either nohz or highres are enabled. otherwise 0.
 */
int tick_oneshot_mode_active(void)
{
        unsigned long flags;
        int ret;

        local_irq_save(flags);
        ret = __this_cpu_read(tick_cpu_device.mode) == TICKDEV_MODE_ONESHOT;
        local_irq_restore(flags);

        return ret;
}

현재 cpu의 tick 디바이스가 oneshot 모드인지 여부를 반환한다.

 


Tick 디바이스로 지정

tick_setup_device()

kernel/time/tick-common.c

/*
 * Setup the tick device
 */
static void tick_setup_device(struct tick_device *td,
                              struct clock_event_device *newdev, int cpu,
                              const struct cpumask *cpumask)
{
        void (*handler)(struct clock_event_device *) = NULL;
        ktime_t next_event = 0;

        /*
         * First device setup ?
         */
        if (!td->evtdev) {
                /*
                 * If no cpu took the do_timer update, assign it to
                 * this cpu:
                 */
                if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {
                        tick_do_timer_cpu = cpu;

                        tick_next_period = ktime_get();
                        tick_period = NSEC_PER_SEC / HZ;
#ifdef CONFIG_NO_HZ_FULL
                        /*
                         * The boot CPU may be nohz_full, in which case set
                         * tick_do_timer_boot_cpu so the first housekeeping
                         * secondary that comes up will take do_timer from
                         * us.
                         */
                        if (tick_nohz_full_cpu(cpu))
                                tick_do_timer_boot_cpu = cpu;

                } else if (tick_do_timer_boot_cpu != -1 &&
                                                !tick_nohz_full_cpu(cpu)) {
                        tick_take_do_timer_from_boot();
                        tick_do_timer_boot_cpu = -1;
                        WARN_ON(tick_do_timer_cpu != cpu);
#endif
                }

                /*
                 * Startup in periodic mode first.
                 */
                td->mode = TICKDEV_MODE_PERIODIC;
        } else {
                handler = td->evtdev->event_handler;
                next_event = td->evtdev->next_event;
                td->evtdev->event_handler = clockevents_handle_noop;
        }

        td->evtdev = newdev;

        /*
         * When the device is not per cpu, pin the interrupt to the
         * current cpu:
         */
        if (!cpumask_equal(newdev->cpumask, cpumask))
                irq_set_affinity(newdev->irq, cpumask);

        /*
         * When global broadcasting is active, check if the current
         * device is registered as a placeholder for broadcast mode.
         * This allows us to handle this x86 misfeature in a generic
         * way. This function also returns !=0 when we keep the
         * current active broadcast state for this CPU.
         */
        if (tick_device_uses_broadcast(newdev, cpu))
                return;

        if (td->mode == TICKDEV_MODE_PERIODIC)
                tick_setup_periodic(newdev, 0);
        else
                tick_setup_oneshot(newdev, handler, next_event);
}

요청한 클럭 이벤트 디바이스를 현재 cpu에 대한 틱 디바이스로 지정한다. 현재 cpu에 대해 처음 틱 디바이스가 지정된 경우 틱 디바이스의 모드를 periodic으로 시작한다. (hrtimer가 초기화되어 사용되기 전에는 oneshot 모드로 바꾸지 않는다.)

  • 코드 라인 11~42에서  현재 cpu에 대해 tick 디바이스의 클럭 이벤트 디바이스가 설정되지 않은 경우 모드를 periodic으로 설정한다. 그리고 모든 cpu를 대상으로 가장 처음 틱 디바이스가 설정되면 틱에 대한 주기 등 관련 값들을 설정한다. (tick_do_timer_cpu, tick_next_period, tick_period)
    • tick_do_timer_cpu는 디폴트 값으로 TICK_DO_TIMER_BOOT 값으로 설정되어 cpu가 아직 지정되지 않았음을 의미한다.
  • 코드 라인 43~47에서 tick 디바이스의 클럭 이벤트 디바이스가 이미 설정되어 있지만 새로운 요청이 있는 경우 틱 이벤트 핸들러에 빈 함수를 지정한 후 기존 이벤트 핸들러에서 사용 중인 핸들러를 사용할 준비를 한다.
    • 해당 cpu에 대한 틱 디바이스의 클럭 이벤트 디바이스가 변경되는 과정에서 이벤트 핸들러가 동작되어야 할 때 자연스럽게 아무것도 처리하지 않게 한다.
  • 코드 라인 49에서 tick 디바이스에 클럭 이벤트 디바이스를 연결한다.
  • 코드 라인 55~56에서 새 클럭 이벤트 디바이스가 cpu 내장형(per-cpu)이 아니면 현재 cpu에서 새 틱 디바이스의 irq를 처리할 수 있도록 affinity 설정을 한다.
    • FEAT_IRQDYN 기능이 있는 클럭 이벤트 디바이스만이 irq를 요청한 cpu로 연결할 수 있다.
    • 브로드캐스트 목적의 인터럽트가 특정 cpu에 고정되지 않고 cpu를 선택(set_irq_affinity)하여 사용할 수 있으며 다음 드라이버에서 사용되고 있다.
      • armv7 또는 armv8 아키텍처에 내장된 generic 타이머의 메모리 mapped 드라이버 – “arm,armv7-timer-mem”
      • “st,nomadik-mtu”
  • 코드 라인 65~66에서 새 클럭 이벤트 디바이스가 브로드캐스트 디바이스로 동작하는 경우 처리가 완료되었으므로 함수를 빠져나간다.
  • 코드 라인 68~71에서 tick 디바이스의 모드에 맞게 틱 디바이스를 periodic 및 oneshot 모드로 설정한다.

 

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로 운영되는지 여부를 반환한다.

 

tick_nohz_full_enabled()

include/linux/tick.h

static inline bool tick_nohz_full_enabled(void)
{
        if (!context_tracking_is_enabled())
                return false;

        return tick_nohz_full_running;
}

nohz full로 운영되는지 여부를 반환한다.

 

틱 디바이스를 브로드캐스트 디바이스로 등록

tick_device_uses_broadcast()

kernel/time/tick-broadcast.c

/*
 * Check, if the device is disfunctional and a place holder, which
 * needs to be handled by the broadcast device.
 */
int tick_device_uses_broadcast(struct clock_event_device *dev, int cpu)
{
        struct clock_event_device *bc = tick_broadcast_device.evtdev;
        unsigned long flags;
        int ret = 0;

        raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

        /*
         * Devices might be registered with both periodic and oneshot
         * mode disabled. This signals, that the device needs to be
         * operated from the broadcast device and is a placeholder for
         * the cpu local device.
         */
        if (!tick_device_is_functional(dev)) {
                dev->event_handler = tick_handle_periodic;
                tick_device_setup_broadcast_func(dev);
                cpumask_set_cpu(cpu, tick_broadcast_mask);
                if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
                        tick_broadcast_start_periodic(bc);
                else
                        tick_broadcast_setup_oneshot(bc);
                ret = 1;
        } else {
                /*
                 * Clear the broadcast bit for this cpu if the
                 * device is not power state affected.
                 */
                if (!(dev->features & CLOCK_EVT_FEAT_C3STOP))
                        cpumask_clear_cpu(cpu, tick_broadcast_mask);
                else
                        tick_device_setup_broadcast_func(dev);

                /*
                 * Clear the broadcast bit if the CPU is not in
                 * periodic broadcast on state.
                 */
                if (!cpumask_test_cpu(cpu, tick_broadcast_on))
                        cpumask_clear_cpu(cpu, tick_broadcast_mask);

틱 디바이스 상태에 따라 브로드캐스트 디바이스로 동작시킬지 아닐지 여부를 다음과 같이 결정한다.

  • 더미 틱 디바이스인 경우 현재 cpu에 대해 브로드캐스트 디바이스로 동작시킨다. 결과=1
  • oneshot 틱 디바이스인 경우 현재 cpu를 브로드캐스트에서 제외한다. 결과=0
  • periodic 모드인 경우 브로드캐스트할 cpu가 없는 경우 shutdown한다. 결과=현재 cpu가 브로드캐스트에 포함되어 있는지 여부

 

  • 코드 라인 15~23에서 클럭 이벤트 디바이스가 periodic 및 oneshot 기능이 없는 더미인 경우 핸들러 함수와 브로드캐스트 함수를 설정한다. 그리고 현재 cpu를 tick_broadcast_mask에 추가한다.
    • 틱 브로드캐스트 디바이스가 더미 디바이스 상태에 있을 때에도 핸들러 함수가 정상적으로 호출되도록 설정한다.
    • 더미 디바이스를 틱 브로드캐스트 디바이스로 사용하고 결과로 1을 반환한다. 모드가 periodic을 지원하면 periodic 모드로 동작시키고 그렇지 않으면 oneshot 모드로 동작시킨다.
  • 코드 라인 24~32에서 절전을 위한 c3stop 기능이 없는 경우 tick_broadcast_mask에서 요청 cpu를 제외시킨다. 반대로 c3stop 기능이 있는 경우 현재 디바이스에 브로드캐스트 함수를 설정한다.
    • c3stop 기능이 없으면 타이머 전원을 끄지 않고 계속 동작시킨다.
  • 코드 라인 38~39에서 periodic용 tick_broadcast_on에 현재 cpu가 포함되지 않은 경우 tick_broadcast_mask에서 현재 cpu를 클리어한다.

 

                switch (tick_broadcast_device.mode) {
                case TICKDEV_MODE_ONESHOT:
                        /*
                         * If the system is in oneshot mode we can
                         * unconditionally clear the oneshot mask bit,
                         * because the CPU is running and therefore
                         * not in an idle state which causes the power
                         * state affected device to stop. Let the
                         * caller initialize the device.
                         */
                        tick_broadcast_clear_oneshot(cpu);
                        ret = 0;
                        break;

                case TICKDEV_MODE_PERIODIC:
                        /*
                         * If the system is in periodic mode, check
                         * whether the broadcast device can be
                         * switched off now.
                         */
                        if (cpumask_empty(tick_broadcast_mask) && bc)
                                clockevents_shutdown(bc);
                        /*
                         * If we kept the cpu in the broadcast mask,
                         * tell the caller to leave the per cpu device
                         * in shutdown state. The periodic interrupt
                         * is delivered by the broadcast device, if
                         * the broadcast device exists and is not
                         * hrtimer based.
                         */
                        if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER))
                                ret = cpumask_test_cpu(cpu, tick_broadcast_mask);
                        break;
                default:
                        break;
                }
        }
        raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
        return ret;
}
  • 코드 라인 1~13에서 틱 브로드캐스트 디바이스가 oneshot 모드로 동작하는 경우 요청 cpu를 브로드캐스트 대상에서 제외하고 결과로 0을 반환한다.
    • tick_broadcast_oneshot_mask와 tick_broadcast_pending_mask에서 요청 cpu를 제외시킨다.
  • 코드 라인 15~33에서 틱 브로드캐스트 디바이스가 periodic 모드로 동작하는 경우 요청 cpu가 브로드캐스트 대상에 포함되어 있는지 여부를 반환한다. 만일 브로드캐스트 대상이 없는 경우 디바이스를 shutdown 한다.

 

tick_device_is_functional()

kernel/time/tick-internal.h

/*
 * Check, if the device is functional or a dummy for broadcast
 */
static inline int tick_device_is_functional(struct clock_event_device *dev)
{
        return !(dev->features & CLOCK_EVT_FEAT_DUMMY);
}

클럭 이벤트 디바이스가 periodic 및 oneshot 기능 중 하나가 있는 경우 true를 반환한다.

  • dummy 디바이스는 periodic 및 oneshot 기능 모두 없다.

 

tick_device_setup_broadcast_func()

kernel/time/tick-broadcast.c

static void tick_device_setup_broadcast_func(struct clock_event_device *dev)
{
        if (!dev->broadcast)
                dev->broadcast = tick_broadcast;
        if (!dev->broadcast) {
                pr_warn_once("%s depends on broadcast, but no broadcast function available\n",
                             dev->name);
                dev->broadcast = err_broadcast;
        }
}

틱 디바이스에 브로드캐스트 함수가 설정되지 않은 경우 브로드캐스트 함수를 설정한다.

  • 틱 브로드캐스트 함수에서 대상 cpu로 IPI(Inter Process Interrupt)를 발생시킨다.

 

tick_broadcast()

arch/arm/kernel/smp.c

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
void tick_broadcast(const struct cpumask *mask)
{
        smp_cross_call(mask, IPI_TIMER);
}
#endif

대상 cpu들을 IPI(Inter Process Interrupt)로 깨워 IPI _TIMER 기능에 대한 처리를 수행하게 요청한다.

 

tick_broadcast_start_periodic()

kernel/time/tick-broadcast.c

/*
 * Start the device in periodic mode
 */
static void tick_broadcast_start_periodic(struct clock_event_device *bc)
{
        if (bc)
                tick_setup_periodic(bc, 1);
}

요청한 클럭 이벤트 디바이스를 periodic 모드의 브로드캐스트 디바이스로 사용하여 tick을 발생시킨다.

 


틱 디바이스 모드 설정

틱 디바이스를 periodic 모드로 설정

tick_setup_periodic()

/*
 * Setup the device for a periodic tick
 */
void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
        tick_set_periodic_handler(dev, broadcast);

        /* Broadcast setup ? */
        if (!tick_device_is_functional(dev))
                return;

        if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
            !tick_broadcast_oneshot_active()) {
                clockevents_set_mode(dev, CLOCK_EVT_MODE_PERIODIC);
        } else {
                unsigned long seq;
                ktime_t next; 

                do {
                        seq = read_seqbegin(&jiffies_lock);
                        next = tick_next_period;
                } while (read_seqretry(&jiffies_lock, seq));

                clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);

                for (;;) {
                        if (!clockevents_program_event(dev, next, false))
                                return;
                        next = ktime_add(next, tick_period);
                }
        }
}

요청한 클럭 이벤트 디바이스를 periodic 모드의 틱 디바이스로 사용하여 tick을 발생시킨다. broadcast=1로 요청한 경우 브로드캐스트 핸들러를 준비한다.

  • 코드 라인 3에서 periodic 핸들러 함수를 지정한다. @broadcast가 요청된 경우 broadcast용 periodic 핸들러 함수를 지정한다.
  • 코드 라인 6~7에서 클럭 이벤트 디바이스가 dummy 디바이스인 경우 이미 브로드 캐스트 디바이스이므로 함수를 빠져나간다.
  • 코드 라인 9~11에서 클럭 이벤트 디바이스 기능에 periodic이 있고 틱 브로드캐스트 디바이스가 oneshot 모드가 아닌 경우 클럭 이벤트 디바이스를 periodic로 설정한다.
  • 코드 라인 12~21에서 1 tick을 더해 다음 만료 시간을 지정한 후 클럭 이벤트 디바이스를 oneshot 모드로 설정한다.
  • 코드 라인 23~27에서 클럭 이벤트 디바이스에 프로그램하여 성공하면 함수를 빠져나간다. 만일 실패하는 경우 1 tick 만큼 만료 시간을 더해 성공할 때까지 루프를 돌며 시도한다.

 

tick_set_periodic_handler()

kernel/time/tick-broadcast.c

/*
 * Set the periodic handler depending on broadcast on/off
 */
void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
        if (!broadcast)
                dev->event_handler = tick_handle_periodic;
        else
                dev->event_handler = tick_handle_periodic_broadcast;
}

periodic 핸들러 함수를 지정한다. @broadcast가 요청된 경우 broadcast용 periodic 핸들러 함수를 지정한다.

 

틱 디바이스를 oneshot 모드로 설정

tick_setup_oneshot()

kernel/time/tick-oneshot.c

/**
 * tick_setup_oneshot - setup the event device for oneshot mode (hres or nohz)
 */
void tick_setup_oneshot(struct clock_event_device *newdev,
                        void (*handler)(struct clock_event_device *),
                        ktime_t next_event)
{
        newdev->event_handler = handler;
        clockevents_set_mode(newdev, CLOCK_EVT_MODE_ONESHOT);
        clockevents_program_event(newdev, next_event, true);
}

oneshot 모드로 클럭이벤트 디바이스를 설정하고 핸들러를 대입한 후 이벤트를 프로그램한다.

 

tick_is_broadcast_device()

kernel/time/tick-broadcast.c

/*
 * Check, if the device is the broadcast device
 */
int tick_is_broadcast_device(struct clock_event_device *dev)
{
        return (dev && tick_broadcast_device.evtdev == dev);
}

요청한 클럭 이벤트 디바이스가 브로드캐스트 디바이스인지 여부를 반환한다.

 


틱 디바이스 이벤트 핸들러

틱 디바이스 핸들러 -1- (hz based, 저해상도 타이머)

tick_handle_periodic()

kernel/time/tick-common.c

/*
 * Event handler for periodic ticks
 */
void tick_handle_periodic(struct clock_event_device *dev)
{
        int cpu = smp_processor_id();
        ktime_t next = dev->next_event;

        tick_periodic(cpu);
#if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON)
        /*
         * The cpu might have transitioned to HIGHRES or NOHZ mode via
         * update_process_times() -> run_local_timers() ->
         * hrtimer_run_queues().
         */
        if (dev->event_handler != tick_handle_periodic)
                return;
#endif
        if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
                return;
        for (;;) {
                /*
                 * Setup the next period for devices, which do not have
                 * periodic mode:
                 */
                next = ktime_add(next, tick_period);

                if (!clockevents_program_event(dev, next, false))
                        return;
                /*
                 * Have to be careful here. If we're in oneshot mode,
                 * before we call tick_periodic() in a loop, we need
                 * to be sure we're using a real hardware clocksource.
                 * Otherwise we could get trapped in an infinite
                 * loop, as the tick_periodic() increments jiffies,
                 * which then will increment time, possibly causing
                 * the loop to trigger again and again.
                 */
                if (timekeeping_valid_for_hres())
                        tick_periodic(cpu);
        }
}

틱 디바이스가 periodic 모드로 동작하여 인터럽트가 발생하면 이 함수가 호출되며 이 때 tick_periodic() 함수를 호출한다. oneshot 모드를 사용하는 클럭 이벤트 디바이스인 경우 다음  tick을 프로그램한다.

  • 코드 라인 6에서 정규 틱 마다 할 일(스케줄, account process, rcu, irq work, posix cpu 타이머 등)을 처리한다. 추가로 현재 cpu가 do_timer() 호출 전담인 경우 jiffies를 1 증가시키고 wall time 등을 갱신한다.
  • 코드 라인 13~14에서 periodic 핸들러가 아닌 경우 함수를 빠져나간다.
  • 코드 라인 16~17에서 클럭 이벤트 디바이스가 이미 oneshot 모드가 아닌 경우 더 이상 처리를 할 필요 없으므로 핸들러를 빠져나간다.
    • 결국 periodic 모드에서는 tick 마다 인터럽트 발생하고 jiffies++하고 wall  time 만 갱신한다.
  • 코드 라인 18~38에서 tick에 대한 시간을 추가하여 oneshot 이벤트를 프로그램 한다. 프로그램에 실패한 경우 timekeeping용 클럭 소스에서 CLOCK_SOURCE_VALID_FOR_HRES 플래그를 사용했으면 다시 한 번 jiffies를 1 증가시키고 wall time 등을 갱신하게 한다. 그 후 다시 반복한다.

 

tick_periodic()

kernel/time/tick-common.c

/*
 * Periodic tick
 */
static void tick_periodic(int cpu)
{
        if (tick_do_timer_cpu == cpu) {
                write_seqlock(&jiffies_lock);

                /* Keep track of the next tick event */
                tick_next_period = ktime_add(tick_next_period, tick_period);

                do_timer(1);
                write_sequnlock(&jiffies_lock);
                update_wall_time();
        }                          

        update_process_times(user_mode(get_irq_regs()));
        profile_tick(CPU_PROFILING);
}

정규 틱 마다 할 일(스케줄, account process, rcu, irq work, posix cpu 타이머 등)을 처리한다. 추가로 현재 cpu가 do_timer() 호출 전담인 경우jiffies를 1 증가시키고 글로벌 로드를 계산하고 wall time 등을 갱신한다

  • 코드 라인 3~10에서 현재 cpu가 do_timer() 호출 전담인 경우 다음 틱 시간을 갱신하고 jiffies를 1 증가시키고 global  load를 계산한다.
    • 32bit arm은 jiffies 증가 시 64비트인 jiffies_64를 사용하므로 접근 시에는 반드시 시퀀스 락을 사용하여야 한다.
  • 코드 라인 11에서 wall time을 갱신한다.
  • 코드 라인 14에서 cpu 처리 시간을 갱신한다.
  • 코드 라인 15에서 cpu profile 정보를 남긴다

 

do_timer()

kernel/time/timekeeping.c

/*              
 * Must hold jiffies_lock
 */
void do_timer(unsigned long ticks)
{
        jiffies_64 += ticks;
        calc_global_load(ticks);
}

jiffies 값을 요청한 ticks 만큼 증가시키고 global load를 계산한다.

 

profile_tick()

kernel/profile.c

void profile_tick(int type)
{
        struct pt_regs *regs = get_irq_regs();

        if (!user_mode(regs) && prof_cpu_mask != NULL &&
            cpumask_test_cpu(smp_processor_id(), prof_cpu_mask))
                profile_hit(type, (void *)profile_pc(regs));
}

cpu profile 정보를 남긴다.

  • 코드 라인 3에서 마지막 exception 프레임의 주소가 담긴 현재 cpu의 프레임 포인터를 알아온다.
  • 코드 라인 5~7에서 커널에서 exception되었으며 prof_cpu_mask에 현재 cpu가 포함된 경우 cpu profile 정보를 남긴다.

 

profile_hit()

include/linux/profile.h

/*
 * Single profiler hit:
 */
static inline void profile_hit(int type, void *ip)
{
        /*
         * Speedup for the common (no profiling enabled) case:
         */
        if (unlikely(prof_on == type))
                profile_hits(type, ip, 1);
}

커널이 요청 타입에 대한 profile 타입을 준비한 경우 ip 주소에 대해 profile 정보를 남긴다.

  • 요청 타입이 profiling 중인 타입이 아닌 거나 profile 버퍼가 설정되지 않은 경우 처리를 포기한다.
    • cpu, sched, sleep, kvm profiling 중 하나를 선택할 수 있다.
    • “profile=[schedule,]<number>”
      • 스케쥴 포인트에 대해 profile
      • <number>
        • step/bucket 사이즈를 2의 차수 값을 사용하며 통계 시간 기반 profile
    • “profile=sleep”
      • D-state sleeping (ms)에 대한 profile
    • “profile=kvm”
      • VM 종료에 대한 profile
  • 참고:

 

update_process_times()

kernel/time/timer.c

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
        struct task_struct *p = current;

        /* Note: this timer irq context must be accounted for as well. */
        account_process_tick(p, user_tick);
        run_local_timers();
        rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
        if (in_irq())
                irq_work_tick();
#endif
        scheduler_tick();
        if (IS_ENABLED(CONFIG_POSIX_TIMERS))
                run_posix_cpu_timers();
}

매 틱마다 호출되는 처리해야 하는 루틴들을 묶어놓은 함수이다.

  • 코드 라인 6에서 cpu가 처리되는 시간 비율을 측정한다. user, system 및 idle 타임으로 구분하여 측정한다
  • 코드 라인 7에서 만료된 hrtimer 및 timer 함수를 호출한다.
    • hrtimer는 periodic 모드에서만 호출된다. oneshot 모드는 별도의 hrtimer_interrupt() 핸들러에서 직접 처리한다.
    • timer는 softirq에서 처리하도록 raise 한다.
  • 코드 라인 8에서 rcu core에 대한 처리를 수행한다.
  • 코드 라인 10~11에서 enque되어 대기중인 irq_work를 처리한다.
  • 코드 라인 13에서 현재 동작중인 태스크의 스캐줄러에 스케줄 틱을 호출한다.
  • 코드 라인 14~15에서 posix cpu 타이머에 대한 처리를 수행한다.

 

run_local_timers()

kernel/time/timer.c

/*
 * Called by the local, per-CPU timer interrupt on SMP.
 */
void run_local_timers(void)
{
        struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

        hrtimer_run_queues();
        /* Raise the softirq only if required. */
        if (time_before(jiffies, base->clk)) {
                if (!IS_ENABLED(CONFIG_NO_HZ_COMMON))
                        return;
                /* CPU is awake, so check the deferrable base. */
                base++;
                if (time_before(jiffies, base->clk))
                        return;
        }
        raise_softirq(TIMER_SOFTIRQ);
}

만료된 hrtimer 및 timer를 처리한다.

  • 코드 라인 5에서 periodic 모드에서 호출된 경우 hrtimer 해시 리스트에서 대기중인 만료된 hrtimer 함수들을 처리하도록 호출한다.
  • 코드 라인 7~14에서 jiffies 까지 모든 timer가 처리된 경우 함수를 빠져나간다.
  • 코드 라인 15에서 타이머 휠을 처리하도록 softirq를 raise 한다.

 

틱 디바이스 핸들러 -2- (nohz  기반, 저해상도 타이머)

tick_nohz_handler()

kernel/time/tick-sched.c

/*
 * The nohz low res interrupt handler
 */
static void tick_nohz_handler(struct clock_event_device *dev)
{
        struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
        struct pt_regs *regs = get_irq_regs();
        ktime_t now = ktime_get();

        dev->next_event.tv64 = KTIME_MAX;

        tick_sched_do_timer(now);
        tick_sched_handle(ts, regs);

        /* No need to reprogram if we are running tickless  */
        if (unlikely(ts->tick_stopped))
                return;

        while (tick_nohz_reprogram(ts, now)) {
                now = ktime_get();
                tick_do_update_jiffies64(now);
        }
}

nohz 기반의 저해상도 타이머를 사용한 틱 인터럽트 핸들러 루틴이다.

  • 코드 라인 9에서 jiffies를 update하고 wall time을 조정한다.
  • 코드 라인 10에서 process accounting 및 profile에 관련한 일들을 처리한다.
  • 코드 라인 13~14에서 낮은 확률로 tick이 정지된 경우 함수를 빠져나간다.
  • 코드 라인 16~19에서 tick을 재프로그램한다. 실패 시 반복한다.

 

틱 디바이스 핸들러 -3- (hz/nohz  기반, 고해상도 타이머)

hrtimer를 사용하여 tick을 프로그래밍한 인터럽트로 인해 hrtimer_interrupt()가 호출되면 등록된 tick 스케쥴 핸들러에 도달한다.

tick_sched_timer()

kernel/time/tick-sched.c

/*      
 * We rearm the timer until we get disabled by the idle code.
 * Called with interrupts disabled.
 */
static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
{               
        struct tick_sched *ts =
                container_of(timer, struct tick_sched, sched_timer);
        struct pt_regs *regs = get_irq_regs();
        ktime_t now = ktime_get();

        tick_sched_do_timer(now);

        /*
         * Do not call, when we are not in irq context and have
         * no valid regs pointer
         */
        if (regs)
                tick_sched_handle(ts, regs);
        else
                ts->next_tick = 0;

        /* No need to reprogram if we are in idle or full dynticks mode */
        if (unlikely(ts->tick_stopped))
                return HRTIMER_NORESTART;

        hrtimer_forward(timer, now, tick_period);

        return HRTIMER_RESTART;
}

hz/nohz 기반의 고해상도 타이머를 사용한 틱 인터럽트 핸들러 루틴이다.

  • 코드 라인 8에서 jiffies를 update하고 wall time을 조정한다.
  • 코드 라인 14~17에서 process accounting 및 profile에 관련한 일들을 처리한다.
  • 코드 라인 20~21에서 틱 스케쥴러에서 틱을 멈춰달라는 요청이 있는 경우 HRTIMER_NORESTART를 결과로 함수를 빠져나간다.
  • 코드 라인 23~25에서 틱을 다시 프로그램하고 HRTIMER_RESTART를 결과로 함수를 빠져나간다.

 


Tick 디바이스의 Oneshot 모드 전환

periodic 틱에서 매번 oneshot 준비 상태 확인

틱 디바이스가 periodic 모드로 동작할 때 매 tick 인터럽트마다 호출되어 처리되는 함수 중 update_process_times() -> run_local_timers() -> hrtimer_run_queues() 함수 내에서 periodic 모드용 hrtimer(for hardirq)를 처리한다. hrtimer_run_queues() 함수 내부에서 high-resolution hw 타이머가 준비되어 틱 모드를 oneshot으로 전환할 수 있는지 여부를 체크하는 tick_check_oneshot_change() 함수를 통해 oneshot으로 전환된다. 이렇게 oneshot 모드 전환되면 hrtimer 처리 경로가 바뀌게 된다.

 

hrtimer_run_queues()

kernel/time/hrtimer.c

/*
 * Called from run_local_timers in hardirq context every jiffy
 */
void hrtimer_run_queues(void)
{
        struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
        unsigned long flags;
        ktime_t now;

        if (__hrtimer_hres_active(cpu_base))
                return;

        /*
         * This _is_ ugly: We have to check periodically, whether we
         * can switch to highres and / or nohz mode. The clocksource
         * switch happens with xtime_lock held. Notification from
         * there only sets the check bit in the tick_oneshot code,
         * otherwise we might deadlock vs. xtime_lock.
         */
        if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) {
                hrtimer_switch_to_hres();
                return;
        }

        raw_spin_lock_irqsave(&cpu_base->lock, flags);
        now = hrtimer_update_base(cpu_base);

        if (!ktime_before(now, cpu_base->softirq_expires_next)) {
                cpu_base->softirq_expires_next = KTIME_MAX;
                cpu_base->softirq_activated = 1;
                raise_softirq_irqoff(HRTIMER_SOFTIRQ);
        }

        __hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);
        raw_spin_unlock_irqrestore(&cpu_base->lock, flags);
}

hrtimer 해시 리스트에서 대기중인 만료된 hrtimer를 처리한다. 이 루틴은 periodic 틱에서만 동작한다.

  • 코드 라인 7~8에서 oneshot 모드로 전환하여 이미 hres 타이머가 active된 경우 함수를 빠져나간다.
  • 코드 라인 17~20에서 oneshot 모드로 전환한다. 만일 hres 타이머를 사용할 수 있는 경우 hres 타이머용 oneshot/nohz 모드로 전환한다.
    • oneshot 모드 전환 시 두 가지 모드 지원
      • low-resolution 타이머용 nohz 전환
      • high-reslution 타이머용 nohz 전환
  • 코드 라인 23에서 periodic 모드인 경우에만 처리되는데 현재 cpu의 hrtimer 시각을 알아온다.
  • 코드 라인 25~29에서 softirq용 hrtimer가 만료된 경우 이를 처리하도록 softirq를 raise 한다.
  • 코드 라인 31에서 hardirq용 hrtimer에 대한 처리를 수행한다.

 

hrtimer_hres_active()

kernel/time/hrtimer.c

static inline int hrtimer_hres_active(void)
{
        return __hrtimer_hres_active(this_cpu_ptr(&hrtimer_bases));
}

현재 cpu의 hrtimer가 고해상도 모드로 동작중인지 여부를 반환한다.

 

/*
 * Is the high resolution mode active ?
 */
static inline int __hrtimer_hres_active(struct hrtimer_cpu_base *cpu_base) 
{
        return IS_ENABLED(CONFIG_HIGH_RES_TIMERS) ?
                cpu_base->hres_active : 0; 
}

@cpu_base의 hrtimer가 고해상도 모드로 동작중인지 여부를 반환한다.

 

hrtimer_is_hres_enabled()

kernel/time/hrtimer.c

/*
 * hrtimer_high_res_enabled - query, if the highres mode is enabled
 */
static inline int hrtimer_is_hres_enabled(void) 
{
        return hrtimer_hres_enabled;
}

고해상도모드의 hrtimer가 enable되어 있는지 여부를 반환한다.

  • 디폴트 값으로 on되어 있고 “highres=off” 커널 파라메터를 사용하여 disable할 수 있다.

tick_check_oneshot_change()

kernel/time/tick-sched.c

/**
 * Check, if a change happened, which makes oneshot possible.
 *
 * Called cyclic from the hrtimer softirq (driven by the timer
 * softirq) allow_nohz signals, that we can switch into low-res nohz
 * mode, because high resolution timers are disabled (either compile
 * or runtime). Called with interrupts disabled.
 */
int tick_check_oneshot_change(int allow_nohz)
{
        struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);

        if (!test_and_clear_bit(0, &ts->check_clocks))
                return 0;

        if (ts->nohz_mode != NOHZ_MODE_INACTIVE)
                return 0;

        if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
                return 0;

        if (!allow_nohz)
                return 1;

        tick_nohz_switch_to_nohz();
        return 0;
}

oneshot 모드로의 변경이 일어난 경우 체크한다.

  • 코드 라인 5~6에서 tick_sched의 check_clocks에서 0번 cpu에 해당하는 비트가 설정된 경우 클리어한다. 설정되지 않은 경우 0을 반환한다.
  • 코드 라인 8~9에서 nohz 모드가 활성화되지 않은 경우 0을 반환한다.
  • 코드 라인 11~12에서 timekeeping에 사용하는 클럭 이벤트 소스의 플래그에 valid_for_hres가 설정되지 않았거나 틱 이벤트 디바이스가 oneshot 모드가 아닌 경우 0을 반환한다.
  • 코드 라인 14~15에서 인수 allow_nohz가 0인 경우 1을 반환한다.
  • 코드 라인 17~18에서 low-resolution 타이머용 nohz 모드로 전환하고 hrtimer를 사용하여 다음 스케쥴 틱을 프로그래밍 한다.

 


oneshot 모드 및 low-resolution nohz 전환

tick_nohz_switch_to_nohz()

kernel/time/tick-sched.c

/**
 * tick_nohz_switch_to_nohz - switch to nohz mode
 */
static void tick_nohz_switch_to_nohz(void)
{
        struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
        ktime_t next;

        if (!tick_nohz_enabled)
                return;

        if (tick_switch_to_oneshot(tick_nohz_handler))
                return;

        /*
         * Recycle the hrtimer in ts, so we can share the
         * hrtimer_forward with the highres code.
         */
        hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
        /* Get the next period */
        next = tick_init_jiffy_update();

        hrtimer_set_expires(&ts->sched_timer, next);
        hrtimer_forward_now(&ts->sched_timer, tick_period);
        tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
        tick_nohz_activate(ts, NOHZ_MODE_LOWRES);
}

oneshot 및 nohz 모드로 전환하고 hrtimer를 사용하여 다음 스케쥴 틱을 프로그래밍 한다.

  • 코드 라인 6~7에서 nohz가 enable되어 있지 않은 경우 함수를 빠져나간다.
  • 코드 라인 9~10에서 틱 디바이스를 oneshot 모드로 전환시키고 처리할 핸들러를 준비한다. 만일 전환이 실패하는 경우 함수를 빠져나간다.
  • 코드 라인 16~22에서 hrtimer를 사용하여 1 jiffies 틱을 프로그램한다.
  • 코드 라인 23에서 nohz 모드가 저해상도 타이머 모드로 동작됨을 틱 sched에 표시한다.

 

tick_switch_to_oneshot()

kernel/time/tick-oneshot.c

/**
 * tick_switch_to_oneshot - switch to oneshot mode
 */
int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
        struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
        struct clock_event_device *dev = td->evtdev;

        if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT) ||
                    !tick_device_is_functional(dev)) {

                printk(KERN_INFO "Clockevents: "
                       "could not switch to one-shot mode:");
                if (!dev) {
                        printk(" no tick device\n");
                } else {
                        if (!tick_device_is_functional(dev))
                                printk(" %s is not functional.\n", dev->name);
                        else
                                printk(" %s does not support one-shot mode.\n",
                                       dev->name);
                }
                return -EINVAL;
        }

        td->mode = TICKDEV_MODE_ONESHOT;
        dev->event_handler = handler;
        clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);
        tick_broadcast_switch_to_oneshot();
        return 0;
}

틱 디바이스를 oneshot 모드로 전환시키고 처리할 핸들러를 준비한다.

  • 이 함수는 다음 두 군데에서 호출되어 사용된다.
    • 고해상도 타이머가 필요할 때 hrtimer_switch_to_hres() 함수 내부의 tick_init_hres() 함수에서 사용된다.
    • dynamic tick을 처리하기 위해 nohz 모드로 전환하기 위해  tick_nohz_switch_to_nohz() 함수에서 사용된다.

 

  • 코드 라인 6~21에서 clock_event_device가 준비되지 않았거나 oneshot 모드로 동작하지 않는 경우 에러 메시지를 출력하고 -EINVAL 에러를 반환한다.
  • 코드 라인 23~25에서 인수로 받은 이벤트 핸들러를 디바이스에 대입한다. 또한 clock_event_device의 set_mode() 후크를 통해 드라이버에서 oneshot 모드를 설정하게 한다.
    • rpi2: arch_timer_set_mode_virt() 호출되는데 shutdown 모드가 아닌 oneshot 모드에서는 특별히 설정하는 것이 없다.
  • 코드 라인 26에서 tick broadcast 디바이스를 위해 oneshot 모드로 변경한다.

 

tick_broadcast_switch_to_oneshot()

kernel/time/tick-broadcast.c

/*  
 * Select oneshot operating mode for the broadcast device
 */
void tick_broadcast_switch_to_oneshot(void)
{       
        struct clock_event_device *bc;
        unsigned long flags;
        
        raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

        tick_broadcast_device.mode = TICKDEV_MODE_ONESHOT;
        bc = tick_broadcast_device.evtdev;
        if (bc)
                tick_broadcast_setup_oneshot(bc);

        raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}

tick broadcast 디바이스의 모드를 oneshot 모드로 바꾸고 tick broadcast 디바이스의 클럭 이벤트 디바이스가 준비된 경우 다음 tick을 설정한다.

 


oneshot 모드 및 high-resolution nohz 전환

hrtimer_switch_to_hres()

kernel/time/hrtimer.c

/*
 * Switch to high resolution mode
 */
static void hrtimer_switch_to_hres(void)
{
        struct hrtimer_cpu_base *base = this_cpu_ptr(&hrtimer_bases);

        if (tick_init_highres()) {
                pr_warn("Could not switch to high resolution mode on CPU %u\n",
                        base->cpu);
                return;
        }
        base->hres_active = 1;
        hrtimer_resolution = HIGH_RES_NSEC;

        tick_setup_sched_timer();
        /* "Retrigger" the interrupt to get things going */
        retrigger_next_event(NULL);
}

hrtimer를 high resolution 모드로 설정하고 스케쥴 틱 타이머를 동작시킨다.

  • 코드 라인 5~9에서 틱 디바이스를 oneshot 모드로 전환한다. 전환되지 않는 경우 경고 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 10~11에서 hrtimer가 high resolution 상태임을 설정하고 4 개 클럭의 해상도에 1ns를 대입한다.
  • 코드 라인 13에서 스케줄 틱을 설정한다.
  • 코드 라인 15에서 hrtimer를 리프로그램하고 1을 반환한다.

 

tick_init_highres()

kernel/time/tick-oneshot.c

/**
 * tick_init_highres - switch to high resolution mode
 *
 * Called with interrupts disabled.
 */
int tick_init_highres(void)
{
        return tick_switch_to_oneshot(hrtimer_interrupt);
}

고해상도 모드의 oneshot 타이머를 동작시키고 인터럽트 발생 시 hrtimer 인터럽트 핸들러 콜백 함수인 hrtimer_interrupt() 함수를 호출하게 한다.

 

고해상도 hrtimer를 사용한 Sched Tick 설정

tick_setup_sched_timer()

kernel/time/tick-sched.c

/**
 * tick_setup_sched_timer - setup the tick emulation timer
 */
void tick_setup_sched_timer(void)
{
        struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
        ktime_t now = ktime_get();

        /*
         * Emulate tick processing via per-CPU hrtimers:
         */
        hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
        ts->sched_timer.function = tick_sched_timer;

        /* Get the next period (per-CPU) */
        hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update());

        /* Offset the tick to avert jiffies_lock contention. */
        if (sched_skew_tick) {
                u64 offset = ktime_to_ns(tick_period) >> 1;
                do_div(offset, num_possible_cpus());
                offset *= smp_processor_id();
                hrtimer_add_expires_ns(&ts->sched_timer, offset);
        }

        hrtimer_forward(&ts->sched_timer, now, tick_period);
        hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED_HARD);
        tick_nohz_activate(ts, NOHZ_MODE_HIGHRES);
}

고해상도 hrtimer를 사용하여 틱 스케쥴 타이머를 가동한다. 또한 nohz 모드로 설정한다.

  • 코드 라인 9~10에서 틱 스케쥴에서 사용할 hrtimer를 초기화하고 핸들러 함수를 지정한다.
    • tick_sched_timer()에서 jiffies 관리 cpu를 지정하고, 매번 틱을 프로그램한다.
  • 코드 라인 13에서 틱 스케쥴용 hrtimer의 만료 시간은 1 jiffies 기간으로 한다.
  • 코드 라인 16~21에서 jiffies lock 혼잡을 피하기 위해 cpu 마다 offset을 산출하고 추가하여 틱 만료시간을 재설정한다.
  • 코드 라인 23~24에서 hrtimer를 forward한 후 프로그램한다.
  • 코드 라인 25에서 nohz 모드를 highres로 변경하고 active 되었음을 설정한다.

 

oneshot 틱 프로그램

tick_program_event()

kernel/time/tick-oneshot.c

/**
 * tick_program_event
 */
int tick_program_event(ktime_t expires, int force)
{
        struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);

        return clockevents_program_event(dev, expires, force);
}

요청한 만료 시간에 틱이 발생하도록 해당 cpu의 클럭 이벤트 디바이스에 프로그램한다.

 


틱 브로드캐스트 디바이스 핸들러

tick_handle_periodic_broadcast()

kernel/time/tick-broadcast.c

/*
 * Event handler for periodic broadcast ticks
 */
static void tick_handle_periodic_broadcast(struct clock_event_device *dev)
{
        struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
        bool bc_local;

        raw_spin_lock(&tick_broadcast_lock);

        /* Handle spurious interrupts gracefully */
        if (clockevent_state_shutdown(tick_broadcast_device.evtdev)) {
                raw_spin_unlock(&tick_broadcast_lock);
                return;
        }

        bc_local = tick_do_periodic_broadcast();

        if (clockevent_state_oneshot(dev)) {
                ktime_t next = ktime_add(dev->next_event, tick_period);

                clockevents_program_event(dev, next, true);
        }
        raw_spin_unlock(&tick_broadcast_lock);

        /*
         * We run the handler of the local cpu after dropping
         * tick_broadcast_lock because the handler might deadlock when
         * trying to switch to oneshot mode.
         */
        if (bc_local)
                td->evtdev->event_handler(td->evtdev);
}

브로드캐스트가 필요한 cpu를 대상으로 브로드캐스트하여 cpu를 idle 상태에서 깨운다. 클럭 이벤트 디바이스의 모드가 oneshot인 경우 틱을 프로그램한다.

  • 코드 라인 9~12에서 브로드 캐스트에 사용할 클럭 이벤트 디바이스가 shutdown된 상태라면 함수를 빠져나간다.
  • 코드 라인 14에서 브로드캐스트가 필요한 cpu를 대상으로 브로드캐스트하여 cpu를 idle 상태에서 깨운다.
  • 코드 라인 16~20에서 클럭 이벤트 디바이스가 oneshot 모드인 경우 틱을 프로그래밍한다.
  • 코드 라인 28~29에서 브로드 캐스트용 이벤트 핸들러를 호출한다.

 

다음 그림은 nohz idle 상태에 있는 cpu#1~#3 들을 깨우도록 브로드캐스트 하는 과정을 보여준다.

  • 틱 브로드캐스트 디바이스가 periodic 모드로 동작

 

tick_handle_oneshot_broadcast()

kernel/time/tick-broadcast.c

/*
 * Handle oneshot mode broadcasting
 */
static void tick_handle_oneshot_broadcast(struct clock_event_device *dev)
{
        struct tick_device *td;
        ktime_t now, next_event;
        int cpu, next_cpu = 0;
        bool bc_local;

        raw_spin_lock(&tick_broadcast_lock);
        dev->next_event = KTIME_MAX;
        next_event = KTIME_MAX;
        cpumask_clear(tmpmask);
        now = ktime_get();
        /* Find all expired events */
        for_each_cpu(cpu, tick_broadcast_oneshot_mask) {
                /*
                 * Required for !SMP because for_each_cpu() reports
                 * unconditionally CPU0 as set on UP kernels.
                 */
                if (!IS_ENABLED(CONFIG_SMP) &&
                    cpumask_empty(tick_broadcast_oneshot_mask))
                        break;

                td = &per_cpu(tick_cpu_device, cpu);
                if (td->evtdev->next_event <= now) {
                        cpumask_set_cpu(cpu, tmpmask);
                        /*
                         * Mark the remote cpu in the pending mask, so
                         * it can avoid reprogramming the cpu local
                         * timer in tick_broadcast_oneshot_control().
                         */
                        cpumask_set_cpu(cpu, tick_broadcast_pending_mask);
                } else if (td->evtdev->next_event < next_event) {
                        next_event = td->evtdev->next_event;
                        next_cpu = cpu;
                }
        }

        /*
         * Remove the current cpu from the pending mask. The event is
         * delivered immediately in tick_do_broadcast() !
         */
        cpumask_clear_cpu(smp_processor_id(), tick_broadcast_pending_mask);

        /* Take care of enforced broadcast requests */
        cpumask_or(tmpmask, tmpmask, tick_broadcast_force_mask);
        cpumask_clear(tick_broadcast_force_mask);

        /*
         * Sanity check. Catch the case where we try to broadcast to
         * offline cpus.
         */
        if (WARN_ON_ONCE(!cpumask_subset(tmpmask, cpu_online_mask)))
                cpumask_and(tmpmask, tmpmask, cpu_online_mask);

        /*
         * Wakeup the cpus which have an expired event.
         */
        bc_local = tick_do_broadcast(tmpmask);

        /*
         * Two reasons for reprogram:
         *
         * - The global event did not expire any CPU local
         * events. This happens in dyntick mode, as the maximum PIT
         * delta is quite small.
         *
         * - There are pending events on sleeping CPUs which were not
         * in the event mask
         */
        if (next_event != KTIME_MAX)
                tick_broadcast_set_event(dev, next_cpu, next_event);

        raw_spin_unlock(&tick_broadcast_lock);

        if (bc_local) {
                td = this_cpu_ptr(&tick_cpu_device);
                td->evtdev->event_handler(td->evtdev);
        }
}

브로드캐스트가 필요한 cpu를 대상으로 브로드캐스트하여 cpu를 idle 상태에서 깨운다. 클럭 이벤트 디바이스의 모드가 oneshot인 경우 틱을 프로그램한다.

  • 코드 라인 14~36에서 브로드 캐스트가 필요한 cpu들에 대해 틱 만료된 cpu들을 tmp 마스크에 알아온다.
  • 코드 라인 42~46에서 현재 cpu는 pending 마스크에서 제거하고 tmp 마스크 대상에 force 마스크를 추가한다.
  • 코드 라인 52~53에서 tmp 마스크에서 online된 cpu들은 제거한다.
  • 코드 라인 58에서 tmp 마스크를 대상으로 브로드캐스트를 수행하여 idle 상태에 있는 cpu들을 모두 깨운다.
  • 코드 라인 70~71에서 남아있는 이벤트가 있는 경우 브로드캐스트 디바이스로 사용되는 클럭 이벤트 디바이스의 모드를 oneshot으로 변경하고 프로그램한다.
  • 코드 라인 75~78에서 등록된 브로드 캐스트용 이벤트 핸들러를 호출한다.

 

다음 그림은 nohz idle 상태에 있는 cpu#1~#3 들을 깨우도록 브로드캐스트 하는 과정을 보여준다.

  • 틱 브로드캐스트 디바이스가 oneshot 모드로 동작

 

다음 그림은 nohz를 사용하여 idle 상태에 있는 3개의 cpu가 브로드캐스트에 의해 각 만료시간마다 깨어나는 과정을 보여준다.

  • 별표와 같이 프로그램된 틱들이 브로드캐스트를 발송하는 cpu의 틱 타임에 맞춰 약간씩 지연되는 모습을 볼 수 있다.

 

tick_broadcast_set_event()

kernel/time/tick-broadcast.c

static void tick_broadcast_set_event(struct clock_event_device *bc, int cpu,
                                     ktime_t expires)
{
        if (!clockevent_state_oneshot(bc))
                clockevents_switch_state(bc, CLOCK_EVT_STATE_ONESHOT);

        clockevents_program_event(bc, expires, 1);
        tick_broadcast_set_affinity(bc, cpumask_of(cpu));
}

틱 브로드캐스트 디바이스에서 사용되는 클럭 이벤트 디바이스의 모드를 oneshot으로 변경하고 프로그램 한다. 만일 실패하는 경우 요청 cpu로 irq affinity를 설정한다.

 

tick_broadcast_set_affinity()

kernel/time/tick-broadcast.c

/*
 * Set broadcast interrupt affinity
 */
static void tick_broadcast_set_affinity(struct clock_event_device *bc,
                                        const struct cpumask *cpumask)
{
        if (!(bc->features & CLOCK_EVT_FEAT_DYNIRQ))
                return;

        if (cpumask_equal(bc->cpumask, cpumask))
                return;

        bc->cpumask = cpumask;
        irq_set_affinity(bc->irq, bc->cpumask);
}

클럭 이벤트 디바이스의 인터럽트를 요청한 cpu 들이 수신할 수 있도록 설정한다. 단 FEAT_DYNIRQ 기능이 없는 클럭 이벤트 디바이스는 수행할 수 없다.

 


C3Stop과 틱 브로드캐스트

타이머의 절전 기능이 구현된 SoC 즉, c3stop이 설정되었거나 per-cpu 타이머가 없는 경우 cpu가 deep idle 상태에 진입하고 나갈 때 마다 전역 비트맵에 알림 요청을 하고 다른 cpu에서 deep idle 상태의 cpu들을 broadcast 기능을 통해 깨울 수 있다.

 

cpuidle_setup_broadcast_timer()

drivers/cpuidle/driver.c

/**
 * cpuidle_setup_broadcast_timer - enable/disable the broadcast timer on a cpu
 * @arg: a void pointer used to match the SMP cross call API
 *
 * If @arg is NULL broadcast is disabled otherwise enabled
 *
 * This function is executed per CPU by an SMP cross call.  It's not
 * supposed to be called directly.
 */
static void cpuidle_setup_broadcast_timer(void *arg)
{
        if (arg)
                tick_broadcast_enable();
        else
                tick_broadcast_disable();
}

틱 브로드캐스트 기능을 켜거나 끈다.

 

tick_broadcast_enable()

include/linux/tick.h

static inline void tick_broadcast_enable(void)
{
        tick_broadcast_control(TICK_BROADCAST_ON);
}

틱 브로드캐스트 기능을 켠다.

 

tick_broadcast_disable()

include/linux/tick.h

static inline void tick_broadcast_disable(void)
{
        tick_broadcast_control(TICK_BROADCAST_OFF);
}

틱 브로드캐스트 기능을 끈다.

 

tick_broadcast_control()

kernel/time/tick-broadcast.c

/**
 * tick_broadcast_control - Enable/disable or force broadcast mode
 * @mode:       The selected broadcast mode
 *
 * Called when the system enters a state where affected tick devices
 * might stop. Note: TICK_BROADCAST_FORCE cannot be undone.
 */
void tick_broadcast_control(enum tick_broadcast_mode mode)
{
        struct clock_event_device *bc, *dev;
        struct tick_device *td;
        int cpu, bc_stopped;
        unsigned long flags;

        /* Protects also the local clockevent device. */
        raw_spin_lock_irqsave(&tick_broadcast_lock, flags);
        td = this_cpu_ptr(&tick_cpu_device);
        dev = td->evtdev;

        /*
         * Is the device not affected by the powerstate ?
         */
        if (!dev || !(dev->features & CLOCK_EVT_FEAT_C3STOP))
                goto out;

        if (!tick_device_is_functional(dev))
                goto out;

        cpu = smp_processor_id();
        bc = tick_broadcast_device.evtdev;
        bc_stopped = cpumask_empty(tick_broadcast_mask);

        switch (mode) {
        case TICK_BROADCAST_FORCE:
                tick_broadcast_forced = 1;
                /* fall through */
        case TICK_BROADCAST_ON:
                cpumask_set_cpu(cpu, tick_broadcast_on);
                if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_mask)) {
                        /*
                         * Only shutdown the cpu local device, if:
                         *
                         * - the broadcast device exists
                         * - the broadcast device is not a hrtimer based one
                         * - the broadcast device is in periodic mode to
                         *   avoid a hickup during switch to oneshot mode
                         */
                        if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER) &&
                            tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
                                clockevents_shutdown(dev);
                }
                break;

        case TICK_BROADCAST_OFF:
                if (tick_broadcast_forced)
                        break;
                cpumask_clear_cpu(cpu, tick_broadcast_on);
                if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_mask)) {
                        if (tick_broadcast_device.mode ==
                            TICKDEV_MODE_PERIODIC)
                                tick_setup_periodic(dev, 0);
                }
                break;
        }

        if (bc) {
                if (cpumask_empty(tick_broadcast_mask)) {
                        if (!bc_stopped)
                                clockevents_shutdown(bc);
                } else if (bc_stopped) {
                        if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
                                tick_broadcast_start_periodic(bc);
                        else
                                tick_broadcast_setup_oneshot(bc);
                }
        }
out:
        raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}
EXPORT_SYMBOL_GPL(tick_broadcast_control);

현재 cpu의 상태 변화에 따른 브로드캐스트 여부를 결정할 수 있는 비트마스크 설정을 한다.

  • 코드 라인 16~17에서 c3stop 기능이 없는 경우 함수를 빠져나간다.
  • 코드 라인 19~20에서 기능이 구현되지 않은 dummy 디바이스인 경우 함수를 빠져나간다.
  • 코드 라인 30~45에서 broadcast on 요청을 받은 경우 tick_broadcast_on 및 tick_broadcast_mask의 현재 cpu에 해당하는 비트를 설정한다. 만일 틱 브로드캐스트 모드가 periodic 이면서 tick이 발생중이면 tick을 정지시킨다.
  • 코드 라인 47~56에서 broadcast off 요청을 받은 경우 tick_broadcast_on 및 tick_broadcast_mask의 현재 cpu에 해당하는 비트를 클리어한다. 만일 틱 브토드캐스트 모드가 periodic이면서 tick이 발생하지 않는 상태인 경우 tick을 발생시키도록 요청한다.
  • 코드 라인 60~62에서 브로드캐스트할 cpu가 없는 경우 클럭이벤트 디바이스를 shutdown 한다.
  • 코드 라인 63~68에서 브로드캐스트할 cpu가 있는 경우 틱 브로드캐스트 모드에 따라 periodic 또는 oneshot 모드로 설정한다.

 

다음 그림은 틱 브로드 캐스트를 켜고 끌 때 비트마스크들의 상황을 보여준다.

 


스케줄 틱 분석

먼저 타이머 인터럽트의 처리 경로를 다시 한 번 살펴보면 여러 개의 경로 중 rpi2의 경우 다음 타이머 인터럽트의 처리 경로가 있다.

  • tick_handle_periodic()
    • 루틴내에서 bottom-half 처리를 목적으로 softirq를 사용하여 타이머휠에 대한 처리를 수행한다. (run_timer_softirq())
  • hrtimer_interrupt() → __run_hrtimer() → tick_sched_timer()
    • hrtimer를 사용한 softirq는 커널 4.2.rc1부터 사용하지 않는다.

 

다음 그림은 스케줄틱이 처음 tick_handle_periodic() 함수로 처리되다가 hrtimer가 준비되면 tick_sched_timer() 함수로 이전되는 것을 보여준다.

 

nohz 진출입 함수

  • tick_nohz_idle_enter()
  • tick_nohz_idle_exit()

 

SMP 시스템에서 타이머 인터럽트가 발생되는 상황에 대해 다음 컴포넌트들의 상태와 비교하여 살펴보자

  • cpu 마다 스케줄 틱은 조금씩 어긋나게 프로그램된다.
  • 타이머 인터럽트를 발생시키는 틱 디바이스 상태 변화
    • SMP 코어 중 하나는 jiffies를 규칙적으로 발생하므로 nohz로 진입할 수 없다. 그 외의 cpu들은 nohz 상태에 진입가능하다.
  • 타이머를 프로그램하는 클럭이벤트디바이스 상태 변화
    • armv7 & armv8에 내장된 아키텍처 타이머는 oneshot으로만 프로그래밍 가능하므로 매번 CONFIG_HZ 단위로 틱을 프로그래밍해야 한다.

 

구조체

tick_device 구조체

kernel/time/tick-sched.h

struct tick_device {
        struct clock_event_device *evtdev;
        enum tick_device_mode mode;
};
  • *evtdev
    • 틱 디바이스로 사용하는 클럭 이벤트 디바이스
  • mode
    • TICKDEV_MODE_PERIODIC
      • nohz 모드를 사용하지 못하는 틱 디바이스 또는 부트업 과정
    • TICKDEV_MODE_ONESHOT
      • nohz 모드를 사용할 수 있는 틱 디바이스(possible)

 

tick_sched 구조체

kernel/time/tick-sched.h

/**
 * struct tick_sched - sched tick emulation and no idle tick control/stats
 * @sched_timer:        hrtimer to schedule the periodic tick in high
 *                      resolution mode
 * @check_clocks:       Notification mechanism about clocksource changes
 * @nohz_mode:          Mode - one state of tick_nohz_mode
 * @inidle:             Indicator that the CPU is in the tick idle mode
 * @tick_stopped:       Indicator that the idle tick has been stopped
 * @idle_active:        Indicator that the CPU is actively in the tick idle mode;
 *                      it is resetted during irq handling phases.
 * @do_timer_lst:       CPU was the last one doing do_timer before going idle
 * @got_idle_tick:      Tick timer function has run with @inidle set
 * @last_tick:          Store the last tick expiry time when the tick
 *                      timer is modified for nohz sleeps. This is necessary
 *                      to resume the tick timer operation in the timeline
 *                      when the CPU returns from nohz sleep.
 * @next_tick:          Next tick to be fired when in dynticks mode.
 * @idle_jiffies:       jiffies at the entry to idle for idle time accounting
 * @idle_calls:         Total number of idle calls
 * @idle_sleeps:        Number of idle calls, where the sched tick was stopped
 * @idle_entrytime:     Time when the idle call was entered
 * @idle_waketime:      Time when the idle was interrupted
 * @idle_exittime:      Time when the idle state was left
 * @idle_sleeptime:     Sum of the time slept in idle with sched tick stopped
 * @iowait_sleeptime:   Sum of the time slept in idle with sched tick stopped, with IO outstanding
 * @timer_expires:      Anticipated timer expiration time (in case sched tick is stopped)
 * @timer_expires_base: Base time clock monotonic for @timer_expires
 * @next_timer:         Expiry time of next expiring timer for debugging purpose only
 * @tick_dep_mask:      Tick dependency mask - is set, if someone needs the tick
 */
struct tick_sched {
        struct hrtimer                  sched_timer;
        unsigned long                   check_clocks;
        enum tick_nohz_mode             nohz_mode;

        unsigned int                    inidle          : 1;
        unsigned int                    tick_stopped    : 1;
        unsigned int                    idle_active     : 1;
        unsigned int                    do_timer_last   : 1;
        unsigned int                    got_idle_tick   : 1;

        ktime_t                         last_tick;
        ktime_t                         next_tick;
        unsigned long                   idle_jiffies;
        unsigned long                   idle_calls;
        unsigned long                   idle_sleeps;
        ktime_t                         idle_entrytime;
        ktime_t                         idle_waketime;
        ktime_t                         idle_exittime;
        ktime_t                         idle_sleeptime;
        ktime_t                         iowait_sleeptime;
        unsigned long                   last_jiffies;
        u64                             timer_expires;
        u64                             timer_expires_base;
        u64                             next_timer;
        ktime_t                         idle_expires;
        atomic_t                        tick_dep_mask;
};
  • sched_timer
    • hres timer가 활성된 이후 스케줄 틱을 발생하는 hrtimer
  • check_clocks
    • 클럭 소스의 변화가 있거나 틱 디바이스 모드가 변경될 때 1로 설정된다.
  • nohz_mode
    • NOHZ_MODE_INACTIVE
      • nohz 모드가 동작하지 않는 경우
    • NOHZ_MODE_LOWRES
      • low-resolution 타이머를 사용하는 nohz 모드
    • NOHZ_MODE_HIGHRES
      • high-resolution 타이머를 사용하는 nohz 모드
  • inidle:1
    • nohz idle 진입 여부
    • tick_nohz_idle_enter() 함수에서 1로 설정하고, tick_nohz_idle_exit() 함수에서 0으로 클리어한다.
  • tick_stopped:1
    • nohz로 인해 tick이 멈춘 상태 여부 (nohz idle & nohz full)
  • idle_active:1
    • cpu가 틱 idle 모드인지 여부
  • do_timer_last:1
    • idle 진입 전 do_timer() 함수를 수행했었는지 여부
  • last_tick
    • 마지막 프로그래밍된 틱 시간(ns)
    • nohz를 멈추고 다시 periodic 틱을 발생 시킬 때 기억해둔 이 틱 시간 이후로 틱을 forward하여 사용한다.
  • idle_expires
    • nohz idle 진입 시 nohz 만료 예정 시각(ns)

 

참고

 

댓글 남기기