ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Timer - Dynamic tick(tick sched)
    Linux/kernel 2023. 9. 8. 03:22

    글의 참고

    - https://lwn.net/Kernel/Index/#Dynamic_tick

    - http://www.wowotech.net/timer_subsystem/tick-device-layer.html

    - https://0xax.gitbooks.io/linux-insides/content/Timers/linux-timers-3.html

    - https://elinux.org/Kernel_Timer_Systems

    - https://people.eecs.berkeley.edu/~culler/cs262b/sp09/papers/maximum_tickless2007.pdf

    - http://events17.linuxfoundation.org/sites/events/files/slides/Timekeeping%20in%20the%20Linux%20Kernel_0.pdf

    - https://blog.csdn.net/u011279649/article/details/46041621

    - https://blog.csdn.net/droidphone/article/details/8112948

    - https://blog.csdn.net/Roland_Sun/article/details/106412763

    - https://zhuanlan.zhihu.com/p/449883327

    - https://kernel.meizu.com/2018/07/12//linux-time.html/


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    : 시스템의 타이머 동작 모드가 `high-resolution mode` 로 변경되면, 기존에 사용되던 `low-resolution mode`는 더 이상 동작하지 않게된다. 즉, `tick` 기반의 동작을 할 수 없어지기 때문에, 시스템에 여러 가지 영향을 주게 된다. 예를 들어, 유저 스페이스에서 보여지는 시간들은 모두 `tick`이 발생했을 때, 업데이트되는 시간이다. 그리고, 프로세스 스케줄링 또한 당연히 `tick`을 기반으로 동작한다.

     

    : `tick simulation layer`는 기존에 `tick`을 기반으로 동작하던 시스템을 사용하지 않고 `hrtimer`을 사용하더라도, 시스템에게 마치 `tick`이 제공되는 듯한 착각을 만들어주는 레이어다. 그리고, `tick simulation layer`는 `dynamic tick`을 제공해서 CPU가 IDLE 상태에서 `tick`을 멈춰서 `전력 소비`를 줄여준다.

     

    : NO_HZ 코드는 next event 를 선정할 때, jiffies 를 기반으로 한다. 그런데, tick 은 nano-second 를 기반으로 한다. 즉, tick은 nano-second 단위로 증가한다. 만약, high-res 을 사용할 수 없는 환경이라면, nano-second 를 jiffies 로 컨버팅해야 하는 작업이 진행해야 한다[참고1].

     

    : tickless 는 `tick-sched.c` 파일을 기반으로 한다. 이 파일이 처음 만들어졌을 때, 패치 내용은 다음과 같다. low-res(tick based)와 high-res(dyntick)는 tick-sched 를 통해서 각각 jiffies 를 관리하고, 더 긴 idle period 를 가질 수 있다.

    [PATCH] tick-management: dyntick / highres functionality
    With Ingo Molnar <mingo@elte.hu>

    Add functions to provide dynamic ticks and high resolution timers. The code which keeps track of jiffies and handles the long idle periods is shared between tick based and high resolution timer based dynticks. The dyntick functionality can be disabled on the kernel commandline. Provide also the infrastructure to support high resolution timers.

    - 참고 : https://github.com/torvalds/linux/commit/79bf2bb335b85db25d27421c798595a2fa2a0e82

     

    : 그리고, Kconfig 의 `help` 설명은 다음과 같다.

    This option enables a tickless system: timer interrupts will only trigger on an as-needed basis both when the system is busy and when the system is idle.

    - 참고 : https://github.com/torvalds/linux/commit/79bf2bb335b85db25d27421c798595a2fa2a0e82#diff-80cb6e2014679a0feb31cdb85d5d68a37e0b3f6d4db0dc963f3571c0f953279b

    : 즉, `tick-sched`는 시스템에 요청에 따라, low-res 와 high-res 를 적절하게 사용한다는 것이다. 예를 들어,  tick-sched는 시스템이 NORMAL 할 때는 period 방식을 사용하다가, IDLE 상태로 바뀌면, dyntick 으로 바꾼다.

     

     

    : 2007 년도 글이지만, dynamic tick 을 통해 인터럽트 발생 횟수가 17배 가까이 줄고, `CPU idle residency` 가 15 배나 증가한것을 확인할 수 있다. 


    https://people.eecs.berkeley.edu/~culler/cs262b/sp09/papers/maximum_tickless2007.pdf

     

     

    - What is dynamic tick with high-res ?

    : high-res 은 기본적으로 one-shot 을 기반으로 하며, per-CPU 타이머들이 high-res 타이머가 된다. 최신 리눅스에서 시스템 글로벌 타이머를 high-res 타이머라고 부르지 않는다. dynamic tick 은 반드시 하드웨어적으로 `one-shot mode`를 지원해야 한다. 그렇다면, high-res 을 기반으로 한 dynamic tick 은 무슨 뜻일까? SoC 측면에서 이런 경우는 2가지로 나뉜다.

    1. per-CPU 타이머가 있고, 시스템 글로벌 타이머도 있는 경우
    2. per-CPU 타이머는 있는데, 시스템 글로벌 타이머가 없는 경우

    : 먼저, 첫 번째 경우를 보자. 가장 보편화 되어있는 구조다. 브로드 캐스트 타이머로 글로벌 타이머가 등록되고, 모든 per-CPU 타이머들은 CPUIDLE 상태에서 SHOWDOWN 되는 구조다.

     

    : 두 번째는, per-CPU 타이머들만 존재하는 경우다. 이런 경우는 per-CPU 타이머 중에 하나가 브로드 캐스트 타이머가 된다. 브로드 캐스트 타이머가 될 per-CPU 타이머는 제조사에서 `CLOCK_EVT_FEAT_HRTIMER` 플래그를 설정한다.

     

     

     

    - What is dynamic tick with low-resolution ? [참고1]

    : low-resolution 에 기반한 dynamic tick 이라는 것이 뭘까? 일반적으로, 리눅스에서 low-resolution 은 periodic tick & classical timer 등으로 표현된다. 그렇다면, 주기적인 dynamic tick 이라는 말일까? 그럴 수 는 없다. dynamic tick 은 반드시 하드웨어적으로 `one-shot mode`를 지원해야 한다. 거기다가 per-CPU 타이머, 시스템 글로벌 타이머 상관없이 반드시 1개의 브로드 캐스트 타이머가 지원되어야 한다. 그렇다면, SoC 측면에서 이런 경우를 만족하는 경우는 딱 하나있다.

    1. per-CPU 타이머는 없고, 1개의 시스템 글로벌 타이머만 존재하는 경우다.
    2. 시스템 글로벌 타이머는 반드시 one-shot 모드를 지원해야 한다.

     

     

    - How to manage the jiffies in dynamic tick ?

    : `dynamic tick`은 사실 테크닉적인 용어다. 실제로는 `tickless` 라고 부른다. 그런데, 리눅스 커널의 tickless 에는 2가지 종류가 있다.

    1. tickless : CPUIDLE 상태에서는 periodic tick 을 중단한다. 대신 dynamic tick 을 받는다.
    2. full tickless : 하나의 CPU를 제외하고 나머지 CPU 들은 아예 tick 을 중단한다. dynamic tick 도 안받는다. 하나의 CPU만 RUNNABLE 하게 동작하면서, 모든 타이머 서비스를 처리한다.

    : 2개의 모드에서 각각 jiffies 를 어떻게 관리할까? tickless 같은 경우는, 불규칙적으로 tick 이 발생하기 때문에, 임의의 CPU가 wake-up 되었을 때, jiffies 를 계사한다. `jiffies += (현재 시간 - 이전 tick) / tick_period` 을 통해서 jiffies 를 계산한다. full-tickless 는 대개 boot CPU가 periodic tick 을 계속 받기 때문에, tick 때마다 jiffies 를 업데이트한다. 

     

     

    - Relationship between tick and idle ?

    : scheduler tick 과 cpudile 의 관계는 생각보다 심플하다(여기서 `scheduler tick` 은 period tick 이라고 보면 된다). 만약, wake-up 시점이 tick 보다 먼저 발생한다면 tick 을 멈추는 것은 에너지 낭비다(이 때, wake-up 은 종류를 가리지 않는다. 즉, 시스템을 깨울 수 있는 모든 wake-up 을 의미한다). 반대로, wake-up 발생 시점이 tick 보다 더 뒤에서 발생한다면, tick 을 멈춘다(지금 이 글에서 하는 얘기는 dyntick 을 사용하지 않고, period tick 을 기준으로 설명하는 것이다). 왜냐면, idle에 들어갔다는 것은 할 일이 없다는 뜻이고, tick 때문에 wake-up 되는 것을 막기 위함이다.

      wake-up time <= tick period wake-up time > tick period
    check if stopping tick X O
    Whether or not it makes sense to stop the scheduler tick in the idle loop depends on what is expected by the governor. First, if there is another (non-tick) timer due to trigger within the tick range, stopping the tick clearly would be a waste of time, even though the timer hardware may not need to be reprogrammed in that case. Second, if the governor is expecting a non-timer wakeup within the tick range, stopping the tick is not necessary and it may even be harmful. Namely, in that case the governor will select an idle state with the target residency within the time until the expected wakeup, so that state is going to be relatively shallow. The governor really cannot select a deep idle state then, as that would contradict its own expectation of a wakeup in short order. Now, if the wakeup really occurs shortly, stopping the tick would be a waste of time and in this case the timer hardware would need to be reprogrammed, which is expensive. On the other hand, if the tick is stopped and the wakeup does not occur any time soon, the hardware may spend indefinite amount of time in the shallow idle state selected by the governor, which will be a waste of energy. Hence, if the governor is expecting a wakeup of any kind within the tick range, it is better to allow the tick trigger. Otherwise, however, the governor will select a relatively deep idle state, so the tick should be stopped so that it does not wake up the CPU too early.

    - 참고 : https://www.kernel.org/doc/html/v5.4/admin-guide/pm/cpuidle.html

     

    - Data structure

    : cpudile 관련 필드들과 period tick을 one-shot으로 구현하기 때문에, nex time tick을 계산하기 위한 필드들이 주를 이룬다.

    //kernel/time/tick-sched.h - v6.5
    enum tick_nohz_mode {
    	NOHZ_MODE_INACTIVE,
    	NOHZ_MODE_LOWRES,
    	NOHZ_MODE_HIGHRES,
    };
    
    struct tick_sched {
    	/* Common flags */
    	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;
    
    	/* Tick handling: jiffies stall check */
    	unsigned int			stalled_jiffies;
    	unsigned long			last_tick_jiffies;
    
    	/* Tick handling */
    	struct hrtimer			sched_timer;
    	ktime_t				last_tick;
    	ktime_t				next_tick;
    	unsigned long			idle_jiffies;
    	ktime_t				idle_waketime;
    
    	/* Idle entry */
    	seqcount_t			idle_sleeptime_seq;
    	ktime_t				idle_entrytime;
    
    	/* Tick stop */
    	enum tick_nohz_mode		nohz_mode;
    	unsigned long			last_jiffies;
    	u64				timer_expires_base;
    	u64				timer_expires;
    	u64				next_timer;
    	ktime_t				idle_expires;
    	unsigned long			idle_calls;
    	unsigned long			idle_sleeps;
    
    	/* Idle exit */
    	ktime_t				idle_exittime;
    	ktime_t				idle_sleeptime;
    	ktime_t				iowait_sleeptime;
    
    	/* Full dynticks handling */
    	atomic_t			tick_dep_mask;
    
    	/* Clocksource changes */
    	unsigned long			check_clocks;
    };

     

    inidle : NO_HZ를 eanble / disable 과 한다고 보면 된다. tick_nohz_idle_enter / tick_nohz_idle_exit 에서 각각 트리거(`1`, `0`)된다.

    idle_active : `inidle`이 베이스로 깔려있는 상황에서 active / de-active 가 된다. 예를 들어, inidle이 `1`인 상황에서 active / de-active 를 논할 수 있다. indile이 `0`이면 active 건 de-active 건 상관없이 동작할 수 없다. tick_nohz_start_idle / tick_nohz_stop_idle 에서 각각(active, de-active) 트리거된다.

    got_idle_tick : 이 값은 tickless 상태에서 타이머 인터럽트가 발생하면, SET 된다. 그리고, 이 값이 SET 되어있다는것은, NOHZ 환경에서 타이머 인터럽트 때문에 CPU가 wake-up 됬음을 의미한다. 주로, cpuidle_reflect 에서 어떤 이유 때문에 wake-up 됬는지를 알기 위해 사용된다.

    do_timer_last : 이 필드는 이전에 시스템 jiffies 업데이트 책임자가 누구였는지를 나타낸다.

    idle_waketime : dynamic tick 을 통해 CPU 가 wake-up 됬을 때의 시점을 저장한다. 

    idle_entrytime : 얼마나 NO_HZ 상태였는지를 나타낸다. idle_active 가 1 일 때만, 유효하다.

    tick_nohz_mode : `dynamic tick` 모드에서 동작 상태를 나타낸다.
    - NOHZ_MODE_INACTIVE : tick-sched 프레임워크에 타이머 핸들러가 등록되지 않은 상태를 의미한다. 즉, 이 상태는 아직 tick-sched layer 를 사용할 수 없는 상태를 의미한다. 
    - NOHZ_MODE_LOWRES : tick-sched 가 `low-resolution`을 기반으로 동작 중.
    - NOHZ_MODE_HIGHRES : tick-sched 가 `high-resolution`을 기반으로 동작 중.

    : 모드는 저렇게 나누는 이유는 tick-sched 가 low-res 와 high-res 를 추상화했기 때문이다. 개발자들 입장에서는 단순히 tick-sched API 를 사용하면 끝이지만, tick-sched 내부에서는 현재 시스템이 low-res 인지 high-res 인지에 따라서, 각 모드에 맞는 함수를 호출해줘야 한다. 예를 들어, next tick 을 예약할 때, tick-sched 내부적으로 아래와 같은 코드들이 사용된다.
    nohz_mode == high-res
     call high-res call
    else 
     call low-res call

    - stalled_jiffies : jiffies 관련 디버깅 변수다. timekeeper 가 멈춰있는지를 판별하려고 할 때 사용된다. 자신의 이전 jiffies 와 시스템 글로벌 jiffies 가 같을 때 마다, 1씩 증가하는 변수다. 위에 last_tick_jiffies 와 함께 사용된다. 

    - last_tick_jiffies : jiffies 관련 디버깅 변수다. timekeeper 가 멈춰있는지를 판별하려고 할 때 사용된다. 이전 타이머 인터럽트 발생시에 jiffies를 저장한다. 아래의 stalled_jiffies 와 함께 사용된다.

    - next_tick : nex time tick 을 저장한다(`tick_nohz_stop_tick`).

    - timer_expires_base : monotime 은 절대 시간이 아니다. 특정 시점을 기준으로 얼마나 시간이 지났는지를 판단한다. 아래 timer_expires 는 이 값을 기준으로 얼마나 

    - idle_calls : NOHZ를 요청한 횟수(tick_nohz_idle_stop_tick 함수 참고). 그러나, 이 값이 tick 이 멈춘다는 것을 보장하지는 않다. 단순히, tick_nohz_idle_stop_tick 함수를 호출하면 증가하는 값.

     

    : 아래에서 볼 수 있다시피, 각 CPU 마다 `tick_sched` 구조체를 가지고 있다. 즉, 모든 CPU 자신만의 `dynamic tick`을 별도로 관리한다.

    // kernel/time/tick-sched.c - v6.5
    /*
     * Per-CPU nohz control structure
     */
    static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
    
    struct tick_sched *tick_get_tick_sched(int cpu)
    {
    	return &per_cpu(tick_cpu_sched, cpu);
    }

     

    : tick-sched 에서는 2가지 전역 변수가 존재한다. tick_nohz_active 는 cpuidle 에서 사용하는 변수다. 

    // kernel/time/tick-sched.c - v6.5
    /*
     * NO HZ enabled ?
     */
    bool tick_nohz_enabled __read_mostly  = true;
    unsigned long tick_nohz_active  __read_mostly;

    : tick_nohz_enabled 변수는 굉장히 어려운 변수다. 이 변수는 딱 정의하기가 어려운 변수다. tick_nohz_enabled 은 ts->nohz_mode 와 연관이 깊다. 왜냐면, tick_nohz_enabled 변수 SET 되어있지 않으면, ts->nohz_mode 는 변경이 불가능하다.

     

     

     

    - How to switch from low-resolution mode to high-resolution mode ?

    : one-shot mode 가 지원되는 상황에서 low-resolution mode 에서 high-resolution mode 로 어떻게 전환될까? 그림으로 표현하면, tick-sched가 high-res를 사용하는 모습이라고 볼 수 있다.

     

    : 최초에 디폴트로 동작하는 타이머 핸들러는 `tick_handle_periodic` 함수다. 이 함수는 주기적으로 `one-shot mode`로 스위칭이 가능한지를 체크한다. `one-shot mode`가 스위칭이 가능한지 체크한 후, `hrtimer_hres_enabled` 이 true 라면, `high-resolution mode`로 변경하게 된다. 이 때, 호출되는 함수가 `hrtimer_switch_to_hres` 다.

    // include/linux/hrtimer_defs.h - v6.5
    # define HIGH_RES_NSEC		1
    ....
    
    // kernel/time/hrtimer.c - v6.5
    ....
    
    unsigned int hrtimer_resolution __read_mostly = LOW_RES_NSEC;
    EXPORT_SYMBOL_GPL(hrtimer_resolution);
    
    /*
     * 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()) { // --- 1
    		pr_warn("Could not switch to high resolution mode on CPU %u\n",
    			base->cpu);
    		return;
    	}
    	base->hres_active = 1; // --- 2
    	hrtimer_resolution = HIGH_RES_NSEC; // --- 2
    
    	tick_setup_sched_timer(); // --- 3
    	/* "Retrigger" the interrupt to get things going */
    	retrigger_next_event(NULL);
    }
    1. `tick_init_highres` 함수는 내부적으로 `tick_switch_to_oneshot` 함수를 호출해서 현재 CPU의 로컬 타이머가 `one-shot mode`로 변경 가능한지를 체크한다. 이 내용을 통해 알 수 있는 것은 high-resolution mode 는 기본적으로 one-shot mode를 전제한다는 것을 알 수 있다. 왜 그럴까? tick 을 기반으로 하지 않는다는 것은 period mode 를 기반으로 하지 않는다는 것과 같은 말이다. 그렇다면, 남은 동작 모드는 one-shot mode 밖에 없다. 자세한 내용은 이 글을 참고하자.

    2. per-CPU 타이머의 high-resolution mode 를 활성화한다. hrtimer_resolution 는 전역 변수다. 이 변수는 현재 high-resolution 의 분해능을 나타낸다. HIGH_RES_NSEC 는 1ns 로 타이머 인터럽트를 발생시킬 수 있다는 것을 의미한다. 초기값은 LOW_RES_NSEC 로 기존의 low-resolution mode 에서 사용하는 resolution 을 사용한다(1ns 보다 훨씬 큰 값을 갖는다).

    3. `tick scheduling` 을 위해, `ts->sched_timer`를 초기화한다. 여기서, `tick scheduling`이란, next tick 을 언제 발생시킬지를 계산하는 과정을 의미한다. 이 때, tick-sched는 시스템이 low-res 기반인지 high-res 기반인지를 추상화해준다. 즉, 드라이버 개발자들은 원하는 동작 모드, 즉, period 혹은 oneshot 모드만 던져주면, tick-sched 가 알아서 시스템의 resolution 을 파악해서 next tick 을 계산해 반환해준다는 것이다.

     

    : 시스템이 high-resolution mode로 변경이 가능하다면, 타이머 핸들러로 `hrtimer_interrupt` 함수를 사용한다.

    /**
     * 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) || // --- 1
    		    !tick_device_is_functional(dev)) {
    
    		pr_info("Clockevents: could not switch to one-shot mode:");
    		....
            
    		return -EINVAL;
    	}
    
    	td->mode = TICKDEV_MODE_ONESHOT; // --- 2
    	dev->event_handler = handler; // --- 2
    	clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT); // --- 3
    	tick_broadcast_switch_to_oneshot(); // --- 4
    	return 0;
    }
    
    /**
     * tick_init_highres - switch to high resolution mode
     *
     * Called with interrupts disabled.
     */
    int tick_init_highres(void)
    {
    	return tick_switch_to_oneshot(hrtimer_interrupt);
    }
    1. one-shot mode로 동작하려면, 2가지 조건이 모두 충족되어야 한다. 먼저, 하드웨어적으로 one-shot mode 를 지원해야 한다. 그리고, per-CPU 타이머가 존재해야 한다(DUMMY TIMER 를 사용한다는 말은 per-CPU 타이머가 없다는 말과 같다).

    2. 위에 2가지 조건이 충족되면, tick-sched 에게 현재 per-CPU 타이머는 one-shot mode 로 동작할 것이라고 알린다(`td->mode = TICKDEV_MODE_ONESHOT`). 그리고, high-res 에서 사용하는 타이머 핸들러는 `hrtimer_interrupt`를 사용한다.

    3. per-CPU 타이머는 하드웨어적으로 반드시 존재해야 한다고 했다. 그리고, 위에서 소프트웨어 동작 모드를 변경했으니, 이제는 하드웨어 동작 모드를 바꿀 차례다.

    4. `tick_broadcast_switch_to_oneshot` 함수는 브로드 캐스트 타이머의 동작 모드를 one-shot mode 으로 변경한다. 그런데, 왜 값자기 뜬금없이 per-CPU 타이머를 one-shot mode 로 바꾸는 곳에서 시스템 글로벌하게 존재하는 브로드 캐스트 타이머까지 one-shot mode 로 변경할까? per-CPU 타이머를 high-res 모드로 변경했을 때, advantage 가 정확성만 있을까? high-res 모드가 되면, tick 을 기반으로 하지 않기 때문에 one-shot mode 로 전환해야 한다고 했다. 이 말은, 규칙성이 tick 이 필요없고, on-demand 한 상황에서만 타이머 인터럽트를 발생시키겠다는 말과 같다. 즉, 필요없을 때는 사용하지 않겠다는 의미다. 즉, high-res 모드가 된다는 것은 CPU 가 IDLE 로 더 쉽게 들어갈 수 있는 말과 같다. CPU 가 IDLE 상태에 들어가면, 자신을 깨워줄 타이머가 필요하다. 즉, 브로드 캐스트 타이머를 활성화해야 한다. 물론, 브로드 캐스트 타이머도 필요할 때만 사용할 것이므로, one-shot mode 로 설정해야 한다. 정리하자면, per-CPU 타이머를 high-res 모드로 보내는 것은 CPU 가 IDLE 로 더 자주 혹은 더 쉽게 들어갈 수 있다는 것을 의미하므로, 브로드 캐스트 타이머도 이에 맞게 준비한다고 보면된다.
    tick_switch_to_oneshot tick_broadcast_switch_to_oneshot
    per-CPU 타이머의 동작 모드(소프트웨어 & 하드웨어)를 one-shot mode 로 변경 브로드 캐스트 타이머의 동작 모드(소프트웨어 & 하드웨어)를 one-shot mode 로 변경

     

    : `tick_setup_sched_timer` 함수는 high-rse 기반의 next tick 을 계산하기 위해 tick scheduling handler 로 `tick_sched_timer`를 설정한다.

    // kernel/time/tick-sched.c - v6.5
    /**
     * 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); // --- 1
    	ts->sched_timer.function = tick_sched_timer; // --- 1
    
    	/* Get the next period (per-CPU) */
    	hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update()); // --- 2
    
    	/* Offset the tick to avert jiffies_lock contention. */
    	if (sched_skew_tick) { // --- 3
    		u64 offset = TICK_NSEC >> 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_NSEC); // --- 4
    	hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED_HARD); // --- 4
    	tick_nohz_activate(ts, NOHZ_MODE_HIGHRES); // --- 5
    }
    1. one-shot mode 이건, period mode 이건 시간 관련해서 처리해야 하는 공통적인 작업들이 반드시 존재하기 마련이다. tick_sched_timer 함수는 one-shot mode 에서 periodic tick 을 시뮬레이션하는 함수다.

    2. tick_next_period 를 TICK_NSEC 에 정렬시킨다. 그리고, last_jiffies_update 를 tick_next_period 로 초기화한뒤 반환한다. one-shot mode 로 전환되더라도, tickless 가 활성화 되기전까지는 periodic tick 을 발생시킨다. 그러므로, next event 에 tick_init_jiffy_update 함수를 호출하는 것이다.

    3. sched_skew_tick 은 커널 파라미터를 통해 동적으로 변경이 가능한 값이다. 코드에서 보다시피 이 값이 true 가 되면,  period tick 의 주기가 늘어나서, 이전보다 더 늦게 발생한다. 즉, power consumption 이 증가한다. 그러나, period tick 이 더 늦게 발생하는 만큼 퍼포먼스에 영향을 줄 수 있다. 특정 상황에서만 사용하는 파라미터다.
    skew_tick= [KNL] 
    Offset the periodic timer tick per cpu to mitigate xtime_lock contention on larger systems, and/or RCU lock contention on all systems with CONFIG_MAXSMP set.

    Format: { "0" | "1" }
    0 -- disable. (may be 1 via CONFIG_CMDLINE="skew_tick=1"
    1 -- enable.

    Note: increases power consumption, thus should only be enabled if running jitter sensitive (HPC/RT) workloads.
    - 참고 : https://github.com/torvalds/linux/commit/5307c9556bc17e3cd26d4e94fc3b2565921834de

    4. TICK_NSEC 주기로 periodic tick 을 발생시켜야 함으로, 현재 설정된 expires (last_jiffies_update)값에 TICK_NSEC 값을 더한다. hrtimer_start_expires 함수를 실행하면, hrtmer 에 설정된 정보를 기준으로 hrtimer 큐에 삽입한다.

    5. ts->nohz_mode 를 NOHZ_MDE_HIGHRES 로 변경한다. 동작 모드가 NOHZ_MODE_INACTIVE 에서 NOHZ_MDE_HIGHRES 혹은 NOHZ_MDE_LOWRES 가 됬다는 것은 dyntick & one-shot 모드 초기화 및 설정이 완료되었음을 의미한다. 

    : 여기서 궁금증이 생긴다. hrtimer_interrupt 와 tick_sched_timer의 차이가 뭘까? hrtimer_interrupt는 말 그대로 타이머 핸들러다. 타이머 인터럽트를 받으면, 아키텍처 종속적인 코드는 hrtimer_interrupt 핸들러를 제일 먼저 호출한다. tick_sched_timer 는 one-shot mode 에서 low-res & high-res 건 상관없이 next tick & jiffies 를 업데이트하는 함수다. 이 함수는 말 그대로 `기능` 이다. high-resolution 에서 period mode 에서 사용하는 tick_periodic 과 같은 함수를 사용하기 위해 만든 함수가 tick_sched_timer 함수라고 보면 된다.

     

    : tick_nohz_handler 과 tick_sched_timer 은 모두 one-shot mode 를 지원하고 resolution 만 다른 함수인것을 알 수 있다. 기능적으로 어떤 차이가 있을까? 2개 모두 tick_sched_do_timer & tick_sched_handle 함수를 호출하는 것을 알 수 있다. 정리하면, tick_nohz_handler 과 tick_sched_timer 은 같은 기능을 수행한다고 볼 수 있다. 차이가 있다면, tick_nohz_handler 함수는 실제 타이머 핸들러 역할을 맡고 있다. 그러나, tick_sched_timer 는 타이머 핸들러가 아니다.

    1. next tick & jiffies 업데이트
    2. periodic tick 예약
    one-shot & low-res one-shot & high-res
    //kernel/time/tick-sched.c - v6.5
    /*
     * 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 = KTIME_MAX;

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

    if (unlikely(ts->tick_stopped)) {
    /*
     * The clockevent device is not reprogrammed, so change the
     * clock event device to ONESHOT_STOPPED to avoid spurious
     * interrupts on devices which might not be truly one shot.
     */
        tick_program_event(KTIME_MAX, 1);
        return;
    }

    hrtimer_forward(&ts->sched_timer, now, TICK_NSEC);
    tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
    }
    // kernel/time/tick-sched.c - v6.5
    /*
     * 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(ts, 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_NSEC);

    return HRTIMER_RESTART;
    }

     

     

    - How to emulate a tick in high-resolution mode ?

    : high-resolution mode 환경에서 타이머 인터럽트를 발생시킬 때는 2가지 방법을 기반으로 한다.

    1. NOHZ 환경이 아니라면, one-shot 타이머 인터럽트를 주기적으로 발생시킨다.
    2. NOHZ 환경이라면, 필요에 따라 one-shot 타이머 인터럽트를 발생시킨다.
    // kernel/time/tick-sched.c - v6.5
    /*
     * 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(ts, now); // --- 1
    
    	/*
    	 * Do not call, when we are not in irq context and have
    	 * no valid regs pointer
    	 */
    	if (regs) // --- 2
    		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)) // --- 3
    		return HRTIMER_NORESTART;
    
    	hrtimer_forward(timer, now, TICK_NSEC); // --- 4
    
    	return HRTIMER_RESTART; // --- 4
    }
    1. tick_sched_do_timer 함수는 tick-sched 환경에서 jiffies 및 next tick 을 관리한다. 뒤에서 다시 알아본다.

    2. regs 가 NULL 이 아니라는 것은 현재 인터럽트 컨택스트에 있다는 것을 의미한다. 왜냐면, 인터럽트 컨택스트가 아닌 상황에서 regs 는 유효하지 않기 때문이다.
    It will always return NULL outside of IRQ context - and only returns valid pointers when used inside IRQ context.

    - 참고 : https://linux-arm-kernel.infradead.narkive.com/i4qvHNGE/get-irq-regs-from-soft-irq

    3. ts->tick_stopped 이 true 라는 것은, NO_HZ 상태이면서 CPU가 IDLE 상태에 있다는 것을 의미한다. 이 때는 주기적으로 tick 을 발생시키면 안된다. 그러므로, HRTIMER_NORESTART 를 반환해서 next time tick 은 주기적으로가 아닌, 필요할 때 직접 발생시키도록 한다.

    4. 시스템이 NOHZ 상황이 아니면, one-shot 타이머 인터럽트를 `TICK_NSEC` 주기로 발생시킨다. hrtimer_forward 함수는 `next event = 두 번째 인자 + 세 번째 인자`로 설정한다. 이 함수에서는 hrtimer 타이머를 다시 실행시키기 위해 호출하는 함수가 딸랑 hrtimer_forward 밖에없다. 왜 그럴까? 이미, tick_setup_sched_timer 함수에서 ts->sched_timer 관련한 초기화와 모든 설정들을 마무리했기 때문이다. 그런데, hrtimer_forward 함수는 단지 hrtimer의 next event 만 늘려주는데, 어디서 hrtimer 타이머를 다시 실행시킬까? 이 함수의 반환값에 따라 다시 시작할지 말지를 결정한다. 이 함수는 `__run_hrtimer` 함수에서 호출되는데, HRTIMER_RESTART 를 반환받으면, enqueue_hrtimer 함수를 통해 다시 hrtimer rb-tree 에 삽입된다.  

     

     

    : 리눅스 커널에서 타이머 인터럽트를 받으면, 커널이 디폴트로 설정한 타이머 핸들러들이 먼저 호출된다. 이 디폴트 타이머 핸들러들이 상황에 따라 다르게 적용된다. 총, 3가지 존재한다. 

    1. In periodic ticks(default) : tick_handle_periodic(기본적으로 동작하는 타이머 인터럽트 핸들러)
    2. In non-periodic ticks(low-resolution) : tick_nohz_handler(NO_HZ & low-resolution 기반으로 동작하는 타이머 인터럽트 핸들러)
    3. In high-resolution : hrtimer_interrupt

    : 위의 타이머 인터럽트 핸들러들은 4가지 상황에 따라 커널 타이머 서브-시스템에 의해서 위에 `struct clock_event_device.event_handler`에 할당된다.

      High-resolution Low-resolution
    Periodic tick hrtimer_interrupt(NOHZ_MODE_INACTIVE) tick_handler_periodic(NOHZ_MODE_INACTIVE)
    Dynamic tick(one-shot) hrtimer_interrupt(NOHZ_MODE_HIGHRES) tick_nohz_handler(NOHZ_MODE_LOWRES)

     

    : 위의 핸들러들은 공통적으로 `jiffies`값을 증가시키고, 내부적으로 개발자들이 설정한 커스텀 타이머 인터럽트 핸들러를 호출한다. 그런데, 서로 루틴이 다르다. 그 과정에 대해 알아본다.

     

    : `jiffies_64`는 타이머 인터럽트가 발생할 때마다, `1`씩 더해지는 값이다. 이 값은 부트 시점부터 현재까지 얼마나 시간이 지났는지를 알려준다. 그런데, 아래 코드에서 보다시피 `jiffies_64`는 `per-CPU` 변수가 아니다. 즉, 시스템 전역적으로 모든 CPU가 공유하는 변수다. 그런데, 만약에 4개의 코어가 존재하는 상황에서 모든 CPU가 타이머 인터럽트가 발생했다. 이 때, 모든 CPU가 `jiffies_64`를 증가시키위해 경쟁 관계가 될까?

    // include/linux/jiffies.h - v6.5
    /*
     * The 64-bit value is not atomic - you MUST NOT read it
     * without sampling the sequence number in jiffies_lock.
     * get_jiffies_64() will do this for you as appropriate.
     */
    extern u64 __cacheline_aligned_in_smp jiffies_64;

     

    : 이 때, `tick_do_timer_cpu` 변수를 이용한다. 이 변수는 `jiffies_64` 증가시킬 CPU 번호를 저장하고 있는 변수다(그래서 보면, `__read_mostly`를 통해서 값이 거의 변하지 않을 것 이라는 힌트를 컴파일러에 알려준다). `period tick` 시스템에서는 `do_timer` 함수가 `jiffies_64`를 `1` 증가시킨다. 그런데, 이 함수는 `tick_periodic` 함수에서 현재 코드를 실행중인 CPU의 번호가 `tick_do_timer_cpu` 변수의 값과 같아야 `do_tiemr(1)`을 실행한다.

    // kernel/time/tick-internal.h - v6.5
    # define TICK_DO_TIMER_NONE	-1
    # define TICK_DO_TIMER_BOOT	-2
    
    // kernel/time/tick-common.c - v6.5
    /*
     * Tick devices
     */
    DEFINE_PER_CPU(struct tick_device, tick_cpu_device);
    /*
     * Tick next event: keeps track of the tick time. It's updated by the
     * CPU which handles the tick and protected by jiffies_lock. There is
     * no requirement to write hold the jiffies seqcount for it.
     */
    ktime_t tick_next_period;
    
    /*
     * tick_do_timer_cpu is a timer core internal variable which holds the CPU NR
     * which is responsible for calling do_timer(), i.e. the timekeeping stuff. This
     * variable has two functions:
     *
     * 1) Prevent a thundering herd issue of a gazillion of CPUs trying to grab the
     *    timekeeping lock all at once. Only the CPU which is assigned to do the
     *    update is handling it.
     *
     * 2) Hand off the duty in the NOHZ idle case by setting the value to
     *    TICK_DO_TIMER_NONE, i.e. a non existing CPU. So the next cpu which looks
     *    at it will take over and keep the time keeping alive.  The handover
     *    procedure also covers cpu hotplug.
     */
    int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;
    #ifdef CONFIG_NO_HZ_FULL
    /*
     * tick_do_timer_boot_cpu indicates the boot CPU temporarily owns
     * tick_do_timer_cpu and it should be taken over by an eligible secondary
     * when one comes online.
     */
    static int tick_do_timer_boot_cpu __read_mostly = -1;
    #endif

     

     

     

    - How to switch one-shot mode ?

    : `tick_check_oneshot_change` 함수는 어떻게 보면, 타이머 서브-시스템에서 가장 중요한 함수 중 하나다. 이 함수는 one-shot mode 지원 여부와 high-res 지원 여부에 따라 어떤 타이머 핸들러 및 동작 모드로 변경해야 하는지를 알려주는 함수다.

    //kernel/time/tick-sched.c - v6.5
    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)) // --- 1
    		return 0;
    
    	if (ts->nohz_mode != NOHZ_MODE_INACTIVE) // --- 2
    		return 0;
    
    	if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available()) // --- 3
    		return 0;
    
    	if (!allow_nohz) // --- 4
    		return 1;
    
    	tick_nohz_switch_to_nohz();
    	return 0;
    }
    1. ts->check_clocks 은 2가지 이유로 SET 될 수 있다. 아래의 2개는 `AND`가 아닌, `OR`의 관계를 갖는다. 그래서, per-CPU 타이머가 one-shot 을 지원하지 않아도, 브로드 캐스트 타이머가 지원한다면, ts->check_clocks 은 SET 된다.
    1. one-shot 모드를 지원하는 브로드 캐스트 타이머가 등록된 경우.
    2. one-shot 모드를 지원하는 per-CPU 타이머가 등록된 경우.
    그렇다면, 이 변수의 의미는 뭘까? CPU 가 one-shot 타이머 서비스를 받을 수 있는지를 의미한다. 그게 per-CPU 타이머한테 받든, 브로드 캐스트 타이머에게 받든 상관없이 말이다. 그렇다면, 왜 `test_and_clear_bit` 함수를 사용했을까? 이렇게 하면, 한 번 검사한 후에 ts->check_clocks 비트가 CLEAR 되버리는데 말이다. 왜냐면, `tick_check_oneshot_change` 함수는 `determined` 성향을 가지고 있기 때문이다. 즉, 값이 이미 정해져있다는 뜻이다. 정확하게 말하면, 이 함수안에는 동적으로 값이 변경될 요소들이 거의 존재하지 않는다. 혹은, 바뀌더라도 한 번 바뀌고 나면 거의 변경될 일이 없는 변수들로만 이루어져 있다. 그래서, 예를 들어, 최초 한 번 호출되었을 때 값이 10만 번째 호출되었을 때의 값과 비슷할 확률이 매우 높다. 이러한 특성 때문에, ts->check_clocks 을 CLEAR 하더라도 최종 결과는 처음이나 그 이후에나 계속 동일할 것이라고 판단할 수 있다. 즉, one-shot mode 로 전환된 후에는 다른 모드(period mode)로 변경할 일이 없다는 것이다. 그리고, ts->check_clocks 을 CLEAR 함으로써 얻는 또 다른 이점은 퍼포먼스다. 이 함수는 타이머 인터럽트가 발생할 때 마다 호출되는 함수기 때문에 처리량이 적을 수 록 좋다. 그래서, 제일 처음 조건문에서 결정이 나도록 CLEAR 하면, 자주 호출되서 발생하는 성능 문제를 그나마 완화시킬 수 있다. 

    뒤에서 더 설명하겠지만 (2) 번 조건문은 뒤에서 설명하고, `(3)` 조건문은 하드웨어적인 부분이라 아예 변경될 일이없다. 그리고, (4)번은 kernel parameter 로 변경이 가능하지만, 그런 짓은 99.999...% 안한다고 보면 된다. 결국, (1) 만 신경쓰면 된다. ts->check_clocks 

    2. NOHZ_MODE_INACTIVE : 아직 tick-sched 프레임워크에 타이머 핸들러가 등록되지 않은 상태를 의미한다. 그렇기 때문에, 이 시점에서는 nohz 타이머 핸들러를 등록이 허용된다. ts->nohz_mode 가 NOHZ_MODE_LOWRES 혹은 NOHZ_MODE_HIGHRES 라면, 이미 nohz 타이머 핸들러가 등록되었다는 뜻이므로, 프로세스를 종료한다.
    - low resolution : tick_nohz_switch_to_nohz -> tick_nohz_handler 핸들러 설정 -> NOHZ_MODE_LOWRES 설정
    - high resolution : tick_setup_sched_timer -> hrtimer_interrupt 핸들러 설정 -> NOHZ_MODE_HIGHRES 설정
    그런데, `ts->nohz_mode`는 한 번 LOWRES 혹은 HIGHRES 로 변경되면 다시는 다른 상태로 변경되지 않는다. 커널 v6.5 곳곳을 찾아봐도 `ts->nohz_mode` 가 한 번 변경되면, 절대 다른 상태로 변경되지 않는다. 그 이유를 한 번 생각해봤다.

    타이머의 동작 모드는 특정 상황이나 케이스에 의존하여 동적으로 바뀐다기 보다는 하드웨어와 깊은 관련이 있다. 즉, 굉장히 정적이다. 그래서, 보면 clock event device 의 CLOCK_EVT_FEAT_* 플래그를 검사해서 동작 모드를 컨트롤한다. CLOCK_EVT_FEAT_* 플래그는 제조사에서 설정해주는 값이다. 즉, 이 플래그는 clock event device 의 하드웨어적 특성을 나타낸다. 그리고, one-shot 을 선호하는 이유는 정확성과 유연함 때문이다. tick 기반이 아닌, nano-second 단위의 타이머 서비스를 제공할 수 도 있고, 거기에다가 one-shot 모드 하나로 periodic tick 과 single-shot tick 을 모두 지원할 수 있기 때문이다. 그렇다면, 하드웨어적으로 one-shot mode 와 period mode 를 모두 지원하는데도, one-shot mode 만 사용하는 이유가 뭘까? 예를 들어, periodic tick 이 필요할 땐, period mode 로 세팅하고, single-shot tick 이 필요할 땐, one-shot 모드를 사용하면 안될까? 멀티 프로세서에서 periodic tick 이 모든 프로세서들에게 필요할까? 최적화를 위해서는 on-demand 서비스만 있으면 된다. 굳이, 필요하지 않은데 서비스를 제공하는 것은 자원 낭비다. 그래서, jiffies 를 관리하는 BSP 는 period mode 로 동작하게 할 수는 있어도, 나머지 AP`s 들은 one-shot mode 로 동작하는게 훨씬 이득이다.


    3. 이 함수들은 하드웨어에 의존적인 값들을 timekeeping_valid_for_hres 함수는 clocksource 레벨에서 high-res 를 사용할 수 있는지를 판단하고, tick_is_oneshot_available 함수는 one-shot & NO C3STOP 타이머가 존재하는지를 판단한다. 결국, 2개의 함수 모두 하드웨어에 상당히 의존적인 값들이다.

    4. allow_nohz 는 low-res nohz 를 허용한다는 뜻이다. 만약, high-res 를 지원한다면, low-res nohz 는 허용되지 않는다.이 값은 `hrtimer_hres_enabled` 변수에 의해서 결정되는데, 커널 파라미터를 통해서 변경이 가능하다. 그러나, 거의 수정하지 않는다.

     

    : `tick_check_oneshot_change` 함수는 어떤 위치에서 함수가 반환되는지가 굉장히 중요하다. (1) 번 영역에서 반환될 경우, one-shot 핸들러를 아예 설정하지도 못한 경우다. (2) 번 영역에서 1을 반환하면, one-shot 모드와 high-res 를 지원하는 경우다. 이럴 경우, hrtimer_interrupt 핸들러가 사용된다. (3) 번 영역은 one-shot 모드는 지원하지만, high-res 를 지원하지 않는 경우다. 이럴 경우, tick_nohz_handler 가 사용된다. 그런데, 이때도 전역 변수 tick_nohz_enabled 여부에 따라 달라진다.

      (1) (2) (3)
    timer handler tick_handle_periodic hrtimer_interrupt 1. if tick_nohz_enabled == true, tick_nohz_handler
    2. if tick_nohz_enabled == false, tick_handle_periodic

     

     

     

    - How to switch to low-resolution mode with one-shot mode ?

    : 하드웨어적으로 `one-shot mode` 를 지원하더라도, 리눅스 커널이 `CONFIG_HIGH_RES_TIMERS` 컨피그가 설정되지 않은 상태로 컴파일되거나 `kenel parameter highres=off` 되서 부팅될 경우, `tick_check_oneshot_change` 함수에서 `tick_nohz_switch_to_nohz` 함수를 호출한다[참고1]. 이 함수는 one-shot mode 기반으로 low-resolution 타이머 서비스를 제공한다.

     

    : 사실, one-shot mode 를 기반으로 low-resolution 타이머 서비스를 이용하는 경우는 매우 드물다. one-shot 모드를 지원하는 하드웨어에 리눅스 커널 2.6 이상의 버전만 갔는다면, nano-second 기반에 hrtimer 를 사용할 수 있기 때문에 굳이 low-resolution 서비스를 이용할 필요가 없다.

    //kernel/time/tick-sched.c - v6.5
    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) // --- 1
    		return;
    
    	if (tick_switch_to_oneshot(tick_nohz_handler)) // --- 2
    		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); // -- 3
    	/* Get the next period */ 
    	next = tick_init_jiffy_update(); // --- 4
     
    	hrtimer_set_expires(&ts->sched_timer, next); // --- 5
    	hrtimer_forward_now(&ts->sched_timer, TICK_NSEC); // --- 5
    	tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1); // --- 6
    	tick_nohz_activate(ts, NOHZ_MODE_LOWRES); // --- 7
    }
    1. `tick_nohz_enabled` 변수는 딱 한 마디로 정의가 어렵다. 그렇기 때문에, 이 변수가 false 되었을 때, 시스템에 어떤 영향을 주는지 알아보는 것이 빠르다.
    1. ts->nohz_mode 를 변경할 수 없다 -> 계속 ts->nohz_mode = NOHZ_MODE_INACTIVE 로 남는다 -> `tick_check_oneshot_chage` 함수에서 2번 째 조건문을 계속 PASS 하게 되므로, 오버헤드가 발생할 수 있다.
    2. 시스템이 NOHZ(tickless) 상황이 되었을 때, 타이머를 예약할 수 가 없다(`tick_nohz_stop_tick`, `tick_nohz_restart` 참고).
    tick_nohz_enabled 가 false 면, 말 그대로 NOHZ 환경을 제공하지 않기 때문에 NOHZ 환경에서 타이머 서비스를 받을 수 가 없다. 그런데, NOHZ 가 아닌, 정상 모드에서는 타이머 서비스는 정상적으로 제공받을 수 있다. 왜냐면, tick_nohz_enabled 변수는 nohz 환경에서만 영향을 주기 때문이다.

    tick_nohz_enabled 가 false 일 때, 케이스를 한 번 나눠보았다. 결국, `tick_nohz_enabled 변수는 NOHZ 환경에서 타이머 서비스를 제공할지 말지를 결정한다` 라고 볼 수 있다.
    supported operating mode high-res & low-res high-res low-res
    timer handler hrtimer_interrupt hrtimer_interrupt tick_periodic_handle
    comment - `high-resolution mode` 를 지원하기 때문에, NOHZ 상황만 아니라면, 다른 때와 다름없이 정상 동작한다.   - `high-resolution mode` 를 지원하기 때문에, NOHZ 상황만 아니라면, 다른 때와 다름없이 정상 동작한다. - tick_nohz_enabled 가 CLEAR 되면, tick-sched 에서 사용하는 tick_nohz_handler 핸들러를 사용할 수 없다. 그래서, clock event device 가 커널에 처음 등록될 때, 디폴트로 tick_periodic_handle 함수가 타이머 핸들러로 등록되는데, 이걸 그대로 사용한다. 

    2. 이 함수까지 온거면, 하드웨어적으로 one-shot 은 지원하지만, 소프트웨어적으로 high-res 를 지원하지 않는다는 뜻이므로, `low-res & one-shot` 설정을 진행한다. 이 때, 사용되는 타이머 핸들러가 `tick_nohz_handler` 다.

    3. `one-shot & low-res` 에서는 ts->sched_timer 를 사용하지 않는다. 그럼에도, ts->sched_timer 를 초기화하는 이유는 hrtimer API 를 사용하기 위해서다(ts->sched_timer 는 `struct hrtimer` 구조체 사용). 

    one-shot mode 이건, period mode 이건 시간 관련해서 처리해야 하는 공통적인 작업들이 반드시 존재하기 마련이다. 예를 들어, next tick & jiffies 업데이트 등이 있다. 이 걸 처리하는 함수다. ts->sched_timer 다.

    4. 다음 번에 발생할 타이머 인터럽트 시간을 얻어온다. 

    5. 타이머 인터럽트 발생할 시간(hrtimer_set_expires)을 설정하고, 설정한 시간에 TICK_NSEC 을 더해서 실제 타이머 인터럽트 발생 시간을 조정한다(hrtimer_forward_now). 그런데, 왜 이렇게 번거롭게 계산할까? hrtimer_set_expires 함수만으로 가능하긴하다. 그러나, 이 함수는 에러 처리를 체크하는 루틴이 하나도 없어서, 이상한 값을 넣을 경우 문제가 발생할 수 있다. 예를 들어, hrtimer_forward_now 함수안에는 현재 하드웨어적으로 지원하는 최소 resolution 을 체크한다. 예를 들어, 이 값이 100 이라면, 100ns 이하의 정확도는 지원하지 않는다는 뜻이다. 즉, 100ns 주기로 요청이 가능하기 때문에 121ns 와 같은 타이머 서비스를 요청할 경우, 원하는 타이밍에 타이머 인터럽트가 발생하지 않을 것이다. 

    그렇다면, hrtimer_forward_now 함수만으로는 안될까? 이 함수를 사용하기 위해서는 미리 hrtimer_set_expires 를 통해서 `hrtimer expires`가 설정되어 있어야 한다. 결론적으로, 최초에 hrtimer 를 생성할 때는 `hrtimer_init , hrtimer_set_expires , hrtimer_forward` 함수가 필수적으로 사용된다고 보면 된다.

    6. `low-res & one-shot` 가 `high-res & one-shot` 은 사용하는 API 가 다르다. `low-res & one-shot` 는 타이머 서비스를 hrtimer 를 사용하지 못하기 때문에, clock event device API 를 이용해서 직접 예약한다.

    7. tickless 환경에서 low-resolution 타이머 서비스를 제공하도록 등록한다.

     

    : 위에서 tick_nohz_enabled 함수를 말로만 설명했는데, 실제 코드는 아래와 같다. 커널 파리미터를 통해서 이 변수를 변경이 가능한데, 어지간하면 건드리지 않는게 좋다. 관련 함수는 `setup_tick_nohz` 함수다.

     

    : 그리고, `tick_nohz_activate` 함수는 ts->nohz_mode 를 변경한다. 그런데, tick_nohz_enabled 가 true 일 때만, 변경이 가능하다. 이것 때문에, tick_nohz_enabled 가 false 면, ts->nohz_mode 가 계속 `NOHZ_MODE_INACTIVE`로 있는 것이다.

     */
    #ifdef CONFIG_NO_HZ_COMMON
    /*
     * NO HZ enabled ?
     */
    bool tick_nohz_enabled __read_mostly  = true;
    unsigned long tick_nohz_active  __read_mostly;
    /*
     * Enable / Disable tickless mode
     */
    static int __init setup_tick_nohz(char *str)
    {
    	return (kstrtobool(str, &tick_nohz_enabled) == 0);
    }
    
    __setup("nohz=", setup_tick_nohz);
    ....
    
    static inline void tick_nohz_activate(struct tick_sched *ts, int mode)
    {
    	if (!tick_nohz_enabled)
    		return;
    	ts->nohz_mode = mode;
    	/* One update is enough */
    	if (!test_and_set_bit(0, &tick_nohz_active))
    		timers_update_nohz();
    }
    
    ....
    #else
    
    static inline void tick_nohz_switch_to_nohz(void) { }
    static inline void tick_nohz_irq_enter(void) { }
    static inline void tick_nohz_activate(struct tick_sched *ts, int mode) { }
    
    #endif /* CONFIG_NO_HZ_COMMON */

     

    : `tick_init_jiffy_update` 함수는 tick_next_period 변수를 TICK_NSEC 에 정렬시키는 함수다. 그리고, 전역 변수인 `last_jiffies_update` 변수를 초기화하는 함수다. 이 값이 초기화되지 않았을 때는, tick_next_period 값으로 초기화한다. tick_next_period 변수는 `tick_periodic` 함수가 호출될 때마다, TICK_NSEC 값을 더한다.

    // kernel/time/tick-sched.c - v6.5
    /*
     * Initialize and return retrieve the jiffies update.
     */
    static ktime_t tick_init_jiffy_update(void)
    {
    	ktime_t period;
    
    	raw_spin_lock(&jiffies_lock);
    	write_seqcount_begin(&jiffies_seq);
    	/* Did we start the jiffies update yet ? */
    	if (last_jiffies_update == 0) { // --- 1
    		u32 rem;
    
    		/*
    		 * Ensure that the tick is aligned to a multiple of
    		 * TICK_NSEC.
    		 */
    		div_u64_rem(tick_next_period, TICK_NSEC, &rem); // --- 2
    		if (rem) // --- 2
    			tick_next_period += TICK_NSEC - rem; // --- 2
    
    		last_jiffies_update = tick_next_period;
    	}
    	period = last_jiffies_update;
    	write_seqcount_end(&jiffies_seq);
    	raw_spin_unlock(&jiffies_lock);
    	return period;
    }
    1. last_jiffies_update 값이 초기화되지 않았다는 것은 tick_next_period 이 TICK_NSEC에 정렬되어 있지 않다는 뜻이다. 해당 내용은 아래서 설명한다.

    2. div_u64_rem 함수는 첫 번째 인자를 두 번째 인자로 나눈뒤, 나머지를 세 번째 인자에 전달한다. 그런데, 왜 나머지를 구하는 것일까? tick_next_period 는 tick_setup_device 함수가 호출될 때, 초기화된다. 그런데, 이 시점에 tick_next_period 는 TICK_NSEC 에 정렬되어 있지 않다. 최초 부팅시에 빨리 타이머 서비스를 제공하기 위해 `tick_next_period = ktime_get` 을 한 것이다. 이 때, ktime_get 에서 운좋게 TICK_NSEC 에 정렬된 값을 반환할 수 도 있지만, 일반적으로는 그렇지 않을 것이다. 그렇기 때문에, 한 번은 정렬을 시킬 필요가 있다. 왜냐면, tick_next_period 변수가 만들어진 이유는 tick 을 주기적으로 발생시키기 위해서 만들어졌기 때문에, tick 주기에 정렬될 필요가 있다. 그렇다면, 하필이면 왜 이시점에 정렬을 할까? tick_next_period 변수가 정렬되는 위치는 2가지 특징이 있다. 아래 2개는 `AND` 조건으로 묶인다.
    1. one-shot 모드가 초기화 되는 시점(tick_setup_sched_timer | tick_nohz_handler)
    2. last_jiffies_update 가 초기화되지 않았을 때
    (1) 번 조건은 거의 발생하지 않는다. 그 이유는 위에 작성한 `tick_check_oneshot_change` 함수를 참고하자. (2) 번 조건은 한 번 초기화가 되면, 시스템이 reboot 하지 않은 이상, 다시 `0` 으로 돌아갈 일이 없다. 이 2개의 조건을 모두 만족시키는 경우는, 다시 말해, tick_next_period 변수를 정렬시키는 코드는 시스템 라이프 타임에서 딱 한번 실행된다.

    정리하면, 주기적인 tick 을 발생시키기 위해, TICK_NSEC으로 나눈 나머지를 구한다고 보면 된다. 예를 들어, tick_next_period = 13, TICK_NSEC = 4 이면, 위에 식을 통해 tick_next_period = 13 + 4 - 1 = 16 이 된다. 이제 TICK_NSEC에 정렬된 값이 됬다.

    : 그런데, tick_next_period 와 last_jiffies_update 변수의 차이는 뭘까? 심플하다. 아래와 같다[참고1]. last_jiffies_update 는 one-shot 모드가 지원될 때만, 사용할 수 있기 때문에 타이머 핸들러가 tick_handle_periodic 일 때는 사용되지 않는다. 그리고, 2개의 변수 모두 TICK_NSEC 에 정렬되어 있어야 한다.

    tick_next_period = last_jiffies_update + TICK_NSEC

     

     

     

    - Low-resolution 기반에서 dynamic tick 핸들링

    : 리눅스 커널이 low-resolution 기반의 dynamic tick 모드`로 전환되면, 타이머 인터럽트 핸들러는 `tick_nohz_handler` 함수가 된다.

    //kernel/time/tick-sched.c - v6.5
    /*
     * 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 = KTIME_MAX; // --- 1
    
    	tick_sched_do_timer(ts, now); // --- 2
    	tick_sched_handle(ts, regs); // --- 3
    
    	if (unlikely(ts->tick_stopped)) { // --- 4
    		/*
    		 * The clockevent device is not reprogrammed, so change the
    		 * clock event device to ONESHOT_STOPPED to avoid spurious
    		 * interrupts on devices which might not be truly one shot.
    		 */
    		tick_program_event(KTIME_MAX, 1);
    		return;
    	}
    
    	hrtimer_forward(&ts->sched_timer, now, TICK_NSEC); // --- 5
    	tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
    }
    1. 이 코드는 현재 시점에서는 큰 의미가 없다. (4) 번 루트로 가도 인자가 KTIME_MAX 로 들어갈 경우, 자동으로 dev->next_event 는 KTIME_MAX 가 된다. (5)번 루트로 가면, hrtimer_forward 함수는 hrtimer 변수만 검사하고, dev->next_event 를 검사하지 않는다. 그리고, tick_program_event 함수 또한 마찬가지다. 현재 이 코드는 의미를 잃어버린 코드로 보인다. 

    2. one-shot 모드에서 do_timer 와 비슷한 역할을 한다. 즉, next tick & jiffies 를 업데이트한다. 뒤에서 다시 알아본다.

    3. period mode 에서는 tick_periodic 함수가 주기적으로 호출되서 processor 의 idle time 을 측정하기가 수월하다. 그러나, one-shot mode 는 불규칙적으로 호출되기 때문에 별도의 계산 방법이 필요하다. 이 때, 필요한 데이터를 이 함수에서 준비한다. 뒤에서 다시 알아본다.

    4. 만약, tickless 가 활성화되면, low-res 타이머는 사용하지 않는다.

    5. 현재 시점을 기준으로 next tick = now + TICK_NSEC 로 설정하여, low-res & one-shot 모드에서 periodic tick 을 발행한다.

     

    : `tick_program_event` 함수는 `clockevents_program_event` 의 wrapper 함수다. 이 함수는 주로, tick-sched.c & hrtimer.c 파일에서만 사용되는데, 그 이유는 이 함수가 one-shot mode 만 지원하기 때문이다. 함수의  내용이 쉽기 때문에, 자세한 설명은 생략한다. 

    // kernel/time/tick-oneshot.c - v6.5
    /**
     * tick_program_event - program the CPU local timer device for the next event
     */
    int tick_program_event(ktime_t expires, int force)
    {
    	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);
    
    	if (unlikely(expires == KTIME_MAX)) {
    		/*
    		 * We don't need the clock event device any more, stop it.
    		 */
    		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT_STOPPED);
    		dev->next_event = KTIME_MAX;
    		return 0;
    	}
    
    	if (unlikely(clockevent_state_oneshot_stopped(dev))) {
    		/*
    		 * We need the clock event again, configure it in ONESHOT mode
    		 * before using it.
    		 */
    		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
    	}
    
    	return clockevents_program_event(dev, expires, force);
    }

     

     

     

     

    - How does a one-shot handler handle the same job with `tick_periodic` ?

    : one-shot mode 가 되더라도 periodic tick 을 반드시 필요하다. 즉, period mode 에서 사용하던 `tick_periodic` 함수과 같은 일을 함수가 one-shot mode 에서도 필요하다. tick_periodic 함수가 하던 일은 다음과 같다.

    1. next tick & jiffies 계산 <-> tick_sched_do_timer
    2. system profiling <-> tick_sched_handle

    : 오른쪽에 매핑되는 함수들은 one-shot 모드에서 각 기능에 대응되는 함수를 명시했다. 이제 one-shot 모드에서 위에 2가지 기능을 어떻게 구현했는지 알아보자.

     

    1. How to calculate the next tick and jiffies in one-shot ?

    : `tick_sched_do_timer` 함수는 tick-sched 환경에서 jiffies 를 업데이트 하는 함수다.

    // kernel/time/tick-sched.c - v6.5
    #define MAX_STALLED_JIFFIES 5
    
    static void tick_sched_do_timer(struct tick_sched *ts, ktime_t now)
    {
    	int cpu = smp_processor_id();
    
    #ifdef CONFIG_NO_HZ_COMMON
    	if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE)) { // --- 1
    #ifdef CONFIG_NO_HZ_FULL
    		WARN_ON_ONCE(tick_nohz_full_running);
    #endif
    		tick_do_timer_cpu = cpu;
    	}
    #endif
    
    	if (tick_do_timer_cpu == cpu) // --- 2
    		tick_do_update_jiffies64(now);
    
    	if (ts->last_tick_jiffies != jiffies) { // --- 3
    		ts->stalled_jiffies = 0;
    		ts->last_tick_jiffies = READ_ONCE(jiffies);
    	} else {
    		if (++ts->stalled_jiffies == MAX_STALLED_JIFFIES) {
    			tick_do_update_jiffies64(now);
    			ts->stalled_jiffies = 0;
    			ts->last_tick_jiffies = READ_ONCE(jiffies);
    		}
    	}
    
    	if (ts->inidle) // --- 4
    		ts->got_idle_tick = 1;
    }
    1. tick_do_timer_cpu 변수는 시스템에 최초 등록된 per-CPU 타이머를 갖는 CPU 번호를 저장한다. 이 CPU 가 periodic mode 에서 jiffies 를 업데이트한다. 그런데, tick_do_timer_cpu 값이 TICK_DO_TIMER_NONE 이라는 것은 현재 jiffies 업데이트 해야하는 CPU 가 IDLE 상태여서, jiffies 를 업데이트할 수 있는 CPU 가 없다는 뜻이다(`tick_nohz_stop_tick` 함수 참고). 일단 jiffies 를 업데이트 시켜야 하기 때문에, 현재 해당 코드를 실행하고 있는 CPU 에게 jiffies 를 업데이트 시킬 수 있는 권한을 준다(tick_do_timer_cpu = cpu). 그리고, 바로 뒤에서 jiffies 를 업데이트 시킨다(`tick_do_update_jiffies64`).
    뒤에서 다시 설명하겠지만, tickless 환경에서는 jiffies 를 업데이트하는 CPU 들이 수도 없이 바뀌게 된다. 즉, 어차피 이 CPU 도 현재 타이머 인터럽트 서비스가 끝나면, 다시 IDLE 로 진입하게 되서, 곧 다른 CPU 가 jiffies 를 업데이트하는 역할을 맡게 될 가능성이 높다.

    2. 위에서 말했다시피, tick_do_timer_cpu 에 설정된 CPU 만이 jiffies 를 업데이트할 수 있다. tick_do_update_jiffies64 함수가 jiffies 를 업데이트한다.

    3. ts->last_tick_jiffies 은 이전에 high-res 모드에서 타이머 인터럽트가 발생했을 때, jiffies 값을 저장하고 있다. 그래서 일반적으로는 ts->last_tick_jiffies 와 jiffies 와 같으면 안된다. 왜 그럴까? jiffies 는 주기적으로 발생하기 때문에, 꾸준히 증가해야 하는 값이다. 심지어, 10ms 든 1ms 든 어느정도 정확한 속도로 증가를 해야한다. 그런데, 특정 CPU 에게 타이머 인터럽트가 발생했는데, 자신이 이전에 발생했을 때 jiffies 와 현재 시스템 글로벌 jiffies 가 동일하다는 것은 `next tick & jiffies duty` 임무를 갖는 CPU 가 동작하고 있지 않다는 소리다. 즉, 정리하면 다음과 같다.
    1. `next tick & jiffies duty` 임무를 갖는 CPU 가 동작하고 있지 않다.
    2. 동작을 못하고 있다면, `next tick & jiffies duty` 임무를 포기해야 하는데(TICK_DO_TIMER_NONE), 무슨 문제가 발생했는지 포기를 안한다.

    물론, 자신의 이전 jiffies 와 현재 시스템 글로벌 jiffies 가 같은 경우가 있을 수 있다. 예를 들어, 시스템 jiffies 가 10ms 간격으로 증가하는데, 자신은 5us 간격으로 타이머 인터럽트를 발생시켰다면, 자신의 이전 jiffies 와 시스템 jiffies 가 같을 수 있다. 그래서, 한 번 같다고 주의를 에러를 뿜지 않는다. 5번의 기회를 준다(MAX_STALLED_JIFFIES). 만약, 자신에게 타이머 인터럽트가 5번이나 발생했는데, 그 때마다 자신의 이전 jiffies 와 시스템 jiffies 가 동일하다면, 이건 timekeeper 에 문제가 있다고 판단한다. `next tick & jiffies duty` 임무를 갖는 CPU 에서 문제가 발생했을 확률이 높다. 인터럽트가 disabled 된 상황이 길어지고 있거나, 모종의 다른 이유가 있을 것이다. jiffies를 program 하는 코드는 모두 boot CPU 가 컨트롤하기 때문에, boot CPU 에 쪽의 정보를 더 확인해볼 필요가 있어보인다. 일단, 이 코드는 2022년도에 패치된 코드기 때문에 개선의 여지가 많아보인다[참고1].

    4. tick_sched_do_timer 함수는 tick-sched framework 가 활성화 되어있다면, low-res 이든 high-res 이든 상관없이 tick 관린를 위해서 타이머 인터럽트 핸들러에서 무조건 호출하는 함수다. 그런데, 이 시점이 NO_HZ 환경이라면, 타이머 인터럽트가 NOHZ 환경에서 실행됬다는 것을 알리기 위해 `ts->got_idle_tick` 을 SET 한다. 결국, 타이머 인터럽트 때문에 CPU 가 wake-up 됬다는 것을 알리기 위해 사용된다. 
    - low resolution : tick_nohz_handler -> tick_sched_do_timerr -> NOHZ 환경이라면, ts->got_idle_tick = 1
    - high resolution : hrtimer_interrupt -> tick_sched_timer -> tick_sched_do_timerr -> NOHZ 환경이라면, ts->got_idle_tick = 1 

     

    : nex titck 과 jiffies 를 계산하는 것 또한 period mode 와 one-shot mode 가 서로 다르다.

      period mode one-shot mode
    next tick & jiffies tick_periodic tick_do_update_jiffies64

    : 이제 one-shot mode 에서 next tick & jiffies 는 tick_do_update_jiffies64 함수가 한다는 것을 알았으니, 이 함수에 대해 깊게 분석해보자. one-shot mode 에서 next tick & jiffies 계산 메커니즘을 이해하기 위해서는 몇 가지 사전지식이 필요하다.

    1. 일반적으로, one-shot mode 에서도 period mode 와 같이 BSP 가 next tick & jiffies 를 업데이트 시킨다.
    2. 그러나, BSP 도 IDLE 상태에 들어갈 수 가 있다. 이럴 때, next tick & jiffies duty 에 부재가 발생한다.
    3. 이럴 경우, 제일 먼저 타이머 인터럽트를 받은 CPU가 next tick & jiffies duty 를 맡는다.
    4. tick-sched 가 활성화된 시점부터는, 즉, next tick & jiffies 계산이 tick_periodic 이 아닌 tick_do_update_jiffies64 함수가 맡는 순간부터 `tick_next_period = last_jiffies_update + TICK_NSEC` 가 성립한다.

    : 분석하기 전에 주의 사항이 있다. tick_do_update_jiffies64 함수는 `next tick & jiffies` 만 업데이트하는 함수다. 즉, 시간만 업데이트하는 함수다. one-shot mode 에서 이 업데이트된 시간을 가지고 next event 를 판단하는 함수는 tick_nohz_next_event 함수다. 이제 진짜 함수를 분석해보자.

    // kernel/time/tick-sched.c - v6.5
    static void tick_do_update_jiffies64(ktime_t now)
    {
    	unsigned long ticks = 1; // --- 1
    	ktime_t delta, nextp;
    
    	/*
    	 * 64bit can do a quick check without holding jiffies lock and
    	 * without looking at the sequence count. The smp_load_acquire()
    	 * pairs with the update done later in this function.
    	 *
    	 * 32bit cannot do that because the store of tick_next_period
    	 * consists of two 32bit stores and the first store could move it
    	 * to a random point in the future.
    	 */
    	if (IS_ENABLED(CONFIG_64BIT)) { // --- 1
    		if (ktime_before(now, smp_load_acquire(&tick_next_period)))
    			return;
    	} else {
    		unsigned int seq;
    
    		/*
    		 * Avoid contention on jiffies_lock and protect the quick
    		 * check with the sequence count.
    		 */
    		do {
    			seq = read_seqcount_begin(&jiffies_seq);
    			nextp = tick_next_period; // --- 2
    		} while (read_seqcount_retry(&jiffies_seq, seq));
    
    		if (ktime_before(now, nextp)) // --- 3
    			return;
    	}
    
    	/* Quick check failed, i.e. update is required. */
    	raw_spin_lock(&jiffies_lock);
    	/*
    	 * Reevaluate with the lock held. Another CPU might have done the
    	 * update already.
    	 */
    	if (ktime_before(now, tick_next_period)) { // --- 4
    		raw_spin_unlock(&jiffies_lock);
    		return;
    	}
    
    	write_seqcount_begin(&jiffies_seq);
    
    	delta = ktime_sub(now, tick_next_period); // --- 5
    	if (unlikely(delta >= TICK_NSEC)) { // --- 5
    		/* Slow path for long idle sleep times */
    		s64 incr = TICK_NSEC;
    
    		ticks += ktime_divns(delta, incr);
    
    		last_jiffies_update = ktime_add_ns(last_jiffies_update, // --- 5.1
    						   incr * ticks);
    	} else {
    		last_jiffies_update = ktime_add_ns(last_jiffies_update, // --- 5.2
    						   TICK_NSEC);
    	}
    
    	/* Advance jiffies to complete the jiffies_seq protected job */
    	jiffies_64 += ticks;
    
    	/*
    	 * Keep the tick_next_period variable up to date.
    	 */
    	nextp = ktime_add_ns(last_jiffies_update, TICK_NSEC); // --- 6
    
    	if (IS_ENABLED(CONFIG_64BIT)) {
    		/*
    		 * Pairs with smp_load_acquire() in the lockless quick
    		 * check above and ensures that the update to jiffies_64 is
    		 * not reordered vs. the store to tick_next_period, neither
    		 * by the compiler nor by the CPU.
    		 */
    		smp_store_release(&tick_next_period, nextp); // --- 6
    	} else {
    		/*
    		 * A plain store is good enough on 32bit as the quick check
    		 * above is protected by the sequence count.
    		 */
    		tick_next_period = nextp; // --- 6
    	}
    
    	/*
    	 * Release the sequence count. calc_global_load() below is not
    	 * protected by it, but jiffies_lock needs to be held to prevent
    	 * concurrent invocations.
    	 */
    	write_seqcount_end(&jiffies_seq);
    
    	calc_global_load();
    
    	raw_spin_unlock(&jiffies_lock);
    	update_wall_time();
    }
    1. ticks 에 기본값이 `1`임에 주의해야 한다. 왜, `0` 이 아니고 `1` 일까? tick_do_update_jiffies64 함수가 호출된 시점은 이미 single-shot tick 이 발생된 시점이다. 그러므로, `tick` 이 이미 발생한 것이기 때문에, `1`로 초기화한다. 뒤에서 이 값에 추가적인 연산을 수행한 뒤, 전역 변수 `jffies_64`에 더한다. 이 시점에 last_jiffies_update, tick_next_period, now 관계가 아래와 같다고 전제한다.

    2. 다른 곳에서 tick_next_period 를 업데이트 한게 있는지 체크한다. one-shot mode 에서는 주기적으로 tick_next_period 를 증가시키지 못하기 때문에, 기회가 될 때마다 업데이트하려고 한다. 타이머 인터럽트가 그 중 하나이며, 다른 케이스들은 각 상황에 맞게 설명한다.

    3. now 는 현재 시간이다. 그리고, tick_next_period 는 말 그대로 `다음 틱` 을 의미한다. 정상적이라면, 현재를 기준으로 next tick 은 미래에 있어야 한다. 여기서 `정상적` 이라는 것은 `next tick & jiffies duty` 임무를 가진 CPU 가 일을 잘하고 있다는 것을 의미한다. 그런데, 비정상적인 경우라면 어떨까? 예를 들어, `now >= next tick` 과 같은 경우를 말이다. next tick 은 현재 시간보다 항상 미래에 있어야 하는데, 현재 시간과 같거나 과거에 있다면 이건 `next tick & jiffies duty` 임무를 가진 CPU 가 정상적으로 일을 하지 않았다는 것과 같다. 즉, IDLE 상태에 있을 가능성이 높다는 것을 의미한다. 짧은 시간동안은 next tick & jiffies 의 동기화가 맞지 않을 수 있다. 그러나, 이게 축적되면 당연히 문제가 커진다. 그렇기 때문에 업데이트를 해줘야 한다.

    그렇다면, 언제 업데이트를 해줘야할까? one-shot 은 타이머 서비스가 언제 발생할 지 알 수 없기 때문에, 발생할 때 마다 next tick & jiffies 를 업데이트 시켜주는 것이 좋다. 

    4. 이 시점에 last_jiffies_update, tick_next_period, now 관계가 아래와 같다고 전제한다.

    5. 여기까지 오면, tick_next_period 이 빨리 업데이트가 필요한 상황이라는 것을 의미한다. 이제, jiffies 를 얼마나 업데이트 해야할 지 정해야 하므로, 이전 tick 을 기준으로 시간이 얼마나 지났는지를 계산한다(`현재 시간 - 이전 tick`).

    5.1. 만약, 이전 tick 을 기준으로 한 개 이상의 TICK_NSEC 보다 더 지났다면, 약간의 계산이 필요하다. 그림으로 표현하면 아래와 같다. tick 에서 `2+1`은 다음과 같다.
    1. `2`는 tick_next_period 는 TICK_NSEC 에 정렬되어 증가되야 하므로, delta / TICK_NSEC = `2` 가 더해진다.
    2. `1` 은 tick 의 기본값을 의미한다. 왜냐면,  tick_do_update_jiffies64 함수가 호출됬다는 것은 이미 타이머 인터럽트가 발생했다는 것을 의미하기 때문이다.

    5.2 만약, 이전 tick 을 기준으로 한 개의 tick period 도 지나지 못했다면, jiffies 를 업데이트하지 않는다. 대신, last_jiffies_update만 nex tick 으로 업데이트한다. 

    6. tick_next_period 는 next tick 을 의미하므로, `last_jiffies_update + TICK_NSEC` 값을 갖게 된다.

     

     

     

    2. How to check system profile ?

    : system profile 은 `tick_sched_handle` 함수에서 처리한다.

    // kernel/time/tick-sched.c - v6.5
    static void tick_sched_handle(struct tick_sched *ts, struct pt_regs *regs)
    {
    #ifdef CONFIG_NO_HZ_COMMON
    	/*
    	 * When we are idle and the tick is stopped, we have to touch
    	 * the watchdog as we might not schedule for a really long
    	 * time. This happens on complete idle SMP systems while
    	 * waiting on the login prompt. We also increment the "start of
    	 * idle" jiffy stamp so the idle accounting adjustment we do
    	 * when we go busy again does not account too much ticks.
    	 */
    	if (ts->tick_stopped) {
    		touch_softlockup_watchdog_sched(); // --- 1
    		if (is_idle_task(current))
    			ts->idle_jiffies++; // --- 2
    		/*
    		 * In case the current tick fired too early past its expected
    		 * expiration, make sure we don't bypass the next clock reprogramming
    		 * to the same deadline.
    		 */
    		ts->next_tick = 0;
    	}
    #endif
    	update_process_times(user_mode(regs)); // --- 3
    	profile_tick(CPU_PROFILING);
    }
    1. idle 상태로 진입하면 당연히 watchdog에 응답하지 못하게 된다. 그러므로, watchdog 에게 timer 가 곧 idle 상태로 진입할 것임을 미리 알려야 한다[참고1].

    2. tickless 상황에서 타이머 인터럽트가 발생했고, 현재 프로세스가 idle process 일 경우, skipped tick`s 를 계산하기 위해 ts->idle_jiffies 를 증가한다.

    3.  user_mode 함수는 인자로 받은 regs 를 통해서 유저 모드에서 타이머 인터럽트를 받은 것인지(1) 혹은 커널 모드에서 받은 것인지를 판단한다(0). update_process_times 함수는 타이머 인터럽트가 어느 위치에서 발생했는지에 따라서 유저 혹은 커널 시간을 업데이트한다.

     

     

     

    - How to stop a normal tick ?

    : 먼저 구조를 좀 알고 가자. CPU 가 IDLE 로 진입할 때, tick 또한 NOHZ 상태로 진입하게 된다. 이 때, tick 을 멈추게 된다. 그리고, CPU 가 IDLE 에서 깨어나면, 다시 tick 이 재시작된다. 함수로 매핑하면 관계는 아래와 같다.

    stop normal tick re-start normal tick
    tick_nohz_idle_enter tick_nohz_idle_exit
    tick_nohz_idle_stop_tick tick_nohz_idle_restart_tick

     

    : stop_tick, restart_tick, exit 함수 모두 enter 함수를 기반으로 한다. 즉, 이 함수들은 제일 먼저 enter 가 호출된 후에 그 위에서 동작하게 된다. 그리고, 일반적인 `stop tick -> re-start tick` 플로우는 아래 그림에서 보여주는 순서로 진행된다.

     

    : `tick_nohz_idle_enter` 함수는 `idle process`에 의해서 호출되는 함수로, CPU를 IDLE 상태로 진입하기전에 tick stop 을 위한 prepare 단계라고 보면 된다. 그러므로, 이 함수를 호출한다고 해서 tick 은 멈추지 않는다.

    //kernel/time/tick-sched.c - v6.5
    /**
     * tick_nohz_idle_enter - prepare for entering idle on the current CPU
     *
     * Called when we start the idle loop.
     */
    void tick_nohz_idle_enter(void)
    {
    	struct tick_sched *ts;
    
    	lockdep_assert_irqs_enabled();
    
    	local_irq_disable(); // --- 1
    
    	ts = this_cpu_ptr(&tick_cpu_sched);
    
    	WARN_ON_ONCE(ts->timer_expires_base);
    
    	ts->inidle = 1; // --- 2
    	tick_nohz_start_idle(ts); // --- 3
    
    	local_irq_enable(); // --- 1
    }
    1. tick_nohz_start_idle 함수 내부에 sched_clock_idle_sleep_event 함수는 caller 측에서 인터럽트를 비활성화 한 상태에서 호출해야 하는 함수다. 이렇게 특정 함수를 호출하기전에 먼저 다른 함수의 호출 조건을 먼저 살펴봐야 한다.

    2. ts->inidle = 1 로 설정한다. 이 값은 CPU 가 IDLE 상태로 진입을 시작한다는 것을 알린다. 그리고, tick_nohz_idle_enter 함수가 호출되었음을 알리기도 한다. 뒤에서 다시 다룬다.

    3. idle 의 시작 시간을 기록한다.

     

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_start_idle(struct tick_sched *ts)
    {
    	write_seqcount_begin(&ts->idle_sleeptime_seq);
    	ts->idle_entrytime = ktime_get(); // --- 1
    	ts->idle_active = 1; // --- 2
    	write_seqcount_end(&ts->idle_sleeptime_seq);
    
    	sched_clock_idle_sleep_event(); // --- 3
    }
    1. 이 함수가 호출되는 시점은 idle process 가 do_idle 함수를 실행하면서 실행된다. 즉, idle의 시작 시점이라 봐도 무방하다. 

    2. idle_active 변수는 2가지 의미가 있다. 첫 번째로 tick_nohz_start_idle & sched_clock_idle_sleep_event 함수가 호출되었음을 나타낸다. ts->idle_entrytime 필드의 유효성을 판단한다. 즉, ts->idle_active 가 1일 때만, ts->idle_entrytime 가 유효하다.

    3. 현재 sched_clock_idle_sleep_event 함수는 CONFIG_HAVE_UNSTABLE_SCHED_CLOCK 컨피그에 따라 커널에 디폴트 정의와 아키텍처 종속적인 정의가 가능하다. 이 글은 리눅스 커널 디폴트 정의를 기준으로 한다. 그런데, 리눅스 커널의 디폴트 정의는 사실상 내용이 거의없다. 그러므로, 더 자세한 내용을 이 글을 참고하자.

     

     

    : `tick_nohz_idle_stop_tick` 함수는 cpuidle 에 의해서 굉장히 자주 호출되는 함수다. 이 함수는 2가지 역할을 한다.

    1. tick_nohz_next_event - next timer event 가 언제 발생할지를 계산
    2. tick_nohz_stop_tick - next timer event 가 발생하는 시점에 따라 tick 을 중지할지 말지를 결정.
    // kernel/time/tick-sched.c - v6.5
    /**
     * tick_nohz_idle_stop_tick - stop the idle tick from the idle task
     *
     * When the next event is more than a tick into the future, stop the idle tick
     */
    void tick_nohz_idle_stop_tick(void)
    {
    	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
    	int cpu = smp_processor_id();
    	ktime_t expires;
    
    	/*
    	 * If tick_nohz_get_sleep_length() ran tick_nohz_next_event(), the
    	 * tick timer expiration time is known already.
    	 */
    	if (ts->timer_expires_base) // --- 1
    		expires = ts->timer_expires;
    	else if (can_stop_idle_tick(cpu, ts))
    		expires = tick_nohz_next_event(ts, cpu);
    	else
    		return;
    
    	ts->idle_calls++; // --- 2
    
    	if (expires > 0LL) { // --- 3
    		int was_stopped = ts->tick_stopped;
    
    		tick_nohz_stop_tick(ts, cpu); // --- 3
    
    		ts->idle_sleeps++; // --- 4
    		ts->idle_expires = expires; // --- 5
    
    		if (!was_stopped && ts->tick_stopped) { // --- 6
    			ts->idle_jiffies = ts->last_jiffies;
    			nohz_balance_enter_idle(cpu);
    		}
    	} else { // --- 7
    		tick_nohz_retain_tick(ts);
    	}
    }
    1. NOHZ 환경에서 MONOTIME 기반의 next event 를 만들기 위해서는 base 가 필요하다.  ts->timer_expires_base 가 base 역할을 맞는다. 이 값은 tick_nohz_next_event 함수에서 `last_jiffies_update` 로 초기화된다. 

    2. `ts->idle_calls` 의 카운트가 증가하는 위치가 굉장히 애매하다. can_stop_idle_tick 함수보다는 뒤에 있지만, tick_nohz_stop_tick & tick_nohz_retain_tick 함수보다는 앞에 있다. 이 위치는 tick 을 멈출 수 있는 요건은 갖춰졌지만, 멈출지 말지는 확정되지 않은 곳이다. 물론, 이 함수가 호출됬다는 것만으로 CPU가 IDLE 로 진입한다는 전제가 깔려있다. 즉, tick 을 멈출지 말지에 대한 여부와는 관계없이 tick 을 멈출 조건이 되고 CPU가 IDLE 로 진입하려고 할 때, `ts->idle_calls` 이 증가한다. 참고로, `ts->idle_calls` 은 tick-sched 에서 사용하기 보다는 다른 서브-시스템에서 사용한다.

    3. expires 가 양수라는 것은 tick 을 멈춘다는 것을 의미한다. tick_nohz_stop_tick 함수가 호출되면, 경우에 따라서 next tick 까지는 발생할 수 도 있다. 그러나, ts->tick_stopped 가 true 가 되면서 periodic tick 은 확실히 멈추게 된다.

    4. tick_nohz_stop_tick 함수가 호출되면, periodic tick 이 멈추기 때문에 sleep 에 들어간다고 봐도 무방하다.

    5. ts->idle_expires 는 tick 이 멈춘 시간을 저장한다.

    6. 이 조건문이 true 라는 것은, 이전에는 period tick 이었지만, 현재는 tick 이 멈췄다는 것을 의미한다. 즉, 최초로 tick 을 멈췄을 때마다 뭔가를 하려는 것이다. ts->last_jiffies 는 tick_nohz_next_event 함수가 호출될 때, 가장 최신 jiffies 값으로 업데이트된다. ts->idle_jiffies 는 tickless 환경에서(ts->tick_stopped == 1) 타이머 인터럽트가 한 번 호출될 때 마다, 1씩 증가한다. ts->idle_jiffies 는 뒤에서 NOHZ의 load average 를 구하는 데, 사용된다. 

    만약, 이미 tick 이 멈춘 상태라면, 이 조건문은 false 가 된다.

    7. expires 가 0 이라는 것은 next tick 이 TICK_NSEC 보다 짧기 때문이다. 즉, tick 을 멈추는 것이 더 손해라고 판단해서, tick 을 유지(retain)시킨다는 것이다. 대신, 다음 next tick 계산을 위해, 기준으로 사용되는 ts->timer_expires_base 를 0 으로 초기화한다.
     // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_retain_tick(struct tick_sched *ts)
    {
        ts->timer_expires_base = 0;
    }

     

    : `can_stop_idle_tick` 함수는 이름 그대로 현재 periodic tick 을 멈출 수 있는지에 대한 여부를 반환한다.

    // kernel/time/tick-sched.c - v6.5
    static bool can_stop_idle_tick(int cpu, struct tick_sched *ts)
    {
    	/*
    	 * If this CPU is offline and it is the one which updates
    	 * jiffies, then give up the assignment and let it be taken by
    	 * the CPU which runs the tick timer next. If we don't drop
    	 * this here the jiffies might be stale and do_timer() never
    	 * invoked.
    	 */
    	if (unlikely(!cpu_online(cpu))) {
    		if (cpu == tick_do_timer_cpu)
    			tick_do_timer_cpu = TICK_DO_TIMER_NONE; // --- 1
    		/*
    		 * Make sure the CPU doesn't get fooled by obsolete tick
    		 * deadline if it comes back online later.
    		 */
    		ts->next_tick = 0; // --- 2
    		return false;
    	}
    
    	if (unlikely(ts->nohz_mode == NOHZ_MODE_INACTIVE)) // --- 3
    		return false;
    
    	if (need_resched()) // --- 4
    		return false;
    
    	if (unlikely(report_idle_softirq())) // --- 5
    		return false;
    
    	if (tick_nohz_full_enabled()) {
    		/*
    		 * Keep the tick alive to guarantee timekeeping progression
    		 * if there are full dynticks CPUs around
    		 */
    		if (tick_do_timer_cpu == cpu)  // --- 6
    			return false;
    
    		/* Should not happen for nohz-full */
    		if (WARN_ON_ONCE(tick_do_timer_cpu == TICK_DO_TIMER_NONE)) // --- 6
    			return false;
    	}
    
    	return true;
    }
    1. jiffies 를 업데이트 할 CPU 가 offline 이라는 것은, 얼른 다른 CPU 에게 jiffies duty 를 양도해야 함을 의미한다. 그러므로, tick_do_timer_cpu 에 TICK_DO_TIMER_NON 을 설정한다.

    2. CPU 가 online 이 아니면, offline 밖에 없다. 그런데, offline 이라는 것은 idle 보다 더 강력한 sleep 이다. 즉, offline 은 power-off 와 같은 말이기 때문에, [참고1]

    3. ts->nohz_mode 가 NOHZ_MODE_INACTIVE 이라는 것은, 아직 nohz 타이머 핸들러가 설정이 안된 것을 의미한다. 즉, 아직 nohz 초기화가 진행되지 않음을 의미한다(혹은, 실패). nohz 타이머 핸들러가 설정되지 않았는데, tick 을 종료할  경우 심각한 문제가 발생할 수 있다.

    4. 실행할 프로세스가 있는지를 검사한다. 일반적으로 tick 을 멈추다는 것은 idle 에 진입한다는 것을 전제한다. 그렇기 때문에, 실행할 프로세스가 있다는 것은 idle 에 진입하는 것을 막겠다는 의미고, 결국 tick 을 멈추지 않겠다는 뜻이다.

    5. pending softirq 가 있는지를 검사한다. 존재한다면, tick 을 멈추지 못한다.

    6. 만약에 full-tickless 를 지원하다면, jiffies 를 업데이트하는 CPU(tick_do_timer_cpu) 는 tick 을 계속 발생시켜야 한다. 그러므로, 코드를 실행중 인 CPU 가 앞에서 언급한 CPU 라면, tick 을 off 하지 못하게 false 를 리턴한다.그리고 만약 tick_do_timer_cpu 가 TICK_DO_TIMER_NONE 이라면, jiffies 를 업데이트 시키는 CPU 가 부재됬음을 의미한다. 이건 절대로 있어서는 안되는 일이다. 

     

     

    : `tick_nohz_next_event` 함수는 다음에 발생 할 next timer event 를 계산하는 함수다. 만약 0을 반환하면 tick 을 유지한다. 그렇지 않으면, 반환값이 0 보다 크다면, 이 값은 next timer event 가 된다. 그러므로, tick 은 stop 될 수 있다. 그런데, 반환값이 양수이면서, KTIME_MAX 라면 next event 가 필요없다는 뜻이다. dyntick 에서 next timer event 를 계산할 때, 원칙이 하나있다.

    - single tick period 보다 더 길게 IDLE 상태를 유지할 수 있도록 해야 한다. 즉, next event 가 현재 시점을 기준으로 single tick period 보다 짧다면, IDLE 로 진입하지 않는다.
    // kernel/time/tick-sched.c - v6.5
    static ktime_t tick_nohz_next_event(struct tick_sched *ts, int cpu)
    {
    	u64 basemono, next_tick, delta, expires;
    	unsigned long basejiff;
    	unsigned int seq;
    
    	/* Read jiffies and the time when jiffies were updated last */
    	do {
    		seq = read_seqcount_begin(&jiffies_seq);
    		basemono = last_jiffies_update; // --- 1
    		basejiff = jiffies; // --- 1
    	} while (read_seqcount_retry(&jiffies_seq, seq));
    	ts->last_jiffies = basejiff;
    	ts->timer_expires_base = basemono;
    
    	/*
    	 * Keep the periodic tick, when RCU, architecture or irq_work
    	 * requests it.
    	 * Aside of that check whether the local timer softirq is
    	 * pending. If so its a bad idea to call get_next_timer_interrupt()
    	 * because there is an already expired timer, so it will request
    	 * immediate expiry, which rearms the hardware timer with a
    	 * minimal delta which brings us back to this place
    	 * immediately. Lather, rinse and repeat...
    	 */
    	if (rcu_needs_cpu() || arch_needs_cpu() || // --- 2
    	    irq_work_needs_cpu() || local_timer_softirq_pending()) {
    		next_tick = basemono + TICK_NSEC;
    	} else { // --- 3
    		/*
    		 * Get the next pending timer. If high resolution
    		 * timers are enabled this only takes the timer wheel
    		 * timers into account. If high resolution timers are
    		 * disabled this also looks at the next expiring
    		 * hrtimer.
    		 */
    		next_tick = get_next_timer_interrupt(basejiff, basemono);
    		ts->next_timer = next_tick;
    	}
    
    	delta = next_tick - basemono; // --- 4
    	if (delta <= (u64)TICK_NSEC) {
    		/*
    		 * Tell the timer code that the base is not idle, i.e. undo
    		 * the effect of get_next_timer_interrupt():
    		 */
    		timer_clear_idle();
    		
    		if (!ts->tick_stopped) { // --- 5
    			ts->timer_expires = 0;
    			goto out;
    		}
    	}
    
    	delta = timekeeping_max_deferment(); // --- 6
    	if (cpu != tick_do_timer_cpu &&
    	    (tick_do_timer_cpu != TICK_DO_TIMER_NONE || !ts->do_timer_last))
    		delta = KTIME_MAX; // --- 7
    
    	/* Calculate the next expiry time */
    	if (delta < (KTIME_MAX - basemono)) // --- 8
    		expires = basemono + delta;
    	else
    		expires = KTIME_MAX;
    
    	ts->timer_expires = min_t(u64, expires, next_tick);
    
    out:
    	return ts->timer_expires;
    }
    1. 왜 현재 시간이 아닌, last_jiffies_update 를 이용해서 next event 를 구할까? `now` 혹은 `tick_next_period` 를 사용할 수는 없던걸까? 만약, next event 가 정확히 period next tick 과 동일할 수 도 있다. 그러면, basemono = tick_next_period 로 할 수 있을 것이다. 그러나, 이런 경우에 `basemono = ktime_get()` 은 조금 힘들다. ktime_get 은 현재 시간을 주기 때문에, TICK_NSEC 에 정렬되어 있지 않다. 정확히 period next tick 과 동일한 시점에 발생하고 싶다면, basemono = tick_next_period 혹은 basemono = last_jiffies_update + TICK_NSEC 같은 방법을 사용할 수 있다. 그렇다면, basemono = tick_next_period 이 안되는 이유는 뭘까? 만약에 next event 의 발생 조건이 `last_jiffies_update < next event < tick_next_period` 사이라면 어떨까? 이럴 경우, basemono = tick_next_period 가 되면, next event 는 과거가 되버리기 때문에, 결국, `basemono = last_jiffies_mono` 가 가장 적합한 방법이다.

    2. 이 조건안에 들어오게 될 경우, tick 을 멈추지 않게 된다.
    - rcu_needs_cpu : When a CPU is needed for RCU the tick has to continue even when it was stopped before.[참고1]

    - arch_needs_cpu : Allow the architecture to request a normal jiffy tick when the system
    goes idle and tick_nohz_stop_sched_tick is called[참고1].

    - irq_work_needs_cpu : Don't stop the tick if we have pending irq works on the queue, otherwise if the arch can't raise self-IPIs, we may not find an opportunity to execute the pending works for a while[참고1].

    - local_timer_softirq_pending : 대기중인 timer softirq 가 있을 경우, tick 을 멈추지 않는다[참고1].
    arch_needs_cpu 함수는 아키텍처 종속적인 함수로, 각 아키텍처 회사가 tick 을 계속 유지하고 싶다면, 저 함수에 true 를 반환하는 코드를 작성해 놓으면 된다.

    3. 여기서 실행될 타이머가 없다면, KTIME_MAX 를 반환한다. get_next_timer_interrupt 함수는 hrtimer API 가 아니다. 즉, next pending timer 로 time wheel 기반에 값을 반환한다.

    4. 만약, next timer event 가 next period 보다 더 일찍 발생한다면, IDLE 에 진입하지 않는게 이득이다. 어차피 곧 깨어날 것 이기 때문이다.

    5. next timer event 가 next period 보다 더 일찍 발생하는 상황에서 아직 idle 상태가 아니라면, 시스템에서는 여전히 period tick 이 발생하고 있을것이다. dyntick 은 periodic tick 보다 짧게 idle 상태로 가는 것을 허용하지 않는다. 왜냐면, `cpuidle resideny >= tick period` 여야 퍼포먼스를 약간 양보하지만, 더 큰 전력 소비의 이득을 볼 수 있기 때문이다. 즉, 이 조건문안에 들어가게 된다는 것은 normal tick 을 유지한다는 뜻이다.

    6. 위에서 말했다시피, dyntick 은 커널이 `a single tick` 보다 더 길게 idle 상태를 유지할 수 있도록 해준다. 그렇다고, 무조건 그렇다는 것은 아니다. 현재 시점을 기준으로 next timer event 가 발생하는 시점이 single tick period 보다 짧다면, idle 진입 자체를 허용하지 않는다. single tick period 보다 더 길때만, idle 을 허용한다. 그런데, 길게 잔다고 좋은게 절대 아니다. 너무  오래 idle 에 있으면, clock source 가 time tracking 을 못할 수 도 있다. 즉, 오래 자고 일어났을 때, time tracking 이 소실될 수 도 있다. 그래서, clock source 가 안전하게 time tracking 을 유지할 수 있는 시간 동안만 idle 되어야 한다. 이건 제조사에서 명시하는 값이다(struct clock_source.max_idle_ns 에 저장된다). 모든 CPU 에게 이 내용을 적용해야 할까? 아니다. jiffies 를 업데이트 시키는 CPU 만 이 시간이 적용되어야 한다. 나머지 CPU 는 얼마나 오래 IDLE 에 있든 상관없다.
    [참고1]

    7. 이 조건문안에 들어오는 녀석들은 jiffies 를 업데이트하지 않는 CPU`s 들이다. 즉, 아주 길게 idle 상태에 있을 수 있는 CPU`s 들이다. 그런데, 조건문이 조금 보기가 까다로울 수 있다. 괄호로 묶으면 아래와 같다.
    cpu != tick_do_timer_cpu && ((tick_do_timer_cpu != TICK_DO_TIMER_NONE) || !ts->do_timer_last)
    : 앞에서 부터 해석하면, jiffies 를 업데이트하는 CPU가 아니여야 한다. 그리고, 현재 jiffies 업데이트하는 CPU 가 없으면 안된다. 사실 마지막에 `!ts->do_timer_last`는 왜 필요한지 의문이다.


    8. `delta < (KTIME_MAX - basemono)` 는 직관적으로는 이해하기 힘든식이다. 이 식을 이해하기 위해서는 부등식의 성질을 이용하면 된다. basemono 를 양변에 더해보자. `delta + basemono < KTIME_MAX` 가 된다. `delta + basemono`는 뭘까? (4) 번을 보면 next_tick = delta + basemono` 임을 알 수 있다. 즉, 이 식은 `next_tick < KTIME_MAX` 으로 바꿀 수 있다. 결론적으로, `next tick & jiffies duty` 없는 CPU 들은 false 가 되고, `next tick & jiffies duty` 가 있는 CPU는 true 가 된다. 

     

     

    : `tick_nohz_stop_tick` 함수는 periodic tick 멈추는 것을 의미한다.

    static void tick_nohz_stop_tick(struct tick_sched *ts, int cpu)
    {
    	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);
    	u64 basemono = ts->timer_expires_base;
    	u64 expires = ts->timer_expires;
    	ktime_t tick = expires;
    
    	/* Make sure we won't be trying to stop it twice in a row. */
    	ts->timer_expires_base = 0; // --- 1
    
    	if (cpu == tick_do_timer_cpu) { // --- 2
    		tick_do_timer_cpu = TICK_DO_TIMER_NONE;
    		ts->do_timer_last = 1;
    	} else if (tick_do_timer_cpu != TICK_DO_TIMER_NONE) {
    		ts->do_timer_last = 0;
    	}
    
    	/* Skip reprogram of event if its not changed */
    	if (ts->tick_stopped && (expires == ts->next_tick)) { // --- 3
    		/* Sanity check: make sure clockevent is actually programmed */
    		if (tick == KTIME_MAX || ts->next_tick == hrtimer_get_expires(&ts->sched_timer))
    			return;
    
    		WARN_ON_ONCE(1);
    		printk_once("basemono: %llu ts->next_tick: %llu dev->next_event: %llu timer->active: %d timer->expires: %llu\n",
    			    basemono, ts->next_tick, dev->next_event,
    			    hrtimer_active(&ts->sched_timer), hrtimer_get_expires(&ts->sched_timer));
    	}
    
    	/*
    	 * nohz_stop_sched_tick can be called several times before
    	 * the nohz_restart_sched_tick is called. This happens when
    	 * interrupts arrive which do not cause a reschedule. In the
    	 * first call we save the current tick time, so we can restart
    	 * the scheduler tick in nohz_restart_sched_tick.
    	 */
    	if (!ts->tick_stopped) {
    		calc_load_nohz_start(); // --- 5
    		quiet_vmstat();
    
    		ts->last_tick = hrtimer_get_expires(&ts->sched_timer);
    		ts->tick_stopped = 1; // --- 6
    		trace_tick_stop(1, TICK_DEP_MASK_NONE);
    	}
    
    	ts->next_tick = tick;
    
    	/*
    	 * If the expiration time == KTIME_MAX, then we simply stop
    	 * the tick timer.
    	 */
    	if (unlikely(expires == KTIME_MAX)) { // --- 7
    		if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
    			hrtimer_cancel(&ts->sched_timer);
    		else
    			tick_program_event(KTIME_MAX, 1);
    		return;
    	}
    
    	if (ts->nohz_mode == NOHZ_MODE_HIGHRES) { // --- 8
    		hrtimer_start(&ts->sched_timer, tick,
    			      HRTIMER_MODE_ABS_PINNED_HARD);
    	} else {
    		hrtimer_set_expires(&ts->sched_timer, tick);
    		tick_program_event(tick, 1);
    	}
    }
    1. ts->timer_expires_base 는 NO_HZ 환경에서 next timer event 를 계산할 때, 기준이 되는 값이다. NO_HZ 환경에서 next timer event 를 계산하는 기능은 `tick_nohz_next_event` 함수에서 맡고있다. 

    2. tick_do_timer_cpu 는 jiffies 를 업데이트 시킬 CPU 번호가 저장되어 있다. 그런데, 이 CPU 또한 IDLE로 진입할 수 있다. 이 때, 해당 CPU는 tick_do_timer_cpu 에 TICK_DO_TIMER_NONE 을 저장한다. 이 의미는 `jiffies 를 업데이트 시킬 CPU가 부재되어 있다`는 뜻이다. `tick_sched_do_timer` 함수에서 이 값을 통해 jiffies 를 업데이트 시킬 CPU 를 다시 설정하게 된다. ts->do_timer_last 는 이전에 시스템 jiffies 업데이트 책임자가 누구였는지를 나타낸다.

    3. 만약, tick 이 멈춘 상황에서(ts->tick_stopped) next timer event(expires) 와 last timer event(ts->next_tick) 의 발생 시점이 같다면, 타이머를 re-programming 할 필요가 없다. 왜냐면, 이 시점에서는 아직 ts->next_tick 을 업데이트하지 않았기 때문에, ts->next_tick 은 last_tick 이 된다. 그런데, 이게(ts->next_tick) 새로운 next event(expires) 와 같다면, 뭔가 문제가 발생한 것이다. 여기서 좀 더 디테일한 검사가 필요하다.

    먼저, `if(tick == KTIME_MAX)` 라면, next event 가 필요없다는 것을 의미한다. 왜냐면, 이 코드까지 왔다는 것은 `tick == expires` 이라는 것을 의미하고, expires 가 KTIME_MAX 면, 타이머를 멈출 일만 남았다. 그런데, ts->tick_stopped 이 true 이기 때문에, 이미 tick 은 멈춰있다. 그러므로, 즉각적으로 이 함수를 종료할 수 가 있다.

    그리고, `if(ts->next_tick == hrtimer_get_expires(&ts->sched_timer)` 가 참이면, 이것 또한 next event 가 없음을 의미한다. 왜냐면, 이 시점에 ts->next_tick 에 저장된 값은 last tick 이다. 그리고, hrtimer_get_expires 함수에서도 last tick 을 반환한다면, hrtimer 에 pending next event 가 없음을 의미하기 때문에, 즉각적으로 종료가 가능하다.

    5. global cpu load average 를 업데이트한다. 이 함수는 로드 밸런스를 어떻게 해야할 지에 대한 지표를 제공해주는데, 별도의 글에서 다루도록 한다.

    6. 아직, period tick 이 동작하고 있다면 `ts->tick_stopped` 을 SET 함으로써 period tick 을 막는다. 그런데, 실제 tick 을 막는 코드는 뒤에 `if (unlikely(expires == KTIME_MAX)) { ... }` 조건문안에 들어가야 되는거 아닌가? 즉, `ts->tick_stopped = 1` 코드는 위의 코드안에 들어가야 하는거 아닐까? 위에 조건문안에 들어가면, next event 거 없기 때문에 즉각적으로 tick 종료한다. 그런데, expires != KTIME_MAX 라면 next tick 이 있기 때문에, next event 만 처리한 후에 period tick 을 종료한다.

    어떻게 보면, `tick_nohz_stop_tick` 함수를 호출하는 것 만으로 `ts->tick_stopped = 1` 을 하기 때문에 무조건 period tick 은 stop 이 된다. 이걸 확인하기 위해서는 `tick_nohz_handler` / `tick_sched_tiemr` 를 확인해보면 알 수 있다. 2개의 함수 모두 `ts->tick_stopped` 여부에 따라 더 이상 period tick 을 발생시키지 않는다.

    7. expies == KTIME_MAX 인 경우는 필요한 next timer event 가 없다는 뜻이다. 이런 경우에는 타이머를 stop 하게 된다. 그런데, `unlikely` 를 사용한 것만 봐도 알겠지만, 사실 이런 경우는 극히 드물다. 그렇다면, 어떤 경우에 expires 가 KTIME_MAX 가 될까? ts->timer_expires 값은 tick_nohz_next_event 함수에서만 변경된다. 그런데, 이 함수는 next timer event 를 계산할 때, get_next_timer_interrupt 함수를 통해서 next tick 을 얻는 케이스가 있는데, 이 함수에서 CPU 가 offline 이거나 pending timer 가 없으면 KTIME_MAX를 리턴한다.

    8. 이 코드까지 왔다는 것은 next event 가 TICK_NSEC 보다 크다는 것을 의미한다. 즉, 다음에 발생할 타이머 인터럽트는 한 주기보다 더 뒤에 발생한다는 뜻이다. 타이머를 예약할 때는, NOHZ 환경에서 동작하는 모드(ts->nohz_mode)에 따라, 사용할 API 가 달라진다. 

     

     

    - How to re-start normal tick from stoped tick ?

    1. Re-start tick

    : `tick_nohz_idle_restart_tick` 함수는 `tick_nohz_idle_stop_tick` 과 pair 를 이룬다. 그렇기 때문에, tick 이 멈추지 않았다면 이 함수는 아무동작도 하지않고 종료한다. tick 이 멈춰있다면, tick 을 재시작하고 이전 idle time 을 계산한다. 

    // kernel/time/tick-sched.c - v6.5
    void tick_nohz_idle_restart_tick(void)
    {
    	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
    
    	if (ts->tick_stopped) {
    		ktime_t now = ktime_get();
    		tick_nohz_restart_sched_tick(ts, now);
    		tick_nohz_account_idle_time(ts, now);
    	}
    }

     

    : `tick_nohz_restart_sched_tick` 함수는 tick을 재시작 하기전에 prepare 작업들을 수행하고, 본격적으로 periodic tick 을 재시작한다.

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_restart_sched_tick(struct tick_sched *ts, ktime_t now)
    {
    	/* Update jiffies first */
    	tick_do_update_jiffies64(now); // --- 1
    
    	/*
    	 * Clear the timer idle flag, so we avoid IPIs on remote queueing and
    	 * the clock forward checks in the enqueue path:
    	 */
    	timer_clear_idle(); // --- 2
    
    	calc_load_nohz_stop(); 
    	touch_softlockup_watchdog_sched();
    	/*
    	 * Cancel the scheduled timer and restore the tick
    	 */
    	ts->tick_stopped  = 0; // --- 3
    	tick_nohz_restart(ts, now); // --- 4
    }
    1. tickless 에서는 next tick & jiffies 가 불규칙적으로 동기화된다. re-start 가 시작되었으니, 이제 곧 period tick 을 발행할 것이다. 그러모르, 미리 next tick & jiffies 업데이트한다.

    2. tickless 가 되면 timer 또한 idle 상태로 진입한다고 볼 수 있다. 그런데, 해당 CPU 때문에 다른 곳에서 문제가 생겨서 빨리 IDLE 에서 깨어나야 하는 경우가 발생할 수 있다. 이 때, IPI 를 통해서 wake-up 하게 되는데, CPU 가 이제 깨어날 것이므로 IPI 를 받을 필요가 없다.

    3. ts->tick_stopped = 0 함으로써, dyntick 이 끝나고 period tick 으로 복귀됨을 의미한다.

    4. 이전에 설정된 타이머를 제거하고, 새로운 period tick 을 설정한다. 바로 뒤에서 다시 설명한다.

     

    : `tick_nohz_restart` 함수는 periodic tick 을 다시 재개한다.

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
    {
    	hrtimer_cancel(&ts->sched_timer); // --- 1
    	hrtimer_set_expires(&ts->sched_timer, ts->last_tick); // --- 2
    
    	/* Forward the time to expire in the future */
    	hrtimer_forward(&ts->sched_timer, now, TICK_NSEC); // --- 2
    
    	if (ts->nohz_mode == NOHZ_MODE_HIGHRES) { // --- 3
    		hrtimer_start_expires(&ts->sched_timer,
    				      HRTIMER_MODE_ABS_PINNED_HARD);
    	} else {
    		tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
    	}
    
    	/*
    	 * Reset to make sure next tick stop doesn't get fooled by past
    	 * cached clock deadline.
    	 */
    	ts->next_tick = 0; // --- 4
    }
    1. tick_nohz_restart 가 호출된다는 것은 CPU 가 idle exit 되었으니, period tick 을 다시 시작하라는 뜻이다. 그러므로, 이전에 예약된 dyntick 이 있다면, 즉각적으로 제거하고 period tick 을 발행시킬 준비를 해야한다.

    2. hrtimer 에 next event 를 발행하려면, `now >= last tick` 이 성립해야 한다(hrtimer_forward 함수 내부 구조 참고). 그런데, 이 코드가 실행될 때는 이전 dyntick 에서 next tick 이 예약되어 있을 수 있다. 이 next tick 은 `now <= next tick` 일 가능성이 높기 때문에, hrtimer_forward 함수를 사용하기 위해서는 tick 값을 now 보다 이전 tick 으로 조정해 줄 필요가 있다. 그리고, period tick 을 발생시켜야 함으로 hrtimer_forward 세 번째 인자에 TICK_NSEC 를 전달한다.

    이 시점에서 예약되는 next event 는 TICK_NSEC 에 정렬되어 있지 않을 수 있다. 그러나, 타이머 인터럽트가 호출될 때 마다, tick_do_update_jiffies64 함수를 통해서, 그 이후에 next event 들은 TICK_NSEC 에 정렬된다. 

    3. next event 를 예약한다.

    4. ts->next_tick 필드는 tickless 환경에서만 사용된다. 특히, next event 와 last event 와 같은지를 비교하는 목적으로 사용된다(tick_nohz_stop_tick 함수 참고). 그래서, tickless 가 끝나면 사용되지가 않는다. 그러므로, 0 으로 초기화한다.

     

    : `tick_nohz_account_idle_time` 함수는 skipped tick`s 을 계산해서 idle time 에 추가한다.

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_account_idle_time(struct tick_sched *ts,
    					ktime_t now)
    {
    	unsigned long ticks;
    
    	ts->idle_exittime = now; // --- 1
    
    	if (vtime_accounting_enabled_this_cpu())
    		return;
    	/*
    	 * We stopped the tick in idle. Update process times would miss the
    	 * time we slept as update_process_times does only a 1 tick
    	 * accounting. Enforce that this is accounted to idle !
    	 */
    	ticks = jiffies - ts->idle_jiffies; // --- 2
    	/*
    	 * We might be one off. Do not randomly account a huge number of ticks!
    	 */
    	if (ticks && ticks < LONG_MAX) // --- 3
    		account_idle_ticks(ticks);
    }
    1. idle 상태에 있었던 시간을 계산할 수 있다는 것은 idle 이 끝났다는 것을 의미한다. 아직 idle 이 진행중인데, idle 상태에 있었던 시간을 계산할 수 있을까? 

    2. CPU 가 IDLE 상태에 얼마나 있었는지를 계산하기 위해서 정확한 IDLE 상태에서 발생한 tick 의 개수를 알아야 한다. 예를 들어, tick 이 10번 발생했을 때, IDLE 상태에서 tick 이 4번 발생했다면, 10번 tick 주기동안 CPU 가 60% 만 ACTIVE 했다라고 평가할 수 있다. 이걸 평가하는 함수가 update_process_times 함수인데, 이 함수는 호출될 때 마다, 1 tick 을 기준으로 계산한다(`account_process_tick` 함수 참고). 즉, tickless 에서 3 주기 만에 1개의 tick 이 발생하더라도, 1 tick 만 계산한다는 뜻이다. 이 때, 2개의 tick 이 누락된 것이다.

    ts->idle_jiffies 는 tick 을 멈춘 후에 가장 최신 jiffies(ts->last_jiffies)를 저장하게 된다. 그 이후 타이머 인터럽트가 발생할 때 마다, 1씩 증가한다. 그러나, skipped tick`s 들에 대해서는 저장하지 않는다. 이 skipped tick`s 들은 모두 tick_do_update_jiffies64 함수에서 jiffies_64(jiffies)에 저장한다. 결국, skipped tick`s 의 개수를 알기 위해서는 jiffies(jiffies_64) - ts->idle_jiffies 를 하면 된다.

    : 위에 그림을 보면, jiffies 에는 총 13개의 tick 이 기록되어 있다. Start NOHZ  전에는 모두 normal tick 이 발생한 구간이다. 즉, 첫 번째 타이머 인터럽트가 발생한 시점을 기준으로 보면 된다. jiffies 와 ts->idle_jiffies 은 첫 번째 타이머 인터럽트때 까지만 해도 값이 동일하게 6이다. 그러나, 두 번째 타이머 인터럽트가 발생하면 skipped tick`s 까지 계산하는 jiffies 는 9가 되고, ts->idle_jiffies 는 7이 된다. 최종적으로 jiffies 는 13개의 tick 을 tracking 했고, ts->idle_jiffies는 8개를 tracking 했다. 2개를 빼면, 빨간색 박스로 표시된 tick 과 같이 총 5개의 skipped tick`s 이 있다는 것을 알 수 있다.

    3. 위에서 계산된 값의 유효성을 판단하여, idle time 을 계산할 때 metric 으로 사용하기 위해 전달된다.
    // kernel/sched/cputime.c - v6.5
    /*
     * Account multiple ticks of idle time.
     * @ticks: number of stolen ticks
     */
    void account_idle_ticks(unsigned long ticks)
    {
        u64 cputime, steal;
        ....

        cputime = ticks * TICK_NSEC;
        ....
        
        account_idle_time(cputime);
    }

     

    2. Exit tickless

    : CPU 가 IDLE 에서 깨어나면, periodic tick 을 재시작하기 위해 제일 먼저 호출하는 함수가 `tick_nohz_idle_exit` 함수다. 말 그대로, tickless 를 종료하고, periodic tick 을 re-start 한다.

    // kernel/time/tick-sched.c - v6.5
    /**
     * tick_nohz_idle_exit - restart the idle tick from the idle task
     *
     * Restart the idle tick when the CPU is woken up from idle
     * This also exit the RCU extended quiescent state. The CPU
     * can use RCU again after this function is called.
     */
    void tick_nohz_idle_exit(void)
    {
    	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
    	bool idle_active, tick_stopped;
    	ktime_t now;
    
    	local_irq_disable();
    
    	WARN_ON_ONCE(!ts->inidle);
    	WARN_ON_ONCE(ts->timer_expires_base);
    
    	ts->inidle = 0; // --- 1
    	idle_active = ts->idle_active;
    	tick_stopped = ts->tick_stopped;
    
    	if (idle_active || tick_stopped) // --- 2
    		now = ktime_get();
    
    	if (idle_active)
    		tick_nohz_stop_idle(ts, now); // --- 3
    
    	if (tick_stopped)
    		tick_nohz_idle_update_tick(ts, now); // --- 4
    
    	local_irq_enable();
    }
    1. ts->inidle 필드는 `tick_nohz_idle_exit` 함수를 호출하면 CLEAR 되고, `tick_nohz_idle_enter` 함수를 호출하면 SET 된다. 이 말은 위 2개 함수가 항상 `paired` 로 동작해야 함을 의미한다. 그러므로, tick_nohz_idle_exit 함수를 호출할 때, ts->inidle 필드는 반드시 1 이라는 것을 전제해야 한다. 즉, tick_nohz_idle_exit 함수를 호출하면, 반드시 ts->inidle 을 0으로 만들고, tick_nohz_idle_enter 함수를 호출하면 반드시 1로 만들어야 한다[참고1].

    앞에서 단순히 ts->inidle 은 tick_nohz_idle_enter 함수가 호출되는 용도의 의미정도라고 했다. 여기서 그 의미를 알 수가 있다. ts->idle_active 와 ts->tick_stopped 은 변수의 값을 체크해서 특정 함수를 호출할지 말지를 판단한다. 즉, 완전하게 paried 로 동작해야 함을 보장해야 한다는 뜻이다. 그런데, inidle 같은 경우는 tick_nohz_idle_exit 함수가 호출될 경우, 무조건 0 이 된다. 즉, 이 값을 체크하지 않고 무조건 바꾼다는 것은 앞에 어떤 값이 었는지 상관없다는 뜻이다. 즉, 저 값에 의존하는 기능이 없다는 것을 의미한다. 예를 들어, ts->idle_active 가 1이면, sched_clock_idle_sleep_event 가 호출되었음을 의미한다. 그래서, tick_nohz_idle_exit 함수에서는 idle_active 가 1일 때만, sched_clock_idle_sleep_event 의 반대 함수인 sched_clock_idle_wakeup_event 함수를 호출한다. 이 말은 sched_clock_idle_wakeup_event 함수가 호출되려면 반드시 먼저 sched_clock_idle_sleep_event 가 먼저 호출되어야 함을 말하는 것이다. tick_stopped 도 이와 같이 함수의 의존 관계를 표현하기 위해서 사용된다. 그러나, inidle 은 그런게 없다. 의존 관계를 표현하지는 않지만, tick_nohz_idle_enter 함수가 호출되었다는 것을 표현해준다.

    2. iowait 때문이면, idlc_active 만 1 이 된다. 그러나, tickless 라면 idle_active 와 tick_stopped 2개 모두 1이 된다. 2개 모두 아니라면 idle 에 들어갔다고 보기가 어렵기 때문에, idle time 계산을 하지 않는다.

    3. tick_nohz_stop_idle 은 뒤에서 보겠지만, idle time 을 계산하는 함수다. 그런데, 왜 idle_active 가 true 일 때만 호출할까 ? tickless 때문에 발생한 idle 에 대한 시간 계산 뿐만 아니라, iowait 에 대한 부분도 같이 계산한다. 

    4. 멈췄던 periodic tick 을 restart 한다.

     

    : `tick_nohz_stop_idle` 함수는 직전 idle time 을 계산하는 함수다. 

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_stop_idle(struct tick_sched *ts, ktime_t now)
    {
    	ktime_t delta;
    
    	if (WARN_ON_ONCE(!ts->idle_active)) // --- 1
    		return;
    
    	delta = ktime_sub(now, ts->idle_entrytime); // --- 2
    
    	write_seqcount_begin(&ts->idle_sleeptime_seq);
    	if (nr_iowait_cpu(smp_processor_id()) > 0)
    		ts->iowait_sleeptime = ktime_add(ts->iowait_sleeptime, delta); // --- 2
    	else
    		ts->idle_sleeptime = ktime_add(ts->idle_sleeptime, delta); // --- 2
    
    	ts->idle_entrytime = now; // --- 3
    	ts->idle_active = 0; // --- 3
    	write_seqcount_end(&ts->idle_sleeptime_seq);
    
    	sched_clock_idle_wakeup_event();
    }
    1. tick_nohz_stop_idle 과 tick_nohz_start_idle 은 반드시 pair 로 호출되어야 한다.

    2. 이 코드가 실행되는 시점에 ts->idle_entrytime 에는 tick_nohz_start_idle 함수가 호출될 때의 시간이 들어가 있다. now 는 말 그대로 현재 시간을 의미한다. 2개를 뺀다는 것은, 즉, delta 는 직전에 얼마나 idle 상태에 있었는지를 나타낸다. 그런데, CPU가 IDLE 에 있다가 인터럽트(어떤 인터럽트든 상관없다. wake-up 인터럽트기만 하면 된다) 때문에 wake-up 했을 때, 2가지를 판단하여 idle time 을 계산한다.
    1. I/O Wait(I/O sleeptime) : I/O 결과를 기다리느라 IDLE 에 빠진 시간 -> ts->iowait_sleeptime
    2. tickless : next event 를 기다리는 동안 할 게 없어서 IDLE 에 빠진 시간 -> ts->idle_sleeptime
    그런데, 왜 ts->idle_entrytime 을 now 로 변경할까? [참고1]

    3. `ts->idle_entrytime = now` 의 의미가 뭘까? 왜 idle 시작 시점을 나타내는 필드에 idle 종료하는 시점을 저장하는 걸까? 이 코드의 의미는 이제 더 이상 idle_entrytime 필드가 유효하지 않다는 것을 의미한다. 좀 더, 정확하게 말하면 ts->idle_active 가 0 일 때는 ts->idle_entrytime 도 유효하지 않다는 것을 의미한다. 이 근거는 아래의 코드에서 찾아볼 수 있다. idle_active 가 true 일 때, idle_entrytime 를 반환한다 .
    static u64 get_cpu_sleep_time_us(struct tick_sched *ts, ktime_t *sleeptime, bool compute_delta, u64 *last_update_time)
    {
        ktime_t now, idle;
        ....

        do {
            ....

            if (ts->idle_active && compute_delta) {
                ktime_t delta = ktime_sub(now, ts->idle_entrytime);
                ....
        } ....
    ....
    }
    그런데, 하나 걱정되는 부분이 있다. tick_nohz_get_sleep_length 함수가 외부로 export 되어있는데, ts->idle_entrytime 을 사용할 때, idle_active 를 검사하지 않는다. v6.5 를 기준으로 아래 함수가 사용되는 위치가 모두 idle_active 가 true 인 곳으로 보이지만, 개인적으로 유효성 판단이 필요해보인다.

    ktime_t tick_nohz_get_sleep_length(ktime_t *delta_next)
    {
        ....
        ktime_t now = ts->idle_entrytime;
        ....
    }

     

     

    : `tick_nohz_idle_update_tick` 함수는 tick을 re-start 시키는 함수다. 

    // kernel/time/tick-sched.c - v6.5
    static void tick_nohz_idle_update_tick(struct tick_sched *ts, ktime_t now)
    {
    	if (tick_nohz_full_cpu(smp_processor_id()))
    		__tick_nohz_full_update_tick(ts, now);
    	else
    		tick_nohz_restart_sched_tick(ts, now);
    
    	tick_nohz_account_idle_time(ts, now);
    }
    1. full-tickless / dyntick 인지 여부에 따라 호출하는 함수가 달라진다. 왜냐면, 2개의 구조 및 동작이 서로 달라서 tick을 다시 활성화하는 방법도 다르기 때문이다. full-tickless 는 서버-사이드 글에서 별도로 다룰 것이기 때문에, 여기서는 dyntick 을 기준으로 설명한다. 그런데, tick_nohz_restart_sched_tick 함수는 위에 `Re-start tick` 섹션에서 설명했다. 해당 글을 참고하자.

    2. tick_nohz_account_idle_time 함수 또한 위에 `Re-start tick` 섹션에서 설명했다. 해당 글을 참고하자.

     

Designed by Tistory.