ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Timer - clock event device
    Linux/kernel 2023. 9. 25. 15:23

    글의 참고

    - https://docs.kernel.org/timers/timekeeping.html

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

    - http://www.wowotech.net/timer_subsystem/clock-event.html


    글의 전제

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

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


    글의 내용

    - Overview

    : clock event framework 는 실제 timer interrupt 를 발생시키는 하드웨어 디바이스를 소프트웨어로 추상화시키는데 사용된다. 제조사에서 작성한 clock source driver 에서 clock event & clock source 프레임워크를 이용해서 자신들의 하드웨어를 소프트웨어 레이어로 추상화시킨다. clock event 프레임워크는 주로 타이머 인터럽트를 어떻게 언제 발생시킬 것인지와 관련이 있다.

     

     

     

    - Data structure

    : clock event device 는 주로 칩 제조사 및 벤더사에서 제공해주기 때문에 하드웨어와 깊은 관련이 있다. 여기서 `____cacheline_aligned` 속성이 중요하다. 이 속성은 L1 캐쉬 사이즈가 맞춰 정렬시킨다는 것이다. 예를 들어, L1 캐쉬가 64 바이트면 아래 구조체가 생성될 때 마다, 64바이트에 정렬된 주소로 만들어진다. 거기다가 clock event device 구조체는 멤버 변수들의 순서도 관계가 있는 필드들끼리 엮었다. 왜냐면, 캐쉬에 들어갈 때, 연속적인 덩어리로 들어가기 때문에 같이 사용되는 애들끼리 묶는것이다. 이 섹션뒤에서 `____cacheline_aligned` 속성에 대해 다시 알아본다.

    // include/linux/clockchips.h - v6.5
    struct clock_event_device {
    	void			(*event_handler)(struct clock_event_device *);
    	int			(*set_next_event)(unsigned long evt, struct clock_event_device *);
    	int			(*set_next_ktime)(ktime_t expires, struct clock_event_device *);
    	ktime_t			next_event;
    	u64			max_delta_ns;
    	u64			min_delta_ns;
    	u32			mult;
    	u32			shift;
    	enum clock_event_state	state_use_accessors;
    	unsigned int		features;
    	unsigned long		retries;
    
    	int			(*set_state_periodic)(struct clock_event_device *);
    	int			(*set_state_oneshot)(struct clock_event_device *);
    	int			(*set_state_oneshot_stopped)(struct clock_event_device *);
    	int			(*set_state_shutdown)(struct clock_event_device *);
    	int			(*tick_resume)(struct clock_event_device *);
    
    	void			(*broadcast)(const struct cpumask *mask);
    	void			(*suspend)(struct clock_event_device *);
    	void			(*resume)(struct clock_event_device *);
    	unsigned long		min_delta_ticks;
    	unsigned long		max_delta_ticks;
    
    	const char		*name;
    	int			rating;
    	int			irq;
    	int			bound_on;
    	const struct cpumask	*cpumask;
    	struct list_head	list;
    	struct module		*owner;
    } ____cacheline_aligned;
    - set_next_event : cycle counter 를 인자로 받아서 타이머 인터럽트를 발생시키는 함수다. 제조사에서 제공해줄 경우에 사용이 가능하다. 일반적으로는 nano-second 인자를 받는 것보다는 cycle counter 를 인자로 받아 타이머 인터럽트를 발생시키는 것을 더 선호한다.

    - set_next_ktime : nano-second 를 인자로 받아서 타이머 인터럽트를 발생시키는 함수다. 제조사에서 제공해줄 경우에 사용이 가능하다.

    - next_event : 말 그대로 다음에 발생할 타이머 인터럽트 시간을 가지고 있다.

    - max_delta_ns : 시스템에 존재하는 `clock source`들은 자신만의 카운트 레지스터를 가지고 있다. 그리고, 이 카운트 레지스터의 값이 오버플로우가 될 때 마다, 타이머 인터럽트가 발생한다. 100MHz 로 동작하는 32비트 카운트 레지스터는 43초(`4294967295/100000000`)에서 오버플로우가 발생하면서 0이 된다. 이 때, 43초가 max_delta_ns 가 된다. 이 말은 43초 이후의 시간을 설정하는 것은 의미가 없다는 뜻이다. 예를 들어, max_delta_ns가 43초인데, 강제로 48초를 입력하면, 5초 뒤에 next event 가 발생할 것이다. 

    - min_delta_ns : 현재 `clock event device`가 구분할 수 있는 최소 단위의 `timing interval`을 의미한다. 예를 들어, 10MHz로 동작하는 `clock source`는 최소 `timing interval`이 100ns 이상은 되어야 한다. 왜냐면, 10Mhz = 10,000,000(1초를 기준으로 천 만번 주파수 발생 = 1 / 10,000,000), 1ns = 1,000,000,000 ->  13,000,000 / 1,000,000,000(`s`가 아닌 `ns` 로 단위를 변환해야 함) = 0.01 -> 즉, 주파수 하나 발생시키는데 0.01ns 밖에 걸리지 않는다는 소리. 이걸 1ns에 몇 개의 주파수가 발생하는지로 변환해야 한다. 즉, `0.01 * X = 1` 에서 `X`를 구하자 -> 100ns에 한 번 주파수가 발생. 그러므로, 최소 `timing interval`이 100ns 보다 크거나 같아야 한다. 만약 작다면, 에러를 발생한다.  

    - mult & shift : 대개, SoC 마다 한 개의 clock source 는 반드시 존재하기 마련이다. 이 디바이스는 고정된 주파수로 동작을 하며, 한 개의 주파수가 발생할 때마다 카운트를 1씩 증가시키는 특징이 있다. 주의할 점은 clock source 는 timer 를 혼동해서는 안된다. clock source 는 매우 높은 고정 주파수로 동작을 하며, 다른 하드웨어 모듈에 의존하지 않고 자기 혼자만으로 동작하는 모듈이다. 즉, 누군가에게 의존하지 않고, 간섭도 받지 않는다는 뜻이다. 단지, counter value 를 계속 축적해나가면 언젠가 CPU가 이 값을 읽어서 시간이 얼마나 흘렀는지를 알 수 있게 되는 것 뿐이다. 

    타이머 인터럽트를 발생시키는 주체는 timer 이고, 타이머의 timing interval 은 clock source 의 period interval 보다 반드시 커야한다. 왜냐면, 타이머가 인터럽트를 발생시키는 기준을 clock source의 period interval 로 하기 때문이다. 그렇다면, 현재 얼마나 시간이 흘렀는지를 어떻게 알 수 있을까? 시간이 얼마나 흘렀는지를 알기 위해서는 현재 초당 주파수와 현재 사이클이 얼마나 카운트 되었는지를 알아야 한다. 즉, `Cycle / Frequency` 로 알 수 있다. 아래 그림에서 알 수 있듯이, HZ는 초당 발생하는 사이클 개수를 의미한다. 그리고, Cycle 은 한 주기를 나타낸다. 결국, 주파수는 1초에 몇 번의 주기가 발생했는지를 의미한다. 시간을 구할 때, 주파수가 필요한 이유는 주파수를 표현할 때, 간접적으로 `1초` 나타내기 때문이다. 예를 들어, 32 사이클을 `초`로 표현하고 싶다면, 주파수가 5HZ 라고 가정했을 때, 6.4초가 지났음을 의미한다(32 / 5).
    그런데, 커널은 퍼포먼스 문제때문에 나눗셈과 소수 연산을 좋아하지 않는다. 심지어, 소수 연산을 지원하는 모듈이 없는 시스템도 존재한다. 그래서, 오직 INT 로만 처리할 수 있는 알고리즘이 필요했다.

    - state_use_accessors : `clock event device` 가 현재 하드웨어적으로 어떤 모드로 동작중인지를 나타낸다.

    - features : 이 필드는 해당 `clock event device`가 하드웨어적으로 어떤 기능을 지원하는지를 나타낸다. 뒤에서 다시 다룬다.

    - retries : next event 예약에 실패할 경우, 단번으로 끝내지 않는다. v6.5 기준으로 10번을 재시도한다. 이 때, 재시도한 횟수를 기록한다.

    - max_delta_ticks : max_delta_ns 와 완전 동일한 의미를 갖지만, 표현을 number of cycles 로 표현한다. 예를 들어, 32비트 카운트 레지스터는 100MHz 로 1씩 증가시키면, 43초 정도가 되면 오버플로우가 된다. 결국, tick 은 4300MHz 번 발생하게 된다. max_delta_ns 같은 경우는 10ns 에 1번의 tick 이 발생하므로, 4,300,000,000 * 10 ns 가 된다. 즉, 32 카운트 레지스터를 오버플로우를 발생시키는데, 43,000,000,000 ns 시간이 필요하고, cycle 횟수는 4,300,000,000 번이 필요하다.

    - min_delta_ticks : min_delta_ticks 와 완전 동일한 의미를 갖지만, nano-second 가 아닌 number of cycles 로 표현한다. min_delta_ns & max_delta_ticks 을 참고하자.

    - cpumask : clock event device 가 어떤 CPU 에서 동작하는지를 알려준다. 예를 들어, 이 값이 4(0x0000_0004) 라면, 현재 clock event device 는 2번 CPU 에서 등륵되었다는 것을 알 수 있다.

     

    : clock event device 레이어는 2개의 전역 연결리스트를 통해서 시스템의 모든 clock event device 를 관리한다.

    // kernel/time/clockevents.c - v6.5
    /* The registered clock event devices */
    static LIST_HEAD(clockevent_devices);
    static LIST_HEAD(clockevents_released);
    - clockevent_devices : tick layer 에 정착한 clock event device 들을 나타낸다. per-CPU 타이머 및 브로드 캐스트 타이머에 상관없이, 실제 물리적 디바이스와 매핑이 된 디바이스를 나타낸다.

    - clockevents_released : tick layer 에서 clock event device layer 로 반환된 디바이스들을 저장한다. 이 연결리스트에 연결된 디바이스들은 clockevent_devices 에 연결된 디바이스들보다 우선 순위가 낮음을 의미한다. 이 디바이스들은 새로운 clock event devie 가 커널에 등록될 때, 다시 리매칭 프로세스를 거치게 된다.

     

    : arm64 를 기준으로 `____cacheline_aligned` 속성은 다음과 같이 정의된다.

    // arch/arm64/include/asm/cache.h - v6.5
    #define L1_CACHE_SHIFT		(6)
    #define L1_CACHE_BYTES		(1 << L1_CACHE_SHIFT)
    // include/linux/cache.h - v6.5
    #ifndef SMP_CACHE_BYTES
    #define SMP_CACHE_BYTES L1_CACHE_BYTES
    #endif
    ....
    
    #ifndef ____cacheline_aligned
    #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES)))
    #endif

    : 일단 arm64 의 L1 cache line 이 64B 인것이 확인됬다. 그리고, GCC 에서 __attribute__((__aligned__(X))) 속성은 인자로 전달된 `X` 값에 데이터 주소를 정렬시킨다는 의미를 갖는다.

     

    - Feature

    : `clock event device`가 하드웨어적으로 어떤 기능을 지원하는지를 나타낸다. 이 플래그들은 하드웨어적인 특성을 나타내기 때문에, 초기에 설정되면 런타임에는 변경될 일이 없다.

    // include/linux/clockchips.h - v6.5
    /*
     * Clock event features
     */
    # define CLOCK_EVT_FEAT_PERIODIC	0x000001
    # define CLOCK_EVT_FEAT_ONESHOT		0x000002
    # define CLOCK_EVT_FEAT_KTIME		0x000004
    
    /*
     * x86(64) specific (mis)features:
     *
     * - Clockevent source stops in C3 State and needs broadcast support.
     * - Local APIC timer is used as a dummy device.
     */
    # define CLOCK_EVT_FEAT_C3STOP		0x000008
    # define CLOCK_EVT_FEAT_DUMMY		0x000010
    
    /*
     * Core shall set the interrupt affinity dynamically in broadcast mode
     */
    # define CLOCK_EVT_FEAT_DYNIRQ		0x000020
    # define CLOCK_EVT_FEAT_PERCPU		0x000040
    
    /*
     * Clockevent device is based on a hrtimer for broadcast
     */
    # define CLOCK_EVT_FEAT_HRTIMER		0x000080
    - CLOCK_EVT_FEAT_PERIODIC : 이 플래그가 설정된 디바이스는 하드웨어적으로 period mode 를 지원함을 의미한다.

    - CLOCK_EVT_FEAT_ONESHOT : 이 플래그가 설정된 디바이스는 하드웨어적으로 one-shot mode 를 지원함을 의미한다.

    - CLOCK_EVT_FEAT_KTIME : 이 플래그가 설정된 디바이스는 cycle counter 를 받는게 아니라, nano-second 값을 직접 받아서 타이머 인터럽트를 예약할 수 있다.

    - CLOCK_EVT_FEAT_C3STOP : x86 기준으로 C3 는 ACPI 스펙에 명시된 CPU 의 파워 상태를 의미한다. C3 는 일반적으로 로컬 타이머를 OFF 시키는 것을 의미한다(arch(x86) 의존적인 feature 여서 `misfeature` 라고도 불린다).

    - CLOCK_EVT_FEAT_DUMMY : 이 플래그는 x86 전용으로 `/arch/x86/kernel/apic/apic.c` 파일에서 주로 사용된다. 로컬 타이머에게 할당되는 플래그이며, 2가지로 용도로 사용된다(arch(x86) 의존적인 feature 여서 `misfeature` 라고도 불린다).
     1. 이 플래그 CLEAR 되어있으면, 해당 타이머는 로컬 타이머로는 사용한다.
     2. 이 플래그 SET 되어있으면, 해당 로컬 타이머는 브로드 캐스트 용으로 사용한다.
    [PATCH] tick-management: broadcast functionality

    With Ingo Molnar <mingo@elte.hu> Add broadcast functionality, so per cpu clock event devices can be registered as dummy devices or switched from/to broadcast on demand.

    - 참고 : https://github.com/torvalds/linux/commit/f8381cba04ba8173fd5a2b8e5cd8b3290ee13a98
    - per-CPU 타이머가 브로드 캐스트 타이머로 등록될 경우, 로컬 타이머로서의 동작들은 멈춰야 한다. CLOCK_EVT_FEAT_DUMMY 플래그를 사용해서 이 플래그 SET 되면, 로컬 타이머로써의 기능은 STOP 하는 것이다. 그리고, 마치 소프트웨어적으로 브로드 캐스트 타이머와 같은 업무를 수행하게 된다[참고1 참고2] 

    - CLOCK_EVT_FEAT_DYNIRQ : 이 플래그는 브로드 캐스트 타이머에만 설정된다.이 플래그가 설정된 브로드 캐스트 타이머는 가장 빠른 `earliest expiry time`을 가지는 CPU를 깨운다. 즉, 동적으로 `IRQ Affinity` 를 변경할 수 있다는 것을 의미한다[참고1].

    - CLOCK_EVT_FEAT_PERCPU : 이 플래그가 SET 되어있으면, 특정 CPU에 소속된 로컬 타이머를 의미한다. 그러나, 이 플래그는 실질적으로 거의 사용되지 않는다. 주로, 브로드 캐스트 타이머를 걸러낼 목적으로 사용된다.

    - CLOCK_EVT_FEAT_HRTIMER : 이 플래그는 `broadcast timer based on hrtmer` 를 구분할 때 사용된다. 브로드 캐스트 타이머는 일반적으로 2가지 특징이 있다.
    1. not per-CPU timer (external timer)
    2. not affected by power state (Always-on)
    그런데, 브로드 캐스트 타이머가 없는 시스템도 존재한다. 이럴 경우, 특정 per-CPU timer 가 브로드 캐스트 타이머 역할을 대신하게 된다. 이 때, per-CPU timer 에 CLOCK_EVT_FEAT_HRTIMER 플래그가 SET 된다. 이 부분은 각 제조사 및 벤더사에서 정하기 때문에, 동적으로 변경이 불가능한 부분이다. 만약, CLOCK_EVT_FEAT_PERCPU & CLOCK_EVT_FEAT_HRTIMER 가 같이 설정되어 있다면, 해당 시스템에 브로드 캐스트 타이머가 존재하지 않을 수 도 있다.

     

     

     

    - State

    : `clock event device` 가 현재 하드웨어적으로 어떤 모드로 동작중인지를 나타낸다. 예를 들어, clock event device 는

    CLOCK_EVT_FEAT_ONESHOT 플래그와 CLOCK_EVT_FEAT_PERIODIC 플래그가 모두 SET 되어있다면 무슨 뜻일까? 하드웨어적으로 `PERIODIC`과 `ONESHOT` 모드로 동작이 가능하다는 소리다. 그런데, 2가지 모드를 모두 지원할 수는 있어도, 한 시점에는 하나의 모드로만 동작하고 있어야 한다. 즉, CLOCK_EVT_STATE_* 는 `현재 동작 상태` 를 나타낸다. 만약, 현재 clock event device 의 상태가 CLOCK_EVT_STATE_PERIODIC 라면, 어떻게 해석해야 할까? `해당 clock event device 는 하드웨어적으로 periodic & one-shot 를 지원하고, 현재 시점으로 하드웨어 동작 상태는 periodic mode 로 동작하도록 프로그래밍 되어있는 상태`라고 해석할 수 있다.

    // include/linux/clockchips.h - v6.5
    enum clock_event_state {
    	CLOCK_EVT_STATE_DETACHED,
    	CLOCK_EVT_STATE_SHUTDOWN,
    	CLOCK_EVT_STATE_PERIODIC,
    	CLOCK_EVT_STATE_ONESHOT,
    	CLOCK_EVT_STATE_ONESHOT_STOPPED,
    };

    : 그렇다면, 궁금한 점이 있을 수 있다. 최신 타이머는 하드웨어적으로 one-shot mode 만 지원하는 경우가 많은데, 어떻게 periodic tick 을 발생시킬까? 기존 리눅스 커널은 low-resolution 및 periodic 기반이기 때문에, one-shot 을 지원하지 않는다. one-shot mode 에 관한 구체적인 내용 및 one-shot mode 로 periodic tick 을 구현하는 방법은 hrtimer 에서 확인해본다.

     

    : 그리고, CLOCK_EVT_STATE_ONESHOT 플래그만 SET 되어 있는 디바이스는 CLOCK_EVT_STATE_PERIODIC 상태가 될 수는 없는 것인가? 이건 뒤에서 `__clockevents_switch_state` 함수에서 보겠지만, 결론적으로 안된다. 

     

    : `clock_state_*` 함수들은 모두 `is` 계열의 함수들이다. 즉, 인자로 전달된 clock event device 의 상태를 체크한다.

    // kernel/time/tick-internal.h - v6.5
    ....
    
    static inline enum clock_event_state clockevent_get_state(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors;
    }
    
    static inline void clockevent_set_state(struct clock_event_device *dev,
    					enum clock_event_state state)
    {
    	dev->state_use_accessors = state;
    }
    ....
    
    // include/linux/clockchips.h - v6.5
    /* Helpers to verify state of a clockevent device */
    static inline bool clockevent_state_detached(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors == CLOCK_EVT_STATE_DETACHED;
    }
    
    static inline bool clockevent_state_shutdown(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors == CLOCK_EVT_STATE_SHUTDOWN;
    }
    
    static inline bool clockevent_state_periodic(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors == CLOCK_EVT_STATE_PERIODIC;
    }
    
    static inline bool clockevent_state_oneshot(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors == CLOCK_EVT_STATE_ONESHOT;
    }
    
    static inline bool clockevent_state_oneshot_stopped(struct clock_event_device *dev)
    {
    	return dev->state_use_accessors == CLOCK_EVT_STATE_ONESHOT_STOPPED;
    }

     

     

     

    - How to configure a clock event devices ?

    : arm 에서 clock event device 를 초기화할때, max_delta 를 세팅하는 값이 clock event device 의 max_delta_ticks 값이된다.

    // drivers/clocksource/arm_arch_timer.c - v6.5
    static void __arch_timer_setup(unsigned type,
    			       struct clock_event_device *clk)
    {
    	u64 max_delta;
    
    	clk->features = CLOCK_EVT_FEAT_ONESHOT;
    
    	if (type == ARCH_TIMER_TYPE_CP15) {
    		....
    		max_delta = __arch_timer_check_delta();
    	} else {
    		....
    		max_delta = CLOCKSOURCE_MASK(56);
    	}
    	....
        
    	clockevents_config_and_register(clk, arch_timer_rate, 0xf, max_delta);
    }
    
    // kernel/time/clockevents.c - v6.5
    /**
     * clockevents_config_and_register - Configure and register a clock event device
     * @dev:	device to register
     * @freq:	The clock frequency
     * @min_delta:	The minimum clock ticks to program in oneshot mode
     * @max_delta:	The maximum clock ticks to program in oneshot mode
     *
     * min/max_delta can be 0 for devices which do not support oneshot mode.
     */
    void clockevents_config_and_register(struct clock_event_device *dev,
    				     u32 freq, unsigned long min_delta,
    				     unsigned long max_delta)
    {
    	dev->min_delta_ticks = min_delta;
    	dev->max_delta_ticks = max_delta;
    	clockevents_config(dev, freq);
    	clockevents_register_device(dev);
    }

     : arch_timer_rate 는 해당 clock event device 의 clock frequency 를 의미한다. 그런데, 이 값이 2군데서 설정이 된다.

    1. 디바이스트리 : 
    " arm 에서는 시스템에 존재하는 모든 clock event device 들에 대한 정보를 디바이스 트리에 작성해야 한다. `arch_sys_timer` 같은 경우는 로컬 타이머를 의미하며, `arch_mem_timer` 는 글로벌 타이머를 의미한다. 

    2. APCI : arch_timer_mem_frame_get_cntfrq
    " arm 에서 ACPI 스펙을 따른다는 것은 서버 시장쪽 arm 을 염두에 둔것이다.

     

    : arch_timer_rate 는 디바이스 트리의 `clock-frequency` 속성에 매핑되는 것을 볼 수 있다.

    // drivers/clocksource/arm_arch_timer.c - v6.5
    static void __init arch_timer_of_configure_rate(u32 rate, struct device_node *np)
    {
    	/* Who has more than one independent system counter? */
    	if (arch_timer_rate)
    		return;
    
    	if (of_property_read_u32(np, "clock-frequency", &arch_timer_rate))
    		arch_timer_rate = rate;
    
    	/* Check the timer frequency. */
    	if (validate_timer_rate())
    		pr_warn("frequency not available\n");
    }
    // https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/arch_timer.txt
    timer {
    		compatible = "arm,cortex-a15-timer",
    			     "arm,armv7-timer";
    		interrupts = <1 13 0xf08>,
    			     <1 14 0xf08>,
    			     <1 11 0xf08>,
    			     <1 10 0xf08>;
    		clock-frequency = <100000000>;
    	};

     

    : clock event device 의 핵심은 cycle counter 를 ns 로 변경하는 작업이다.

    // kernel/time/clockevents.c - v6.5
    static void clockevents_config(struct clock_event_device *dev, u32 freq)
    {
    	u64 sec;
    
    	if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT)) // --- 1
    		return;
    
    	/*
    	 * Calculate the maximum number of seconds we can sleep. Limit
    	 * to 10 minutes for hardware which can program more than
    	 * 32bit ticks so we still get reasonable conversion values.
    	 */
    	sec = dev->max_delta_ticks; // --- 2
    	do_div(sec, freq);
    	if (!sec)
    		sec = 1;
    	else if (sec > 600 && dev->max_delta_ticks > UINT_MAX)
    		sec = 600;
    
    	clockevents_calc_mult_shift(dev, freq, sec);
    	dev->min_delta_ns = cev_delta2ns(dev->min_delta_ticks, dev, false);
    	dev->max_delta_ns = cev_delta2ns(dev->max_delta_ticks, dev, true);
    }
    1. 하드웨어적으로 period mode 모드는 지원하지 않고 one-shot mode 만 지원할 경우, 소프트웨어 레이어에서 next event 를 주기적으로 발행해서 마치 물리적으로 period mode 를 지원하는 것처럼 시뮬레이션 할 수 있다. 그러나, period mode 만 지원한다면, 소프트웨어 레이어에서 one-shot tick 을 시뮬레이션 할 수 없음을 물론이고, 하드웨어에서 알아서 주기적으로 next event 가 발생하기 때문에 next event 를 계산하기 위한 소프트웨어적인 작업들이 필요없어진다. 즉, one-shot mode 를 지원하지 않을 경우, clockevents_config 함수에서 수행하는 일들이 next event 를 계산하기 위한 소프트웨어 변수들을 초기화하는 것이므로, 뒤에 코드는 실행할 의미가 없다.

    2. dev->max_delta_ticks 은 최대 cycle counter 를 의미한다. 그런데, 주의할 점이 있다. 이 값은 system counter 가 아니다. per-CPU 타이머의 최대 cycle counter 다. `Cycle(sec) / Freq` 는 현재 몇 초가 지났는지를 의미한다. 즉, sec 는 per-CPU 타이머의 counter 가 0 에서부터 오버플로우가 발생하는데 걸린 시간을 초 단위로 저장한다. 만약, sec가 0 이라면, 주파수가 너무 빠른것이다. 카운터 레지스터를 0 에서부터 오버플로우를 발생시키기까지 1초도 안걸리는 것이다. 이럴 경우, 1초를 판단하기가 어려워 지기 때문에, 강제로 sec 를 1로 설정한다. 만약, sec 가 10분을 넘는다면 이거 또한 문제다. 주파수가 너무 느린것이다. 그래서 적당한 타협선에서 10분으로 sec 를 설정한다. max limit 을 두는 이유가 뭘까? IDLE과 과련이 있다.

    예를 들어, one-shot mode 에서 next event 가 20초 뒤에 발생하고, 현재 할일이 없다면 CPU는 IDLE 상태로 진입할 수 있다. 언젠 wake-up 할까? 외부 인터럽트가 없었다고 가정한다면 타이머 인터럽트에 맞춰서 20초뒤에 wake-up 하게 될 것이다. 이 말은, dev->max_delta_ticks 값에 따라 CPU 가 IDLE 상태에서 최대 얼마동안 있을 수 있을지가 결정된다고 볼 수 있다. 그렇다면, sec 을 10분으로 제한하는 것은 CPU 가 10분 이상 IDLE 상탤 진입하지 못한다는 것을 의미한다. 왜 그럴까? CPU가 혹여나 특정 리소스에 대한 lock 을 소유한채로 IDLE 에 들어갈 경우, 데드락이 발생할 수 있기 때문이다.

     

    : `clockevents_calc_mult_shift` 함수는 clock event device 가 `time -> cycle` 로 변경할 때, 사용되는 helper constant 들을 얻기 위해서 사용된다.

    // include/linux/clockchips.h - v6.5
    static inline void clockevents_calc_mult_shift(struct clock_event_device *ce, u32 freq, u32 maxsec)
    {
    	return clocks_calc_mult_shift(&ce->mult, &ce->shift, NSEC_PER_SEC, freq, maxsec);
    }

     

    - How to register a new clock event devices to kernel ?

    : clock event device 는 각 제조사의 clock source driver 파일에서 하드코딩 및 디바이스 트리를 통해서 만들기 때문에, register 작업은 특별히 할 것이 없다. 이름 그대로 커널에 clock event device 를 등록한다.

    // kernel/time/clockevents.c - v6.5
    void clockevents_register_device(struct clock_event_device *dev)
    {
    	unsigned long flags;
    
    	/* Initialize state to DETACHED */
    	clockevent_set_state(dev, CLOCK_EVT_STATE_DETACHED); // --- 1
    
    	if (!dev->cpumask) { 
    		WARN_ON(num_possible_cpus() > 1);
    		dev->cpumask = cpumask_of(smp_processor_id()); // --- 2
    	}
    
    	if (dev->cpumask == cpu_all_mask) {
    		WARN(1, "%s cpumask == cpu_all_mask, using cpu_possible_mask instead\n",
    		     dev->name);
    		dev->cpumask = cpu_possible_mask; // --- 3
    	}
    
    	raw_spin_lock_irqsave(&clockevents_lock, flags);
    
    	list_add(&dev->list, &clockevent_devices); // --- 4
    	tick_check_new_device(dev); // --- 4
    	clockevents_notify_released(); // --- 4
    
    	raw_spin_unlock_irqrestore(&clockevents_lock, flags);
    }
    1. 리눅스 커널에서 초기화되지 않은 clock event device 를 CLOCK_EVT_STATE_DETACHED 상태로 나타낸다. 여기서 `DETACHED` 란 clock event device 가 특정 로컬 타이머에 매칭이 되지 못하 상황을 일컫는 말이다.
    /*
     * Shutdown an event device on a given cpu:
     *
     * This is called on a life CPU, when a CPU is dead. So we cannot
     * access the hardware device itself.
     * We just set the mode and remove it from the lists.
     */
    void tick_shutdown(unsigned int cpu)
    {
        struct tick_device *td = &per_cpu(tick_cpu_device, cpu);
        struct clock_event_device *dev = td->evtdev;

        td->mode = TICKDEV_MODE_PERIODIC;
        if (dev) {
         /*
          * Prevent that the clock events layer tries to call
          * the set mode function!
          */
            clockevent_set_state(dev, CLOCK_EVT_STATE_DETACHED);
            clockevents_exchange_device(dev, NULL);
            dev->event_handler = clockevents_handle_noop;
            td->evtdev = NULL;
        }
    }

    2. 각 제조사의 clock source driver 파일에는 하드코딩으로 작성된 clock event device 들이 존재한다. 이 때, 각 제조사는  자신들이 작성한 clock event device 가 현재 코드를 실행중 인 per-CPU 타이머에 매핑될 수 밖에 없는 코드를 작성해놓는다. 그렇기 때문에, if(!dev->cpumask) 조건이 참이 되는 것은 쉽지 않다. 즉, dev->cpumask 가 NULL 이기가 쉽지 않다. 만약, NULL 이더라도 `dev->cpumask = cpumask_of(smp_processor_id())` 를 통해서 인자로 전달된 clock event device 가 현재 코드를 실행중 인 per-CPU 타이머에 매핑될 수 밖에 없는 구조를 만들어준다.

    3. cpu_all_mask 는 offline CPU`s 도 포함된다. 이럴 경우, 쓸데없는 오버헤드가 발생할 수 있으므로, possible CPU`s 들에게만 타이머 인터럽트를 제공할 수 있도록 값을 변경한다. 대개, 이 조건문안에 들어오는 경우 브로드 캐스트 타이머가 된다. 

    4. clockevent_devices 는 attached 가 완료된 clock event device`s 들을 모아둔 연결리스트다. 재미있는 건, 매핑이 아직 확정되지도 않았는데, clockevent_devices 에 연결시키고 있다는 것이다. 이 말은, tick_check_new_device 함수가 무조건 매칭을 시킨다는 뜻이다. clockevents_nofity_released 함수는 tick_check_new_device 함수가 무조건 매칭을 시키다보니, suitable matching 이 실패할 수 도 있다. 이럴 경우를 대비해서, 더 적합한 디바이스가 나타나면 old 디바이스를 교체할 수 있는 알고리즘이 필요하다. 그 역할을 하는 함수가 clockevents_nofity_released 다.  

     

    : arm64의 arch timer 같은 경우 clock source driver 를 만들 때, 하드코딩으로 clock event device 를 생성하고 있다. 중요한 건, `->cpumask` 에 어떤 값을 할당하냐이다. 로컬 타이머에는 smp_processor_id 를 통해 코드를 실행중 인 CPU 번호를 주고 있다. 이럴 경우, clock event device 는 생성되는 조건에서 이미 per-CPU 타이머와 올바르게 매핑된다고 볼 수 있다.

    // drivers/clocksource/arm_arch_timer.c - v6.5
    static void __arch_timer_setup(unsigned type,
    			       struct clock_event_device *clk)
    {
    	....
    	if (type == ARCH_TIMER_TYPE_CP15) {
    		....
    		clk->cpumask = cpumask_of(smp_processor_id());
    		....
    	} else {
    		....
    		clk->cpumask = cpu_possible_mask;
    		....
    }

     

     

     

     

    - Re-program clock event devices

    : `clock event device`의 상태 정보가 변경되어야 할 경우가 있다. 예를 들어, 주파수가 변경되었다거나, 타임 아웃이 발생해서 다음에 스케줄링될 타임을 설정해야 한다거나 해야 하면, `clock event device`를 `재설정(re-program)` 해야 한다.

    /**
     * clockevents_program_event - Reprogram the clock event device.
     * @dev:	device to program
     * @expires:	absolute expiry time (monotonic clock)
     * @force:	program minimum delay if expires can not be set
     *
     * Returns 0 on success, -ETIME when the event is in the past.
     */
    int clockevents_program_event(struct clock_event_device *dev, ktime_t expires,
    			      bool force)
    {
    	unsigned long long clc;
    	int64_t delta;
    	int rc;
    
    	if (WARN_ON_ONCE(expires < 0)) // --- 1
    		return -ETIME;
    
    	dev->next_event = expires;
    
    	if (clockevent_state_shutdown(dev)) // --- 2
    		return 0;
    
    	/* We must be in ONESHOT state here */
    	WARN_ONCE(!clockevent_state_oneshot(dev), "Current state: %d\n",
    		  clockevent_get_state(dev));
    
    	/* Shortcut for clockevent devices that can deal with ktime. */
    	if (dev->features & CLOCK_EVT_FEAT_KTIME) // --- 3
    		return dev->set_next_ktime(expires, dev);
    
    	delta = ktime_to_ns(ktime_sub(expires, ktime_get()));
    	if (delta <= 0) 
    		return force ? clockevents_program_min_delta(dev) : -ETIME; // --- 4
    
    	delta = min(delta, (int64_t) dev->max_delta_ns); // --- 5
    	delta = max(delta, (int64_t) dev->min_delta_ns); // --- 5
    
    	clc = ((unsigned long long) delta * dev->mult) >> dev->shift; // --- 6
    	rc = dev->set_next_event((unsigned long) clc, dev); // --- 6
    
    	return (rc && force) ? clockevents_program_min_delta(dev) : rc; // --- 7
    }
    1. 인자로 전달된 expires 은 next event 의 발생 시점을 나타낸다. 즉, 미래의 시간을 나타낸다. 참고로, 시간은 음수가 될 수 없다.

    2. next event 를 발생시켜야 하는데, 당연히 타이머는 On 되어 있어야 한다. 절대 shutdown 되어있으면 안된다.

    3. 물리적으로 nano-second 를 지원한다면, 직접 next tick time(expires) 입력해서 next event 를 발행한다.

    4. 인자로 전달된 next event(`expires`)가 현재 시점(`now`) 보다 과거일 경우, 이건 에러다. 왜냐면, 과거 시간으로 타이머 인터럽트를 예약하는 것과 같기 때문이다. 만약, `delta <= 0 ` 이라면, 사용자가 next event 를 잘못 설정해서 전달한 경우가 된다. 이 때, `force`가 true 라면, 에러가 발생하더라도 에러를 발생시키지 않고, `minimum expiration time` 을 설정해서 next event 가 발생할 수 있도록 retry 를 시도한다(`clockevents_program_min_delta`). 뒤에서 다시 설명한다.

    5. 갠히 코드를 복잡하게 작성했다. 정리하면 아래와 같다. 즉, 상한과 하한 사이에 와야 한다는 뜻이다. 그러나, 이 코드에는 숨은 뜻이 하나 더 있다.
    if max_delta_ns > delta && delta > min_delta_ns
    만약, 코드를 위와 같이 if 문에 짜여저 있는데로 바꾼다면, delta 값이 말도 안되는 큰 값이 될 수도 있다. 예를 들어, delta 가 10000 이고, max_delta_ns = 5000, min_delta_ns = 200 이라 치자. 만약, 코드가 위에 조건문 형식으로 바뀐다면 delta 값은 10000 이 된다. 이렇게 되면 에러가 발생한다. 그런데, max & min 을 이용하면 5000 이 된다.

    6. `nano-second -> cycle counter` 로 교체해주는 공식이다. 이 글을 참고하자. set_next_event 함수는 제조사 및 벤더사에서 제공하는 함수다. 즉, 아키텍처 독립적인 함수다. 실제 물리적으로 하드웨어 레지스터에 쓰는 코드가 이 함수에 작성된다.

    7. clock_event_device.set_next_event 함수가 0을 반환하면 next event 가 예약된 것을 의미한다. 0 이 아니면, 실패를 의미한다. 이 때, force 도 true 라면, retry 를 시도한다. 그러나, arm64 의 `arm_arch_timer` 코드를 확인해보면, set_next_timer 함수는 무조건 0 을 반환하도록 되어있다(아마, 다른 아키텍처에서는 0 이 아닌 경우도 반환하는 것으로 보인다).

     

    : `clockevents_program_event` 함수에서 next event 가 now 보다 과거일 경우에, 그리고 event 가 강제로 발생하는 것을 보장하는 경우에(force == true), minimum expiration time

    //kernel/time/clockevents.c - v6.5
    static int clockevents_program_min_delta(struct clock_event_device *dev)
    {
    	unsigned long long clc;
    	int64_t delta = 0;
    	int i;
    
    	for (i = 0; i < 10; i++) {
    		delta += dev->min_delta_ns; // --- 1
    		dev->next_event = ktime_add_ns(ktime_get(), delta); // --- 1
    
    		if (clockevent_state_shutdown(dev))
    			return 0;
    
    		dev->retries++;
    		clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
    		if (dev->set_next_event((unsigned long) clc, dev) == 0)
    			return 0;
    	}
    	return -ETIME;
    }
    1. 총 10 번의 retry 를 시도한다. 왜 이렇게 여러 번 시도할까? CPU 가 tick interrupt 를 받지 못 할 경우, 언제까지 idle 에 있을지 알 수 가 없다. 그런데, 이 시점에 특정 리소스의 lock 을 소유하고 있었다면, 데드락이 발생할 수 있다. 그러므로, next tick interrupt 를 10번 정도 재시도를 해서 CPU 를 깨울 수 있도록 해야 한다.
    If the driver could not satisfy the program_min_delta for any reason, the lack of a retry means the CPU may not receive a tick interrupt, potentially until the counter does a full period. This leads to rcu_sched timeout messages as the stalled CPU is detected by other CPUs, and other issues if the CPU is holding locks or other resources at the point at which it stalls.

    - 참고 : https://github.com/torvalds/linux/commit/3a29ddb1c5986a6d3f941bfb1f434105203ce7f6
    그리고, 매 번 루프를 돌 때마다 delta에 min_delta_ns 값이 축적하고 있다. 즉, 루프를 돌 때마다 retry 가 실패하면, 주기를 하나 더 추가해서 시도하는 것이다.
    1. 첫 번째 재시도 : now + min_delta_ns
    2. 두 번째 재시도 : now + (min_delta_ns + min_delta_ns)
    3. 세 번째 재시도 : now + (min_delta_ns + min_delta_ns + min_delta_ns)
    ....
    이 뒤로는 특별히 어려운 내용이 없다. 

     

     

    - How to shutdown clock event devices

    : clock event device 를 shutdown 할 때, 왜 `dev->next_event = KTIME_MAX` 를 해야 할까? KTIME_MAX 는 next event 가 없다는 것을 의미하거나 혹은 last event 가 없었다는 것을 의미하기도 한다. 예를 들어, 새로운 타이머 이벤트를 발행할 때, dev->next_event 를 탐색한다. 만약, KTIME_MAX 라면 이전 타이머가 없었다는 뜻이므로 즉각적으로 next event 를 예약할 수 있다. 그런데, KTIME_MAX 가 아니고, 현재 시간보다 이전 시간으로 설정되어 있다면, 이미 타이머 인터럽트가 발생해서 현재 타이머 핸들러가 실행중 이라는 뜻일 수 가 있다. 이럴 경우, next event 를 예약하기가 애매해진다. shutdown 할 때, dev->next_event 를 clear 하지 않기 때문에, 나중에 re-activated 되었을 때, 위와 같은 문제가 발생할 수 있다. 그러므로, KTIME_MAX 로 설정하는 것이다.

    /**
     * clockevents_shutdown - shutdown the device and clear next_event
     * @dev:	device to shutdown
     */
    void clockevents_shutdown(struct clock_event_device *dev)
    {
    	clockevents_switch_state(dev, CLOCK_EVT_STATE_SHUTDOWN);
    	dev->next_event = KTIME_MAX;
    }
    The device shut down does not cleanup the next_event variable of the clock event device. So when the device is reactivated the possible stale next_event value can prevent the device to be reprogrammed as it claims to wait on a event already.

    - 참고 : https://github.com/torvalds/linux/commit/2344abbcbdb82140050e8be29d3d55e4f6fe860b

     

    : `clockevetns_switch_state` 함수는 `__clockevents_switch_state` 함수가 clock event device 의 하드웨어 모드를 성공적으로 변경시키면, CLOCK_EVT_STATE_*(소프트웨어) 를 변경한다.

    /**
     * clockevents_switch_state - set the operating state of a clock event device
     * @dev:	device to modify
     * @state:	new state
     *
     * Must be called with interrupts disabled !
     */
    void clockevents_switch_state(struct clock_event_device *dev,
    			      enum clock_event_state state)
    {
    	if (clockevent_get_state(dev) != state) {
    		if (__clockevents_switch_state(dev, state))
    			return;
    
    		clockevent_set_state(dev, state);
    
    		/*
    		 * A nsec2cyc multiplicator of 0 is invalid and we'd crash
    		 * on it, so fix it up and emit a warning:
    		 */
    		if (clockevent_state_oneshot(dev)) {
    			if (WARN_ON(!dev->mult))
    				dev->mult = 1;
    		}
    	}
    }

     

    : `__clockevents_switch_state` 함수는 clock event device 의 소프트웨어적인 동작 상태 뿐만 아니라, 하드웨어 모드도 바꾸는 함수다. `dev->set_state_*` 인터페이스는 각 제조사 및 벤더사(아키텍처 독립적인)에서 제공하는 함수를 호출한다. 즉, 디바이스의 하드웨어 모드를 바꾼다는 뜻이다. 그래서 하드웨어 모드를 변경하기 전에 `if(dev->features & CLOCK_EVT_FEAT_*)`를 통해서하드웨어적으로 기능이 지원되는지를 먼저 검사한다.

    // kernel/time/clockevents.c - v6.5
    static int __clockevents_switch_state(struct clock_event_device *dev,
    				      enum clock_event_state state)
    {
    	if (dev->features & CLOCK_EVT_FEAT_DUMMY) // --- 1
    		return 0;
    
    	/* Transition with new state-specific callbacks */
    	switch (state) {
    	case CLOCK_EVT_STATE_DETACHED:
    		/* The clockevent device is getting replaced. Shut it down. */
    
    	case CLOCK_EVT_STATE_SHUTDOWN:
    		if (dev->set_state_shutdown)
    			return dev->set_state_shutdown(dev); // --- 2
    		return 0;
    
    	case CLOCK_EVT_STATE_PERIODIC:
    		/* Core internal bug */
    		if (!(dev->features & CLOCK_EVT_FEAT_PERIODIC)) // --- 3
    			return -ENOSYS;
    		if (dev->set_state_periodic)
    			return dev->set_state_periodic(dev); // --- 2
    		return 0;
    
    	case CLOCK_EVT_STATE_ONESHOT:
    		/* Core internal bug */
    		if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT)) // --- 3
    			return -ENOSYS;
    		if (dev->set_state_oneshot)
    			return dev->set_state_oneshot(dev); // --- 2
    		return 0;
    
    	case CLOCK_EVT_STATE_ONESHOT_STOPPED:
    		/* Core internal bug */
    		if (WARN_ONCE(!clockevent_state_oneshot(dev), // --- 3
    			      "Current state: %d\n",
    			      clockevent_get_state(dev)))
    			return -EINVAL;
    
    		if (dev->set_state_oneshot_stopped)
    			return dev->set_state_oneshot_stopped(dev); // --- 2
    		else
    			return -ENOSYS;
    
    	default:
    		return -ENOSYS;
    	}
    }
    1. CLOCK_EVT_FEAT_DUMMY 플래그 SET 된 clock event device 는 왜 제외할까? __clockevents_switch_state 함수는 타이머 하드웨어의 동작 상태를 바꾸는 함수다. 그래서 각 칩 제조사 및 벤더사의 함수를 호출한다. 근데, CLOCK_EVT_FEAT_DUMMY 플래그를 가진 clcok event device 는 실제 하드웨어적으로는 존재하지 않는 디바이스를 의미한다. 가상의 per-CPU 타이머를 만드는 플래그가 CLOCK_EVT_FEAT_DUMMY 이다. 더미 타이머에 대한 자세한 내용은 이 글을 참고하자.

    2. 실제 제조사 및 벤더사와 관련된 코드다. 일반적으로, clocksource 를 커널에 등록하면서, 동시에 clockevent 도 같이 등록하기 때문에, `/drivers/clocksource/` 폴더에 아키텍처 종속적인 clocksource & clockevent 관련 파일들을 작성해놓았다.

    3. CLOCK_EVT_FEAT_* 과 CLOCK_EVT_STATE_* 의 관계를 잘 보여주는 코드다. 예를 들어, CLOCK_EVT_STATE_ONESHOT 동작 모드로 변경하고 싶다면, 반드시 CLOCK_EVT_FEAT_ONESHOT 을 지원해야 한다. PERIODIC 도 마찬가지다.

     

    : arm64 를 기준으로 shutdown 함수는 아래와 같이 구현되어 있다. 아래 ARCH_TIMER_CTRL_ENABLE 은 타이머 인터럽트를 비활성화하는 함수다. 즉, 타이머 자체를 shutdown 하지는 않는다는 것이다. 그렇다면, 언제 다시 활성화할까?

    // drivers/clocksource/arm_arch_timer.c - v6.5
    static __always_inline int arch_timer_shutdown(const int access,
    					       struct clock_event_device *clk)
    {
    	unsigned long ctrl;
    
    	ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
    	ctrl &= ~ARCH_TIMER_CTRL_ENABLE;
    	arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);
    
    	return 0;
    }

     

     

     

    - When to re-enable timer interrupt ?

    : arm64 를 기준으로 타이머 디바이스의 shutdown 함수는 인터럽트 비활성화 라는 것을 알았다. 그렇다면, arm64 는 언제 다시 인터럽트를 활성화할까? `set_next_event` 함수가 호출될 때, 다시 활성화된다. 즉, next event 를 설정하는 API 를 호출하면 된다는 뜻인데, 문제가 있다. 대개, clock event device 레이어에서 next event 를 발행하기 위해서는 디바이스가 CLOCK_EVT_STATE_SHUTDOWN 이 아니여야 한다는 조건을 주로 체크한다(`clockevent_state_shutdown`). 즉, next event 를 발행하기 위해서는 먼저 CLOCK_EVT_STATE_SHUTDOWN 상태를 빠져나와야 한다. 그렇다면, SHUTDOWN 상태가 아니면 ON 이라는 상태로 전환하면 될까? 아니다. clock event device 는 ON 상태는 존재하지 않는다. clock event device 는 동작하고 있다는 거 자체가 ON을 의미한다. 즉, CLOCK_EVT_STATE_PERIODIC 혹은 CLOCK_EVT_STATE_ONESHOT 모드가 `WORKING STATE + ON STATE` 를 나타낸다.

    // drivers/clocksource/arm_arch_timer.c - v6.5
    static __always_inline void set_next_event(const int access, unsigned long evt,
    					   struct clock_event_device *clk)
    {
    	unsigned long ctrl;
    	u64 cnt;
    
    	ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
    	ctrl |= ARCH_TIMER_CTRL_ENABLE;
    	ctrl &= ~ARCH_TIMER_CTRL_IT_MASK;
    
    	if (access == ARCH_TIMER_PHYS_ACCESS)
    		cnt = __arch_counter_get_cntpct();
    	else
    		cnt = __arch_counter_get_cntvct();
    
    	arch_timer_reg_write(access, ARCH_TIMER_REG_CVAL, evt + cnt, clk);
    	arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);
    }

     

    : `tick_resume_oneshot` 함수를 보면, resume 이라고 되어있는데, 단지 동작 모드만 ONESHOT 으로 변경시키는 것을 알 수 있다. 이렇게 함으로써, next event 를 발행시키는 clockevents_program_event 함수를 호출하면 내부적으로 `dev->set_next_event` 함수가 호출되면서 인터럽트가 enable 된다. clockevents_program_event 함수에 두 번째 인자로 현재 시간을 전달하는 이유는 next event 를 발행시킬 목적이 없기 때문이다. 단지, clock event device 의 인터럽트를 re-enable 하기 위해 호출하는 것이기 때문이다. 그런데, 여기서 세 번째인 force 에 true 를 주지 않을 경우 인터럽트를 re-enable 할 수 없다. 그러므로, 반드시 true 를 넣어줘야 한다.

    // kernel/time/tick-oneshot.c - v6.5
    /**
     * tick_resume_oneshot - resume oneshot mode
     */
    void tick_resume_oneshot(void)
    {
    	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);
    
    	clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
    	clockevents_program_event(dev, ktime_get(), true);
    }

     

     

     

    - Release

    : `clockevents_exchange_device` 함수는 기존에 특정 타이머에 ATTACHED 되어있던 clock event device 를 재활용한다. 왜냐면, clock event devie 의 매칭 방식이 우선순위 방식이기 때문이다. 즉, 더 높은 우선 순위를 갖는 디바이스가 나타낼 경우, 새로운 디바이스가 기존 디바이스의 자리를 대신하게 된다. 그렇면서, 기존 디바이스는 리매칭 프로세스를 대기하게 된다.

    // kernel/time/clockevents.c - v6.5
    /**
     * clockevents_exchange_device - release and request clock devices
     * @old:	device to release (can be NULL)
     * @new:	device to request (can be NULL)
     *
     * Called from various tick functions with clockevents_lock held and
     * interrupts disabled.
     */
    void clockevents_exchange_device(struct clock_event_device *old,
    				 struct clock_event_device *new)
    {
    	/*
    	 * Caller releases a clock event device. We queue it into the
    	 * released list and do a notify add later.
    	 */
    	if (old) { 
    		module_put(old->owner);
    		clockevents_switch_state(old, CLOCK_EVT_STATE_DETACHED); // --- 1
    		list_move(&old->list, &clockevents_released); // --- 2
    	}
    
    	if (new) {
    		BUG_ON(!clockevent_state_detached(new)); // --- 3
    		clockevents_shutdown(new); // --- 3
    	}
    }
    1. old 디바이스는 더 이상 동작하면 안된다. 그리고, 이제는 더 이상 특정 타이머에 ATTACHED 되어있는 것이 아니므로, CLOCK_EVT_STATE_DETACHED(==CLOCK_EVT_STATE_SHUTDOWN) 상태로 진입시킨다.

    2.  list_move 함수는 첫 번째 인자로 전달된 리스트 엔트리를 연결하고 있는 연결 리스트(clockevent_devices)에서 삭제하고, 두 번째 인자인 clockevent_released 연결리스트에 추가된다. clockevents_released 리스트에 저장되면, 리매칭 대상이 된다. 뒤에서 다시 다룬다.

    3. new 디바이스가 이미 초기화가 끝나서 특정 타이머에 ATTACHED 가 되어있었다면, 이건 버그다. 이 내용은 굉장히 중요하다. 왜냐면, clockevents_exchange_device 함수가 void 이기 때문이다. 이 함수는 드라이버 개발자들에게 export 되어있다. 즉, 마음대로 사용이 가능하다는 뜻이다. 그런데, 드라이버 개발자가 여기에 실수로 이미 ATTACHED 된 clock event device 를 전달하면, 커널 패닉을 일으킬 것이라는 경고다. 사용법을 명확히 알려주고 있으니 반드시 따라야 한다.

    일반적으로, 로컬 타이머를 power-off 하는 경우는 CPU 도 같이 power-off 가 되거나 idle 상태일 경우가 높다. 로컬 타이머만 별도로 power-off 하지는 않는다. 앞에서도 봤겠지만, arm 혹은 x86 같은 경우는 `set_state_shutdown` 콜백 함수를 인터럽트 비활성화 정도로 구현하고 있다. 이제 막 등록되었기 때문에, 동작 모드가 확정되기 전까지는 인터럽트를 비활성화 하는 것이다.

     

    : `clockevents_notify_released` 함수는 clockevents_register_device 함수에서만 호출되는 함수다. 즉, 커널에 새로운 clock event device 를 등록할 때만 호출하는 함수다. 리눅스 커널에서 clock event device 가 CPU 에 매칭되는 과정은 한 번에 BEST 하게 이루어지지 않는다. 새로운 clock event device 가 커널에 등록될 때 마다, 이전에 등록된 디바이스와 새로 등록된 다비이스를 비교하면서 누가 더 suitable 한지를 판단한다. 이 과정에서 이미 등록된 clock event device 가 교체되는 경우가 발생할 수 있다. 이 디바이스는 다른 CPU 소속일 가능성이 있다. 그러모르, 절대 버리면 안된다. 재탐색을 진행해야 한다. 이 때, 이전 clock event device 를 재활용 리스트에 넣는 함수가 `clockevents_exchange_device` 함수이고, 이 재활용 리스트를 이용해서 특정 CPU에 리매칭 시키는 함수가 `clockevents_notify_released` 함수다.

    // kernel/time/clockevents.c - v6.5
    /*
     * Called after a notify add to make devices available which were
     * released from the notifier call.
     */
    static void clockevents_notify_released(void)
    {
    	struct clock_event_device *dev;
    
    	while (!list_empty(&clockevents_released)) { // --- 1
    		dev = list_entry(clockevents_released.next,
    				 struct clock_event_device, list);
    		list_move(&dev->list, &clockevent_devices); // --- 2
    		tick_check_new_device(dev); // --- 3
    	}
    }
    1. `clockevetns_released 연결 리스트에는 리매칭 프로세스를 기다리는 clock event device 들이 대기하고 있다. 모조리 탐색해서 리매칭 프로세스를 진행한다.

    2. clock event device 매칭 프로세스는 deterministic 하다. 즉, 무조건 매칭이 되버린다. 그래서, 매칭이 된 목록만 저장되는 clockevent_devices 에 해당 clock event device 를 연결한다. list_move 함수는 첫 번째 인자로 전달된 리스트 엔트리를 연결하고 있는 연결 리스트(clockevent_released)에서 삭제하고, 두 번째 인자인 clockevent_devices 연결리스트에 추가된다.

    3. 로컬 타이머에 ATTACHED(매칭) 하기위해 리매칭을 시작한다.
Designed by Tistory.