CPU Hotplug
리눅스 커널의 CPU 온라인/오프라인 메커니즘을 상태 머신부터 아키텍처별 구현, 서브시스템 연동, 실전 활용까지 심층 분석합니다.
핵심 요약
CPU Hotplug 6가지 핵심 개념
- 런타임 CPU 전환: CPU Hotplug은 시스템 실행 중에 논리 CPU를
online또는offline상태로 전환하는 커널 메커니즘입니다. 물리적 CPU 제거 없이 소프트웨어적으로 CPU를 비활성화하여 전력 절감, 격리(Isolation), 유지보수에 활용합니다. - cpuhp_state 상태 머신: CPU의 bring-up과 tear-down은
CPUHP_OFFLINE에서CPUHP_ONLINE까지 수십 개의 상태를 순차적으로 거칩니다. 각 상태에 등록된 콜백(Callback)이 호출되며, 실패 시 자동으로 롤백(Rollback)됩니다. - 3단계 콜백 구조: Hotplug 콜백은 Prepare(제어 CPU에서 실행, 메모리 할당 등), Starting(타겟 CPU, IRQ 비활성 상태에서 HW 초기화), Online(타겟 CPU, IRQ 활성 상태에서 서브시스템 알림)의 3개 영역으로 나뉩니다.
- 리소스 마이그레이션: CPU가 오프라인될 때 RCU 콜백, hrtimer, timer wheel, IRQ affinity, workqueue의 pending work 등 CPU에 바인딩된 리소스를 다른 온라인 CPU로 안전하게 이동합니다.
- sysfs 사용자 제어:
/sys/devices/system/cpu/cpuN/online파일에 0 또는 1을 기록하여 사용자 공간(User Space)에서 CPU를 제어합니다.online,offline,possible,present비트마스크로 상태를 확인합니다. - 전력/가상화(Virtualization) 핵심: cpufreq/thermal governor가 CPU를 동적으로 끄고 켜며 전력을 관리하고, QEMU/KVM 등 하이퍼바이저(Hypervisor)에서 vCPU를 hot-add/remove하는 기반이 됩니다. suspend/hibernate 시에도 BSP 외 CPU를 모두 오프라인합니다.
단계별 이해
CPU Hotplug 학습 로드맵
-
Step 1: CPU 상태 확인
lscpu명령으로 시스템의 CPU 구성을 파악합니다./sys/devices/system/cpu/디렉터리에서online,offline,possible,present파일을 읽어 현재 CPU 비트마스크를 확인합니다.nproc으로 활성 CPU 수를 빠르게 조회할 수 있습니다. -
Step 2: CPU offline/online 실습
echo 0 > /sys/devices/system/cpu/cpu3/online로 CPU를 오프라인하고,echo 1로 다시 온라인합니다.dmesg와/proc/interrupts의 변화를 관찰하여 커널이 어떤 작업을 수행하는지 파악합니다. -
Step 3: Hotplug 콜백 구조 이해
cat /sys/devices/system/cpu/hotplug/states로 전체 cpuhp 상태 목록을 확인합니다. 각 상태에 등록된 콜백 이름이 표시되므로, 어떤 서브시스템이 어떤 단계에서 동작하는지 파악할 수 있습니다. -
Step 4: 서브시스템 마이그레이션 관찰
CPU 오프라인 전후로
/proc/interrupts,/proc/softirqs,/proc/timer_list를 비교하여 IRQ, 타이머(Timer), RCU 콜백이 어떻게 마이그레이션되는지 관찰합니다. -
Step 5: 드라이버 hotplug 콜백 작성
cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, ...)으로 간단한 콜백을 등록하는 커널 모듈(Kernel Module)을 작성해 봅니다. 온라인/오프라인 시 콜백이 호출되는 것을printk로 확인합니다. -
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 컬럼의 카운터가 더 이상 증가하지 않음
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
/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 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은 역순입니다.
상태 머신의 핵심 원칙: bring-up 시 상태 번호가 증가하며 각 상태의 startup 콜백이 호출되고, teardown 시 역순으로 teardown 콜백이 호출됩니다. 어느 콜백이든 실패하면 이미 수행된 콜백을 역순으로 롤백(undo)합니다.
상태 머신
cpuhp 상태는 크게 3개 영역으로 나뉩니다.
| 영역 | 상태 범위 | 실행 위치 | IRQ | 용도 |
|---|---|---|---|---|
| Prepare | CPUHP_OFFLINE+1 ~ CPUHP_BRINGUP_CPU | 제어 CPU | 활성 | 메모리 할당, 스레드(Thread) 생성 등 준비 작업 |
| Starting | CPUHP_AP_OFFLINE ~ CPUHP_AP_ONLINE-1 | 타겟 CPU | 비활성 | 저수준 HW 초기화, GIC/타이머 설정 |
| Online | CPUHP_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을 실행하면 다음 경로를 따릅니다.
/* 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_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로 이동해야 합니다.
/* 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 환경에서 같은 노드 내 이동이 메모리 접근 지연을 최소화합니다.
- 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가 다시 온라인되어도 자동 복원되지 않습니다.
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가 다시 온라인되면 자동 재시작됩니다. - 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_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를 보고해야 합니다.
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 로 확인 */
| 항목 | 일반 CPU | NOCB CPU |
|---|---|---|
| 콜백 실행 위치 | softirq (RCU_SOFTIRQ) | 전용 kthread (rcuop/N) |
| 오프라인 시 콜백 처리 | 다른 CPU로 이동 필요 | kthread가 계속 처리 |
| 오프라인 지연(Latency) | 콜백 수에 비례 | 최소 |
| RT 적합성 | softirq 지연 발생 | kthread 우선순위(Priority)로 제어 가능 |
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(전통적 타이머) 두 가지 타이머 체계를 사용하며, 각각 별도의 마이그레이션 경로를 갖습니다.
/* 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 상태 | 특이사항 |
|---|---|---|---|
| hrtimer | hrtimers_cpu_dying() | CPUHP_AP_HRTIMERS_DYING | RB-tree 순회, 4개 clock base |
| timer wheel | timers_dead_cpu() | CPUHP_TIMERS_DEAD | 512 슬롯 벡터 전체 순회 |
| clock_event | tick_cleanup_dead_cpu() | CPUHP_AP_TICK_DYING | HW 타이머 장치 해제 |
| NOHZ tick | tick_nohz_cpu_down() | CPUHP_AP_TICK_NOHZ | 마지막 housekeeping CPU 보호 |
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;
}
cpu_possible_mask 기준이므로, nr_cpu_ids(또는 maxcpus= 부팅 파라미터)가 크면 미사용 메모리가 낭비될 수 있습니다. possible_cpus=로 제한할 수 있습니다.
스케줄러 통합
CPU Hotplug 시 스케줄러는 sched_domain 계층을 재구성해야 합니다.
/* 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 isolation | isolcpus= 부팅 파라미터로 스케줄러에서 제외 |
/* 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에서 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를 깨우는 방법은 두 가지입니다.
| 방식 | 메커니즘 | 사용 환경 |
|---|---|---|
| PSCI | Secure Monitor(EL3) 호출: psci_cpu_on() | 대부분의 프로덕션 SoC (ATF/OP-TEE) |
| spin-table | release_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;
}
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;
}
디버깅
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"
/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}/online | SMT 비활성화 효과 (보안) |
| 커널 업데이트 테스트 | 반복 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();
cpus_read_lock() 안에서 cpu_up()/cpu_down()을 호출하면 데드락입니다. 읽기 잠금(Lock) 보유 중 쓰기 잠금 요청은 영원히 블록됩니다. lockdep이 이를 감지하여 경고합니다.
Workqueue 마이그레이션 전략
Workqueue는 CPU Hotplug에서 특히 복잡한 처리가 필요한 서브시스템입니다. bound workqueue(per-CPU)와 unbound workqueue의 동작이 근본적으로 다르기 때문입니다.
/* 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_HIGHPRI | nice -20 pool도 동일 드레인 | 높음 |
| WQ_CPU_INTENSIVE | concurrency 관리 제외, 동일 드레인 | 보통 |
/* 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에 큐잉됨 */
alloc_workqueue() 시 WQ_MEM_RECLAIM 플래그가 있으면 rescuer thread가 개입하여 교착을 방지합니다.
WQ_UNBOUND 또는 alloc_ordered_workqueue()를 사용하면 마이그레이션 오버헤드(Overhead)를 피할 수 있습니다. per-CPU workqueue는 캐시(Cache) 지역성이 중요한 경우에만 사용하세요.
일반적인 경쟁 조건(Race Condition)과 디버깅
CPU Hotplug은 비동기적으로 CPU 상태를 변경하므로, 다양한 경쟁 조건이 발생할 수 있습니다. 가장 흔한 패턴과 해결 방법을 살펴봅니다.
가장 빈번한 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
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
| 콜백 단계 | 대표적 비용 높은 콜백 | 소요 시간 범위 | 원인 |
|---|---|---|---|
| Prepare | CPUHP_PERF_PREPARE | 1-10ms | 메모리 할당, 버퍼(Buffer) 초기화 |
| Prepare | CPUHP_WORKQUEUE_PREP | 1-5ms | worker pool 준비 |
| Starting | CPUHP_AP_IRQ_GIC_STARTING | 0.1-1ms | GIC redistributor 설정 |
| Online | CPUHP_AP_SCHED_STARTING | 1-50ms | sched_domain 재빌드 (CPU 수 비례) |
| Teardown | 태스크 마이그레이션 | 1-100ms | 런큐 태스크 수 비례 |
| Teardown | IRQ 재분배 | 0.5-10ms | IRQ 개수 비례 |
cpuset으로 도메인을 분할하면 재빌드 범위를 제한할 수 있습니다. 또한 CPUHP_AP_ONLINE_DYN 슬롯에 등록된 불필요한 콜백을 정리하면 전체 hotplug 시간을 단축할 수 있습니다.
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 유사) | 동결 |
| CPU | BSP 외 전체 offline | BSP 외 전체 offline |
| 메모리 | 유지 (전원 공급) | 디스크에 이미지 저장 |
| 복원 | wakeup IRQ → CPU online → thaw | 부팅 → 이미지 로드 → CPU online → thaw |
| CPU 복원 방식 | thaw_secondary_cpus() | enable_nonboot_cpus() |
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에 대응하는 펌웨어 인터페이스입니다.
/* 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 */
}
| 항목 | x86 | ARM64 | RISC-V |
|---|---|---|---|
| 부팅 메커니즘 | INIT/SIPI IPI | PSCI CPU_ON (SMC) | SBI HSM hart_start (ecall) |
| 정지 메커니즘 | HLT/MWAIT + APIC | PSCI CPU_OFF | SBI HSM hart_stop |
| 상태 조회 | APIC 레지스터(Register) | PSCI AFFINITY_INFO | SBI HSM hart_get_status |
| 펌웨어 | BIOS/UEFI | ATF/OP-TEE (EL3) | OpenSBI (M-mode) |
| 진입점 전달 | 트램펄린 물리 주소(Physical Address) | PSCI cpu_on arg | hart_start start_addr |
| 특권 레벨 | Ring 0 (BSP → AP) | EL1 → EL3 → EL1 | S-mode → M-mode → S-mode |
| suspend 지원 | S3 (ACPI) | PSCI SUSPEND | HSM HART_SUSPEND |
lib/sbi/sbi_hsm.c에서 hart 상태 전이와 전원 관리를 처리합니다. SiFive HiFive Unmatched, StarFive VisionFive 2 등 상용 보드에서 사용됩니다.
-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 |
|---|---|---|---|---|
| Prepare | CPUHP_BP_PREPARE_DYN | 20개 | 제어 CPU에서 실행, 메모리 할당 가능 | cpuhp_setup_state(CPUHP_BP_PREPARE_DYN, ...) |
| Online | CPUHP_AP_ONLINE_DYN | 40개 | 타겟 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);
콜백 함수(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;
}
드라이버 핫플러그 대응 패턴
디바이스 드라이버(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 확인
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 상속 */
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가 오프라인되면 파티션 무효화 | 일반 모드로 전환 |
cpuset.cpus.partition을 root로 설정하면 해당 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
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 또는 BSP | nohz_full= 파라미터 확인 | 항상 온라인 유지할 CPU 계획 |
| 반복 on/off 시 메모리 누수 | teardown에서 메모리 미해제 | kmemleak 활성화 | startup/teardown 대칭 확인 |
| 온라인 후 성능 저하 | sched_domain 비효율적 재빌드 | /proc/schedstat 확인 | cpuset 분할로 재빌드 범위 제한 |
| IRQ storm after online | managed IRQ 재활성화 실패 | /proc/interrupts 비교 | IRQ 드라이버 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 비활성화 |
커널 설정 옵션 정리
CPU Hotplug 관련 주요 커널 설정(Kernel Configuration) 옵션을 정리합니다.
| 옵션 | 기본값 | 설명 | 영향 |
|---|---|---|---|
CONFIG_HOTPLUG_CPU | y (대부분) | CPU Hotplug 기본 지원 | 필수: 없으면 cpu on/off 불가 |
CONFIG_SMP | y | 대칭 멀티프로세싱(SMP) | 전제 조건 |
CONFIG_HOTPLUG_SMT | y | SMT(Hyper-Threading) 제어 | smt/control sysfs 노출 |
CONFIG_CPU_HOTPLUG_STATE_CONTROL | n | hotplug/fail sysfs 노출 | 디버깅: 특정 상태에서 실패 주입 |
CONFIG_BOOTPARAM_HOTPLUG_CPU0 | n | CPU 0 오프라인 허용 | x86: BSP 오프라인 가능 (위험) |
CONFIG_DEBUG_HOTPLUG_CPU0 | n | 부팅 시 CPU 0 off/on 테스트 | 테스트 용도 |
CONFIG_MICROCODE_LATE_LOADING | y (x86) | 런타임 마이크로코드 로딩 | hotplug 시 마이크로코드 적용 |
CONFIG_RCU_NOCB_CPU | n | RCU NOCB 지원 | hotplug 지연 감소, RT 격리 |
CONFIG_NO_HZ_FULL | n | Adaptive-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 목록 | 스케줄러에서 격리할 CPU | isolcpus=2,3 또는 isolcpus=managed_irq,2-3 |
nohz_full= | CPU 목록 | Adaptive-tick CPU (tick 중지) | nohz_full=2-7 |
rcu_nocbs= | CPU 목록 | RCU 콜백 오프로드 CPU | rcu_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에서 미해제 (누수) |
| Workqueue | hotplug 빈번 환경: WQ_UNBOUND 또는 ordered wq | per-CPU wq에서 장시간 work 실행 |
| IRQ | managed IRQ 사용, affinity 자동 관리 위임 | 수동 affinity 설정 후 hotplug 미대응 |
| NUMA | 같은 노드 내 최소 1 CPU 유지 | 전체 노드 CPU 오프라인 (원격 메모리 접근) |
| 디버깅 | lockdep + KASAN + ftrace cpuhp 이벤트 | 로그 없이 반복 on/off 테스트 |
| RT | isolcpus + nohz_full + rcu_nocbs 조합 | RT CPU를 hotplug으로 관리 (지연 발생) |
| 보안 | SMT 비활성화: nosmt 또는 smt/control | SMT 활성 상태에서 사이드 채널(Side-Channel) 미대응 |
| 가상화 | maxcpus=로 초기 vCPU 제한, 동적 추가 | nr_cpus= 과도 설정 (per-CPU 메모리 낭비) |
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 스트레스 테스트 통과.
참고 자료
커널 공식 문서
- CPU Hotplug in the Kernel — cpuhp 상태 머신, 콜백 등록 API
- CPU Hotplug (admin-guide) — sysfs 인터페이스, maxcpus 부팅 매개변수
- Suspend and CPU Hotplug — 서스펜드 시 CPU 오프라인 처리
- Testing Hotplug States — cpuhp 상태별 테스트 방법
- CPU Topology (sysfs) — 온라인/오프라인 CPU 토폴로지 정보
LWN.net 기사
- CPU hotplug rework — cpuhp 상태 머신 리팩터링 배경
- Reworking CPU hotplug, again — 단계별 콜백 등록 새 API
- CPU hotplug and IRQ affinity — 오프라인 CPU의 IRQ 재배치
- The stop_machine() API — CPU hotplug 시 stop_machine 동기화
- SMT control and security — SMT disable 시 CPU hotplug 활용
- Multi-instance CPU hotplug callbacks — cpuhp_setup_state_multi() 사용법
커널 소스
kernel/cpu.c— CPU hotplug 코어, cpuhp 상태 머신kernel/smpboot.c— SMP 부팅, idle 스레드 생성kernel/stop_machine.c— stop_machine 동기화 메커니즘include/linux/cpuhotplug.h— cpuhp 상태 enum 정의kernel/sched/core.c— 스케줄러 CPU hotplug 콜백kernel/rcu/tree.c— RCU CPU hotplug 핸들러kernel/workqueue.c— Per-CPU worker pool hotplug 처리
관련 문서
CPU Hotplug과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.