이 코드는 어떤 문제를 푸나요?
스케줄러는 여러 작업 중 하나를 고르지만, 실행할 일반 작업이 하나도 없는 경우도 표현해야 합니다. CPU마다 idle task가 있는 이유입니다. sched_init 전체의 자료구조 초기화를 읽기 전에 init_idle 한 함수를 통해 task와 CPU의 실행 큐가 어떻게 연결되는지 살펴보겠습니다.
읽을 범위: v6.6 · kernel/sched/core.c · init_idle 9250–9319행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
runqueue
CPU별 실행 상태와 실행 가능한 작업을 관리하는 자료구조입니다. 단순한 하나의 연결 리스트가 아니라 스케줄링 클래스별 정보를 포함합니다.
idle task
해당 CPU가 다른 작업을 실행할 수 없을 때 선택하는 특별한 task입니다. 일반 사용자 task와 같은 정책으로 경쟁하도록 만들어 놓지 않습니다.
현재 task와 idle task
rq->curr는 현재 실행 task, rq->idle은 해당 CPU의 기본 idle task를 가리킵니다. 초기 idle 설정에서는 둘 다 같은 idle을 가리키게 합니다. 이 v6.6 함수에는 rq->donor 설정이 없습니다.
처음 읽을 때
CPU 0과 CPU 1의 rq를 각각 그리고 각자의 idle task를 연결해 보세요. 아무 작업이 없다는 상태도 NULL 대신 실행 가능한 특별한 task로 표현합니다.
더 깊이 살펴볼 때
CONFIG_SMP 조건부 컴파일과 pi_lock·rq 잠금·RCU 게시의 역할을 구분해 보세요. __sched_fork가 idle에 필요한 스케줄링 필드를 초기화하는 위치도 최신 버전과 직접 비교할 수 있습니다.
그림으로 보는 변화

1. CPU별 실행 기반 선택
cpu_rq(cpu), cpumask_of(cpu)
화살표는 초기화 순서입니다. CPU를 이동시키는 동작이 아니라 대상 CPU의 자료를 선택합니다.
2. idle task를 CPU에 연결
TASK_RUNNING, CPU affinity, __set_task_cpu
idle은 지정된 CPU에 머무는 특별한 task로 준비됩니다.
3. 실행 큐의 기본 task로 등록
rq->idle / rq->curr
연결 화살표는 포인터 참조를 뜻합니다. v6.6의 이 초기 상태에서는 두 포인터가 같은 idle task를 가리킵니다.
init_idle를 한 줄씩 읽기
줄 번호는 v6.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.
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();
/*
* PF_KTHREAD should already be set at this point; regardless, make it
* look like a proper per-CPU kthread.
*/
idle->flags |= PF_KTHREAD | PF_NO_SETAFFINITY;
kthread_set_per_cpu(idle, cpu);
#ifdef CONFIG_SMP
/*
* It's possible that init_idle() gets called multiple times on a task,
* in that case do_set_cpus_allowed() will not do the right thing.
*
* And since this is boot we can forgo the serialization.
*/
set_cpus_allowed_common(idle, &ac);
#endif
/*
* We're having a chicken and egg problem, even though we are
* holding rq->lock, the CPU isn't yet set to this CPU so the
* lockdep check in task_group() will fail.
*
* Similar case to sched_fork(). / Alternatively we could
* use task_rq_lock() here and obtain the other rq->lock.
*
* Silence PROVE_RCU
*/
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
}void __init init_idle(struct task_struct *idle, int cpu)이미 준비된 idle task와 대상 CPU 번호를 받아 둘을 연결합니다. 이 함수 자체가 새 task_struct를 할당하는 것은 아닙니다.
#ifdef CONFIG_SMP다중 CPU 지원 CONFIG_SMP가 켜진 빌드에서만 다음 affinity 설정 인자를 만듭니다. 실행 중 CPU 개수를 비교하는 if문이 아닙니다.
struct affinity_context ac = (struct affinity_context) {CPU 허용 범위를 설정할 affinity_context 구조체를 초기화합니다. 다음 두 줄은 함수 호출이 아니라 구조체 필드 값입니다.
.new_mask = cpumask_of(cpu),대상 CPU 하나만 포함하는 마스크를 지정합니다. idle task를 임의의 CPU로 이동시키지 않으려는 설정입니다.
.flags = 0,이번 affinity 갱신에 추가 동작 플래그를 지정하지 않습니다.
#endifSMP 빌드에만 필요한 affinity 인자 선언 부분이 끝납니다.
struct rq *rq = cpu_rq(cpu);대상 CPU의 runqueue를 찾습니다. 이후 초기 상태를 기록할 곳입니다.
unsigned long flags;잠금 전의 인터럽트 허용 상태를 보관할 변수를 마련합니다.
__sched_fork(0, idle);idle task의 스케줄링 entity와 실행 큐 관련 초기 필드를 준비합니다. 함수 이름에 fork가 있지만 여기서 새로운 task를 복제하는 시스템 호출은 아닙니다.
raw_spin_lock_irqsave(&idle->pi_lock, flags);idle task의 pi_lock을 잡으면서 로컬 인터럽트 상태를 저장하고 차단합니다. task 관련 상태를 일관되게 바꾸기 위한 시작입니다.
raw_spin_rq_lock(rq);대상 runqueue 잠금도 잡아 task와 rq의 연결을 보호합니다.
idle->__state = TASK_RUNNING;idle task를 스케줄러가 실행 가능한 상태로 표시합니다. 실제 유용한 일을 하고 있다는 뜻은 아닙니다.
idle->se.exec_start = sched_clock();실행 시간 계상의 기준 시작 시각을 넣습니다. 이후 경과 시간을 계산할 출발점입니다.
idle->flags |= PF_KTHREAD | PF_NO_SETAFFINITY;커널 스레드임을 표시하고 일반적인 affinity 변경을 금지합니다. 기존 flags를 지우지 않도록 OR 대입을 사용합니다.
kthread_set_per_cpu(idle, cpu);커널 스레드 관리 정보에서도 CPU 전용 task로 등록합니다.
#ifdef CONFIG_SMP다중 CPU 지원이 있는 빌드에서만 CPU 허용 범위 설정 코드를 포함합니다.
set_cpus_allowed_common(idle, &ac);대상 CPU 하나를 지정한 마스크를 적용합니다. 부팅 시점이며 idle 초기화가 같은 task에 다시 호출될 수 있다는 조건을 고려해 공통 설정 함수를 사용합니다.
#endifSMP 전용 CPU 허용 범위 설정이 끝납니다.
rcu_read_lock();다음 CPU 연결 갱신에서 요구되는 RCU 읽기 영역에 들어갑니다. 소스 주석은 부팅 초기 task_group 검사의 상황도 설명합니다.
__set_task_cpu(idle, cpu);idle task가 속한 CPU 번호를 실제로 설정합니다. 단지 허용 마스크를 바꾸는 앞 호출과 구분됩니다.
rcu_read_unlock();CPU 연결 갱신에 사용한 RCU 읽기 영역을 끝냅니다.
rq->idle = idle;이 rq의 특별한 idle task 포인터를 기록합니다.
rcu_assign_pointer(rq->curr, idle);현재 실행 task 포인터를 RCU 방식으로 게시합니다. 이후 읽는 코드가 일관된 포인터를 보도록 하는 표기입니다.
idle->on_rq = TASK_ON_RQ_QUEUED;idle을 rq에 등록된 상태로 표시합니다. 일반 fair task의 목록 삽입을 호출한다는 뜻은 아닙니다.
#ifdef CONFIG_SMP다중 CPU 지원 빌드에서만 다음 on_cpu 필드를 설정합니다.
idle->on_cpu = 1;이 초기화 단계에서 CPU 위의 task임을 표시합니다.
#endifSMP 전용 실행 CPU 표시가 끝납니다.
raw_spin_rq_unlock(rq);runqueue 상태 갱신이 끝났으므로 rq 잠금을 풉니다.
raw_spin_unlock_irqrestore(&idle->pi_lock, flags);task 잠금을 풀고 저장했던 인터럽트 상태를 복원합니다. 무조건 인터럽트를 켜는 것과 다릅니다.
init_idle_preempt_count(idle, cpu);잠금 밖에서 idle의 초기 선점 카운트를 설정합니다. 소스가 요구한 순서를 그대로 지킵니다.
idle->sched_class = &idle_sched_class;일반 fair나 RT 정책 대신 idle 전용 스케줄링 클래스를 연결합니다.
ftrace_graph_init_idle_task(idle, cpu);idle task의 함수 그래프 추적 상태를 준비합니다. 추적 설정에 따라 실제 작업이 달라질 수 있습니다.
vtime_init_idle(idle, cpu);idle에 대한 가상 CPU 시간 계상 상태를 초기화합니다.
#ifdef CONFIG_SMPSMP 빌드에서 CPU별로 구별되는 idle 이름을 만듭니다.
sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu);CPU 번호가 포함된 이름을 넣어 로그나 task 목록에서 구분할 수 있게 합니다.
#endifCPU 번호를 붙이는 조건부 컴파일 부분이 끝납니다.
함께 생각해 볼 질문
idle task가 TASK_RUNNING이면 바쁘게 일한다는 뜻인가요?
TASK_RUNNING은 실행 가능 상태를 뜻합니다. idle은 스케줄러가 선택할 수 있는 특별한 task이며 실제 대기 방법은 idle 경로가 결정합니다.
각 CPU가 하나의 idle task를 같이 쓰면 안 되나요?
각 CPU의 실행 문맥과 CPU별 상태를 별도로 유지해야 합니다. idle task도 해당 CPU에 연결됩니다.
잠금을 풀기 전에 preempt count를 초기화하지 않는 이유는 무엇인가요?
소스는 잠금 바깥에서 설정하도록 명시합니다. 잠금 획득·해제가 사용하는 선점 상태와 초기 idle 선점 상태 설정이 충돌하지 않도록 순서를 지킵니다.
출처와 읽은 범위
Linux stable v6.6 · kernel/sched/core.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고
