-
[리눅스 커널] Timer - Broadcast timerLinux/kernel 2023. 9. 17. 15:37
글의 참고
- https://lwn.net/Articles/574962/
- https://lwn.net/Articles/576925/
- https://0xax.gitbooks.io/linux-insides/content/Timers/linux-timers-3.html
- https://blog.csdn.net/dachai/article/details/89876560
- http://www.wowotech.net/timer_subsystem/tick-broadcast-framework.html
- http://www.wowotech.net/timer_subsystem/clock-event.html
- https://blog.csdn.net/roland_sun/article/details/106576101
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Overview
: 리눅스 커널에는 CPU 코어 파워 매니지먼트를 담당하는 `cpuidle framework`가 존재한다. CPU 코어는 실행할 프로세스가 없으면, `idle process`를 실행해서 CPU를 IDLE 상태로 진입시킨다. 그런데, 코어의 IDLE 상태는 여러 개가 존재한다. 대게 `shallow-sleep(C1)`부터 `deep-sleep(C3)` 까지 다양하게 존재한다. 여기서 코어가 강한 IDLE 상태로 진입하면, 로컬 타이머가 Off 된다. 로컬 타이머가 Off 되면, 모든 소프트에어 타이머들 또한 동작할 수 가 없게 된다. 그러면, 잠자고 있는 코어를 wake-up 시킬 수 가 없다. 이 문제를 해결하기 위해 하드웨어적으로 등장한 타이머가 `시스템 타이머`다. 이 타이머는 C3 에서 Off 되지 파워 도메인에 속한다(Always-On 파워 도메인). 그러나, 만약에 시스템 타이머가 없다면 ? per-CPU 타이머만 존재한다면 ? 이걸 위해서 소프트웨어적인 테크닉이 등장했다. 그게 바로, `tick broadcast framework(kernel/time/tick-broadcast.c)`를 도입했다[참고1 참고2 참고3].
: 모든 상황에서 broadcast timer service 가 활성화되어야 하는 것은 아니다. 브로드 캐스트 서비스는 CPUidle 상태에서 필요하기 때문에, IDLE 로 진입할 때, On 했다가 나올 때, Off 한다.
- Working principle
: `ARM 멀티 프로세서` 아키텍처에서 CPU 코어들이 IDLE 상태에 들어가면, 기본적으로 `local timer`들 또한 같이 멈춘다. 그렇게 되면, 해당 CPU 코어에 등록된 소프트웨어 타이머들은 더 이상 동작을 수행할 수 없게된다. 그런데, 이 소프트웨어 타이머가 기능적으로 계속 상태를 유지하기 위해서 2가지 방법이 존재한다.
1. 시스템 레벨 HW 타이머를 사용한다. 일반적으로, 시스템 레벨 HW 타이머는 모든 `CPUidle event`에 독립적이다. 즉, 영향을 받지 않는다. 그리고, 시스템 SLEEP 상태에서도 `always-on` 동작을 수행하기 때문에 시스템 SLEEP에도 영향을 받지 않는다. 시스템 HW 타이머는 특정 CPU에 속해있지 않다. 만약에 GIC를 사용중이라면, CPU 코어를 wake-up 시키기 위해 SoC 타이머 인터럽트는 SPI 타입의 인터럽트가 되고, 로컬 타이머 인터럽트는 PPI가 된다.
2. 만약, 시스템 레벨 HW 타이머가 없다면, 특정 CPU 코어의 로컬 타이머를 `wakeup source`로 설정할 수 있다. 설정된 CPU 코어는 다른 IDLE 상태의 CPU 코어를 깨울 때, `IPI`를 통해서 깨우게 된다. 그러나, 이 방식의 최대 단점은 설정된 CPU 코어는 IDLE 상태로 진입하지 못한다는 점이다. 왜냐면, 다른 CPU 코어를 깨워야 하는 임무가 있기 때문이다.: 먼저 `1`번 케이스를 살펴보자. 시스템에 `ARM generic timer`와 `시스템 글로벌 HW 타이머`가 있다고 가정한다. 시스템이 초기화되는 시점에 `per-CPU 로컬 타이머`는 각 코어의 `per-CPU tick device`로 등록된다. 그리고, `시스템 글로벌 HW 타이머`에 대응되는 `clock event device`는 `broadcast tick device`로 선택된다.
: CPU 코어가 IDLE 상태로 진입할 때, `CPUIDLE_FLAG_TIMER_STOP` 플래그가 설정되어 있으면, CPU 코어는 `tick broadcast framework`에게 `CLOCK_EVT_NOTIFIY_BROADCAST_ENTER` 메세지를 보낸다. 이 뜻은 `로컬 타이머를 Off 하고, broadcast tick을 사용한다`라는 뜻으로 이해할 수 있다.
: 그렇다면, 시스템 레벨 HW 타이머가 존재하는 구조에서 브로드 캐스트 타이머 인터럽트(틱)가 발생하면, 타이머 인터럽트를 어떻게 처리할까? 먼저 CPU 코어는 IDLE 상태로 진입할 때, 로컬 타이머를 중지시킨다. 이 때, `A 초` 뒤에 wake-up 시켜달라는 요청을 한다. 이 `A 초`는 CPU 코어에 등록된 타이머 핸들러 중에서 현재 시간을 기준으로 가장 빨리 발생할 타이머 인터럽트를 선택하게 된다. 다른 CPU 코어들도 마찬가지다. IDLE 상태로 진입하기 직전에 브로드 캐스트 타이머에게 `B 초`뒤에 wakeup 시켜줘, `C 초`뒤에 wakeup 시켜줘 와 같은 요청을 하게된다. 그런데, 시스템 레벨 HW 타이머, 즉, 브로드 캐스트 타이머는 SoC에 하나만 존재한다. 그래서, 브로드 캐스트 타이머는 다음 타이머 인터럽트를 설정하는데 몇 가지 요소를 고려해야 한다.
1. 다음 브로드 캐스트 타이머 인터럽트가 발생하는 시간
2. CPU 코어가 타이머 인터럽트를 요청한 시간: 현재 시간이 x 초이고, 다음 타이머 이벤트가 발생하는 시점이 x+5 초라고 가정하자. 이 요청을 한 코어를 `A-코어` 라고 하자. 이 때, `B-코어`가 IDLE 상태로 진입하면서 브로드 캐스터 타이머에게 `x+7` 초 뒤에 wake-up 시켜달라는 요청을 했다. 브로드 캐스트 타이머는 현재 시점(`x 초`)을 기준으로 미래에 가장 빨리 발생시킬 수 있는 타이머 인터럽트를 설정하기 때문에, `x+7` 요청은 폐기된다. 왜냐면, 다음 실행 될 타이머 인터럽트는 `x+5`초 이기 때문에, `x+7`초 보다 2초 빠르기 때문이다. 그런데, `C-코어`가 등장해서 `x+3`초를 요청했다. 그러면, 브로드 캐스트 타이머는 다음에 발생할 타이머 인터럽트를 `x+3`초로 교체한다. 이 때, 주의할 점은 브로드 캐스트 타이머가 시스템 전역 타이머이기 때문에, 어떤 코어에게 인터럽트 해야할 지 알고 있어야 한다. 그러므로, IRQ Affinity를 설정하고 시스템 레벨 타이머에서 다른데를 거치지 않고 다이렉트로 `C-코어`에 타이머 인터럽트를 발생시켜야 한다.
http://www.wowotech.net/timer_subsystem/tick-broadcast-framework.html: 그런데, 사실 `broadcast timer`는 코어들의 타이머 요청을 폐기하지 않는다. `broadcast timer`는 타이머 인터럽트를 발생시킨 뒤에, 다시 새로운 타이머 인터럽트를 선택해야 한다. 이 때, 이전에 요청했던 타이머 인터럽트중에 타임아웃이 제일 적게 남은 타이머 인터럽트를 설정하게 된다.
: 그렇면, 시스템이 브로드 캐스트 타이머 인터럽트를 받으면 이걸 어떻게 처리할까? 2가지 작업을 수행한다.
1. 모든 CPU 코어들이 요청한 타이머 인터럽트 요청, 즉, `all CPU timer list`를 스캔해서 현재 시점을 기준으로 expiration 이 가장 적게 남은 타이머 서비스를 시스템 전역 HW 타이머(브로드 캐스트 타이머)에 설정한다.
2. 브로드 캐스트 타이머는 현재 자신에게 설정된 expiration 이 가장 적게 남은 CPU 만 wake-up 시킨다. 그리고, 웨이크-업된 CPU 는 `all CPU timer list`를 다시 탐색해서 현재 시간을 기준으로 expiration 이 지난 모든 CPU`s 들을 wake-up 시킨다.
- 즉, 2개의 단계가 있다. 브로드 캐스트 타이머가 먼저 CPU 하나를 깨운다. 그리고, 해당 CPU가 나머지 CPU`s 들을 깨운다.- What is always-on per-CPU timer ? [참고1]
: 이미 예전부터 브로드 캐스트 타이머를 통해 wake-up 하는 구조가 아닌, per-CPU 타이머 기반의 wake-up 구조는 많이 언급이 되었다. 성능 위주의 x86 진영에서는 이런 구조를 2000년 후반부터 언급하고 있었다. HPET 가 브로드 캐스트 타이머로 동작하며, C2 이상의 강력한 C-STATE 에서는 모든 per-CPU 타이머가 STOP 되었다.
https://elinux.org/images/e/ee/Jacob-Pan-x86MID-elc2010.pdf: x86 MID는 `x86 Mobile Internet Device` 의 약자로 인텔이 모바일 인터넷 산업 구조에 맞춘 아키텍처를 의미한다[참고1]. 역시나, 망했지만 그래도 ARM 과의 대결에서 전력 소비 측면에서 밀리기 때문에 성능을 우위에 두기 위해 만든 아키텍처 였지만, 결과는 참담했다.
https://elinux.org/images/e/ee/Jacob-Pan-x86MID-elc2010.pdf- What is system one-shot mode ?
: `system one-shot mode` 라는 무슨뜻일까? 일단, 시스템이라는 말은 local 보다는 global 에 가까운 뉘앙스다. 그렇다면, 시스템에 external timer(브로드 캐스트 타이머) 가 있다고 가정하자. 이 타이머가 one-shot mode 일 때, 시스템이 one-shot mode 되었다고 말할 수 있을까? 맞다. 리눅스에서는 `system timer mode` 를 `broadcast timer mode` 와 동일하게 언급하고 있다.
: 또 하나의 궁금증이 있다. 시스템에 CPU 가 4개 라고 가정하자. 브로드 캐스트 타이머의 등장 배경은 모든 per-CPU 가 C3 상태에서 power-off 될 때도, 계속해서 타이머 서비스를 제공하기 위해서 등장했다. 그렇다면, 4개의 CPU 중에서 1개만 IDLE 이고, 3개는 NORMAL 이라면 어떨까? 이 때도 브로드 캐스트 타이머를 사용해야 할까? 3개의 타이머가 나머지 1개를 IPI 로 깨워주는 것은 어떨까? 리눅스에서는 CPU 가 IDLE 상태로 진입하려고 할 때, 브로드 캐스트 타이머를 one-shot 모드로 전환하고 들어간다. 이 때, 다른 CPU 들이 어떤 상태인지는 고려하지 않는다. 즉, 자기 혼자만 IDLE 상태로 들어가더라도 브로드 캐스트 타이머 사용한다.
- When to use the periodic broadcast timer ?
: 브로드 캐스트 타이머의 등장 배경은 모든 per-CPU 가 C3 상태에서 power-off 될 때도, 계속해서 타이머 서비스를 제공하기 위해서 등장했다. 그런데, 모든 CPU 가 NORMAL 상태인데도 브로드 캐스트 타이머를 사용하는 경우가 있다(브로드 캐스트 타이머는 per-CPU 타이머들이 존재하고, 모든 CPU가 NORMAL 이면, 기본적으로 power-off 된다). 심지어, periodic mode 로 말이다. 이런 경우는 시스템에 타이머가 딱 한개만 존재하는 경우로 볼 수 있다. SoC 관점에서 이 타이머는 per-CPU 타이머는 아니고 시스템 글로벌 타이머일 확률이 높다. 이 타이머가 만약 period mode 만 지원한다면, `high-res, one-shot, dyntick` 은 사용할 수 없다.
- What is difference between per-CPU timer and broadcast timer ?
: per-CPU 타이머와 브로드 캐스트 타이머를 선별 및 구분하는 방법은 다음과 같다.
per-CPU timer (tick_check_replacement) broadcast timer (tick_check_broadcast_device) 필수 조건 1. 반드시 타이머와 CPU 코어가 매핑되어야 한다. 즉, 한 프로세서내에 있는 타이머와 CPU 코어가 매핑되어야 한다. 예를 들어, TIMER[1]이 CPU[2]에 매핑되는 것은 안된다. 1. NO DUMMY
2. NO C3STOP
3. NO PERCPU우대 사항 1. one-shot
2. rating1. one-shot
2. rating: per-CPU 타이머가 되기 위한 조건은 명확하다. 특정 CPU 코어는 같은 프로세서안에 있는 타이머만을 원하기 때문에, `best-suitable target` 을 찾기가 쉽다. 그러나, 브로드 캐스트 타이머는 플래그를 통해 선별된다. DUMMY | C3STOP | PERCPU 플래그가 있는 타이머는 브로드 캐스트 타이머가 될 수 없다. 그런데, 사실 ONESHOT, PERIOD, DYNIRQ 를 제외하고 다른 플래그들은 거의 사용되지 않는다. 이 말은 생각보다 쉽게 per-CPU 타이머가 브로드 캐스트 타이머가 될 수 있다는 것을 암시한다. 예를 들어, `arm arch timer` 아키텍처에서 per-CPU 타이머는 `C3STOP` 플래그만 설정된다. 이럴 경우, CPU와 타이머를 매칭하는 과정에서 2개가 같은 프로세서안에 있지 않을 경우, 브로드 캐스트 타이머가 될 수도 있다.
: 참고로, per-CPU 타이머를 선별하는 과정(tick_chekc_replacement)에서 코드를 실행중 인 CPU 코어와 타이머가 같은 프로세서안에 있지 않으면, 곧 바로 브로드 캐스트 타이머 검사(tick_install_broadcast_device)를 받게 된다.
- Data structure
: 리눅스 커널에서는 `broadcast tick device` 또한 `tick device` 종류 중 하나다. 대신 브로드 캐스트 틱 디바이스는 시스템에 한 개만 존재 해야한다. 그래서 `system one-shot or periodic` 이라고 하면, 브로드 캐스트 타이머의 동작 모드 라고 생각해도 된다.
//kernel/time/tick-broadcast.c - v6.5 static struct tick_device tick_broadcast_device;
: 시스템이 초기화되는 시점에 다양한 `clock event devices`들이 시스템에 등록된다. `tick broadcast framework`는 시스템에 등록된 `clock event devices` 들 중에서 적합한 디바이스를 선택해서 `tick broadcast device`로 지정한다.
: 시스템이 `periodic mode`로 동작중일 경우(각 코어의 로컬 타이머들 또한 `periodic mode` 로 동작 중 일 경우), 각 코어들이 `tick broadcast framework` 에게 타이머 서비스를 요청하면, 해당 정보들을 아래의 변수들에 저장한다.
// kernel/time/tick-broadcast.c - v6.5 static cpumask_var_t tick_broadcast_mask __cpumask_var_read_mostly; static cpumask_var_t tick_broadcast_on __cpumask_var_read_mostly; static cpumask_var_t tmpmask __cpumask_var_read_mostly;
: `tick_broadcast_mask`는 코어가 `tick broadcast service`가 필요하면, 대응되는 비트가 `1`이 된다. 예를 들어, `0b0...0111` 일 경우, `tick broadcast framework`는 코어[0|1|2] 에게 타이머 인터럽트를 발생시킨다. 이 때, 타이머 인터럽트를 받은 코어들은 wake-up 되고, 타이머 관련 작업을 처리하게 된다. 주의할 점은 tick_broadcast_mask 는 periodic mode 에서 broadcast 서비스가 필요한 CPU 들을 의미한다. 이 말은, tick_broadcast_mask 는 한 번 SET 되면, periodic mode 가 one-shot 으로 바뀌거나 broadcast 요청을 취소하지 않는 이상, 주기적으로 타이머 인터럽트를 받게된다.
: `tick_broadcast_on`는 `tick_broadcast_mask`와 짝을 이루어 다닌다. 그러나, tick_broadcast_on 은 이름과 맞지 않게 거의 사용되지 않는다.
코어가 IDLE 상태로 진입할 때, 로컬 타이머를 Off 하면, 대응되는 비트가 `1`이 된다. `tmpmask`는 뒤에서 다시 다룬다.
: `CONFIG_TICK_ONESHOT` 컨피그가 설정될 경우, 추가적으로 `one-shot mode` 관련 cpumask 들이 생성된다.
#ifdef CONFIG_TICK_ONESHOT static cpumask_var_t tick_broadcast_oneshot_mask __cpumask_var_read_mostly; static cpumask_var_t tick_broadcast_pending_mask __cpumask_var_read_mostly; static cpumask_var_t tick_broadcast_force_mask __cpumask_var_read_mostly; .... #endif
: `tick_broadcast_oneshot_mask`는 이 mask 가 필요한 이유는 IDLE CPU 와 NORMAL CPU 를 나누기 위함이다. 기본적으로, one-shot 브로드 캐스트 타이머 서비스는 CPU 가 IDLE 상태일 때만 제공되는 서비스다. 이 mask 비트가 SET 되어있다는 것은 해당 비트의 CPU 가 IDLE 상태이며, 브로드 캐스트 타이머를 받을 자격이 있다는 것을 의미한다. 그렇다면, 이 mask 의 비트만 SET 되어있으면, IDLE 상태에서 브로드 캐스트 타이머 서비스는 받는 것일까? 당연히 아니다. 브로드 캐스트 타이머가 필요함을 알리기 위해서는 tick_broadcast_pending_mask 가 사용된다. `tick_broadcast_pending_mask` / `tick_broadcast_force_mask` 는 뒤에서 다시 다룬다. 참고로, CPU 가 IDLE 상태로 진입과정에서 자동으로 tick_broadcast_oneshot_mask 의 CPU 비트가 SET 된다.
: 강제로 `per-pcu tick device`가 아닌, `broadcast tick device`를 사용하도록 할 수 있다. 그러나, 이 변수는 주로 x86에서만 사용되기 때문에 여기서는 자세히 다루지 않는다.
// kernel/time/tick-broadcast.c - v6.5 static int tick_broadcast_forced
- Broadcast timer handler
: 브로드 캐스트 타이머 핸들러는 2개가 존재한다. 당연히 periodic 과 one-shot 으로 나눠볼 수 있다.
Broadcast timer handler Periodic tick tick_handle_periodic_broadcast Dynamic tick(one-shot) tick_handle_oneshot_broadcast - How to check whether new clock event device is broadcast timer or not ?
: 새로운 `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; /* * Replace the eventually existing device by the new * device. If the current device is the broadcast device, do * not give it back to the clockevents layer ! */ if (tick_is_broadcast_device(curdev)) { // --- 2 clockevents_shutdown(curdev); curdev = NULL; } clockevents_exchange_device(curdev, newdev); tick_setup_device(td, newdev, cpu, cpumask_of(cpu)); // --- 3 if (newdev->features & CLOCK_EVT_FEAT_ONESHOT) tick_oneshot_notify(); return; out_bc: /* * Can the new device be used as a broadcast device ? */ tick_install_broadcast_device(newdev, cpu); // --- 4 }
1. `tick_check_replacement` 함수는 새로 등록된 타이머가 기존 타이머를 대체할 수 있는지를 판단한다. 만약, 반환값이 `true`면, 현재 CPU의 로컬 타이머를 새롭게 등록된 타이머로 교체한다. 만약, `false`를 반환하면 브로드 캐스트 타이머로 사용되도록 한다. 더 자세한 내용을 알고 싶다면, 이 글을 참고하자.
2. 내 개인적으로 생각으로 이 케이스가 발생하는 경우를 찾기란 쉽지 않아보인다. 왜냐면, per-CPU 타이머를 결정하는 패스와 브로드 캐스트 타이머를 결정하는 패스가 명확하게 나뉘기 때문이다. 새로운 clock event device 가 커널에 등록되려면, 반드시 tick_check_new_device 함수가 호출되어야 한다. 그런데, 이 함수가 호출되기 전에 항상 새로운 clock event device 를 `clockevent_devices`에 연결한다. clockevent_devices 는 커널에 등록된 디바이스를 저장하는 연결 리스트다. 즉, 새로운 clock event device 가 tick_check_new_deivce 함수에 전달되면, 무조건 커널에 등록된다고 전제한다.
: 예를 들어, A 디바이스가 per-CPU 타이머로 매핑이 되었다고 치자. 이 상황에서 A 디바이스가 브로드 캐스트 타이머까지 될 수 있을까? per-CPU 타이머 자격을 박탈당해서 clockevents_released 연결되면 모를까, A 디바이스가 다시 매칭 프로세스를 할 일은 없어보인다.
: 그렇다면, 이런 케이스가 존재한다고 가정하고 조건문안에 코드들이 합당한지를 체크해보자. per-CPU 타이머와 브로드 캐스트 타이머 역할을 동시에 할 수 있는 clock event device 가 있다고 가정했을 때, 조건문안에 코드는 합당할까? 코드를 다이어그램으로 표현하면 다음과 같다.
: tick_is_broadcast_device 함수가 참이라는 것은 curdev 가 per-CPU 타이머 자격으로 합당하지 못하다는 것을 의미한다. tick_broadcast_device.evtdev 와 curdev 가 동일한 디바이스를 참조하는데, curdev 가 newdev 보다 per-CPU 타이머 자격이 안된다?
: 그런데, 브로드 캐스트 타이머를 왜 shutdown 시킬까? 브로드 캐스트 타이머가 로컬 타이머가 아닌게 판명난 이상, 현재 CPU가 IDLE 상태가 아니라면, 동작 시킬 필요가 없기 때문이다. 일반적으로, 브로드 캐스트 타이머는 CPU 가 IDLE 상태가 아니라면, 필요없다.
: 그렇다면, 왜 curdev 는 리매칭(clockevents_released) 에 포함시키지 않을까? 다른 말로하면, 왜 curdev 에 NULL을 대입할까? 만약에, NULL 을 대입하지 않고, clockevents_released 에 연결되었다고 해보자. 이 때, tick_broadcast_devce.evtdev 도 curdev 와 동일한 디바이스를 참조하고 있다.
clock event device 들은 소프트웨어적인 clock event device 와 물리적인 타이머 하드웨어가 정확하게 매칭 되지 않은것이다.
`tick_is_broadcast_device` 함수는 인자로 전달된 디바이스가 브로드 캐스트 타이머인지를 알려준다. 그런데, 이 함수까지 왔다는 건 새로 등록된 디바이스가 로컬 타이머로 설정하기에 적합한 디바이스라는 뜻이다. 그리고, `clockevents_exchange_device` 함수를 통해 기존 타이머를 새로운 타이머로 교체한다. `clockevents_shutdown` 함수를 굳이 호출하는 이유는 `clockevents_exchange_device` 함수에서 기존 타이머를 release를 하지 않기 때문이다(release 작업을 `clockevents_shutdown` 함수에서 수행한다).
// kernel/time/tick-broadcast.c - v6.5 /* * Check, if the device is the broadcast device */ int tick_is_broadcast_device(struct clock_event_device *dev) { return (dev && tick_broadcast_device.evtdev == dev); }
: 브로드 캐스트 타이머는 `재활용`을 하지 않는다. 이게 무슨뜻일까? `clockevents_exchange_device` 함수가 호출되면, curdev는 clockevents의 리매칭 과정에 참여하게 된다(`clockevents_released`). 그런데, 리매칭에 참여할 수 있는 조건은 물리적으로 per-CPU 타이머여야 한다. 그랫, clockevents_exchange_device 함수에 인자로 전달되는 디바이스들이 반드시 per-CPU 타이머여야 한다.
: 그리고, per-CPU 타이머가 최소 한 개라도 등록되는 상황이 되면, 브로드 캐스트 타이머는 필요가 없어진다. 왜냐면, 브로드 캐스트 타이머는 cpuidle 에서만 필요하기 때문이다. 그렇므로, 종료를 시킨다(`clockevents_shutdown`).
- What is broadcast IPI ?
: 브로드 캐스트 타이머 메커니즘은 브로드 캐스트 타이머로 지정된 디바이스가 일일히 모든 per-CPU 타이머를 wake-up 시키는게 아니다. 브로드 캐스트 타이머 wake-up 절차는 2단계로 나눌 수 있다.
1. 브로드 캐스트 타이머 인터럽트를 받은 per-CPU 타이머(broadcast target)가 wake-up 한다.
2. 타이머 리스트를 탐색해서 현재 시점을 기준으로 타이머 서비스가 필요한 CPU(broadcast IPI target)를 IPI 로 wake-up 시킨다.: 모든 per-CPU 타이머들은 반드시 broadcast IPI 기능을 장착(armed)하고 있어야 한다. 왜냐면, 언제 어떤 CPU 가 브로드 캐스트 타이머 서비스를 받을 지 알 수 없기 때문이다. per-CPU 타이머들에게 broadcast IPI 기능을 장착시키는 함수는 tick_device_users_broadcast 다[참고1]. 이 함수의 반환값이 `0`이 아니면, 현재 브로드 캐스트 타이머 서비스가 필요하다는 것을 의미한다.
// kernel/time/tick-broadcast.c - v6.5 int tick_device_uses_broadcast(struct clock_event_device *dev, int cpu) { struct clock_event_device *bc = tick_broadcast_device.evtdev; unsigned long flags; int ret = 0; raw_spin_lock_irqsave(&tick_broadcast_lock, flags); if (!tick_device_is_functional(dev)) { // --- 1 dev->event_handler = tick_handle_periodic; tick_device_setup_broadcast_func(dev); cpumask_set_cpu(cpu, tick_broadcast_mask); if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC) tick_broadcast_start_periodic(bc); else tick_broadcast_setup_oneshot(bc, false); ret = 1; } else { // --- 2 if (!(dev->features & CLOCK_EVT_FEAT_C3STOP)) // --- 3 cpumask_clear_cpu(cpu, tick_broadcast_mask); else tick_device_setup_broadcast_func(dev); if (!cpumask_test_cpu(cpu, tick_broadcast_on)) // --- 4 cpumask_clear_cpu(cpu, tick_broadcast_mask); switch (tick_broadcast_device.mode) { case TICKDEV_MODE_ONESHOT: tick_broadcast_clear_oneshot(cpu); // --- 5 ret = 0; break; case TICKDEV_MODE_PERIODIC: if (cpumask_empty(tick_broadcast_mask) && bc) // --- 6 clockevents_shutdown(bc); if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER)) // --- 7 ret = cpumask_test_cpu(cpu, tick_broadcast_mask); break; default: break; } } raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags); return ret; }
1. 만약, 인자로 전달된 clock event device가 DUMMY TIMER 라면, 브로드 캐스트 타이머에 의해서 동작하게 된다(이 내용은 이 글 맨 아래 `DUMMY TIMER` 섹션을 참고하자). DUMMY TIMER는 타이머 핸들러가 무조건 `tick_handle_periodic` 으로 고정된다. 그리고, 브로드 캐스트 타이머를 통해 타이머 서비스를 제공받아야 하므로, 현재 브로드 캐스트 동작 상태에 따라 어떤 서비스를 제공할지가 결정된다. 물리적으로 로컬 타이머가 존재할 때는, 로컬 타이머에 상태에 따라 브로드 캐스트 타이머의 동작 모드가 변경됬지만, DUMMY TIMER는 브로드 캐스트 타이머가 지원해주는 기능만을 사용할 수 있기 때문에, 상황이 역전된다.
2. 로컬 타이머가 DUMMY 가 아닐 때, 아래의 내용들이 수행된다.
- 먼저 CPU 로컬 타이머에 C3STOP 플래그가 SET 되어 있지 않으면, 이 CPU 한테는 브로드 캐스트 서비스를 제공할 필요가 없다. 왜? 로컬 타이머가 C3 에서도 power-off 가 안되니, CPU 가 IDLE 상태여도 로컬 타이머를 통해 타이머 서비스를 받을 수 있다. 그러므로, 브로드 캐스트 타이머가 필요없으니, tick_broadcast_mask 변수에서 현재 CPU 비트를 CLEAR 한다. 결국, 해당 CPU 는 IDLE 상태에 있어도, 브로드 캐스트 타이머가 아닌, 로컬 타이머를 통해 타이머 서비스를 제공받게 된다.
- 만약, per-CPU 타이머가 C3STOP 플래그가 SET 되어 있다면, IPI 를 broadcast 기능을 장착해야 한다. 오해하면 안된다. per-CPU 타이머가 브로드 캐스트 타이머가 되는게 아니다. `struct clock_event_device.broadcast` 필드가 있다. 이 필드에 `tick_broadcast` 핸들러를 등록하는 것이다. 왜냐면, 브로드 캐스트 타이머는 하나의 CPU 만 wake-up 시키고, 그 이후에 웨이크업된 CPU 가 나머지 CPU 들을 wake-up 시키기 위해서는 모든 CPU 에게 broadcast 기능을 장착시켜줘야 한다. 그런데, C3STOP 플래그가 CLEAR 되어있는 로컬 타이머는 broadcast 기능을 장착시키지 않는 이유가 뭘까? C3STOP 플래그 CLEAR 되어있는 로컬 타이머는 브로드 캐스트 타이머가 될 확률이 높다. 그러므로, broadcast 기능을 장착하지 않는것이다.
5. 브로드 캐스트 타이머가 one-shot 모드 라는 것이 무엇을 의미할까? 어떤 CPU 인지는 몰라도, IDLE 상태에 있다는 것을 의미한다.
브로드 캐스트 타이머를 공부할 때, 알아야 할 원칙이 있다. `브로드 캐스트 타이머를 one-shot 모드로 동작시킬 것이라면, 반드시 현재 CPU 비트를 tick_broadcast_oneshot_mask 에서 CLEAR 해야 한다` 는 것이다. 왜냐면, 브로드 캐스트 타이머는 일반적으로 CPU 가 IDLE 상태에 있을 때만, 동작하도록 설계된 기능이다. 그래서, oneshot mode broadcast bit 는 idle 로 진입할 때, set 되고 exit 할 때 clear 된다. 그런데, 현재 CPU 가 IDLE 상태가 아닌데, broadcast bit 가 SET 되어있다면, 이건 문제가 있는 것이다. 그러므로, 현재 CPU 가 running 상태인데, broadcast active bit 가 SET 되어있다면, 반드시 CLEAR 해주자[참고1 참고2]
The oneshot mode broadcast bit for the other cpus is sticky and gets only cleared when those cpus exit idle. If a cpu was not idle while the bit got set in consequence the bit prevents that the broadcast device is armed on behalf of that cpu when it enters idle for the first time after it switched to oneshot mode.
....
The solution is simply to clear the broadcast active bit unconditionally when a cpu switches to oneshot mode after the first cpu switched the broadcast device over. It's not idle at that point otherwise it would not be executing that code.
- 참고 : https://github.com/torvalds/linux/commit/07f4beb0b5bbfaf36a64aa00d59e670ec578a95a
6. 브로드 캐스트 타이머가 period mode 라는 것이 무엇을 의미할까? 2 가지 경우가 있다.
1. 할 일이 없는 경우(DUMMY TIMER가 없는 경우).
2. DUMMY TIMER 때문에 브로드 캐스트 타이머가 열일 해야하는 경우(DUMMY TIMER가 있 경우).
7. 이 조건은 브로드 캐스트 타이머가 per-CPU 타이머 아니라면, 즉, 시스템 글로벌 타이머라면, 코드를 실행중 인 CPU가 요청한 브로드 캐스트 서비스가 있는지 체크한다. 사실, 이 코드까지 왔다는 건 일단 period 브로드 캐스트 요청이 있다는 말이다. 그런데, 그 요청들중에 코드를 실행중 인 CPU에 요청이 들어있는지를 판단한다. 이걸 왜 판단할까? 이 함수가 끝나고 나면, per-CPU 타이머의 동작 모드와 타이머 핸들러를 교체하는 코드가 나온다. 그런데, 코드를 실행중 인 CPU가 곧 있다가 브로드 캐스트 서비스를 받을 거라면, 조금 뒤로 미루는 것이 좋다. 만약, 교체하는 도중에 브로드 캐스트 서비스가 도착하면 낭패이기 때문이다.: `tick_device_uses_broadcast` 함수는 커널에 새로 등록된 로커 타이머에 broadcast 기능을 추가해주는 함수라고 했다. 실제로 `broadcast` 기능을 추가해주는 함수는 `tick_device_setup_broadcast_func` 함수다. 이 함수를 보면, dev->broadcast = tick_broadcast 가 있는데, 이게 바로 broadcast 기능(IPI)을 추가해주는 코드라고 볼 수 있다.
// kernel/time/tick-broadcast.c - v6.5 static void tick_device_setup_broadcast_func(struct clock_event_device *dev) { if (!dev->broadcast) dev->broadcast = tick_broadcast; if (!dev->broadcast) { pr_warn_once("%s depends on broadcast, but no broadcast function available\n", dev->name); dev->broadcast = err_broadcast; } }
: broadcast 기능이 IPI 를 기반으로 하다보니, 아키텍처마다 구현 방식이 다르다. arm64 같은 경우는, 아래와 같이 구현되어 있다.
// include/linux/clockchips.h # ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST # ifdef CONFIG_ARCH_HAS_TICK_BROADCAST extern void tick_broadcast(const struct cpumask *mask); # else # define tick_broadcast NULL # endif extern int tick_receive_broadcast(void); # endif // arch/arm64/kernel/smp.c - v6.5 #ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST void tick_broadcast(const struct cpumask *mask) { smp_cross_call(mask, IPI_TIMER); } #endif
: smp_cross_call 함수를 통해 `IPI_TIMER`를 전달할 경우, arm64 에서는 인터럽트 핸들러로 `do_handle_IPI` 함수가 동작한다. tick
// arch/arm64/kernel/smp.c - v6.5 /* * Main handler for inter-processor interrupts */ static void do_handle_IPI(int ipinr) { unsigned int cpu = smp_processor_id(); if ((unsigned)ipinr < NR_IPI) trace_ipi_entry(ipi_types[ipinr]); switch (ipinr) { .... #ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST case IPI_TIMER: tick_receive_broadcast(); break; #endif .... default: pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr); break; } if ((unsigned)ipinr < NR_IPI) trace_ipi_exit(ipi_types[ipinr]); }
: broadcast IPI 를 받은 CPU 들은 tick_received_broadcast 함수가 호출된다. broadcast IPI 를 받은 CPU 들은 직접 타이머 핸들러를 호출한다.
// kernel/time/tick-broadcast.c - v6.5 int tick_receive_broadcast(void) { struct tick_device *td = this_cpu_ptr(&tick_cpu_device); struct clock_event_device *evt = td->evtdev; if (!evt) return -ENODEV; if (!evt->event_handler) return -EINVAL; evt->event_handler(evt); return 0; }
- How to register broadcast timer ?
: 앞에서 설명했다시피, 커널에 새로운 `clock event device`가 등록되면, `tick_check_new_device` 함수가 호출되어서 새로 등록된 `clock event device`가 현재 설정된 `clock event device(old)` 를 대체할 수 있는지를 체크한다. 즉, 현재 CPU 코어의 로컬 타이머로 설정된 `clock event device`보다 새로운 `clock event device`가 더 좋은 적합하다고 판단되면, 현재 CPU 코어의 로컬 타이머를 새로운 타이밍 디바이스로 교체한다. 그러나, 새로운 디바이스가 특정 조건을 충족시키지 못하거나, 현재 설정된 타이밍 디바이스보다 좋지 못하다면, `tick_install_broadcast_device` 함수를 호출해서 `tick broadcast device`로 사용될 수 있는지를 판단한다. 이 함수까지 도착했다는 건 새로 등록된 디바이스는 무조건 글로벌 타이머가 될까? 아니다. 로컬 타이머도 이 함수까지 도달할 수 있다.
// kernel/time/tick-broadcast.c - v6.5 void tick_install_broadcast_device(struct clock_event_device *dev, int cpu) { struct clock_event_device *cur = tick_broadcast_device.evtdev; if (tick_set_oneshot_wakeup_device(dev, cpu)) // --- 1 return; if (!tick_check_broadcast_device(cur, dev)) // --- 2 return; if (!try_module_get(dev->owner)) return; clockevents_exchange_device(cur, dev); // --- 3 if (cur) cur->event_handler = clockevents_handle_noop; // --- 4 tick_broadcast_device.evtdev = dev; // --- 5 if (!cpumask_empty(tick_broadcast_mask)) // --- 6 tick_broadcast_start_periodic(dev); if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT)) // --- 7 return; if (tick_broadcast_oneshot_active()) { // --- 8 tick_broadcast_switch_to_oneshot(); return; } /* * Inform all cpus about this. We might be in a situation * where we did not switch to oneshot mode because the per cpu * devices are affected by CLOCK_EVT_FEAT_C3STOP and the lack * of a oneshot capable broadcast device. Without that * notification the systems stays stuck in periodic mode * forever. */ tick_clock_notify(); // --- 9 }
1. `tick_set_oneshot_wakeup_device` 함수는 `per-CPU wake-up timer`를 설정하는 역할을 한다. 일부 SoC 에서는 per-CPU timer 를 소프트웨어적으로 2가지 방법으로 구현한다.
1. C3STOP 플래그 SET 된 per-CPU 타이머
2. C3STOP 플래그 CLEAR 된 per-CPU 타이머: 이건 말 그대로 소프트웨어적인 방법이다. 하드웨어적으로는 per-CPU 타이머들은 모두 동일한 파워 도메인을 사용하기 때문에, C3 상태에서 Off 되는 것이 정상이다. 그런데, 이걸 소프트웨어적으로 C3STOP 플래그가 CLEAR 된 high-rated 타이머들은 C3 에서 Off 시키지 않는 것이다. 이렇게 하는 이유가 뭘까? 일부 SoC 에서는 low-rated 성능을 갖는 브로드 캐스트 타이머보다 C3STOP 플래그가 CLEAR 된 wake-up 타이머를 더 선호하기 때문이다.
: C3STOP 플래그가 SET 된 타이머만 존재할 경우, 브로드 캐스트 타이머에 의존할 수 밖에 없고, IPI 로 인한 성능적인 부분에서 오버헤드도 크다. 그런데, 각 CPU 마다 브로드 캐스트 타이머와는 별개로 `per-CPU wake-up timer` 를 개별적으로 갖게된다면, 브로드 캐스트 타이머에 대한 의존성과 IPI 로 인한 오버헤드를 방지할 수 있다. 이 기능은 각 CPU 로컬 타이머들에게 C3STOP 플래그를 CLEAR 하게 하는데, 이러면 CPU 는 `deep-idle`이 아닌, `shallow-idle` 정도까지만 진입만 하게 된다. 부작용이 있을까? 당연히 소비 전류 문제가 발생할 것이다. 개인적인 생각이지만, 퍼포먼스를 위해 소비 전류를 포기하는 방식이라는 생각이든다[참고1].
// kernel/time/tick-broadcast.c - v6.5 static bool tick_set_oneshot_wakeup_device(struct clock_event_device *newdev, int cpu) { struct clock_event_device *curdev = tick_get_oneshot_wakeup_device(cpu); if (!newdev) goto set_device; if ((newdev->features & CLOCK_EVT_FEAT_DUMMY) || // --- 1 (newdev->features & CLOCK_EVT_FEAT_C3STOP)) return false; if (!(newdev->features & CLOCK_EVT_FEAT_PERCPU) || // --- 2 !(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) return false; if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) // --- 3 return false; if (curdev && newdev->rating <= curdev->rating) // --- 4 return false; if (!try_module_get(newdev->owner)) return false; newdev->event_handler = tick_oneshot_wakeup_handler; set_device: clockevents_exchange_device(curdev, newdev); per_cpu(tick_oneshot_wakeup_device, cpu) = newdev; return true; }
1. `per-CPU wake-up timer`로 설정될 디바이스는 `CLOCK_EVT_FEAT_C3STOP` 플래그를 허용하면 안된다. 이 조건문은 `per-PCU timer`가 `wake-up timer`가 되는 것을 막는다.
2. `per-CPU wake-up timer`로 설정될 디바이스는 `one-shot` & `per-cpu` 디바이스여야 한다.
3. 현재 CPU에 속한 디바이스여야 한다.
4. per-CPU 타이머는 대개 rating(성능) 값이 동일하다. 그러면서, rating 값이 높다. 시스템에 per-CPU 타이머 외에 브로드 캐스트 타이머 또한 존재한다면, 당연히 브로드 캐스트 타이머는 per-CPU 보다 rating 값이 낮다. 결국, 이 조건문은 현재 per-CPU 타이머로 등록된 타이머가 per-CPU 타이머이고, 새로운 들어온 타이머가 브로드 캐스트 타이머일 때, 교체하지 않겠다는 조건문이다.2. `tick_check_broadcast_device` 함수는 새로 등록된 `clock event device`가 `tick boradcast device`가 될 수 있는지를 판단한다[참고1].
// kernel/time/tick-broadcast.c - v6.5 static bool tick_check_broadcast_device(struct clock_event_device *curdev, struct clock_event_device *newdev) { if ((newdev->features & CLOCK_EVT_FEAT_DUMMY) || // --- 1 (newdev->features & CLOCK_EVT_FEAT_PERCPU) || (newdev->features & CLOCK_EVT_FEAT_C3STOP)) return false; if (tick_broadcast_device.mode == TICKDEV_MODE_ONESHOT && // --- 2 !(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) return false; return !curdev || newdev->rating > curdev->rating; // --- 3 }
1. 이 3개의 플래그는 모두 로컬 타이머에만 설정되는 플래그들이다. `tick broadcast device`는 `dummy device`이면 안된다(`CLOCK_EVT_FEAT_DUMMY`). 그리고, CPU 로컬 타이머 여도 안된다(`CLOCK_EVT_FEAT_PERCPU`).
2. 이 조건을 통해 `tick broadcast device`는 일반적으로 `one-shot mode`를 지원해야 한다는 것을 알 수 있다.
3. 앞에 조건을 모두 충족했을 경우, 현재 `tick broadcast device`로 설정된 디바이스가 없으면, 새로운 디바이스가 브로드 캐스트 디바이스가 된다. 혹은 새로운 타이머가 기존에 설정되어 있는 브로드 캐스트 타이머보다 좋은 성능을 가지고 있다면, 교체한다.3. 이 시점까지 오면, 새로운 디바이스는 브로드 캐스트 타이머될 자격을 갖추고 있으며, 기존의 브로드 캐스트 타이머 보다 성능도 좋다는 뜻이다. 그러므로, 이전 브로드 캐스트 타이머는 `clock event device register` 과정에서 리매칭 될 수 있도록 재활용한다.
4. 방금 새로운 per-CPU 타이머를 등록했으므로, 이전 per-CPU 타이머 핸들러를 clockevents_handle_noop 으로 교체한다. 그런데, 좀 이상하다. 그냥, `cur = NULL` 같이 하면 안될까? 바로 위에서, 기존 브로드 캐스트 타이머는 리매칭을 위해 큐에 집어넣었다. 만약에 여기서 cur 에 NULL 을 넣어버리면, 리매칭 큐에 들어가있는 기존 브로드 캐스트 타이머가 NULL 이 되버린다. 즉, 정보가 날아가 버린다. 그러므로, 리매칭을 한다면 NULL 을 넣으면 안된다. 만약에 리매칭을 안할거라면 NULL 을 넣어도 상관없다.
: 만약, `cur = clockevents_handle_noop` 코드가 없다면 어떻게 될까? cur은 이전 broadcast handler 를 계속 참조하고 있기 때문에 브로드 캐스트 타이머가 발생하면, 2번의 브로드 캐스트 타이머 핸들러가 호출될 수 있다. 이런 이슈를 방지하기 위해 아무것도 하지 않는 핸들러를 장착시킨다.(cur 에 NULL 을 넣을 수 없으니, 핸들러라도 교체)
// kernel/time/clockevents.c - v6.5 /* * Noop handler when we shut down an event device */ void clockevents_handle_noop(struct clock_event_device *dev) { }
5. 브로드 캐스트 타이머를 교체한다. 여기서 중요한 건, td->mode 는 변경하지 않는다는 것이다. 왜냐면, 디바이스가 교체되더라도 기존 동작을 계속 유지해야 하기 때문이다.
6. `tick_broadcast_mask`는 어떤 CPU 코어가 `tick broadcast service in periodic mode`를 요청했는지를 알려준다. 그리고, 이 값이 `0`이 아니라는 것은 periodic 브로드 캐스트 요청이 있다는 뜻이다. 만약, 요청이 있다면, 지금 막 설정된 브로드 캐스트 타이머를 `periodic mode`로 동작시켜야 시킨다.
: `tick_broadcast_start_periodic` 함수는 단순히 `tick_setup_periodic` 함수를 호출해주는 역할을 한다.
// kernel/time/tick-broadcast.c - v6.5 static void tick_broadcast_start_periodic(struct clock_event_device *bc) { if (bc) tick_setup_periodic(bc, 1); }
7. 브로드 캐스트 타이머가 one-shot 모드를 하드웨어적으로 지원하는지 검사한다. 여기서 이걸 체크하는 이유는 이 코드 뒤부터는 one-shot 모드 관련 코드만 존재하기 때문이다.
8. 만약 기존 브로드 캐스트 타이머가 one-shot mode 라면, 기존과 호환성을 위해 새로 등록된 타이머 또한 one-shot 으로 동작해야 자연스럽다.
// kernel/time/tick-broadcast.c - v6.5 /* * Check, whether the broadcast device is in one shot mode */ int tick_broadcast_oneshot_active(void) { return tick_broadcast_device.mode == TICKDEV_MODE_ONESHOT; }
: `tick_broadcast_switch_to_oneshot` 함수는 현재 시스템에 설정된 브로드 캐스트 타이머의 모드를 one-shot mode 로 변경한다. 그리고, 브로드 캐스트 타이머의 실제 동작 또한 one-shot 으로 동작하게 한다.
// kernel/time/tick-broadcast.c - v6.5 void tick_broadcast_switch_to_oneshot(void) { struct clock_event_device *bc; enum tick_device_mode oldmode; unsigned long flags; raw_spin_lock_irqsave(&tick_broadcast_lock, flags); oldmode = tick_broadcast_device.mode; tick_broadcast_device.mode = TICKDEV_MODE_ONESHOT; bc = tick_broadcast_device.evtdev; if (bc) tick_broadcast_setup_oneshot(bc, oldmode == TICKDEV_MODE_PERIODIC); raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags); }
: `tick_broadcast_setup_oneshot` 함수는 브로드 캐스트 타이머를 one-shot mode 로 바꾸는 역할을 한다. 브로드 캐스트 타이머의 동작 모드를 바꾸는 함수다보니 모든 CPU 가 이 함수를 반복해서 호출할 수 있다. 그리고, 이 함수를 호출하는 CPU 는 브로드 캐스트 타이머를 one-shot 모드로 바꾸려는 목적이 있다. 그렇다면, 이 함수를 호출하는 케이스는 2가지로 나눌 수 있다.
tick_broadcast_setup_oneshot 함수 호출 여부 의미가 있는지 여부 periodic -> one-shot O O one-shot -> one-shot O X : 이 함수 분석해 보면, `periodic -> one-shot` 으로의 변화만 의미가 있다는 것을 알 수 있다[참고1].
// kernel/time/tick-broadcast.c - v6.5 static void tick_broadcast_setup_oneshot(struct clock_event_device *bc, bool from_periodic) { int cpu = smp_processor_id(); ktime_t nexttick = 0; if (!bc) return; if (bc->event_handler == tick_handle_oneshot_broadcast) { // --- 1 tick_broadcast_clear_oneshot(cpu); // --- 2 return; } bc->event_handler = tick_handle_oneshot_broadcast; // --- 3 bc->next_event = KTIME_MAX; if (from_periodic) { // --- 4 cpumask_copy(tmpmask, tick_broadcast_mask); cpumask_clear_cpu(cpu, tmpmask); cpumask_or(tick_broadcast_oneshot_mask, tick_broadcast_oneshot_mask, tmpmask); nexttick = tick_get_next_period(); // --- 5 tick_broadcast_init_next_event(tmpmask, nexttick); if (clockevent_state_oneshot(bc)) // --- 6 return; } if (!cpumask_empty(tick_broadcast_oneshot_mask)) // --- 7 tick_broadcast_set_event(bc, cpu, nexttick); }
1. 브로드 캐스트 타이머는 시스템 글로벌 자원이기 때문에, 여러 CPU 에서 액세스가 가능하다. 각 CPU 는 IDLE 상태로 진입하려고 할 때, NO_HZ 컨피그가 설정되어 있으면, 브로드 캐스트 타이머를 one-shot mode 로 변경하려고 한다. 그래서 이 함수는 CPU 가 IDLE 로 진입하는 `prepare` 단계에서 호출되는 함수다. 특정 CPU 가 IDLE 로 진입하기 위해 이 함수를 호출했는데, 이미 브로드 캐스트 타이머 핸들러가 one-shot 핸들러라는 것은 이미 브로드 캐스트 타이머가 하드웨어적으로 one-shot mode 라는 소리다. 그러므로, 추가적으로 할 것이 없다. 그런데, CPU 가 이 함수를 실행하고 있다는 것은 아직 IDLE 상태가 아니라는 뜻이기 때문에, 자신을 broadcast active mask 에서 제외한다. IDLE 상태로 진입하면서 자동으로 broadcast active mask 가 SET 된다.
2. 이 코드를 실행됬다는 것은 이미 브로드 캐스트 타이머가 one-shot 인 경우를 의미한다. 이 때는, broadcast active bit 만 CLEAR 해서 IDLE 로 진입할 수 있는 준비만하고, 함수를 종료한다.
3. 이 코드까지 오는 CPU 는 브로드 캐스트 타이머를 `periodic -> one-shot` 으로 설정하는 첫 번째 CPU 다. 그러므로, 브로드 캐스트 타이머 핸들러를 tick_handle_oneshot_broadcast 로 설정하고, next event 를 초기화한다.
4. 이 코드까지 오는 CPU 는 브로드 캐스트 타이머를 `periodic -> one-shot` 으로 설정하는 첫 번째 CPU 다. 나머지 CPU`s 들은 브로드 캐스트 타이머를 one-shot 으로 설정하려고, 이 함수를 호출했지만, 이미 one-shot 이여서 tick_broadcast_clear_oneshot(cpu) 함수만 호출하고 끝이 난다. 이 브로드 캐스트 타이머를 `periodic -> one-shot` 으로 설정하는 첫 번째 CPU 는 여기서 중요한 일을 맡게된다.
브로드 캐스트 타이머는 `periodic -> one-shot` 으로 모드가 전환할 때만 `arm` 될 수 있다(여기서 `arm` 은 `장전하다` 의 의미로 보면 된다. 예를 들어, `next tick` 이 이미 `armed` 라면, `이미 next tick 이 준비(장전)되어 있다` 라고 볼 수 있다). 모드 전환 시 `periodic -> one-shot` 전환 시 고려해야 할 부분이 있다[참고1].
- 브로드 캐스트 타이머가 periodic mode 로 동작하고 있을 때, periodic 타이머 서비스를 기다리는 CPU`s 들이 있을 것이다(tick_broadcast_mask). 그런데, 브로드 캐스트 타이머가 one-shot mode 가 되더라도 계속해서 periodic tick 을 원하는 CPU`s 들에게는 periodic tick 을 제공해야 한다. 그러모르, tick_broadcast_mask(periodic tick) 와 tick_broadcast_oneshot_mask(one-shot tick) 를 OR 연산을 한 값을 기준으로 타이머 서비스를 제공해야 한다. 그런데, 주의할 점이 있다.
이 함수가 실행되었다는 것은 브로드 캐스트 타이머를 one-shot 모드로 변경한다는 것이고, 이 말은 현재 코드를 실행중 인 CPU가 곧 있으면 IDLE 상태로 진입할 것이라는 뜻이다. 그런데, IDLE 상태로 진입하는 시점에 tick_broadcast_oneshot_mask 를 검사한다. 이 mask 는 CPU 들이 IDLE 상태로 진입했는지를 알려주는 mask 다. 근데, IDLE 상태로 진입하는 시점에 해당 CPU 이 mask 에 SET 되어있으면, 로컬 타이머를 shotdown 시키지 않는다. 그러므로, IDLE 로 진입하는 전에 반드시 tick_broadcast_oneshot_mask 에서 자신의 비트를 CLEAR 해놓고 진입해야 한다.
5. 브로드 캐스트 타이머가 `periodic -> one-shot` 으로 전환되더라도, 브로드 캐스트 타이머의 periodic 서비스를 기다리던 CPU`s 들이 있을 것이다. 이 CPU`s 들한테는 계속 periodic 서비스를 제공해줘야 한다. 그러므로, next periodic tick 을 가져온다. 그리고, tmpmask 에 next periodic tick 이 필요한 CPU`s 들로 초기화한다. 그리고, `tick_broadcast_init_next_event` 함수를 통해서 tmpmask 에 SET 된 CPU`s 들에게 next periodic tick 을 설정한다. 이 시점에서 mask 들의 상태는 아래와 같다.
- tick_broadcast_mask : periodic 브로드 캐스트 타이머 서비스가 필요한 CPU`s 들이 SET 되어 있음.
- tmpmask : tick_broadcast_mask 와 동일하나, 자기 자신은 제외. 왜냐면, 이제 periodic 이 아닌, one-shot 서비스가 필요하기 때문에 periodic 제외한다. 즉, 여기서 tmpmask 는 자기 자신을 제외한 periodic 브로드 캐스트 서비스가 필요한 CPU`s 들을 의미한다.
- tick_broadcast_oneshot_mask : tmpmask | tick_broadcast_oneshot_mask
6. 하드웨어 동작 모드가 이미 one-shot 모드라면, 뒤쪽 코드를 실행할 필요가 없으므로, 함수를 종료한다. 근데, 이런 케이스가 있을까? 예를 들어, `from_periodic` 이 true 라는 것은 이전 상태가 periodic 이라는 것이고, 아직 상태를 바꾸지 않았는데 현재 상태가 어떻게 one-shot 일 수 있을까? from_periodic 변수의 값은 `struct clock_event_device` 가 아닌, `struct tick_deivce`를 기반으로 하기 때문이다. tick_device.device_mode 는 소프트웨어 동작 모드다. 즉, 하드웨어와는 무관하다. 예를 들어, 하드웨어적으로 one-shot mode(CLOCK_EVT_STATE_ONESHOT) 여도 소프트웨어적으로TICKDEV_MODE_PERIODIC 가 될 수 있다. 그렇기 때문에, 소프트웨어 동작 모드인 tick_device.device_mode 는 이미, tick_broadcast_switch_to_oneshot 함수에서 one-shot 으로 바꾸기 때문에, 여기서는 하드웨어 동작 모드를 검사 및 변경한다.
7. 위에서 많은 작업들을 했지만, 아직 하드웨어에서 next tick 을 입력하지 않은 상태다. 여기서 실제 next timer event 를 하드웨어 레지스터에 쓴다. tick_broadcast_oneshot_mask 가 empty 하지 않다는 것은 타이머 서비스를 필요로 하는 CPU 가 존재한다는 소리다. 이 시점에서는 브로드 캐스트 타이머에 설정된 next event 가 `tick_get_next_period` 이지만(periodic tick 을 원하는 CPU`s 들 때문에), CPU 가 IDLE 상태로 진입하는 시점에 ___tick_broadcast_oneshot_control 함수가 호출한다. 이 때, `dynamic tick` 기반의 타이머 스케줄링 알고리즘을 통해 next event 가 재설정된다.9. 만약, 시스템에 브로드 캐스트 타이머가 등록되고, one-shot 모드를 지원한다면, 모든 동작가능 CPU들의 `tick_sched.check_clocks` 을 SET 한다. 그런데, 브로드 캐스트 타이머가 아닌, 즉, 개별적인 per-CPU 타이머가 one-shot 모드를 지원할 경우에는, 해당 CPU의 `tick_sched.check_clocks` 만 SET 하게 한다. 이게 무슨 의미일까? 이 글을 참고하자.
// kernel/time/tick-sched.c - v6.5 /* * Async notification about clocksource changes */ void tick_clock_notify(void) { int cpu; for_each_possible_cpu(cpu) set_bit(0, &per_cpu(tick_cpu_sched, cpu).check_clocks); }
: 이제 BSP 로컬 타이머는 초기화가 모두 완료되고, `periodic mod`로 동작중인 상태다. 이와 동시에 BSP 로컬 타이머는 시스템 글로벌 타이머로도 인식된다(여기서 시스템 글로벌 타이머는 브로드 캐스트 타이머를 의미하는게 아니다. jiffies 를 증가시킬 글로벌 타이머를 의미한다).
물론, 로컬 타이머는 기본적으로 `one-shot mode` 또한 지원한다. 그렇다면, 이 시점에 `one-shot mode`로 전환해야 할까? 초기화 과정에서는 `broadcast tick device`가 설정되지 않아 failed 된다. hrtimer 기준으로 타이머 인터럽트가 발생하면, 기본적인 작업을 처리한 후, softirq를 통해서 후반 처리 작업을 한다. 이 때, 타이머 softirq 핸들러가 `hrtimer_run_queues` 핸들러다. 이 함수는 결국 hrtimer 타이머 인터럽트가 발생할 때마다, 호출되서 주기적으로 `one-shot mode`로 전환이 가능한지를 체크한다. 그 함수가 `tick_check_oneshot_change` 다.
// 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)) // --- 초기화 과정에서 통과 return 0; if (ts->nohz_mode != NOHZ_MODE_INACTIVE) // --- 초기화 과정에서 `ts->nohz_mode`는 `NOHZ_MODE_INACTIVE`다. 그러므로, passed return 0; if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available()) // --- 초기화 과정에서 여기서 FAILED return 0; if (!allow_nohz) return 1; tick_nohz_switch_to_nohz(); return 0; }
: 초기화 과정에서 위 함수는 `tick_is_oneshot_available` 함수에서 `0`을 반환한다. 조금 더 자세히 설명하면, `tick_broadcast_oneshot_available` 함수에서 `0`을 반환한다. 왜냐면, `broadcast tick device`가 아직 초기화되지 않았기 때문이다. 그러므로, 이 BSP 로컬 타이머는 시스템 글로벌 타이머 초기화가 시작되기 전까지는 계속 `periodic mode`로 동작하게 된다.
- How to switch a system to one-shot mode ?
: CPU 로컬 타이머는 bring-up 이 되면 디폴트 동작 모드가 `periodic mode`다. 그러나, 현재 대부분의 로컬 타이머들은 one-shot mode 을 지원하며, high-resolution 를 선호한다. 그래서, 타이머 인터럽트가 주기적으로 발생할 때 마다 one-shot mode 로 전환할 수 있는지를 체크한다.
: 좀 더 구체적으로 설명하면 다음과 같다. 타이머 인터럽트가 발생하면, 기본적인 작업을 처리한 후, softirq를 통해서 후반 처리 작업을 한다(softirq로 동작하기 때문에, `hardirq context`에서 동작한다). 이 때, 타이머 softirq 핸들러가 `hrtimer_run_queues` 핸들러다. 이 함수는 타이머 인터럽트가 발생할 때마다, 호출되서 `one-shot mode`로 전환이 가능한지를 체크한다. 이 때, one-shot mode 로 전환할 수 있는지를 주기적으로 체크하는 함수가 바로 `tick_check_oneshot_change` 함수다.
//kernel/time/hrtimer.c - v6.5 /* * Called from run_local_timers in hardirq context every jiffy */ void hrtimer_run_queues(void) { struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases); .... if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) { hrtimer_switch_to_hres(); return; } .... }
: 그리고 이 함수안에 `tick_is_oneshot_available` 함수가 있는데, 이 함수는 IDLE 상태에서 one-shot 타이머 서비스를 제공할 수 있는지를 판단한다. 이 함수의 기능은 2가지로 요약할 수 있다.
1. 어떤 타이머든 상관없다. `C3 상태에서도 깨어있음 + one-shot 타이머 서비스 제공 가능한` 타이머 인지만 판단한다.
2. 위의 조건 때문에, per-CPU 타이머와 브로드 캐스트 타이머, 2개 모두 검사한다. 처음 2개의 조건문은 per-CPU 타이머에 대한 조건이고, 마지막 세 번째는 브로드 캐스트 타이머에 대한 조건이다.//kernel/time/tick-common.c - v6.5 int tick_is_oneshot_available(void) { struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev); if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT)) // --- 1 return 0; if (!(dev->features & CLOCK_EVT_FEAT_C3STOP)) // --- 2 return 1; return tick_broadcast_oneshot_available(); // --- 3 }
1. `CLOCK_EVT_FEAT_ONESHOT` 은 하드웨어적으로 one-shot mode 를 지원하는지 나타낸다. 당연히, per-CPU 타이머가 one-shot mode 를 지원하지 않을 경우, `0` 을 리턴한다. 지원할 경우, 다음 조건문으로 넘어간다.
2. 이 조건문까지는 per-CPU 타이머에 대한 판단한다. 만약에, per-CPU 타이머에 C3STOP 플래그가 CELAR 되어있다면, Always-on 타이머일 가능성이 높다. 이럴 경우, 브로드 캐스트 타이머한테 서비스를 받을 필요가 없다. deep idle 에 들어가더라도, per-CPU 타이머가 계속 동작하고 있을테니 말이다. 이 때는 per-CPU 타이머를 통해서 타이머 서비스를 제공받으면 된다. 그러므로, `1` 을 리턴할 수 있다.
3. `tick_broadcast_oneshot_available` 함수는 per-CPU 타이머가 one-shot 은 지원하지만, C3 에서 멈출 때 실행된다. 이 함수는 브로드 캐스트 타이머가 one-shot 모드를 지원하는지만 검사한다. 왜, C3STOP 플래그는 검사하지 않을까? tick_broadcast_device 변수에서 참조되고 있다는 것은 이미 C3STOP 플래그 SET 되어있다는 뜻이다. 그러므로, C3STOP 검사는 생략이 가능하다.// kernel/time/tick-broadcast.c - v6.5 bool tick_broadcast_oneshot_available(void) { struct clock_event_device *bc = tick_broadcast_device.evtdev; return bc ? bc->features & CLOCK_EVT_FEAT_ONESHOT : false; }
- How does the broadcast tick device `in periodic tick mode` work ?
: 브로드 캐스트 타이머가 `period mode` 로 동작한다면, 타이머 핸들러는 tick_handle_periodic_broadcast 가 사용된다.
// kernel/time/tick-broadcast.c - v6.5 static void tick_handle_periodic_broadcast(struct clock_event_device *dev) { struct tick_device *td = this_cpu_ptr(&tick_cpu_device); bool bc_local; raw_spin_lock(&tick_broadcast_lock); /* Handle spurious interrupts gracefully */ if (clockevent_state_shutdown(tick_broadcast_device.evtdev)) { // --- 1 raw_spin_unlock(&tick_broadcast_lock); return; } bc_local = tick_do_periodic_broadcast(); // --- 2 if (clockevent_state_oneshot(dev)) { // --- 3 ktime_t next = ktime_add_ns(dev->next_event, TICK_NSEC); clockevents_program_event(dev, next, true); } raw_spin_unlock(&tick_broadcast_lock); if (bc_local) // --- 4 td->evtdev->event_handler(td->evtdev); }
1. 브로드 캐스트 타이머 핸들러가 호출됬는데, 브로드 캐스트 타이머가 shutdown 되어있는지 확인한다? 브로드 캐스트 타이머가 필요없는 환경이 있다. per-CPU 타이머만 존재하는데, 모두 C3STOP 플래그가 CELAR 되어있다면 브로드 캐스트 타이머가 필요없다. 이런 경우, 브로드 캐스트 타이머의 next_event 가 KTIME_MAX 가 된다. 그런데, 어찌하다 보니 실수로 브로드 캐스트 타이머에 next event 를 요청해버렸다.`next_event = KTIME_MAX + next tick` 을 하면, 음수가 되버린다. 이런 문제를 방지하기 위해 가장 좋은 건 핸들러 함수안에 방어 코드를 넣는 것이다. 언제 발생할 지 알 수 없기 때문에, 모든 발생원은 막을 수 는 없다. 그러나, 발생했다면 내부에서 에러 처리 후 종료할 수 는 있다[참고1].
2. 현재 시점을 기준으로 timer service 가 필요한 모든 CPU를 wake-up 한다.
3. tick_handle_periodic_broadcast 함수가 호출됬다는 것은 td->mode 가 `TICKDEV_MODE_PERIODIC` 인것은 확실하다. 그러나, 이것이 물리적으로 periodic mode 로 동작한다는 것을 보장하지는 않는다. 물리적으로도 periodic mode 라면, 하드웨어에 접근할 필요가 없다. 자동으로 타이머 인터럽트가 발생할 것이기 때문이다. 그러나, one-shot mode 라면 매번 재설정이 필요하다.
4. 브로드 캐스트 타이머는 단지 wake-up 만 시켜주는 인터럽트다. 실제 동작은 per-CPU 타이머에 설정된 타이머 핸들러가 호출되어야 한다.: `cpu_online_mask`는 시스템에 존재하는 `online CPU`들을 저장한다. `tick_broadcast_mask`는 `브로드 캐스트 타이머 서비스`를 요청한 CPU 코어들을 저장한다. `cpumask_and` 함수는 두 번째 인자와 세 번째 인자를 `&` 연산해서, 첫 번째 인자에 대입한다. 즉, `tmpmask` 는 `online CPU`s 중에서 브로드 캐스트 타이머 서비스를 요청한 CPU`s` 이라고 정의할 수 있다.
// kernel/time/tick-broadcast.c - v6.5 static bool tick_do_periodic_broadcast(void) { cpumask_and(tmpmask, cpu_online_mask, tick_broadcast_mask); return tick_do_broadcast(tmpmask); }
: `tick_do_broadcast` 함수가 broadcast IPI 를 구현한 함수다.
// kernel/time/tick-broadcast.c - v6.5 static bool tick_do_broadcast(struct cpumask *mask) { int cpu = smp_processor_id(); struct tick_device *td; bool local = false; if (cpumask_test_cpu(cpu, mask)) { // --- 1 struct clock_event_device *bc = tick_broadcast_device.evtdev; cpumask_clear_cpu(cpu, mask); local = !(bc->features & CLOCK_EVT_FEAT_HRTIMER); // --- 2 } if (!cpumask_empty(mask)) { // --- 3 td = &per_cpu(tick_cpu_device, cpumask_first(mask)); td->evtdev->broadcast(mask); } return local; }
1. `tick_do_broadcast` 함수는 먼저 현재 CPU 코어가 `브로드 캐스트 타이머 서비스`가 필요한지를 검사한다. 만약, 필요하다면 이제 서비스를 제공할 것이므로, `tmpmask`에서 해당 CPU는 제거한다(`cpumask_clear_cpu`).
2. 일반적으로, `hrtimer`란 로컬 타이머를 의미한다. 왜냐면, 브로드 캐스트 타이머보다 로컬 타이머들이 성능이 더 좋기 때문이다. 브로드 캐스트 타이머가 없는 시스템에서는 로컬 타이머가 브로드 캐스트 타이머가 될 수 있다[참고1]. 위에 코드는 현재 CPU의 로컬 타이머가 시스템 전역적으로 사용되는 브로드 캐스트 타이머로 선택됬는데, 타이머 인터럽트를 받게 된 것이다(`hrtimer based broadcast` 라고 하면, 브로드 캐스트 타이머로 로컬 타이머가 사용되고 있다고 보면 된다[참고1]). 그래서 이 인터럽트를 처리하기 위해 타이머 인터럽트 핸들러를 호출해야 하는데, 브로드 캐스트 타이머로 선택된 타이머의 인터럽트 핸들러는 `tick_handle_periodic_broadcast 혹은 tick_handle_oneshot_broadcast` 함수다. 이럴 경우, 현재 발생한 타이머 인터럽트를 처리할 수 가 없다. 만약, 브로드 캐스트 타이머가 per-CPU 타이머가 아니라면, 타이머 인터럽트를 처리할 수 있다. 그러므로, `local`에 true를 반환한다. 이 값은 `tick_handle_periodic_broadcast 혹은
tick_handle_oneshot_broadcast` 함수에 반환되서(`bc_local`) 현재 CPU의 타이머 인터럽트 핸들러를 호출할 수 있게 해준다. per-CPU 타이머가 브로드 캐스트 타이머로 설정된 경우, 타이머 인터럽트 핸들러는 2개가 된다.
1. CPU 브로드 캐스트 타이머 핸들러 : tick_broadcast_device.evtdev->event_handler
2. CPU 로컬 타이머 핸들러 : td->evtdev->event_handler
3. `cpumas_empty(mask)`가 false면, 다른 CPU 코어들이 브로드 캐스트 타이머 서비스가 필요하다는 것을 의미한다. 그래서, 현재 CPU는 `브로드 캐스트 타이머 서비스`가 필요한 CPU 코어들 중에서 제일 번호가 작은 CPU를 추출한다. 그리고, 해당 코어의 `strcut clock_event_device.boardcast` 콜백 함수를 호출해서 `브로드 캐스트 타이머 서비스`가 필요한 모든 CPU들을 wake-up 한다. broadcast 메커니즘은 IPI를 이용한다(`IPI_TIMER`). 더 구체적인 내용은 `tick_broadcast` 함수를 참고하자.- How does the broadcast tick device `in one-shot mode` work ?
1. Processing when each CPU`s enter/exit idle
: `cpuidle driver`가 초기화되는 과정에서 현재 시스템에서 지원하는 cpuidle 상태들을 읽게된다. 이 때, `CPUIDLE_FLAG_TIMER_STOP` 플래그가 SET 되어있는 cpuidle 상태가 있으면 `cpuidle_driver->bc_timer` 필드가 SET 된다. `bc_timer`가 SET 되어있으면, CPU 코어가 IDLE 상태에 `enter / exit` 할 때, `tick_broadcast_enter / tick_broadcast_exit` 함수가 호출되고, 브로드 캐스트 타이머를 On / Off 하게 된다.
// include/linux/tick.h - v6.5 static inline int tick_broadcast_enter(void) { return tick_broadcast_oneshot_control(TICK_BROADCAST_ENTER); } static inline void tick_broadcast_exit(void) { tick_broadcast_oneshot_control(TICK_BROADCAST_EXIT); } // drivers/cpuidle/cpuidle.c - v6.5 noinstr int cpuidle_enter_state(struct cpuidle_device *dev, struct cpuidle_driver *drv, int index) { .... if (broadcast && tick_broadcast_enter()) { .... broadcast = false; } .... entered_state = target_state->enter(dev, drv, index); .... if (broadcast) tick_broadcast_exit(); .... return entered_state; }
: `tick_broadcast_oneshot_control` 함수는 현재 CPU 로컬 타이머의 C3STOP 플래그를 확인한다. 확인해서 뭐하려고 하는걸까? `tick_broadcast_enter` 함수가 호출되었다는 것은 시스템이 CPUidle 상태를 요청한 것이다. 그렇기 때문에, 로컬 타이머는 Off 될 준비를 해야한다. 이 때, tick_broadcast_enter 타이머가 하는 일중 하나가 로컬 타이머를 Off 시키는 작업이다. 그런데, 로컬 타이머가 C3STOP 이 설정되어 있지 않다면 ? 더 이상 할 게 없다. 이런 경우는 로컬 타이머가 브로드 캐스트 타이머로 설정된 경우다.
// kernel/time/tick-common.c - v6.5 int tick_broadcast_oneshot_control(enum tick_broadcast_state state) { struct tick_device *td = this_cpu_ptr(&tick_cpu_device); if (!(td->evtdev->features & CLOCK_EVT_FEAT_C3STOP)) return 0; return __tick_broadcast_oneshot_control(state); }
: `__tick_broadcast_oneshot_control` 함수는 현재 코드를 실행중인 CPU 를 wake-up 시킬 타이머로 `per-CPU wake-up timer` 선택할지 `broadcast timer` 로 할지를 정하는 함수다. 우선 순위는 코드 순서상 앞에 있는 per-CPU wake-up timer 가 강하다. 만약, per-CPU wake-up timer 가 있다면, 브로드 캐스트 타이머가 아닌 per-CPU wake-up timer 를 통해서 wake-up 된다.
// kernel/time/tick-broadcast.c - v6.5 int __tick_broadcast_oneshot_control(enum tick_broadcast_state state) { struct tick_device *td = this_cpu_ptr(&tick_cpu_device); int cpu = smp_processor_id(); if (!tick_oneshot_wakeup_control(state, td, cpu)) return 0; if (tick_broadcast_device.evtdev) return ___tick_broadcast_oneshot_control(state, td, cpu); /* * If there is no broadcast or wakeup device, tell the caller not * to go into deep idle. */ return -EBUSY; }
: `tick_oneshot_wakeup_control` 함수는 일부 SoC 에서 퍼포먼스를 위해서 `per-cpu timer` 를 wake-up timer 로 사용한다[참고1 참고2]. 여기서 재미있는 건, 기존 clock event device 를 `ONESHOT_STOPPED` 하고, per-CPU wake-up timer 를 `ONESHOT` 모드로 동작시킨다는 것이다. 이 말은, 하드웨어적으로 하나의 per-CPU 타이머를 NORMAL 상태에서는 normal timer 로 사용하고(`dev`), IDLE 상태가 되면, wake-up timer 로 사용한다는 것이다(`wake-up`). 내 개인적인 생각이지만, 만약에 per-CPU 타이머가 2개 존재했다면, 기존 `dev` 과 `wd` 가 완전히 별개의 타이머이기 때문에, `dev` 를 `ONESHOT_STOPPED` 이 아닌, `SHOTDOWN` 을 했을것이다.
// kernel/time/tick-broadcast.c - v6.5 static int tick_oneshot_wakeup_control(enum tick_broadcast_state state, struct tick_device *td, int cpu) { struct clock_event_device *dev, *wd; dev = td->evtdev; if (td->mode != TICKDEV_MODE_ONESHOT) return -EINVAL; wd = tick_get_oneshot_wakeup_device(cpu); if (!wd) return -ENODEV; switch (state) { case TICK_BROADCAST_ENTER: clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT_STOPPED); clockevents_switch_state(wd, CLOCK_EVT_STATE_ONESHOT); clockevents_program_event(wd, dev->next_event, 1); break; case TICK_BROADCAST_EXIT: /* We may have transitioned to oneshot mode while idle */ if (clockevent_get_state(wd) != CLOCK_EVT_STATE_ONESHOT) return -ENODEV; } return 0; }
: `__tick_broadcast_oneshot_control` 함수는 idle entry 시에 로컬 타이머를 Off 하고, 필요하다면 next broadcast timer 를 재설정한다. idle exit 시에는
// kernel/time/tick-broadcast.c - v6.5 static int ___tick_broadcast_oneshot_control(enum tick_broadcast_state state, struct tick_device *td, int cpu) { struct clock_event_device *bc, *dev = td->evtdev; int ret = 0; ktime_t now; raw_spin_lock(&tick_broadcast_lock); bc = tick_broadcast_device.evtdev; if (state == TICK_BROADCAST_ENTER) { // --- 1 ret = broadcast_needs_cpu(bc, cpu); // --- 2 if (ret) goto out; if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC) { // --- 3 /* If it is a hrtimer based broadcast, return busy */ if (bc->features & CLOCK_EVT_FEAT_HRTIMER) ret = -EBUSY; goto out; } if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_oneshot_mask)) { // --- 4 WARN_ON_ONCE(cpumask_test_cpu(cpu, tick_broadcast_pending_mask)); /* Conditionally shut down the local timer. */ broadcast_shutdown_local(bc, dev); // --- 4 if (cpumask_test_cpu(cpu, tick_broadcast_force_mask)) { // --- 5 ret = -EBUSY; } else if (dev->next_event < bc->next_event) { // --- 6 tick_broadcast_set_event(bc, cpu, dev->next_event); ret = broadcast_needs_cpu(bc, cpu); if (ret) { cpumask_clear_cpu(cpu, tick_broadcast_oneshot_mask); } } } } else { if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_oneshot_mask)) { // --- 7 clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT); if (cpumask_test_and_clear_cpu(cpu, // --- 8 tick_broadcast_pending_mask)) goto out; if (dev->next_event == KTIME_MAX) // --- 9 goto out; now = ktime_get(); if (dev->next_event <= now) { // --- 10 cpumask_set_cpu(cpu, tick_broadcast_force_mask); goto out; } tick_program_event(dev->next_event, 1); // --- 11 } } out: raw_spin_unlock(&tick_broadcast_lock); return ret; }
1. 여기에 들어왔다는 건, CPU 가 IDLE 상태로 진입할 것이라는 신호다.
2. broadcast_needs_cpu 함수는 현재 시스템에 설정된 브로드 캐스트 타이머가 현재 코드를 실행중 인 CPU의 로컬 타이머인지를 검사한다. 즉, 2가지 작업을 수행한다.
1. 브로드 캐스트 타이머가 per-CPU 기반인지
2. 위에 내용이 참일 때, 브로드 캐스트 타이머가 현재 코드를 실행중 인 CPU 의 로컬 타이머인지
3. ___tick_broadcast_oneshot_control 함수가 호출되는 시점에 브로드 캐스트 타이머 periodic mode 라는 것은 해석이 불가능한 오류다. 마치 NULL 이 들어와서 오류를 체크하는 것과 같다고 보면 된다. 왜냐면, ___tick_broadcast_oneshot_control 함수가 호출되기 위해서 반드시 `prepare` 작업들이 필요하기 때문이다. `prepare` 작업중에 하나가 브로드 캐스트 타이머는 미리 one-shot mode 로 동작중인 상태여야 한다(prepare 작업중에 또 하나가, 이 함수를 실행하는 CPU 비트가 tick_broadcast_oneshot_mask 에서 CLEAR 되어 있어야 한다).
4. `cpumask_test_and_set_cpu` 함수는 첫 번째 인자가 두 번째 bitmask 에 속하면 true 를 반환, 속하지 않으면 false 를 반환한다. 그리고, 반환값과 상관없이 첫 번째 인자를 bitmask에 set 한다. 이 함수를 호출하는 CPU 는 아직 IDLE 상태가 아니여야 한다. 그 증표는 `tick_broadcast_oneshot_mask` 로 판단한다. 그 변수에 SET 되어있는 CPU 는 아직 IDLE 상태가 아니라고 판단한다. IDLE 상태가 아니라면, 아직 로컬 타이머가 On 되어있을 것이므로, shutdown 시킨다(`broadcast_shutdown_local`).
5. force_mask 에 SET 되어있다는 것은 `곧 타이머 서비스를 받을 것이므로, re-programming 하지 마라` 라는 뜻이다.
6. 이제 브로드 캐스트 타이머의 `next event` 를 `re-programming` 할 때 가 됬다. 브로드 캐스트 타이머에 설정되는 next event 는 모든 CPU`s 타이머 중에서 미래에 가장 빨리 실행될 타이머(bc->next_event)가 설정된다. 왜냐면, 브로드 캐스트 타이머는 여러 타이머를 동시에 설정할 수 없기 때문이다. 이 비교문을 구현하기 위해서는 아래와 같이 조건을 걸면 된다.
if [ 현재 CPU 로컬 타이머 next event < 브로드 캐스트 타이머 next event ]
- 브로드 캐스트 타이머 next event = 현재 CPU 로컬 타이머 next event
else
- 유지
7. 특정 CPU 가 IDLE 상태를 종료하고, 브로드 캐스트 타이머를 exit 할 때, 이 조건문으로 들어온다. 즉, 이 시점부터는 로컬 타이머가 다시 동작하게 되고, 브로드 캐스트 타이머는 사용하지 않는다. 그렇기 때문에, tick_broadcast_oneshot_mask 에서 현재 CPU 비트를 CLEAR 한다. 이제부터 코드를 실행중 인 CPU 는 one-shot 브로드 캐스트 타이머 서비스에서 제외된다.
8. 지금 이 코드가 실행되는 시점은 브로드 캐스트 타이머 서비스가 더 이상 필요없는 시점이다. 왜냐면, 이제 CPU 가 IDLE 상태를 exit 하고, 로컬 타이머에서 타이머 서비스를 받을 수 있기 때문이다. 그런데, tick_broadcast_pending_mask 에 현재 CPU 비트가 SET 되어있다는 것은 이전 one-shot 브로드 캐스트 타이머 서비스(tick_handle_oneshot_broadcast)를 처리한 CPU 가 dynamic tick 스케줄링을 하는 과정에서 현재 CPU 한테 브로드 캐스트 타이머를 필요하다고 생각해서 tick_broadcast_pending_mask 에 현재 CPU 비트를 SET 한 것이다. 이제 문제는 이걸 어떻게 처리할 것이냐이다. 지금 상황이라면, 로컬 타이머 핸들러도 처리할 수 있긴하다. 그러나, 리눅스 커널은 아래와 같은 답변을 내놓는다. 즉, 로컬 타이머 핸들러 타이머 인터럽트를 처리하지 않고, broadcast IPI handler 로 처리하겠다는 소리다.
So we are going to handle the expired event anyway via the broadcast IPI handler. No need to reprogram the timer with an already expired event.
- 참고 : https://github.com/torvalds/linux/blob/82714078aee4ccbd6ee7579d5a21f8a72155d0fb/kernel/time/tick-broadcast.c#L795
9. CPU 로컬 타이머의 next event 가 KTIME_MAX 면, 현재 필요한 타이머 서비스가 없다는 뜻이다.
10. 굉장히 복잡한 조건문이다. 여기까지 왔다는 것은 코드를 실행중 인 CPU는 broadcast IPI를 받은 CPU가 아닌, 브로드 캐스트 타이머 서비스를 직접 처리한 CPU 라는 뜻이다(tick_oneshot_pending_mask를 패스했으므로). 혹은, 다른 이유에서건 wake-up 된 것일 수 도 있다. 이 시점에서 `if (dev->next_event <= now)` 가 참이라는 것은, 로컬 타이머로 타이머 서비스를 받아야 정상이다. 왜냐면, 현재 CPU는 이 시점에 브로드 캐스트 타이머 서비스( tick_broadcast_oneshot_mask)가 CLEAR 됬기 때문에, 브로드 캐스트 타이머 서비스를 받을 수 없다. 그런데, 또 이 시점에 로컬 타이머에 타이머 서비스를 요청하면, `핑퐁` 현상이 발생한다. 예를 들어, `if (dev->next_event <= now)` 가 참이되서 , 로컬 타이머에 타이머 서비스를 세팅했다고 치자. 이 때, tick_program_event 함수를 이용해서 로컬 타이머에 타이머 서비스를 요청할 것이다. 근데, 운 나쁘게 로컬 타이머 서비스가 발생하기 전에 다시 IDLE 로 가게 되버렸다. 이제 로컬 타이머를 shotdown 되버린다. 이전에 로컬 타이머에 설정한 타이머 서비스가 사라져 버린것이다. 그래도 아직 기회는 있다. CPU가 IDLE 상태로 진입하면서, 다시 브로드 캐스트 타이머 서비스를 받을 수 있으니깐 말이다. 이제 다른 CPU 가 one-shot 브로드 캐스트 타이머를 받고, 현재 CPU 의 expiry timer service 를 확인해서 tick_broadcast_pending_mask 에 등록해줬다. 그런데, 현재 CPU의 expiry timer service 가 가장 빨리 실행되는 타이머 서비스여서, 브로드 캐스트 타이머 서비스를 직접 처리(tick_handle_oneshot_broadcast)하는 CPU 가 되버렸다. 이러면, tick_broadcast_pending_mask 에서 CLEAR 가 된다. 문제가 반복되게 된다. 이 때는 이미 시간이 지났는데, 발생하지 않았다고 강제로 다시 타이머 서비스를 요청(`forced re-programming`) 하지 말고, `tick_broadcast_force_mask` 에 자신을 SET 하고 타이머 인터럽트가 발생하기를 기다린다. tick_broadcast_force_mask 에 설정된 CPU 들은 tick_handle_oneshot_broadcast 핸들러가 호출되면, 모두 wake-up 시킨다[참고1].
11. 이 코드까지 왔다는 것은 CPU 로컬 타이머의 next_event 가 설정은 되어 있는데( != KTIME_MAX), 현재 시간보다 더 뒤에 실행된다는 것을 의미하므로, 로컬 타이머에 다음에 발생할 타이머를 설정하게 된다. 이 분기문에서는(TICK_BROADCAST_EXIT) 이제 더 이상 브로드 캐스트 타이머를 사용하지 않으므로, 로컬 타이머에 next event 를 등록한다.2. How does the broadcast tick device broadcast tick events to each CPU core ?
: 브로드 캐스트 타이머로부터 one-shot 타이머 인터럽트를 받으면, 모든 CPU 는 high-level 인터럽트 핸들러로 `tick_handle_oneshot_broadcast` 를 실행하게 된다.
static void tick_handle_oneshot_broadcast(struct clock_event_device *dev) { struct tick_device *td; ktime_t now, next_event; int cpu, next_cpu = 0; bool bc_local; raw_spin_lock(&tick_broadcast_lock); dev->next_event = KTIME_MAX; next_event = KTIME_MAX; cpumask_clear(tmpmask); now = ktime_get(); for_each_cpu(cpu, tick_broadcast_oneshot_mask) { // --- 1 if (!IS_ENABLED(CONFIG_SMP) && cpumask_empty(tick_broadcast_oneshot_mask)) break; td = &per_cpu(tick_cpu_device, cpu); if (td->evtdev->next_event <= now) { // --- 2 cpumask_set_cpu(cpu, tmpmask); cpumask_set_cpu(cpu, tick_broadcast_pending_mask); // --- 3 } else if (td->evtdev->next_event < next_event) { // --- 4 next_event = td->evtdev->next_event; next_cpu = cpu; } } cpumask_clear_cpu(smp_processor_id(), tick_broadcast_pending_mask); // --- 5 cpumask_or(tmpmask, tmpmask, tick_broadcast_force_mask); // --- 6 cpumask_clear(tick_broadcast_force_mask); if (WARN_ON_ONCE(!cpumask_subset(tmpmask, cpu_online_mask))) // --- 7 cpumask_and(tmpmask, tmpmask, cpu_online_mask); bc_local = tick_do_broadcast(tmpmask); // --- 8 if (next_event != KTIME_MAX) tick_broadcast_set_event(dev, next_cpu, next_event); // --- 9 raw_spin_unlock(&tick_broadcast_lock); if (bc_local) { // --- 10 td = this_cpu_ptr(&tick_cpu_device); td->evtdev->event_handler(td->evtdev); } }
1. `tick_broadcast_oneshot_mask`의 각 비트가 SET 되었다는 것을 2가지 의미가 있다.
- 해당 코어가 현재 IDLE 상태임을 나타냄.
- 해당 코어가 현재 브로드 캐스트 타이머를 요청한 상태임.
즉, 해당 코드는 루프를 돌면서, 브로드 캐스트 타이머 인터럽트 서비스가 필요한 코어를 탐색하는 것이다.
2. 이 코드는 현재 시간을 기준으로 이미 타임 아웃이 지난 코어들에게 wake-up 타이머 인터럽트를 발생시킨다는 것이다. 리눅스 커널은 `dynamic tick` 환경에서 한 번 타이머 인터럽트가 발생했을 때, 모든 코어들의 `next_event`를 확인해서 타임 아웃된 코어들에게 wake-up 타이머 인터럽트를 브로드 캐스트한다.
3. tick_broadcast_pending_mask 는 one-shot 브로드 캐스트 서비스가 필요한 CPU 비트가 SET 되어있는 mask 다. 이 코드에서는 tmpmask 와 tick_broadcast_pending_mask 이 동일한 역할을 할 것 처럼 보이지만, 사실 tmpmask 는 용도가 정해져있다. 뒤에서 다시 다룬다.
4. 지금 당장 `broadcast tick`을 통해 wake-up 시켜야 할 CPU 코어들이 없다면, 즉, `2`번 조건이 `false`라면, 다음 타이머 인터럽트를 준비한다. 타이머 서비스가 필요한 모든 코어들을 순회하면서, 현재 설정된 next_event와 각 코어들의 next_event를 비교해서 가장 빨리 발생시켜야 할 타이머 인터럽트와 CPU를 선택한다. 이 함수의 핵심은 `2`번과 `4`번 코드에 있다. `2`번은 타임아웃이 발생한 모든 코어들에게 wake-up 브로드 캐스트 타이머 인터럽트를 발생시키고, `4`번은 다음번에 wake-up 할 시점을 정한다.
5. tick_broadcast_pending_mask 는 one-shot 브로드 캐스트 서비스가 필요한 CPU 비트가 SET 되어있는 mask 다. 좀 더 정확히 말하면, 이제 곧 broadcast IPI 를 통해서 깨워야 할 CPU 비트들이 저장되어 있다. 현재 코드를 실행중 인 CPU 는 브로드 캐스트 타이머를 받아서 코드를 실행하고 있는 것이기 때문에, tick_broadcast_pending_mask 에서 CLEAR 되도 된다. 그렇다면, broadcast IPI 를 받은 CPU`s 들도 tick_broadcast_pending_mask 를 CLEAR 해야 할 텐데, 어디서 CLEAR 할까? one-shot 브로드 캐스트 타이머를 받으면, 모든 CPU 는 바로 이 함수, 즉, `tick_handle_oneshot_broadcast` 함수(핸들러)를 호출한다. 그렇다면, 결국에는 이 라인(`cpumask_clear_cpu(smp_processor_id(), tick_broadcast_pending_mask)`)에 와서 자기자신을 CLEAR 하게 될 것이다.
6. tick_broadcast_force_mask 에 설정된 CPU 들은 이미 시간이 지났거나, 곧 깨어날 CPU 를 의미한다. 그러므로, next event 에 대한 re-programming 이 필요없는 CPU 들이 tick_broadcast_force_mask 에 SET 된다. tmpmask 와 tick_broadcast_pending_mask 또한 이제 깨워야 할 CPU 들을 저장하고 있으므로, 여기에 이미 지난 CPU(tick_broadcast_force_mask) 까지 더해서 깨워주면 금상첨화다.
7. CPU hot-plug 때문에 off-line 된 CPU 들은 제외시켜야 한다. 즉, online CPU`s 들만 wake-up 한다.
8. 타임아웃이 된 모든 코어들을 IPI 를 통해 wake-up 한다(위에서 `tick_do_broadcast` 함수 설명함).
9. tick_handle_oneshot_broadcast 함수에서 next_event 변수의 초기값은 KTIME_MAX 다. 일반적으로, KTIME_MAX 가 설정되면, next event 가 없다는 것을 의미한다. 만약, next_event 가 KTIME_MAX 가 아니라면, next_event 에는 현재 시점을 기준으로 가장 빨리 만료되는 타이머 인터럽트 시간이 들어가게 된다. 그리고, next_event 발생 시에 어떤 CPU 코어에게 타이머 인터럽트를 발생시킬 지를 설정한다(`tick_broadcast_set_event`). 계속 언급했지만, 브로드 캐스트 타이머는 하나의 CPU 만 깨우고, 웨이크-업된 CPU 가 시간 초과된 나머지 CPU 들을 IPI 로 wake-up 시킨다.
10. tick_do_broadcast 함수에서 설명했던 내용이다.: 위에서 `tick_broadcast_pending_mask` 와 `tmpmask` 역할이 비슷해보이는데 무슨 차이가 있을까? tmpmask 는 `총합`과 같다. tick_broadcast_pending_mask 에는 이제 곧 브로드 캐스트 타이머를 받아서 wake-up 할 CPU 들이 저장되어 있다. 그런데, 브로드 캐스트 타이머를 보낼 때, online CPU`s 들 에게만 전송해야 한다. 이제 어떻게 해야 할까? 2개의 내용을 and 연산한 mask 가 있으면 된다. 그런데, 이걸 어디에 저장해야 할까? tick_broadcast_pending_mask 는 online & offline CPU 에 상관없이 모든 CPU 를 대상으로 하는 mask 로 남겨놓고 싶다. 새로운 mask 를 만들면 된다. 바로 그게 `tmpmask` 다.
tmpmask = tick_broadcast_pending_mask & cpu_online_mask
: tmpmask 는 전역 변수이기 때문에, tick_handle_oneshot_broadcast 함수에서만 사용되는 것은 아니다. 다른 곳에서도 위와 같은 용도로 사용되기 때문에, 위에 내용만 잘 숙지해도 이해하는데 어려움은 없을 것이다.
- When to shutdown a broadcast timer ?
: 브로드 캐스트 타이머는 언제 shodown 될까 ? 이론적으로, 모든 CPU 가 NORMAL 상태라면, 브로드 캐스트 타이머는 사용되지 않는다. 이 조건을 어떻게 코드로 표현할까 ? 일단, 브로드 캐스트 타이머 요청은 2가지로 나뉜다는 것 부터 기억하자. 왜 이게 필요하냐면, 요청이 없을 때, 브로드 캐스트 타이머를 종료하면 되기 때문이다.
1. period : tick_broadcast_mask
2. one-shot : tick_broadcast_oneshot_mask: 그런데, tick-broadcasts.c 파일을 보면, shutdown 조건은 다음과 같다. PERIODIC 모드일 때만 shutdown 을 한다. 왜 그럴까 ?
if bc && (bc.mode = TICKDEV_MODE_PERIODIC) && cpumask_empty(tick_broadcast_mask) then, shutdown(bc)
: 브로드 캐스트 타이머는 애초에 CPUIDLE 상태에서 동작하도록 만들어졌다. 그래서, 동작 모드가 `one-shot mode` 인 것이 정상이다. `period mode` 로 사용되는 것은 특이 케이스다. 그러므로, `period mode` 인데 `periodic tick` 요청도 없으면 종료가 가능한 것이다.
- What is dummy timer ? [참고1 참고2 참고3]
: 틱 브로드 캐스트 프레임워크의 등장으로 dummy timer 도 등장하게 되었다. 더미 타이머는 일반적으로 로컬 타이머에 적용된다. 아래 패치 내용을 보면 알겠지만, 하드웨어적으로 per-CPU 타이머가 없는 환경에서 더미 타이머를 이용하면, 소프트웨어적으로는 마치 per-CPU 타이머가 존재하는 것처럼 동작하게 된다. 예를 들어, 시스템에 1개의 글로벌 타이머와 4개의 CPU 가 있는데, 각 CPU 는 별도의 per-CPU 타이머가 없는 구조라고 가정하자. 이 때, 소프트웨어 적으로 어떻게 per-CPU 타이머가 존재하는 것처럼 할 수 있을까? 더미 타이머를 사용할 때와 실제로 per-CPU 를 사용할 때, 플로우가 어떻게 달라질까?
Several architectures have a dummy timer driver tightly coupled with their broadcast code to support machines without cpu-local timers (or where there is a lack of driver support).
- 참고 : https://lists.infradead.org/pipermail/linux-arm-kernel/2013-May/167948.html: 더미 타이머는 실제로 존재하지 않기 때문에, 새로운 타이머를 등록하면, 시스템 글로벌 타이머에게 타이머 서비스를 요청한다. 그러나, per-CPU 타이머는 실제로 존재하기 때문에, 새로운 타이머를 등록하면, 로컬 타이머에게 타이머 서비스를 요청한다. 이 때, 중요한 점은 `더미 타이머`와 `per-CPU` 의 인터페이스 차이가 없다는 것이다. 예를 들어, 더미 타이머에 새로운 타이머를 등록하는 함수와 per-CPU 에 새로운 타이머를 등록하는 함수의 인터페이스는 동일하다. 이런 장점 때문에 더미 타이머를 쓰는 것이다.
: per-CPU 가 있는 구조는 아래와 같다고 볼 수 있다. 각 CPU 가 타이머 서비스를 요청하면, CPU 프로세서 내에 있는 `per-CPU Timer`를 통해서 모든 처리가 끝난다.
: per-CPU 가 없고, 시스템 글로벌 타이머 한 개만 존재하는 시스템은 아래와 같은 구조라고 볼 수 있다. 각 CPU Affinity 한 타이머 인터럽트를 받기 위해서 인터럽트 컨트롤러에서 오버헤드가 발생한다. 왜? 특정 타이머 인터럽트가 어떤 CPU 에 대응하는지를 알기 위해서 인터럽트 컨트롤러에서 계산이 필요할 것이기 때문이다. 만약, 글로벌 타이머에서 4개의 타이머 채널이 있다면, 얘기가 달라지겠지만, per-CPU 타이머 보다 성능상 느릴 수 밖에 없는 구조라는 것은 변함이 없다. 왜냐면, 프로세서 내부 버스와 외부 버스의 속도 차이가 다르기 때문이다. 거디다가 물리적 거리 문제도 있다. 그러나, 프로세서 내부에서 구조를 보면, DUMMY TIMER 가 마치 per-CPU Timer 와 같은 역할을 해서 소프트웨어적으로 2개 구조는 인터페이스 차이가 없다.
: 그런데, CPU 로컬 타이머가 없는 환경이 존재할까? x86 초기 시절에는 APIC 가 등장하기 전에는 시스템 전역적으로 타이머가 PIT 하나만 존재했다. 각 CPU 마다 자신의 타이머를 갖기 시작한건 멀티 프로세서가 도입되면서 부터 시작됬다.
- Case study [ 참고1 ]
: 현재 시스템에 4개의 코어가 동작중이라고 가정하자. CPU[0|2|3]은 굉장히 바쁘게 프로그램을 실행하는 중이고, CPU[1]은 실행할 프로그램이 없으므로, IDLE 상태로 들어갔다고 가정하자. 이런 상황에서는 당연히 시스템 전체가 IDLE 상태로 진입하지는 못한다.
: ACPI 스펙에서는 CPU[1]에 `wakeup source`가 존재하지 않을 경우, 계속 IDLE 상태를 유지해야 한다. 그러나, 실제로는 `C4` 상태에 있는 CPU[1]은 주기적으로 `wakeup` 한다. 왜 그럴까? `Rescheduling interrupt`일 가능성이 높다. 즉, 프로세스가 특정 CPU 코어에게만 몰리는 현상을 방지하기 위해 스케줄러가 다른 CPU 코어에게 IPI를 전송해서 작업을 분배하는 것이다. 문제는 이 때, IPI를 받은 CPU 코어는 IDLE 상태에서 `wakeup` 이 된다.
: 그래서, 커널 부트 파라미터에서 `isolcpus=1`을 통해 CPU[1]이 `Rescheduling interrupt`를 받는 것을 막았다고 가정하자. 이제 `cat /proc/interrupts`에서 CPU[1]의 인터럽트 카운트가 더 이상 증가하지 않는 것은 확인했다. 그러나, 여전히 `wakeup / sleep` 상태가 반복되는 것은 사라지지 않았다.
: tickless 커널에서, CPU가 IDLE 상태로 진입할 때, polling 모드를 사용하는 것이 아니라면, 대부분의 CPU는 IDLE 상태로 진입하기 전에 다음 타이머 인터럽트에 브로드 캐스트 타이머 인터럽트가 발생하는 시점을 저장해놓는다. CPU[1]이 wakeup 하는 이슈가 바로 `브로드 캐스트 타이머` 때문이었다. 각 코어의 타이머 리스트를 확인하려면, `cat /proc/timer_list`를 확인하면 된다.
cpu: 1 clock 0: .base: (ptrval) .index: 0 .resolution: 10000000 nsecs .get_time: ktime_get active timers: #0: < (ptrval)>, watchdog_timer_fn, S:01 expires at 24470000000-24470000000 nsecs [in 4063953389 to 4063953389
: 위에 내용을 보면, CPU[1]에 설정된 타이머는 `watchdog_timer` 딱 한 개 있다. 이 타이머는 4초 간격으로 호출되는 것으로 나타난다. 커널 부트 파리미터에 `nowatchdog` 옵션을 추가하면, CPU[1]은 `IDLE` 상태를 상대적으로 더 오래 유지할 수 있게된다.
: 리눅스 커널은 `watchdog`을 활성화하면, 각 코어마다 4초의 주기를 가지고 `watchdog_timer_fn` 콜백 함수가 호출된다.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] PM - Platform-dependent power management (0) 2023.09.18 [리눅스 커널] PM - restart & shutdown & halt (0) 2023.09.18 [리눅스 커널] Cpuidle - idle process (0) 2023.09.12 [리눅스 커널] PM QoS - CPU latency QoS framework (0) 2023.09.11 [리눅스 커널] Timer - Dynamic tick(tick sched) (0) 2023.09.08