ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Timer - tick layer
    Linux/kernel 2023. 9. 28. 14:48

    글의 참고

    - https://github.com/torvalds/linux

    - https://blog.csdn.net/Roland_Sun/article/details/105667098?spm=1001.2014.3001.5502


    글의 전제

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

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


    글의 내용

    - Overview

    : tick layer 는 주로 tick-broadcast 와 tick-sched 에서 자주 참조된다. tick-layer 는 하드웨어에 의존적인 `clock event device`를  추상화시키는 역할을 맡고있다. 그러나, clock event device 의 모든 기능을 추상화 시키지는 못하고, `register` 및 `power` 관련 기능들에 대해서는 추상화된 API를 제공한다.

     

    : broadcast tick / scheduling tick 메커니즘은 완전히 다르기 때르다. 그래서, 공통 부분을 추상화시키는 것이 오히려 코드를 더 복잡하게 만든다. 그리고, 각 알고리즘에 따라 하드웨어에 의존하는 부분 또한 서로 다르기 때문에 (특히 타이머 핸들러. 3개의 레이어에서 사용하는 타이머 핸들러가 모두 다르다), 직접 clock event device API 를 사용한다. 물론, tick layer 에서 공통 핸들러로 제공되는 tick_handle_periodic 은 모든 레이어에서 디폴트 핸들러로 사용될 수 있다.

     

     

    - Data structure

    // kernel/time/tick-sched.h - v6.5
    struct tick_device {
    	struct clock_event_device *evtdev;
    	enum tick_device_mode mode;
    };

    : `clock_event_device` 구조체는 타이머 인터럽트를 발생시키는 디바이스를 소프트웨어적으로 표현하기 위한 구조체다. 그리고, `mode` 는 tick device 의 소프트웨어 동작 모드를 나타낸다. 틱 다비이스의 동작 모드에는 `주기 모드`와 `싱글 샷` 모드가 있다.

     

    // kernel/time/tick-sched.h - v6.5
    enum tick_device_mode {
    	TICKDEV_MODE_PERIODIC,
    	TICKDEV_MODE_ONESHOT,
    };

    : 리눅스 커널에 등록된 `tick device`의 동작 모드는 반드시 `PERIODIC` 혹은 `ONESHOT` 모드 중 하나여야 한다. 그런데, 여기서 주의할 점이 있다. `clock_event_device(하드웨어)`와 `tick_device(소프트웨어)`는 개념적으로 다르다. 예를 들어, 타이밍 디바이스가 하드웨어적으로 `periodic mode`로 동작하고 있을 때, `tick device(소프트웨어)`는 `periodic mode`만 사용가능하다. 그러나, 타이밍 디바이스가 하드웨어적으로 `one-shot(single-trigger) mode`로 동작하고 있다면, 이건 소프트웨어적으로 `periodic mode` 혹은 `one-shot mode` 모두가 가능하다. 즉, `tick_device_mode` 모드는 현재 `tick device`가 소프트웨어적인 동작 모드임을 명심해야 한다(`clock event device.feature`와 헷갈리면 안된다).

     

      clock_event_device.features clock_event_device.state_use_accessors tick_device.mode
    타입 하드웨어 하드웨어 소프트웨어
    의미 하드웨어에서 어떤 기능들을 지원 해주는지 나타냄. 하드웨어에서 지원하는 기능중에 현재 어떤 기능을 사용하고 있는지를 나타냄. 소프트웨어적으로 동작하고 있는 모드
    예시 현재 디바이스는 ONESHOT & PERIOD 를 모두 지원함. 현재 디바이스는 ONESHOT & PERIOD 를 모두 지원하지만, 한 시점에는 하나의 동작 모드만 ENABLE 되어야 한다.  1. 하드웨어가 ONESHOT MODE 로 ENABLE 되어 있을 경우 : 소프트웨어적으로 `TICKDEV_MODE_PERIODIC` 과 ``TICKDEV_MODE_ONESHOT` 모두 설정 가능.

    2. 하드웨어가 PERIODIC MODE 로 ENABLE 되어 있을 경우 : 소프트웨어적으로 `TICKDEV_MODE_PERIODIC` 만 설정 가능.

     

    : tick layer 에서 정의하는 전역 변수들은 broadcast layer 와 tick-sched layer 에서 많이 사용된다.

    // kernel/time/tick-common.c - v6.5
    
    DEFINE_PER_CPU(struct tick_device, tick_cpu_device); // --- 1
    
    ktime_t tick_next_period; // --- 2
    
    /*
     * 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; // --- 3
    #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; // --- 4
    #endif
    1. 멀티 프로세서가 도래하면서, 각 CPU 코어마다 별도의 인터럽트 컨트롤러와 타이머가 할당되었다. 이걸 표현하기 위해 CPU 마다 개별적인 tick_device(타이머)를 할당한다.


    2. `tick_next_period` 변수는 jiffies 기반이 아닌, 수십시간부터 나노초까지 타이머 인터럽트를 지원하기 등장했다. 이 변수에는 다음에 tick 이 발생할 시간이 nano-second 단위로 저장된다. 이 변수는 2가지 용도로 사용된다.
    1. 현재 시간을 요청한 경우  
    2. nex tick 을 결정해야 하는 경우
    : next tick 을 결정하는 과정은 다음과 같다.
    1.occur timer interrupt
    2.call timer handler
    3. tick_next_period += ${TICK_PERIOD}
    4. next_tick = tick_next_period
    5. write next_tick to timer register  
     
    3. tick_do_timer_cpu 는 jiffies 업데이트하는 CPU 를 지정한다. 시스템에 전역적으로 하나만 존재하는 jiffies 를 여러 CPU 가 업데이트하면, 동기화 문제가 발생한다. 그러므로, 한 개의 CPU 만 jiffies 를 업데이트해야 한다. 참고로, 이 변수는 NOHZ 환경에서 `jiffies duty` 관련해서 복잡한 프로세스에 기여하게 된다. 이 글을 참고하자.


    4. tick_do_timer_boot_cpu 는 NO_HZ_FULL(full tickless) 컨피그가 설정된 경우에만 사용된다. full tickless 는 IDLE 상태에서 뿐만 아니라, NORMAL 상태에서도 타이머 인터럽트를 OFF 한다. 그렇다면, 어떻게 타이머 인터럽트를 받을까? 딱 한개의 CPU 만 타이머 서비스를 핸들링한다. 그 역할을 boot CPU 가 맡는다. 왜, 이 역할을 boot CPU 가 맡아야 할까? 제일 먼저 tick_setup_device 함수를 호출하는 함수를 호출해서 시스템 글로벌 tick 을 초기화하는 녀석이 boot CPU 이기 때문이다. boot CPU 가 tick 을 초기화하는 시점부터가 시스템에 시간이라고 개념이 도입되는 순간이기도 하다. 그래서, boot CPU 가 이 역할을 맡게된다.

     

    : jiffies 와 tick_next_period 의 차이가 뭘까? tick_periodic 함수를 보면 알 수 있다. 타이머 인터럽트가 발생하면, jiffies 에는 `1`을 더한다. 그런데, tick_next_peirod 에는 TICK_NSEC 를 더한다. 이게 무슨 의미일까? 해당 내용은 dyntick 에서 다루도록 한다.

    // kernel/time/timekeeping.c - v6.5
    void do_timer(unsigned long ticks)
    {
    	jiffies_64 += ticks;
    	....
    }
    
    // kernel/time/tick-common.c - v6.5
    static void tick_periodic(int cpu)
    {
    	if (tick_do_timer_cpu == cpu) {
    		....
    
    		/* Keep track of the next tick event */
    		tick_next_period = ktime_add_ns(tick_next_period, TICK_NSEC);
    
    		do_timer(1);
    		....
    	}
    	....
        
    }

     

     

     

    - How to initialize a tick layer ?

    : `tick_init` 함수는 start_kernel 함수에서 호출되는 함수다. 즉, 커널이 초기화되는 시점에 호출되는 함수다. 타이머 서브-시스템 중에서는 timekeeping 다음으로 초기화가 빠른 레이어다.

    - start_kernel
      - tick_init
        - tick_broadcast_init
        - tick_nohz_init

     

    : 간단하게 broadcast layer 와 tick-sched layer 를 초기화한다. 이 2개의 레이어는 CPUIDLE 과 직접적으로 관련이 있는 부분이기 때문에, 각각 별도의 글에서 다루기로 한다.

    // kernel/time/tick-common.c - v6.5
    /**
     * tick_init - initialize the tick control
     */
    void __init tick_init(void)
    {
    	tick_broadcast_init();
    	tick_nohz_init();
    }

     

     

     

    - How to register new clock event device in tick layer ?

    : 커널에 새로운 `clock event device` 등록할 때, `clockevents_regiseter_device -> tick_check_new_device` 순으로 함수가 호출된다. 이 섹션에서는 제조사에서 코드에 작성한 clock event device 가 커널에 어떻게 등록되는지 살펴본다. 타이머 초기화 과정을 ARM 아키텍처(arm generic timer)를 기준으로 설명한다.

     

    : `clock event device`가 시스템에 등록되는 과정은 다음과 같다.

    1. BSP 로컬 타이머를 `clock event device`로 등록한다.
    2. 시스템 글로벌 HW 타이머를 `clock event device`로 등록한다.
    3. AP`s 로컬 타이머를 `clock event device`로 등록한다.

     

    : 먼저 `BSP 로컬 타이머를 `clock event device`로 등록한다` 과정에 대해 분석해본다. 새로운 clock event device 가 시스템에 등록 될 때, 어떤 `broadcast tick` 관련 동작들이 수행될까 ? 새로운 clock event device 가 커널에 등록하려면, clockevents_register_device 함수를 호출해야 한다. 그런데, 실제 커널에 clock event device 를 등록하는 기능은 `tick_check_new_device` 함수가 맡고있다.

    // kernel/time/tick-common.c - v6.5
    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;
    
    	if (!tick_check_replacement(curdev, newdev)) // --- 1
    		goto out_bc;
    
    	if (!try_module_get(newdev->owner))
    		return;
    
    	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)); // --- 2
    	if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
    		tick_oneshot_notify(); // --- 3
    	return;
    
    out_bc:
    	
    	tick_install_broadcast_device(newdev, cpu); // --- 4
    }

    1. 커널에 새롭게 등록된 clock_event_device가 어떻게 사용될지가 정해진다. 여기서 사용되는 용도는 2가지가 있다. 새로운 clock event device를 `per-CPU 타이머` 혹은`브로드 캐스트 타이머`로 사용할지를 결정해야 한다.

    : `tick_check_replacement` 함수는 새로 등록된 타이머가 기존 타이머를 대체할 수 있는지를 판단한다. 만약, 반환값이 `true`면, 현재 CPU의 로컬 타이머를 새롭게 등록된 타이머로 교체한다. 만약, `false`를 반환하면 브로드 캐스트 타이머로 사용되도록 한다. 이 함수에서 타이머를 교체하는 판단 기준은 다음과 같다.

     

    1. tick_check_percpu : 먼저 새로 등록된 타이머가 per-CPU 타이머 인지를 판단한다. 만약, 로컬 타이머가 아니라면, 브로드 캐스트 타이머로 등록되도록 한다.

    2. tick_check_perferred : per-CPU 타이머 인지가 확정이 됬다면, 이게 기존에 등록되어 있는 per-CPU 타이머보다 더 적합한지를 판단한다. `tick_check_percpu` 함수에서 합격된 타이머는 per-CPU 타이머에 적합하다고 판단할 수는 있지만, 기존것보다 좋은지는 알 수 없다. 적합한지에 대한 기준은 아래에서 설명한다.

     

    // drivers/base/class.c - v6.5
    bool tick_check_replacement(struct clock_event_device *curdev,
    			    struct clock_event_device *newdev)
    {
    	if (!tick_check_percpu(curdev, newdev, smp_processor_id()))
    		return false;
    
    	return tick_check_preferred(curdev, newdev);
    }

     

    : `tick_check_percpu` 함수는 새로운 타이머가 현재 CPU와 동일 프로세서내에 있는지를 판단한다. 만약, true를 반환하면, 2가지를 의미를 갖는다. 

      의미
    2번 조건문에서 true 반환 동일 프로세서 내에 타이머가 존재. 무조건 교체한다.
    함수 종료시점에 true 반환 기존에 등록된 per-CPU 타이머가 없고, 제조사에서 선정한 브로드 캐스트 타이머라면 true 를 반환한다.

    : `0` 을 반환하면 2 가지 경우가 있다.

    1. 로컬 타이머지만, per-CPU 타이머는 아닌 경우 : 주로 (1) 조건문에서 걸림
    2. 브로드 캐스트 타이머인 경우 : 주로 (4) 번 조건문에서 걸림

     

    : `cpumask_test_cpu` 함수는 `cpu`가 `cpumask`에 SET 되어있으면, true를 반환한다. 예를 들어, 번호가 2번인 CPU면, cpumask 에서는 2번째 비트가 SET 되어있어야 한다. 브로드 캐스트 타이머는 모든 코어에게 인터럽트를 발생시킬 수 있기 때문에, `cpumask`가 전부 1이다(정확히는 `cpu_possible_mask` 에만 해당). 그러므로, 브로드 캐스트 타이머라면, cpuasmk_test_cpu 함수를 무조건 통과하게 된다. 일단, 이 조건만으로 동일 프로세서내에 존재하지 per-CPU 타이머는 거르게 된다. 그 다음 조건문이 이 함수의 핵심이다. `cpumask_equal` 함수는 새로운 타이머와 CPU 코어가 동일 프로세서내에 있는지를 체크한다. 만약, `cpu`가 3이고, `newdev->cpumask`의 3번째 비트가 SET 되었다면, 동일 프로세서내에 존재한다는 뜻이므로, 곧 바로 true를 반환한다.

    // drivers/base/class.c - v6.5
    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;
    }

    : `irq_can_set_affinity` 함수는 인자로 전달된 irq가 `affinity` 될 수 있는지를 나타낸다. 반환값이 `0`이면, `irq affinity`가 불가능하다는 뜻이고, `1`이면 가능하다는 뜻이다. 그리고, 마지막으로 기존에 등록되어 있는 per-CPU 타이머가 이미 존재하고, 심지어, 동일 프로세서내에 존재하는 타이머라면, 교체를 하지 않는다. 대게, (4) 번 조건문까지 오는 타이머는 브로드 캐스트 타이머 뿐이다.

     

    : `tick_check_perferred` 함수는 더 좋은 성능을 가진 로컬 타이머를 찾는 함수다. 쉽게 글로벌 타이머보다 로컬 타이머를 더 선호한다는 소리다. `preferred`의 의미는 2가지가 있다. 

    1. one-shot mode를 지원해야 한다. 왜 `one-shot mode`를 지원하는 타이머를 더 선호할까? one-shot mode 를 지원할 경우, 소프트웨어를 통해서 period & one-shot tick 모두를 구현할 수 있기 때문이다. 그러나, period mode 만 지원하면, period tick 만 사용할 수 있다. 이 뿐만이 아니다. 일반적으로, `siggnle-trigger(one-shot)` 모드를 지원하는 타이밍 디바이스들이 `periodic` 모드만 지원하는 타이밍 디바이스보다 더 높은 정확성을 갖는다. 또, dyntick을 위해서는 반드시 one-shot mode 를 지원해야 한다.

    2. 기존 타이머와 새로운 타이머 모두가 one-shot mode 를 지원할 경우, 높은 `rating`을 선호. 여기서 `rating`은 타이머의 주파수 및 속성과 관계가 있다. 예를 들어, `System HW Timer(SPI-interrupt)` 보다는 `per-CPU local timer(PPI - interrupt)` 가 더 높다. 그리고, 타이머의 주파수가 높을 수 록 rating 도 높다.
    // drivers/base/class.c - v6.5 - v6.5
    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;
    	}
    
    	return !curdev ||
    		newdev->rating > curdev->rating ||
    	       !cpumask_equal(curdev->cpumask, newdev->cpumask);
    }

     

    : `tick_oneshot_mode_active`는 현재 디바이스가 `one-shot mode` 동작중인지를 나타낸다. 이 함수의 주요 용도는 `NO_HZ` 모드인지를 판별하는데 사용된다[참고1].

    // kernel/time/tick-oneshot.c - v6.5
    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;
    }

     

     

    2. `tick_setup_device` 함수는 커널에 새로 등록된 clock event device 를 현재 CPU의 로컬 타이머로 등록하는 함수다. 이 함수까지 도착하면 일단 per-CPU 타이머라는 것은 확정이다.

    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;
    
    	if (!td->evtdev) { // --- 1
    		if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) { // -- 2
    			tick_do_timer_cpu = cpu;
    			tick_next_period = ktime_get();
    #ifdef CONFIG_NO_HZ_FULL
    			if (tick_nohz_full_cpu(cpu))
    				tick_do_timer_boot_cpu = cpu;
              
    		} else if ( .... ) {
            	....
    #endif /* CONFIG_NO_HZ_FULL */
            	}
    
    		td->mode = TICKDEV_MODE_PERIODIC; // --- 3
    	} else { // --- 4
    		handler = td->evtdev->event_handler;
    		next_event = td->evtdev->next_event;
    		td->evtdev->event_handler = clockevents_handle_noop;
    	}
    
    	td->evtdev = newdev; // -- 5
    
    	if (!cpumask_equal(newdev->cpumask, cpumask)) // --- 6
    		irq_set_affinity(newdev->irq, cpumask);
    
    	if (tick_device_uses_broadcast(newdev, cpu)) // --- 7
    		return;
    
    	if (td->mode == TICKDEV_MODE_PERIODIC) // --- 8
    		tick_setup_periodic(newdev, 0);
    	else
    		tick_setup_oneshot(newdev, handler, next_event);
    }

    3.1 SMP 환경에서는 다수의 프로세서가 존재한다. 그런데, 동기화 문제없이 시스템 시간 관리를 위해서는 한 개의 CPU만 `jiffies` 업데이트하는 역할을 맞게 된다. 이러한 타이머를 `global tick device`라고 한다. tick_setup_device 함수가 최초로 호출되는 시점에는 jiffies 도 초기화가 안되어 있을뿐만 아니라, BSP의 per-CPU 타이머도 등록이 안되어 있는 상태다. 그러므로, `if(!td->evtdev) ...` 분기안을 수행하게 된다. BSP 및 AP에 따라 `if(!td->evtdev) ...` 조건문안에서 수행되는 내용은 다음과 같다.

    1. BSP : BSP 로컬 타이머를 `global tick device` 로 설정한다. 그리고, period time tick 의 기준이 되어주는 tick_next_period를 초기화한다. 최종적으로, 동작 모드를 `periodic mode`로 설정한다.
    2. AP : 로컬 타이머들의 동작 모드를 `periodic mode`로 설정한다.

     

    3.2 `tick_do_timer_cpu`는 `do_timer`를 호출할 수 있는 CPU 번호를 저장한다. `tick_do_timer_cpu`는 초기값이 `TICK_DO_TIMER_BOOT(-2)` 라면, 현재 시스템에 `global tick device`가 존재하지 않는다는 소리다. 즉, `jiffies`를 관리하는 `tick device`가 없다는 뜻이다. 그러므로, `tick_setup_device` 함수를 최초로 호출하면서 `tick device`가 없다면, 해당 CPU가 `global tick device`가 된다. 이 시점부터, 시스템 시간 및 `jiffies`는 `tick_do_timer_cpu`에 저장된 값을 가진 CPU가 담당하게 된다[참고1].

    / /kernel/time/tick-internal.h - v6.5
    # define TICK_DO_TIMER_NONE	-1
    # define TICK_DO_TIMER_BOOT	-2
    ....
    
    extern ktime_t tick_next_period;
    extern int tick_do_timer_cpu __read_mostly;

    : 이 시점에 ktime_get 함수를 통해 time_next_period 를 초기화하고 있다. tick_next_period 가 초기화된다는 것은 periodic tick 이 시작될 수 있음을 의미한다. 그런데, 이 시점에 ktime_get 함수는 정확한 시간을 보장해줄까? 기본적으로, ktime_get 은 `jiffies + delta` 기반으로 시간을 계산한다. jiffies 만 사용할 때 보다는 높은 정확도를 보장하지만, 그래도 jiffies 자체에 약간의 오차가 존재하기 마련이다. 그런데, 이 시점은 시스템에 최초의 clock event device 가 등록된 순간이다. 즉, 아직 jiffies 가 없는 순간이다. 그렇기 때문에, 하드웨어에서 가져온 값 그대로 최초의 값이 된다. 즉, 이 값이 기준이 되기 때문에 오차라는 개념이 사라진다. 이제 이 값을 기준으로 periodic tick이 정렬되어 타이머 인터럽트가 발생하게 된다.

     

     

    3.3 모든 per-CPU 타이머의 최초 동작 모드는 `PERIODIC MODE` 다. 즉, 주기적으로 타이머 인터럽트가 발생하는 모드다.

     

    3.4 만약에 현재 CPU에 기존에 등록된 틱 디바이스가 있다면, 타이머 디바이스가 교체되더라도 `동작 모드(periodic 혹은 one-shot)`는 계속 유지해야 한다. 예를 들어, 기존 per-CPU 타이머가 `one-shot mode`로 사용하고 있는 상황에서, 새로운 타이머로 교체되더라도, 기존 타이머와 동일하게 `one-shot mode` 로 동작해야 한다. 그러므로, 기존에 사용하던 handler 와 next time tick 을 새로운 타이머에 저장한다. 그리고, `td->mode` 는 수정되지 않는 것을 볼 수 있다. 이제 기존 타이머 핸들러가 동작하면 안되므로, 기존 핸들러에 `clockevents_handle_noop` 를 등록한다.

     

    3.5 기존 타이머 디바이스를 새로 등록된 디바이스로 교체한다.

     

    3.6 만약, 현재 CPU에 새로 등록된 타이머가 현재 CPU의 로컬 타이머가 아니라면, 타이머 인터럽트가 발생했을 때, 하드웨어상으로 현재 CPU 로 패스되지 않는다. 그러므로, 현재 CPU로 패스하도록 변경한다.

     

    3.7 `tick_device_uses_broadcast` 함수는 인자로 전달된 clock_event_device 에 broadcast 기능을 등록해주는 함수다. 모든 로컬 타이머는 다른 로컬 타이머를 깨울 수 있는 브로드 캐스트 기능(IPI)을 가지고 있어야 한다(`tick_device_setup_broadcast_func(dev)`)[참고1]. 이 함수의 반환값이 `0`이 아니면, 현재 브로드 캐스트 타이머 서비스가 필요하다는 것을 의미한다.

     

    3.8 이 코드까지 오게 되면, 현재 per-CPU 타이머가 period 브로드 캐스트 서비스를 요청한 것이 없다고 판단하여, per-CPU 타이머의 동작 모드를 변경할 수 있게된다. 왜냐면, period 브로드 캐스트 서비스 요청이 있는 상황에서 하드웨어 동작 모드를 바꾸는 것은 위험하기 때문이다. 여기서 변경되는 동작 모드는 2가지 케이스를 고려해 볼 수 있다.

    1. 최초로 타이머가 등록된 경우 : 이 때는 무조건 동작 모드가 PERIDO MODE 로 되어있다.
    2. 기존 타이머가 새로운 타이머로 교체된 경우 : 기존 타이머의 동작 모드를 따라간다. 기존에 타이머가 이미 존재할 경우, 타이머는 교체하지만, 동작 모드를 초기화하는 코드가 없다는 것을 확인할 수 있다.

     

    : `tick_setup_periodic` 함수는 인자로 전달된 clock event deivce 에 2가지 일을 할당한다.

    1. 인자로 전달된 clock event device 에게 period timer handler 를 설정한다. 
    2. 인자로 전달된 clock event device 의 하드웨어 동작 모드를 변경한다.
    // kernel/time/tick-common.c - v6.5
    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;
    }
    
    void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
    {
    	tick_set_periodic_handler(dev, broadcast); // --- 1
    
    	/* Broadcast setup ? */
    	if (!tick_device_is_functional(dev)) // --- 2
    		return;
    
    	if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) && // --- 3
    	    !tick_broadcast_oneshot_active()) {
    		clockevents_switch_state(dev, CLOCK_EVT_STATE_PERIODIC);
    	} else { // --- 4
    		unsigned int seq;
    		ktime_t next;
    
    		do {
    			seq = read_seqcount_begin(&jiffies_seq);
    			next = tick_next_period;
    		} while (read_seqcount_retry(&jiffies_seq, seq));
    
    		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
    
    		for (;;) {
    			if (!clockevents_program_event(dev, next, false))
    				return;
    			next = ktime_add_ns(next, TICK_NSEC);
    		}
    	}
    }

    8.1. 제일 먼저, 타이머 인터럽트 핸들러 함수를 설정한다. 이 때, `broadcast mode` 에서는 `tick_handle_periodic_broadcast` 함수가 사용된다. `broadcast mode`가 아닐 경우는 `tick_handle_periodic` 함수가 사용된다.

      normal period broadcast period
    timer handler tick_handle_periodic tick_handle_periodic_broadcast

     

    8.2 DUMMY TIMER는 per-CPU 타이머에 대한 설정되는 플래그다. 즉, 이 코드는 DUMMY 타이머로 설정된 per-CPU 타이머를 걸러낸다. 여기서 DUMMY TIMER 를 걸래내는 이유는 뒤쪽 코드에서 하드웨어에 접근해 동작 모드를 변경할 것 이기 때문이다(DUMMY는 존재하지 않는 디바이스이기 때문에, 하드웨어 액세스라는 것이 존재하지 않는다).

    // kernel/time/tick-internal.h - v6.5
    static inline int tick_device_is_functional(struct clock_event_device *dev)
    {
    	return !(dev->features & CLOCK_EVT_FEAT_DUMMY);
    }

     

    8.3 전달된 clock event device 가 하드웨어적으로 `periodic mode`를 지원하고, 브로드 캐스트 타이머가 one-shot 모드가 아니라면, clock event device 물리적 동작 모드를 periodic mode 로 변경한다. 즉, 하드웨어적으로 `periodic` 으로 동작하도록 한다. 그런데, 브로드 캐스트 타이머가 one-shot mode 일 때는, 왜 동작 모드를 바꿀 수 없을까? 구체적인 내용은 이 패치를 참고하자.

     

    그런데, 동작 모드를 변경하기만 하고 끝낸다. 그래도, 최소 한 번은 nex tick 을 써줘야, 이후에 자동으로 tick 이 발생하지 않을까? periodic mode 와 같은 경우는 제조사에서 clocksource 드라이버를 만들 때, clock_event_device.set_state_periodic 콜백 함수도 함께 제공한다. one-shot 과 달리 period 는 동작 모드가 바뀌는 시점에 period 를 함께 전달해야 한다. 왜냐면, period 는 모드가 바뀌자마자 자동으로 실행되기 때문이다. 그런데, 제조사는 period 파라미터를 요구하지 않는다. 왤까? 리눅스에서는 이미 암묵적인 합의로 period 가 존재한다. 바로 `HZ` 상수다. 제조사들은 자신들의 `->set_state_periodic` 함수를 구현할 때, HZ 를 사용해서 적합한 period 를 하드웨어에 전달한다(` drivers/clocksource/arm_global_timer.c:gt_clockevent_set_periodic - v6.5` 참고).

     

    8.4 clock event device 가 하드웨어적으로 `periodic mode`를 지원하지 않고 `one-shot mode` 만 지원하는 경우가 있다(최근에는 대부분의 디바이스가 다 이런 형태다). 이럴 때는, 하드웨어적인 `one-shot mode`를 통해서 소프트웨어적으로 `periodic mode`를 구현하게 된다. `for(...)`에서 `next`를 다시 구하는 경우는 next tick time 이 현재 시간과 같거나 더 앞선 경우다(`next <= ktime_get`). 이런 경우에는 당연히 nex tick 으로 점프해야 한다.

     

    : `tick_setup_oneshot` 함수는 내용이 쉽기 때문에 설명은 생략한다.

    //linux/kernel/time/tick-oneshot.c - v6.5
    void tick_setup_oneshot(struct clock_event_device *newdev,
    			void (*handler)(struct clock_event_device *),
    			ktime_t next_event)
    {
    	newdev->event_handler = handler;
    	clockevents_switch_state(newdev, CLOCK_EVT_STATE_ONESHOT);
    	clockevents_program_event(newdev, next_event, true);
    }

     

    3. 만약, 새로운 clock event device 가 하드웨어적으로 one-shot mode 를 지원한다면 tick-sched layer 에 이 사실을 알린다. `ts->check_clocks` 을 통해서 tick-sched layer 에게 해당 per-CPU 타이머는 one-shot 모드를 지원한다는 것을 알린다.

    /*
     * Async notification about clock event changes
     */
    void tick_oneshot_notify(void)
    {
    	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
    
    	set_bit(0, &ts->check_clocks);
    }

     

    4. 만약, tick_check_replacement 함수에서 false 반환할 경우, 새로운 clock event device 는 per-CPU 타이머에 적합하지 않다는 것을 의미한다. 그래서, 브로드 캐스트 타이머로 사용가능한지를 검사한다. 자세한 내용은 이 글을 참고하자.

     

    : 여기까지 register 과정을 마무리 짓는다. 정리하면, register 과정에서 clock event device 에 할당되는 타이머 핸들러와 동작 모드는 다음과 같다.

      동작 모드 타이머 핸들러
    per-CPU 타이머에 기존 타이머가 있는 경우 periodic mode or one-shot mode (기존 타이머의 동작 모드를 따라간다) ? (기존 타이머의 핸들러를 따라간다)
    per-CPU 타이머에 기존 타이머가 없는 경우 periodic mode tick_handle_periodic

     

     

     

    - What is default timer event handler of clock event device ?

    : `tick_handle_periodic` 함수는 period tick 이 발생할 때 마다 호출되는 타이머 핸들러다. 그런데, 이 함수는 내용보다는 어느 타이밍에 이 함수가 timer handler 로 설정되는지가 중요하다. 

    1. DUMMY TIMER의 디폴트 타이머 핸들러 : tick_handle_periodic
    2. per-CPU 타이머의 디폴트 타이머 핸들러 : tick_handle_periodic

    : 위 2개의 케이스 모두 타이머가 초기화되는 시점에 `tick_handle_periodic`가 디폴트 타이머 핸들러로 사용된다. 그렇다면, 왜 `tick_handle_periodic`이 디폴트로 사용할까? 

     

    1. 하드웨어 디펜던시가 가장 적은 동작 모드가 `period mode` 다. 아래표에서 볼 수 있다시피, periodic tick은 모든 동작 모드에서 지원한다.
      one-shot mode period mode
    supported tick one-shot & periodic tick periodic tick

    2. tick_handle_periodic 함수는 tick 관련 일반적이면서 공통적인 코드만 모아놨기 때문에, 특정 하드웨어 및 기술에 덜 의존적이다.`tick_handle_periodic` 함수가 `tick-common.c` 파일에 있는 것은 이러한 이유 때문이다.

    3. 특정 기술에 디펜던시가 없다. 예를 들어, broadcast, dyntick 은 별도의 타이머 핸들러가 존재한다. 이 핸들러는 각 레이어에 의존적인 기술들이 포함되어 있어서 디폴트 핸들러로는 사용할 수 없다.

    4. 기존부터 사용되오던 함수다. 예전에는 period mode 만 존재했었다(물론, 현재는 one-shot mode 만 지원하는 하드웨어도 굉장히 많아지고 있다)
    // kernel/time/tick-common.c - v6.5
    void tick_handle_periodic(struct clock_event_device *dev)
    {
    	int cpu = smp_processor_id(); 
    	ktime_t next = dev->next_event;
    
    	tick_periodic(cpu); // --- 1
    
    #if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON)
    	if (dev->event_handler != tick_handle_periodic) // --- 2
    		return;
    #endif
    
    	if (!clockevent_state_oneshot(dev)) // --- 3
    		return;
    	for (;;) {
    		next = ktime_add_ns(next, TICK_NSEC);
    
    		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);
    	}
    }
    1. 타임 서브-시스템에서 전역적으로 사용되는 2가지 전역 변수를 각각 증가시킨다.
      Unit
    jiffies(old) nano-second / TICK_NSEC
    tick_next_period(new) nano-second

    2. 이 코드는 타이머 서브-시스템의 구조를 이해하지 못하면 어려울 수 있는 코드다. 왜냐면, 지금 이 함수가 호출된 이유는 호출된 이유는 dev->event_handler 에 tick_handle_periodic 이 설정되어 있기 때문이다. 그런데, 대뜸 dev->event_handler 가 tick_handle_periodic 이 맞는지를 검사하고 있다. 무조건 맞는거 아닐까? 그렇지 않다. 그 앞에 `tick_periodic` 함수가 호출되면, high-res or nohz 로 변경할 수 있는지를 항시 체크한다. 프로세스는 다음과 같다.
    tick_periodic -> update_process_times -> run_local_timers -> hrtimer_run_queues -> tick_check_oneshot_change
    위에서 tick_check_oneshot_chage 함수가 호출되면, one-shot mode 모드로 변경이 될 수 있는지 확인한다. 그리고, 가능하다면 모드를 변경한다. 이 때, 타이머 핸들러 또한 당연히 변경된다. 그래서, 핸들러가 변경됬다면 여기서 함수를 종료하는 것이다. 여기서 종료해도 될까? tick_handle_periodic 함수에서 핵심적인 기능은 tick_periodic 함수에서 다 한다. 나머지 코드들은 에러 처리거나 one-shot mode 일 때, next period time tick 을 계산하는 코드뿐이다.


    3. 타이머가 one-shot mode 가 아니라면, 함수를 종료한다. 즉, 동작 모드가 period mode 라면 함수를 종료한다. 왤까? 타이머의 동작 모드가 period mode 라면, 하드웨어가 알아서 counter register 를 초기화할 것이기 때문에 계산을 할 필요가 없다. 그런데, one-shot mode 라면, period tick을 발생시키기 위해 next period time tick 을 계산해야 한다. `for(;;) ... ` 는 이 계산하는 코드가 들어있다. 내용이 쉽기 때문에 설명은 생략한다.

     

    : `tick_periodic` 함수는 시스템 전역적으로 사용되는 시간 관련 내용들을 업데이트하는 함수다. 

    // kernel/time/tick-common.c - v6.5
    /*
     * Periodic tick
     */
    static void tick_periodic(int cpu)
    {
    	if (tick_do_timer_cpu == cpu) { // --- 1
    		raw_spin_lock(&jiffies_lock);
    		write_seqcount_begin(&jiffies_seq);
    
    		/* Keep track of the next tick event */
    		tick_next_period = ktime_add_ns(tick_next_period, TICK_NSEC); // --- 2
    
    		do_timer(1); // --- 3
    		write_seqcount_end(&jiffies_seq);
    		raw_spin_unlock(&jiffies_lock);
    		update_wall_time();
    	}
    
    	update_process_times(user_mode(get_irq_regs()));
    	profile_tick(CPU_PROFILING);
    }
    1.  tick_do_timer_cpu 는 jiffies 업데이트하는 CPU 를 지정한다. 시스템에 전역적으로 하나만 존재하는 jiffies 를 여러 CPU 가 업데이트하면, 동기화 문제가 발생한다. 그러므로, 한 개의 CPU 만 jiffies 를 업데이트해야 한다.

    2. 타이머가 period mode 로 동작중이라면, 이 값은 의미가 없을 수 도 있다. 왜냐면, 이 값을 통해 next time tick 시간을설정하는건데, peride mode 면 하드웨어에서 자동으로 발생시키기 때문이다. 그러나, one-shot mode 라면 얘기가 다르다. one-shot 모드에서는 tick_next_period 를 통해 next time tick 을 발생시킨다.

    3. jiffies_64를 `1` 증가시킨다.

     

Designed by Tistory.