CPU Hotplug

리눅스 커널의 CPU 온라인/오프라인 메커니즘을 상태 머신부터 아키텍처별 구현, 서브시스템 연동, 실전 활용까지 심층 분석합니다.

전제 조건: 스케줄러(Scheduler), 인터럽트(Interrupt), 전원 관리(Power Management) 문서를 먼저 읽으세요. CPU Hotplug은 CPU 상태 전이만의 문제가 아니라 인터럽트 이동, 타이머(Timer) 이동, 스케줄러 도메인 재구성까지 함께 이해해야 실전에서 해석할 수 있습니다.
일상 비유: CPU Hotplug은 영업 중인 매장의 근무자 재배치(Relocation)와 비슷합니다. 한 직원을 퇴근시키기 전에 담당 손님과 작업을 다른 직원에게 넘기고, 다시 출근시킬 때는 출입 등록과 역할 배정을 순서대로 마쳐야 합니다. 커널도 CPU를 끄고 켤 때 인터럽트, 타이머, workqueue, 스케줄러 상태를 같은 방식으로 정리합니다.
논리 CPU(Logical CPU)란? Linux 커널이 CPU N이라 부르는 단위는 논리 CPU입니다. 물리적으로는 소켓(Socket) 안의 코어(Core), SMT(Hyper-Threading) 환경에서는 각 하드웨어 스레드(HW Thread)가 독립된 논리 CPU로 매핑(Mapping)됩니다. 예를 들어 물리 4코어 + SMT 2스레드 구성이면 논리 CPU는 0~7, 총 8개입니다 (lscpu의 CPU(s): 8이 이 수치입니다). CPU Hotplug은 이 논리 CPU 번호 단위로 온라인·오프라인을 제어합니다. 소켓에서 물리 CPU를 제거하는 것과는 다릅니다. 자세한 토폴로지 구조는 CPU 토폴로지 문서를 참고하세요.

핵심 요약

  • 런타임 CPU 전환 — 물리적 제거 없이 논리 CPU를 online/offline으로 전환하여 전력 절감·격리(Isolation)·유지보수에 활용합니다.
  • cpuhp_state 상태 머신 — CPUHP_OFFLINE에서 CPUHP_ONLINE까지 수십 개 단계를 순차 실행하고, 실패 시 자동 롤백(Rollback)합니다.
  • 3단계 콜백(Callback) — Prepare(제어 CPU)·Starting(타겟 CPU, IRQ off)·Online(타겟 CPU, IRQ on)로 구분해 HW/소프트웨어 초기화를 나눕니다.
  • 리소스 마이그레이션 — RCU 콜백, hrtimer, timer wheel, IRQ affinity, workqueue pending work를 다른 온라인 CPU로 안전하게 옮깁니다.
  • sysfs 제어 — /sys/devices/system/cpu/cpuN/online에 0/1을 써서 사용자 공간(User Space)에서 CPU를 끄고 켭니다.

단계별 이해

  1. 상태 파악과 실습
    lscpu, /sys/devices/system/cpu/online|offline|possible|present로 현재 CPU 구성을 확인합니다.
  2. offline/online 해보기
    echo 0 > .../cpuN/online으로 CPU를 끄고 dmesg, /proc/interrupts의 변화를 관찰합니다.
  3. 상태 머신 이해
    /sys/devices/system/cpu/hotplug/states로 각 단계에 어떤 서브시스템 콜백이 등록되어 있는지 확인합니다.
  4. 마이그레이션 관찰
    오프라인 전후의 /proc/interrupts, /proc/softirqs, /proc/timer_list를 비교해 IRQ·타이머·RCU가 어디로 옮겨가는지 봅니다.
  5. 콜백 작성과 디버깅(Debugging)
    cpuhp_setup_state()로 간단한 콜백을 등록하고 ftrace의 cpuhp 이벤트로 실행 순서·소요 시간을 추적합니다.

CPU Hotplug 입문 가이드

CPU를 왜 끌까요? 실무에서 CPU Hotplug을 사용하는 대표적인 이유는 다음과 같습니다.

목적설명대표 사례
전력 절감유휴 CPU 오프라인으로 소비 전력 감소모바일 기기 big.LITTLE, 서버 야간 운영
열 관리(Thermal Management)과열 CPU 오프라인으로 온도 제어thermal governor CPU 차단
보안SMT(Hyper-Threading) 비활성화MDS/L1TF 취약점(Vulnerability) 대응
유지보수MCE 발생 CPU 격리하드웨어 오류 CPU 분리

가장 기본적인 실습부터 시작합니다.

# 현재 CPU 상태 확인
lscpu | grep -E "^CPU\(s\)|On-line|Off-line"
# CPU(s):                  8
# On-line CPU(s) list:     0-7

# CPU 3 오프라인
echo 0 > /sys/devices/system/cpu/cpu3/online

# 상태 확인
cat /sys/devices/system/cpu/online   # 0-2,4-7
cat /sys/devices/system/cpu/offline  # 3
# 오프라인 전후 인터럽트 분포 비교
cat /proc/interrupts | head -5
#            CPU0       CPU1       CPU2       CPU3       CPU4 ...
# 오프라인 후 CPU3 컬럼 없음 → IRQ가 다른 CPU로 재분배됨

# softirq 변화 확인
cat /proc/softirqs | head -5
# CPU3 컬럼의 카운터가 더 이상 증가하지 않음
커널 설정: CPU Hotplug을 사용하려면 CONFIG_HOTPLUG_CPU=y가 필요합니다. 일반적인 배포판 커널에서는 활성화된 경우가 많지만, 최소 구성 커널이나 특수 목적 커널은 예외일 수 있습니다. zcat /proc/config.gz | grep HOTPLUG_CPU로 확인할 수 있습니다.
# CPU 온라인 복원
echo 1 > /sys/devices/system/cpu/cpu3/online

# 전체 CPU 일괄 온라인 복원
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
    [ -f "$cpu/online" ] && echo 1 > "$cpu/online" 2>/dev/null
done
CPU 0 보호: 대부분의 시스템에서 CPU 0(부트 CPU)은 오프라인할 수 없습니다. /sys/devices/system/cpu/cpu0/online 파일이 존재하지 않거나, 쓰기 시 -EBUSY를 반환합니다. x86에서는 BSP(Bootstrap Processor)가 이에 해당합니다.

CPU Hotplug 개요

CPU Hotplug은 시스템 실행 중에 논리 CPU를 온라인(online) 또는 오프라인(offline) 상태로 전환하는 커널 메커니즘입니다. 물리적 CPU 제거 없이도 논리적으로 CPU를 비활성화할 수 있으며, 다음과 같은 사용 사례가 있습니다.

사용 사례설명
전원 관리유휴 CPU 오프라인으로 전력 절감 (모바일/임베디드)
CPU 격리RT 태스크(Task) 전용 CPU 확보 (isolcpus= 대안)
유지보수MCE(Machine Check Exception) 발생 CPU 격리
가상화(Virtualization)VM에 vCPU 동적 추가/제거
NUMA 최적화특정 NUMA 노드 CPU만 활성화
suspend/resume시스템 절전 시 BSP 제외 모든 CPU 오프라인

커널은 CPU 상태를 4가지 비트마스크로 관리합니다.

/* include/linux/cpumask.h */
extern struct cpumask __cpu_possible_mask;  /* 사용할 수 있는 CPU 번호의 상한 */
extern struct cpumask __cpu_present_mask;   /* 물리적으로 존재 */
extern struct cpumask __cpu_online_mask;    /* 스케줄러에 참여 중 */
extern struct cpumask __cpu_active_mask;    /* 마이그레이션 대상 가능 */

/* 사용 예 */
for_each_online_cpu(cpu) {
    /* 온라인 CPU 순회 */
}
CPU 상태 비트마스크 4종의 포함 관계 cpu_active_mask ⊆ cpu_online_mask ⊆ cpu_present_mask ⊆ cpu_possible_mask cpu_possible_mask 커널이 사용할 수 있는 CPU 번호의 상한, CONFIG_NR_CPUS 기준 possible − present = 상한에는 있으나 하드웨어에 없는 CPU 슬롯 cpu_present_mask 부팅 시 탐지되어 실제로 존재하는 CPU present − online = 오프라인 상태로 남아 있는 CPU cpu_online_mask 스케줄러가 관리하여 태스크를 실행하는 CPU online − active = 오프라인 진행 중인 CPU cpu_active_mask 태스크 배치·마이그레이션의 유효 목적지 (cpu_offline() 진입 시 가장 먼저 해제) CPU 0 CPU 1 CPU 2 CPU 3 부팅 직후에는 네 마스크가 모두 동일하며, 오프라인이 진행되면 안쪽부터 하나씩 벗겨집니다.
CPU 상태 비트마스크 4종의 포함 관계 — 바깥의 cpu_possible_mask가 가장 큰 집합이고 안쪽으로 cpu_present_mask, cpu_online_mask, cpu_active_mask가 중첩됩니다. 각 마스크 사이의 경계선(ring)은 앞 마스크에만 속하는 CPU를 뜻하며, 오른쪽에 각 차집합의 의미를 표기했습니다 (개념 개요, v7.2 커널 소스 기준)
마스크설정 시점변경 가능성주요 사용처
cpu_possible_mask 컴파일 시 CONFIG_NR_CPUS 불변 Per-CPU 변수 할당 크기 계산
cpu_present_mask 부팅 시 firmware(ACPI/MADT, DT)에서 감지 x86: 불변
ARM64(virtual): vCPU hot-add 시 변경 가능
sysfs CPU 디렉토리 생성 기준
cpu_online_mask cpu_up() 성공 시 변경 (cpu_up/cpu_down) 스케줄러, kthread, workqueue
cpu_active_mask cpuhp 상태 머신에서 CPUHP_AP_ACTIVE 단계 변경 (cpuhp 상태 머신) 태스크 마이그레이션 대상
RCU, timer, workqueue
active vs online의 차이: set_cpu_active(cpu, false)는 해당 CPU를 active_mask에서 제거하지만 online_mask에는 유지합니다. 이때 해당 CPU에 묶인 태스크는 즉시 다른 CPU로 강제 마이그레이션됩니다. 이 기법은 태스크 마이그레이션 자체를 테스트하거나, 특정 CPU에서 실행 중인 태스크를 안전하게 다른 CPU로 옮길 때 유용합니다.

CPU 오프라인 시 처리해야 할 자원

CPU를 오프라인으로 전환하는 것은 단순히 "코어 전원을 끄는" 작업이 아닙니다. 해당 논리 CPU에 묶여 있던 모든 커널 자원을 다른 CPU로 안전하게 이관하거나 정리해야 합니다. 이 과정을 빠뜨리면 시스템 패닉, 데이터 유실, 영구 행(hang)이 발생할 수 있습니다. 아래에서는 "무엇을", "왜", "어떻게" 정리하는지를 살펴봅니다.

CPU N 오프라인 시 정리 대상 자원과 이관처 자원 정리 함수 처리 결과 CPU N 오프라인 예정 cpu_down() cpuhp 상태 머신이 순서를 강제함 다른 CPU로 이관 태스크 / 스레드 stop_one_cpu_nowait() 다른 CPU 런큐로 이동 타이머 휠 (jiffies) timers_dead_cpu() 다른 CPU 휠로 이동 hrtimer hrtimers_cpu_dying() 다른 CPU에 재등록 CPU 제외 반영 IRQ 어피니티 irq_migrate_all_off_this_cpu() 어피니티에서 CPU 제거 sched_domain rebuild_sched_domains() 도메인 계층 재구성 큐 드레인 RCU 콜백 rcutree_dying_cpu() dead CPU로 등록, 대기 워크큐 워크 workqueue_offline_cpu() pending work 재배치
CPU N 오프라인 시 정리 대상 자원 7종 — 자원을 처리 결과에 따라 다른 CPU로 이관, CPU 제외 반영, 큐 드레인의 세 그룹으로 나눴습니다. 정리 함수명은 v7.2 커널 소스에서 확인한 실제 심볼이며, 실행 순서는 cpuhp 상태 머신이 강제합니다 (개념 개요, v7.2 커널 소스 기준)
자원 왜 정리해야 하는가 정리 방법 담당 서브시스템
태스크 / 스레드 해당 CPU의 런큐(runqueue)에 남아 있으면 스케줄되지 못하고 영원히 대기 상태가 됨 balance_push_set() + stop_one_cpu_nowait()로 온라인 CPU 런큐에 이관 스케줄러 (kernel/sched/core.c)
IRQ 어피니티 인터럽트가 오프라인 CPU로 라우팅되면 영구 손실 irq_migrate_all_off_this_cpu()로 친화성 마스크 갱신 IRQ 서브시스템
hrtimer per-CPU hrtimer 휠은 해당 CPU에서만 실행되므로 만료 시점을 놓칠 수 있음 hrtimers_cpu_dying()로 활성 타이머를 다른 CPU에 재등록 고해상도 타이머
타이머 휠 (jiffies) 저해상도 타이머도 per-CPU 휠에 존재 — 같은 이유로 손실 가능 timers_dead_cpu()로 이관 타이머 휠
RCU 콜백 RCU 콜백 큐가 비어야 해당 CPU의 grace period가 종료됨 — 큐가 남으면 전체 RCU가 블록됨 rcutree_dying_cpu()으로 콜백을 다른 CPU로 오프로드 RCU
워크큐 워크 per-CPU 워크큐의 대기 중인 워크가 처리되지 않아 자원 누수나 데드락 가능성 workqueue_offline_cpu()로 플러시(Flush) 후 unbound 풀로 재배치 워크큐
sched_domain 토폴로지 로드밸런서가 오프라인 CPU를 대상으로 포함하면 불필요한 IPI 또는 불균형 발생 rebuild_sched_domains()로 도메인 재구성 스케줄러

이 자원들의 정리 순서도 중요합니다. 예를 들어 태스크를 이관하기 전에 IRQ 어피니티를 먼저 변경하면, 이관 중인 태스크가 인터럽트를 수신하지 못하는 짧은 구간이 생길 수 있습니다. Linux 커널은 이 순서를 cpuhp 상태 머신을 통해 엄격하게 제어합니다. 각 자원 처리가 정확히 어느 단계(Prepare / Starting / Online)에서 일어나는지는 다음 절에서 설명합니다.

CPU Hotplug 아키텍처

CPU Hotplug의 핵심은 cpuhp 상태 머신(state machine)입니다. CPU가 CPUHP_OFFLINE에서 CPUHP_ONLINE으로 전이할 때, 중간의 각 상태에서 등록된 콜백이 순차적으로 호출됩니다. 오프라인은 역순으로 콜백이 호출됩니다.

왜 Prepare·Starting·Online 3단계인가

3단계 구분의 핵심은 "어느 CPU에서 실행되는가"와 "슬립(Sleep)이 허용되는가" 두 가지 축의 조합입니다. 각 단계는 이 두 축에 따라 수행할 수 있는 작업이 근본적으로 달라집니다.

단계 실행 CPU IRQ 상태 슬립 가능 대표 작업
Prepare 제어 CPU
(온라인 상태의 다른 CPU)
활성화 예 (GFP_KERNEL 허용) 메모리 할당, 자료구조 초기화, per-CPU 변수 사전 준비
Starting 타겟 CPU
(방금 깨어난 CPU 자신)
비활성화 아니오 (원자적(Atomic) 컨텍스트) 스케줄러 초기화, GIC/인터럽트 컨트롤러(Interrupt Controller) 등록, RCU 참여
Online 타겟 CPU 활성화 예 per-CPU 스레드 생성, sched_domain 재구성, perf/tracing 등록

Starting 단계의 제약: IRQ가 비활성화된 원자적 컨텍스트이므로 GFP_KERNEL 메모리 할당이나 mutex_lock() 같은 슬립 가능 연산은 사용할 수 없습니다. Starting 콜백은 반드시 GFP_ATOMIC 또는 사전 할당된 메모리만 사용해야 합니다. 이 제약을 어기면 커널 워닝(might_sleep()) 또는 데드락이 발생합니다. Prepare 단계에서 메모리를 미리 할당하는 이유가 바로 이것입니다.

/* include/linux/cpuhotplug.h — 주요 상태 (발췌) */
enum cpuhp_state {
    CPUHP_INVALID        = -1,
    CPUHP_OFFLINE        = 0,
    /* --- Prepare 콜백 (제어 CPU에서 실행) --- */
    CPUHP_CREATE_THREADS,
    CPUHP_PERF_PREPARE,
    CPUHP_WORKQUEUE_PREP,
    CPUHP_HRTIMERS_PREPARE,
    /* ... */
    CPUHP_BRINGUP_CPU,
    /* --- 여기서 타겟 CPU가 깨어남 --- */
    /* --- Starting 콜백 (타겟 CPU에서 실행, IRQ 비활성) --- */
    CPUHP_AP_IDLE_DEAD,
    CPUHP_AP_OFFLINE,
    CPUHP_AP_SCHED_STARTING,
    CPUHP_AP_RCUTREE_DYING,
    CPUHP_AP_IRQ_GIC_STARTING,
    /* ... */
    CPUHP_AP_ONLINE,
    /* --- Online 콜백 (타겟 CPU에서 실행, IRQ 활성) --- */
    CPUHP_AP_ACTIVE,
    CPUHP_AP_SMPBOOT_THREADS,
    CPUHP_AP_ONLINE_DYN,
    /* ... */
    CPUHP_ONLINE,
};
코드 설명

include/linux/cpuhotplug.h에 정의된 cpuhp_state 열거형(Enum)은 CPU hotplug 상태 머신의 모든 단계를 정의합니다. 상태는 세 영역으로 나뉩니다.

  • CPUHP_OFFLINE ~ CPUHP_BRINGUP_CPUPrepare 영역: 제어 CPU(보통 BSP)에서 실행됩니다. 메모리 할당, 스레드(Thread) 생성 등 슬립 가능한 작업을 수행합니다.
  • CPUHP_AP_IDLE_DEAD ~ CPUHP_TEARDOWN_CPUStarting 영역: 타겟 CPU에서 IRQ 비활성 상태로 실행됩니다. GIC 초기화, 스케줄러 시작 등 인터럽트 없이 처리해야 하는 저수준 작업이 여기에 배치되며, 이 구간은 실패가 허용되지 않습니다.
  • CPUHP_AP_ONLINE_IDLE ~ CPUHP_ONLINEOnline 영역: 타겟 CPU에서 IRQ 활성 상태로 실행됩니다. 서브시스템별 온라인 콜백(perf, workqueue, RCU 등)이 이 영역에서 호출됩니다.

CPU bring-up은 CPUHP_OFFLINE에서 CPUHP_ONLINE으로 순방향 진행하고, teardown은 역순입니다.

cpuhp 상태 머신 — 3단 구간과 양방향 전이 bring-up (번호 증가) teardown (번호 감소) CPUHP_ONLINE (236) — 완전 온라인 ONLINE 구간 실행 CPU: 타겟 CPU · IRQ: 활성 · 슬립: 가능 CPUHP_AP_ONLINE_IDLE(144) ~ CPUHP_ONLINE(236) 서브시스템별 온라인 콜백 (perf, workqueue, RCU, IRQ affinity) per-CPU 핫플러그 스레드에서 실행 STARTING 구간 실행 CPU: 타겟 CPU · IRQ: 비활성 · 슬립: 불가 CPUHP_AP_IDLE_DEAD(84) ~ CPUHP_TEARDOWN_CPU(143) 하드웨어 초기화. 84~141은 실패가 허용되지 않는 구간 PREPARE 구간 실행 CPU: 제어 CPU · IRQ: 활성 · 슬립: 가능 CPUHP_OFFLINE(0) ~ CPUHP_BRINGUP_CPU(83) 메모리 할당, 스레드 생성 등 슬립 가능한 사전 준비 startup 실패 시 롤백 CPUHP_OFFLINE (0) — 오프라인
cpuhp 상태 머신 — PREPARE(제어 CPU·IRQ 활성·슬립 가능), STARTING(타겟 CPU·IRQ 비활성·슬립 불가), ONLINE(타겟 CPU·IRQ 활성·슬립 가능)의 3단 구간과 bring-up(번호 증가)·teardown(번호 감소) 양방향 전이. 구간 경계와 상태 번호는 v7.2 include/linux/cpuhotplug.h 기준이며, 세로 축척은 상태 수에 비례하지 않습니다 (개념 개요)

상태 머신의 핵심 원칙: bring-up 시 상태 번호가 증가하며 각 상태의 startup 콜백이 호출되고, teardown 시 역순으로 teardown 콜백이 호출됩니다. 어느 콜백이든 실패하면 이미 수행된 콜백을 역순으로 롤백(undo)합니다.

상태 머신

cpuhp 상태는 크게 3개 영역으로 나뉩니다.

영역상태 범위실행 위치IRQ용도
PrepareCPUHP_OFFLINE+1 ~ CPUHP_BRINGUP_CPU제어 CPU활성메모리 할당, 스레드 생성 등 준비 작업
StartingCPUHP_AP_OFFLINE ~ CPUHP_AP_ONLINE-1타겟 CPU비활성저수준 HW 초기화, GIC/타이머 설정
OnlineCPUHP_AP_ONLINE ~ CPUHP_ONLINE타겟 CPU활성서브시스템 알림, 워커 스레드 시작
/* kernel/cpu.c — 상태 배열 */
static struct cpuhp_step cpuhp_hp_states[] = {
    [CPUHP_OFFLINE] = {
        .name = "offline",
    },
    [CPUHP_CREATE_THREADS] = {
        .name = "threads:prepare",
        .startup.single = smpboot_create_threads,
        .teardown.single = NULL,
    },
    /* ... 수십 개의 상태 ... */
    [CPUHP_ONLINE] = {
        .name = "online",
    },
};
코드 설명

kernel/cpu.c의 cpuhp_hp_states[] 배열은 각 cpuhp_state 열거값을 인덱스로 사용하여 해당 상태의 콜백 함수를 저장합니다.

  • cpuhp_hp_states[]배열의 각 원소는 struct cpuhp_step으로, .name(디버그/sysfs 표시용), .startup.single(bring-up 콜백), .teardown.single(teardown 콜백)을 포함합니다.
  • [CPUHP_OFFLINE]상태 0(오프라인)은 콜백이 없는 시작점입니다.
  • [CPUHP_CREATE_THREADS]smpboot_create_threads가 per-CPU 스레드(ksoftirqd, migration 등)를 생성합니다. teardown이 NULL이면 역방향 시 아무 작업도 하지 않습니다.
  • [CPUHP_ONLINE]최종 상태로, CPU가 완전히 온라인이 되었음을 나타냅니다.
동적 상태: CPUHP_AP_ONLINE_DYN과 CPUHP_BP_PREPARE_DYN 영역은 모듈이 런타임에 콜백을 등록할 수 있는 동적 슬롯입니다. cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)으로 사용합니다.

CPU Bring-up 과정

사용자가 echo 1 > /sys/devices/system/cpu/cpu3/online을 실행하면 다음 경로를 따릅니다.

CPU bring-up 흐름 — 제어 CPU에서 타겟 CPU로의 핸드오프 시간은 위에서 아래로 흐릅니다 · 박스 안의 세 줄은 함수 · 대상 cpuhp 상태 · 실행 조건 순서입니다 제어 CPU (BSP) sysfs write > cpu_online_store() cpu_device_up() > cpu_up(dev->id, CPUHP_ONLINE) cpu_maps_update_begin() · cpus_write_lock() _cpu_up() — cpuhp_up_callbacks() target = min(CPUHP_ONLINE, CPUHP_BRINGUP_CPU) PREPARE 콜백 순차 실행 · IRQ 활성 · 실패 시 롤백 CPUHP_BP_KICK_AP — cpuhp_kick_ap_alive() arch_cpuhp_kick_ap_alive() > __cpu_up() x86 INIT/SIPI · ARM64 PSCI CPUHP_BRINGUP_CPU — cpuhp_bringup_ap() irq_lock_sparse() > cpuhp_bp_sync_alive() ALIVE 대기 후 SYNC_STATE_SHOULD_ONLINE 기록 bringup_wait_for_ap_online() CPUHP_AP_ONLINE_IDLE 까지 대기 irq_lock_sparse() 유지 — IRQ alloc/free 차단 bringup_wait_for_ap_online() 해제 cpuhp_kick_ap() > cpuhp/3 스레드 기동 wait_for_ap_thread(): CPUHP_ONLINE 까지 대기 cpus_write_unlock() > cpu_maps_update_done() write() 는 CPUHP_ONLINE 확인 후 반환 sysfs write 핸들러로 복귀 타겟 CPU (cpu3) cpu3 는 아직 오프라인 — 실행 중인 코드가 없습니다 PREPARE 구간이 끝나야 타겟 CPU가 깨어납니다 아키텍처 저수준 부팅 진입 x86 smp_callin() · ARM64 secondary_start_kernel() 첫 STARTING 상태 CPUHP_AP_IDLE_DEAD · IRQ 비활성 cpuhp_ap_sync_alive() SYNC_STATE_ALIVE 보고 > spin 대기 제어 CPU가 해제하기 전까지 진행 불가 notify_cpu_starting() STARTING 콜백 · IRQ 비활성 · 실패 불가 CPUHP_AP_IDLE_DEAD ~ CPUHP_AP_ONLINE cpu_startup_entry(CPUHP_AP_ONLINE_IDLE) cpuhp_online_idle() > complete_ap_thread() ONLINE 콜백 · IRQ·선점 활성 · 실패 허용 CPUHP_ONLINE (online) cpu_online_mask 반영 — ONLINE 구간 완료 이후 teardown 는 역순으로 되돌림 ① AP 깨움 ② alive 양방향 동기화 실행은 타겟 CPU 단독 ③ 온라인 완료 통보 ① ~ ③ 는 두 CPU 사이의 상호작용 지점 · 점선 박스 = 해당 CPU가 블로킹 대기 · 실선 박스 = 실행 중
CPU bring-up 흐름 — 제어 CPU(BSP)와 타겟 CPU(cpu3) 두 레인으로 나눈 시간순 도해입니다. PREPARE 구간(CPUHP_OFFLINE 다음 상태 ~ CPUHP_BRINGUP_CPU)만 제어 CPU에서 실행되고, STARTING 구간은 타겟 CPU의 저수준 부팅 코드에서 IRQ 비활성 상태로, ONLINE 구간은 per-CPU hotplug 스레드에서 IRQ·선점 활성 상태로 실행됩니다. ① CPUHP_BP_KICK_AP 콜백의 __cpu_up()이 실행을 타겟 CPU로 넘기는 핸드오프 지점이며, ② CPUHP_BRINGUP_CPU 콜백(cpuhp_bringup_ap())과 타겟 CPU의 cpuhp_ap_sync_alive()가 서로를 기다리는 유일한 양방향 동기화 지점입니다. 점선 박스는 블로킹 대기로 멈춰 있는 CPU를, 실선 박스는 실행 중인 CPU를 나타냅니다 (개념 개요, v7.2 kernel/cpu.c 기준)
/* kernel/cpu.c — bring-up 핵심 */
static int _cpu_up(unsigned int cpu, int tasks_frozen,
                   enum cpuhp_state target)
{
    struct cpuhp_cpu_state *st = per_cpu_ptr(&cpuhp_state, cpu);
    int ret;

    cpus_write_lock();  /* percpu rwsem 쓰기 잠금 */

    /* Prepare 단계: 제어 CPU에서 콜백 실행 */
    for (st->state++; st->state < CPUHP_BRINGUP_CPU; st->state++) {
        ret = cpuhp_invoke_callback(cpu, st->state, true, NULL);
        if (ret)
            goto rollback;
    }

    /* 아키텍처별 CPU 시작 */
    ret = __cpu_up(cpu, idle_thread_get(cpu));
    if (ret)
        goto rollback;

    /* 타겟 CPU에서 Starting + Online 콜백 처리 */
    ret = bringup_wait_for_ap_online(st);

    cpus_write_unlock();
    return ret;
}
코드 설명

kernel/cpu.c의 _cpu_up()은 CPU bring-up의 핵심 함수입니다. 세 단계로 진행됩니다.

  • cpus_write_lock()percpu rwsem 쓰기 잠금(Lock)을 획득하여 hotplug 진행 중 다른 코어의 cpu_online_mask 읽기를 차단합니다.
  • for (st->state++; ...)Prepare 단계: CPUHP_OFFLINE부터 CPUHP_BRINGUP_CPU 직전까지 상태를 순차 진행하며, 제어 CPU에서 각 상태의 startup 콜백을 호출합니다. 실패 시 rollback으로 이동하여 이미 완료된 콜백의 teardown을 역순 호출합니다.
  • __cpu_up(cpu, idle_thread_get(cpu))아키텍처별 코드(x86: native_cpu_up(), ARM64: psci_cpu_on())를 통해 물리 CPU를 실제로 깨웁니다. idle_thread_get()은 해당 CPU의 idle 태스크를 전달합니다.
  • bringup_wait_for_ap_online(st)타겟 CPU가 Starting 콜백과 Online 콜백을 자체적으로 실행 완료할 때까지 대기합니다. 타겟 CPU의 cpuhp_thread가 이 작업을 수행합니다.

CPU Teardown 과정

echo 0 > /sys/devices/system/cpu/cpu3/online은 bring-up의 역순입니다. 핵심 작업은 태스크 마이그레이션, IRQ 재분배, 타이머 이동입니다.

/* kernel/cpu.c — teardown 핵심 */
static int __ref _cpu_down(unsigned int cpu, int tasks_frozen,
                           enum cpuhp_state target)
{
    /* 1. cpu_active_mask에서 제거 → 새 태스크 배치 차단 */
    set_cpu_active(cpu, false);

    /* 2. 마이그레이션: 실행 가능한 태스크를 다른 CPU로 이동 */
    sched_cpu_deactivate(cpu);

    /* 3. Online 콜백 역순 호출 (IRQ 활성 상태) */
    cpuhp_down_callbacks(cpu, st, target);

    /* 4. Starting 콜백 역순 (IRQ 비활성) */
    /* 5. 아키텍처별 CPU 정지 */
    /* 6. cpu_online_mask에서 제거 */
    set_cpu_online(cpu, false);

    /* 7. Prepare 콜백 역순 호출 (제어 CPU에서) */
}
코드 설명

kernel/cpu.c의 _cpu_down()은 CPU teardown의 핵심 함수로, bring-up의 정확한 역순으로 진행됩니다.

  • set_cpu_active(cpu, false)cpu_active_mask에서 해당 CPU를 제거하여 스케줄러가 새 태스크를 이 CPU에 배치하지 않도록 합니다. 이 시점에서 CPU는 아직 cpu_online_mask에 남아 있습니다.
  • sched_cpu_deactivate(cpu)stop_machine()을 사용하여 해당 CPU에서 실행 중인 모든 태스크를 다른 CPU로 마이그레이션합니다. 이 과정에서 select_fallback_rq()가 대상 CPU를 결정합니다.
  • cpuhp_down_callbacks()Online 영역의 teardown 콜백을 역순으로 호출합니다. perf 이벤트 비활성화, workqueue 정리 등이 이 단계에서 수행됩니다.
  • set_cpu_online(cpu, false)Starting 콜백 역순 실행과 아키텍처별 CPU 정지 후, cpu_online_mask에서 최종 제거합니다. 이후 Prepare 콜백을 제어 CPU에서 역순 호출합니다.
마지막 CPU 보호: 부트 CPU(보통 CPU 0)는 오프라인 불가합니다. cpu_is_hotpluggable(cpu)가 false를 반환하여 요청이 거절됩니다. x86에서는 BSP(Bootstrap Processor)가 이에 해당합니다.

콜백 등록

드라이버와 서브시스템은 cpuhp_setup_state() 계열 함수로 hotplug 콜백을 등록합니다.

/* 정적 상태에 콜백 등록 */
ret = cpuhp_setup_state(CPUHP_AP_PERF_X86_STARTING,
                        "perf/x86:starting",
                        x86_pmu_starting_cpu,   /* startup 콜백 */
                        x86_pmu_dying_cpu);      /* teardown 콜백 */

/* 동적 상태에 등록 (슬롯 자동 할당) */
ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                        "my_driver:online",
                        my_online_cb, my_offline_cb);
/* ret > 0 이면 할당된 상태 번호 */

/* 인스턴스 기반: 여러 인스턴스 각각에 콜백 */
ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN,
                              "net/mlx5:online",
                              mlx5e_cpu_online,
                              mlx5e_cpu_offline);
/* cpuhp_state_add_instance()로 인스턴스 추가 */
코드 설명

cpuhp_setup_state() 계열 API는 CPU hotplug 콜백을 등록하는 핵심 인터페이스입니다.

  • cpuhp_setup_state(CPUHP_AP_PERF_X86_STARTING, ...)정적 상태 등록: 미리 정의된 상태 번호에 콜백을 직접 등록합니다. 등록 즉시 이미 온라인인 모든 CPU에 대해 startup 콜백이 호출됩니다.
  • cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)동적 상태 등록: CPUHP_AP_ONLINE_DYN 또는 CPUHP_BP_PREPARE_DYN을 지정하면 커널이 빈 슬롯을 자동 할당합니다. 반환값이 양수이면 할당된 상태 번호이며, cpuhp_remove_state() 호출 시 이 번호를 사용합니다.
  • cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, ...)멀티 인스턴스: 네트워크 디바이스처럼 동일 타입의 인스턴스가 여러 개인 경우 사용합니다. 콜백에 struct hlist_node 포인터가 추가로 전달되어 인스턴스를 식별합니다.
API설명인스턴스
cpuhp_setup_state()콜백 등록 + 이미 온라인인 CPU에도 콜백 호출단일
cpuhp_setup_state_nocalls()콜백 등록만, 기존 CPU에 호출 안 함단일
cpuhp_setup_state_multi()멀티 인스턴스 콜백 등록복수
cpuhp_remove_state()콜백 해제 + 온라인 CPU에 teardown 호출단일

sysfs 인터페이스

사용자 공간에서 CPU Hotplug을 제어하는 주요 sysfs 경로입니다.

# CPU 상태 확인
cat /sys/devices/system/cpu/online       # 예: 0-7
cat /sys/devices/system/cpu/offline      # 예: 8-15
cat /sys/devices/system/cpu/possible     # 예: 0-255
cat /sys/devices/system/cpu/present      # 예: 0-15

# CPU 3 오프라인
echo 0 > /sys/devices/system/cpu/cpu3/online

# CPU 3 온라인
echo 1 > /sys/devices/system/cpu/cpu3/online

# hotplug 상태 머신 디버그 정보
ls /sys/devices/system/cpu/hotplug/
# states — 전체 cpuhp 상태 목록과 현재 등록된 콜백
cat /sys/devices/system/cpu/hotplug/states | head -20

태스크 마이그레이션

CPU가 오프라인되면, 해당 CPU의 런큐(Runqueue)에 있는 모든 태스크를 다른 CPU로 이동해야 합니다.

태스크 마이그레이션 — 오프라인 대상 CPU 의 런큐에서 남은 CPU 로 왼쪽에서 오른쪽으로 진행 · ① 이 출발점을 막고 ② 가 목적지를 정한 뒤 ③ 으로 enqueue 합니다 출발 — 오프라인 대상 CPU ② 대상 CPU 선택 ③ 도착 — 남은 온라인 CPU CPU 3 · 런큐 오프라인 대상 · 실행 가능 태스크 Task A Task B Task C ① set_cpu_active(3, false) sched_cpu_deactivate() cpu_active_mask 에서 CPU3 제거 balance_push_set(3, true) → 배치 차단 · 기존 태스크 밀어냄 태스크가 다음 스케줄될 때 이동 select_task_rq() > select_fallback_rq() 1 같은 NUMA 노드 node 내 첫 is_cpu_allowed() CPU 2 affinity(p->cpus_ptr) 안 다른 노드라도 허용되면 선택 3 affinity 해제 후 재탐색 set_cpus_allowed_force() · 복원 안 됨 enqueue 남은 온라인 CPU 의 런큐 cpu_active_mask 에 남아 있음 CPU 0 · rq CPU 1 · rq CPU 2 · rq 태스크마다 목적지가 다릅니다 여러 태스크가 같은 CPU 에 모일 수 있습니다 ④ sched_cpu_dying() — CPU3 런큐에 idle 태스크만 남았는지 검증 rq->nr_running != 1 이면 "Dying CPU not properly vacated!" 경고가 발생합니다 이동 대상은 is_cpu_allowed() 가 false 인 태스크이며, migrate_disabled 태스크와 per-CPU kthread 는 대상이 아닙니다
태스크 마이그레이션 — 오프라인 대상 CPU의 런큐에 있던 실행 가능 태스크를 남은 온라인 CPU의 런큐로 옮기는 과정. 먼저 set_cpu_active(3, false) 로 새 태스크 배치를 차단하고 balance_push_set(3, true) 로 기존 태스크를 밀어내도록 한 뒤, 각 태스크가 다음 스케줄될 때 select_task_rq() 가 is_cpu_allowed() 실패를 감지해 select_fallback_rq() 로 목적지를 정합니다. 목적지는 1순위 같은 NUMA 노드, 2순위 affinity 범위, 3순위 affinity 해제 순으로 결정되며 태스크마다 독립적이라 여러 태스크가 하나의 CPU로 모일 수 있습니다. 마지막으로 sched_cpu_dying() 이 idle 태스크만 남았는지 검증합니다 (개념 개요, v7.2 kernel/sched/core.c 및 kernel/cpu.c 기준)
/* kernel/sched/core.c — 폴백 런큐 선택 */
static int select_fallback_rq(int cpu, struct task_struct *p)
{
    int nid = cpu_to_node(cpu);
    const struct cpumask *nodemask;

    /* 1순위: 같은 NUMA 노드의 활성 CPU */
    nodemask = cpumask_of_node(nid);
    for_each_cpu(dest_cpu, nodemask) {
        if (cpumask_test_cpu(dest_cpu, &p->cpus_mask) &&
            cpu_active(dest_cpu))
            return dest_cpu;
    }

    /* 2순위: 아무 활성 CPU */
    /* 3순위: affinity 무시하고 아무 CPU (최후 수단) */
    do_set_cpus_allowed(p, cpu_possible_mask);
    dest_cpu = cpumask_any(cpu_active_mask);
    return dest_cpu;
}
코드 설명

kernel/sched/core.c의 select_fallback_rq()는 CPU 오프라인 시 태스크를 이동할 대상 CPU를 결정하는 폴백 로직입니다.

  • cpu_to_node(cpu)오프라인되는 CPU의 NUMA 노드 ID를 조회합니다. NUMA 환경에서 같은 노드 내 이동이 메모리 접근 지연(Latency)을 최소화합니다.
  • 1순위: cpumask_of_node(nid)같은 NUMA 노드의 활성 CPU 중 태스크의 cpus_mask(affinity)에 포함된 CPU를 선택합니다.
  • 2순위: 아무 활성 CPU같은 노드에 적합한 CPU가 없으면 다른 노드의 활성 CPU를 탐색합니다.
  • do_set_cpus_allowed(p, cpu_possible_mask)최후 수단: affinity 제한으로 갈 곳이 없는 태스크는 affinity를 강제 해제하여 아무 활성 CPU에 배치합니다. 이 변경은 CPU가 다시 온라인되어도 자동 복원되지 않습니다.
affinity 고정 태스크: sched_setaffinity()로 오프라인되는 CPU에만 고정된 태스크는, 최후 수단으로 affinity가 해제됩니다. 다시 온라인 시 원래 affinity가 복원되지 않습니다. 사용자 공간에서 수동 복원이 필요합니다.

인터럽트 처리

CPU 오프라인 시 해당 CPU에 설정된 인터럽트 affinity를 다른 CPU로 재분배해야 합니다.

/* kernel/irq/cpuhotplug.c */
void irq_migrate_all_off_this_cpu(void)
{
    struct irq_desc *desc;
    unsigned int irq;

    for_each_active_irq(irq) {
        desc = irq_to_desc(irq);
        /* managed IRQ: 해당 CPU 전용이면 셧다운 */
        if (irqd_affinity_is_managed(&desc->irq_data)) {
            irq_force_complete_move(desc);
            continue;
        }
        /* 일반 IRQ: 다른 온라인 CPU로 이동 */
        if (cpumask_test_cpu(smp_processor_id(), desc->irq_common_data.affinity))
            irq_set_affinity_locked(desc, cpu_online_mask, true);
    }
}
코드 설명

kernel/irq/cpuhotplug.c의 irq_migrate_all_off_this_cpu()는 CPU 오프라인 시 해당 CPU에 할당된 모든 인터럽트를 다른 CPU로 이동합니다.

  • for_each_active_irq(irq)시스템에 등록된 모든 활성 IRQ를 순회합니다.
  • irqd_affinity_is_managed()Managed IRQ는 커널이 자동 관리하는 인터럽트(주로 MSI-X)입니다. 해당 CPU 전용이면 다른 CPU로 이동하지 않고 irq_force_complete_move()로 셧다운합니다. CPU가 다시 온라인되면 자동 재시작(Reboot)됩니다.
  • cpumask_test_cpu(smp_processor_id(), ...)일반 IRQ의 affinity 마스크에 현재(오프라인되는) CPU가 포함되어 있는지 확인합니다.
  • irq_set_affinity_locked(desc, cpu_online_mask, true)affinity를 cpu_online_mask로 변경하여 나머지 온라인 CPU 중 하나가 인터럽트를 처리하도록 합니다. 마지막 인자 true는 강제 이동을 의미합니다.
IRQ 유형오프라인 시 동작온라인 시 동작
일반 IRQ다른 온라인 CPU로 affinity 이동affinity에 CPU 추가 (자동 복원 아님)
Managed IRQ해당 CPU 전용이면 irq_shutdown()irq_startup()으로 재활성화
Per-CPU IRQ자동 비활성화 (GIC 등 HW 수준)자동 재활성화

타이머 마이그레이션 (요약)

오프라인 CPU에 등록된 타이머는 다른 CPU로 이동해야 합니다.

/* kernel/time/hrtimer.c — hrtimer 마이그레이션 */
int hrtimers_cpu_dying(unsigned int dying_cpu)
{
    struct hrtimer_cpu_base *old_base, *new_base;
    int i;

    old_base = this_cpu_ptr(&hrtimer_bases);
    new_base = &per_cpu(hrtimer_bases, cpumask_first(cpu_active_mask));

    /* 모든 클록 베이스의 타이머를 이동 */
    for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
        migrate_hrtimer_list(&old_base->clock_base[i],
                             &new_base->clock_base[i]);
    }
    return 0;
}

/* kernel/time/timer.c — timer wheel 마이그레이션 */
int timers_dead_cpu(unsigned int cpu)
{
    struct timer_base *old_base = per_cpu_ptr(&timer_bases[BASE_STD], cpu);
    struct timer_base *new_base = per_cpu_ptr(&timer_bases[BASE_STD],
                                             smp_processor_id());
    migrate_timer_list(new_base, old_base->vectors);
    return 0;
}
코드 설명

CPU 오프라인 시 해당 CPU에 등록된 타이머를 다른 CPU로 이동하는 두 가지 콜백입니다.

  • hrtimers_cpu_dying()kernel/time/hrtimer.c에서 고해상도 타이머(hrtimer)를 마이그레이션합니다. CPUHP_AP_HRTIMERS_DYING 상태(Starting 영역, IRQ 비활성)에서 호출됩니다.
  • per_cpu(hrtimer_bases, cpumask_first(cpu_active_mask))대상 CPU는 cpu_active_mask의 첫 번째 CPU입니다. 모든 클록 베이스(CLOCK_MONOTONIC, CLOCK_REALTIME 등)의 타이머를 순회하며 이동합니다.
  • timers_dead_cpu()kernel/time/timer.c에서 일반 타이머 휠(timer wheel)을 마이그레이션합니다. CPUHP_TIMERS_DEAD 상태(Prepare 영역)에서 호출되므로 제어 CPU에서 실행됩니다.
  • migrate_timer_list()오프라인 CPU의 timer_base에 있는 모든 벡터의 타이머를 현재 CPU의 timer_base로 옮깁니다.

RCU와 CPU Hotplug

RCU는 CPU Hotplug과 긴밀하게 연동됩니다. 오프라인되는 CPU는 RCU의 관점에서 정지 상태(quiescent state)를 보고해야 하며, 진행 중인 grace period를 완료해야 합니다.

/* kernel/rcu/tree_plugin.h */
int rcutree_dying_cpu(unsigned int cpu)
{
    /* CPUHP_AP_RCUTREE_DYING 상태에서 호출 */
    /* 1. 보류 중인 RCU 콜백 다른 CPU로 이동 */
    rcu_migrate_callbacks(cpu);

    /* 2. rcu_node에서 해당 CPU 비트 제거 */
    rnp = rdp->mynode;
    mask = rdp->grpmask;
    raw_spin_lock_irqsave(&rnp->lock, flags);
    rnp->qsmaskinitnext &= ~mask;  /* 다음 GP에서 제외 */
    raw_spin_unlock_irqrestore(&rnp->lock, flags);
    return 0;
}

int rcutree_online_cpu(unsigned int cpu)
{
    /* CPU 온라인 시: rcu_node에 비트 설정 */
    rnp->qsmaskinitnext |= mask;
    return 0;
}
SRCU: Sleepable RCU는 per-CPU 카운터를 사용하므로, CPU 오프라인 시 카운터 합산 방식이 달라집니다. srcu_cpu_notify()에서 처리합니다.
RCU NOCB CPU 연동(CONFIG_RCU_NOCB_CPU): rcu_nocbs= 커널 파라미터로 지정한 CPU에서는 RCU callback이 직접 실행되지 않고, dedicated kthread(rcu_preempt, rcu_bh, rcu_sched)으로 오프로드됩니다. CPU 오프라인 시 해당 CPU의 NOCB kthread가 중지되고, 보류 중인 callback은 다른 CPU의 NOCB kthread로 재배치됩니다. Real-Time(RT) 격리 시나리오에서 필수적입니다.

Workqueue 처리

Per-CPU workqueue의 worker 풀은 CPU Hotplug에 따라 관리됩니다.

/* kernel/workqueue.c */
/* CPU 오프라인 시: bound pool의 worker를 unbound pool로 이동 */
static void wq_worker_dying(struct worker *worker)
{
    /* 보류 중인 work item을 다른 CPU 풀로 이동 */
    list_for_each_entry_safe(work, n, &pool->worklist, entry) {
        move_linked_works(work, &pool->worklist, NULL);
    }
}

/* CPU 온라인 시 */
static int workqueue_online_cpu(unsigned int cpu)
{
    /* bound worker pool 재활성화 */
    /* unbound pool의 CPU affinity 갱신 */
}

RCU Grace Period

CPU가 오프라인될 때 RCU 서브시스템은 복잡한 절차를 거칩니다. 단순히 콜백을 이동하는 것만이 아니라, 진행 중인 grace period가 해당 CPU의 quiescent state 보고를 기다리고 있을 수 있으므로, 죽어가는 CPU가 명시적으로 QS를 보고해야 합니다.

CPU 오프라인과 RCU — dying CPU 의 QS 보고부터 콜백 이관까지 위에서 아래로 진행 · ① ② 는 죽는 CPU 가, ③ 은 남은 온라인 CPU 가 수행합니다 죽는 CPU (cpu3) ① rcutree_dying_cpu() CPUHP_AP_RCUTREE_DYING teardown stop_machine 안에서 실행 trace 만 남깁니다 — cpuofl / cpuofl-bgp ② rcutree_report_cpu_dead() cpuhp_step 을 거치지 않고 CPU 가 직접 호출 rcu_preempt_deferred_qs() rnp->qsmask & mask 면 QS 보고 qsmaskinitnext &= ~mask — 오프라인 표시 cpu_started = false lockdep_assert_irqs_disabled() IRQ 가 켜진 채로 지나가면 새 READ-side 가 다시 들어옵니다 남은 온라인 CPU 왜 dying CPU 가 직접 보고하는가 진행 중이던 grace period 가 이 CPU 의 QS 를 기다릴 수 있음 rnp->qsmask 에 CPU 비트 → GP 대기 ③ rcutree_migrate_callbacks() 온라인 CPU 가 IPI 로 진행을 이어받아 실행 rcu_barrier_entrain(rdp) rcu_segcblist_merge(&my_rdp, &rdp) NOCB CPU 면 cblist 가 비어 있어 즉시 반환 옮기지 못한 콜백이 남으면 WARN needwake 면 rcu_gp_kthread_wake() dead CPU 의 rdp->cblist 는 0 이어야 합니다 콜백 이관 ④ rcutree_dead_cpu() — CPUHP_RCUTREE_PREP teardown, 제어 CPU 에서 실행 n_online_cpus 를 1 줄이고 tick_dep_clear(TICK_DEP_BIT_RCU) 로 tick 의존을 해제 이 단계에 이르기까지 dead CPU 의 콜백 목록이 비어 있어야 합니다
CPU 오프라인과 RCU — 죽는 CPU 가 마지막 quiescent state 를 직접 보고하고, 남은 온라인 CPU 가 그 콜백 목록을 인계받는 순서. ① rcutree_dying_cpu() 는 CPUHP_AP_RCUTREE_DYING teardown 으로 trace 만 남기고, ② rcutree_report_cpu_dead() 가 IRQ 비활성 상태에서 qsmaskinitnext 의 CPU 비트를 지워 대기 중이던 grace period 를 진행시킵니다. 이어 ③ rcutree_migrate_callbacks() 가 dead CPU 의 cblist 를 온라인 CPU 로 합치며, 마지막으로 ④ rcutree_dead_cpu() 가 n_online_cpus 를 감소시킵니다 (개념 개요, v7.2 kernel/rcu/tree.c 및 kernel/cpu.c 기준)

RCU의 CPU 오프라인 처리는 크게 세 단계로 진행됩니다.

/* kernel/rcu/tree.c — CPU dead 보고 */
void rcu_report_dead(unsigned int cpu)
{
    struct rcu_data *rdp = per_cpu_ptr(&rcu_data, cpu);
    struct rcu_node *rnp = rdp->mynode;
    unsigned long mask = rdp->grpmask;

    /* 1단계: 현재 진행 중인 GP에 대해 QS 보고 */
    rcu_report_qs_rdp(rdp);

    /* 2단계: rcu_node에서 해당 CPU 비트 제거 */
    raw_spin_lock_irqsave_rcu_node(rnp, flags);
    rnp->qsmaskinitnext &= ~mask;
    raw_spin_unlock_irqrestore_rcu_node(rnp, flags);

    /* 3단계: 미처리 콜백이 있으면 orphan 큐로 이동 */
    rdp->cpu_started = false;
}

RCU NOCB 설정 — 콜백 오프로드

NOCB CPU의 콜백 처리 kthread는 다음과 같습니다.

항목일반 CPUNOCB CPU
콜백 실행 위치softirq (RCU_SOFTIRQ)전용 kthread (rcuop/N)
오프라인 시 콜백 처리다른 CPU로 이동 필요kthread가 계속 처리
오프라인 지연콜백 수에 비례최소
RT 적합성softirq 지연 발생kthread 우선순위(Priority)로 제어 가능
Grace Period 블로킹: PREEMPT_RCU에서 CPU가 오프라인되려 하지만 해당 CPU에서 rcu_read_lock() 임계 구간이 아직 열려 있으면, grace period가 완료되지 않아 오프라인이 지연될 수 있습니다. 이때 /sys/kernel/debug/rcu/rcu_preempt/rcudata에서 pending 상태를 확인합니다.
디버깅 팁: rcutorture 모듈(CONFIG_RCU_TORTURE_TEST)은 CPU hotplug과 RCU를 동시에 스트레스 테스트합니다. modprobe rcutorture로 실행하면 자동으로 CPU on/off를 반복하면서 RCU 정합성을 검증합니다.
# RCU 상태 확인
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# CPU별 completed GP, pending 콜백 수 확인

# NOCB 상태 확인
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata | grep -E "nocb|offload"

# grace period 통계
cat /sys/kernel/debug/rcu/rcu_preempt/rcugp
# completed=N gpnum=M → N != M이면 GP 진행 중

타이머 마이그레이션

CPU가 오프라인될 때 해당 CPU에 등록된 모든 타이머를 안전하게 이동해야 합니다. Linux 커널은 hrtimer(고해상도 타이머(hrtimer))와 timer wheel(전통적 타이머) 두 가지 타이머 체계를 사용하며, 각각 별도의 마이그레이션 경로를 갖습니다.

타이머 마이그레이션 — dying CPU 의 타이머를 남은 CPU 로 넘기는 두 경로 왼쪽은 죽는 CPU · 오른쪽은 제어 CPU 에서 실행되며, cpuhp 구간에 따라 실행 위치가 갈립니다 죽는 CPU (cpu3) — IRQ 비활성 ① tick_cpu_dying() — CPUHP_AP_TICK_DYING timekeeper 인계: tick_do_timer_cpu 재지정 tick_sched_timer_dying() 로 재인계 방지 tick_offline_cpu() — broadcasting 제거 clockevents_lock 잡고 tick_shutdown() clockevents_exchange_device(dev, NULL) → DETACHED ② hrtimers_cpu_dying() CPUHP_AP_HRTIMERS_DYING teardown 목적지 = active ∩ housekeeping(HK_TYPE_TIMER) old_base 락을 먼저, new_base 락을 중첩 clock base 루프 — 같은 인덱스끼리 이동 __remove_hrtimer(ENQUEUED) 후 base 변경 enqueue_hrtimer(HRTIMER_MODE_ABS) — 만료 유지 smp_call_function_single(ncpu, retrigger_next_event) old_base->online = false 제어 CPU (BSP) 왜 실행 위치가 다른가 cpuhp 는 PREPARE 구간을 제어 CPU 에서, STARTING 구간을 죽는 CPU 에서 실행합니다 두 구간 모두 IRQ 비활성 · 실패 불가 PREPARE 는 죽은 다음에 제어 CPU 로 돌아와 실행되므로 ③ 이 마지막입니다 ③ timers_dead_cpu() CPUHP_TIMERS_PREPARE teardown new_base 락을 먼저, old_base 락을 중첩 forward_timer_base() 로 만료 기준 갱신 running_timer 없음을 확인 후 NULL base 마다 WHEEL_SIZE 벡터 전체 순회 WHEEL_SIZE = 64 × LVL_DEPTH detach_timer() → internal_add_timer() 두 경로의 공통점 — 만료 시각을 유지한 채 옮기고, 두 base 락을 동시에 잡습니다 hrtimer 는 old_base 를 먼저, timer wheel 은 new_base 를 먼저 — base 번호 오름차순 규칙입니다 cpuhp 가 hotplug 을 전역으로 직렬화하므로 락을 두 개씩 잡아도 교착이 발생하지 않습니다
타이머 마이그레이션 — 오프라인 대상 CPU의 타이머를 남은 온라인 CPU로 넘기는 두 경로. hrtimer 경로(hrtimers_cpu_dying, CPUHP_AP_HRTIMERS_DYING)는 죽는 CPU에서 실행되어 목적지 base와 old_base 락을 함께 잡고 8개 clock base를 같은 인덱스끼리 옮긴 뒤, HRTIMER_MODE_ABS 로 만료 시각을 유지해 재삽입하고 대상 CPU에 retrigger_next_event를 요청합니다. timer wheel 경로(timers_dead_cpu, CPUHP_TIMERS_PREPARE)는 PREPARE 구간이므로 죽은 CPU에 대해 제어 CPU가 실행하며, NR_BASES 개 base의 WHEEL_SIZE 벡터를 모두 순회합니다. clock_event_device 해제는 tick_cpu_dying 이 clockevents_lock을 잡은 채 tick_shutdown() 으로 clockevents_exchange_device(dev, NULL) 를 호출해 CLOCK_EVT_STATE_DETACHED 로 전환하고 evtdev를 NULL로 만드는 것으로 이루어집니다 (개념 개요, v7.2 kernel/time/hrtimer.c · kernel/time/timer.c · kernel/time/clockevents.c 및 kernel/cpu.c 기준)
/* kernel/time/hrtimer.c — hrtimer 마이그레이션 상세 */
static void migrate_hrtimer_list(
    struct hrtimer_clock_base *old_base,
    struct hrtimer_clock_base *new_base)
{
    struct hrtimer *timer;
    struct timerqueue_node *node;

    /* RB-tree의 모든 타이머를 순회 */
    while ((node = timerqueue_getnext(&old_base->active))) {
        timer = container_of(node, struct hrtimer, node);
        /* old_base에서 제거 */
        __remove_hrtimer(timer, old_base, HRTIMER_STATE_MIGRATE);
        timer->base = new_base;
        /* new_base에 삽입 (만료 시각 유지) */
        enqueue_hrtimer(timer, new_base, HRTIMER_MODE_ABS);
    }
}
/* NOHZ_FULL 인터랙션 */
/* tick_nohz_cpu_down()은 CPUHP_AP_TICK_NOHZ 상태에서 호출 */
static int tick_nohz_cpu_down(unsigned int cpu)
{
    /* nohz_full CPU가 오프라인되면: */
    /* 1. adaptive-tick 모드 해제 */
    /* 2. 마지막 online housekeeping CPU인지 검사 */
    /* 3. 마지막이면 오프라인 거부 (-EBUSY) */
    if (tick_nohz_cpu_hotpluggable(cpu))
        return 0;
    return -EBUSY;
}
타이머 유형마이그레이션 콜백cpuhp 상태특이사항
hrtimerhrtimers_cpu_dying()CPUHP_AP_HRTIMERS_DYINGRB-tree 순회, 4개 clock base
timer wheeltimers_dead_cpu()CPUHP_TIMERS_DEAD512 슬롯 벡터 전체 순회
clock_eventtick_cleanup_dead_cpu()CPUHP_AP_TICK_DYINGHW 타이머 장치 해제
NOHZ ticktick_nohz_cpu_down()CPUHP_AP_TICK_NOHZ마지막 housekeeping CPU 보호
NOHZ_FULL 주의: nohz_full=로 지정된 CPU 중 하나를 오프라인하는 것은 문제 없지만, 모든 housekeeping CPU(nohz_full에 포함되지 않은 CPU)를 오프라인하려 하면 -EBUSY로 거부됩니다. 최소 1개의 housekeeping CPU가 반드시 온라인이어야 합니다.

Per-CPU 데이터 처리

Per-CPU 변수 자체는 CPU 오프라인 시 해제되지 않습니다. 메모리는 cpu_possible_mask 기준으로 부팅 시 할당되므로, 오프라인 CPU의 per-CPU 영역도 유지됩니다. 그러나 서브시스템은 자체 콜백에서 per-CPU 데이터를 적절히 정리해야 합니다.

/* 예: perf 서브시스템의 per-CPU 정리 */
static int perf_event_exit_cpu(unsigned int cpu)
{
    struct perf_cpu_context *cpuctx = per_cpu_ptr(&perf_cpu_context, cpu);
    /* 해당 CPU의 모든 perf 이벤트를 다른 CPU로 이동 또는 비활성화 */
    perf_event_exit_cpu_context(cpuctx);
    return 0;
}
메모리 해제: per-CPU 메모리가 cpu_possible_mask 기준이므로, nr_cpu_ids(또는 maxcpus= 부팅 파라미터)가 크면 미사용 메모리가 낭비될 수 있습니다. possible_cpus=로 제한할 수 있습니다.

스케줄러 통합

CPU Hotplug 시 스케줄러는 sched_domain 계층을 재구성해야 합니다.

sched_domain 3단계 계층과 CPU 3 오프라인 재구성 아래로 갈수록 span 이 줄어듭니다 · 코어 하나가 통째로 사라지는 게 아니라 CPU 3 만 빠집니다 NUMA 노드 MC 그룹 Core (SMT) 해체된 도메인 코어 n의 스레드는 2n 과 2n+1 ① CPU 3 온라인 8 논리 CPU · NUMA 노드 1개 NUMA 노드 0 · span {0,1,2,3,4,5,6,7} flags: SD_NUMA | SD_SERIALIZE MC 그룹 A · span {0,1,2,3} SD_SHARE_CPUCAPACITY · SD_PREFER_SIBLING Core 0 · {0,1} CPU 0 CPU 1 Core 1 · {2,3} CPU 2 CPU 3 MC 그룹 B · span {4,5,6,7} SD_SHARE_CPUCAPACITY · SD_PREFER_SIBLING Core 2 · {4,5} CPU 4 CPU 5 Core 3 · {6,7} CPU 6 CPU 7 ② CPU 3 오프라인 후 set_cpu_active(3, false) → partition_sched_domains() 재계산 NUMA 노드 0 · span {0,1,2,4,5,6,7} flags: SD_NUMA | SD_SERIALIZE — 변경 없음 MC 그룹 A · span {0,1,2} SD_SHARE_CPUCAPACITY · SD_PREFER_SIBLING Core 0 · {0,1} CPU 0 CPU 1 Core 1 해체 → {2} CPU 2 MC 그룹 B · span {4,5,6,7} 변경 없음 — CPU 3 은 이 그룹에 없었습니다 Core 2 · {4,5} CPU 4 CPU 5 Core 3 · {6,7} CPU 6 CPU 7 재구성 규칙 — partition_sched_domains() 가 cpu_active_mask 기준으로 다시 계산 안 바뀐 도메인은 그대로 재사용하고, CPU 3 을 담은 SMT 도메인은 detach_destroy_domains() 로 분리됩니다 분리된 CPU 3 은 def_root_domain 에 붙고, CPU 2 는 새 SMT 도메인 없이 MC 그룹 A 에 직접 속합니다
sched_domain 3단계 계층과 CPU 3 오프라인 재구성 — 위는 오프라인 전, 아래는 set_cpu_active(3, false) 이후 partition_sched_domains() 가 cpu_active_mask 기준으로 다시 계산한 상태입니다. 스레드가 하나만 남은 Core 1 은 두 CPU 사이의 용량 공유가 사라지므로 통째로 해체되고 CPU 2 는 MC 그룹 A 에 직접 붙습니다. 분리된 CPU 3 은 cpu_attach_domain(NULL, &def_root_domain) 로 단일 루트 도메인에 연결됩니다. MC 그룹 B 와 NUMA 도메인은 CPU 3 을 담고 있지 않아 재사용됩니다 (개념 개요, v7.2 kernel/sched/topology.c 및 include/linux/sched/sd_flags.h 기준)
rebuild_sched_domains(): kernel/sched/topology.c의 rebuild_sched_domains()는 CPU 상태 변경 시 호출되어 전역 sched_domain 계층을 재계산합니다. 각 domain의 span(CPU 마스크), flags(도메인 속성), balance_interval(로드 밸런싱 주기)를 갱신합니다. NUMA 간 imbalance 시 SD_NUMA 도메인에서 cross-node balancing이 작동합니다.
/* kernel/sched/topology.c */
static int sched_cpu_deactivate(unsigned int cpu)
{
    /* 1. cpu_active_mask에서 제거 */
    set_cpu_active(cpu, false);

    /* 2. sched_domain 재빌드 요청 */
    cpuset_cpu_inactive(cpu);

    /* 3. 해당 CPU의 실행 가능 태스크 이동 */
    balance_push_set(cpu, true);

    sched_cpu_dying(cpu);
    return 0;
}

static int sched_cpu_activate(unsigned int cpu)
{
    set_cpu_active(cpu, true);
    /* sched_domain 재빌드 → 로드 밸런싱 영역 갱신 */
    cpuset_cpu_active(cpu);
    return 0;
}

전원 관리 연동

CPU Hotplug은 시스템 전원 관리의 핵심 메커니즘입니다.

시나리오CPU Hotplug 역할
Suspend (S3)BSP 제외 모든 CPU 오프라인 → S3 진입 → 재개 시 전체 온라인
Hibernate (S4)S3과 동일하게 CPU 오프라인 후 디스크에 이미지 저장
cpuidle깊은 C-state 진입 시 per-CPU 타이머/IRQ 고려 (hotplug과 유사)
CPU isolationisolcpus= 부팅 파라미터로 스케줄러에서 제외
/* kernel/power/suspend.c — suspend 시 non-boot CPU 오프라인 */
static int suspend_disable_secondary_cpus(void)
{
    int error;
    error = freeze_secondary_cpus(0);  /* CPU 0(BSP) 제외 모두 오프라인 */
    return error;
}

/* resume 시 */
static void suspend_enable_secondary_cpus(void)
{
    thaw_secondary_cpus();  /* 모든 CPU 재온라인 */
}
# 런타임 CPU 격리 (RT 태스크 전용 CPU 확보)
# 방법 1: 부팅 파라미터
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3

# 방법 2: 런타임 hotplug (더 유연)
echo 0 > /sys/devices/system/cpu/cpu2/online
echo 0 > /sys/devices/system/cpu/cpu3/online
taskset -c 2,3 ./rt_workload  # 오프라인이면 실패!

# 방법 3: cpuset cgroup (권장)
echo 2-3 > /sys/fs/cgroup/rt-tasks/cpuset.cpus

가상화 환경

가상 머신에서 vCPU의 동적 추가/제거도 CPU Hotplug 메커니즘을 사용합니다.

vCPU hotplug — 호스트는 알리기고, 게스트가 cpuhp 상태 머신을 직접 돌립니다 가운데 빈 공간이 hypervisor 경계입니다 · 게스트 안의 online/offline 경로는 네이티브 CPU 와 동일합니다 hypervisor 경계 ① vCPU 추가 (hot-add) 호스트 (QEMU/KVM) ① QMP device_add (cpu-add) ② KVM_CREATE_VCPU ioctl ③ 게스트에 알림 — vCPU 생성 통보 게스트 커널 ④ arch_register_cpu(cpu) ⑤ __cpu_up() → _cpu_up() ⑥ cpuhp 상태 머신 → CPUHP_ONLINE 인터럽트 ② vCPU 제거 (hot-del) 호스트 (QEMU/KVM) 게스트 teardown 가 끝날 때까지 vCPU 스레드를 그대로 유지합니다 ⑨ QMP device_del 게스트 커널 ⑦ cpu_offline() → _cpu_down() ⑧ teardown 역순 + 자원 이관 CPUHP_OFFLINE — vCPU 정지 완료 통보 vCPU 추가의 상한 — maxcpus QEMU 의 maxcpus=N 이 게스트 MADT 의 _MAX_CPUS 로 실려, 게스트의 cpu_possible_mask 를 결정합니다 이 마스크에 없는 CPU 는 cpu_up() 대상이 될 수 없습니다 — 추가 전에 maxcpus 여유를 확보해야 합니다
vCPU hotplug 흐름 — 호스트는 vCPU를 생성하고 알리기만 하고, 실제 online/offline 전환은 게스트 커널이 자기 cpuhp 상태 머신으로 수행합니다. 추가 경로(arch_register_cpu → __cpu_up → _cpu_up → PREPARE/STARTING/ONLINE)와 제거 경로( cpu_offline → _cpu_down → teardown 역순)는 네이티브 CPU hotplug과 동일하며, 게스트의 cpu_possible_mask 가 결정하는 상한 안에서만 동작합니다. 게스트 teardown 이 끝나기 전에 호스트가 vCPU 스레드를 없애면 자원이 유실되므로 제거는 게스트 쪽이 먼저 완료되어야 합니다 (가상화 연동 개념도, v7.2 kernel/cpu.c 및 include/linux/cpu.h 기준)
# QEMU에서 vCPU 핫플러그 (QMP)
# 1. maxcpus 설정으로 VM 시작 (실제 sockets x cores x threads 조합)
qemu-system-x86_64 -smp 2,maxcpus=4 -cpu host ...

# 2. QMP로 vCPU 추가
{ "execute": "device_add",
  "arguments": { "driver": "host-x86_64-cpu", "socket-id": 0,
                 "core-id": 2, "thread-id": 0, "id": "cpu-2" } }

# 3. 게스트에서 확인
lscpu | grep "CPU(s):"
cat /sys/devices/system/cpu/online

ARM64 특성

ARM64에서 secondary CPU를 깨우는 방법은 두 가지입니다.

방식메커니즘사용 환경
PSCISecure Monitor(EL3) 호출: psci_cpu_on()대부분의 프로덕션 SoC (ATF/OP-TEE)
spin-tablerelease_addr에 진입점(Entry Point) 기록 → SEV로 깨움간단한 펌웨어(Firmware), 개발 보드
/* arch/arm64/kernel/psci.c */
static int cpu_psci_cpu_boot(unsigned int cpu)
{
    phys_addr_t pa = __pa_symbol(secondary_entry);
    int err;

    /* PSCI CPU_ON 호출: 타겟 MPIDR + 진입점 */
    err = psci_ops.cpu_on(cpu_logical_map(cpu), pa);
    return err;
}

static int cpu_psci_cpu_kill(unsigned int cpu)
{
    int err;
    /* CPU 실제 전원 차단을 위해 PSCI AFFINITY_INFO 폴링 */
    do {
        err = psci_ops.affinity_info(cpu_logical_map(cpu), 0);
    } while (err != PSCI_0_2_AFFINITY_LEVEL_OFF);
    return 0;
}
ARM64 PSCI CPU bring-up — EL1 → EL3 → EL1 로 넘어가는 3단 경로 권한 구역은 세로로 나뉘며, 화살표는 두 번의 예외 레벨 전환을 나타냅니다 EL1 — Linux 커널 (부팅 CPU) ① cpu_psci_cpu_boot() — arch/arm64/kernel/psci.c arch_cpuhp_kick_ap_alive() → psci_ops.cpu_on(cpuid, entry_point) cpuid = cpu_logical_map(cpu) — Linux CPU 번호가 아닌 MPIDR 기반 값 entry_point = __pa_symbol(secondary_entry) — 물리 주소, 타깃 CPU 는 MMU off 로 점프 SMC (SMCCC) — x1 에 함수 ID EL3 — ATF (Trusted OS) ② PSCI CPU_ON 처리 — 전원 ON + 리셋 후 진입점으로 분기 psci_tos_resident_on() 이 참이면 CPU_ON 과 CPU_OFF 를 거부합니다 그래서 커널은 -EPERM 을 오류로 로그하지 않습니다 CPU 를 깨워 물리 주소로 직접 분기 EL1 — 타깃 CPU 가 부팅 ③ secondary_start_kernel() — MMU off 상태로 PA 에 진입 cpu_uninstall_idmap() → rcutree_report_cpu_starting(cpu) check_local_cpu_capabilities() · store_cpu_topology(cpu) notify_cpu_starting() — Enable GIC and timers (IRQ 비활성) set_cpu_online(true) → cpu_startup_entry(CPUHP_AP_ONLINE_IDLE) SMC 인자 배치 — PSCI_0_2_FN_BASE(0x84000000) + 함수 번호 x1 = 0x84000003 (PSCI_0_2_FN_CPU_ON) · x2 = 대상 CPU 의 MPIDR 값 · x3 = secondary_entry 물리 주소 CPU_OFF 는 0x84000002, AFFINITY_INFO 는 0x84000004 — kill 경로에서 전원 OFF 확인에 쓰입니다
ARM64 PSCI CPU bring-up 3단 경로 — ① 부팅 CPU 의 Linux 커널이 cpu_psci_cpu_boot() → psci_ops.cpu_on() 으로 PSCI_CPU_ON 을 요청하고, ② EL3 의 Trusted OS 가 전원을 켜고 리셋한 뒤 진입점으로 분기하며, ③ 타깃 CPU 가 EL1로 돌아와 secondary_start_kernel() 부터 온라인까지 진행합니다. cpuid 인자는 cpu_logical_map() 이 산출한 MPIDR 기반 값이고 진입점은 __pa_symbol() 로 물리 주소로 변환됩니다. 타깃 CPU 가 MMU 꺼진 상태로 점프하므로 물리 주소여야 하며, Trusted OS 가 거절한 -EPERM 은 정상 경로로 취급됩니다 (ARM64 아키텍처 구현, v7.2 arch/arm64/kernel/psci.c · arch/arm64/kernel/smp.c 및 include/uapi/linux/psci.h 기준)

x86 특성

x86에서는 INIT/SIPI(Startup IPI) 시퀀스로 AP(Application Processor)를 시작합니다.

/* arch/x86/kernel/smpboot.c */
static int do_boot_cpu(int apicid, int cpu,
                       struct task_struct *idle)
{
    /* 1. 트램펄린 코드를 1MB 이하 물리 메모리에 복사 */
    /*    (AP는 리얼 모드에서 시작하므로) */
    copy_trampoline_code();

    /* 2. INIT IPI → 100ms 대기 → SIPI 2회 전송 */
    apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT | APIC_DM_INIT,
                   apicid);
    udelay(10000);  /* 10ms */
    apic_icr_write(APIC_DM_INIT, apicid);  /* de-assert */
    udelay(10000);

    /* SIPI: 벡터 주소 = trampoline_base >> 12 */
    apic_icr_write(APIC_DM_STARTUP | (trampoline_phys >> 12), apicid);
    udelay(300);
    /* 두 번째 SIPI (MP Spec 권장) */
    apic_icr_write(APIC_DM_STARTUP | (trampoline_phys >> 12), apicid);

    /* 3. AP가 start_secondary()에 도달할 때까지 대기 */
    wait_for_ap_online(cpu);
    return 0;
}
x86 AP 기동 — INIT/SIPI 는 세 갈래 분기의 마지막 폴백입니다 BSP 가 AP 를 깨우는 방법은 하나가 아니며, 아래 두 구역이 각각 다르게 동작합니다 ① do_boot_cpu() — arch/x86/kernel/smpboot.c initial_code = start_secondary · idle 스택 설정 · init_espfix_ap() · smp_mb() apic->wakeup_secondary_cpu_64 64비트 모드로 직접 깨웁니다 이 경로가 가장 우선입니다 apic->wakeup_secondary_cpu real mode 로 깨웁니다 64비트 방법이 없을 때 wakeup_secondary_cpu_via_init() INIT + STARTUP 폴백 아무 방법도 없을 때만 ② INIT / STARTUP 폴백 — BSP 가 하는 일 INIT assert → deassert udelay(init_udelay) — 최신 CPU 는 0 STARTUP(SIPI) 전송 num_starts = 2 또는 0 (APIC_INTEGRATED) 진입점을 벡터로 전달 apic_icr_write(APIC_DM_STARTUP | start_eip >> 12) 전송 완료 확인 safe_apic_wait_icr_idle() + ESR, 오류 시 break preempt_disable() 로 감싸여 실행됩니다 ③ start_secondary() — AP 가 C 코드로 진입 cr4_init() · load_ucode_ap() 64비트는 이미 paging 이 설정된 상태 cpuhp_ap_sync_alive() 제어 CPU 와의 동기화 지점 cpu_init() · TSC 동기화 확인 rcutree_report_cpu_starting() set_cpu_online(true) → local_irq_enable() IRQ 는 여기서서야 켜집니다 cpu_startup_entry(CPUHP_AP_ONLINE_IDLE) 조건부 값 — INIT 대기 시간과 STARTUP 횟수는 고정값이 아닙니다 init_udelay: Intel Pentium Pro 이후 / Hygon 0x18 이후 / AMD 0xF 이후 는 0, 그보다 구형이면 10000 (10ms) num_starts: APIC_INTEGRATED() 면 2회, 아니면 0회 — 커맨드라인 cpu_init_udelay= 로 덮어씁니다 MP 규격이 권하는 10ms 대기가 현대 CPU 의 부팅을 늦춘다고 소스 주석이 밝힙니다
x86 AP 기동 경로 — do_boot_cpu() 는 APIC 드라이버가 제공하는 방법에 따라 세 갈래로 분기하며, INIT/SIPI 는 마지막 폴백입니다. 폴백 경로에서도 INIT 은 assert 와 deassert 두 번이고, STARTUP 은 APIC 버전에 따라 2회 또는 0회 전송됩니다. 진입점 주소는 STARTUP 벡터의 상위 비트에 실려 실(real) 모드에서 읽힙니다. AP 는 트램펄린을 거쳐 64비트 paging 이 이미 설정된 상태로 start_secondary() 에 진입하며, IRQ 는 set_cpu_online() 뒤에야 켜집니다 (x86 아키텍처 구현, v7.2 arch/x86/kernel/smpboot.c 기준)

디버깅

CPU Hotplug 문제를 디버깅하는 주요 도구와 방법입니다.

# cpuhp 상태 머신 전체 상태 확인
cat /sys/devices/system/cpu/hotplug/states

# 특정 CPU의 현재 cpuhp 상태
cat /sys/devices/system/cpu/cpu3/hotplug/state
cat /sys/devices/system/cpu/cpu3/hotplug/target
cat /sys/devices/system/cpu/cpu3/hotplug/fail

# ftrace로 hotplug 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
echo 0 > /sys/devices/system/cpu/cpu3/online
cat /sys/kernel/debug/tracing/trace

# trace events: cpuhp_enter, cpuhp_exit, cpuhp_multi_enter
# 출력 예:
# cpuhp_enter: cpu=3 target=206 step=202 (perf/x86:starting)
# cpuhp_exit:  cpu=3 state=202 step=202 ret=0

# stress test (반복 on/off)
for i in $(seq 1 100); do
    echo 0 > /sys/devices/system/cpu/cpu3/online
    echo 1 > /sys/devices/system/cpu/cpu3/online
done

# 커널 로그에서 hotplug 관련 메시지
dmesg | grep -i "cpu.*online\|cpu.*offline\|hotplug\|smpboot"
흔한 문제: CPU 오프라인이 "hang" 상태가 되면, 대부분 콜백 내 데드락입니다. /sys/devices/system/cpu/cpu3/hotplug/fail에 실패한 상태 번호가 기록되며, /sys/devices/system/cpu/hotplug/states에서 해당 번호의 콜백 이름을 확인할 수 있습니다.

실전 활용

CPU Hotplug의 대표적인 실전 시나리오와 스크립트입니다.

# 시나리오 1: RT 태스크용 CPU 격리 (hotplug + cset)
# CPU 2,3을 일반 워크로드에서 제외
cset shield --cpu 2-3 --kthread=on
taskset -c 2-3 chrt -f 90 ./rt_application

# 시나리오 2: 에너지 절약 (서버 낮은 부하)
# 현재 부하 기반으로 불필요한 CPU 오프라인
LOAD=$(cat /proc/loadavg | cut -d' ' -f1 | cut -d'.' -f1)
ONLINE=$(nproc)
TARGET=$((LOAD + 2))
if [ "$ONLINE" -gt "$TARGET" ]; then
    for cpu in $(seq $TARGET $((ONLINE-1))); do
        echo 0 > /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null
    done
fi

# 시나리오 3: MCE 발생 CPU 격리
# /var/log/mcelog에서 에러 CPU 확인 후 격리
echo 0 > /sys/devices/system/cpu/cpu7/online
echo "CPU 7 isolated due to MCE" | logger
시나리오명령어주의사항
NUMA 노드 격리for cpu in $(cat /sys/devices/system/node/node1/cpulist | tr ',' ' '); do echo 0 > .../cpu$cpu/online; done메모리는 오프라인 안 됨
대칭 멀티코어echo 0 > /sys/devices/system/cpu/cpu{1,3,5,7}/onlineSMT 비활성화 효과 (보안)
커널 업데이트 테스트반복 on/off 스트레스 테스트lockdep, KASAN 활성화 권장

동기화 메커니즘

CPU Hotplug 과정에서의 동기화는 percpu rwsem 기반의 cpu_hotplug_lock으로 보호됩니다.

/* include/linux/cpu.h — CPU hotplug 잠금 API */

/* 읽기 잠금: CPU 상태 변경을 차단 (여러 스레드 동시 사용 가능) */
void cpus_read_lock(void);    /* = get_online_cpus() (구식 이름) */
void cpus_read_unlock(void);

/* 쓰기 잠금: CPU 상태 변경 시 (커널 내부 전용) */
void cpus_write_lock(void);
void cpus_write_unlock(void);

/* 사용 예: for_each_online_cpu를 안전하게 사용 */
cpus_read_lock();
for_each_online_cpu(cpu) {
    /* 이 루프 중 CPU가 오프라인되지 않음 보장 */
    per_cpu(my_data, cpu) = compute_value(cpu);
}
cpus_read_unlock();
cpu_hotplug_lock — per-CPU 읽기 카운터가 만드는 쓰기 게이트 읽는 쪽은 CPU 마다 독립이고, 쓰는 쪽만 전부 기다립니다 ① cpu_hotplug_lock 의 정체 DEFINE_STATIC_PERCPU_RWSEM(cpu_hotplug_lock) — kernel/cpu.c:484 cpus_read_lock() = percpu_down_read() — 자기 CPU 의 read_count 만 증가시킵니다 cpus_write_lock() = percpu_down_write() — 모든 CPU 의 read_count 가 0 이 될 때까지 대기합니다 ② 읽기 — CPU 마다 독립 CPU 0 read_count = 1 CPU 1 read_count = 1 CPU 2 read_count = 1 서로 다른 CPU 의 reader 는 경합하지 않습니다 같은 CPU 에서 중첩 read_lock 만 카운트를 올립니다 게이트 ③ 쓰기 — 전부 기다림 CPU 0 read_count = 0 CPU 1 read_count = 0 writer 진입 writer 는 모든 CPU 의 read_count 가 0 이 될 때까지 대기한 뒤 배타적으로 진입합니다 ④ _cpu_down() 의 임계 구간 — kernel/cpu.c:1415~1468 cpu_maps_update_begin() — cpu_add_remove_lock (mutex) cpus_write_lock() — 모든 CPU 의 read_count 가 0 일 때까지 대기 housekeeping CPU 를 하나 이상 남기는지 확인 → 아니면 -EBUSY cpuhp_down_callbacks() — PREPARE 구간 teardown cpus_write_unlock() cpu_maps_update_done()
cpu_hotplug_lock 상호작용 — 이 잠금은 전역 읽기 쓰기 잠금이 아니라 per-CPU 읽기 카운터(read_count)를 가진 형태(DEFINE_STATIC_PERCPU_RWSEM)입니다. cpus_read_lock() 은 자기 CPU 의 카운터만 올리므로 서로 다른 CPU 의 reader 는 서로를 막지 않고, cpus_write_lock() 만 모든 CPU 의 카운터가 0 이 될 때까지 기다립니다. _cpu_down() 은 그 임계 구간을 cpu_add_remove_lock 이라는 별도의 뮤텍스가 감싼 상태로 실행됩니다. 따라서 cpus_read_lock() 을 쥔 채 cpu_down() 을 호출하면 같은 잠금에 쓰기를 요청하게 되어 영원히 블록됩니다 (개념 개요, v7.2 kernel/cpu.c 및 include/linux/percpu-rwsem.h 기준)
데드락 주의: cpus_read_lock() 안에서 cpu_up()/cpu_down()을 호출하면 데드락입니다. 읽기 잠금 보유 중 쓰기 잠금 요청은 영원히 블록됩니다. lockdep이 이를 감지하여 경고합니다.

Workqueue 마이그레이션 전략

Workqueue는 CPU Hotplug에서 특히 복잡한 처리가 필요한 서브시스템입니다. bound workqueue(per-CPU)와 unbound workqueue의 동작이 근본적으로 다르기 때문입니다.

per-CPU worker pool — 풀은 남고, 동시성 관리만 꺼집니다 오프라인된 CPU 의 per-CPU bound workqueue 가 어떻게 바뀌는지 ① 오프라인 — CPUHP_AP_WORKQUEUE_ONLINE 의 teardown workqueue_offline_cpu() 로컬 CPU 에서 실행되어야 합니다 WARN_ON(cpu != smp_processor_id()) unbind_workers(cpu) per-CPU 풀마다 wq_pool_attach_mutex 와 pool->lock 을 잡고 처리합니다 worker->flags |= WORKER_UNBOUND pool->flags |= POOL_DISASSOCIATED pool->nr_running = 0 kick_pool() · unbind_worker() worker 를 unbound 풀로 이동하고 wq_online_cpumask 에서 CPU 를 지웁니다 새 work 는 더 이상 이 풀에 들어오지 못합니다 unbound 풀은 별도로 계속 존재합니다 ② 같은 풀의 상태 변화 온라인 전 — associated pool->flags 에 POOL_DISASSOCIATED 없음 모든 worker 가 해당 CPU 에 고정됨 WORKER_UNBOUND 미설정 동시성 관리 동작 — max_active 적용 pending work 는 이 CPU 만 처리 오프라인 후 — POOL_DISASSOCIATED CPU 는 오프라인일 수 있습니다 모든 worker 에 WORKER_UNBOUND 설정 동시성 관리 꺼짐 — nr_running 은 0 유지 need_more_worker()·keep_working() 항상 참 worker 는 어느 CPU 에서든 실행 가능 이후 붙는 worker 도 unbound 로 추가 풀 자체는 원래 자리에 남습니다 unbound 풀처럼 동작합니다 온라인 복원 — workqueue_online_cpu() → rebind_workers() kthread_set_per_cpu() · set_cpus_allowed_ptr() 로 CPU 고정을 되돌린 뒤 WORKER_UNBOUND 를 해제합니다 thread 재생성이 아니라 재바인딩이며, 풀과 worker 는 그대로 유지됩니다
per-CPU worker pool 마이그레이션 — 오프라인된 CPU 의 per-CPU bound workqueue 는 다른 CPU 의 bound pool 로 work 를 넘기는 방식이 아니라, 자기 자리에 남으면서 동시성 관리를 끄는 방식으로 바뀝니다. workqueue_offline_cpu() 가 unbind_workers() 로 모든 worker 에 WORKER_UNBOUND 를, 풀에 POOL_DISASSOCIATED 를 설정하고 nr_running 을 0 으로 만들면 그 풀은 unbound 풀처럼 동작하며 worker 는 어느 CPU 에서든 실행될 수 있습니다. 이 작업은 CPUHP_AP_WORKQUEUE_ONLINE 상태의 teardown 콜백이므로 죽는 CPU 자신에서 실행되며, 반대 방향인 rebind_workers() 는 CPU 가 다시 올라올 때 thread 를 새로 만들지 않고 고정을 되돌립니다 (개념 개요, v7.2 kernel/workqueue.c 및 kernel/cpu.c 기준)
/* kernel/workqueue.c — CPU offline 처리 */
static int workqueue_offline_cpu(unsigned int cpu)
{
    struct worker_pool *pool;

    /* 1. per-CPU bound pool: pending work 드레인 */
    for_each_cpu_worker_pool(pool, cpu) {
        /* 실행 중인 work 완료까지 대기 */
        pool->flags |= POOL_DISASSOCIATED;
        /* worker thread unbind → 다른 CPU에서 실행 가능 */
        unbind_workers(pool);
    }

    /* 2. unbound pool: CPU affinity mask 갱신만 */
    wq_update_unbound_numa(cpu, false);

    return 0;
}
/* kernel/workqueue.c — CPU online 복원 */
static int workqueue_online_cpu(unsigned int cpu)
{
    struct worker_pool *pool;

    /* 1. per-CPU pool 재활성화 */
    for_each_cpu_worker_pool(pool, cpu) {
        pool->flags &= ~POOL_DISASSOCIATED;
        /* 새 worker thread 생성 (필요 시) */
        rebind_workers(pool);
    }

    /* 2. unbound pool NUMA affinity 복원 */
    wq_update_unbound_numa(cpu, true);

    return 0;
}
Workqueue 유형CPU Offline 시 동작영향도
Per-CPU bound (system_wq)pending work 드레인 → worker unbind높음: work 지연 발생
WQ_UNBOUND (system_unbound_wq)NUMA affinity mask 갱신만낮음: 자동 재분배
Ordered (alloc_ordered_workqueue)순서 보장(Ordering) 유지 (내부 unbound)낮음
WQ_HIGHPRInice -20 pool도 동일 드레인높음
WQ_CPU_INTENSIVEconcurrency 관리 제외, 동일 드레인보통
/* ordered workqueue 생성 예 */
struct workqueue_struct *ordered_wq;
ordered_wq = alloc_ordered_workqueue("my_ordered", 0);
/* max_active = 1, 내부적으로 unbound → CPU hotplug에 안전 */
/* work 실행 순서가 큐잉 순서와 동일하게 보장됨 */

/* per-CPU workqueue에서 flush 후 재큐잉 패턴 */
flush_workqueue(system_wq);
/* CPU 오프라인 후 새 work는 다른 CPU pool에 큐잉됨 */
draining 지연: per-CPU workqueue에 장시간 실행되는 work가 있으면, CPU 오프라인이 해당 work 완료까지 블로킹됩니다. alloc_workqueue() 시 WQ_MEM_RECLAIM 플래그가 있으면 rescuer thread가 개입하여 교착을 방지합니다.
CMWQ (Common Multi-Workqueue) drain 과정 상세: CPU 오프라인 시 CPUHP_WORKQUEUE_DYING 단계에서 drain_local_pool()이 호출됩니다. 이 함수는 다음 단계를 수행합니다: (1) 현재 CPU의 bound pool을 POOL_DISASSOCIATED 상태로 전환하여 새로운 work 큐잉을 차단, (2) 실행 중인 work가 완료될 때까지 대기(wait_for_idle()), (3) pend 중인 work를 move_linked_works()로 다른 CPU의 pool로 재배치, (4) worker thread를 unbind하여 cpu_active_mask 밖의 CPU에서 실행 가능하게 합니다. WQ_UNBOUND 풀은 이 과정이 면제되며, NUMA affinity mask만 갱신됩니다.
best practice: hotplug 빈번한 환경에서는 WQ_UNBOUND 또는 alloc_ordered_workqueue()를 사용하면 마이그레이션 오버헤드(Overhead)를 피할 수 있습니다. per-CPU workqueue는 캐시(Cache) 지역성이 중요한 경우에만 사용하세요.

일반적인 경쟁 조건(Race Condition)과 디버깅

CPU Hotplug은 비동기적으로 CPU 상태를 변경하므로, 다양한 경쟁 조건이 발생할 수 있습니다. 가장 흔한 패턴과 해결 방법을 살펴봅니다.

cpu_online_mask 경쟁 — 스냅샷이 아닌 루프가 죽는 CPU 를 건드립니다 for_each_online_cpu() 는 매 반복마다 공유 마스크를 다시 읽습니다 ① for_each_online_cpu() 는 스냅샷이 아닙니다 for_each_online_cpu(cpu) ≡ for_each_cpu(cpu, cpu_online_mask) for_each_set_bit() 가 매 반복마다 공유 마스크를 다시 읽습니다 따라서 커서는 앞으로만 가고, 뒤의 비트 변화는 되돌아보지 않습니다 ② 경주 시나리오 — Reader 루프와 Writer 오프라인이 겹치는 순간 Reader for_each_online_cpu() cpu3 을 반환한 직후 본문 실행 중 — 아직 여기 per_cpu(cpu3) 접근 커서는 앞으로만 갑니다 — 이미 지나간 CPU 를 다시 돌려보지 않습니다 Writer cpu_down(cpu3) cpu_down(cpu3) 진입 cpu3 오프라인 완료 · 비트 해제 CPU 내부 자원 정리 진행 결과: 시점3에서 Reader 는 죽고 있는 cpu3 의 per-CPU 데이터에 접근합니다 해결 — writer 를 막으면 마스크가 고정됩니다 cpus_read_lock() / cpus_read_unlock() 로 감싸면 hotplug 이 진행되지 못합니다 해제 후에도 남는 성질 이미 지나간 CPU 를 되돌려 보지 않습니다 그래서 순회 중 CPU 상태 변경을 막아야 합니다 읽기만 하는 경우에도 cpus_read_lock() 은 필요합니다 비트가 켜지는 쪽으로는 조용히 넘어가지만, 꺼지는 쪽에서는 죽는 CPU 를 건드리기 때문입니다
cpu_online_mask 경쟁 조건 — for_each_online_cpu() 는 공유 마스크를 매 반복마다 다시 읽는 반복문이라 스냅샷이 아닙니다. 그래서 Reader 가 cpu3 을 반환한 뒤 본문을 실행하는 사이에 Writer 가 cpu3 을 오프라인시켜 비트를 해제해도, Reader 의 커서는 이미 지나갔으므로 그 CPU 를 그대로 처리합니다. 결과는 죽고 있는 CPU 의 per-CPU 데이터 접근이며, cpus_read_lock() / cpus_read_unlock() 로 writer 를 막아 순회 중 CPU 상태가 바뀌지 않도록 하는 것이 표준 해결책입니다 (개념 개요, v7.2 include/linux/cpumask.h 및 kernel/cpu.c 기준)

가장 빈번한 4가지 경쟁 조건 패턴입니다.

/* 패턴 1: cpu_online_mask 경쟁 — 잘못된 코드 */
for_each_online_cpu(cpu) {
    /* ← 이 루프 도중 cpu가 오프라인될 수 있음! */
    per_cpu(my_data, cpu) = do_work(cpu);
}

/* 패턴 1: 올바른 코드 */
cpus_read_lock();
for_each_online_cpu(cpu) {
    per_cpu(my_data, cpu) = do_work(cpu);
}
cpus_read_unlock();

/* 패턴 2: per-cpu 변수 접근 — 잘못된 코드 */
int val = *per_cpu_ptr(&my_counter, target_cpu);
/* target_cpu가 오프라인이면 데이터가 stale/invalid */

/* 패턴 2: 올바른 코드 */
cpus_read_lock();
if (cpu_online(target_cpu)) {
    val = *per_cpu_ptr(&my_counter, target_cpu);
}
cpus_read_unlock();
/* 패턴 3: IRQ affinity 설정 실패 */
ret = irq_set_affinity(irq, cpumask_of(target_cpu));
/* target_cpu가 오프라인이면 -EINVAL 반환 */
/* 해결: cpu_online() 확인 또는 cpus_read_lock() */

/* 패턴 4: hotplug 콜백 내에서 GFP_KERNEL 할당 */
/* Starting 콜백은 IRQ 비활성 상태 → 슬립 불가! */
/* Prepare 콜백에서 메모리를 미리 할당해야 함 */
# ftrace로 hotplug 경쟁 조건 디버깅
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/cpuhp_enter/enable
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/cpuhp_exit/enable
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/cpuhp_multi_enter/enable

# lockdep으로 교착 감지
# CONFIG_PROVE_LOCKING=y 활성화 후
dmesg | grep "possible circular locking"
# cpus_read_lock ↔ mutex ↔ cpus_write_lock 순환 패턴 확인

# hotplug tracepoint 출력 예:
#   cpuhp_enter: cpu=3 target=0 step=202 (perf/x86:starting) ret=0
#   cpuhp_exit:  cpu=3 state=202 step=201 ret=0
lockdep 활용: CONFIG_PROVE_LOCKING=y와 CONFIG_LOCKDEP=y를 활성화하면, cpus_read_lock() 안에서 cpu_up()/cpu_down()을 호출하는 교착 패턴을 컴파일 시점이 아닌 런타임에 즉시 감지합니다. 개발/테스트 환경에서는 반드시 활성화하세요.

Hotplug 성능/지연 분석

CPU online/offline 작업의 소요 시간은 등록된 콜백 수와 각 콜백의 처리 시간에 따라 크게 달라집니다. 대규모 시스템(100+ CPU)에서는 hotplug 지연이 수백 밀리초에 이를 수 있어 성능 분석이 중요합니다.

# 방법 1: ftrace로 각 콜백 소요 시간 측정
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/enable
echo 1 > /sys/kernel/debug/tracing/options/latency-format

echo 0 > /sys/devices/system/cpu/cpu3/online

# trace에서 각 cpuhp_enter ~ cpuhp_exit 시간 차이 분석
cat /sys/kernel/debug/tracing/trace | grep cpuhp
# 출력 예:
#  [001]   125.300: cpuhp_enter: cpu=3 target=0 step=190 (perf:starting)
#  [001]   125.312: cpuhp_exit:  cpu=3 state=190 step=189 ret=0
# → perf:starting 콜백에 12ms 소요
# 방법 2: 직접 시간 측정 스크립트
measure_hotplug() {
    local cpu=$1
    local start end elapsed

    start=$(date +%s%N)
    echo 0 > /sys/devices/system/cpu/cpu${cpu}/online
    end=$(date +%s%N)
    elapsed=$(( (end - start) / 1000000 ))
    echo "CPU${cpu} offline: ${elapsed}ms"

    start=$(date +%s%N)
    echo 1 > /sys/devices/system/cpu/cpu${cpu}/online
    end=$(date +%s%N)
    elapsed=$(( (end - start) / 1000000 ))
    echo "CPU${cpu} online: ${elapsed}ms"
}
# 사용: measure_hotplug 3
콜백 단계대표적 비용 높은 콜백상대 비용원인
PrepareCPUHP_PERF_PREPARE중간메모리 할당, 버퍼(Buffer) 초기화
PrepareCPUHP_WORKQUEUE_PREP중간worker pool 준비
StartingCPUHP_AP_IRQ_GIC_STARTING작음GIC redistributor 설정
OnlineCPUHP_AP_SCHED_STARTING큼 (CPU 수에 비례)sched_domain 재빌드
Teardown태스크 마이그레이션큼 (런큐 태스크 수에 비례)실행 중 태스크를 다른 CPU로 이동
TeardownIRQ 재분배중간 (IRQ 개수에 비례)오프라인 CPU의 IRQ를 다른 CPU로 이동
상대 비용 기준: 위 표는 특정 하드웨어에서 측정한 절대 시간(밀리초)이 아니라, 콜백이 수행하는 작업의 종류에 따른 상대적 비용 순서를 나타냅니다. 실제 소요 시간은 CPU 개수, 런큐 태스크 수, IRQ 개수, per-CPU 상태 크기, 빌드 설정에 따라 크게 달라지므로, 실제 시스템의 값은 위 표 대신 본 문서의 measure_hotplug() 스크립트로 직접 측정하시기 바랍니다.
SLA 주의: CPU hotplug 소요 시간은 커널 빌드 옵션과 하드웨어에 따라 크게 달라지며, 시스템 규모가 커질수록 대체로 증가합니다. 실시간(Real-time) 시스템에서 hotplug을 사용한다면, 최악의 경우 지연을 사전에 측정하고 SLA에 반영해야 합니다.

Freeze/Thaw 통합 (suspend/hibernate)

시스템 suspend(S3)와 hibernate(S4) 시, 부트 CPU(BSP)를 제외한 모든 CPU가 오프라인됩니다. 이 과정은 일반 CPU hotplug과 동일한 상태 머신을 거치지만, freeze_secondary_cpus()라는 별도 진입점을 사용합니다.

/* kernel/cpu.c — nonboot CPU 일괄 오프라인 */
int freeze_secondary_cpus(int primary)
{
    int cpu, error = 0;

    cpu_maps_update_begin();
    /* 현재 online 마스크 저장 (resume 시 복원용) */
    cpumask_copy(&frozen_cpus, cpu_online_mask);
    cpumask_clear_cpu(primary, &frozen_cpus);

    /* primary(BSP) 제외 모든 CPU 순차 오프라인 */
    for_each_cpu(cpu, &frozen_cpus) {
        error = _cpu_down(cpu, 1, CPUHP_OFFLINE);
        if (error) {
            /* 실패 시: 이미 오프라인한 CPU 롤백 */
            break;
        }
    }
    cpu_maps_update_done();
    return error;
}

/* resume 시 복원 */
void thaw_secondary_cpus(void)
{
    int cpu;

    cpu_maps_update_begin();
    /* 저장된 frozen_cpus 마스크의 CPU를 순차 온라인 */
    for_each_cpu(cpu, &frozen_cpus) {
        _cpu_up(cpu, 1, CPUHP_ONLINE);
    }
    cpu_maps_update_done();
}

freeze_processes()와의 상호작용 — suspend 전체 흐름

  1. freeze_processes() — 사용자 프로세스(Process) 동결
  2. freeze_kernel_threads() — 커널 스레드(Kernel Thread) 동결
  3. suspend_disable_secondary_cpus() — CPU 오프라인
  4. syscore_suspend() — 최종 HW 설정
  5. (실제 절전 진입)

복원은 역순으로 진행됩니다.

  1. syscore_resume()
  2. suspend_enable_secondary_cpus() — CPU 온라인
  3. thaw_kernel_threads()
  4. thaw_processes()
단계Suspend (S3)Hibernate (S4)
프로세스동결 (SIGSTOP 유사)동결
CPUBSP 외 전체 offlineBSP 외 전체 offline
메모리유지 (전원 공급)디스크에 이미지 저장
복원wakeup IRQ → CPU online → thaw부팅 → 이미지 로드 → CPU online → thaw
CPU 복원 방식thaw_secondary_cpus()enable_nonboot_cpus()
freeze 순서의 중요성: 프로세스를 먼저 동결한 후 CPU를 오프라인해야 합니다. 만약 CPU를 먼저 오프라인하면, 동결되지 않은 프로세스가 줄어든 CPU에서 실행되면서 성능 저하와 스케줄링 이상이 발생합니다. tasks_frozen 플래그가 1로 전달되어, 콜백이 suspend 컨텍스트임을 알 수 있습니다.

RISC-V CPU Hotplug

RISC-V 아키텍처에서는 SBI(Supervisor Binary Interface)의 HSM(Hart State Management) Extension을 통해 CPU(hart) hotplug을 구현합니다. x86의 INIT/SIPI, ARM64의 PSCI에 대응하는 펌웨어 인터페이스입니다.

RISC-V SBI HSM — 7개 상태, 4개 함수, suspend 와 resume 은 함수 하나 HART_SUSPEND 한 번의 호출이 인자 최상위 비트로 두 대기 상태를 나눕니다 ① 시작 · 정지 사이클 — sbi_hart_start() / sbi_hart_stop() HART_START boot HART_STOP STOPPED 1 · 정지됨 START_PENDING 2 · 부팅 진행 중 STARTED 0 · 실행 중 STOP_PENDING 3 · 정지 진행 중 정지 완료 ② suspend · resume 사이클 — HART_SUSPEND 하나로 두 경로를 나눕니다 HART_SUSPEND wake retained 값을 유지 STARTED 0 · 실행 중 SUSPEND_PENDING 5 · 정지 대기 SUSPENDED 4 · 정지 상태 non-retained cpu_suspend() STARTED 0 · 실행 중 RESUME_PENDING 6 · 재개 대기 STARTED 0 · 실행 중 SUSPENDED 상태에서 HART_STOP 을 걸면 ① 의 정지 사이클로 합류합니다 ③ 인자 분기 — suspend.c 의 riscv_sbi_hart_suspend() state & SBI_HSM_SUSP_NON_RET_BIT (0x80000000) set 이면 suspend_type 을 그대로 넘겨 retentive 로 대기 → SUSPEND_PENDING clear 면 cpu_suspend() 를 경유해 non-retentive 로 대기 → RESUME_PENDING 상태 값: 0 STARTED · 1 STOPPED · 2 START_PENDING · 3 STOP_PENDING · 4 SUSPENDED · 5 SUSPEND_PENDING · 6 RESUME_PENDING HSM 함수 4개: HART_START=0 · HART_STOP=1 · HART_STATUS=2 · HART_SUSPEND=3 — resume 전용 함수가 없습니다
RISC-V SBI HSM Hart 상태 머신 — 7개 상태(0 STARTED · 1 STOPPED · 2 START_PENDING · 3 STOP_PENDING · 4 SUSPENDED · 5 SUSPEND_PENDING · 6 RESUME_PENDING)와 4개 함수(HART_START · HART_STOP · HART_STATUS · HART_SUSPEND)로 구성됩니다. resume 전용 함수가 없어 HART_SUSPEND 한 번이 suspend_type 인자의 SBI_HSM_SUSP_NON_RET_BIT(0x80000000) 여부로 SUSPEND_PENDING 과 RESUME_PENDING 을 나눕니다. 상태 값과 함수 번호는 v7.2 arch/riscv/include/asm/sbi.h 기준이며, 실패 시 되돌아가는 전이는 본 그림에 그리지 않았습니다 (RISC-V 아키텍처 구현)
/* arch/riscv/kernel/smpboot.c — RISC-V CPU boot */
static int riscv_cpu_start_secondary(
                         struct task_struct *tidle)
{
     unsigned int cpu = tidle->cpu;
     unsigned long hartid = cpuid_to_hartid_map(cpu);
     unsigned long start_addr = __pa_symbol(secondary_start_sbi);

     /* SBI HSM: Hart 시작 요청 (SBI_EXT_HSM_HART_START) */
     struct sbiret ret = sbi_hsm_hart_start(hartid, start_addr, hartid);

     if (ret.error)
         return -EIO;

     /* SBI 펌웨어(OpenSBI 등)가 secondary_start_sbi() 진입점에서 */
     /* hart를 깨움 → smpboot_thread_cpu()를 통해 cpuhp 상태 머신 진행 */
     return 0;
}
/* arch/riscv/kernel/cpu-hotplug.c — RISC-V CPU stop */
void arch_cpu_idle_dead(void)
{
    /* cpuhp 상태 머신이 teardown 완료 후 호출 */
    idle_task_exit();

    /* SBI HSM: 현재 hart 정지 */
    sbi_ecall(SBI_EXT_HSM, SBI_EXT_HSM_HART_STOP,
              0, 0, 0, 0, 0, 0);

    /* 여기에 도달하면 안 됨 — hart는 STOPPED 상태 */
    unreachable();
}
/* SBI HSM 상태 조회 */
static int sbi_hart_get_status(unsigned long hartid)
{
    struct sbiret ret = sbi_ecall(
        SBI_EXT_HSM,
        SBI_EXT_HSM_HART_GET_STATUS,
        hartid, 0, 0, 0, 0, 0);
    return ret.value;
    /* 0: STARTED, 1: STOPPED, 2: START_PENDING, */
    /* 3: STOP_PENDING, 4: SUSPENDED, */
    /* 5: SUSPEND_PENDING, 6: RESUME_PENDING */
}
항목x86ARM64RISC-V
부팅 메커니즘INIT/SIPI IPIPSCI CPU_ON (SMC)SBI HSM hart_start (ecall)
정지 메커니즘HLT/MWAIT + APICPSCI CPU_OFFSBI HSM hart_stop
상태 조회APIC 레지스터(Register)PSCI AFFINITY_INFOSBI HSM hart_get_status
펌웨어BIOS/UEFIATF/OP-TEE (EL3)OpenSBI (M-mode)
진입점 전달트램펄린 물리 주소(Physical Address)PSCI cpu_on arghart_start start_addr
특권 레벨Ring 0 (BSP → AP)EL1 → EL3 → EL1S-mode → M-mode → S-mode
suspend 지원S3 (ACPI)PSCI SUSPENDHSM HART_SUSPEND
OpenSBI 구현: OpenSBI는 RISC-V의 대표적 M-mode 펌웨어로, HSM Extension을 구현합니다. lib/sbi/sbi_hsm.c에서 hart 상태 전이와 전원 관리를 처리합니다. SiFive HiFive Unmatched, StarFive VisionFive 2 등 상용 보드에서 사용됩니다.
RISC-V 테스트: QEMU의 -machine virt에서 -smp 4로 다중 hart를 에뮬레이션하고, 게스트 Linux에서 CPU hotplug을 테스트할 수 있습니다. OpenSBI는 QEMU에 내장되어 있어 별도 빌드 없이 HSM이 동작합니다.

cpuhp_state 열거형 상세

cpuhp_state 열거형(Enumeration)은 CPU의 bring-up과 tear-down 과정에서 거쳐야 하는 모든 상태를 정의합니다. v7.2 기준 include/linux/cpuhotplug.h에는 이름이 붙은 상태가 179개 정의되어 있고, CPUHP_OFFLINE(값 0)부터 CPUHP_ONLINE(값 236)까지의 번호를 갖습니다. 실제 사용되는 상태 수는 커널 버전과 활성화된 CONFIG 옵션(예: KVM, FTRACE, RCU_NOCB_CPU, NO_HZ_FULL)에 따라 달라집니다. 크게 정적 상태와 동적 상태로 나뉩니다.

/* include/linux/cpuhotplug.h — 상태 열거형 상세 (커널 7.x) */
/* 실제 커널(v7.2)은 이름이 붙은 상태 179개를 가지며, 여기서는 핵심 상태만 추려 순서를 보여줌 */
/* 아래 ... 는 그 외 상태를 생략한 표시이며, 실제 값은 include/linux/cpuhotplug.h 기준 */
enum cpuhp_state {
    CPUHP_INVALID            = -1,
    CPUHP_OFFLINE            = 0,

    /* ====== PREPARE 영역 (제어 CPU에서 실행, IRQ 활성) ====== */
    CPUHP_CREATE_THREADS,           /* smpboot 스레드 생성 */
    CPUHP_PERF_X86_PREPARE,         /* x86 perf 이벤트 준비 */
    CPUHP_SLUB_DEAD,                /* SLUB per-CPU 캐시 정리 */
    CPUHP_PRINTK_DEAD,              /* printk ring buffer 정리 */
    CPUHP_PAGE_ALLOC,               /* per-CPU 페이지 할당자 정리 */
    CPUHP_RANDOM_PREPARE,           /* 난수 풀 준비 */
    CPUHP_WORKQUEUE_PREP,           /* worker pool 준비 */
    CPUHP_POWER_NUMA_PREPARE,       /* NUMA 전원 관리 준비 */
    CPUHP_HRTIMERS_PREPARE,         /* hrtimer 베이스 초기화 */
    CPUHP_X2APIC_PREPARE,           /* x2apic 모드 준비 (x86) */
    CPUHP_SMPCFD_PREPARE,           /* SMP 함수 호출 데이터 할당 */
    CPUHP_RELAY_PREPARE,            /* relay 채널 버퍼 할당 */
    CPUHP_RCUTREE_PREP,             /* RCU 트리 노드 준비 */
    CPUHP_CPUIDLE_COUPLED_PREPARE,  /* coupled cpuidle 준비 */
    CPUHP_TOPOLOGY_PREPARE,         /* 토폴로지 매핑 준비 */
    CPUHP_TIMERS_PREPARE,           /* 타이머 휠 준비 */
    CPUHP_TMIGR_PREPARE,            /* timer migration 준비 */

    /* 동적 PREPARE 슬롯: 모듈용 */
    CPUHP_BP_PREPARE_DYN,           /* 동적 할당 시작점 */
    CPUHP_BP_PREPARE_DYN_END  = CPUHP_BP_PREPARE_DYN + 20,

    CPUHP_BP_KICK_AP,               /* 병렬 bringup 하트비트 */
    CPUHP_BRINGUP_CPU,              /* 아키텍처별 CPU 시작 */

    /* ====== STARTING/DYING 영역 (타겟 CPU, IRQ 비활성) ====== */
    CPUHP_AP_IDLE_DEAD,             /* idle 태스크 정리 */
    CPUHP_AP_OFFLINE,               /* AP 오프라인 마커 */
    CPUHP_AP_SCHED_STARTING,        /* 스케줄러 시작 */
    CPUHP_AP_RCUTREE_DYING,         /* RCU 트리 정리 */
    CPUHP_AP_IRQ_GIC_STARTING,      /* GIC 초기화 (ARM) */
    CPUHP_AP_IRQ_HIP04_STARTING,    /* HiSilicon GIC */
    CPUHP_AP_IRQ_ARMADA_XP_STARTING,/* Marvell MPIC */
    CPUHP_AP_IRQ_BCM2836_STARTING,  /* RPi BCM2836 */
    CPUHP_AP_IRQ_RISCV_SBI_IPI_STARTING, /* RISC-V SBI IPI */
    /* ... (타이머 상태 20여 개, perf 상태 10여 개) ... */
    CPUHP_AP_PERF_X86_STARTING,     /* x86 PMU 초기화 */
    CPUHP_AP_ARM64_DEBUG_MONITORS_STARTING, /* ARM64 디버그 모니터 */
    /* ... (여러 아키텍처 타이머 상태) ... */
    CPUHP_AP_ARM_ARCH_TIMER_STARTING,/* ARM 아키텍처 타이머 */
    /* ... (타이머 상태 다수) ... */
    CPUHP_AP_ARM64_ISNDEP_STARTING, /* ARM64 ISA 비종속 초기화 */
    CPUHP_AP_SMPCFD_DYING,          /* SMP 함수 호출 정리 */
    CPUHP_AP_HRTIMERS_DYING,        /* hrtimer 마이그레이션 */
    CPUHP_AP_TICK_DYING,            /* tick 장치 해제 */

    /* ====== ONLINE 영역 (타겟 CPU, IRQ 활성) ====== */
    CPUHP_AP_ONLINE,                /* 온라인 마커 */
    CPUHP_TEARDOWN_CPU,             /* teardown 완료 후 복귀 */
    CPUHP_AP_ONLINE_IDLE,           /* idle 로직 온라인 */
    CPUHP_AP_KVM_ONLINE,            /* KVM vCPU 온라인 */
    CPUHP_AP_SMPBOOT_THREADS,       /* smpboot 스레드 시작 */
    CPUHP_AP_IRQ_AFFINITY_ONLINE,   /* IRQ affinity 재계산 */
    CPUHP_AP_PERF_ONLINE,           /* perf 이벤트 활성화 */
    CPUHP_AP_WORKQUEUE_ONLINE,      /* workqueue 온라인 */
    CPUHP_AP_RANDOM_ONLINE,         /* 난수 풀 온라인 */
    CPUHP_AP_RCUTREE_ONLINE,        /* RCU 트리 온라인 */
    CPUHP_AP_KTHREADS_ONLINE,       /* kthread 생성 */

    /* 동적 ONLINE 슬롯: 모듈용 */
    CPUHP_AP_ONLINE_DYN,            /* 동적 할당 시작점 */
    CPUHP_AP_ONLINE_DYN_END = CPUHP_AP_ONLINE_DYN + 40,

    CPUHP_AP_ACTIVE,                /* cpu_active_mask 설정 (최종) */
    CPUHP_ONLINE,                   /* 최종 온라인 상태 */
};
영역동적 슬롯슬롯 수용도등록 API
PrepareCPUHP_BP_PREPARE_DYN20개제어 CPU에서 실행, 메모리 할당 가능cpuhp_setup_state(CPUHP_BP_PREPARE_DYN, ...)
OnlineCPUHP_AP_ONLINE_DYN40개타겟 CPU에서 실행, IRQ 활성cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)
판정 함수조건 (v7.2)값 범위의미
cpuhp_is_atomic_state()CPUHP_AP_IDLE_DEAD <= state && state < CPUHP_AP_ONLINE84 ~ 141IRQ 비활성 상태에서 실행되며 실패하면 안 되는 상태
cpuhp_is_ap_state()state > CPUHP_BRINGUP_CPU && state != CPUHP_TEARDOWN_CPU84 ~ 142 (143 제외)타겟 CPU에서 실행되는 상태
동적 PREPARE 슬롯CPUHP_BP_PREPARE_DYN ~ CPUHP_BP_PREPARE_DYN_END61 ~ 81 (20개)cpuhp_setup_state()로 모듈이 점유
동적 ONLINE 슬롯CPUHP_AP_ONLINE_DYN ~ CPUHP_AP_ONLINE_DYN_END192 ~ 232 (40개)cpuhp_setup_state()로 모듈이 점유
Starting 영역 콜백은 실패하면 안 됩니다: kernel/cpu.c의 cpuhp_is_atomic_state()가 판정하는 구간(CPUHP_AP_IDLE_DEAD 이상 CPUHP_AP_ONLINE 미만)의 콜백은 IRQ가 비활성화된 상태에서 실행되며, 소스 코드 주석에 "STARTING/DYING must not fail"이라고 명시되어 있습니다. 이 구간에서 startup 콜백이 오류를 반환하면 커널이 WARN_ON_ONCE()로 경고를 출력합니다. 이 구간의 teardown 쪽도 "Rollback must not fail"이라는 동일 규칙이 적용되므로, 실패 가능한 처리는 반드시 Prepare 또는 Online 구간으로 옮겨야 합니다. 하드웨어(HW) 초기화가 목적인 Starting 콜백은 오류 대신 WARN_ON() 같은 진단만 남기고 성공을 반환하는 편이 안전합니다.
상태 번호 조회: cat /sys/devices/system/cpu/hotplug/states로 현재 커널에 등록된 모든 cpuhp 상태와 콜백 이름을 확인할 수 있습니다. 각 줄은 상태번호: 콜백이름 형식입니다.
# 아래 번호는 include/linux/cpuhotplug.h(v7.2) 기준이며,
# 실제 출력에는 콜백이 등록된 상태만 나타나므로 CONFIG에 따라 달라집니다
cat /sys/devices/system/cpu/hotplug/states
# 출력 예:
#   0: offline
#   1: threads:prepare
#   2: perf:X86:prepare
#   ...  
#  87: sched:starting
#  88: RCU/tree:dying
# 148: smpboot/threads:online
# 189: RCU/tree:online
#   ...  
# 236: online

# 동적 슬롯 사용 현황
cat /sys/devices/system/cpu/hotplug/states | grep "DYN\|dyn"
# 또는 상태 번호 범위로 필터링 (CPUHP_AP_ONLINE_DYN=192 ~ CPUHP_AP_ACTIVE=235)
cat /sys/devices/system/cpu/hotplug/states | awk -F: '{if ($1+0 >= 192 && $1+0 <= 235) print}'

커널 API 상세

CPU Hotplug 콜백을 등록하고 해제하는 API 계열은 여러 변형이 있으며, 각각의 동작 특성을 정확히 이해해야 합니다.

/* include/linux/cpu.h — 핵심 API 시그니처 */

/* 1. 기본 등록: startup/teardown 콜백 + 이미 온라인 CPU에도 호출 */
int cpuhp_setup_state(
    enum cpuhp_state state,   /* 상태 번호 또는 DYN */
    const char *name,          /* 디버그용 이름 */
    int (*startup)(unsigned int cpu),   /* bring-up 콜백 */
    int (*teardown)(unsigned int cpu)   /* tear-down 콜백 */
);
/* 반환: 동적 상태면 할당된 번호 (>0), 정적이면 0, 에러면 <0 */

/* 2. nocalls: 콜백만 등록, 기존 온라인 CPU에 호출 안 함 */
int cpuhp_setup_state_nocalls(
    enum cpuhp_state state,
    const char *name,
    int (*startup)(unsigned int cpu),
    int (*teardown)(unsigned int cpu)
);

/* 3. 멀티 인스턴스: 하나의 콜백에 여러 인스턴스 연결 */
int cpuhp_setup_state_multi(
    enum cpuhp_state state,
    const char *name,
    int (*startup)(unsigned int cpu, struct hlist_node *node),
    int (*teardown)(unsigned int cpu, struct hlist_node *node)
);
/* cpuhp_state_add_instance()로 인스턴스 추가 */
/* cpuhp_state_remove_instance()로 인스턴스 제거 */

/* 4. 해제 */
void cpuhp_remove_state(enum cpuhp_state state);
void cpuhp_remove_state_nocalls(enum cpuhp_state state);
void cpuhp_remove_multi_state(enum cpuhp_state state);

v7.2 기준 API 전체 목록과 호출 규칙

include/linux/cpuhotplug.h에는 위 시그니처 외에도 cpus_read_lock()을 이미 보유한 컨텍스트에서만 호출해야 하는 변형이 다수 존재합니다. 이 변형들은 내부적으로 __cpuhp_setup_state_cpuslocked()를 호출하며, cpuhp_setup_state()가 내부에서 락을 걸어 주는 것과 달리 호출자가 락 보유권을 이미 가지고 있어야 한다는 점이 결정적입니다. 아래 표는 v7.2 헤더의 invoke 인자와 래퍼 구조를 그대로 옮긴 것입니다.

API이미 온라인 CPU에 startup 호출cpus_read_lock() 보유 필요주요 용도
cpuhp_setup_state()예아니오 (내부에서 획득)기본 등록. 모듈 로드 시 per-CPU 자원 초기화
cpuhp_setup_state_cpuslocked()예예이미 읽기 잠금을 잡은 경로에서 등록. 중복 락 획득 방지
cpuhp_setup_state_nocalls()아니오아니오등록만. 초기화가 별도로 수행되는 서브시스템
cpuhp_setup_state_nocalls_cpuslocked()아니오예읽기 잠금 보유 상태에서 등록만
cpuhp_setup_state_multi()아니오 (멀티 상태로 선언)아니오하나의 상태에 여러 인스턴스를 붙일 수 있도록 표시
cpuhp_state_add_instance()예아니오멀티 인스턴스 추가 + startup 호출
cpuhp_state_add_instance_nocalls()아니오아니오멀티 인스턴스 추가만
cpuhp_state_add_instance_nocalls_cpuslocked()아니오예읽기 잠금 보유 상태에서 인스턴스 추가만
cpuhp_remove_state()teardown 호출아니오콜백 제거 + teardown 실행
cpuhp_remove_state_nocalls()teardown 미호출아니오콜백만 제거 (리소스 정리는 별도)
cpuhp_remove_state_nocalls_cpuslocked()teardown 미호출예읽기 잠금 보유 상태에서 콜백만 제거
cpuhp_remove_multi_state()teardown 미호출아니오멀티 상태 해제. 모든 인스턴스 제거 후 호출
cpuhp_state_remove_instance()teardown 호출아니오인스턴스 제거 + teardown 실행
cpuhp_state_remove_instance_nocalls()teardown 미호출아니오인스턴스만 제거
cpuslocked 변형을 써야 하는 경우: cpuhp_setup_state()는 내부에서 cpus_read_lock()/cpus_write_lock()을 획득하므로, 이미 cpus_read_lock()을 보유한 컨텍스트에서 호출하면 재귀 락 획득(deadlock 또는 lockdep 경고)으로 이어집니다. cpu_alloc/cpu_add 같은 이미 cpuhp_state를 조작하는 내부 경로에서는 이 변형들이 정식 사용처입니다.

cpuhp_online_idle() — idle 태스크를 통한 온라인 위임

kernel/cpu.c의 cpuhp_online_idle()는 일반적인 Online 콜백과 달리 idle 태스크(idle task)에서 호출되는 진입점입니다. 소스 코드 주석에 명시된 동작은 다음과 같습니다.

/* kernel/cpu.c */
/*
 * Called from the idle task. Wake up the controlling task which brings the
 * hotplug thread of the upcoming CPU up and then delegates the rest of the
 * online bringup to the hotplug thread.
 */
void cpuhp_online_idle(enum cpuhp_state state)
{
    struct cpuhp_cpu_state *st = this_cpu_ptr(&cpuhp_state);

    /* 부트 CPU에서는 CPUHP_AP_ONLINE_IDLE 상태가 아니므로 즉시 반환 */
    if (state != CPUHP_AP_ONLINE_IDLE)
        return;

    cpuhp_ap_update_sync_state(SYNC_STATE_ONLINE);

    /* idle 루프 진입 전에 stopper 스레드를 unpark */
    stop_machine_unpark(smp_processor_id());

    st->state = CPUHP_AP_ONLINE_IDLE;
    complete_ap_thread(st, true);
}
구분일반 Online 콜백cpuhp_online_idle()
실행 컨텍스트per-CPU hotplug 스레드해당 CPU의 idle 태스크
트리거hotplug 스레드가 상태를 순차 진행idle 경로가 자기 준비 완료를 알림
선행 조치없음stop_machine_unpark()로 stopper 태스크를 먼저 준비
상태 기록콜백이 st->state 갱신st->state = CPUHP_AP_ONLINE_IDLE 후 complete_ap_thread()
부트 CPU해당 없음상태가 CPUHP_AP_ONLINE_IDLE이 아니면 즉시 반환
왜 idle 태스크가 필요한가: idle 기반 드라이버는 CPU가 실제 유휴 상태에 진입하기 전까지 "온라인 준비"를 끝낼 수 없습니다. Online 콜백에서 슬립하면서 기다리는 방식은 인터럽트가 활성인 Online 구간에서는 가능하지만, idle 진입 전후의 상태를 정확히 알려야 하는 경우에는 이처럼 idle 태스크가 제어 태스크를 깨워 hotplug 스레드에 넘기는 구조가 필요합니다. 참고로 CPU의 hotplug 스레드는 kernel/cpu.c의 cpuhp_threads(struct smp_hotplug_thread)로 등록되어 있으며, thread_comm이 "cpuhp/%u"이고 selfparking이 true이므로 작업이 끝나면 스스로 parking합니다.
cpuhp_setup_state 4종 — invoke 여부 × 잠금 책임 네 API 모두 같은 __cpuhp_setup_state_cpuslocked() 구현으로 모입니다 ① 행이 invoke, 열이 잠금 책임입니다 cpus_read_lock() 을 내부에서 잡음 호출자가 이미 cpus_read_lock() 을 잡음 invoke = true cpuhp_setup_state() 읽기 잠금을 직접 잡고 풀어줍니다 cpuhp_setup_state_cpuslocked() 이미 잡은 잠금을 그대로 씁니다 invoke = false cpuhp_setup_state_nocalls() 등록만 하고 startup 는 부르지 않습니다 cpuhp_setup_state_nocalls_cpuslocked() 등록만 하고 잠금은 호출자 몫 ② 공통 파이프라인 — invoke 값과 무관하게 항상 수행되는 부분 1. 선행 검사 lockdep_assert_cpus_held() state/name 이 아니면 -EINVAL 2. 콜백 등록 mutex_lock(&cpuhp_state_mutex) cpuhp_store_callbacks() 3. startup 호출 invoke·startup 모두 있을 때 아니면 여기서 끝 4. 반환 DYN 상태는 동적 번호 그 외 0 또는 오류 코드 startup 만 넘기고 teardown 을 NULL 로 두면 invoke=true 여도 호출이 일어나지 않습니다 ③ invoke = true 일 때의 호출 대상과 실패 처리 호출 대상 — 온라인 CPU 가 아닙니다 for_each_present_cpu() 로 present CPU 를 돌고 cpuhp_cpu_state.state < state 면 건너뜁니다 즉 그 상태를 이미 통과한 CPU 만 대상 실패 시 처리 teardown 이 있으면 cpuhp_rollback_install() 그다음 cpuhp_store_callbacks() 로 등록 자체를 지웁니다
cpuhp_setup_state() vs cpuhp_setup_state_nocalls() 동작 비교 — 실제 API 는 invoke 여부와 잠금 책임을 곱한 4종이며, 네 함수 모두 __cpuhp_setup_state_cpuslocked() 하나로 구현됩니다. __cpuhp_setup_state() 계열은 그 안에서 cpus_read_lock() 을 잡고 풀지만, _cpuslocked 계열은 lockdep_assert_cpus_held() 로 호출자가 이미 읽기 잠금을 쥐었음을 요구하므로 그 안에서 다시 잠글 수 없습니다. invoke=true 일 때 startup 이 호출되는 대상은 온라인 CPU 전체가 아니라 for_each_present_cpu() 중 cpuhp_cpu_state.state 가 대상 상태 이상인 CPU이며, 하나라도 실패하면 teardown 롤백 후 등록까지 취소됩니다 (개념 개요, v7.2 kernel/cpu.c 및 include/linux/cpuhotplug.h 기준)

콜백 함수(Callback Function) 작성 시 주의사항입니다.

영역콜백 제약사항허용 동작금지 동작
Prepare제어 CPU에서 실행, IRQ 활성GFP_KERNEL 할당, mutex, 슬립타겟 CPU per-CPU 데이터 직접 수정
Starting타겟 CPU, IRQ 비활성spinlock, 레지스터 접근, 원자적 연산(Atomic Operation)슬립, GFP_KERNEL, mutex, printk
Online타겟 CPU, IRQ 활성GFP_KERNEL, mutex, 슬립, smp_call_function장시간 블로킹 (hotplug 전체 지연)
/* 콜백 작성 모범 사례 */

/* Prepare 콜백: 메모리 사전 할당 */
static int my_prepare_cb(unsigned int cpu)
{
    struct my_per_cpu_data *data;

    /* GFP_KERNEL 사용 가능 — 슬립 가능한 컨텍스트 */
    data = kzalloc(sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    per_cpu(my_data_ptr, cpu) = data;
    return 0;
}

/* Starting 콜백: HW 초기화 (IRQ 꺼진 상태) */
static int my_starting_cb(unsigned int cpu)
{
    struct my_per_cpu_data *data = per_cpu(my_data_ptr, cpu);

    /* 슬립 불가! spinlock만 사용 */
    /* HW 레지스터 직접 접근 가능 */
    data->hw_reg = readl(data->base + HW_CTRL);
    writel(HW_ENABLE, data->base + HW_CTRL);
    return 0;
}

/* Online 콜백: 서브시스템 알림 */
static int my_online_cb(unsigned int cpu)
{
    struct my_per_cpu_data *data = per_cpu(my_data_ptr, cpu);

    /* IRQ 활성, 슬립 가능 */
    data->worker = kthread_create(my_worker_fn, data,
                                  "my_worker/%d", cpu);
    kthread_bind(data->worker, cpu);
    wake_up_process(data->worker);
    return 0;
}

/* Teardown 콜백: 역순 정리 */
static int my_teardown_cb(unsigned int cpu)
{
    struct my_per_cpu_data *data = per_cpu(my_data_ptr, cpu);

    /* worker 정지 */
    kthread_stop(data->worker);
    /* HW 비활성화 */
    writel(HW_DISABLE, data->base + HW_CTRL);
    /* 메모리 해제 */
    kfree(data);
    per_cpu(my_data_ptr, cpu) = NULL;
    return 0;
}
롤백 보장: startup 콜백이 에러를 반환하면, 상태 머신은 이미 성공한 모든 상태의 teardown 콜백을 역순으로 호출합니다. 따라서 startup과 teardown은 반드시 대칭적이어야 합니다. startup에서 할당한 자원은 teardown에서 해제해야 하며, 부분 초기화 상태에서도 teardown이 안전하게 동작해야 합니다.

드라이버 핫플러그(Hotplug) 대응 패턴

디바이스 드라이버(Device Driver)가 CPU Hotplug에 올바르게 대응하려면, per-CPU 자원 관리와 CPU affinity 변경 처리를 체계적으로 구현해야 합니다.

per-CPU 자원 관리 패턴

/* 패턴 1: 정적 per-CPU 변수 + hotplug 콜백 */
static DEFINE_PER_CPU(struct my_stats, cpu_stats);
static enum cpuhp_state hp_state;

static int my_cpu_online(unsigned int cpu)
{
    struct my_stats *stats = per_cpu_ptr(&cpu_stats, cpu);
    memset(stats, 0, sizeof(*stats));
    stats->active = true;
    return 0;
}

static int my_cpu_offline(unsigned int cpu)
{
    struct my_stats *stats = per_cpu_ptr(&cpu_stats, cpu);
    /* 오프라인 CPU의 통계를 CPU 0에 합산 */
    struct my_stats *target = per_cpu_ptr(&cpu_stats, 0);
    target->count += stats->count;
    target->bytes += stats->bytes;
    stats->active = false;
    return 0;
}

/* 패턴 2: 동적 할당 per-CPU 데이터 */
static DEFINE_PER_CPU(struct ring_buffer *, cpu_buffer);

static int my_prepare(unsigned int cpu)
{
    struct ring_buffer *buf;
    /* Prepare 단계에서 메모리 할당 */
    buf = ring_buffer_alloc(BUFFER_SIZE, GFP_KERNEL);
    if (!buf)
        return -ENOMEM;
    per_cpu(cpu_buffer, cpu) = buf;
    return 0;
}

static int my_dead(unsigned int cpu)
{
    struct ring_buffer *buf = per_cpu(cpu_buffer, cpu);
    /* Prepare teardown: 메모리 해제 */
    ring_buffer_free(buf);
    per_cpu(cpu_buffer, cpu) = NULL;
    return 0;
}

CPU Affinity 변경 대응

/* NIC 드라이버의 IRQ affinity 재분배 패턴 */
static int my_nic_cpu_online(unsigned int cpu,
                            struct hlist_node *node)
{
    struct my_nic_priv *priv = hlist_entry_safe(
        node, struct my_nic_priv, cpuhp_node);

    /* 새 CPU가 온라인되면 RX 큐 affinity 재분배 */
    my_nic_rebalance_irq_affinity(priv);
    return 0;
}

static int my_nic_cpu_offline(unsigned int cpu,
                             struct hlist_node *node)
{
    struct my_nic_priv *priv = hlist_entry_safe(
        node, struct my_nic_priv, cpuhp_node);

    /* 오프라인 CPU에 할당된 RX 큐를 다른 CPU로 이동 */
    for (int q = 0; q < priv->num_rx_queues; q++) {
        if (priv->rx_queue[q].cpu == cpu) {
            int new_cpu = cpumask_any_but(cpu_online_mask, cpu);
            priv->rx_queue[q].cpu = new_cpu;
            irq_set_affinity_hint(priv->rx_queue[q].irq,
                                  cpumask_of(new_cpu));
        }
    }
    return 0;
}

/* 프로브 시 멀티 인스턴스 등록 */
static int my_nic_probe(struct pci_dev *pdev, ...)
{
    /* ... */
    ret = cpuhp_state_add_instance(hp_state, &priv->cpuhp_node);
    if (ret)
        goto err;
    /* ... */
}

static void my_nic_remove(struct pci_dev *pdev)
{
    cpuhp_state_remove_instance(hp_state, &priv->cpuhp_node);
    /* ... */
}
패턴사용 시점API대표 사용자
단일 콜백글로벌 서브시스템 (perf, RCU)cpuhp_setup_state()perf, workqueue, slab
nocalls별도 초기화 경로가 있는 경우cpuhp_setup_state_nocalls()early boot 서브시스템
멀티 인스턴스여러 디바이스 인스턴스cpuhp_setup_state_multi()mlx5, bnxt, nvme
Prepare + Online메모리 할당이 필요한 경우두 상태에 각각 등록perf (prepare + online)

NUMA 토폴로지(Topology)와 CPU Hotplug

NUMA(Non-Uniform Memory Access) 시스템에서 CPU Hotplug은 단순한 CPU 제거/추가를 넘어 메모리 접근 지역성(Locality)과 스케줄러 도메인(Domain)에 영향을 미칩니다.

NUMA 환경 CPU Hotplug — 하우스키핑 제약부터 노드 이동까지 CPU 하나를 끄면 그 노드의 도메인 구조와 부하가 함께 바뀝니다 ① 첫 번째 제약 — 하우스키핑 CPU 를 노드에 하나는 남겨야 합니다 if (cpumask_any_and(cpu_online_mask, housekeeping_cpumask(HK_TYPE_DOMAIN)) >= nr_cpu_ids) ret = -EBUSY; 소스 주석 — 빈 sched_domain span 이 생기지 않도록 하우스키핑 CPU 를 하나는 남깁니다 즉 nohz_full 로 지정한 코어가 전부인 노드는 그 노드의 CPU 를 끌 수 없습니다 이 검사를 통과해야 sched_domain 재구성 단계로 넘어갑니다 ② 검사를 통과하면 partition_sched_domains() 가 도메인을 다시 계산합니다 Node 0 — CPU 3 이 속한 쪽 SMT 레벨이 통째로 사라집니다 Core 1 에는 CPU 2 만 남습니다 CPU 3 은 def_root_domain 로 분리됩니다 MC 그룹의 span 이 줄어듭니다 Node 1 — 그대로 재사용됩니다 partition 이 바뀌지 않았으므로 도메인 재구성 대상이 아닙니다 부하도 그대로입니다 cross-node 이동은 아래 ③ 에서만 ③ 태스크가 실제로 노드를 옮기는 순간 — select_fallback_rq() 의 3단계 폴백 1순위 — 같은 NUMA 노드 node 안에서 active 이고 affinity 에 들어가는 CPU 2순위 — affinity 범위 노드 상관없이 active CPU 를 고릅니다 3순위 — 강제 해제 affinity 를 넓힌 뒤 active CPU 를 고릅니다 오프라인만으로 크로스 노드 이관이 즉시 일어나지는 않습니다 CPU 가 빠진 자리만 그 노드의 남은 CPU 가 메꿉우므로, 1순위가 실패하는 태스크만 노드를 넘습니다
NUMA 환경 CPU Hotplug — CPU 3 오프라인이 하우스키핑 CPU 보존 검사와 sched_domain 재구성에 미치는 영향. 첫 단계의 -EBUSY 거부가 NUMA 환경에서 가장 먼저 드러나는 제약이며, 이를 통과한 뒤에야 그 노드의 도메인 구조가 바뀌고, 태스크가 다른 노드로 옮겨 가는 것은 select_fallback_rq() 폴백이 1순위를 만족하지 못할 때뿐입니다 (개념 개요, v7.2 kernel/cpu.c · kernel/sched/topology.c 및 include/linux/cpuhotplug.h 기준)
# NUMA 노드별 CPU 확인
lscpu | grep "NUMA"
# NUMA node(s):          2
# NUMA node0 CPU(s):     0-3
# NUMA node1 CPU(s):     4-7

# 또는 sysfs에서 직접 확인
cat /sys/devices/system/node/node0/cpulist  # 0-3
cat /sys/devices/system/node/node1/cpulist  # 4-7

# 특정 NUMA 노드의 CPU 전체 오프라인
for cpu in $(cat /sys/devices/system/node/node1/cpulist | tr ',' '\n' | tr '-' ' ' | while read a b; do seq $a ${b:-$a}; done); do
    [ -f "/sys/devices/system/cpu/cpu${cpu}/online" ] && \
    echo 0 > /sys/devices/system/cpu/cpu${cpu}/online
done

# NUMA 메모리 통계 확인 (CPU 오프라인 전후 비교)
numastat -m
# Node 1의 MemFree/MemUsed는 CPU 오프라인과 무관하게 유지

# 크로스 노드 마이그레이션 확인
cat /proc/vmstat | grep numa_
# numa_hit, numa_miss, numa_foreign 카운터 변화 관찰
NUMA 상황CPU Hotplug 영향권장 대응
노드의 모든 CPU 오프라인해당 노드 메모리는 리모트 접근만 가능, 성능 저하최소 1 CPU 유지 또는 메모리 마이그레이션 고려
PCIe 장치의 NUMA 노드 CPU 감소IRQ 처리 CPU가 다른 노드로 이동, 지연 증가irqbalance에 NUMA 힌트 설정
memcg와 NUMA 정책 충돌cpuset의 cpus와 mems 불일치 가능cpuset 자동 업데이트 또는 수동 조정
NUMA balancing 영향자동 NUMA 밸런싱 대상 CPU 감소큰 영향 없음 (자동 적응)

NUMA 인식 fallback CPU 선택 — kernel/sched/core.c의 select_fallback_rq() NUMA 우선 로직

  1. 같은 NUMA 노드 + affinity mask 내 활성 CPU
  2. 다른 NUMA 노드 + affinity mask 내 활성 CPU
  3. affinity 무시 + 아무 활성 CPU (최후 수단)

cpuset과의 상호작용

NUMA 노드 완전 오프라인: 한 NUMA 노드의 모든 CPU를 오프라인하면, 해당 노드의 메모리에 접근하는 모든 프로세스가 리모트 노드 CPU에서 실행됩니다. QPI/UPI 인터커넥트(Interconnect)를 통한 원격 메모리 접근은 로컬 접근보다 상당히 느리며, 메모리 집약적인 워크로드에서는 병목(Bottleneck)이 될 수 있습니다.

cpuset 연동

cpuset cgroup은 프로세스 그룹에 특정 CPU와 메모리 노드를 할당하는 메커니즘으로, CPU Hotplug과 밀접하게 연동됩니다.

# cpuset cgroup v2 설정
# CPU 2,3을 RT 태스크 전용으로 분리
mkdir -p /sys/fs/cgroup/rt-tasks
echo "2-3" > /sys/fs/cgroup/rt-tasks/cpuset.cpus
echo "0" > /sys/fs/cgroup/rt-tasks/cpuset.mems
echo $$ > /sys/fs/cgroup/rt-tasks/cgroup.procs
# 이제 현재 셸과 자식 프로세스는 CPU 2,3에서만 실행

# CPU 2를 오프라인하면?
echo 0 > /sys/devices/system/cpu/cpu2/online
cat /sys/fs/cgroup/rt-tasks/cpuset.cpus.effective
# 출력: 3 (CPU 2가 effective에서 자동 제거됨)

# CPU 2를 다시 온라인하면?
echo 1 > /sys/devices/system/cpu/cpu2/online
cat /sys/fs/cgroup/rt-tasks/cpuset.cpus.effective
# 출력: 2-3 (자동 복원)
/* kernel/cgroup/cpuset.c — CPU hotplug 처리 */
static void cpuset_hotplug_workfn(struct work_struct *work)
{
    /* CPU online/offline 시 호출 */
    /* 1. top_cpuset의 effective_cpus 갱신 */
    cpumask_copy(&top_cpuset.effective_cpus, cpu_active_mask);

    /* 2. 하위 cpuset 재귀 갱신 */
    update_cpumasks_hier(css, &tmp, false);

    /* 3. effective_cpus가 비어진 cpuset 처리 */
    /*    → 부모 cpuset의 effective_cpus 상속 */
    /* 4. 영향받은 태스크의 allowed mask 갱신 */
    rebuild_sched_domains_locked();
}
상황cpuset 동작태스크 영향
cpuset 내 CPU 일부 오프라인cpuset.cpus.effective에서 제거남은 CPU에서 계속 실행
cpuset 내 CPU 전체 오프라인부모 cpuset의 effective_cpus 상속부모 cpuset CPU에서 실행 (경고 발생)
오프라인 CPU 재온라인cpuset.cpus에 포함되어 있으면 effective 복원자동 복원
cpuset.cpus.partition 모드격리된(Partitioned) CPU가 오프라인되면 파티션 무효화(Invalidation)일반 모드로 전환
Partition 모드: cgroup v2의 cpuset.cpus.partition을 root로 설정하면 해당 cpuset의 CPU가 부모에서 완전히 분리됩니다. 이때 분리된 CPU를 오프라인하면 파티션이 invalid 상태가 되며, 커널 로그에 경고가 출력됩니다. 복원하려면 CPU를 다시 온라인한 후 partition을 재설정해야 합니다.

마이크로코드(Microcode) 적용과 CPU Hotplug

CPU 마이크로코드 업데이트는 CPU Hotplug의 bring-up 과정에서 적용됩니다. 보안 취약점 패치(Patch)나 하드웨어 에라타(Errata) 수정을 위해 런타임에 마이크로코드를 갱신할 수 있으며, hotplug 상태 머신의 Starting 단계에서 처리됩니다.

/* arch/x86/kernel/cpu/microcode/core.c */
/* CPUHP_AP_MICROCODE_STARTING 상태에서 호출 */
static int microcode_bsp_resume(void)
{
    /* suspend/resume 시 BSP 마이크로코드 재적용 */
    return microcode_ops->apply_microcode(smp_processor_id());
}

/* AP (Application Processor) bring-up 시 */
static int mc_cpu_starting(unsigned int cpu)
{
    /* 이미 로드된 마이크로코드를 이 CPU에 적용 */
    microcode_ops->apply_microcode(cpu);
    return 0;
}

/* 등록 */
cpuhp_setup_state_nocalls(CPUHP_AP_MICROCODE_STARTING,
                          "x86/microcode:starting",
                          mc_cpu_starting, NULL);
# 마이크로코드 런타임 업데이트 (late loading)
# 1. 마이크로코드 파일 준비
cp intel-ucode/06-8e-0c /lib/firmware/intel-ucode/

# 2. reload 트리거
echo 1 > /sys/devices/system/cpu/microcode/reload

# 3. 적용 결과 확인
cat /sys/devices/system/cpu/cpu0/microcode/version
# 또는
dmesg | grep microcode
# [12345.678] microcode: updated early: 0x88 → 0x8c

# 4. CPU off/on으로 개별 CPU에 마이크로코드 적용 확인
echo 0 > /sys/devices/system/cpu/cpu3/online
echo 1 > /sys/devices/system/cpu/cpu3/online
cat /sys/devices/system/cpu/cpu3/microcode/version
late loading 주의: 커널 6.x부터 마이크로코드 late loading은 기본적으로 CONFIG_MICROCODE_LATE_LOADING=y 설정이 필요합니다. Late loading은 모든 CPU를 일시적으로 rendez-vous 상태로 만든 후 순차 적용하므로, 대규모 시스템에서는 수백 밀리초의 서비스 중단이 발생할 수 있습니다.

실전 드라이버 전체 코드

CPU Hotplug 콜백을 올바르게 구현하는 완전한 커널 모듈(Kernel Module) 예제입니다. Prepare + Online 두 단계에 콜백을 등록하고, per-CPU 자원을 안전하게 관리합니다.

/*
 * cpuhp_example.c — CPU Hotplug 콜백 예제 모듈
 *
 * 기능:
 * - per-CPU 통계 버퍼 할당/해제
 * - CPU 온라인 시 worker 스레드 시작
 * - CPU 오프라인 시 통계 합산 및 정리
 * - procfs를 통한 통계 조회
 */
#include <linux/module.h>
#include <linux/cpu.h>
#include <linux/cpuhotplug.h>
#include <linux/percpu.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

struct cpuhp_example_data {
    u64 events_processed;
    u64 bytes_transferred;
    u32 online_count;   /* 이 CPU가 온라인된 횟수 */
    bool active;
    void *buffer;       /* 작업 버퍼 */
};

#define BUFFER_SIZE  (4096)

static DEFINE_PER_CPU(struct cpuhp_example_data, cpu_data);
static enum cpuhp_state hp_online;  /* 동적 할당된 상태 번호 */
static enum cpuhp_state hp_prepare; /* prepare 상태 번호 */

/* ---- Prepare 콜백: 제어 CPU에서 실행 (슬립 가능) ---- */
static int cpuhp_example_prepare(unsigned int cpu)
{
    struct cpuhp_example_data *data = per_cpu_ptr(&cpu_data, cpu);

    pr_info("cpuhp_example: prepare cpu %u\n", cpu);

    /* 메모리 할당 — GFP_KERNEL 사용 가능 */
    data->buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!data->buffer)
        return -ENOMEM;

    return 0;
}

/* ---- Prepare teardown: 메모리 해제 ---- */
static int cpuhp_example_dead(unsigned int cpu)
{
    struct cpuhp_example_data *data = per_cpu_ptr(&cpu_data, cpu);

    pr_info("cpuhp_example: dead cpu %u\n", cpu);

    kfree(data->buffer);
    data->buffer = NULL;

    return 0;
}

/* ---- Online 콜백: 타겟 CPU에서 실행 (IRQ 활성) ---- */
static int cpuhp_example_online(unsigned int cpu)
{
    struct cpuhp_example_data *data = this_cpu_ptr(&cpu_data);

    pr_info("cpuhp_example: online cpu %u\n", cpu);

    /* per-CPU 데이터 초기화 */
    data->events_processed = 0;
    data->bytes_transferred = 0;
    data->online_count++;
    data->active = true;

    return 0;
}

/* ---- Online teardown: 통계 합산 후 비활성화 ---- */
static int cpuhp_example_offline(unsigned int cpu)
{
    struct cpuhp_example_data *data = this_cpu_ptr(&cpu_data);
    struct cpuhp_example_data *cpu0_data;

    pr_info("cpuhp_example: offline cpu %u (events=%llu)\n",
            cpu, data->events_processed);

    /* 오프라인 CPU의 통계를 CPU 0에 합산 */
    cpu0_data = per_cpu_ptr(&cpu_data, 0);
    cpu0_data->events_processed += data->events_processed;
    cpu0_data->bytes_transferred += data->bytes_transferred;

    data->active = false;

    return 0;
}

/* ---- procfs: 통계 출력 ---- */
static int cpuhp_example_show(struct seq_file *m, void *v)
{
    int cpu;

    seq_printf(m, "%-6s %-8s %-16s %-16s %-8s\n",
               "CPU", "Active", "Events", "Bytes", "Online#");
    seq_puts(m, "----------------------------------------------\n");

    cpus_read_lock();
    for_each_possible_cpu(cpu) {
        struct cpuhp_example_data *data = per_cpu_ptr(&cpu_data, cpu);
        seq_printf(m, "%-6d %-8s %-16llu %-16llu %-8u\n",
                   cpu,
                   data->active ? "yes" : "no",
                   data->events_processed,
                   data->bytes_transferred,
                   data->online_count);
    }
    cpus_read_unlock();

    return 0;
}

static int cpuhp_example_open(struct inode *inode, struct file *file)
{
    return single_open(file, cpuhp_example_show, NULL);
}

static const struct proc_ops cpuhp_example_ops = {
    .proc_open    = cpuhp_example_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

/* ---- 모듈 초기화/종료 ---- */
static int __init cpuhp_example_init(void)
{
    int ret;

    /* 1단계: Prepare 콜백 등록 (메모리 할당) */
    ret = cpuhp_setup_state(CPUHP_BP_PREPARE_DYN,
                            "cpuhp_example:prepare",
                            cpuhp_example_prepare,
                            cpuhp_example_dead);
    if (ret < 0) {
        pr_err("cpuhp_example: prepare state failed: %d\n", ret);
        return ret;
    }
    hp_prepare = ret;

    /* 2단계: Online 콜백 등록 (서비스 시작) */
    ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                            "cpuhp_example:online",
                            cpuhp_example_online,
                            cpuhp_example_offline);
    if (ret < 0) {
        pr_err("cpuhp_example: online state failed: %d\n", ret);
        cpuhp_remove_state(hp_prepare);
        return ret;
    }
    hp_online = ret;

    /* procfs 엔트리 생성 */
    proc_create("cpuhp_example", 0444, NULL, &cpuhp_example_ops);

    pr_info("cpuhp_example: loaded (prepare=%d, online=%d)\n",
            hp_prepare, hp_online);
    return 0;
}

static void __exit cpuhp_example_exit(void)
{
    remove_proc_entry("cpuhp_example", NULL);

    /* Online 콜백 해제 → 온라인 CPU에 teardown 호출 */
    cpuhp_remove_state(hp_online);

    /* Prepare 콜백 해제 → 온라인 CPU에 dead 호출 */
    cpuhp_remove_state(hp_prepare);

    pr_info("cpuhp_example: unloaded\n");
}

module_init(cpuhp_example_init);
module_exit(cpuhp_example_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("CPU Hotplug callback example");
MODULE_AUTHOR("Example");
# 모듈 빌드 (Makefile)
# obj-m := cpuhp_example.o
# make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 모듈 로드 및 테스트
insmod cpuhp_example.ko
cat /proc/cpuhp_example
# CPU    Active   Events           Bytes            Online#
# ----------------------------------------------
# 0      yes      0                0                1
# 1      yes      0                0                1
# ...

# CPU off/on 테스트
echo 0 > /sys/devices/system/cpu/cpu3/online
# dmesg: cpuhp_example: offline cpu 3 (events=0)
# dmesg: cpuhp_example: dead cpu 3

echo 1 > /sys/devices/system/cpu/cpu3/online
# dmesg: cpuhp_example: prepare cpu 3
# dmesg: cpuhp_example: online cpu 3

cat /proc/cpuhp_example
# CPU 3의 Online# 카운터가 2로 증가

# 모듈 제거
rmmod cpuhp_example
# dmesg: cpuhp_example: unloaded
# (각 CPU에 offline + dead 콜백 자동 호출)

고급 디버깅 기법

CPU Hotplug 문제는 타이밍에 민감한 경쟁 조건이 많아 재현이 어렵습니다. 체계적인 디버깅 방법론을 소개합니다.

ftrace 활용

# 1. cpuhp 전용 트레이스 설정
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo > trace  # 버퍼 초기화

# cpuhp 이벤트 활성화
echo 1 > events/cpuhp/cpuhp_enter/enable
echo 1 > events/cpuhp/cpuhp_exit/enable
echo 1 > events/cpuhp/cpuhp_multi_enter/enable

# 함수 트레이서로 상세 호출 추적
echo function_graph > current_tracer
echo '*cpu_up*' > set_ftrace_filter
echo '*cpu_down*' >> set_ftrace_filter
echo 'cpuhp_*' >> set_ftrace_filter

echo 1 > tracing_on

# CPU off/on 수행
echo 0 > /sys/devices/system/cpu/cpu3/online
echo 1 > /sys/devices/system/cpu/cpu3/online

echo 0 > tracing_on

# 결과 분석
cat trace | head -100
# 각 콜백의 진입/퇴장 시각, 소요 시간, 반환 값 확인

hotplug 실패 강제 주입

# fail 인터페이스로 특정 상태에서 실패 강제
# (CONFIG_CPU_HOTPLUG_STATE_CONTROL=y 필요)
echo 118 > /sys/devices/system/cpu/cpu3/hotplug/fail

# CPU 3 온라인 시도 → 상태 118에서 실패 + 롤백
echo 1 > /sys/devices/system/cpu/cpu3/online
# 실패: -1 반환

# dmesg에서 롤백 과정 확인
dmesg | tail -20
# [xxx] cpuhp/3: Bringing up 3 failed, attempt to roll back

# fail 해제
echo 0 > /sys/devices/system/cpu/cpu3/hotplug/fail

자동화 스트레스 테스트

#!/bin/bash
# cpu_hotplug_stress.sh — CPU hotplug 스트레스 테스트

ITERATIONS=1000
DELAY=0  # 초 (0 = 최대 속도)
LOG="/tmp/cpuhp_stress.log"

# 테스트 대상 CPU (CPU 0 제외)
CPUS=$(cat /sys/devices/system/cpu/online | tr ',' '\n' | tr '-' ' ' | \
       while read a b; do seq $a ${b:-$a}; done | tail -n +2)

echo "Starting stress test: $ITERATIONS iterations" | tee $LOG

fail_count=0
for i in $(seq 1 $ITERATIONS); do
    # 랜덤 CPU 선택
    cpu=$(echo $CPUS | tr ' ' '\n' | shuf -n 1)

    # 현재 상태 확인 후 토글
    state=$(cat /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null)
    if [ "$state" = "1" ]; then
        echo 0 > /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null
        result=$?
    else
        echo 1 > /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null
        result=$?
    fi

    if [ $result -ne 0 ]; then
        echo "[$i] FAIL: cpu$cpu (state=$state)" | tee -a $LOG
        ((fail_count++))
    fi

    [ "$DELAY" != "0" ] && sleep $DELAY
done

# 모든 CPU 복원
for cpu in $CPUS; do
    echo 1 > /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null
done

echo "Completed: $ITERATIONS iterations, $fail_count failures" | tee -a $LOG
echo "Check dmesg for kernel warnings/errors"

일반적인 문제와 해결

증상원인진단 방법해결
CPU 오프라인 hang콜백 내 데드락 또는 무한 루프fail sysfs + ftrace 콜백 추적lockdep 활성화, 콜백 코드 검토
오프라인 후 커널 패닉(Kernel Panic)오프라인 CPU per-CPU 데이터 접근KASAN + cpus_read_lock() 미사용 확인hotplug 잠금 추가
-EBUSY 반환마지막 housekeeping CPU 또는 BSPnohz_full= 파라미터 확인항상 온라인 유지할 CPU 계획
반복 on/off 시 메모리 누수teardown에서 메모리 미해제kmemleak 활성화startup/teardown 대칭 확인
온라인 후 성능 저하sched_domain 비효율적 재빌드/proc/schedstat 확인cpuset 분할로 재빌드 범위 제한
IRQ storm after onlinemanaged IRQ 재활성화 실패/proc/interrupts 비교IRQ 드라이버 hotplug 콜백 점검
커널 빌드 권장 옵션: CPU Hotplug 디버깅 시 다음 커널 옵션을 활성화하세요: CONFIG_PROVE_LOCKING=y (lockdep), CONFIG_KASAN=y (메모리 접근 검사), CONFIG_KMEMLEAK=y (메모리 누수 감지), CONFIG_CPU_HOTPLUG_STATE_CONTROL=y (fail 주입), CONFIG_FTRACE=y (트레이싱).

SMT 보안과 CPU Hotplug

하이퍼스레딩(Hyper-Threading, SMT) 관련 하드웨어 취약점(MDS, L1TF, SRBDS 등)에 대응하기 위해 CPU Hotplug으로 SMT sibling을 비활성화하는 것은 중요한 보안 전략입니다.

# SMT 상태 확인
cat /sys/devices/system/cpu/smt/active
# 1 = SMT 활성, 0 = SMT 비활성

cat /sys/devices/system/cpu/smt/control
# on / off / forceoff / notsupported

# SMT 비활성화 (런타임)
echo off > /sys/devices/system/cpu/smt/control
# → 모든 SMT sibling CPU가 자동 오프라인

# 또는 부팅 파라미터
# nosmt — SMT 부팅 시 비활성화
# mitigations=auto,nosmt — 취약점 완화 + SMT 비활성화

# SMT 토폴로지 확인
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
    [ -f "$cpu/topology/thread_siblings_list" ] && \
    echo "$(basename $cpu): siblings=$(cat $cpu/topology/thread_siblings_list)"
done
# cpu0: siblings=0,4
# cpu1: siblings=1,5
# → CPU 4,5,6,7이 SMT sibling

# 수동으로 SMT sibling만 오프라인
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
    siblings=$(cat $cpu/topology/thread_siblings_list 2>/dev/null)
    core_id=$(cat $cpu/topology/core_id 2>/dev/null)
    # 각 코어의 두 번째 스레드만 오프라인
    first=$(echo $siblings | cut -d',' -f1)
    num=$(basename $cpu | tr -dc '0-9')
    if [ "$num" != "$first" ] && [ -f "$cpu/online" ]; then
        echo 0 > $cpu/online
    fi
done
/* kernel/cpu.c — SMT control 구현 */
static int cpuhp_smt_disable(enum cpuhp_smt_control ctrlval)
{
    int cpu;

    /* SMT sibling을 모두 오프라인 */
    for_each_online_cpu(cpu) {
        if (topology_smt_supported() &&
            !topology_is_primary_thread(cpu)) {
            /* secondary thread → 오프라인 */
            ret = cpu_down_maps_locked(cpu, CPUHP_OFFLINE);
        }
    }
    cpu_smt_control = ctrlval;
    return 0;
}

/* SMT 비활성 시 CPU 온라인 거부 */
static int cpu_smt_allowed(unsigned int cpu)
{
    if (cpu_smt_control == CPU_SMT_ENABLED)
        return true;
    if (topology_is_primary_thread(cpu))
        return true;
    /* secondary thread는 온라인 거부 */
    return false;
}
취약점영향SMT 비활성화 효과대안
MDS (Microarchitectural Data Sampling)SMT sibling 간 버퍼 데이터 누출완전 차단VERW 명령 삽입 (성능 비용)
L1TF (L1 Terminal Fault)L1 캐시 데이터 크로스 스레드 접근완전 차단L1D flush (VM exit 시)
SRBDS (Special Register Buffer)RDRAND/RDSEED 값 누출완전 차단마이크로코드 업데이트
TAA (TSX Asynchronous Abort)TSX 트랜잭션(Transaction) 데이터 누출부분 차단TSX 비활성화
성능 영향: SMT 비활성화는 동시 실행에 의존하는 워크로드에서 처리량(Throughput) 감소를 유발할 수 있습니다. 특히 컴파일, 웹 서버, 데이터베이스 등 동시성이 높은 워크로드에서 영향이 큽니다. 감소 폭은 워크로드와 코어 구성에 따라 달라지므로, 보안 요구사항과 성능 요구사항을 함께 고려해 측정 후 결정하는 것이 좋습니다.

커널 설정 옵션 정리

CPU Hotplug 관련 주요 커널 설정(Kernel Configuration) 옵션을 정리합니다.

옵션기본값설명영향
CONFIG_HOTPLUG_CPUy (대부분)CPU Hotplug 기본 지원필수: 없으면 cpu on/off 불가
CONFIG_SMPy대칭 멀티프로세싱(SMP)전제 조건
CONFIG_HOTPLUG_SMTySMT(Hyper-Threading) 제어smt/control sysfs 노출
CONFIG_CPU_HOTPLUG_STATE_CONTROLnhotplug/fail sysfs 노출디버깅: 특정 상태에서 실패 주입
CONFIG_BOOTPARAM_HOTPLUG_CPU0nCPU 0 오프라인 허용x86: BSP 오프라인 가능 (위험)
CONFIG_DEBUG_HOTPLUG_CPU0n부팅 시 CPU 0 off/on 테스트테스트 용도
CONFIG_MICROCODE_LATE_LOADINGy (x86)런타임 마이크로코드 로딩hotplug 시 마이크로코드 적용
CONFIG_RCU_NOCB_CPUnRCU NOCB 지원hotplug 지연 감소, RT 격리
CONFIG_NO_HZ_FULLnAdaptive-tick 지원housekeeping CPU 보호 로직
# 현재 커널의 hotplug 관련 설정 확인
zcat /proc/config.gz 2>/dev/null | grep -E "HOTPLUG|SMP|MICROCODE|RCU_NOCB|NO_HZ_FULL"
# 또는
grep -E "HOTPLUG|SMP|MICROCODE|RCU_NOCB|NO_HZ_FULL" /boot/config-$(uname -r)

# CPU 0 오프라인 가능 여부 확인
if [ -f /sys/devices/system/cpu/cpu0/online ]; then
    echo "CPU 0 hotplug: supported"
else
    echo "CPU 0 hotplug: not supported"
fi

# 부팅 파라미터 확인
cat /proc/cmdline | tr ' ' '\n' | grep -E "maxcpus|possible_cpus|nr_cpus|isolcpus|nohz|rcu_nocbs|nosmt"

부팅 파라미터 참조

CPU Hotplug 동작에 영향을 주는 주요 커널 부팅 파라미터(Boot Parameter)입니다.

파라미터형식설명사용 예
maxcpus=정수부팅 시 온라인할 최대 CPU 수maxcpus=4 (나머지는 present but offline)
nr_cpus=정수커널이 지원할 최대 CPU 수 (possible)nr_cpus=8 (per-CPU 메모리 절약)
possible_cpus=정수nr_cpus와 동일 (아키텍처별)possible_cpus=16
isolcpus=CPU 목록스케줄러에서 격리할 CPUisolcpus=2,3 또는 isolcpus=managed_irq,2-3
nohz_full=CPU 목록Adaptive-tick CPU (tick 중지)nohz_full=2-7
rcu_nocbs=CPU 목록RCU 콜백 오프로드 CPUrcu_nocbs=2-7
nosmt플래그SMT sibling 비활성화nosmt
cpu0_hotplug플래그CPU 0 오프라인 허용 (x86)cpu0_hotplug
# 실전 부팅 파라미터 조합 예시

# RT 워크로드: CPU 0-1 = 시스템, CPU 2-7 = RT 전용
isolcpus=managed_irq,domain,2-7 nohz_full=2-7 rcu_nocbs=2-7

# 가상화: 최대 128 vCPU 지원, 초기 4개만 온라인
maxcpus=4 nr_cpus=128

# 보안: SMT 비활성 + CPU 0 hotplug 활성
nosmt cpu0_hotplug

# 디버깅: hotplug 상태 제어 + CPU 0 테스트
cpu0_hotplug

모범 사례 요약

CPU Hotplug을 안전하고 효율적으로 사용하기 위한 핵심 모범 사례를 정리합니다.

영역모범 사례안티패턴
동기화cpus_read_lock()으로 for_each_online_cpu() 보호잠금 없이 cpu_online_mask 순회
메모리 할당Prepare 콜백에서 GFP_KERNEL 할당, Starting에서 사용Starting 콜백에서 GFP_KERNEL (슬립 불가!)
콜백 대칭startup/teardown이 정확히 역 관계startup에서 할당, teardown에서 미해제 (누수)
Workqueuehotplug 빈번 환경: WQ_UNBOUND 또는 ordered wqper-CPU wq에서 장시간 work 실행
IRQmanaged IRQ 사용, affinity 자동 관리 위임수동 affinity 설정 후 hotplug 미대응
NUMA같은 노드 내 최소 1 CPU 유지전체 노드 CPU 오프라인 (원격 메모리 접근)
디버깅lockdep + KASAN + ftrace cpuhp 이벤트로그 없이 반복 on/off 테스트
RTisolcpus + nohz_full + rcu_nocbs 조합RT CPU를 hotplug으로 관리 (지연 발생)
보안SMT 비활성화: nosmt 또는 smt/controlSMT 활성 상태에서 사이드 채널(Side-Channel) 미대응
가상화maxcpus=로 초기 vCPU 제한, 동적 추가nr_cpus= 과도 설정 (per-CPU 메모리 낭비)
체크리스트: 드라이버에 CPU Hotplug 지원을 추가할 때 다음 항목을 확인하세요. (1) for_each_online_cpu() 사용 시 cpus_read_lock() 보호 여부, (2) per-CPU 데이터 접근 시 해당 CPU 온라인 상태 확인, (3) IRQ affinity가 오프라인 CPU를 포함하지 않는지, (4) 콜백 등록 시 cpuhp_setup_state() vs nocalls vs multi 선택 근거, (5) 모듈 언로드 시 cpuhp_remove_state() 호출 여부, (6) KASAN/lockdep 활성화 상태에서 반복 on/off 스트레스 테스트 통과.

최신 변경사항 (v6.14~v7.2, 2024-2026)

2024년부터 2026년까지 CPU 핫플러그 서브시스템은 RISC-V 병렬 핫플러그(HOTPLUG_PARALLEL), ACPI idle 등록 리팩터, x86/AMD 토폴로지 탐지 정리, arm64 HOTPLUG_PARALLEL 도입 논의 등 핵심 변경을 거쳤습니다. 버전 기준은 2026년 10월 현재 안정판 v7.2이며, 유지 중인 longterm 커널 버전은 v6.18입니다.

버전변경영향
v6.14 커널 스레드 선호 CPU 친화도(Affinity) API 추가 (kthread_affine_preferred()) include/linux/kthread.h 대조 결과 v6.13에는 kthread_bind_mask()까지만 존재하고 kthread_affine_preferred()는 없으며, v6.14에서 처음 등장합니다(v7.2까지 유지). 힌트일 뿐 요구 조건이 아니므로, CPU 오프라인 후 워커 재배치 시 NUMA 지역성을 보존하는 힌트로 활용됩니다
v6.18 x86 AMD 토폴로지 탐지 코드 리팩터링 v6.18에서 x86 AMD 토폴로지 탐지 코드가 리팩터링되어 255개가 넘는 vCPU 환경에서 올바른 토폴로지 정보를 확보합니다. 관련 패치 세트는 KVM/QEMU 게스트에서 topoext를 명시적으로 켜지 않을 때 발생하던 [Firmware Bug]: CPU 512: APIC ID mismatch 보고를 다루며, 원인은 8비트 APIC ID의 표현 범위를 넘었기 때문입니다
v6.18 ACPI idle 드라이버 등록(Driver Registration) 최적화 → 긴급 리버트 v6.18 머지 윈도우에서 커밋 ACPI: processor: idle: Optimize ACPI idle driver registration이 CPU 핫플러그 콜백이 아닌 "모든 CPU 온라인 이후 일괄 등록" 방식으로 idle 드라이버를 등록하도록 바꿨습니다. 이 변경이 cpuhp_setup_state() 관련 변경에 의존해 일부 시스템에서 커널 크래시를 유발해 출시 직전 v6.18에서 되돌려졌습니다(Rafael J. Wysocki, "Urgent ACPI support fix for v6.18"). CPU 상태 머신의 등록 순서 의존성을 보여 주는 대표 사례입니다
v6.19 RISC-V 보조 CPU 병렬 핫플러그(CONFIG_HOTPLUG_PARALLEL) arch/riscv/kernel/smpboot.c 대조 결과 v6.18에는 cpu_running 컴플리션이 무조건 선언되어 있었으나, v6.19에서 #ifndef CONFIG_HOTPLUG_PARALLEL 가드로 바뀌면서 arch_cpuhp_kick_ap_alive() 구현과 cpuhp_ap_sync_alive() 호출이 smp_callin()에 추가되었습니다. 즉 런타임 cpu_up() 경로가 x86/ARM64과 동일한 병렬 bringup 구조를 갖게 되었습니다
v7.2 (검증됨) cpuhp 상태 영역의 지속적인 확장 v7.2 include/linux/cpuhotplug.h 기준 이름 붙은 상태는 179개이며, CPUHP_ONLINE 값은 236입니다. 디바이스별 PREPARE 상태(CPUHP_IOMMU_IOVA_DEAD 등)와 아키텍처별 STARTING 상태가 계속 추가되고 있습니다. 참고로 CPUHP_AP_PCIE_* 같은 상태는 이 버전에 존재하지 않습니다
v7.2 (확증) Rust의 cpuhp 바인딩 부재 v7.2 rust/kernel/ 104개 항목에는 cpu.rs, cpufreq.rs, cpumask.rs가 있으나 cpuhp 전용 바인딩 모듈은 없습니다. cpu.rs는 nr_cpu_ids()와 CpuId, from_cpu()만 제공하며, from_cpu() 문서는 CPU 오프라인 시 디바이스가 unregistered 된 뒤에도 메모리가 해제되지 않으므로 CPU hotplug notifier로 참조를 정리하라고 안내합니다. 즉 Rust 드라이버가 cpuhp 상태를 직접 등록하는 경로는 없으며, CPU 생존 여부는 CpuId로만 다룰 수 있습니다
검증 기준: 위 표의 각 행은 다음 기준으로 확인했습니다.
  • 커널 소스 대조 — v6.14 kthread.h vs v6.13, v6.18/v6.19 arch/riscv/kernel/smpboot.c, v7.2 include/linux/cpuhotplug.h·kernel/cpu.c·rust/kernel/·drivers/resctrl/ 원문 대조
  • 패치 시리즈와 릴리스 공지 — v6.18 AMD 토폴로지 리팩터링과 ACPI idle 등록 긴급 리버트는 해당 패치 세스와 Rafael J. Wysocki의 "Urgent ACPI support fix for v6.18" 릴리스 공지를 근거로 했습니다
실제 배포 커널에 적용 여부를 판단할 때는 /sys/devices/system/cpu/hotplug/states와 커널 Makefile의 VERSION/PATCHLEVEL/SUBLEVEL 값을 직접 확인하시기 바랍니다.

RISC-V 병렬 핫플러그 (v6.19)

RISC-V는 SBI(Supervisor Binary Interface) HSM(Hart State Management) 확장을 통해 보조 하트(hart)를 on/off합니다. 여기서 주의할 점은 두 "병렬" 개념이 다르다는 것입니다. 부팅 시 병렬 브링업(parallel boot) 프레임워크는 x86에서 먼저 만들어져 kernel/cpu.c의 공통 코드(CPUHP_BP_KICK_AP, cpuhp_bringup_cpus_parallel())에 구현되어 있고, v6.19 이전의 RISC-V는 이 경로를 사용하지 않았습니다. arch/riscv/kernel/smpboot.c를 버전별로 대조하면 v6.18까지 cpu_running 컴플리션이 무조건 선언되어 wait_for_completion_timeout()로 보조 하트를 하나씩 순차 대기한 반면, v6.19에서 #ifndef CONFIG_HOTPLUG_PARALLEL 가드로 바뀌면서 arch_cpuhp_kick_ap_alive() 구현과 smp_callin()의 cpuhp_ap_sync_alive() 호출이 추가되었습니다. 즉 v6.19의 변경은 새로운 병렬화를 도입한 것이 아니라, 런타임 CPU hotplug(cpu_up()) 경로가 이미 존재하는 공통 병렬 bringup 프레임워크를 RISC-V도 공유하게 된 것입니다.

HOTPLUG_PARALLEL의 동작 원리: 병렬 bringup에서는 제어 CPU가 각 AP를 CPUHP_BP_KICK_AP 상태까지 한꺼번에 진행시킨 뒤 즉시 다음 AP로 넘어갑니다. 각 AP는 저수준 bringup 코드를 스스로 통과해 마지막 대기점에서 합류한 뒤, 제어 CPU가 CPUHP_ONLINE까지를 하나씩 최종 진행시킵니다. 순차 방식과 달리 제어 CPU가 각 AP의 CPUHP_BRINGUP_CPU 응답을 차례로 기다리지 않으므로, 전체 지연은 각 AP 지연의 합이 아니라 개별 최대 지연에 좌우됩니다. 아래 코드는 v7.2 kernel/cpu.c와 arch/riscv/kernel/smpboot.c의 실제 구현입니다.
/* kernel/cpu.c — 아키텍처 공통 병렬 bringup 진입점 (CONFIG_HOTPLUG_PARALLEL) */
/*
 * On architectures which have enabled parallel bringup this invokes all BP
 * prepare states for each of the to be onlined APs first. The last state
 * sends the startup IPI to the APs. The APs proceed through the low level
 * bringup code in parallel and then wait for the control CPU to release
 * them one by one for the final onlining procedure.
 *
 * This avoids waiting for each AP to respond to the startup IPI in
 * CPUHP_BRINGUP_CPU.
 */
static bool cpuhp_bringup_cpus_parallel(unsigned int ncpus)
{
    const struct cpumask *mask = cpu_present_mask;

    if (__cpuhp_parallel_bringup)
        __cpuhp_parallel_bringup = arch_cpuhp_init_parallel_bringup();
    if (!__cpuhp_parallel_bringup)
        return false;

    if (cpuhp_smt_aware()) {
        const struct cpumask *pmask = cpuhp_get_primary_thread_mask();
        static struct cpumask tmp_mask __initdata;

        /*
         * X86 requires to prevent that SMT siblings stopped while
         * the primary thread does a microcode update for various
         * reasons. Bring the primary threads up first.
         */
        cpumask_and(&tmp_mask, mask, pmask);
        cpuhp_bringup_mask(&tmp_mask, ncpus, CPUHP_BP_KICK_AP);
        cpuhp_bringup_mask(&tmp_mask, ncpus, CPUHP_ONLINE);
        ncpus -= num_online_cpus();
        if (!ncpus)
            return true;
        /* Create the mask for secondary CPUs */
        cpumask_andnot(&tmp_mask, mask, pmask);
        mask = &tmp_mask;
    }

    /* Bring the not-yet started CPUs up */
    cpuhp_bringup_mask(mask, ncpus, CPUHP_BP_KICK_AP);
    cpuhp_bringup_mask(mask, ncpus, CPUHP_ONLINE);
    return true;
}

/* 기본값은 true — 아키텍처가 오버라이드하지 않아도 CONFIG가 켜져 있으면 병렬 동작 */
bool __weak arch_cpuhp_init_parallel_bringup(void)
{
    return true;
}
/* arch/riscv/kernel/smpboot.c — RISC-V측 구현 (v6.19에서 추가) */
static int start_secondary_cpu(unsigned int cpu, struct task_struct *tidle)
{
    if (cpu_ops->cpu_start)
        return cpu_ops->cpu_start(cpu, tidle);

    return -EOPNOTSUPP;
}

#ifdef CONFIG_HOTPLUG_PARALLEL */
int arch_cpuhp_kick_ap_alive(unsigned int cpu, struct task_struct *tidle)
{
    return start_secondary_cpu(cpu, tidle);
}
#else
int __cpu_up(unsigned int cpu, struct task_struct *tidle)
{
    int ret;
    tidle->thread_info.cpu = cpu;

    ret = start_secondary_cpu(cpu, tidle);
    if (!ret) {
        wait_for_completion_timeout(&cpu_running, secs_to_jiffies(1));

        if (!cpu_online(cpu)) {
            pr_crit("CPU%u: failed to come online\n", cpu);
            ret = -EIO;
        }
    } else {
        pr_crit("CPU%u: failed to start\n", cpu);
    }

    return ret;
}
#endif

/* 보조 하트 진입점(smp_callin) — 병렬 경로에서는 alive 통지만 수행 */
asmlinkage __visible void smp_callin(void)
{
    /* ... 토폴로지 저장 / IPI 활성화 / NUMA 등록 ... */
#ifdef CONFIG_HOTPLUG_PARALLEL
    cpuhp_ap_sync_alive();
#endif
    /* ... set_cpu_online(), icache/tlb flush ... */
    cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);
}
확인 방법: 병렬 bringup 경로가 실제로 동작했는지는 kernel/cpu.c의 cpuhp 트레이스포인트로 확인할 수 있습니다. /sys/kernel/debug/tracing/events/cpuhp/에 cpuhp_enter, cpuhp_exit, cpuhp_multi_enter 이벤트가 있으므로 이를 활성화한 뒤 echo 1 > /sys/devices/system/cpu/cpuN/online을 수행하면 각 CPU의 상태 전이 시각을 비교해 병렬/순차 여부를 판별할 수 있습니다. 상세 절차는 본 문서의 ftrace 활용 절에 있습니다.

ARM64 MPAM과 cpuhp

MPAM(Memory Partitioning and Monitoring)은 CPU와 캐시·메모리 컨트롤러가 제공하는 기능으로, 메모리 트래픽에 라벨을 붙이고 분할·모니터링합니다. 커널은 이를 resctrl 사용자 인터페이스로 노출하며, 정책은 resctrl의 schemata 파일로 설정하고 모니터 값은 resctrl로 읽습니다. 드라이버는 drivers/resctrl/mpam_devices.c, mpam_resctrl.c, mpam_internal.h로 구성되며, 사용 가능한 제어 기능은 L2/L3의 캐시 portion bitmap(CPOR), L3 이후 메모리 대역폭(Bandwidth) 상한(MBW_MAX), L3 그룹의 캐시 저장 사용량(CSU) 카운터입니다.

실제 cpuhp 연동 확인 결과: 이 드라이버는 v7.2 기준으로 include/linux/cpuhotplug.h의 동적 ONLINE 슬롯 CPUHP_AP_ONLINE_DYN에 콜백을 등록합니다. 전용 cpuhp 상태를 열거형에 추가하지 않고 동적 슬롯을 쓰므로, /sys/devices/system/cpu/hotplug/states에는 등록된 콜백 이름(mpam:drv_probe, mpam:online)으로 나타나고 정적 상태 수에는 포함되지 않습니다.

드라이버는 cpuhp 상태를 두 단계에 걸쳐 재등록합니다. mpam_register_cpuhp_callbacks()는 기존 상태를 cpuhp_remove_state()로 해제한 뒤 새 콜백을 등록하고, 등록된 상태 번호는 mpam_cpuhp_state에 캐시되며 mpam_cpuhp_state_lock 뮤텍스(Mutex)로 보호됩니다.

단계등록 시점online 콜백offline 콜백콜백 이름
1. 탐색(probe) 펌웨어가 알린 모든 MSC(memory system component) 디바이스의 probe가 완료된 시점
(atomic_add_return(1, &mpam_num_msc) == fw_num_msc)
mpam_discovery_cpu_online() 없음(NULL) mpam:drv_probe
2. 활성화(enabled) static_branch_enable(&mpam_enabled) 이후 resctrl 활성화 시점 mpam_cpu_online() mpam_cpu_offline() mpam:online
/* drivers/resctrl/mpam_devices.c — 동적 ONLINE 슬롯 등록 */
static int mpam_cpuhp_state;
static DEFINE_MUTEX(mpam_cpuhp_state_lock);

static void mpam_register_cpuhp_callbacks(int (*online)(unsigned int online),
                                  int (*offline)(unsigned int offline),
                                  char *name)
{
    mutex_lock(&mpam_cpuhp_state_lock);
    if (mpam_cpuhp_state) {
        cpuhp_remove_state(mpam_cpuhp_state);
        mpam_cpuhp_state = 0;
    }

    mpam_cpuhp_state = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, name, online,
                     offline);
    if (mpam_cpuhp_state <= 0) {
        pr_err("Failed to register cpuhp callbacks");
        mpam_cpuhp_state = 0;
    }
    mutex_unlock(&mpam_cpuhp_state_lock);
}
/* online 콜백 — CPU가 온라인될 때 해당 CPU가 접근 가능한 MSC를 다시 설정 */
static int mpam_cpu_online(unsigned int cpu)
{
    struct mpam_msc *msc;

    guard(srcu)(&mpam_srcu);
    list_for_each_entry_srcu(msc, &mpam_all_msc, all_msc_list,
                     srcu_read_lock_held(&mpam_srcu)) {
        /* 이 MSC를 접근할 수 있는 CPU인지 확인 */
        if (!cpumask_test_cpu(cpu, &msc->accessibility))
            continue;

        if (msc->reenable_error_ppi)
            _enable_percpu_irq(&msc->reenable_error_ppi);

        /* 첫 번째 온라인 CPU일 때만 하드웨어 재설정 */
        if (atomic_fetch_inc(&msc->online_refs) == 0)
            mpam_reprogram_msc(msc);
    }

    if (mpam_resctrl_enabled)
        return mpam_resctrl_online_cpu(cpu);

    return 0;
}
이 구현이 잘 보여 주는 설계 요점:
  • Online 구간을 선택한 이유 — CPUHP_AP_ONLINE_DYN은 IRQ와 선점(Preemption)이 활성화된 상태이므로 슬립이 가능합니다. 드라이버도 그에 맞게 srcu 가드와 mutex_lock()을 씁니다. Starting 구간(CPUHP_AP_ONLINE_DYN보다 앞선 IRQ 비활성 구간)에 등록했다면 이 코드들은 슬립 때문에 호출되지 않았을 것입니다. 이는 본 문서의 "콜백 작성 시 주의사항" 표의 Online 구간 허용/금지 항목이 실제 코드에서 어떻게 지켜지는지 보여 주는 사례입니다.
  • Online 콜백은 실패해도 된다 — mpam_discovery_cpu_online()은 하드웨어 probe 오류를 그대로 반환합니다. 실패 시 상태 머신이 이미 성공한 단계의 teardown을 역순 호출합니다. 반면 IRQ 비활성인 Starting 구간은 실패가 허용되지 않으므로, 실패 가능한 처리는 Online 구간에 두어야 합니다.
  • 레퍼런스 카운팅으로 중복 작업 억제 — mpam_cpu_online()은 atomic_fetch_inc(&msc->online_refs)가 0에서 1이 될 때만 mpam_reprogram_msc()를 호출합니다. 하나의 MSC를 여러 CPU가 공유하는 구조에서 하드웨어 재설정을 CPU 수만큼 반복하지 않기 위한 기법입니다. 오프라인 쪽 mpam_cpu_offline()은 atomic_dec_and_test()가 참일 때 RIS(reset information state)를 되돌립니다.
  • 재등록 시 반드시 이전 상태를 먼저 해제 — mpam_register_cpuhp_callbacks()는 등록 전에 cpuhp_remove_state()를 호출합니다. 정적 상태는 한 번만 등록하면 되지만, 드라이버처럼 여러 단계에서 다른 콜백으로 전환해야 하는 경우에는 이 순서가 중요합니다.

arm64 HOTPLUG_PARALLEL (2026, 진행 중)

x86과 RISC-V에 이미 들어간 병렬 CPU bringup(HOTPLUG_PARALLEL)을 ARM64에 도입하려는 작업이 2026년 6월부터 진행되고 있습니다. Jinjie Ruan이 제안한 [PATCH RFC] arm64: Add HOTPLUG_PARALLEL support for secondary CPUs 시리즈가 그 시작으로, Will Deacon이 리뷰를 이어가고 있습니다. arch/arm64/kernel/smp.c를 v7.2 기준으로 대조하면 아직 static DECLARE_COMPLETION(cpu_running);과 __cpu_up()의 순차 경로가 남아 있고 CONFIG_HOTPLUG_PARALLEL 분기가 없으므로, v7.2 시점에는 mainline에 병합되지 않은 상태입니다. 따라서 정식 버전 번호는 아직 정해지지 않았습니다.

병렬 bringup의 필요성: 대형 ARM64 서버는 secondary CPU를 순차로 부팅하면 각 CPU를 하나씩 기다리는 시간이 모두 더해집니다. 병렬화하면 CPUHP_BP_KICK_AP 지점까지 여러 AP를 동시에 진행시켜 합산 대기 시간(Latency)을 줄일 수 있습니다. 참고로 RISC-V의 경우 v6.18의 wait_for_completion_timeout() 순차 경로가 v6.19에서 arch_cpuhp_kick_ap_alive() 기반 병렬 경로로 바뀐 사례가 있어, arm64에도 유사한 변경이 적용될 것으로 보입니다.

CONFIG_PARALLEL_SMT_PRIMARY_FIRST (2026)

arm64 병렬 bringup과 함께 SMT(동시 멀티스레딩) 프라이머리 스레드를 먼저 부팅하는 정책 옵션 CONFIG_PARALLEL_SMT_PRIMARY_FIRST(후기 버전에서 CONFIG_HOTPLUG_PARALLEL_SMT로 개명 논의)가 제안되었습니다. SMT sibling을 최대한 늦게 부팅해, 병렬 도메인 초기화 중 캐시·TLB 등 공유 자원 경합(Contention)을 줄이는 것이 목적입니다. x86의 parallel_cpus 재정렬 로직과 동일한 원리입니다.

arm64: secondary CPU의 RCU reporting 지연 (2026)

arm64 병렬 bringup을 위해 secondary CPU가 로컬 CPU 능력(capability) 검사 전까지 RCU quiescent state reporting을 지연하는 변경(arm64: smp: Defer RCU reporting until after local CPU capability checks)도 함께 제안되었습니다. 병렬로 부팅되는 CPU들이 동시에 RCU 상태를 보고하면서 발생할 수 있는 데이터 경합(Race)을 막기 위한 것입니다.

참고자료

커널 공식 문서

문서 이력 참고: 예전에는 Documentation/admin-guide/cpu-hotplug.rst 문서가 별도로 존재했으나 v7.2 기준 트리에서 제거되었습니다(Documentation/admin-guide/에 cpu-hotplug.rst가 없음). sysfs 인터페이스와 maxcpus 설명은 Documentation/core-api/cpu_hotplug.rst로 통합되었으므로, 링크가 404가 되면 위의 core-api 문서를 사용하세요. 과거 core-api/cpu_hotplug.html#testing-of-hotplug-states 앵커도 현재 문서에 해당 섹션이 없어 더 이상 유효하지 않습니다.

LWN.net 기사

커널 소스

필수 관련 문서:
  • CPU 토폴로지 (CPU Topology) — 논리 CPU 번호가 소켓·코어·SMT로 어떻게 매핑되는지 정의합니다. CPU Hotplug의 대상 단위(논리 CPU)를 이해하는 전제 조건입니다.
  • 프로세스 스케줄러 — sched_domain 재구성, 태스크 마이그레이션, 런큐 이동 규칙. CPU 오프라인 때 가장 큰 비용이 발생하는 구간을 다룹니다.
  • 인터럽트 (Interrupts) — IRQ affinity와 인터럽트 라우팅(Routing). 오프라인 CPU가 보유한 IRQ를 다른 CPU로 재분배하는 동작의 근거입니다.
  • RCU (Read-Copy-Update) — grace period와 quiescent state. 오프라인 CPU가 RCU 관점에서 정지 상태를 보고해야 하는 이유를 설명합니다.
  • Per-CPU 변수 — per-CPU 영역 할당과 접근 규칙. Prepare/Online 콜백에서 per-CPU 자원을 다루는 규칙의 근거가 됩니다.
보조 관련 문서:
  • 전원 관리 (Power Management) — suspend/resume와 PM QoS. 서스펜드 시 CPU 오프라인과 연동되는 freeze/thaw 경로를 다룹니다.
  • CPUFreq — CPU 주파수 스케일링(Frequency Scaling)과 governor. CPU 오프라인/온라인에 맞춰 디바이스가 제거·재등록되는 과정이 다뤄집니다.
  • CPUIdle — CPU 유휴 상태(Idle State)(C-states) 관리. idle 데몬 기동 정지(CPUHP_AP_IDLE_DEAD)와 cpuidle 결합 상태 처리를 다룹니다.
  • 열 관리 (Thermal Management) — thermal zone과 cooling device. 과열 CPU를 오프라인시키는 열 제어 경로를 다룹니다.
  • Workqueue (CMWQ) — per-CPU worker pool 관리. 오프라인 CPU의 pending work를 다른 CPU로 옮기는 마이그레이션 정책을 다룹니다.