Scheduler · Linux v6.6 · source + diagram note

NOHZ

idle 진입, tick 정지, scheduler의 NOHZ idle balance 기록을 v6.6 코드에서 분리해 봅니다.

전체 흐름

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 정지 판단에는 별도 의존성 검사와 갱신 경로가 있다.

그림 1. v6.6 idle tick의 상태 경계
① idle 시작tick_nohz_idle_enterts->inidle = 1과 idle 시간 측정 시작
② 정지 판단tick_nohz_idle_stop_tick정지 가능 조건과 다음 타이머 만료를 확인
③ 두 결과가까운 이벤트·의존성 → 새 stop 보류처음 실제 정지 → ts->tick_stopped = 1
④ 첫 정지에서만nohz_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행. 왼쪽 번호는 위 발췌문의 줄 번호이며 고정 원문과 대조한 행만 설명한다.

01void tick_nohz_idle_enter(void)

현재 CPU가 idle loop에 들어갈 때 호출하는 준비 함수다. 이 함수 자체는 주기 tick의 정지를 결정하지 않는다.

03 struct tick_sched *ts;

현재 CPU의 tick 상태를 가리킬 포인터를 선언한다. 실제 주소는 아래의 per-CPU 조회에서 얻는다.

05 lockdep_assert_irqs_enabled();

호출 시점에 로컬 인터럽트가 허용돼 있어야 한다는 조건을 확인한다. 끝의 재허용과 연결된 진입 전제다.

07 local_irq_disable();

현재 CPU의 인터럽트를 잠시 막고 tick 상태를 변경하는 구간에 들어간다.

09 ts = this_cpu_ptr(&tick_cpu_sched);

per-CPU tick_cpu_sched에서 현재 CPU의 tick_sched 주소를 얻는다. 모든 CPU가 한 ts를 공유하는 것은 아니다.

11 WARN_ON_ONCE(ts->timer_expires_base);

timer_expires_base가 남아 있으면 한 번 경고한다. 경고 자체가 값을 지우거나 tick을 멈추지는 않는다.

13 ts->inidle = 1;

이 CPU의 inidle 상태를 1로 기록한다. v6.6은 필드에 직접 대입하며, 이 값만으로 tick 정지를 뜻하지 않는다.

14 tick_nohz_start_idle(ts);

idle 진입 시각과 계측 상태를 기록한다. 이 호출만으로 주기 tick을 끄지는 않는다.

16 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으로 만료를 당길 수 있다.

그림 2. tick 유지와 정지를 가르는 조건
정지 후보can_stop_idle_tickonline, NOHZ mode, reschedule·softirq, timekeeping 담당 여부 등 확인
다음 이벤트tick_nohz_next_eventRCU·arch·irq_work 요구 및 timer 만료를 반영
가까운 이벤트 또는 정지 불가새 정지는 생략될 수 있음; 이미 멈춘 tick은 다음 이벤트에 맞춰 재조정될 수 있음
유효한 먼 만료 시각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_enterinidle·idle 시간 시작이 시점만으로 tick_stopped는 미확정
can_stop_idle_tick / tick_nohz_next_event즉시 tick 요구와 다음 만료 시각새 정지를 생략하거나 기존 정지 상태를 재조정
tick_nohz_idle_stop_tick정지 시도 전후의 tick_stopped최초 stopped 전환에만 scheduler 호출
nohz_balance_enter_idleactive, housekeeping, domain 검사통과 시 scheduler mask·load 상태 갱신

구조체와 필드

동일한 “idle”이라는 이름이 시간 계층, scheduler 계층, RCU 계층에서 다른 사실을 뜻한다. 상태의 소유자와 갱신 함수를 함께 읽는다.

그림 3. 시간 상태와 scheduler 상태의 분리
시간 계층 · CPU별 tick_schedinidle = idle 진입 기록tick_stopped = 주기 tick 정지 상태idle_active = idle 시간 측정 상태
scheduler 계층 · rq + nohzrq->nohz_tick_stoppednohz.idle_cpus_masknohz.has_blocked·needs_update
첫 실제 정지에서만 →tick-sched.c의 !was_stopped && ts->tick_stopped가 fair.c의 기록 함수를 호출한다. 조건부 반환 때문에 두 상태를 항상 동일시할 수 없다.
RCU는 별도 진행 조건rcu_needs_cpu()는 다음 tick 판정에 들어오지만, scheduler의 idle CPU mask 자체가 RCU quiescent state를 뜻하지 않는다.

시간 계층의 첫 정지 전환이 scheduler 계층에 전달되는 조건을 구분한 그림이다. 공식 v6.6 tick-sched.c:1135–1137, fair.c:11803–11858.

필드·상태소유/뜻갱신 또는 확인 근거
ts->inidletick 계층의 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_stoppedscheduler가 기억하는 NOHZ idle 상태nohz_balance_enter_idle에서 조건부 1, nohz_balance_exit_idle에서 0
nohz.idle_cpus_maskidle balance에서 고려할 CPU 집합scheduler enter/exit에서 조건부 set/clear
nohz.has_blockedblocked load 재검사 요청nohz_balance_enter_idle의 마지막 갱신

실행 단계와 상태 변화

idle 도중에도 IRQ와 timer 변화가 들어올 수 있다. 멈춘 tick의 다음 이벤트를 다시 계산하는 경로와 CPU가 idle을 완전히 벗어나는 경로를 나눠 본다.

그림 4. idle 체류와 복귀의 조건부 타임라인
idle 진입inidle=1; idle 시간 측정
tick 판단조건 통과 시 다음 만료를 설정하고 stop 시도; 그 외 새 stop 보류
idle 중 IRQIRQ exit에서 idle 상태 갱신; 이후 idle loop의 stop 경로에서 다음 tick event 재판정 가능
idle 이탈inidle=0; 조건부 idle accounting 종료·tick 갱신/재시작
scheduler 쪽 경로최초 stop 시 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=1scheduler mask 등록은 추가 조건을 통과할 때만
idle 이탈tick_nohz_idle_exit가 inidle=0; 필요한 경우 tick 재시작scheduler 쪽 mask 해제와 동의어가 아님
관찰 기준: inidle ≠ tick_stopped ≠ rq->nohz_tick_stopped.

불변 조건과 실패 사례

idle과 tick stop 분리

inidle=1만으로 tickless를 선언하지 않는다. 재스케줄 요구·pending softirq·timekeeping 역할·가까운 timer 만료는 새 정지를 막거나 다음 이벤트 재조정을 요구할 수 있다.

시간 계측 보존

tick이 멈춰도 idle 시간 계측이 사라지는 것은 아니다. tick_nohz_idle_exit와 tick_nohz_account_idle_time의 조건부 경로를 확인한다.

scheduler 대상의 조건부 기록

첫 stop 뒤에도 비활성 CPU·housekeeping 제외·null domain이면 idle balance mask 등록이 생략될 수 있다. mask 부재만으로 tick이 계속 뛰었다고 해석하지 않는다.

RCU와 NO_HZ_FULL의 경계

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_stopsuccess=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_full

event가 없으면 설정·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 정리를 어떤 함수에서 확인해야 하는가?