ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Cpuidle - overview & data structure
    Linux/kernel 2023. 9. 4. 16:24

    글의 참고

    - https://intel.github.io/wult/pages/how-it-works.html

    - https://lwn.net/Articles/384146/

    - http://www.wowotech.net/pm_subsystem/cpuidle_overview.html

    - https://landley.net/kdocs/ols/2007/ols2007v2-pages-119-126.pdf

    - https://wiki.linuxfoundation.org/realtime/documentation/howto/applications/cpuidle

    - https://elinux.org/images/8/86/CPU_idle.pdf

    - https://manybutfinite.com/post/what-does-an-idle-cpu-do/

    - https://lwn.net/Articles/618074/

    - https://events.static.linuxfound.org/sites/events/files/slides/lp-linuxcon14.pdf

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

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


    글의 전제

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

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


    글의 내용

    - Cpuidle overview

    : CPU는 명령어를 실행하는 역할을 한다. 그런데, 만약 실행할 명령어가 없을 경우는 어떻게 될까? CPU를 멈추면 된다. 즉, 소비 전력을 줄이기 위해 CPU를 멈추는 것이다. 그런데, 언제 혹은 어떤식으로 CPU를 멈춰야 할까? 이 시점을 결정하는것이 사실 굉장히 어렵다. 리눅스 커널에서는 이 문제를 어떻게 해결하는지 알아보자.

     

    : 리눅스 커널에는 CPU가 사용되는 용도(실행 컨텍스트)를 크게 2가지로 나눈다. 프로세스 혹은 스레드가 CPU를 선점해서 사용하는 `프로세스 컨텍스트`가 하나있고, 인터럽트가 CPU를 선점해서 사용하는 `인터럽트 컨텍스트`가 있다. 리눅스 커널은 CPU가 프로세스(스레드)를 처리하거나 혹은 인터럽트를 처리하는 경우를 제외하면, 사용되지 않는다고 판단한다. 바로, 이 시점에 CPU가 `IDLE` 상태로 들어간다.

     

    : 그런데, 위에 내용은 말은 쉽다. 위에 내용을 소프트웨어적으로 구현하는 것은 굉장히 어렵다. 즉, 소프트웨어적으로 실행 컨텍스트가 없다는 것을 어떻게 알 수 있을까? 즉, 소프트웨어적으로 언제 CPU가 IDLE 상태로 들어가야 할까? 이걸 알려주는 친구가 바로 `Idle process`다. 그렇다면, `Idle process`의 정체가 뭘까?

     

    : `0`번 프로세스는 부트업 초기화 작업(`start_kernel`)을 완료하고, `init process(PID - 1)`를 생성하면, 자발적으로 `idle process`로 변경한다(`idle process`는 각 CPU 코어마다 존재한다). 이 때, 가장 중요한 건 `idle process`는 스케줄러가 런큐에서 스레드를 선택하는데 있어 우선 순위를 가장 낮게 설정되어 있다는 것이다. 즉, 리눅스 커널에서 `idle process`가 실행되었다는 것은 CPU가 더 이상 실행시킬 스레드가 없다는 것을 뜻이고, 이 말은 해당 CPU 코어는 할 일이 없다는 말과 같다. `ARM`같은 경우,`idle process`가 실행되면, `WFI` 명령어를 실행해서 CPU를 IDLE 상태로 보낸다. `idle process`는 실제로 아무일도 하지 않는다. 단지, 무한 루프안에서 반복적으로 `WFI` 명령어를 실행하여 CPUidle 상태를 유지시킨다.

     

    : 이미 위에서 설명했지만, CPU를 IDLE 상태로 보내는 명령어는 `WFI` 명령어다. 그런데, 여기 까지만 보면 `CPUidle framework`가 전혀 필요가 없어보인다. 왜냐면, `idle process`와 `WFI` 명령어만 있으면, 적당한 사용 시나리오에 맞게 커스텀한 개발을 하면 되기 때문이다. 그렇다면, `CPUidle framework`는 왜 필요할까?

     

    1. Standardization : OS는 기본적으로 표준화된 인터페이스를 제공하는 소프트웨어다. 즉, `CPUidle framework`를 통해서 칩 제조사마다 다른 인터페이스를 하나로 통일함으로써, 개발 편리성과 좋은 이식성을 제공한다. `CPUidle` 관련 2가지 중요한 내용을 표준화해야 한다.

    - 여러 개의 CPUidle 상태 표준화 : SoC가 점점 발전할 수록, `CPUidle` 상태는 점점 다양화 되어간다. 예를 들어, x86을 기반으로 한 ACPI는 `C-STATE`라고 해서 여러 개의 프로세서의 IDLE 상태를 정의하고 있다. 각 `CPUidle` 상태는 소비 전력량과 `exit latency` 등이 다르다. 이런 여러개의 `CPUidle` 상태를 소프트웨어적으로 통일화하는 것이다.

    - CPUidle 시점 표준화 : `CPUidle framework`는 어느 시점에서는 어떤 `CPUidle 상태`로 변경해야 하는지를 알려준다. 이 때는 주로 `PM QoS`의 도움을 받게 된다.

     

    : 정리하자면 다음과 같다.

     

    1. 언제 IDLE 상태로 들어갈 것인가 : Idle process
    2. 어떻게 IDLE 상태로 들어갈 것인가 : WFI instruction

     

    : 이번에는 프로세서 IDLE 상태를 정의하는데 있어서, 고려해야 할 부분들에 대해 생각해보자. cpuidle 상태가 여러 개라면, 가장 먼저 고려해야 할 건, 각 상태마다 얼마나 전류를 소모하는 가이다. 그 다음으로 중요한 건 요소들이 몇 가지 존재한다.

    1. Target residency : CPU가 IDLE 상태로 진입하고 나오는데 소요되는 에너지 및 시간이 있다. 이 시간 및 에너지보다 CPU가 IDLE 상태에 더 오래 있을 것이 확실한 경우에만 IDLE 상태로 진입해야 한다. `target residency`는 시간의 양을 의미하며, 아래그림 에서 녹색 부분에 해당한다.
    Deeper C states need to be idle longer to compensate for the energy spent entering and exiting

    https://blog.csdn.net/feelabclihu/article/details/125688355

    1. Exit Latency : CPU가 `IDLE 상태`에서 `풀-파워`로 돌아오는데 걸린 시간의 양
    Deeper C states have higher exit latency

     

    : `ACPI`의 C-STATE를 기준으로 CPU IDLE 상태를 설명하면 다음과 같다. `C-STATE`에서는 숫자가 커 질 수록 더 깊은 IDLE 상태로 빠지게 된다. 그리고 그 만큼 전력 소모를 아낄 수 있게된다. 그러나, 다시 풀-파워 상태로 복귀하는데 시간이 오래 걸린다는 단점이 존재한다.


    https://blog.csdn.net/feelabclihu/article/details/106866457

     

     

     

    - Cpuidle architecture

    : 리눅스 커널 CPUidle 서브-시스템은 총 4개의 요소로 구성되어 있다. 아래 그림에서 `CPU core`는 아키텍처 종속적인 영역이다.

     

    1. kernel sched(`/kernel/sched/idle.c`) : `idle process`
    2. cpuidle core : `idle core`
    3. cpuidle governor : `idle policy`
    4. cpuidle driver : `idle control`

     

    : CPU 스케줄러는 현재 실행 중인 프로세스도 없고, 런큐에 실행되기를 기다리는 프로세스도 없다는 것을 알면, 우선 순위가 가장 낮은 프로세스인 `유휴 프로세스`를 실행한다. 유휴 프로세스가 실행되면, CPU를 IDLE 상태로 보내기 위해서 `cpudile_idle_call` 함수를 호출해서 이 상황을 `Cpuidle 프레임워크`에게 알린다.



    https://zhuanlan.zhihu.com/p/548268554

    https://blog.csdn.net/feelabclihu/article/details/106866457

     

     

     

    - CPUidle data structure

    1. Cpuidle state

    : CPU 제조사마다 별도의 `CPUidle 상태`를 가지고 있다. 심지어, 최신 프로세서들은 여러 개의 `CPUidle 상태`를 가지고 있고, 각 CPU 코어들은 독립적으로 각자의 idle 상태를 컨트롤하게 된다. 리눅스 커널은 `struct cpuidle_state` 구조체를 통해서 아키텍처 종속적인 `CPUidle 상태`를 추상화한다.

    // include/linux/cpuidle.h - v6.5
    struct cpuidle_state {
    	char		name[CPUIDLE_NAME_LEN];
    	char		desc[CPUIDLE_DESC_LEN];
    
    	s64		exit_latency_ns;
    	s64		target_residency_ns;
    	unsigned int	flags;
    	unsigned int	exit_latency; /* in US */
    	int		power_usage; /* in mW */
    	unsigned int	target_residency; /* in US */
    
    	int (*enter)	(struct cpuidle_device *dev,
    			struct cpuidle_driver *drv,
    			int index);
    
    	int (*enter_dead) (struct cpuidle_device *dev, int index);
    
    	/*
    	 * CPUs execute ->enter_s2idle with the local tick or entire timekeeping
    	 * suspended, so it must not re-enable interrupts at any point (even
    	 * temporarily) or attempt to change states of clock event devices.
    	 *
    	 * This callback may point to the same function as ->enter if all of
    	 * the above requirements are met by it.
    	 */
    	int (*enter_s2idle)(struct cpuidle_device *dev,
    			    struct cpuidle_driver *drv,
    			    int index);
    };

    `struct cpuidle_state` 구조체 필드에서 중요한 내용만 요약하면 다음과 같다.

     

    0. name & desc : 시스템에 존재하는 모든 `CPUidle 상태`는 sysfs에 exported 된다. 이 때, `name`으로 sysfs에 나타나고 `desc`는 해당 CPUidle 상태에 대해 설명해준다.

    1. exit_latency_us : `from idle state to working state` 하는 걸린 시간. 이 값은 `CPUidle framework`가 최적의 idle 전략을 결정하는데, 굉장히 큰 역할을 한다. 위에서 이미 한 번 설명을 했다. 

    2. target_residency_us : CPU가 해당 idle 상태가 되었을 때, 최소한 이 시간은 해당 상태에 있어줘야 의미가 있다고 판단되는 시간. 위에서 이미 한 번 설명을 했다.

    3. flags : 해당 `CPUidle 상태`의 상태를 나타낸다. 몇 가지 플래그만 소개한다.
    - CPUIDLE_FLAG_OFF : 해당 `CPUidle 상태`를 비활성화한다. 즉, 사용하지 못한다.
    - CPUIDLE_FLAG_TIMER_STOP : 해당 `CPUidle 상태`에 진입하면 timer를 off 한다.

    4. power_usage : CPU가 해당 idle 상태가 되었을 때, 소비되는 전력 양(단위 mW)

    5. exit_latency, target_residency : 기존 자료 구조와의 호환성을 지키기 위해 사용

    6. enter : CPU가 해당 idle 상태로 들어간다고 결정되었을 때, 호출되는 함수.

     

    : `struct cpuidle_state` 구조체를 정리해보면 2가지 기능을 제공한다.

     

    1. 각 CPU 제조사에서 제공하는 `CPUidle 상태`에 대한 정보를 가지고 있다. 예를 들어, ACPI에서 제시하는 `C-STATE`는 `C0 ~ C[N]` 까지 있는데, `struct cpuidle_state` 구조체는 각각의 C0, C1, C2 상태에 대한 정보를 저장한다.

    2. CPU가 IDLE 상태로 진입해야 할 때, 무조건 가장 강력한 IDLE 상태로만 변경해야 하는 것은 아니다. 여러 가지 내용을 고려해서 현재 상태에서 퍼포먼스에 최대한 영향을 주지 않는 가장 강력한 IDLE 상태를 선택하게 된다. 이 때, `struct cpudile_state` 구조체의 내용을 기반으로 어떤 IDLE 상태로 갈지를 결정한다.

     

     

    2. Cpuidle driver

    : 실제 `struct cpuidle_state`들을 관리하는 객체가 바로 `struct cpuidle_driver` 구조체다. 리눅스 커널의 `CPUidle core` 서브-시스템은 시스템 전역적으로 딱 한개의 `CPUidle driver`만 허용한다(사실, `CPUidle 서브 시스템`에서 `CPUidle driver`가 하는 일은 주로 `register & unregister` 정도의 작업이다). [ 참고1 ]

    // include/linux/cpuidle.h - v6.5
    struct cpuidle_driver {
    	const char		*name;
    	struct module 		*owner;
    
            /* used by the cpuidle framework to setup the broadcast timer */
    	unsigned int            bctimer:1;
    	/* states array must be ordered in decreasing power consumption */
    	struct cpuidle_state	states[CPUIDLE_STATE_MAX];
    	int			state_count;
    	int			safe_state_index;
    
    	/* the driver handles the cpus in cpumask */
    	struct cpumask		*cpumask;
    
    	/* preferred governor to switch at register time */
    	const char		*governor;
    };

    : `struct cpuidle_driver` 구조체 필드에서 중요한 내용만 요약하면 다음과 같다.

    1. bctimer : boardcast timer를 사용할 것인지를 나타낸다. 뒤에서 다시 다룬다.

    2. states & state_count : `struct cpuidle_device`는 자신이 지원하는 cpuidle 상태의 정보만 저장한다. 그래서, 시스템에서 사용 가능한 모든 cpuidle 상태를 어딘가에는 저장해야 한다. 바로, `states` 변수에 저장한다. `state_count`는 cpuidle 상태의 개수를 의미한다.

    3. safe_state_index : coupled idle과 관련있는 변수다. 뒤에서 다시 다룬다.

    4. cpumask : 해당 `CPUidle driver`가 지원하는 CPU 코어들을 나타낸다. 

    5. governor : 해당 CPUidle driver가 디폴트로 사용하고 싶은 CPUidle governor를 명시한다. 만약, 명시하지 않을 경우, CPUidle 정책 중 가장 높은 점수를 차지하고 싶은 정책을 사용한다.

     

     

     

    3. Cpuidle device

    : `CPUidle 상태`의 초기화가 완료되면, 실제 CPU 코어에 대응하는 CPU 디바이스에 연결시켜야 한다. 왜냐면, `CPUidle framework`는 어떤 CPU 디바이스가 어떤 IDLE 상태들을 지원하는지 알아야 하기 때문이다.

    // include/linux/cpuidle.h - v6.5
    struct cpuidle_state_usage {
    	unsigned long long	disable;
    	unsigned long long	usage;
    	u64			time_ns;
    	unsigned long long	above; /* Number of times it's been too deep */
    	unsigned long long	below; /* Number of times it's been too shallow */
    	unsigned long long	rejected; /* Number of times idle entry was rejected */
    #ifdef CONFIG_SUSPEND
    	unsigned long long	s2idle_usage;
    	unsigned long long	s2idle_time; /* in US */
    #endif
    };
    ....
    
    struct cpuidle_device {
    	unsigned int		registered:1;
    	unsigned int		enabled:1;
    	unsigned int		poll_time_limit:1;
    	unsigned int		cpu;
    	ktime_t			next_hrtimer;
    
    	int			last_state_idx;
    	u64			last_residency_ns;
    	u64			poll_limit_ns;
    	u64			forced_idle_latency_limit_ns;
    	struct cpuidle_state_usage	states_usage[CPUIDLE_STATE_MAX];
    	struct cpuidle_state_kobj *kobjs[CPUIDLE_STATE_MAX];
    	struct cpuidle_driver_kobj *kobj_driver;
    	struct cpuidle_device_kobj *kobj_dev;
    	struct list_head 	device_list;
    
    #ifdef CONFIG_ARCH_NEEDS_CPU_IDLE_COUPLED
    	cpumask_t		coupled_cpus;
    	struct cpuidle_coupled	*coupled;
    #endif
    };

    : `struct cpuidle_device` 구조체 필드에서 중요한 내용만 요약하면 다음과 같다.

     

    1. registerd : 해당 CPU 디바이스가 CPU 드라이버에 의해 등록이 되었는지 여부를 나타냄.

    2. enabled : 해당 CPU 디바이스의 `Idlie PM`이 활성화 되어있는지 여부를 나타냄.

    3. poll_time_limit : CPU가 idle 상태에 들어갔다는 것은 해당 CPU 스케줄러에서 더 이상 실행할 작업이 없다는 것을 의미한다. 이 상태를 길게 유지하면 할 수록, 더 많은 소비 전력을 아낄 수 있다. 그런데, 문제는 `WFI` 상태는 주기적으로 타이머 인터럽트에 의해 wakeup이 된다는 것이다. 타이머 인터럽트 때문에 ARM CPU는 `idle->wake->idle->wake ...`를 반복하게 되고, 결국 소비 전력만 더 늘리는 꼴이된다. 이 문제를 해결하기 위해 커널은 `NO_HZ` 모드를 제시했다. 이 모드는 CPU가 idle 상태에 들어가면, 타이머 인터럽트(tick)을 Off 시키는 것을 의미한다.

    그런데, 특별한 경우에는 idle 상태가 굉장히 짧은 경우가 있을 수 있다. 예를 들어, `one tick cycle` 보다 더 짧게 idle 상태에 있을 수 도 있다는 것이다. 이 시점에는 idle 상태로 진입하지 않는게 훨씬 이득이다. 결국, 이 변수는 아래의 `poll_time_ns`을 초과하면 true가 되고, 그 의미는 polling을 설정한 시간만큼 했다는 뜻이 된다.

    4. cpu : 해당 디바이스가 나타내는 CPU의 번호.

    5. next_hrtimer : `CPUidle governor`가 `CPUidle state`를 선택할 때, 다음 타이머 인터럽트가 언제 발생할 지에 대한 부분까지 포함하고 있어야 한다. 예를 들어, 타이머 인터럽트가 발생하면 일반적으로 CPU는 idle 상태를 종료하고 요청한 업무를 처리해야 한다. 이 때, CPU가 다음 번에는 얼마나 idle 상태를 유지할 수 있을지를 계산한다. 이 때, 다음 번 타이머 인터럽트는 매우 핵심적인 요소라고 할 수 있다.

    6. last_state_idx & last_residency_ns : 바로 직전 진입했었던 CPUidle 상태의 인덱스 & 바로 직전 CPUidle 상태에 얼마나 있었는지

    7. poll_limit_ns : `poll idle state` 사용 시, 얼마나 polling을 할 것인지를 저장. 즉, `polling timeout` 이라고 보면 된ㄷ.

    8. states_usage : 해당 CPU 디바이스의 idle 상태에 대한 정보를 저장한다. 예를 들어, `C0` 상태에 대한 정보는 `states_usage[0]`, `C1`은 `states_usage[1]에 저장한다고 볼 수 있다.
    - disable : 해당 디바이스에서 해당 idle 상태는 비활성화임을 나타냄.
    - usage : 디바이스가 해당 idle 상태에 진입했던 횟수
    - time_ns : 디바이스가 해당 idle 상태에 있었던 총 시간
    - above, below : CPU idle governor가 적절한 IDLE 상태를 선택하는데 있어서, 중요한 `hint`를 제공해준다[참고1].

    9. kobjs, kobj_driver, kobj_dev : sysfs 관련된 변수들

    10. device_list : 해당 CPU 디바이스를 글로벌 cpudile 연결 리스트인 `cpuidle_detected_devices`에 연결할 때 사용된다.

    11. coupled_cpus  & coupled : `coupled_cpus` 는 현재 CPU 디바이스가 나타내는 CPU의 coupled cpu`s 들을 나타낸다. 

     

     

    4. Cpuidle governor [ 참고1 ]

    : `CPUidle governor`는 CPU가 idle 상태에 들어갈 때, 어떤 idle 상태에 들어가야 하는지를 결정하는 전략들을 가지고 있다.

    // include/linux/cpuidle.h - v6.5
    struct cpuidle_governor {
    	char			name[CPUIDLE_NAME_LEN];
    	struct list_head 	governor_list;
    	unsigned int		rating;
    
    	int  (*enable)		(struct cpuidle_driver *drv,
    					struct cpuidle_device *dev);
    	void (*disable)		(struct cpuidle_driver *drv,
    					struct cpuidle_device *dev);
    
    	int  (*select)		(struct cpuidle_driver *drv,
    					struct cpuidle_device *dev,
    					bool *stop_tick);
    	void (*reflect)		(struct cpuidle_device *dev, int index);
    };

    : `struct cpuidle_governor` 구조체 필드에서 중요한 내용만 요약하면 다음과 같다.

     

    2. governor_list : 시스템에 존재하는 모든 CPUidle 거버너를 관리하는 리스트

    3. rating : 해당 거버너의 유용성을 의미한다(우선 순위와 같다고 보면 된다). 값이 클수록 유용성이 높다. 리눅스 커널은 한 시점에 한 거버너밖에 사용할 수 없다.

    4. enable & disable : `enable` 함수는 해당 CPU 디바이스의 `Idle PM`기능을 활성화한다. 즉, 이 함수는 해당 CPU 디바이스가 CPUdile 상태로 진입하기전에 딱 한번 호출되는 함수다. `disble` 함수는 그 반대다.

    5. select : 실제 idle 상태를 선택하는 콜백함수. 시스템의 현재 상태와 각 CPUidle 상태를 고려해서 결정한다.

    6. reflect : 해당 CPU 디바이스의 이전 CPUidle 상태를 반환한다.

     

     

    - Broadcast timer [ 참고1 ]

    : `local timer`가 비활성화된 프로세서를 어떻게 `wake-up` 시킬까? 리눅스 커널은 이 문제를 해소하기 위해 `tick broadcast framework`를 제공한다. `broadcast timer`는 기본적으로 `CPUIDLE_FLAG_TIMER_STOP` 플래그가 설정된 CPUidle 상태에 적용되는 타이머다. CPU가 `CPUIDLE_FLAG_TIMER_STOP` 플래그가 설정된 idle 상태로 진입할 경우, 해당 CPU의 local timer를 off 시킨다. 이 시점을 기준으로 `clock event framework`는 더 이상 local timer를 기준으로 인터럽트를 발생시키지 않는다. `broadcast timer`는 시스템에 존재하는 모든 CPU 와는 상관없이, 독립적으로 동작하는 타이머다. 즉, CPU와는 독립적인 모듈이니 CPU의 상태가 변하더라도(C3이 되더라도) 전혀 영향을 받지 않는다.

    //drivers/cpuidle/driver.c - v6.5
    
    static void cpuidle_setup_broadcast_timer(void *arg)
    {
    	if (arg)
    		tick_broadcast_enable();
    	else
    		tick_broadcast_disable();
    }
    
    static void __cpuidle_driver_init(struct cpuidle_driver *drv)
    {
    	int i;
    	....
        
    	for (i = 0; i < drv->state_count; i++) {
    		struct cpuidle_state *s = &drv->states[i];
    
    		if (s->flags & CPUIDLE_FLAG_TIMER_STOP)
    			drv->bctimer = 1;
    		....
    	}
    }
    
    static int __cpuidle_register_driver(struct cpuidle_driver *drv)
    {
    	int ret;
    
    	....
    
    	__cpuidle_driver_init(drv);
    	....
        
    	if (drv->bctimer)
    		on_each_cpu_mask(drv->cpumask, cpuidle_setup_broadcast_timer,
    				 (void *)1, 1);
    
    	return 0;
    }
    
    static void __cpuidle_unregister_driver(struct cpuidle_driver *drv)
    {
    	if (drv->bctimer) {
    		drv->bctimer = 0;
    		on_each_cpu_mask(drv->cpumask, cpuidle_setup_broadcast_timer,
    				 NULL, 1);
    	}
    	....
    }

    : `CPUidle driver`는 시스템 전체에서 하나만 존재하는 드라이버다. CPU 칩 제조사는 자신들이 만든 `CPUidle driver`를 등록하는 과정에서 등록된 CPUidle 상태 중 하나라도 `CPUIDLE_FLAG_TIMER_STOP`이 존재하면, `broadcast timer`를 사용한다고 설정한다(`drv->bctimer = 1`). 그 이후, `on_each_cpu_mask` 함수를 통해서 모든 online CPU`s(`drv->cpumask`) 들한테서 `cpuidle_setup_broadcast_timer` 함수를 호출하도록 한다(세 번째 인자가 인자로 전달된다).

     

    : tick_broadcast_* 함수가 호출되면 어떻게 될까 ? __cpuidle_register_driver 함수는 커널의 초기화 시점에 호출되는 함수다. 이 말은 그 안에 있는 cpuidle_setup_broadcast_timer 함수들도 cpuidle driver 가 커널에 등록되는 시점에 같이 호출된다는 뜻이다. 그런데, 브로드 캐스트 타이머는 CPU가 IDLE 상태에 있을 때만, 필요한 타이머다. 그래서 `tick_broadcast_enable` 함수는 브로드 캐스트 타이머가 ACTIVE 하게 만들지는 않는다. 단순히 ENABLE(ON)만 하게한다. 실제, CPU가 IDLE 상태로 진입할 때는 이 함수가 아닌 `tick_broadcast_enter` 함수를 통해 브로드 캐스트 타이머를 ACTIVE 상태로 만든다. tick_broadcast_enable / tick_broadcast_disable 가 한쌍인 것처럼, tick_broadcast_enter(ACTIVE) / tick_broadcast_exit(DE-ACTIVE) 또한 한쌍이다.

    IDLE entry : tick_broadcast_enable -> tick_broadcast_enter
    IDLE exit : tick_broadcast_exit -> tick_broadcast_disable 

     

     

     

    - Poll idle state [ 참고1 참고2 참고3]

    : `poll idle` 상태는 흔히 `CPU relax` 라고도 한다. 위에서 잠깐 설명했지만, `one tick cycle`보다 짧게 idle 상태를 들어가야 한다거나, 혹은 그저 CPU를 무의미하게 무한 루프를 돌게하는 루틴이다. 이 기능은 `CONFIG_ARCH_HAS_CPU_RELAX` 설정이 되어있어야 사용이 가능하다. 현재 이 기능은 `cpuidle governor` 중에 하나인 `haltpoll` 정책에서 사용하고 있다.

    but if the processor does not support any idle states, or there is not enough time to spend in an idle state before the next wakeup event, or there are strict latency constraints preventing any of the available idle states from being used, the CPU will simply execute more or less useless instructions in a loop until it is assigned a new task to run.

    - 참고 : https://docs.kernel.org/admin-guide/pm/cpuidle.html
    // arch/arm64/include/asm/vdso/processor.h - v6.5
    static inline void cpu_relax(void)
    {
    	asm volatile("yield" ::: "memory");
    }
    ....
    
    // include/linux/cpuidle.h - v6.5
    #if defined(CONFIG_CPU_IDLE) && defined(CONFIG_ARCH_HAS_CPU_RELAX)
    void cpuidle_poll_state_init(struct cpuidle_driver *drv);
    #else
    static inline void cpuidle_poll_state_init(struct cpuidle_driver *drv) {}
    #endif
    ....
    
    // drivers/cpuidle/poll_state.c - v6.5
    #define POLL_IDLE_RELAX_COUNT	200
    
    static int __cpuidle poll_idle(struct cpuidle_device *dev,
    			       struct cpuidle_driver *drv, int index)
    {
    	u64 time_start;
    
    	time_start = local_clock_noinstr();
    
    	dev->poll_time_limit = false;
    
    	raw_local_irq_enable();
    	if (!current_set_polling_and_test()) {
    		unsigned int loop_count = 0;
    		u64 limit;
    
    		limit = cpuidle_poll_time(drv, dev);
    
    		while (!need_resched()) {
    			cpu_relax();
    			if (loop_count++ < POLL_IDLE_RELAX_COUNT)
    				continue;
    
    			loop_count = 0;
    			if (local_clock_noinstr() - time_start > limit) {
    				dev->poll_time_limit = true;
    				break;
    			}
    		}
    	}
    	raw_local_irq_disable();
    
    	current_clr_polling();
    
    	return index;
    }
    
    void cpuidle_poll_state_init(struct cpuidle_driver *drv)
    {
    	struct cpuidle_state *state = &drv->states[0];
    
    	snprintf(state->name, CPUIDLE_NAME_LEN, "POLL");
    	snprintf(state->desc, CPUIDLE_DESC_LEN, "CPUIDLE CORE POLL IDLE");
    	state->exit_latency = 0;
    	state->target_residency = 0;
    	state->exit_latency_ns = 0;
    	state->target_residency_ns = 0;
    	state->power_usage = -1;
    	state->enter = poll_idle;
    	state->flags = CPUIDLE_FLAG_POLLING;
    }
    EXPORT_SYMBOL_GPL(cpuidle_poll_state_init);

     

    : 이 기능이 활성화되면, `CPUidle 상태`중에 [0]번은 반드시 `Poll idle` 상태를 의미하게된다. `exit_latency`, `target_residency` 등이 모두 `0`으로 설정되고, `power_usage`는 `-1`로 설정이 된다. 즉, 이 말은 `Poll idle` 상태는 `CPUidle` 상태는 아니라는 것이다. 왜냐면, idle 상태로 진입하는데 시간이 걸리지 않는다는 것은 `풀-파워`로 동작한다는 말과 같기 때문이다. 그리고, `enter` 콜백에 `poll_idle` 함수를 설정하고, `flags`는 `CPUIDLE_FLAG_POLLING`을 설정한다.

     

    CPUIDLE_FLAG_POLL says that this state causes no latency, but also fails to save any power.

    - 참고 : https://lwn.net/Articles/384146/

     

    : `poll_idle` 함수의 핵심은 `cpu_relax` 함수다. 만약, 스케줄링이 필요없는 경우, 즉, CPU가 할게 없는 경우, `nop` 명령어를 반복해서 수행하게 된다. 즉, 쓸데없는 CPU 사이클을 낭비하는 것이다. 탈출 조건은 2가지가 있다.

    1. 기본적으로, `POLL_IDLE_RELAX_COUNT` 보다 루프를 반복한 횟수(`loop_count`)가 더 커야한다.
    2. 앞에 조건문이 성립했다는 전제하에, 루프를 반복한 시간이, 즉, `poll idle` 상태에 있었던 시간이 `struct cpuidle_device.poll_limit_ns` 보다 더 커야한다.

     

    : 위의 2가지 조건이 순차적으로 만족이 되면, `struct cpuidle_device.poll_time_limit` 이 `true`가 된다. 즉, 정해진 시간동안 `polling`을 모드 마쳤다는 뜻이다. 이 값이 어떻게 쓰이는지는 나중에 `CPUidle govenor` 에서 다시 다루도록 한다. 참고로, `arm64` 에서는 `cpu_relex` 함수를 `yield 명령어`로 구현하고 있다. 이 명령어는 아래와 같은 특징을 가지고 있다.

     

    1. 만약, `yield` 명령어를 지원하지 않는 ARM 프로세서라면, `yield` 명령어는 `nop` 명령어와 같다.
    2. 만약, `yield` 명령어를 지원하는 ARM 프로세서라면, `yield` 명령어는 현재 프로세스는 언제든지 `swapped out`될 준비가 되어있다고 말하는 것이다.

     

    YIELD is a hint instruction. Software with a multithreading capability can use a YIELD instruction to indicate to the PE that it is performing a task, for example a spin-lock, that could be swapped out to improve overall system performance. The PE can use this hint to suspend and resume multiple software threads if it supports the capability.

    - 참고 : https://developer.arm.com/documentation/ddi0596/2020-12/Base-Instructions/YIELD--YIELD-

     

     

    - Coupled idle [ 참고1 참고2 ]

    : SMP 환경에서 CPU`s 들이 개별적으로 `Power-off`가 불가능한 경우들이 있다. 예를 들어, `엔비디아 테그라 2`는 0번 CPU가 부트 시퀀스와 종료 시퀀스를 책임진다. 그런데 이 상황에서, 0번 CPU가 IDLE 상태가 됬다고 치자. 그런데, 유저가 이 시점에 시스템을 파워 OFF 시키는 명령어를 입력했다. 이럴 경우, 누가 시스템을 종료시켜야 할까? 또, L2 캐쉬 및 인터럽트 컨트롤러는 2개의 CPU 코어가 공유하게 된다. CPU[0]과 CPU[1]이 하나의 클러스터로 묶여있고, 인터럽트 컨트롤러 및 L2 캐쉬를 공유한다고 치자. 그런데, 여기서 갑자기 CPU[1]이 `IDLE 상태(C3)` 되었다. 갑자기 `deeper-sleep` 및 `Power-off`가 되버리면, 다른 CPU[0]과 공유하고 있는 L2 캐쉬 및 인터럽트 컨트롤러에 영향을 미치게 된다. 이렇게 파워 상태 관련 강하게 의존하는 관계를 `coupled power state`라고 한다.

Designed by Tistory.