CPU Hotplug

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

관련 문서: CPU Hotplug은 다양한 서브시스템과 연동됩니다. 전원 관리(Power Management) 개요는 전원 관리, CPU 주파수 제어는 CPUFreq, 스케줄러(Scheduler) 통합은 스케줄러, 인터럽트(Interrupt) 처리는 인터럽트 페이지(Page)를 참고하세요.

핵심 요약

CPU Hotplug 6가지 핵심 개념

  1. 런타임 CPU 전환: CPU Hotplug은 시스템 실행 중에 논리 CPU를 online 또는 offline 상태로 전환하는 커널 메커니즘입니다. 물리적 CPU 제거 없이 소프트웨어적으로 CPU를 비활성화하여 전력 절감, 격리(Isolation), 유지보수에 활용합니다.
  2. cpuhp_state 상태 머신: CPU의 bring-up과 tear-down은 CPUHP_OFFLINE에서 CPUHP_ONLINE까지 수십 개의 상태를 순차적으로 거칩니다. 각 상태에 등록된 콜백(Callback)이 호출되며, 실패 시 자동으로 롤백(Rollback)됩니다.
  3. 3단계 콜백 구조: Hotplug 콜백은 Prepare(제어 CPU에서 실행, 메모리 할당 등), Starting(타겟 CPU, IRQ 비활성 상태에서 HW 초기화), Online(타겟 CPU, IRQ 활성 상태에서 서브시스템 알림)의 3개 영역으로 나뉩니다.
  4. 리소스 마이그레이션: CPU가 오프라인될 때 RCU 콜백, hrtimer, timer wheel, IRQ affinity, workqueue의 pending work 등 CPU에 바인딩된 리소스를 다른 온라인 CPU로 안전하게 이동합니다.
  5. sysfs 사용자 제어: /sys/devices/system/cpu/cpuN/online 파일에 0 또는 1을 기록하여 사용자 공간(User Space)에서 CPU를 제어합니다. online, offline, possible, present 비트마스크로 상태를 확인합니다.
  6. 전력/가상화(Virtualization) 핵심: cpufreq/thermal governor가 CPU를 동적으로 끄고 켜며 전력을 관리하고, QEMU/KVM 등 하이퍼바이저(Hypervisor)에서 vCPU를 hot-add/remove하는 기반이 됩니다. suspend/hibernate 시에도 BSP 외 CPU를 모두 오프라인합니다.

단계별 이해

CPU Hotplug 학습 로드맵

  1. Step 1: CPU 상태 확인

    lscpu 명령으로 시스템의 CPU 구성을 파악합니다. /sys/devices/system/cpu/ 디렉터리에서 online, offline, possible, present 파일을 읽어 현재 CPU 비트마스크를 확인합니다. nproc으로 활성 CPU 수를 빠르게 조회할 수 있습니다.

  2. Step 2: CPU offline/online 실습

    echo 0 > /sys/devices/system/cpu/cpu3/online로 CPU를 오프라인하고, echo 1로 다시 온라인합니다. dmesg/proc/interrupts의 변화를 관찰하여 커널이 어떤 작업을 수행하는지 파악합니다.

  3. Step 3: Hotplug 콜백 구조 이해

    cat /sys/devices/system/cpu/hotplug/states로 전체 cpuhp 상태 목록을 확인합니다. 각 상태에 등록된 콜백 이름이 표시되므로, 어떤 서브시스템이 어떤 단계에서 동작하는지 파악할 수 있습니다.

  4. Step 4: 서브시스템 마이그레이션 관찰

    CPU 오프라인 전후로 /proc/interrupts, /proc/softirqs, /proc/timer_list를 비교하여 IRQ, 타이머(Timer), RCU 콜백이 어떻게 마이그레이션되는지 관찰합니다.

  5. Step 5: 드라이버 hotplug 콜백 작성

    cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)으로 간단한 콜백을 등록하는 커널 모듈(Kernel Module)을 작성해 봅니다. 온라인/오프라인 시 콜백이 호출되는 것을 printk로 확인합니다.

  6. Step 6: 디버깅 (ftrace, hotplug 실패 분석)

    ftrace의 cpuhp 이벤트를 활성화하여 각 콜백의 실행 순서와 소요 시간을 추적합니다. 콜백 실패 시 /sys/devices/system/cpu/cpuN/hotplug/fail에서 실패 상태를 분석합니다.

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 격리
가상화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_possible_mask (부팅 시 고정) cpu_present_mask (물리 존재) cpu_online_mask (스케줄러 참여) cpu_active_mask (마이그레이션 대상) 오프라인 CPU

CPU Hotplug 아키텍처

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

/* 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 열거형은 CPU hotplug 상태 머신의 모든 단계를 정의합니다. 상태는 세 영역으로 나뉩니다.

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

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

OFFLINE PREPARE 단계 제어 CPU에서 실행 BRINGUP_CPU 타겟 CPU 깨움 STARTING 단계 타겟 CPU, IRQ 꺼짐 AP_ONLINE ONLINE prepare CB __cpu_up() IRQ off IRQ on online CB ← Teardown: 역순 콜백 호출 →

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

상태 머신

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

영역상태 범위실행 위치IRQ용도
PrepareCPUHP_OFFLINE+1 ~ CPUHP_BRINGUP_CPU제어 CPU활성메모리 할당, 스레드(Thread) 생성 등 준비 작업
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.ccpuhp_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_DYNCPUHP_BP_PREPARE_DYN 영역은 모듈이 런타임에 콜백을 등록할 수 있는 동적 슬롯입니다. cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)으로 사용합니다.

CPU Bring-up 과정

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

sysfs write("1") cpu_device_up() cpu_up() → _cpu_up() cpuhp_kick_ap() Prepare 콜백 순차 호출 __cpu_up() → 아키텍처 x86: INIT/SIPI | ARM: PSCI 타겟 CPU 깨어남 start_secondary()/secondary_start_kernel() Starting 콜백 (IRQ off) GIC, 타이머, perf 초기화 CPUHP_ONLINE 도달 Online 콜백 (IRQ on)
/* 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 쓰기 잠금을 획득하여 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 3 (오프라인 중) Task A Task B Task C CPU 0 ← Task A CPU 1 ← Task B CPU 2 ← Task C select_fallback_rq(): cpumask_any_and(affinity, cpu_active_mask)
/* 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.cselect_fallback_rq()는 CPU 오프라인 시 태스크를 이동할 대상 CPU를 결정하는 폴백 로직입니다.

  • cpu_to_node(cpu)오프라인되는 CPU의 NUMA 노드 ID를 조회합니다. NUMA 환경에서 같은 노드 내 이동이 메모리 접근 지연을 최소화합니다.
  • 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.cirq_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가 다시 온라인되면 자동 재시작됩니다.
  • 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에서 고해상도 타이머를 마이그레이션합니다. 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()에서 처리합니다.

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 N offline rcutree_dying_cpu() rcu_report_dead() QS 강제 보고 rcu_node 마스크 제거 qsmaskinitnext &= ~mask Grace Period 완료 qs_pending 해제 콜백 드레인/이동 rcu_migrate_callbacks() CPU 완전 정지 CPUHP_OFFLINE 도달 CONFIG_RCU_NOCB_CPU (No-Callback CPU) rcu_nocbs= 파라미터로 지정된 CPU의 RCU 콜백을 전용 kthread(rcuog/N, rcuop/N)로 오프로드 Hotplug 시 콜백 이동 불필요 → 오프라인 지연 감소, RT CPU 격리에 필수

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 설정 — 콜백 오프로드 */
/* 부팅 파라미터: rcu_nocbs=2-7 */
/* 또는 커널 6.2+ 동적 전환: */
/* echo 1 > /sys/devices/system/cpu/cpu3/rcu_nocb */

/* NOCB CPU의 콜백 처리 kthread */
/* rcuog/N — grace period 감시 */
/* rcuop/N — 콜백 실행 */
/* ps aux | grep rcu 로 확인 */
항목일반 CPUNOCB CPU
콜백 실행 위치softirq (RCU_SOFTIRQ)전용 kthread (rcuop/N)
오프라인 시 콜백 처리다른 CPU로 이동 필요kthread가 계속 처리
오프라인 지연(Latency)콜백 수에 비례최소
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 hrtimer_cpu_base timer_base (wheel) hrtimer base lock 획득 old_base + new_base 동시 타이머 스캔 4개 clock base Target CPU enqueue 완료 timer_base lock spin_lock_irq(base->lock) 벡터 순회 512 슬롯 전체 Target CPU 재삽입 완료 clock_event_device unbind clockevents_notify(CLOCK_EVT_NOTIFY_CPU_DEAD) → 다음 이벤트 재프로그래밍 NOHZ_FULL: tick_nohz_cpu_down() — adaptive-tick 모드 해제 후 마이그레이션
/* 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 계층을 재구성해야 합니다.

CPU Offline 시 sched_domain 재구성 Before (CPU 0-3 online) NUMA domain: {0,1,2,3} MC: {0,1} MC: {2,3} CPU0 CPU1 CPU2 CPU3 ✗ After (CPU 3 offline) NUMA domain: {0,1,2} MC: {0,1} MC: {2} CPU0 CPU1 CPU2
/* 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 메커니즘을 사용합니다.

호스트 (QEMU/KVM) QMP: device_add ACPI GPE 인젝션 KVM_CREATE_VCPU ioctl 게스트 커널 ACPI: _OST 핫플러그 acpi_processor_add() cpu_up() → ONLINE 게스트 부팅 시 maxcpus=N 으로 초기 vCPU 제한
# 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;
}
Linux 커널 EL1: cpu_psci_cpu_boot() SMC 호출 EL1 → EL3 ATF (EL3) PSCI CPU_ON 처리 Secondary CPU 전원 ON + 리셋 secondary_start_kernel() GIC init → 타이머 → cpuhp Starting 콜백

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 Boot: INIT → SIPI → Protected Mode → Long Mode BSP INIT IPI SIPI ×2 Real Mode 트램펄린 코드 Protected Mode GDT/IDT 설정 Long Mode 64-bit 전환 start_secondary() cpuhp 상태 머신 INIT 10ms SIPI 300μs 페이지 테이블 APIC/GIC 초기화

디버깅

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();
Reader 1 Reader 2 Writer cpus_read_lock cpus_read_lock 대기 (blocked) cpus_write_lock (cpu_down) 모든 reader 해제 후 writer 진입
데드락 주의: cpus_read_lock() 안에서 cpu_up()/cpu_down()을 호출하면 데드락입니다. 읽기 잠금(Lock) 보유 중 쓰기 잠금 요청은 영원히 블록됩니다. lockdep이 이를 감지하여 경고합니다.

Workqueue 마이그레이션 전략

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

Dying CPU: Per-CPU Pool Work A Work B Work C worker_pool (nice 0/nice -20) Drain Pending 실행 중 work 완료 대기 Reassign pending work 재배치 Target CPU Pool Work A Work B Work C worker thread 재생성 WQ_UNBOUND Workqueue — CPU Hotplug 영향 없음 NUMA affinity mask만 갱신, work 재배치 불필요, system_unbound_wq 등 Ordered Workqueue: max_active=1 보장, 실행 순서 변경 없음 (내부적으로 unbound)
/* 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가 개입하여 교착을 방지합니다.
best practice: hotplug 빈번한 환경에서는 WQ_UNBOUND 또는 alloc_ordered_workqueue()를 사용하면 마이그레이션 오버헤드(Overhead)를 피할 수 있습니다. per-CPU workqueue는 캐시(Cache) 지역성이 중요한 경우에만 사용하세요.

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

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

cpu_online_mask 경쟁 조건 CPU A CPU B 결과 for_each_online_cpu() cpu_down(cpu3) per_cpu(data, cpu3) ← stale! BUG: 오프라인 CPU 접근 T1: mask 읽기 T2: cpu3 offline 완료 T3: stale 데이터 사용 해결: cpus_read_lock() mask 읽기 전 잠금

가장 빈번한 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=yCONFIG_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_PREPARE1-10ms메모리 할당, 버퍼(Buffer) 초기화
PrepareCPUHP_WORKQUEUE_PREP1-5msworker pool 준비
StartingCPUHP_AP_IRQ_GIC_STARTING0.1-1msGIC redistributor 설정
OnlineCPUHP_AP_SCHED_STARTING1-50mssched_domain 재빌드 (CPU 수 비례)
Teardown태스크 마이그레이션1-100ms런큐 태스크 수 비례
TeardownIRQ 재분배0.5-10msIRQ 개수 비례
대규모 시스템 최적화: 100+ CPU 시스템에서 sched_domain 재빌드는 O(N^2)에 가까운 비용이 들 수 있습니다. cpuset으로 도메인을 분할하면 재빌드 범위를 제한할 수 있습니다. 또한 CPUHP_AP_ONLINE_DYN 슬롯에 등록된 불필요한 콜백을 정리하면 전체 hotplug 시간을 단축할 수 있습니다.
SLA 주의: CPU hotplug 소요 시간은 커널 빌드 옵션과 하드웨어에 따라 10ms~500ms 이상 변동합니다. 실시간(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()    — 사용자 프로세스 동결 */
/*   2. freeze_kernel_threads() — 커널 스레드 동결 */
/*   3. suspend_disable_secondary_cpus() — CPU 오프라인 */
/*   4. syscore_suspend()     — 최종 HW 설정 */
/*   5. (실제 절전 진입) */
/*   복원은 역순: */
/*   6. syscore_resume() */
/*   7. suspend_enable_secondary_cpus() — CPU 온라인 */
/*   8. thaw_kernel_threads() */
/*   9. thaw_processes() */
단계Suspend (S3)Hibernate (S4)
프로세스(Process)동결 (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 Hart 상태 머신 STOPPED START_PENDING sbi_hart_start() STARTED STOP_PENDING sbi_hart_stop() SUSPENDED (S3 절전) sbi_hart_start boot 완료 sbi_hart_stop 정지 완료 suspend resume sbi_hart_get_status(hartid) — 임의 시점에 hart 상태 조회 가능 (비파괴적)
/* arch/riscv/kernel/smpboot.c — RISC-V CPU boot */
static int __cpu_up_sbi(unsigned int cpu,
                       struct task_struct *tidle)
{
    unsigned long hartid = cpuid_to_hartid_map(cpu);
    unsigned long start_addr = __pa_symbol(secondary_start_sbi);

    /* SBI HSM: hart 시작 요청 */
    struct sbiret ret = sbi_ecall(
        SBI_EXT_HSM,           /* extension ID */
        SBI_EXT_HSM_HART_START,/* function ID */
        hartid,                /* a0: target hartid */
        start_addr,            /* a1: 진입점 물리 주소 */
        0,                     /* a2: opaque 값 (hartid) */
        0, 0, 0);

    if (ret.error)
        return -EIO;

    /* secondary_start_sbi()에서 hart가 깨어남 */
    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 과정에서 거쳐야 하는 모든 상태를 정의합니다. 커널 6.x 기준으로 약 200개 이상의 상태가 정의되어 있으며, 크게 정적 상태동적 상태로 나뉩니다.

/* include/linux/cpuhotplug.h — 상태 열거형 상세 (커널 6.x) */
enum cpuhp_state {
    CPUHP_INVALID            = -1,
    CPUHP_OFFLINE            = 0,

    /* ====== PREPARE 영역 (제어 CPU에서 실행) ====== */
    CPUHP_CREATE_THREADS,           /* smpboot 스레드 생성 */
    CPUHP_PERF_PREPARE,             /* perf 이벤트 버퍼 할당 */
    CPUHP_WORKQUEUE_PREP,           /* worker pool 준비 */
    CPUHP_HRTIMERS_PREPARE,         /* hrtimer 베이스 초기화 */
    CPUHP_SMPCFD_PREPARE,           /* SMP 함수 호출 데이터 할당 */
    CPUHP_RELAY_PREPARE,            /* relay 채널 버퍼 할당 */
    CPUHP_SLAB_PREPARE,             /* SLAB 캐시 per-CPU 구조체 할당 */
    CPUHP_RCUTREE_PREP,             /* RCU 트리 노드 준비 */
    CPUHP_CPUIDLE_PREP,             /* cpuidle 드라이버 준비 */
    CPUHP_PAGE_ALLOC,               /* per-CPU 페이지 할당자 준비 */
    CPUHP_NET_DEV_DEAD,             /* 네트워크 디바이스 정리 */

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

    CPUHP_BRINGUP_CPU,              /* 아키텍처별 CPU 시작 */

    /* ====== STARTING 영역 (타겟 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_ARM_ARCH_TIMER_STARTING,/* ARM 아키텍처 타이머 */
    CPUHP_AP_PERF_X86_STARTING,     /* x86 PMU 초기화 */
    CPUHP_AP_KVM_STARTING,          /* KVM vCPU 초기화 */
    CPUHP_AP_HRTIMERS_DYING,        /* hrtimer 마이그레이션 */
    CPUHP_AP_TICK_DYING,            /* tick 장치 해제 */

    /* ====== ONLINE 영역 (타겟 CPU, IRQ 활성) ====== */
    CPUHP_AP_ONLINE,                /* 온라인 마커 */
    CPUHP_AP_ACTIVE,                /* active_mask 설정 */
    CPUHP_AP_SMPBOOT_THREADS,       /* smpboot 스레드 시작 */
    CPUHP_AP_X86_VDSO_VMA_ONLINE,   /* vDSO 갱신 */
    CPUHP_AP_PERF_ONLINE,           /* perf 이벤트 활성화 */
    CPUHP_AP_WORKQUEUE_ONLINE,      /* workqueue 온라인 */
    CPUHP_AP_RCUTREE_ONLINE,        /* RCU 트리 온라인 */

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

    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, ...)
상태 번호 조회: cat /sys/devices/system/cpu/hotplug/states로 현재 커널에 등록된 모든 cpuhp 상태와 콜백 이름을 확인할 수 있습니다. 각 줄은 상태번호: 콜백이름 형식입니다.
# 등록된 상태 확인
cat /sys/devices/system/cpu/hotplug/states
# 출력 예:
#   0: offline
#   1: threads:prepare
#   2: perf:prepare
#   ...  
# 118: sched:starting
# 119: RCU/tree:dying
#   ...  
# 169: online

# 동적 슬롯 사용 현황
cat /sys/devices/system/cpu/hotplug/states | grep "DYN\|dyn"
# 또는 상태 번호 범위로 필터링
cat /sys/devices/system/cpu/hotplug/states | awk -F: '{if ($1+0 >= 150 && $1+0 <= 169) 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);
cpuhp_setup_state() vs cpuhp_setup_state_nocalls() 동작 비교 cpuhp_setup_state() 1. 콜백 등록 2. 이미 온라인 CPU에 startup() 호출 CPU 0: startup(0) → CPU 1: startup(1) → ... → CPU N: startup(N) 하나라도 실패하면: 성공한 CPU에 teardown() 역순 호출 + 등록 취소 모듈 로드 시 per-CPU 자원 초기화에 적합 cpuhp_setup_state_nocalls() 1. 콜백 등록만 기존 온라인 CPU에 호출 없음 이후 새로 온라인되는 CPU에만 startup() 호출 초기화가 별도로 이루어지는 서브시스템에 적합 cpuhp_setup_state_multi() — 멀티 인스턴스 패턴 1. 콜백 등록 (템플릿) 2. 인스턴스 추가 cpuhp_state_add_instance() 3. CPU 이벤트 시 각 인스턴스별 콜백 호출 사용 사례: NIC 드라이버 (여러 포트), 블록 디바이스 (여러 큐), 암호화 엔진 (여러 컨텍스트) 각 인스턴스가 hlist_node를 포함 → 콜백에서 container_of()로 인스턴스 식별

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

영역콜백 제약사항허용 동작금지 동작
Prepare제어 CPU에서 실행, IRQ 활성GFP_KERNEL 할당, mutex, 슬립(sleep)타겟 CPU per-CPU 데이터 직접 수정
Starting타겟 CPU, IRQ 비활성spinlock, 레지스터 접근, 원자적 연산슬립, 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이 안전하게 동작해야 합니다.

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

디바이스 드라이버(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 토폴로지와 CPU Hotplug

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

NUMA 환경 CPU Hotplug 시 영향 범위 NUMA Node 0 (로컬 메모리 128GB) CPU 0 CPU 1 CPU 2 CPU 3 (offline) sched_domain: MC {0,1,2} (CPU 3 제거됨) 로드 밸런싱 범위 축소 → 남은 CPU 부하 증가 NUMA Node 1 (로컬 메모리 128GB) CPU 4 CPU 5 CPU 6 CPU 7 sched_domain: MC {4,5,6,7} (변경 없음) CPU 3의 태스크 중 일부가 이 노드로 마이그레이션 가능 QPI/UPI CPU 3 오프라인 시 NUMA 영향 스케줄러 Node 0 sched_domain 재빌드 NUMA 밸런싱 가중치 재계산 select_fallback_rq: 같은 노드 우선 메모리 Node 0 메모리는 그대로 유지 per-CPU 메모리 해제 안 됨 NUMA balancing 대상 CPU 감소 IRQ/디바이스 Node 0 장치 IRQ 재분배 PCIe 장치 NUMA affinity 유지 irqbalance 데몬 재계산
# 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과의 상호작용 */
/* CPU 오프라인 → cpuset_cpu_inactive() 호출 */
/* → cpuset의 effective_cpus에서 해당 CPU 제거 */
/* → 자식 cpuset도 재귀적으로 업데이트 */
/* → effective_cpus가 비면 부모 cpuset의 CPU 상속 */
NUMA 노드 완전 오프라인: 한 NUMA 노드의 모든 CPU를 오프라인하면, 해당 노드의 메모리에 접근하는 모든 프로세스가 리모트 노드 CPU에서 실행됩니다. QPI/UPI 인터커넥트(Interconnect)를 통한 원격 메모리 접근은 로컬 대비 1.5~3배 느릴 수 있습니다.

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가 오프라인되면 파티션 무효화일반 모드로 전환
Partition 모드: cgroup v2의 cpuset.cpus.partitionroot로 설정하면 해당 cpuset의 CPU가 부모에서 완전히 분리됩니다. 이때 분리된 CPU를 오프라인하면 파티션이 invalid 상태가 되며, 커널 로그에 경고가 출력됩니다. 복원하려면 CPU를 다시 온라인한 후 partition을 재설정해야 합니다.

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

CPU 마이크로코드 업데이트는 CPU Hotplug의 bring-up 과정에서 적용됩니다. 보안 취약점(Vulnerability) 패치나 하드웨어 에라타(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 활성화, 콜백 코드 검토
오프라인 후 커널 패닉오프라인 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 트랜잭션 데이터 누출부분 차단TSX 비활성화
성능 영향: SMT 비활성화는 워크로드에 따라 15~40%의 처리량 감소를 초래할 수 있습니다. 특히 컴파일, 웹 서버, 데이터베이스 등 동시성이 높은 워크로드에서 영향이 큽니다. 보안 요구사항과 성능 요구사항을 균형 있게 평가해야 합니다.

커널 설정 옵션 정리

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 스트레스 테스트 통과.

참고 자료

커널 공식 문서

LWN.net 기사

커널 소스

CPU Hotplug과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.