전체 흐름
NOHZ는 idle 동안 주기적 scheduler tick을 줄일 수 있지만, idle 진입과 tick 정지는 서로 다른 사건이다. Linux v6.6에서는 tick_nohz_idle_enter가 idle 시간 측정을 시작하고, idle loop의 tick_nohz_idle_stop_tick이 조건과 다음 이벤트를 따져 tick을 멈추거나 유지한다. 처음 멈춘 경우에만 scheduler에 idle balancing 상태를 알린다.
tick_sched는 시간·tick 상태를, scheduler의 rq와 nohz 상태는 멈춘 tick을 가진 idle CPU의 balance·load 통계를 다룬다.
이 글의 세 발췌는 idle tick 경로에 초점을 맞춘다. 실행 중인 CPU의 NO_HZ_FULL tick 정지 판단에는 별도 의존성 검사와 갱신 경로가 있다.
tick_nohz_idle_enterts->inidle = 1과 idle 시간 측정 시작tick_nohz_idle_stop_tick정지 가능 조건과 다음 타이머 만료를 확인ts->tick_stopped = 1nohz_balance_enter_idlescheduler의 idle CPU 기록을 조건부 갱신①이 곧 tick 정지를 보장하지 않는다. ④는 ③에서 실제 stopped 전환이 처음 일어났을 때 호출된다. 공식 v6.6 tick-sched.c:1108–1175, fair.c:11803–11858.
소스 코드 위치
공식 Linux v6.6 tag의 두 파일
kernel/time/tick-sched.c — idle accounting, stop 조건·전환, 복귀
kernel/sched/fair.c — tick 정지 후 NOHZ idle balance 표시설명: 아래 세 발췌는 Torvalds의 공식 v6.6 tag kernel/time/tick-sched.c와 kernel/sched/fair.c에서 지정한 연속 행이다. tick-sched.c:1137의 nohz_balance_enter_idle은 호출이며, 함수 정의는 fair.c:11803에 있다.
버전 경계: 이 글은 Linux v6.6의 idle tick 경로를 다룬다. RCU 내부 구현과 실행 중인 CPU의 full-dynticks 경로는 별도 주제다.
개념 참고: 문C의 Scheduler-16, Timer-5, RCU-3는 관련 개념을 읽을 때 참고할 수 있다. 해당 글의 Linux v5.4 설명과 이 글의 v6.6 코드 경로는 구분해야 한다.
대표 코드
세 코드는 v6.6 원본의 연속 발췌다. 첫 함수가 idle 진입을 기록하고, 둘째 함수가 tick 정지를 결정하며, 셋째 함수가 첫 정지 후 scheduler 측 상태를 갱신한다. 각 함수의 나머지 호출자·분기 전체를 대신하지 않는다.
idle 진입은 tick 정지가 아니다
kernel/time/tick-sched.c, 1159–1175행void tick_nohz_idle_enter(void)
{
struct tick_sched *ts;
lockdep_assert_irqs_enabled();
local_irq_disable();
ts = this_cpu_ptr(&tick_cpu_sched);
WARN_ON_ONCE(ts->timer_expires_base);
ts->inidle = 1;
tick_nohz_start_idle(ts);
local_irq_enable();
}라인별 주석
원본 코드: kernel/time/tick-sched.c, Linux v6.6 1159–1175행. 왼쪽 번호는 위 발췌문의 줄 번호이며 고정 원문과 대조한 행만 설명한다.
void tick_nohz_idle_enter(void)현재 CPU가 idle loop에 들어갈 때 호출하는 준비 함수다. 이 함수 자체는 주기 tick의 정지를 결정하지 않는다.
struct tick_sched *ts;현재 CPU의 tick 상태를 가리킬 포인터를 선언한다. 실제 주소는 아래의 per-CPU 조회에서 얻는다.
lockdep_assert_irqs_enabled();호출 시점에 로컬 인터럽트가 허용돼 있어야 한다는 조건을 확인한다. 끝의 재허용과 연결된 진입 전제다.
local_irq_disable();현재 CPU의 인터럽트를 잠시 막고 tick 상태를 변경하는 구간에 들어간다.
ts = this_cpu_ptr(&tick_cpu_sched);per-CPU tick_cpu_sched에서 현재 CPU의 tick_sched 주소를 얻는다. 모든 CPU가 한 ts를 공유하는 것은 아니다.
WARN_ON_ONCE(ts->timer_expires_base);timer_expires_base가 남아 있으면 한 번 경고한다. 경고 자체가 값을 지우거나 tick을 멈추지는 않는다.
ts->inidle = 1;이 CPU의 inidle 상태를 1로 기록한다. v6.6은 필드에 직접 대입하며, 이 값만으로 tick 정지를 뜻하지 않는다.
tick_nohz_start_idle(ts);idle 진입 시각과 계측 상태를 기록한다. 이 호출만으로 주기 tick을 끄지는 않는다.
local_irq_enable();이 함수가 막았던 로컬 인터럽트를 다시 허용한다. 다른 CPU의 인터럽트 상태는 바꾸지 않는다.
읽는 법: IRQ를 잠시 막고 inidle을 세운 뒤 tick_nohz_start_idle로 idle 시간 측정을 시작한다. 여기에는 tick_nohz_stop_tick 호출이 없다.
다음 이벤트를 보고 tick 정지를 판단
kernel/time/tick-sched.c, 1108–1142행void tick_nohz_idle_stop_tick(void)
{
struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
int cpu = smp_processor_id();
ktime_t expires;
/*
* If tick_nohz_get_sleep_length() ran tick_nohz_next_event(), the
* tick timer expiration time is known already.
*/
if (ts->timer_expires_base)
expires = ts->timer_expires;
else if (can_stop_idle_tick(cpu, ts))
expires = tick_nohz_next_event(ts, cpu);
else
return;
ts->idle_calls++;
if (expires > 0LL) {
int was_stopped = ts->tick_stopped;
tick_nohz_stop_tick(ts, cpu);
ts->idle_sleeps++;
ts->idle_expires = expires;
if (!was_stopped && ts->tick_stopped) {
ts->idle_jiffies = ts->last_jiffies;
nohz_balance_enter_idle(cpu);
}
} else {
tick_nohz_retain_tick(ts);
}
}읽는 법: 저장된 만료 시각을 쓰거나 can_stop_idle_tick과 tick_nohz_next_event를 거친다. 만료 시각이 양수일 때 정지를 시도하며, !was_stopped && ts->tick_stopped일 때만 scheduler에 알린다. 만료 시각이 양수가 아니면 tick_nohz_retain_tick으로 캐시를 지우고 새 정지를 시도하지 않는다.
scheduler의 idle-balance 기록
kernel/sched/fair.c, 11803–11858행void nohz_balance_enter_idle(int cpu)
{
struct rq *rq = cpu_rq(cpu);
SCHED_WARN_ON(cpu != smp_processor_id());
/* If this CPU is going down, then nothing needs to be done: */
if (!cpu_active(cpu))
return;
/* Spare idle load balancing on CPUs that don't want to be disturbed: */
if (!housekeeping_cpu(cpu, HK_TYPE_SCHED))
return;
/*
* Can be set safely without rq->lock held
* If a clear happens, it will have evaluated last additions because
* rq->lock is held during the check and the clear
*/
rq->has_blocked_load = 1;
/*
* The tick is still stopped but load could have been added in the
* meantime. We set the nohz.has_blocked flag to trig a check of the
* *_avg. The CPU is already part of nohz.idle_cpus_mask so the clear
* of nohz.has_blocked can only happen after checking the new load
*/
if (rq->nohz_tick_stopped)
goto out;
/* If we're a completely isolated CPU, we don't play: */
if (on_null_domain(rq))
return;
rq->nohz_tick_stopped = 1;
cpumask_set_cpu(cpu, nohz.idle_cpus_mask);
atomic_inc(&nohz.nr_cpus);
/*
* Ensures that if nohz_idle_balance() fails to observe our
* @idle_cpus_mask store, it must observe the @has_blocked
* and @needs_update stores.
*/
smp_mb__after_atomic();
set_cpu_sd_state_idle(cpu);
WRITE_ONCE(nohz.needs_update, 1);
out:
/*
* Each time a cpu enter idle, we assume that it has blocked load and
* enable the periodic update of the load of idle cpus
*/
WRITE_ONCE(nohz.has_blocked, 1);
}읽는 법: CPU 활성·housekeeping·domain 조건에 따라 일찍 반환할 수 있다. 통과하면 rq->nohz_tick_stopped와 nohz.idle_cpus_mask 등을 갱신한다. 이 함수가 timer를 직접 취소하는 것은 아니다.
함수별 분석
idle tick을 멈출 수 있는지와 scheduler가 그 CPU를 NOHZ idle balance 대상으로 기록하는지는 독립된 판단이다. 특히 tick_nohz_next_event는 RCU·architecture·irq_work·timer softirq 요구가 있으면 가까운 tick으로 만료를 당길 수 있다.
can_stop_idle_tickonline, NOHZ mode, reschedule·softirq, timekeeping 담당 여부 등 확인tick_nohz_next_eventRCU·arch·irq_work 요구 및 timer 만료를 반영tick_nohz_stop_tick; 최초 전환이면 scheduler에 통지공식 v6.6 tick-sched.c:801–882, 1058–1142의 조건을 요약했다. 분기 결과는 현재 tick 상태와 timer 만료 값에도 달려 있다.
1. tick_nohz_idle_enter와 stop 요청
inidle은 CPU가 idle loop에 있음을 나타낸다. tick_nohz_idle_stop_tick은 별도 호출에서 tick 정지를 시도하므로, idle 진입만 관찰해서 tick이 멈췄다고 결론 내릴 수 없다.
2. 최초 stop 후 nohz_balance_enter_idle
tick-sched.c:1135–1137의 최초 전환 검사 뒤 scheduler 함수가 호출된다. fair.c는 CPU가 활성·scheduler housekeeping 대상인지 확인한 뒤 blocked load와 idle CPU mask 등을 기록한다. 이 기록과 RCU quiescent state는 같은 필드가 아니다.
| 함수 | 소스상 결정 | 관찰할 결과 |
|---|---|---|
| tick_nohz_idle_enter | inidle·idle 시간 시작 | 이 시점만으로 tick_stopped는 미확정 |
| can_stop_idle_tick / tick_nohz_next_event | 즉시 tick 요구와 다음 만료 시각 | 새 정지를 생략하거나 기존 정지 상태를 재조정 |
| tick_nohz_idle_stop_tick | 정지 시도 전후의 tick_stopped | 최초 stopped 전환에만 scheduler 호출 |
| nohz_balance_enter_idle | active, housekeeping, domain 검사 | 통과 시 scheduler mask·load 상태 갱신 |
구조체와 필드
동일한 “idle”이라는 이름이 시간 계층, scheduler 계층, RCU 계층에서 다른 사실을 뜻한다. 상태의 소유자와 갱신 함수를 함께 읽는다.
tick_schedinidle = idle 진입 기록tick_stopped = 주기 tick 정지 상태idle_active = idle 시간 측정 상태rq + nohzrq->nohz_tick_stoppednohz.idle_cpus_masknohz.has_blocked·needs_updatetick-sched.c의 !was_stopped && ts->tick_stopped가 fair.c의 기록 함수를 호출한다. 조건부 반환 때문에 두 상태를 항상 동일시할 수 없다.rcu_needs_cpu()는 다음 tick 판정에 들어오지만, scheduler의 idle CPU mask 자체가 RCU quiescent state를 뜻하지 않는다.시간 계층의 첫 정지 전환이 scheduler 계층에 전달되는 조건을 구분한 그림이다. 공식 v6.6 tick-sched.c:1135–1137, fair.c:11803–11858.
| 필드·상태 | 소유/뜻 | 갱신 또는 확인 근거 |
|---|---|---|
| ts->inidle | tick 계층의 idle loop 진입 | tick_nohz_idle_enter에서 1, tick_nohz_idle_exit에서 0 |
| ts->tick_stopped | 주기 tick의 정지 여부 | tick_nohz_stop_tick에서 1, restart에서 0 |
| rq->nohz_tick_stopped | scheduler가 기억하는 NOHZ idle 상태 | nohz_balance_enter_idle에서 조건부 1, nohz_balance_exit_idle에서 0 |
| nohz.idle_cpus_mask | idle balance에서 고려할 CPU 집합 | scheduler enter/exit에서 조건부 set/clear |
| nohz.has_blocked | blocked load 재검사 요청 | nohz_balance_enter_idle의 마지막 갱신 |
실행 단계와 상태 변화
idle 도중에도 IRQ와 timer 변화가 들어올 수 있다. 멈춘 tick의 다음 이벤트를 다시 계산하는 경로와 CPU가 idle을 완전히 벗어나는 경로를 나눠 본다.
inidle=1; idle 시간 측정inidle=0; 조건부 idle accounting 종료·tick 갱신/재시작nohz_balance_enter_idle로 mask를 기록한다. 이후 busy tick의 nohz_balancer_kick 경로에서 nohz_balance_exit_idle가 정리될 수 있다. 이것은 위 네 칸과 매번 같은 시점에 움직이는 단일 상태가 아니다.공식 v6.6 tick-sched.c:1108–1175, 1185–1193, 1339–1364 및 fair.c의 nohz_balance_exit_idle·nohz_balancer_kick 관찰점을 구분했다.
| 관찰 상태 | 시작·변화 조건 | 해석의 경계 |
|---|---|---|
| idle 진입 | tick_nohz_idle_enter가 inidle을 세움 | tick 정지 여부는 아직 모름 |
| 새 정지 생략 | 정지 조건 실패 또는 가까운 이벤트 | 기존 tick 상태·다음 event에 따라 처리; idle 시간 측정은 계속될 수 있음 |
| tick 정지 | tick_nohz_stop_tick으로 tick_stopped=1 | scheduler mask 등록은 추가 조건을 통과할 때만 |
| idle 이탈 | tick_nohz_idle_exit가 inidle=0; 필요한 경우 tick 재시작 | scheduler 쪽 mask 해제와 동의어가 아님 |
inidle ≠ tick_stopped ≠ rq->nohz_tick_stopped.불변 조건과 실패 사례
inidle=1만으로 tickless를 선언하지 않는다. 재스케줄 요구·pending softirq·timekeeping 역할·가까운 timer 만료는 새 정지를 막거나 다음 이벤트 재조정을 요구할 수 있다.
tick이 멈춰도 idle 시간 계측이 사라지는 것은 아니다. tick_nohz_idle_exit와 tick_nohz_account_idle_time의 조건부 경로를 확인한다.
첫 stop 뒤에도 비활성 CPU·housekeeping 제외·null domain이면 idle balance mask 등록이 생략될 수 있다. mask 부재만으로 tick이 계속 뛰었다고 해석하지 않는다.
rcu_needs_cpu()는 idle tick 판정의 입력이다. RCU 내부 quiescent state와 full-dynticks dependency는 별도 코드를 확인해야 한다.
“idle 진입 = tick 중단 = scheduler mask 등록”이라는 한 줄짜리 상태 전이로 해석하지 않는다.
문C의 Linux v5.4 설명에서 개념상 도움을 받더라도 v6.6의 분기·필드·행 번호는 이 글의 v6.6 원본에서 다시 확인한다.
계측과 검증
관찰할 질문을 먼저 나눈다. tick 정지 자체는 tick-sched.c의 상태와 trace_tick_stop 호출 위치를, scheduler balance 대상 기록은 fair.c의 별도 상태를 확인한다. tracefs event는 빌드·설정에 따라 노출 여부가 다르다.
| 관찰 수단 | 확인할 사실 | 한계 |
|---|---|---|
| timer:tick_stop | success=1은 tick stop, success=0 dependency reason은 NO_HZ_FULL 검사 경로 | 모든 idle 정지 실패를 나타내지 않음; scheduler mask 등록도 직접 증명하지 않음 |
| nohz_full CPU 목록 | full-dynticks 구성 CPU 확인 | idle tick 정지 발생 여부와는 별개 |
| v6.6 상태 필드 | ts->tick_stopped와 rq->nohz_tick_stopped의 서로 다른 갱신 함수 | 두 값의 동일 시점 관찰을 전제하지 않음 |
test -r /sys/kernel/tracing/events/timer/tick_stop/format && cat /sys/kernel/tracing/events/timer/tick_stop/format
test -r /sys/devices/system/cpu/nohz_full && cat /sys/devices/system/cpu/nohz_fullevent가 없으면 설정·tracefs 노출 여부를 확인한다. 위 명령은 tracing을 켜거나 설정을 변경하지 않는다.
추가 확인 사항
can_stop_idle_tick의 어떤 조건 때문에 idle에 들어갔지만 tick은 유지되는가?tick_nohz_next_event에서 RCU·architecture·irq_work 요구가 다음 만료 시각에 어떻게 반영되는가?- 처음
ts->tick_stopped가 1이 된 뒤에도rq->nohz_tick_stopped가 0으로 남는 조건은 무엇인가? - idle 중 IRQ와 idle 이탈을 각각 관찰할 때, tick 재판정과 scheduler mask 정리를 어떤 함수에서 확인해야 하는가?