전체 흐름
Linux v6.6의 CPU load balancing은 하나의 연속 호출 사슬이 아니다. 주기적 tick과 NOHZ 요청은 SCHED_SOFTIRQ에서 처리하고, 새로 idle이 된 CPU는 별도 newidle_balance 경로에서 task를 당길 수 있다. 어느 경로든 실제 이동 전에 domain, source group·rq, affinity와 비용 조건을 통과해야 한다.
sched_domain은 탐색 범위를, lb_env는 목적지·후보 CPU·imbalance·이동 제약을 담는다. 후보를 찾았다는 뜻과 이동했다는 뜻을 분리한다.
balance 시도는 담당 CPU 아님, 후보 group/rq 없음, affinity 제한, 이동 비용 등의 이유로 task를 옮기지 않고 끝날 수 있다.
trigger_load_balance가 기한을 확인↓ SCHED_SOFTIRQrun_rebalance_domains → 일반 domain 순회nohz_idle_balance가 먼저 처리; true면 일반 경로는 실행하지 않음SD_BALANCE_NEWIDLE domain에서 load_balance(..., CPU_NEWLY_IDLE, ...)fair.c:12068–12138, 12194–12232. 각 칸은 가능한 진입 분류이며, 모든 실행에서 세 경로가 차례로 발생한다는 뜻이 아니다.
소스 코드 위치
Linux v6.6에서 대조할 파일
kernel/sched/fair.c — 진입, source 선택, migration 분기
kernel/sched/topology.c — sched domain 구성 확인
kernel/sched/sched.h — sched domain·rq 관련 정의 확인설명: 아래 세 발췌와 행 번호는 Torvalds Linux v6.6 공식 Raw fair.c의 지정 연속 행을 기준으로 한다. 인용 행은 이 고정 버전 원문의 실제 줄바꿈을 기준으로 대조했으며, 다른 사본이나 웹 뷰어가 표시하는 행 번호를 섞지 않는다. topology와 구조체 정의는 v6.6 topology.c, v6.6 sched.h에서 함께 확인한다.
공식 tag: Linux v6.6 kernel/sched/fair.c Raw. 함수명이 비슷한 다른 LTS 버전의 행 번호나 흐름을 이 글에 대입하지 않는다.
후기 버전에서 대응 함수의 이름과 분기가 어떻게 달라졌는지는 v6.18.37 비교 표를 참고한다.
대표 코드
기존 카드의 함수 제목과 첨부 코드 위치가 서로 달랐다. 아래는 실제 v6.6 fair.c의 연속 발췌 세 개이며, 전체 함수나 가능한 분기 전부를 대체하지 않는다.
softirq 진입과 주기적 요청
static __latent_entropy void run_rebalance_domains(struct softirq_action *h)
{
struct rq *this_rq = this_rq();
enum cpu_idle_type idle = this_rq->idle_balance ?
CPU_IDLE : CPU_NOT_IDLE;
/*
* If this CPU has a pending nohz_balance_kick, then do the
* balancing on behalf of the other idle CPUs whose ticks are
* stopped. Do nohz_idle_balance *before* rebalance_domains to
* give the idle CPUs a chance to load balance. Else we may
* load balance only within the local sched_domain hierarchy
* and abort nohz_idle_balance altogether if we pull some load.
*/
if (nohz_idle_balance(this_rq, idle))
return;
/* normal load balance */
update_blocked_averages(this_rq->cpu);
rebalance_domains(this_rq, idle);
}
/*
* Trigger the SCHED_SOFTIRQ if it is time to do periodic load balancing.
*/
void trigger_load_balance(struct rq *rq)
{
/*
* Don't need to rebalance while attached to NULL domain or
* runqueue CPU is not active
*/
if (unlikely(on_null_domain(rq) || !cpu_active(cpu_of(rq))))
return;
if (time_after_eq(jiffies, rq->next_balance))
raise_softirq(SCHED_SOFTIRQ);
nohz_balancer_kick(rq);
}읽는 법: nohz_idle_balance가 true이면 여기서 반환한다. 그 밖에는 blocked average를 갱신하고 일반 domain 순회로 간다. trigger_load_balance는 기한이 된 경우에만 SCHED_SOFTIRQ를 올린다.
source 후보를 찾거나 balanced로 종료
redo:
if (!should_we_balance(&env)) {
*continue_balancing = 0;
goto out_balanced;
}
group = find_busiest_group(&env);
if (!group) {
schedstat_inc(sd->lb_nobusyg[idle]);
goto out_balanced;
}
busiest = find_busiest_queue(&env, group);
if (!busiest) {
schedstat_inc(sd->lb_nobusyq[idle]);
goto out_balanced;
}읽는 법: should_we_balance, find_busiest_group, find_busiest_queue 가운데 어느 단계에서든 이동 없이 out_balanced로 갈 수 있다. group 후보와 rq 후보를 같은 것으로 세지 않는다.
detach 뒤 조건부 attach
more_balance:
rq_lock_irqsave(busiest, &rf);
update_rq_clock(busiest);
/*
* cur_ld_moved - load moved in current iteration
* ld_moved - cumulative load moved across iterations
*/
cur_ld_moved = detach_tasks(&env);
/*
* We've detached some tasks from busiest_rq. Every
* task is masked "TASK_ON_RQ_MIGRATING", so we can safely
* unlock busiest->lock, and we are able to be sure
* that nobody can manipulate the tasks in parallel.
* See task_rq_lock() family for the details.
*/
rq_unlock(busiest, &rf);
if (cur_ld_moved) {
attach_tasks(&env);
ld_moved += cur_ld_moved;
}
local_irq_restore(rf.flags);읽는 법: source rq 잠금 아래에서 detach_tasks를 시도한다. 잠금 해제 뒤 이번 반복에서 실제로 떼어낸 task가 있을 때에만 attach_tasks를 호출한다. 이것은 한 반복의 일부이며 pinned 재시도와 active balancing 등은 뒤쪽 분기에서 다룬다.
함수별 분석
rebalance_domains는 domain별 간격을 확인해 load_balance를 시도한다. newidle_balance는 이 주기적 경로와 별도로 pending wakeup, CPU 활성화, root domain overload, idle 시간 대비 예상 비용과 SD_BALANCE_NEWIDLE을 확인한다. 분기는 아래처럼 서로 다른 종료점을 갖는다.
load_balance의 후보 선택과 조기 종료should_we_balance(&env)continue_balancing=0, balanced 출구find_busiest_group(&env)lb_nobusyg, balanced 출구find_busiest_queue(&env, group)lb_nobusyq, balanced 출구fair.c:10629–10765, 10770–10910, 11051–11139. capacity는 앞선 group·rq 후보 선택에 반영된다. detach의 task 검사와 구분한 주요 관문만 요약하며 group type, NUMA, active balancing과 모든 반환 경로를 열거하지 않는다.
1. domain 방문
주기적 경로의 rebalance_domains는 domain별 interval을 확인한 뒤 기한이 된 곳에서 load_balance를 호출한다 (fair.c:11479–11532). 새로 idle 경로는 비용과 flag를 따로 확인한다 (fair.c:12068–12138).
2. source 선택
find_busiest_group은 통계를 모아 source group을 고른다. v6.6은 misfit, asym packing, imbalanced group을 평균 load 비교와 별도로 다루며, 상황에 따라 균형 상태로 종료한다 (fair.c:10629–10669).
3. 실제 이동
source rq까지 골라도 affinity 등 때문에 이동 가능한 task가 0개일 수 있다. detach_tasks의 이번 반복 반환값이 양수일 때만 attach_tasks가 실행된다 (fair.c:11114–11139).
| 관문 | v6.6에서 확인할 조건 | 결과 |
|---|---|---|
| softirq | nohz_idle_balance가 true | 일반 rebalance_domains 실행 전 반환 |
| new-idle pull | SD_BALANCE_NEWIDLE과 비용 조건 | 통과한 domain에서 CPU_NEWLY_IDLE balance 시도 |
| source 선택 | 담당 CPU, group, rq 후보 | 없으면 task 이동 없이 balanced 출구 |
| migration | cur_ld_moved > 0 | 이번 반복의 detach 목록을 목적지 rq에 attach |
구조체와 필드
아래 객체는 순서대로 생성되는 상태가 아니다. 한 domain 안의 local·source group을 비교하고, lb_env가 목적지 rq와 후보 CPU, 이동량을 담은 채 source 선택과 detach에 전달된다.
sched_domain · 탐색 범위/flag/intervallb_env · 이번 balance 시도의 지역 문맥dst_rq, CPU mask, imbalance, LBF_* 제약; src_rq는 source rq 선택 뒤 채움group을 찾은 뒤에도 source rq가 없거나 task affinity 때문에 이동이 0개일 수 있다. fair.c:8559–8584, 8694–8780, 10629–10765, 11051–11139.
| 객체·필드 | 이번 판단에서 맡는 역할 | 혼동하지 말 것 |
|---|---|---|
sched_domain | 방문할 CPU 범위, balance flag와 interval | domain 방문만으로 task 이동 확정 아님 |
sg_lb_stats / capacity | local과 busiest group 비교, misfit 등 group type 판단 | 평균 load 한 수치만으로 source를 정하지 않음 |
lb_env | 목적지 rq·CPU mask·imbalance·이동 flag; source rq는 선택 뒤 채움 | LBF_ALL_PINNED 등은 영구 task 상태가 아님 |
rq / task | CPU별 runnable 후보와 detach·attach 대상 | source rq 선택과 실제 migration은 별개 |
실행 단계와 결과
이동 경로는 source rq와 destination rq를 동시에 한 잠금 아래에서 바꾸는 단일 단계가 아니다. v6.6 코드는 source 쪽에서 task를 detach한 뒤 잠금을 풀고, 이번 반복에서 떼어낸 task가 있는 경우 destination에 attach한다.
rq_lock_irqsavecur_ld_moved = detach_tasksattach_tasks 호출 없음; 이후 재시도는 별도 조건fair.c:11114–11139. task가 이동하지 않는 출구는 모두 오류 복구가 아니다. pinned task 재탐색과 active balancing은 이 축약 그림 밖에 있다.
| 이번 시도 결과 | 조건 예시 | 관찰할 것 |
|---|---|---|
| 일반 경로 생략 | NOHZ 선행 처리에서 true 반환 | 해당 softirq의 일반 domain 순회가 실행되지 않음 |
| 후보 없음 | 담당 CPU·group·rq 관문 중단 | lb_nobusyg / lb_nobusyq 등 조건별 통계 |
| 0개 detach | affinity, hotness 또는 대상 제약 | attach 없음; pinned·재시도 분기 별도 확인 |
| 양수 detach | 옮길 수 있는 task 발견 | 목적지 rq attach와 누적 이동량 |
불변 조건과 놓치기 쉬운 분기
주기적·NOHZ softirq 처리와 newidle_balance를 매번 순서대로 실행되는 하나의 사슬로 그리지 않는다.
misfit task, asym packing, imbalanced group, energy-aware 조건 때문에 단순 평균 load 비교만으로 source를 설명할 수 없다.
source rq를 찾은 뒤에도 이동 가능한 task가 전부 제한될 수 있다. LBF_ALL_PINNED 처리와 다른 destination 재시도는 원문 분기를 따른다.
source rq 잠금 해제 뒤 cur_ld_moved가 양수인 반복에만 attach_tasks가 호출된다. 후보를 세는 통계와 실제 migration을 혼동하지 않는다.
계측과 검증
실제 이동 사건과 balance 내부 판단은 다른 관측값이다. 사용 가능한 tracepoint·schedstat·디버그 인터페이스는 커널 설정에 따라 확인해야 한다.
| 관측 수단 | 알 수 있는 것 | 한계 |
|---|---|---|
sched:sched_migrate_task | task가 CPU를 옮긴 사건 | 후보 없음·0개 detach 분기 자체는 보여주지 않음 |
perf sched map | CPU별 실행·스위치 이력 | group/imbalance 내부 판단을 직접 표시하지 않음 |
schedstat의 lb_count, lb_nobusyg, lb_nobusyq, lb_failed | 시도·후보 없음·이동 실패를 구분 | CONFIG_SCHEDSTATS와 노출 방식 확인 필요 |
/proc/sched_debug | 구성에서 제공할 때 rq/domain 상태 | CONFIG_SCHED_DEBUG 등 설정에 좌우됨 |
cd /sys/kernel/tracing
grep '^sched:sched_migrate_task$' available_events
cat events/sched/sched_migrate_task/formattracefs가 마운트되고 해당 event가 제공될 때의 읽기 전용 확인이다. event가 없어도 balance 자체가 없었다는 증거는 아니다.
추가 확인 사항
- 이 CPU의
sched_domaininterval이 도래했는가, 아니면 new-idle 경로가 별도로 시작했는가? find_busiest_group이 후보를 고르거나 버린 이유는 load, capacity, misfit, energy 조건 중 무엇인가?- source rq를 골랐어도
detach_tasks가 0개를 반환했다면 affinity와 pinned 재시도 분기는 어디로 갔는가?