Scheduler · Linux v6.6 · source + diagram note

sched_init

가능한 CPU의 runqueue 구성요소와 부트 CPU idle 기반, 이후 SMP scheduling domain 준비 시점을 분리해 읽습니다.

전체 흐름

start_kernel()은 타이머 인터럽트와 IRQ 초기화에 앞서 sched_init()을 호출해 scheduler의 초기 자료구조를 마련한다. 이 함수는 가능한 CPU의 runqueue 구성요소를 초기화하고 부트 CPU의 current를 idle task로 설정한다. SMP domain·NUMA를 포함한 후기 설정은 훨씬 뒤의 smp_init() 다음 sched_init_smp()에서 한다.

초기화와 토폴로지 분리

sched_init() 뒤 scheduler가 동작할 기초는 있지만, 이 시점에 SMP scheduling domain이 전부 설정됐다는 뜻은 아니다.

부분 필드 초기화

for_each_possible_cpu는 각 rq의 지정 필드·class별 queue를 설정한다. 구조체 전체를 memset(0) 한다고 읽으면 안 된다.

그림 1. 부팅 중 scheduler 초기화의 두 시점
① sched_init()start_kernel() 초기, IRQ·timer보다 앞
② 다른 초기화IRQ, timer, timekeeping 등
③ smp_init()CPU bring-up 시점
④ sched_init_smp()SMP 빌드에서 domain·NUMA 등 후기 설정

init/main.c:935–946, 1539–1542. ④의 상세 작업은 CONFIG_SMP에 따라 달라진다. 비-SMP 구현은 granularity 설정만 한다.

소스 코드 위치

Linux v6.6에서 대조할 파일

init/main.c — sched_init과 후기 sched_init_smp 호출 순서
kernel/sched/core.c — sched_init, init_idle, sched_init_smp 구현
kernel/sched/fair.c — init_cfs_rq와 fair class 세부 구현

아래 발췌는 사이트가 보존한 v6.6 core.c의 표기된 연속 행이며, 호출 순서는 보존 init/main.c에서 확인했다. core.c 파일 전체가 공식 v6.6 원본과 바이트 단위로 같은지는 입증되지 않았으므로 공식 태그도 함께 대조한다.

공식 태그: core.c · init/main.c · fair.c

대표 코드

기존 페이지의 첫 코드 박스는 sched_init_smp(), 둘째는 __sched_fork()에서 시작해 제목과 다른 함수를 보여 주었다. 아래는 함수 시작과 필요한 구간을 나눠 놓은 보존 소스 발췌다. 생략된 중간 분기는 이어지는 원문에서 확인한다.

sched_init(): class 배치 검사

보존 core.c 9909–9922행
void __init sched_init(void)
{
	unsigned long ptr = 0;
	int i;

	/* Make sure the linker didn't screw up */
	BUG_ON(&idle_sched_class != &fair_sched_class + 1 ||
	       &fair_sched_class != &rt_sched_class + 1 ||
	       &rt_sched_class   != &dl_sched_class + 1);
#ifdef CONFIG_SMP
	BUG_ON(&dl_sched_class != &stop_sched_class + 1);
#endif

	wait_bit_init();

BUG_ON은 linker가 배치한 scheduling class 순서를 검증한다. init_idle()이 전체 class 순서를 만든다는 설명은 맞지 않는다.

sched_init(): 가능한 CPU의 rq 구성요소

보존 core.c 9973–9985행
	for_each_possible_cpu(i) {
		struct rq *rq;

		rq = cpu_rq(i);
		raw_spin_lock_init(&rq->__lock);
		rq->nr_running = 0;
		rq->calc_load_active = 0;
		rq->calc_load_update = jiffies + LOAD_FREQ;
		init_cfs_rq(&rq->cfs);
		init_rt_rq(&rq->rt);
		init_dl_rq(&rq->dl);
#ifdef CONFIG_FAIR_GROUP_SCHED
		INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);

각 possible CPU에서 lock과 nr_running, CFS·RT·DL queue 등을 초기화한다. 이 발췌는 루프 앞부분이며 CONFIG_FAIR_GROUP_SCHED, CONFIG_SMP 등의 뒤쪽 분기까지 대표하지 않는다.

sched_init(): 부트 idle와 완료 표시

보존 core.c 10075–10098행
	/*
	 * Make us the idle thread. Technically, schedule() should not be
	 * called from this thread, however somewhere below it might be,
	 * but because we are the idle thread, we just pick up running again
	 * when this runqueue becomes "idle".
	 */
	init_idle(current, smp_processor_id());

	calc_load_update = jiffies + LOAD_FREQ;

#ifdef CONFIG_SMP
	idle_thread_set_boot_cpu();
	balance_push_set(smp_processor_id(), false);
#endif
	init_sched_fair_class();

	psi_init();

	init_uclamp();

	preempt_dynamic_init();

	scheduler_running = 1;
}

init_idle(current, smp_processor_id())는 부트 CPU의 current를 idle task로 만드는 호출이다. 이어 fair class·PSI·uclamp·dynamic preempt를 준비한 뒤 scheduler_running=1을 쓴다. 이 값만으로 후기 SMP domain 완료를 뜻하지 않는다.

init_idle(): 인자와 초기 상태

보존 core.c 9250–9267행
void __init init_idle(struct task_struct *idle, int cpu)
{
#ifdef CONFIG_SMP
	struct affinity_context ac = (struct affinity_context) {
		.new_mask  = cpumask_of(cpu),
		.flags     = 0,
	};
#endif
	struct rq *rq = cpu_rq(cpu);
	unsigned long flags;

	__sched_fork(0, idle);

	raw_spin_lock_irqsave(&idle->pi_lock, flags);
	raw_spin_rq_lock(rq);

	idle->__state = TASK_RUNNING;
	idle->se.exec_start = sched_clock();

대상 idle task와 CPU의 rq를 받아 __sched_fork() 기본값을 적용하고 task/rq 잠금으로 진입한다. __sched_fork() 자체가 init_idle()의 정의는 아니다.

init_idle(): rq 연결과 idle class

보존 core.c 9294–9319행
	rcu_read_lock();
	__set_task_cpu(idle, cpu);
	rcu_read_unlock();

	rq->idle = idle;
	rcu_assign_pointer(rq->curr, idle);
	idle->on_rq = TASK_ON_RQ_QUEUED;
#ifdef CONFIG_SMP
	idle->on_cpu = 1;
#endif
	raw_spin_rq_unlock(rq);
	raw_spin_unlock_irqrestore(&idle->pi_lock, flags);

	/* Set the preempt count _outside_ the spinlocks! */
	init_idle_preempt_count(idle, cpu);

	/*
	 * The idle tasks have their own, simple scheduling class:
	 */
	idle->sched_class = &idle_sched_class;
	ftrace_graph_init_idle_task(idle, cpu);
	vtime_init_idle(idle, cpu);
#ifdef CONFIG_SMP
	sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu);
#endif
}

rq->idle, rq->curr, idle->on_rq를 설정한다. idle_sched_class 대입은 잠금 해제와 preempt-count 설정 뒤에 나온다. CPU affinity와 on_cpu, comm 관련 일부 행은 SMP 조건부다.

후기 sched_init_smp()의 조건부 구현

보존 core.c 9851–9888행
void __init sched_init_smp(void)
{
	sched_init_numa(NUMA_NO_NODE);

	/*
	 * There's no userspace yet to cause hotplug operations; hence all the
	 * CPU masks are stable and all blatant races in the below code cannot
	 * happen.
	 */
	mutex_lock(&sched_domains_mutex);
	sched_init_domains(cpu_active_mask);
	mutex_unlock(&sched_domains_mutex);

	/* Move init over to a non-isolated CPU */
	if (set_cpus_allowed_ptr(current, housekeeping_cpumask(HK_TYPE_DOMAIN)) < 0)
		BUG();
	current->flags &= ~PF_NO_SETAFFINITY;
	sched_init_granularity();

	init_sched_rt_class();
	init_sched_dl_class();

	sched_smp_initialized = true;
}

static int __init migration_init(void)
{
	sched_cpu_starting(smp_processor_id());
	return 0;
}
early_initcall(migration_init);

#else
void __init sched_init_smp(void)
{
	sched_init_granularity();
}
#endif /* CONFIG_SMP */

CONFIG_SMP 분기는 NUMA/domain, class 후기 설정과 sched_smp_initialized를 다룬다. #else에서는 granularity만 설정한다. sched_init() 초기 경로와 합치지 않는다.

함수별 분석

sched_init()은 전체 rq를 무조건 0으로 지우는 함수가 아니다. class 배치와 조건부 root task group을 처리하고, CPU별 queue 필드를 초기화한 다음 부트 CPU idle task를 연결한다. 후기 토폴로지는 별도 호출에서 완성된다.

그림 2. 초기 sched_init() 안에서 확인되는 네 관문
class 배치 검증BUG_ON으로 linker 순서 검사
조건부 그룹 준비FAIR/RT group, CGROUP 설정에 따른 root 자료구조
possible CPU마다 rq 설정lock, CFS·RT·DL, SMP 필드 등 선택적으로 초기화
부트 CPU idle와 완료 표시init_idle(current,...) 후 fair/PSI/uclamp, scheduler_running=1

core.c:9909–10098. 이는 조건부 코드를 포함한 주요 구간의 요약이며 함수 내부의 모든 필드를 열거하지 않는다.

1. class와 그룹

BUG_ON은 class 객체 주소의 linker 순서를 확인한다. CONFIG_FAIR_GROUP_SCHED, CONFIG_RT_GROUP_SCHED, CONFIG_CGROUP_SCHED는 root task group 코드를 바꾼다. 조건부 배열용 kzalloc(..., GFP_NOWAIT) 결과는 바로 검사하지 않으므로 할당 성공을 전제하지 않는다.

2. CPU별 rq

for_each_possible_cpu는 class별 rq를 초기화한다. SMP 분기는 rq->sd/rd에 먼저 NULL을 대입하고 같은 루프에서 rq_attach_root(rq, &def_root_domain)을 호출한다. 이 초기 root domain 연결과 후기 scheduling domain 구성은 구분한다.

3. 부트 idle와 후기 설정

초기 함수는 현재 부트 CPU의 idle 기준을 세운다. 나중에 smp_init() 뒤 실행되는 sched_init_smp()는 SMP 빌드에서 domain/NUMA를 설정한다. 비-SMP 분기는 같은 작업을 하는 축약판이 아니다.

위치코드에서 확인할 일범위 제한
sched_init 시작class 주소 순서 BUG_ONinit_idle의 class 순서 생성 아님
CPU별 루프특정 rq 필드와 class별 queue 초기화전체 rq memset 아님
부트 idleinit_idle(current,...)모든 CPU의 idle task 생성 아님
후기 SMPsched_init_smp 조건부 domain 설정초기 sched_init과 별도 시점

구조체와 필드

root task group, CPU별 rq, 부트 CPU idle task를 한 줄의 생성 체인으로 보지 않는다. 설정별로 존재하는 root 자료구조와 rq 구성요소를 먼저 분리해야 한다.

그림 3. sched_init()이 만지는 세 층
root task groupFAIR/RT/CGROUP 설정에 따른 필드와 리스트
CPU별 rqCFS·RT·DL queue, lock, SMP 상태 등
부트 current현재 부트 CPU의 idle task로 rq에 연결
CONFIG_SMP 루프: rq->sd/rd = NULL 대입 → rq_attach_root(...) 호출. 전체 scheduling domain 구성은 후기 sched_init_smp()에서 다룬다.

core.c:9930–9971, 9973–10081. 그림의 세 층은 동일한 객체가 아니며 모두 같은 방식으로 할당되거나 공개되는 상태도 아니다.

대상초기화에서 맡는 역할설정 또는 시점 제한
class 객체 주소BUG_ON으로 linker 배치 확인CONFIG_SMP에서 stop class 검사 추가
root_task_groupgroup scheduler용 root 필드·리스트FAIR/RT/CGROUP 빌드 옵션별
rqCPU별 실행 대기 구조와 clock·balance 필드SMP/NO_HZ/SCHED_CORE 필드 조건부
init_task/current부트 CPU idle로 연결되는 task모든 CPU의 idle task와 같지 않음

실행 단계와 상태 변화

init_idle()은 task 기본값부터 rq 연결과 idle class 지정까지 한 함수에서 다룬다. idle class 지정이 rq 연결보다 먼저라고 단정하지 말고, 소스에서 저장 위치와 잠금 경계를 확인한다.

그림 4. init_idle()의 부트 CPU task 연결
① __sched_forkidle task의 scheduler 필드 기본값
② task/CPU 맥락상태·per-CPU kthread flag, CPU 지정
③ rq 연결rq->idle, rq->curr, on_rq 설정
④ idle class잠금 해제와 preempt count 뒤 idle_sched_class 지정

core.c:9250–9319. 일부 CPU affinity·on_cpu·이름 설정은 CONFIG_SMP 조건부다. 부트 CPU 호출은 core.c:10081이다.

단계보존 소스에서 확인되는 변화넘겨짚지 말 것
class 검사linker 주소 배치 검증runtime class 객체를 여기서 생성한다는 뜻 아님
rq 루프possible CPU의 지정 필드·queue 초기화전체 rq zeroing 아님
부트 idlerq->idle/curr와 idle class 설정모든 CPU idle task 완료 아님
scheduler_running=1초기 함수의 끝 표시SMP domain·NUMA 후기 설정 완료 아님
초기 scheduler 기반 준비 ≠ 모든 CPU idle 완료 ≠ SMP domain 설정 완료

불변 조건과 놓치기 쉬운 분기

class 순서의 출처

sched_init()의 BUG_ON은 linker class 배치를 검사한다. init_idle()은 한 task에 idle_sched_class를 지정한다.

rq 필드의 범위

루프는 가능한 CPU마다 특정 queue와 상태를 초기화한다. 설명에 전체 rq 메모리 zeroing을 끼워 넣지 않는다.

컴파일 조건

root group, SMP, NO_HZ, SCHED_CORE 관련 필드와 함수 경로는 각 CONFIG_* 조건 아래에서만 성립한다.

후기 토폴로지

scheduler_running=1 이후에도 smp_init()와 sched_init_smp()가 남는다. 초기 함수의 완료를 full topology와 동일시하지 않는다.

계측과 검증

정적 코드에서 확인한 초기화 순서와 부팅 뒤 관찰 가능한 scheduler 상태를 분리한다. sched_init()은 initcall이 아닌 직접 호출이며, v6.6 init/main.c에서 ftrace_init()·early_trace_init()(930·933행)는 이 함수보다 앞서지만, 일반 trace event가 사용 가능해지는 trace_init()(964행)는 뒤에 나온다.

자료확인 가능한 것한계
init/main.c, core.c호출 순서와 CONFIG별 코드 분기특정 보드의 실행 성공 증명은 아님
커널 빌드 설정SMP, group scheduler, NO_HZ 선택실제 runtime field 값은 별도 확인
/proc/sched_debug제공되는 빌드라면 부팅 뒤 rq/domain 상태초기 함수 내부 단계 기록 아님
sched_switch event사용 가능할 때 이후 context switchftrace:sched_init 기본 event나 초기 단계 증거 아님
부팅 뒤 읽기 전용 점검 예:
grep -E 'CONFIG_(SMP|FAIR_GROUP_SCHED|RT_GROUP_SCHED|CGROUP_SCHED)=' /boot/config-$(uname -r)
test -r /proc/sched_debug && head -n 30 /proc/sched_debug

배포판의 config 경로와 /proc/sched_debug 제공 여부는 다를 수 있다.

추가 확인 사항

  • 현재 커널 빌드에서 root task group과 SMP/NO_HZ/SCHED_CORE 조건부 코드는 각각 포함되는가?
  • 부트 CPU의 current가 rq->idle과 rq->curr에 연결되는 정확한 지점은 어디인가?
  • 해당 아키텍처에서 다른 CPU의 idle task 준비와 후기 scheduling domain 연결은 어느 경로에서 이루어지는가?
  • 부팅 후의 rq/domain 출력과 초기 소스 순서를 혼동하지 않으려면 어떤 별도 관측이 필요한가?