CPUIdle
Linux 커널 CPU 유휴 상태 관리(CPUIdle) 서브시스템을 심층 분석합니다. C-state별 특성과 전이 비용, menu/ladder/teo governor의 예측 로직, tick/nohz 및 timer 이벤트 상호작용, exit latency·target residency 기반 선택 기준, cpufreq·scheduler·interrupt 부하와의 연계, 전력 절감과 tail latency 사이의 트레이드오프를 고려한 실전 튜닝까지 종합적으로 다룹니다.
핵심 요약
- C-state -- CPU 유휴 전력 상태. C0(활성)부터 C10(최심층 수면)까지 깊어질수록 전력 절감이 크지만 복귀 지연도 증가합니다.
- cpuidle_driver -- 하드웨어별 C-state 테이블과 진입/복귀 함수를 등록하는 드라이버 인터페이스입니다.
- cpuidle_governor -- 다음 유휴 시간을 예측하여 최적 C-state를 선택하는 정책 엔진(menu, teo, ladder, haltpoll)입니다.
- do_idle() -- 스케줄러 idle 루프의 핵심 함수. governor 선택 → driver 진입 → 깨어남 → 통계 갱신 순서로 동작합니다.
- PM QoS -- 사용자/드라이버가 최대 허용 복귀 지연 시간을 설정하여 깊은 C-state 진입을 제한하는 메커니즘입니다.
단계별 이해
- 유휴 진입 경로 이해
스케줄러가 실행 큐가 비었음을 감지 → idle 태스크 선택 →do_idle()호출 → CPUIdle 서브시스템으로 진입합니다. - Governor의 C-state 선택
Governor가 다음 타이머 이벤트, 최근 유휴 패턴, PM QoS 제약을 종합하여 최적 C-state 인덱스를 반환합니다. - Driver의 C-state 진입
선택된 C-state의enter()콜백이 호출되어MWAIT,HLT또는 ACPI 방식으로 CPU를 저전력 상태로 전환합니다. - 깨어남과 통계 갱신
인터럽트나 타이머로 깨어나면 실제 수면 시간을 측정하고, governor에 피드백하여 다음 예측 정확도를 높입니다.
개요
CPUIdle은 CPU가 유휴 상태일 때 저전력 C-states로 전환하여 전력을 절약하는 리눅스 커널 서브시스템입니다. 현대 프로세서는 대부분의 시간을 유휴 상태로 보내기 때문에(데스크톱 시스템 기준 70~95%), CPUIdle의 C-state 선택 품질이 전체 시스템 전력 소비와 응답 지연에 직접적인 영향을 미칩니다.
CPUIdle 서브시스템은 drivers/cpuidle/ 디렉토리에 위치하며, 크게 세 가지 컴포넌트로 구성됩니다:
| 컴포넌트 | 역할 | 소스 파일 |
|---|---|---|
| Core | 드라이버/거버너 등록, sysfs 인터페이스, 통계 수집 | cpuidle.c, sysfs.c |
| Driver | 하드웨어별 C-state 테이블, 진입/복귀 함수 | driver.c, intel_idle, acpi_idle |
| Governor | C-state 선택 정책 (예측 알고리즘) | governor.c, governors/menu.c, governors/teo.c |
C-states vs P-states
CPU 전력 관리에는 두 가지 직교하는 차원이 있습니다. C-states는 유휴 시 전력을 줄이고, P-states는 실행 중 주파수/전압을 조절합니다:
| 항목 | C-states (CPUIdle) | P-states (CPUFreq) |
|---|---|---|
| 목적 | 유휴 시 전력 절약 | 실행 중 주파수 조정 |
| 상태 | CPU 유휴 (대기) | CPU 활성 (실행) |
| 전력 절감 | 매우 큼 (90-99%) | 중간 (50-80%) |
| 복귀 지연 | 수 us ~ ms | 거의 없음 (0 us) |
| 트리거 | 스케줄러 (유휴 감지) | 부하 변화 |
| 제어 | CPUIdle Governor | CPUFreq Governor |
| ACPI 표준 | ACPI Cx states | ACPI Px states |
| 커널 소스 | drivers/cpuidle/ | drivers/cpufreq/ |
소스 디렉토리 구조
| 경로 | 설명 |
|---|---|
drivers/cpuidle/cpuidle.c | 코어: cpuidle_idle_call(), 등록/해제, 통계 |
drivers/cpuidle/driver.c | 드라이버 등록/해제, per-CPU 드라이버 관리 |
drivers/cpuidle/governor.c | 거버너 등록/전환, 현재 거버너 관리 |
drivers/cpuidle/sysfs.c | sysfs 인터페이스 (/sys/devices/system/cpu/cpuidle/) |
drivers/cpuidle/governors/menu.c | Menu governor 구현 |
drivers/cpuidle/governors/teo.c | TEO governor 구현 |
drivers/cpuidle/governors/ladder.c | Ladder governor 구현 |
drivers/cpuidle/governors/haltpoll.c | Haltpoll governor 구현 (VM 게스트) |
drivers/idle/intel_idle.c | Intel 전용 네이티브 C-state 드라이버 |
drivers/acpi/processor_idle.c | ACPI 기반 범용 C-state 드라이버 |
include/linux/cpuidle.h | 핵심 구조체 및 API 선언 |
C-states (전력 상태)
C-states는 ACPI 표준에서 정의한 CPU 유휴 전력 상태입니다. 숫자가 클수록 더 많은 하드웨어 컴포넌트를 꺼서 전력을 절약하지만, 다시 활성 상태(C0)로 복귀하는 데 더 오래 걸립니다.
C-state 종류
| C-state | 이름 | 전력 | 복귀 지연 | 동작 |
|---|---|---|---|---|
| C0 | Active | 100% | 0 us | 정상 실행 |
| C1 | Auto Halt | 10-20% | 0-1 us | CPU 클럭 정지 (HLT) |
| C1E | Enhanced Halt | 5-10% | 1-10 us | 클럭 + 전압 낮춤 |
| C2 | Stop Grant | 3-5% | 10-20 us | L2 캐시 유지 |
| C3 | Sleep | 1-2% | 20-100 us | L2 캐시 플러시 |
| C6 | Deep Sleep | 0.1-0.5% | 100-200 us | 코어 전원 차단 |
| C7 | Deeper Sleep | 0.05% | 200-300 us | LLC 캐시 유지 |
| C8 | Deepest Sleep | 0.01% | 300-500 us | LLC 캐시 플러시 |
| C10 | Package | 0.001% | 500+ us | 패키지 전체 차단 |
C-state 동작 원리
- C1:
HLT명령어 실행 → 클럭 게이팅. 코어 로직은 유지되며 인터럽트 즉시 응답 - C1E:
HLT+ 코어 전압 강하. C1보다 절전 효과가 크지만 전압 복원 시간 필요 - C3: L2 캐시 flush → SRAM 전원 차단. 캐시 미스 비용이 복귀 오버헤드에 추가
- C6: 코어 레지스터를 전용 SRAM에 저장 → 코어 전원 차단 → 복귀 시 레지스터 복원
- C7/C8: LLC(Last Level Cache) 관리. C7은 LLC 유지, C8은 LLC까지 플러시
- C10: 패키지 전체 전원 차단 (멀티 소켓 시 패키지 단위). 모든 코어가 깊은 C-state에 진입해야 가능
깊은 C-state일수록 더 많은 하드웨어를 끄므로 절전 효과가 크지만, 복귀 시 하드웨어를 다시 켜야 하므로 지연이 증가합니다.
C-state 하드웨어 진입 메커니즘
x86 아키텍처에서 C-state 진입 방식은 크게 세 가지입니다:
| 방식 | 명령어 | 장점 | 단점 | 사용 드라이버 |
|---|---|---|---|---|
| MWAIT | MONITOR/MWAIT | 힌트 비트로 정밀한 C-state 지정 가능 | Intel 전용(AMD는 제한적) | intel_idle |
| HLT | HLT | 범용, 단순 | C1만 가능 | 폴백 드라이버 |
| ACPI I/O Port | inb(P_LVLx) | BIOS 정의, 범용 | I/O 포트 접근 오버헤드 | acpi_idle |
/* MWAIT를 이용한 C-state 진입 (intel_idle 드라이버) */
static __cpuidle int intel_idle(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
struct cpuidle_state *state = &drv->states[index];
unsigned long eax = flg2MWAIT(state->flags);
unsigned int cstate;
cstate = (((eax) >> 4) & 0xf) + 1; /* MWAIT 힌트에서 C-state 추출 */
/*
* MONITOR: 감시할 메모리 주소 설정
* MWAIT: 해당 주소에 쓰기가 발생하거나
* 인터럽트가 올 때까지 대기
*/
__monitor((void *)¤t_thread_info()->flags, 0, 0);
if (!need_resched())
__mwait(eax, 1); /* eax: C-state 힌트, ecx: 확장 플래그 */
return index;
}
/* ACPI I/O 포트를 이용한 C-state 진입 (acpi_idle 드라이버) */
static int acpi_idle_enter_bm(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
struct acpi_processor_cx *cx = per_cpu(acpi_cstate[index], dev->cpu);
/* P_LVL2/P_LVL3 I/O 포트 읽기로 C-state 진입 */
inb(cx->address); /* 이 I/O 읽기가 CPU를 유휴 상태로 전환 */
/* C3의 경우 Bus Master 활동 확인 필요 */
if (cx->type == ACPI_STATE_C3) {
u32 bm_sts = acpi_read_bit_register(ACPI_BITREG_BUS_MASTER_STATUS);
if (bm_sts)
acpi_write_bit_register(ACPI_BITREG_BUS_MASTER_STATUS, 1);
}
return index;
}
C-state 전이 비용
C-state 전이에는 진입 비용(entry cost)과 복귀 비용(exit cost)이 있습니다. 실제 에너지 절약은 유휴 시간이 break-even point를 넘어야만 발생합니다:
에너지 절약 = (C0_power - Cn_power) * idle_duration - entry_cost - exit_cost
Break-even 조건: idle_duration > (entry_cost + exit_cost) / (C0_power - Cn_power)
예시: C6 state (Skylake)
진입 비용: ~50 uJ
복귀 비용: ~100 uJ
C0 전력: 4.0 W
C6 전력: 0.02 W
Break-even: (50 + 100) / (4.0 - 0.02) = ~37.7 us
→ 37.7 us 이상 유휴 시에만 C6 진입이 이득
C-state 전이의 숨겨진 비용
하드웨어 복귀 지연(exit_latency)만이 비용의 전부가 아닙니다. 깊은 C-state에서 복귀 시 추가 비용이 발생합니다:
- 캐시 콜드 스타트: C3+에서 L2 캐시 플러시 후 캐시 미스 급증 (수십~수백 us 추가)
- TLB 무효화: C6에서 코어 전원 차단 시 TLB 전체 플러시
- 분기 예측기 리셋: 코어 전원 차단 시 분기 예측 히스토리 소실
- 주파수 램프업: 깊은 C-state 복귀 시 P-state가 최저로 시작 → 부스트까지 수십 us
이러한 "소프트" 비용까지 합하면 실제 target_residency는 하드웨어 exit_latency의 2~5배에 달합니다.
아키텍처
CPUIdle은 Driver-Governor 분리 아키텍처를 채택합니다. Driver가 "어떤 C-state가 존재하며 어떻게 진입하는가"를 정의하고, Governor가 "지금 어떤 C-state에 진입할 것인가"를 결정합니다.
cpuidle_state 구조체
/* include/linux/cpuidle.h */
struct cpuidle_state {
char name[CPUIDLE_NAME_LEN]; /* "C1", "C6" 등 */
char desc[CPUIDLE_DESC_LEN]; /* 설명 문자열 */
s64 exit_latency_ns; /* 복귀 지연 (ns) */
s64 target_residency_ns; /* Break-even 시간 (ns) */
unsigned int exit_latency; /* 복귀 지연 (us, 호환) */
int power_usage; /* 전력 소비 (상대값, mW) */
unsigned int target_residency; /* Break-even 시간 (us) */
unsigned int flags;
/*
* 주요 플래그:
* CPUIDLE_FLAG_NONE - 특별한 속성 없음
* CPUIDLE_FLAG_POLLING - Polling 상태 (실제 C-state 아님)
* CPUIDLE_FLAG_COUPLED - 다중 CPU 결합 idle
* CPUIDLE_FLAG_TIMER_STOP - 로컬 타이머 정지됨
* CPUIDLE_FLAG_UNUSABLE - 이 상태 사용 불가
* CPUIDLE_FLAG_OFF - sysfs에서 비활성화됨
* CPUIDLE_FLAG_TLB_FLUSHED - TLB가 플러시됨
* CPUIDLE_FLAG_RCU_IDLE - RCU idle 상태 진입
*/
int (*enter)(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index);
int (*enter_dead)(struct cpuidle_device *dev,
int index);
int (*enter_s2idle)(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index);
};
cpuidle_driver 구조체
/* include/linux/cpuidle.h */
struct cpuidle_driver {
const char *name; /* "intel_idle", "acpi_idle" 등 */
struct module *owner;
/* C-state 배열과 개수 */
struct cpuidle_state states[CPUIDLE_STATE_MAX]; /* 최대 10개 */
int state_count;
/* 이 드라이버가 적용되는 CPU 마스크 */
struct cpumask *cpumask;
/* Governor에게 전달할 BM 감지 여부 */
unsigned int bctimer:1;
};
cpuidle_device 구조체
/* include/linux/cpuidle.h */
struct cpuidle_device {
unsigned int registered:1;
unsigned int enabled:1;
unsigned int poll_time_limit:1;
unsigned int cpu; /* CPU 번호 */
ktime_t next_hrtimer; /* 다음 hrtimer 이벤트 */
int last_state_idx; /* 마지막 선택된 C-state */
u64 last_residency_ns; /* 마지막 실제 수면 시간 */
/* per-state 사용 통계 */
struct cpuidle_state_usage states_usage[CPUIDLE_STATE_MAX];
};
target_residency (Break-even Time)
Break-even Time 계산
target_residency는 C-state 진입/복귀 비용을 회수하기 위한 최소 수면 시간입니다.
예시: C3 state
- 진입 비용: 10 us
- 복귀 비용: 20 us
- 유휴 전력: 5W, C3 전력: 0.5W (90% 절감)
- Break-even: (10 + 20) / 0.9 = 33 us
즉, 33us 이상 유휴 상태가 지속되어야 C3가 이득입니다. 이보다 짧은 유휴에서 C3에 진입하면 오히려 에너지를 낭비합니다.
드라이버 등록 흐름
/* intel_idle 드라이버 등록 예시 */
static struct cpuidle_driver intel_idle_driver = {
.name = "intel_idle",
.owner = THIS_MODULE,
};
static int __init intel_idle_init(void)
{
struct cpuidle_driver *drv = &intel_idle_driver;
int retval;
/* 1. CPU 모델 확인 및 C-state 테이블 선택 */
const struct idle_cpu *icpu = intel_idle_find_cpu();
if (!icpu)
return -ENODEV;
/* 2. C-state 테이블을 드라이버에 복사 */
for (cstate = 0; icpu->state_table[cstate].name; cstate++) {
drv->states[drv->state_count] = icpu->state_table[cstate];
drv->state_count++;
}
/* 3. 드라이버 등록 */
retval = cpuidle_register_driver(drv);
if (retval)
return retval;
/* 4. 각 CPU에 대해 디바이스 등록 */
cpuidle_register_device(&per_cpu(cpuidle_dev, cpu));
return 0;
}
module_init(intel_idle_init);
do_idle() 루프
do_idle()은 CPU가 유휴 상태일 때 반복 실행되는 핵심 루프입니다. 스케줄러의 idle 태스크(swapper 프로세스, PID 0)에서 호출됩니다.
/* kernel/sched/idle.c */
static void do_idle(void)
{
int cpu = smp_processor_id();
while (!need_resched()) {
rmb(); /* need_resched 읽기 배리어 */
local_irq_disable();
if (cpu_is_offline(cpu)) {
tick_nohz_idle_stop_tick();
cpuidle_play_dead(); /* enter_dead 콜백 */
}
arch_cpu_idle_enter();
/*
* 핵심: cpuidle_idle_call()이 governor와 driver를 호출
* 1. governor->select()로 C-state 선택
* 2. tick_nohz_idle_stop_tick()으로 nohz 진입
* 3. driver->states[index].enter()로 C-state 진입
* 4. 깨어난 후 governor->reflect()로 결과 피드백
*/
cpuidle_idle_call();
arch_cpu_idle_exit();
}
/* 스케줄 필요: 타이머, IRQ, 새 태스크 등 */
schedule_idle();
}
cpuidle_idle_call() 상세
/* drivers/cpuidle/cpuidle.c */
static void cpuidle_idle_call(void)
{
struct cpuidle_device *dev = cpuidle_get_device();
struct cpuidle_driver *drv = cpuidle_get_cpu_driver(dev);
int next_state, entered_state;
bool stop_tick = true;
/* 1. 드라이버/디바이스 검증 */
if (!dev || !dev->enabled || !drv || !drv->state_count) {
default_idle_call();
return;
}
/* 2. 강제 idle=poll 모드 확인 */
if (cpuidle_not_available(drv, dev)) {
default_idle_call();
return;
}
/* 3. Governor가 최적 C-state 선택 */
next_state = cpuidle_select(drv, dev, &stop_tick);
/* 4. nohz tick 중단 (필요 시) */
if (stop_tick)
tick_nohz_idle_stop_tick();
/* 5. RCU idle 알림 */
rcu_idle_enter();
/* 6. C-state 진입 */
entered_state = cpuidle_enter(drv, dev, next_state);
/* 7. RCU idle 복귀 */
rcu_idle_exit();
/* 8. nohz tick 재시작 */
tick_nohz_idle_restart_tick();
/* 9. Governor에 결과 피드백 */
cpuidle_reflect(dev, entered_state);
}
POLL 상태와 default_idle_call()
CPUIdle 드라이버의 states[0]은 보통 POLL 상태입니다. 이 상태는 HLT도 실행하지 않고 need_resched()를 빠르게 반복 확인합니다. 매우 짧은 유휴 시간(수 us 미만)에서 C-state 진입 비용 없이 즉시 응답할 수 있지만, CPU가 100% 전력을 소비합니다. default_idle_call()은 CPUIdle이 비활성화되었을 때의 폴백으로, 단순히 HLT를 실행합니다.
CPUIdle Governor
Governor는 어떤 C-state로 들어갈지 결정하는 정책 엔진입니다. 리눅스 커널은 4가지 governor를 제공하며, 런타임에 전환할 수 있습니다.
| Governor | 알고리즘 | 복잡도 | 적합한 경우 | 기본 선택 조건 |
|---|---|---|---|---|
menu | 예측 기반 (통계) | 중간 | 일반 시스템 (기본값) | NO_HZ_IDLE 활성 시 |
teo | Timer Events Oriented | 낮음 | 타이머 중심 워크로드 | NO_HZ_IDLE + 커널 옵션 |
ladder | 단계적 승격/강등 | 낮음 | Tickless 비활성화 시 | 고정 tick 시스템 |
haltpoll | Polling + HLT | 매우 낮음 | VM 게스트 (KVM) | VM 환경 감지 시 |
# 현재 governor 확인
cat /sys/devices/system/cpu/cpuidle/current_governor
# menu
# 사용 가능한 governor 목록
cat /sys/devices/system/cpu/cpuidle/available_governors
# menu teo ladder haltpoll
# governor 전환 (런타임)
echo teo > /sys/devices/system/cpu/cpuidle/current_governor
Menu Governor 동작
Menu governor는 가장 널리 사용되는 기본 governor입니다. 통계 기반 예측으로 다음 유휴 시간을 추정하고, 가장 깊은 적절한 C-state를 선택합니다.
/* drivers/cpuidle/governors/menu.c - menu_select() 핵심 로직 */
static int menu_select(struct cpuidle_driver *drv, struct cpuidle_device *dev,
bool *stop_tick)
{
struct menu_device *data = this_cpu_ptr(&menu_devices);
s64 latency_req = cpuidle_governor_latency_req(dev->cpu);
u64 predicted_ns;
u64 interactivity_req;
int i, idx;
/* 1. 다음 타이머 이벤트까지 남은 시간 */
u64 delta_next = tick_nohz_get_sleep_length(&dev->next_hrtimer);
/* 2. 이전 유휴 패턴 기반 예측 */
predicted_ns = get_typical_interval(data, delta_next);
/* 3. 상호작용성 보정 계수 적용 */
interactivity_req = predicted_ns / data->correction_factor[data->bucket];
/* 4. 가장 깊은 적절한 C-state 선택 */
idx = 0;
for (i = 1; i < drv->state_count; i++) {
struct cpuidle_state *s = &drv->states[i];
if (dev->states_usage[i].disable)
continue;
/* exit_latency가 PM QoS 요구를 초과하면 건너뜀 */
if (s->exit_latency_ns > latency_req)
break;
/* target_residency가 예측 시간을 초과하면 건너뜀 */
if (s->target_residency_ns > predicted_ns)
break;
idx = i;
}
/* 5. 매우 짧은 유휴면 tick 유지 */
if (idx == 0 && drv->states[0].flags & CPUIDLE_FLAG_POLLING)
*stop_tick = false;
return idx;
}
Menu Governor 예측 알고리즘
Menu governor는 최근 유휴 시간의 이동 평균과 분산을 추적하여 다음 유휴 시간을 예측합니다:
/* get_typical_interval: 최근 유휴 패턴 분석 */
static u64 get_typical_interval(struct menu_device *data, u64 expected_ns)
{
/*
* intervals[] 배열: 최근 INTERVALS(=8) 회의 유휴 시간 기록
*
* 알고리즘:
* 1. 최근 8회 유휴 시간에서 표준편차 계산
* 2. 표준편차의 1.5배를 초과하는 이상치 제거
* 3. 남은 값의 평균을 예측값으로 사용
* 4. 다음 타이머 이벤트 시간으로 클램핑
*/
u64 avg, stddev, max, thresh;
int i, divisor;
/* 평균 및 표준편차 계산 */
avg = 0;
for (i = 0; i < INTERVALS; i++)
avg += data->intervals[i];
avg >>= INTERVAL_SHIFT; /* /8 */
/* 이상치 제거 후 재계산 */
thresh = avg + (stddev << 1); /* avg + 2*stddev */
max = min(thresh, expected_ns);
return max;
}
Menu Governor의 보정 계수 (Correction Factor)
Menu governor는 워크로드 유형별로 4개의 보정 버킷을 유지합니다:
| 버킷 | 예측 유휴 시간 | 전형적 워크로드 |
|---|---|---|
| 0 | 1 ms 미만 | 고빈도 인터럽트, 네트워킹 |
| 1 | 1~10 ms | 데스크톱 상호작용 |
| 2 | 10~100 ms | 배치 처리, 컴파일 |
| 3 | 100 ms 이상 | 유휴 서버, 대기 |
각 버킷에 독립적인 보정 계수를 적용하여, 워크로드 변화에 빠르게 적응합니다.
TEO Governor (Timer Events Oriented)
TEO governor는 Linux 5.1에서 Rafael J. Wysocki가 도입한 governor입니다. Menu governor의 통계 기반 예측 대신, 직전 타이머 이벤트 분포를 분석하여 C-state를 선택합니다.
/* drivers/cpuidle/governors/teo.c - teo_select() 핵심 로직 */
static int teo_select(struct cpuidle_driver *drv,
struct cpuidle_device *dev,
bool *stop_tick)
{
struct teo_cpu *cpu_data = per_cpu_ptr(&teo_cpus, dev->cpu);
s64 latency_req = cpuidle_governor_latency_req(dev->cpu);
u64 duration_ns;
int i, idx;
duration_ns = tick_nohz_get_sleep_length(&dev->next_hrtimer);
/*
* TEO 핵심 아이디어:
* 각 C-state 구간별로 "일찍 깨어남(early wakeup)" 확률을 추적
*
* 만약 특정 C-state의 target_residency 이전에 자주 깨어난다면,
* 그 C-state는 비효율적이므로 한 단계 얕은 상태를 선택
*/
idx = 0;
for (i = 1; i < drv->state_count; i++) {
struct cpuidle_state *s = &drv->states[i];
struct teo_bin *bin = &cpu_data->state_bins[i];
if (dev->states_usage[i].disable)
continue;
if (s->exit_latency_ns > latency_req)
break;
if (s->target_residency_ns > duration_ns)
break;
/* early wakeup 비율이 높으면 건너뜀 */
if (bin->early_hits > bin->hits + bin->misses)
continue;
idx = i;
}
return idx;
}
| 비교 항목 | Menu Governor | TEO Governor |
|---|---|---|
| 예측 기반 | 최근 8회 유휴 시간 통계 | C-state 구간별 깨어남 분포 |
| 보정 계수 | 4개 버킷 보정 팩터 | 없음 (직접적 분포 추적) |
| 상호작용성 | iowait/IO 부스트 반영 | 반영하지 않음 |
| 복잡도 | O(states * intervals) | O(states) |
| 장점 | 범용적, 다양한 워크로드 적응 | 단순, 타이머 기반 워크로드에 정확 |
| 단점 | 급격한 패턴 변화에 느린 적응 | 비정형 워크로드에 부정확 |
Ladder Governor
Ladder governor는 가장 단순한 governor로, 단계적으로 깊은/얕은 C-state로 이동합니다. 고정 tick(HZ 기반) 시스템에서 사용됩니다:
/* drivers/cpuidle/governors/ladder.c */
static int ladder_select_state(struct cpuidle_driver *drv,
struct cpuidle_device *dev,
bool *stop_tick)
{
struct ladder_device *ldev = this_cpu_ptr(&ladder_devices);
struct ladder_device_state *last = &ldev->states[ldev->last_state_idx];
/*
* 승격 조건: 현재 상태에서 promotion_count 초과
* → 한 단계 더 깊은 C-state로 이동
*/
if (last->stats.below_count >= last->threshold.promotion_count) {
last->stats.below_count = 0;
if (ldev->last_state_idx < drv->state_count - 1)
ldev->last_state_idx++;
}
/*
* 강등 조건: 실제 수면 시간 < target_residency
* → 한 단계 얕은 C-state로 이동
*/
if (last->stats.above_count >= last->threshold.demotion_count) {
last->stats.above_count = 0;
if (ldev->last_state_idx > 0)
ldev->last_state_idx--;
}
return ldev->last_state_idx;
}
Haltpoll Governor (VM 게스트)
Haltpoll governor는 KVM 가상 머신 게스트에 특화된 governor입니다. VM 환경에서 HLT 실행은 VM Exit를 유발하여 호스트 커널로 전환되는 비용이 큽니다. Haltpoll은 먼저 polling으로 대기하다가, 일정 시간이 지나면 HLT를 실행합니다:
/* drivers/cpuidle/governors/haltpoll.c */
static int haltpoll_select(struct cpuidle_driver *drv,
struct cpuidle_device *dev,
bool *stop_tick)
{
s64 latency_req = cpuidle_governor_latency_req(dev->cpu);
/*
* 전략:
* 1. 짧은 유휴 → POLL (VM Exit 회피)
* 2. 긴 유휴 → HLT (CPU 양보)
*
* guest_halt_poll_ns: polling 상한 시간 (기본 200 us)
* 적응적: 이전 유휴가 짧았으면 polling 시간 증가,
* 길었으면 polling 시간 감소
*/
if (dev->poll_time_limit)
return 0; /* POLL */
return 1; /* HLT */
}
VM에서의 C-state 이슈
가상 머신 게스트에서 깊은 C-state를 사용하면 성능 문제가 발생할 수 있습니다:
- VM Exit 비용:
HLT/MWAIT실행 시 VM Exit → 호스트 커널 진입 → vCPU 디스케줄 → 수 us~수십 us 오버헤드 - 파라가상화: KVM의
kvm_steal_time으로 스틸 타임을 감지하여 governor가 보상 - 권장 설정: VM 게스트에서는
haltpollgovernor와poll_state/halt2-state 드라이버 사용
Intel C-states
Intel 프로세서는 intel_idle 드라이버(drivers/idle/intel_idle.c)를 통해 네이티브 MWAIT 기반 C-state를 제공합니다. 각 프로세서 세대마다 지원하는 C-state와 특성이 다릅니다.
Intel 프로세서별 C-states
| 프로세서 | 코드네임 | 지원 C-states | 최대 코어 C-state | 최대 패키지 C-state |
|---|---|---|---|---|
| Nehalem | NHM | C1, C3, C6 | C6 | C6 |
| Sandy Bridge | SNB | C1, C1E, C3, C6, C7 | C7 | C7 |
| Haswell | HSW | C1, C1E, C3, C6, C7, C8 | C8 | C8 |
| Skylake | SKL | C1, C1E, C3, C6, C7, C8, C10 | C8 | C10 |
| Alder Lake | ADL | C1, C1E, C6 (P-core) / C1, C1E, C6 (E-core) | C6 | C10 |
| Sapphire Rapids | SPR | C1, C1E, C6 | C6 | C6 |
intel_idle C-state 테이블 예시 (Skylake)
/* drivers/idle/intel_idle.c - Skylake 데스크톱 C-state 테이블 */
static struct cpuidle_state skl_cstates[] = {
{
.name = "C1",
.desc = "MWAIT 0x00",
.flags = MWAIT2flg(0x00),
.exit_latency = 2, /* 2 us */
.target_residency = 2, /* 2 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C1E",
.desc = "MWAIT 0x01",
.flags = MWAIT2flg(0x01) | CPUIDLE_FLAG_ALWAYS_ENABLE,
.exit_latency = 10, /* 10 us */
.target_residency = 20, /* 20 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C3",
.desc = "MWAIT 0x10",
.flags = MWAIT2flg(0x10) | CPUIDLE_FLAG_TLB_FLUSHED,
.exit_latency = 70, /* 70 us */
.target_residency = 211, /* 211 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C6",
.desc = "MWAIT 0x20",
.flags = MWAIT2flg(0x20) | CPUIDLE_FLAG_TLB_FLUSHED,
.exit_latency = 85, /* 85 us */
.target_residency = 325, /* 325 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C7s",
.desc = "MWAIT 0x33",
.flags = MWAIT2flg(0x33) | CPUIDLE_FLAG_TLB_FLUSHED,
.exit_latency = 124, /* 124 us */
.target_residency = 800, /* 800 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C8",
.desc = "MWAIT 0x40",
.flags = MWAIT2flg(0x40) | CPUIDLE_FLAG_TLB_FLUSHED,
.exit_latency = 200, /* 200 us */
.target_residency = 800, /* 800 us */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{
.name = "C10",
.desc = "MWAIT 0x60",
.flags = MWAIT2flg(0x60) | CPUIDLE_FLAG_TLB_FLUSHED,
.exit_latency = 890, /* 890 us */
.target_residency = 5000, /* 5000 us (5 ms) */
.enter = &intel_idle,
.enter_s2idle = intel_idle_s2idle,
},
{ .name = NULL }, /* 종료 표지 */
};
MWAIT 힌트 비트 형식
| 비트 | 필드 | 설명 |
|---|---|---|
| [3:0] | Sub C-state | 같은 C-state 내 세부 변종 (예: C7 vs C7s) |
| [7:4] | C-state | MWAIT C-state 번호 (0=C1, 1=C2, ...) |
| [31:8] | Reserved | 예약 |
# MWAIT 힌트 예시
# 0x00 = C-state 0 (C1), Sub-state 0
# 0x01 = C-state 0 (C1), Sub-state 1 (C1E)
# 0x10 = C-state 1 (C2/C3), Sub-state 0
# 0x20 = C-state 2 (C6), Sub-state 0
# 0x33 = C-state 3 (C7), Sub-state 3 (C7s)
# 0x40 = C-state 4 (C8), Sub-state 0
# 0x60 = C-state 6 (C10), Sub-state 0
# CPUID로 MWAIT 지원 확인
cpuid -l 5 # Leaf 5: MONITOR/MWAIT
# MONITOR/MWAIT:
# smallest monitor-line size (bytes) = 0x40 (64)
# largest monitor-line size (bytes) = 0x40 (64)
# number of C0 sub C-states using MWAIT = 0x0 (0)
# number of C1 sub C-states using MWAIT = 0x2 (2)
# number of C2 sub C-states using MWAIT = 0x1 (1)
# number of C3 sub C-states using MWAIT = 0x2 (2)
AMD C-states
AMD 프로세서는 주로 acpi_idle 드라이버를 통해 ACPI 기반 C-state를 제공합니다. AMD의 C-state 구현은 Intel과 몇 가지 중요한 차이가 있습니다.
| 프로세서 | 아키텍처 | 지원 C-states | 드라이버 | 특이사항 |
|---|---|---|---|---|
| Zen/Zen+ | Zen 1세대 | C1, C2(C1E), C6 | acpi_idle | MWAIT 미사용 |
| Zen 2 | Matisse/Rome | C1, C2, C6 | acpi_idle | BIOS에서 C-state 제어 |
| Zen 3 | Vermeer/Milan | C1, C2, C6 | acpi_idle | 향상된 C6 전력 관리 |
| Zen 4 | Raphael/Genoa | C1, C2, C6 | amd_idle | 네이티브 드라이버 도입 시작 |
AMD MWAIT 제한 사항
AMD 프로세서는 MWAIT를 지원하지만, 깊은 C-state에는 제한적입니다:
- MWAIT C1만 안정: AMD Zen 시리즈에서 MWAIT는 C1에만 안정적으로 동작
- C6 진입 경로: AMD C6는 MWAIT가 아닌 ACPI I/O 포트 또는 전용 레지스터 방식
- BIOS 의존성: AMD 시스템에서 C-state 깊이는 BIOS/UEFI 설정에 크게 의존
- 커널 파라미터:
amd_pstate=active또는amd_pstate=passive와 함께 사용
# AMD 시스템에서 C-state 확인
cat /sys/devices/system/cpu/cpuidle/current_driver
# acpi_idle
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name
# POLL
# C1
# C2
# AMD에서 C6 활성화 확인
dmesg | grep -i "cpuidle\|c-state\|acpi_idle"
# [ 0.391204] ACPI: processor limited to max C-state 2
# BIOS에서 C6 활성화 필요
Package C-states
Package C-state(PC-state)는 전체 CPU 패키지 수준의 전력 상태입니다. 패키지 내 모든 코어가 특정 깊이 이상의 코어 C-state에 진입해야만 패키지 C-state로 전이됩니다.
# 패키지 C-state 사용률 확인
turbostat --interval 1 --show Pkg%pc2,Pkg%pc3,Pkg%pc6,Pkg%pc7,Pkg%pc8,Pkg%pc10
# Pkg%pc2 Pkg%pc3 Pkg%pc6 Pkg%pc7 Pkg%pc8 Pkg%pc10
# 5.12 0.00 72.30 0.00 0.00 0.00
# MSR로 패키지 C-state residency 확인 (Intel)
rdmsr 0x3F8 # MSR_PKG_C2_RESIDENCY
rdmsr 0x3F9 # MSR_PKG_C3_RESIDENCY
rdmsr 0x3FA # MSR_PKG_C6_RESIDENCY
rdmsr 0x3FB # MSR_PKG_C7_RESIDENCY
rdmsr 0x630 # MSR_PKG_C8_RESIDENCY
rdmsr 0x631 # MSR_PKG_C9_RESIDENCY
rdmsr 0x632 # MSR_PKG_C10_RESIDENCY
| 패키지 C-state | 코어 조건 | Uncore 동작 | LLC | 메모리 컨트롤러 | 전력 절감 |
|---|---|---|---|---|---|
| PC0 | 하나 이상 C0 | 활성 | 활성 | 활성 | 기준 |
| PC2 | 모든 코어 C1+ | 클럭 감소 | 활성 | 활성 | ~20% |
| PC3 | 모든 코어 C3+ | 클럭 게이팅 | 유지 | 셀프리프레시 | ~50% |
| PC6 | 모든 코어 C6+ | 전원 차단 | 유지 | 셀프리프레시 | ~80% |
| PC8 | 모든 코어 C8+ | 전원 차단 | 플러시 | 셀프리프레시 | ~90% |
| PC10 | 모든 코어 C10 | 전원 차단 | 플러시 | 최소 전력 | ~99% |
s2idle (Suspend-to-Idle)
s2idle(Suspend-to-Idle)은 S0ix 저전력 유휴 상태로, 전통적인 S3 슬립 대신 소프트웨어 기반 절전을 구현합니다. CPUIdle의 가장 깊은 C-state를 활용하여 S3에 가까운 전력 절감을 달성합니다.
/* kernel/power/suspend.c - s2idle 진입 경로 */
static int suspend_enter(suspend_state_t state)
{
if (state == PM_SUSPEND_TO_IDLE) {
/*
* s2idle 경로:
* 1. 디바이스 suspend (IRQ 비활성화)
* 2. 각 CPU가 deepest C-state 진입
* 3. enter_s2idle() 콜백 사용
* 4. 웨이크업 이벤트 시 복귀
*/
cpuidle_enter_s2idle();
return 0;
}
/* S3 경로: ACPI/firmware 기반 */
...
}
| 비교 항목 | S3 (Suspend-to-RAM) | s2idle (S0ix) |
|---|---|---|
| 구현 | 펌웨어/ACPI 기반 | 소프트웨어(커널) 기반 |
| CPU 상태 | 전원 완전 차단 | 가장 깊은 C-state (C10) |
| 메모리 | 셀프리프레시 (내용 유지) | 셀프리프레시 (내용 유지) |
| 복귀 시간 | 1~3초 | 100~500 ms |
| 전력 소비 | 매우 낮음 (~0.5W) | 낮음 (~1~3W) |
| 웨이크업 소스 | PME, 전원 버튼, RTC | 모든 인터럽트 (IRQ 기반) |
| 커널 파라미터 | mem_sleep_default=deep | mem_sleep_default=s2idle |
# s2idle 지원 확인
cat /sys/power/mem_sleep
# s2idle [deep] ← deep이 기본이면 s2idle은 지원만 되는 상태
# [s2idle] deep ← s2idle이 기본 선택
# s2idle을 기본으로 변경
echo s2idle > /sys/power/mem_sleep
# s2idle 진입
echo mem > /sys/power/state
# s2idle 디버깅
echo 1 > /sys/power/pm_debug_messages
dmesg | grep -i "s2idle\|suspend\|freeze"
s2idle 전력 최적화
s2idle에서 최적의 전력 절감을 달성하려면:
- PC10 도달: 모든 코어가 C10에 진입해야 PC10 패키지 C-state 달성
- 디바이스 D3: PCIe 디바이스가 D3 상태로 전환되어야 패키지 C-state 허용
- ASPM L1.2: PCIe Active State Power Management이 L1.2까지 허용
- USB: USB 디바이스가 autosuspend되어야 함
지연시간 vs 전력 트레이드오프
깊은 C-state는 전력은 절약하지만 응답성을 희생합니다. 이 트레이드오프를 이해하는 것이 CPUIdle 튜닝의 핵심입니다.
워크로드별 권장 설정
| 워크로드 | 최대 C-state | latency_req | 이유 | 절전 효과 |
|---|---|---|---|---|
| 데스크톱 | C7+ | 제한 없음 | 절전 우선, 응답 지연 허용 | 매우 높음 |
| 웹 서버 | C1E | 10 us | tail latency 제어 필요 | 중간 |
| 데이터베이스 | C1E~C3 | 50 us | 쿼리 지연 변동 최소화 | 중간 |
| HPC | C1 | 1 us | 최대 처리량, 균일한 지연 | 낮음 |
| 실시간 | C0 (비활성화) | 0 us | 결정적 지연 필요 | 없음 |
| 배치 처리 | C8+ | 제한 없음 | 전력 효율 최우선 | 매우 높음 |
| VM 호스트 | C1E | 20 us | VM Exit 오버헤드 고려 | 낮음~중간 |
Deep C-state 문제
C6 이상의 깊은 C-state는 다음 문제를 일으킬 수 있습니다:
- 지연 시간 증가: 100us+ → 고빈도 트레이딩, 실시간 시스템에 부적합
- TSC (Time Stamp Counter) 정지: C3+ (일부 CPU) → 타이밍 측정 오류
- PCIe ASPM 문제: 일부 하드웨어와 호환 문제
- 성능 변동: Wake-up 지연으로 일시적 성능 저하 (tail latency 증가)
- 캐시 트래싱: C3+에서 캐시 flush 후 캐시 워밍업 비용
고성능 서버는 보통 C1E 이하로 제한합니다.
PM QoS (Power Management Quality of Service)
PM QoS는 커널 드라이버와 사용자 공간 프로그램이 최대 허용 CPU 복귀 지연 시간을 설정하여 깊은 C-state 진입을 제한하는 프레임워크입니다. CPUIdle governor는 PM QoS 제약을 반드시 준수합니다.
사용자 공간 PM QoS
/* 사용자 공간에서 PM QoS 설정 (/dev/cpu_dma_latency) */
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
int main(void)
{
/* CPU DMA 지연시간을 10us로 제한 → C1E 이하만 허용 */
int fd = open("/dev/cpu_dma_latency", O_WRONLY);
if (fd < 0) {
perror("open");
return 1;
}
int32_t latency_us = 10; /* 10 us */
write(fd, &latency_us, sizeof(latency_us));
/*
* fd를 열어둔 동안 지연시간 제한 유지
* close(fd) 호출 시 제한 해제
*
* 주의: fd가 닫히면 즉시 해제되므로,
* 백그라운드에서 유지하려면 프로세스가 살아있어야 함
*/
/* 작업 수행... */
sleep(3600);
close(fd); /* 제한 해제 */
return 0;
}
커널 드라이버 PM QoS
/* 커널 드라이버에서 CPU 지연 제약 설정 */
#include <linux/pm_qos.h>
static struct pm_qos_request my_qos_req;
static int my_driver_probe(struct platform_device *pdev)
{
/* DMA 전송 중 깊은 C-state 방지 */
cpu_latency_qos_add_request(&my_qos_req, 50); /* 50 us */
return 0;
}
static void my_dma_start(void)
{
/* DMA 시작 시 제약 강화 */
cpu_latency_qos_update_request(&my_qos_req, 0); /* 0 us: POLL만 */
}
static void my_dma_complete(void)
{
/* DMA 완료 후 제약 완화 */
cpu_latency_qos_update_request(&my_qos_req, PM_QOS_DEFAULT_VALUE);
}
static int my_driver_remove(struct platform_device *pdev)
{
cpu_latency_qos_remove_request(&my_qos_req);
return 0;
}
Tickless Kernel (NOHZ)
Tickless Kernel은 유휴 시 타이머 인터럽트를 중단하여 깊은 C-state를 허용합니다. CPUIdle과 밀접하게 연동되며, C-state 효율에 직접적인 영향을 미칩니다.
동적 틱 (Dynamic Tick)
/* 기존 (HZ=1000): 1ms마다 타이머 인터럽트 발생 */
/* → C-state가 1ms 이상 유지 불가 */
/* Tickless: 다음 이벤트까지 타이머 중단 */
/* → 수백 ms 동안 깊은 C-state 유지 가능 */
# 커널 설정 확인
zcat /proc/config.gz | grep NO_HZ
# CONFIG_NO_HZ_IDLE=y (유휴 CPU만, 기본 권장)
# CONFIG_NO_HZ_FULL=y (모든 CPU, 실시간/HPC용)
Tickless 효과
| 항목 | HZ=1000 (고정 틱) | NO_HZ_IDLE (Tickless) |
|---|---|---|
| 유휴 타이머 인터럽트 | 1000/sec | 거의 없음 |
| 최대 C-state 지속 | 1ms | 제한 없음 |
| 전력 절감 | 중간 | 높음 |
| 노트북 배터리 | 4시간 | 6시간+ (50% 향상) |
| 패키지 C-state | 도달 어려움 | PC6/PC10 도달 용이 |
| Governor 정확도 | 1ms 제한 | 실제 이벤트 기반 정확 |
NOHZ와 CPUIdle 상호작용
/* CPUIdle이 tick을 제어하는 핵심 코드 */
static void cpuidle_idle_call(void)
{
bool stop_tick = true;
/* Governor가 stop_tick 결정 */
next_state = cpuidle_select(drv, dev, &stop_tick);
/*
* stop_tick = true → 깊은 C-state (C3+)
* tick 중단 → 다음 타이머까지 연속 수면
*
* stop_tick = false → 얕은 C-state (POLL/C1)
* tick 유지 → 1ms마다 깨어남 (스케줄러 체크)
*
* 판단 기준: 예측 유휴 시간 vs tick 주기
* 예측 < 2*tick_period → tick 유지 (stop_tick=false)
* 예측 > 2*tick_period → tick 중단 (stop_tick=true)
*/
if (stop_tick || tick_nohz_tick_stopped())
tick_nohz_idle_stop_tick();
else
tick_nohz_idle_retain_tick();
/* C-state 진입 */
entered_state = cpuidle_enter(drv, dev, next_state);
/* tick 재시작 */
tick_nohz_idle_restart_tick();
}
sysfs 인터페이스
CPUIdle 서브시스템은 /sys/devices/system/cpu/ 아래에 풍부한 sysfs 인터페이스를 제공합니다.
전역 설정
# 현재/사용가능 governor
cat /sys/devices/system/cpu/cpuidle/current_governor
cat /sys/devices/system/cpu/cpuidle/available_governors
# 현재/사용가능 드라이버
cat /sys/devices/system/cpu/cpuidle/current_driver
# intel_idle
per-CPU C-state 정보
# CPU별 C-state 정보 경로
# /sys/devices/system/cpu/cpu/cpuidle/state/
# C-state 이름과 설명
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name
# POLL
# C1
# C1E
# C6
# C7s
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/desc
# CPUIDLE CORE POLL IDLE
# MWAIT 0x00
# MWAIT 0x01
# MWAIT 0x20
# MWAIT 0x33
# exit_latency (us)
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/latency
# 0 (POLL)
# 2 (C1)
# 10 (C1E)
# 85 (C6)
# 124 (C7s)
# target_residency (us)
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/residency
# 0 (POLL)
# 2 (C1)
# 20 (C1E)
# 325 (C6)
# 800 (C7s)
# 사용 시간 (us) 및 진입 횟수
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/time
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage
# 거부 횟수 (exit_latency > latency_req)
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/rejected
# C-state 비활성화/활성화
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state3/disable # 비활성화
echo 0 > /sys/devices/system/cpu/cpu0/cpuidle/state3/disable # 활성화
| sysfs 파일 | 읽기/쓰기 | 설명 |
|---|---|---|
name | R | C-state 이름 (C1, C6, ...) |
desc | R | 설명 (MWAIT 힌트 등) |
latency | R | exit_latency (us) |
residency | R | target_residency (us) |
power | R | 전력 소비 (상대값) |
time | R | 누적 사용 시간 (us) |
usage | R | 누적 진입 횟수 |
rejected | R | 거부된 횟수 |
disable | R/W | 0: 활성, 1: 비활성화 |
above | R | 이 상태보다 얕게 유지된 횟수 |
below | R | 이 상태보다 깊게 유지된 횟수 |
s2idle/time | R | s2idle 모드 누적 시간 |
s2idle/usage | R | s2idle 모드 진입 횟수 |
모니터링
C-state 사용 현황을 확인하는 다양한 도구를 활용할 수 있습니다.
turbostat - 실시간 C-state 모니터링
turbostat --interval 1
# Core CPU Avg_MHz Busy% Bzy_MHz C1% C1E% C6% C7s%
# 0 0 800 20.0 4000 10.0 20.0 40.0 10.0
# 0 4 750 18.0 4166 12.0 25.0 35.0 9.0
# 패키지 C-state도 확인 가능
# Pkg%pc2 Pkg%pc3 Pkg%pc6 Pkg%pc7 Pkg%pc8 Pkg%pc10
# 5.0 10.0 20.0 5.0 2.0 0.5
# 특정 컬럼만 표시
turbostat --show Core,CPU,C1%,C1E%,C6%,Pkg%pc6 --interval 2
powertop - 전력 분석
powertop
# C-state 사용률 표시
# C0 (active): 5.0%
# C1: 10.0%
# C1E: 15.0%
# C6: 60.0%
# C7s: 10.0%
# CSV 보고서 생성
powertop --csv=powertop_report.csv --time=60
ftrace로 CPUIdle 추적
# CPUIdle 이벤트 추적 활성화
echo 1 > /sys/kernel/debug/tracing/events/power/cpu_idle/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예시:
# <idle>-0 [000] 123.456789: cpu_idle: state=4 cpu_id=0
# <idle>-0 [000] 123.457234: cpu_idle: state=4294967295 cpu_id=0 ← 복귀
# perf로 C-state 이벤트 기록
perf record -e power:cpu_idle -a -- sleep 10
perf script
# BPF로 C-state 지연 히스토그램
bpftrace -e '
tracepoint:power:cpu_idle /args->state < 100/ {
@start[cpu] = nsecs;
}
tracepoint:power:cpu_idle /args->state == 4294967295/ {
if (@start[cpu]) {
@us = hist((nsecs - @start[cpu]) / 1000);
delete(@start[cpu]);
}
}'
cpupower idle-info
cpupower idle-info
# CPUidle driver: intel_idle
# CPUidle governor: menu
# analyzing CPU 0:
#
# Number of idle states: 5
# Available idle states: POLL C1 C1E C6 C7s
# POLL:
# Flags/Description: CPUIDLE CORE POLL IDLE
# Latency: 0
# Usage: 12345
# Duration: 67890
# C1:
# Flags/Description: MWAIT 0x00
# Latency: 2
# Usage: 234567
# Duration: 890123
# ...
# 모든 CPU의 C-state 사용률 요약
cpupower idle-set -D 3 # state3(C6) 이상 비활성화
cpupower idle-set -E 3 # state3(C6) 이상 재활성화
성능 튜닝
C-state를 조정하여 응답성 또는 절전을 최적화합니다. 튜닝 방법은 크게 세 가지 수준으로 나뉩니다.
커널 부팅 파라미터
| 파라미터 | 값 | 효과 |
|---|---|---|
intel_idle.max_cstate=N | 0~9 | intel_idle 드라이버의 최대 C-state 제한 |
processor.max_cstate=N | 0~9 | ACPI 프로세서 드라이버의 최대 C-state 제한 |
idle=poll | - | 모든 C-state 비활성화, busy loop만 사용 |
idle=halt | - | HLT만 사용 (C1) |
idle=nomwait | - | MWAIT 사용 금지, ACPI 폴백 |
cpuidle.governor=NAME | 문자열 | 부팅 시 governor 선택 |
nohz=off | - | NO_HZ 비활성화 (고정 tick) |
# GRUB에서 C-state 제한 설정
# /etc/default/grub
GRUB_CMDLINE_LINUX="intel_idle.max_cstate=1" # C1까지만 허용
update-grub
# 또는 동시에 ACPI 드라이버도 제한
GRUB_CMDLINE_LINUX="intel_idle.max_cstate=1 processor.max_cstate=1"
런타임 C-state 비활성화
# 특정 C-state 비활성화 (예: C6, state index 3)
echo 1 > /sys/devices/system/cpu/cpu*/cpuidle/state3/disable
# C1E 이상 모두 비활성화 (고성능 모드)
for cpu in /sys/devices/system/cpu/cpu*/cpuidle; do
for state in $cpu/state*; do
name=$(cat $state/name)
if [ "$name" != "POLL" ] && [ "$name" != "C1" ]; then
echo 1 > $state/disable
fi
done
done
# cpupower로 일괄 비활성화
cpupower idle-set -d 3 # state index 3 이상 비활성화
# 전체 C-state 활성화 복원
for state in /sys/devices/system/cpu/cpu*/cpuidle/state*/disable; do
echo 0 > $state
done
PM QoS 기반 튜닝
# 저지연 웹 서버: C-state를 C1E 이하로 제한
# 방법 1: pm-qa 도구
echo 10 > /dev/cpu_dma_latency # fd를 열어두어야 유효
# 방법 2: 간단한 데몬 스크립트
exec 3> /dev/cpu_dma_latency
echo -ne '\x0a\x00\x00\x00' >&3 # 10 us (리틀엔디안)
# fd 3이 열린 동안 유효
# exec 3>&- 로 해제
# 방법 3: tuned 프로파일 사용 (RHEL/CentOS)
tuned-adm profile latency-performance
# 이 프로파일은 자동으로 cpu_dma_latency=1 설정
서버 시나리오별 최적화 예제
# === 시나리오 1: 저지연 트레이딩 서버 ===
# 목표: 최소 지연, 전력 비고려
echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
GRUB_CMDLINE_LINUX="idle=poll isolcpus=2-7 nohz_full=2-7"
# idle=poll: 모든 C-state 비활성화
# isolcpus: 전용 코어 격리
# nohz_full: 격리 코어에서 tick 완전 제거
# === 시나리오 2: 웹 서버 (nginx/Apache) ===
# 목표: tail latency 억제, 적절한 절전
intel_idle.max_cstate=2 # C1E까지
echo 10 > /dev/cpu_dma_latency # 10 us PM QoS
# === 시나리오 3: 배치/ML 학습 서버 ===
# 목표: GPU 대기 시 CPU 전력 절약
# C-state 제한 없음 (기본값 유지)
echo teo > /sys/devices/system/cpu/cpuidle/current_governor
# === 시나리오 4: 데이터센터 공유 호스트 (KVM) ===
# 목표: VM density 최대화, 적절한 응답성
intel_idle.max_cstate=2 # 호스트: C1E까지
# 게스트: haltpoll governor 자동 선택
idle=poll 경고
idle=poll은 모든 유휴 CPU가 100% 전력을 소비하며, CPU 온도가 급등하여 써멀 스로틀링을 유발할 수 있습니다. 대부분의 저지연 워크로드에서는 intel_idle.max_cstate=1이 idle=poll보다 나은 선택입니다. C1의 복귀 지연은 2us에 불과하며, polling 대비 90%+ 전력을 절약합니다.
커널 설정 (Kconfig)
CPUIdle 관련 주요 커널 설정 옵션입니다.
# CPU_IDLE: CPUIdle 서브시스템 활성화
CONFIG_CPU_IDLE=y
# Governor 선택
CONFIG_CPU_IDLE_GOV_LADDER=y
CONFIG_CPU_IDLE_GOV_MENU=y
CONFIG_CPU_IDLE_GOV_TEO=y
CONFIG_CPU_IDLE_GOV_HALTPOLL=y
# 기본 Governor
# (NO_HZ_IDLE=y면 menu, 아니면 ladder)
CONFIG_CPU_IDLE_GOV_MENU=y # 기본값
# Intel idle 드라이버
CONFIG_INTEL_IDLE=y
# ACPI 프로세서 idle
CONFIG_ACPI_PROCESSOR_IDLE=y
# Tickless 관련
CONFIG_NO_HZ_IDLE=y # 유휴 CPU만 tickless (권장)
CONFIG_NO_HZ_FULL=y # 모든 CPU tickless (실시간/HPC)
CONFIG_HZ_1000=y # 기본 tick 빈도
# PM QoS
CONFIG_PM_QOS=y
CONFIG_CPU_FREQ=y # CPUFreq (P-states와 연동)
| 설정 | 의존성 | 설명 |
|---|---|---|
CPU_IDLE | 없음 | CPUIdle 프레임워크 전체 활성화 |
INTEL_IDLE | X86, CPU_IDLE | Intel MWAIT 기반 드라이버 |
CPU_IDLE_GOV_MENU | CPU_IDLE | Menu governor (일반 시스템 기본) |
CPU_IDLE_GOV_TEO | CPU_IDLE | TEO governor (타이머 기반 워크로드) |
CPU_IDLE_GOV_LADDER | CPU_IDLE | Ladder governor (고정 tick 시스템) |
CPU_IDLE_GOV_HALTPOLL | CPU_IDLE, KVM_GUEST | Haltpoll governor (VM 게스트) |
NO_HZ_IDLE | 없음 | 유휴 시 tick 중단 (C-state 효율 향상) |
커널 트레이싱과 디버깅
CPUIdle 문제를 진단하기 위한 고급 디버깅 기법입니다.
CPUIdle Tracepoints
# 사용 가능한 CPUIdle tracepoint 목록
ls /sys/kernel/debug/tracing/events/power/
# cpu_idle ← C-state 진입/복귀
# cpu_frequency ← P-state 변경
# pm_qos_update ← PM QoS 값 변경
# wakeup_source_activate ← 웨이크업 소스
# wakeup_source_deactivate
# cpu_idle 트레이스포인트 활성화
echo 1 > /sys/kernel/debug/tracing/events/power/cpu_idle/enable
# PM QoS 변경 추적
echo 1 > /sys/kernel/debug/tracing/events/power/pm_qos_update/enable
# 트레이스 읽기
cat /sys/kernel/debug/tracing/trace
# <idle>-0 [000] d.. 12345.678901: cpu_idle: state=3 cpu_id=0
# <idle>-0 [000] d.. 12345.679123: cpu_idle: state=4294967295 cpu_id=0
# state=4294967295 (0xFFFFFFFF) = PWR_EVENT_EXIT = 복귀
BPF 기반 C-state 분석
# C-state 체류 시간 분포 (히스토그램)
bpftrace -e '
tracepoint:power:cpu_idle
{
if (args->state < 10) {
@entry[cpu, args->state] = nsecs;
} else {
/* 복귀 이벤트 (state == PWR_EVENT_EXIT) */
$key = cpu;
if (@entry[$key, 0]) {
@residency_ns = hist(nsecs - @entry[$key, 0]);
delete(@entry[$key, 0]);
}
}
}'
# 어떤 인터럽트가 C-state를 깨우는지 분석
bpftrace -e '
tracepoint:irq:irq_handler_entry
{
@wakeup_irq[args->name] = count();
}'
일반적인 문제와 해결
# 문제 1: C-state가 작동하지 않음
# 1. BIOS에서 C-states 활성화 확인
# 2. intel_idle 드라이버 확인
lsmod | grep intel_idle
cat /sys/devices/system/cpu/cpuidle/current_driver
# 3. C-state 목록 확인
cpupower idle-info
# 4. 커널 파라미터 확인
cat /proc/cmdline | grep idle
# idle=poll이나 intel_idle.max_cstate=0이 없어야 함
# 5. PM QoS 확인
cat /sys/kernel/debug/pm_qos/cpu_dma_latency/requested_value
# 문제 2: 성능 저하 (과도한 C-state)
# 증상: tail latency 급증, 간헐적 응답 지연
# 원인: 깊은 C-state 복귀 지연
# 진단: C-state 사용률 확인
turbostat --interval 1 --show Core,CPU,Avg_MHz,Busy%,C1%,C1E%,C6%
# C6%가 높으면 깊은 C-state가 자주 사용되는 것
# 해결: exit_latency 100us 초과 C-state 비활성화
for state in /sys/devices/system/cpu/cpu*/cpuidle/state*; do
latency=$(cat $state/latency)
if [ $latency -gt 100 ]; then
echo 1 > $state/disable
echo "Disabled $state (latency=${latency}us)"
fi
done
# 문제 3: 패키지 C-state에 도달하지 못함
# 진단: turbostat으로 Pkg%pc6 확인
turbostat --interval 5 --show Pkg%pc2,Pkg%pc6,Pkg%pc10
# Pkg%pc6 = 0%이면 패키지 C-state 미진입
# 원인 찾기:
# 1. 하나의 코어라도 깊은 C-state에 있지 않으면 PC6 불가
turbostat --interval 1 --show Core,CPU,C6% # 모든 코어 C6% 확인
# 2. PCIe 디바이스가 ASPM L1 미진입
lspci -vvv | grep -i "LnkCtl:\|ASPM"
# 3. USB 디바이스 autosuspend 미설정
cat /sys/bus/usb/devices/*/power/control
# "auto"가 아닌 "on"이 있으면 패키지 C-state 방해
디버깅 체크리스트
cpupower idle-info로 드라이버, governor, C-state 목록 확인turbostat으로 실시간 C-state 분포 확인/proc/cmdline으로 부팅 파라미터 확인/dev/cpu_dma_latencyPM QoS 확인dmesg | grep -i "cpuidle\|intel_idle\|acpi_idle"로 초기화 로그 확인- ftrace
power:cpu_idle트레이스포인트로 진입/복귀 패턴 분석
CPUIdle과 다른 서브시스템 연계
CPUIdle은 독립적으로 동작하지 않고, 커널의 여러 서브시스템과 밀접하게 상호작용합니다.
Scheduler와의 연계
/* 스케줄러의 idle 태스크 (PID 0) */
static void cpu_idle_loop(void)
{
while (1) {
/*
* 스케줄러 → CPUIdle 호출 경로:
* schedule() → pick_next_task() → idle 태스크 선택
* → cpu_idle_loop() → do_idle() → cpuidle_idle_call()
*
* 깨어남 후:
* cpuidle_idle_call() 복귀 → schedule_idle()
* → pick_next_task()로 새 태스크 선택
*/
tick_nohz_idle_enter();
while (!need_resched()) {
local_irq_disable();
cpuidle_idle_call();
local_irq_enable();
}
tick_nohz_idle_exit();
schedule_idle();
}
}
CPUFreq와의 연계
CPUIdle(C-states)과 CPUFreq(P-states)는 독립적이지만 상호 보완적입니다:
- 유휴 시: CPUIdle이 C-state로 전력 절약 (CPUFreq 비활성)
- 활성 시: CPUFreq가 주파수/전압 조절 (C0 상태)
- 연계: 깊은 C-state(C6)에서 복귀 시 P-state가 P0로 즉시 복원되지 않아 일시적 성능 저하 발생 가능
- EPP (Energy Performance Preference): HWP(Hardware P-state) 사용 시 EPP 값이 C-state 선택에도 간접 영향
RCU와의 연계
/*
* CPUIdle과 RCU의 상호작용:
*
* C-state 진입 전 rcu_idle_enter() 호출
* → RCU에게 "이 CPU는 유휴 상태"임을 알림
* → RCU grace period 종료 판단에 사용
*
* 모든 CPU가 유휴(rcu_idle) 상태이면
* → RCU grace period가 빠르게 완료
* → 메모리 해제, 콜백 실행 가속
*/
/* cpuidle_enter() 내부 */
rcu_idle_enter();
entered_state = target_state->enter(dev, drv, index);
rcu_idle_exit();
RCU nocb와 CPUIdle
rcu_nocbs=CPU_LIST 커널 파라미터를 사용하면 RCU 콜백 처리를 전용 커널 스레드로 오프로드하여, 격리된 CPU가 더 깊은 C-state에 오래 머물 수 있습니다. 실시간 시스템과 HPC 환경에서 유용합니다.
문제 해결
일반적인 C-state 문제와 체계적인 해결 방법입니다.
C-state가 작동하지 않음
# 단계별 진단
# 1. BIOS에서 C-states 활성화 확인
# → BIOS Setup > Advanced > CPU Configuration > C-States: Enabled
# 2. 현재 드라이버 확인
cat /sys/devices/system/cpu/cpuidle/current_driver
# "none"이면 CPUIdle 비활성화 상태
# 3. intel_idle 모듈 로드 확인
lsmod | grep intel_idle
dmesg | grep intel_idle
# 4. C-state 목록 확인
cpupower idle-info
# 5. 커널 파라미터 확인 (idle=poll 등이 없어야 함)
cat /proc/cmdline | grep idle
# 6. PM QoS 확인 (0이면 POLL만 허용)
cat /sys/devices/system/cpu/cpu0/power/pm_qos_resume_latency_us
성능 저하 (과도한 C-state)
# 증상: p99 latency 급증, 간헐적 지연
# 1. C-state 분포 확인
turbostat --interval 2 --show Core,CPU,Busy%,C1%,C1E%,C6%,C7s%
# 2. C6%가 높으면 → C6 비활성화
for state in /sys/devices/system/cpu/cpu*/cpuidle/state*; do
latency=$(cat $state/latency)
if [ $latency -gt 100 ]; then
echo 1 > $state/disable
fi
done
# 3. PM QoS로 미세 조정
exec 3> /dev/cpu_dma_latency
echo -ne '\x32\x00\x00\x00' >&3 # 50 us
# 4. 효과 확인
turbostat --interval 2 --show Core,CPU,Busy%,C1%,C1E%
전력 소비 과다
# 증상: 노트북 배터리 수명 짧음, 패키지 C-state 미진입
# 1. 패키지 C-state 확인
turbostat --interval 5 --show Pkg%pc2,Pkg%pc6
# Pkg%pc6 = 0%이면 문제
# 2. 어떤 코어가 깊은 C-state에 들어가지 못하는지 확인
turbostat --interval 1 --show Core,CPU,C6%
# C6% = 0인 코어 찾기
# 3. 깨우는 원인 분석
perf record -e power:cpu_idle,irq:irq_handler_entry -a -- sleep 10
perf script | head -50
# 4. 주요 방해 요소 확인
powertop # "Tunables" 탭에서 "Bad" 항목 확인
# 5. PCIe ASPM 활성화
echo powersave > /sys/module/pcie_aspm/parameters/policy
# 6. USB autosuspend 활성화
for dev in /sys/bus/usb/devices/*/power/control; do
echo auto > $dev
done
ARM/RISC-V C-states
ARM과 RISC-V 아키텍처도 CPUIdle 프레임워크를 사용하지만, x86과는 진입 메커니즘과 C-state 구조가 다릅니다.
ARM: WFI/WFE 기반 idle
ARM 프로세서는 WFI(Wait For Interrupt)와 WFE(Wait For Event) 명령어를 사용합니다:
/* arch/arm64/kernel/cpuidle.c */
static int arm_cpuidle_simple_enter(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
/*
* ARM WFI (Wait For Interrupt):
* CPU를 저전력 대기 상태로 전환
* 인터럽트 발생 시 즉시 복귀
* x86의 HLT와 유사
*/
cpu_do_idle(); /* WFI 명령어 실행 */
return index;
}
| ARM C-state | 명칭 | 메커니즘 | exit_latency | 설명 |
|---|---|---|---|---|
| WFI | Clock gating | WFI | 1~5 us | 코어 클럭 게이팅, 캐시 유지 |
| Core retention | Retention | PSCI | 10~50 us | 코어 전원 감소, 상태 보존 |
| Core power down | Power off | PSCI | 100~500 us | 코어 전원 차단, 캐시 플러시 |
| Cluster power down | Cluster off | PSCI | 500~1000 us | 클러스터 전체 전원 차단 |
| System idle | S2idle | PSCI | 1~10 ms | 시스템 전체 저전력 |
ARM PSCI (Power State Coordination Interface)
/* ARM PSCI를 통한 깊은 C-state 진입 */
/* drivers/cpuidle/cpuidle-psci.c */
static int psci_enter_domain_idle_state(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
u32 state;
/* DT에서 정의된 idle-state에 대응하는 PSCI 파워 상태 */
state = state_nodes[index]->psci_state;
/*
* PSCI CPU_SUSPEND 호출:
* → Secure Monitor (EL3)로 전환
* → ATF(ARM Trusted Firmware)가 실제 전원 관리
* → 코어 전원 차단 / 클러스터 전원 차단
*/
return psci_cpu_suspend_enter(state);
}
/* Device Tree에서 ARM idle states 정의 */
/* arch/arm64/boot/dts/vendor/board.dtsi */
idle-states {
entry-method = "psci";
CPU_SLEEP_0: cpu-sleep-0 {
compatible = "arm,idle-state";
arm,psci-suspend-param = <0x0010000>;
local-timer-stop;
entry-latency-us = <40>;
exit-latency-us = <100>;
min-residency-us = <150>;
};
CLUSTER_SLEEP_0: cluster-sleep-0 {
compatible = "arm,idle-state";
arm,psci-suspend-param = <0x1010000>;
local-timer-stop;
entry-latency-us = <500>;
exit-latency-us = <1000>;
min-residency-us = <2500>;
};
};
ARM big.LITTLE과 CPUIdle
ARM big.LITTLE 아키텍처(예: Cortex-A76 + A55)에서는 각 코어 클러스터마다 별도의 idle state가 정의됩니다. big 코어는 성능 우선으로 얕은 C-state를 선호하고, LITTLE 코어는 에너지 효율을 위해 깊은 C-state를 적극 사용합니다. 이는 Intel Alder Lake의 P-core/E-core 구조와 유사한 개념입니다.
RISC-V idle
/* RISC-V idle 진입: WFI 명령어 사용 */
/* arch/riscv/kernel/cpuidle.c */
static int riscv_cpuidle_enter(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
if (index == 0) {
/* WFI: 인터럽트 대기 */
cpu_do_idle(); /* asm volatile ("wfi") */
} else {
/* SBI (Supervisor Binary Interface)를 통한 깊은 idle */
sbi_cpu_suspend(state);
}
return index;
}
| 비교 | x86 (MWAIT) | ARM (PSCI) | RISC-V (SBI) |
|---|---|---|---|
| C1 진입 | MWAIT 0x00 | WFI | wfi |
| 깊은 idle | MWAIT 힌트 비트 | PSCI CPU_SUSPEND | SBI cpu_suspend |
| C-state 정의 | 커널 C 코드 테이블 | Device Tree | Device Tree / 코드 |
| 펌웨어 역할 | 없음 (네이티브) | ATF (EL3) | SBI 구현체 (M-mode) |
| 패키지 idle | 자동 (HW) | OS-initiated (PSCI) | 플랫폼 의존 |
벤치마크와 측정
CPUIdle 설정의 효과를 정량적으로 측정하는 방법입니다.
C-state 복귀 지연 측정
# 방법 1: idle_latency 벤치마크 (커널 도구)
# IPI(Inter-Processor Interrupt)로 idle CPU를 깨우고 응답 시간 측정
# 방법 2: cyclictest로 wakeup latency 측정
cyclictest --mlockall --priority=99 --interval=1000 \
--loops=100000 --histogram=400 --histfile=hist.txt
# C-state 설정에 따른 max latency 비교:
# idle=poll: Max: 3 us
# max_cstate=1 (C1): Max: 8 us
# max_cstate=2 (C1E): Max: 25 us
# 기본값 (C6 허용): Max: 180 us
# 기본값 (C10 허용): Max: 950 us
# 방법 3: schbench로 tail latency 측정
schbench -m 4 -t 16 -r 30
# 50th percentile: 5 us
# 75th percentile: 12 us
# 90th percentile: 35 us
# 99th percentile: 180 us ← C-state 영향
# 99.9th percentile: 450 us
전력 측정
# RAPL (Running Average Power Limit)로 CPU 전력 측정
perf stat -e power/energy-pkg/,power/energy-cores/ -- sleep 10
# Performance counter stats for 'sleep 10':
#
# 2.34 Joules power/energy-pkg/ (패키지 전체)
# 0.89 Joules power/energy-cores/ (코어만)
# C-state 설정별 전력 비교:
# idle=poll: Pkg: 45.2W Cores: 40.1W
# max_cstate=1 (C1): Pkg: 4.8W Cores: 2.1W
# 기본값 (C6 허용): Pkg: 0.9W Cores: 0.1W
# 기본값 (C10 허용): Pkg: 0.3W Cores: 0.02W
# turbostat으로 watt 단위 실시간 측정
turbostat --interval 5 --show PkgWatt,CorWatt,RAMWatt
# PkgWatt CorWatt RAMWatt
# 0.92 0.08 0.45
# 비교 스크립트: C-state 설정별 전력/지연 트레이드오프 측정
for max_cs in 0 1 2 3 4; do
echo "=== max_cstate=$max_cs ==="
# C-state 설정
for s in /sys/devices/system/cpu/cpu*/cpuidle/state*/disable; do
echo 0 > $s
done
for s in /sys/devices/system/cpu/cpu*/cpuidle/state*/disable; do
idx=$(basename $(dirname $s) | sed 's/state//')
if [ $idx -gt $max_cs ]; then
echo 1 > $s
fi
done
# 전력 측정 (10초)
perf stat -e power/energy-pkg/ -- sleep 10 2>&1 | grep Joules
# 지연 측정
cyclictest -m -p 99 -i 1000 -l 10000 2>&1 | grep "Max Latencies"
done
C-state Residency 분석
# 각 C-state의 평균 체류 시간 계산
for cpu in /sys/devices/system/cpu/cpu0/cpuidle/state*; do
name=$(cat $cpu/name)
usage=$(cat $cpu/usage)
time=$(cat $cpu/time)
if [ $usage -gt 0 ]; then
avg=$((time / usage))
echo "$name: usage=$usage, total_time=${time}us, avg_residency=${avg}us"
fi
done
# POLL: usage=12345, total_time=5678us, avg_residency=0us
# C1: usage=234567, total_time=890123us, avg_residency=3us
# C1E: usage=456789, total_time=12345678us, avg_residency=27us
# C6: usage=78901, total_time=987654321us, avg_residency=12518us
# above/below 분석: governor 예측 정확도
for cpu in /sys/devices/system/cpu/cpu0/cpuidle/state*; do
name=$(cat $cpu/name)
above=$(cat $cpu/above)
below=$(cat $cpu/below)
usage=$(cat $cpu/usage)
echo "$name: usage=$usage, above=$above (너무 깊었음), below=$below (너무 얕았음)"
done
# above가 높으면: governor가 너무 깊은 C-state를 자주 선택
# below가 높으면: governor가 너무 얕은 C-state를 자주 선택
벤치마크 시 주의사항
- 워밍업: C-state 변경 후 최소 30초 이상 대기 후 측정 시작 (통계 안정화)
- 반복 측정: 최소 3회 이상 반복하여 중앙값 사용
- 격리: 측정 중 불필요한 프로세스/인터럽트 최소화 (
isolcpus,irqaffinity) - 써멀 영향: idle=poll 측정 시 써멀 스로틀링이 결과를 왜곡할 수 있음
- RAPL 정확도: RAPL은 평균 전력을 보고하므로, 순간 전력 변동은 반영 안 됨
CPUIdle 드라이버 작성 가이드
커스텀 하드웨어용 CPUIdle 드라이버를 작성하는 방법입니다.
최소 CPUIdle 드라이버
/* 최소 CPUIdle 드라이버 예제 */
#include <linux/cpuidle.h>
#include <linux/module.h>
/* C-state 진입 콜백 */
static int my_idle_enter(struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index)
{
/* C1: 단순 HLT */
if (index == 1)
cpu_do_idle(); /* HLT 또는 WFI */
/* C2: 하드웨어 특정 저전력 진입 */
if (index == 2)
my_hw_enter_deep_idle();
return index;
}
static struct cpuidle_driver my_idle_driver = {
.name = "my_idle",
.owner = THIS_MODULE,
.states = {
{ /* State 0: POLL (필수) */
.name = "POLL",
.desc = "CPUIDLE CORE POLL IDLE",
.exit_latency = 0,
.target_residency = 0,
.flags = CPUIDLE_FLAG_POLLING,
.enter = NULL, /* 코어가 처리 */
},
{ /* State 1: C1 */
.name = "C1",
.desc = "HLT",
.exit_latency = 5,
.target_residency = 10,
.enter = my_idle_enter,
},
{ /* State 2: C2 (깊은 idle) */
.name = "C2",
.desc = "Deep power down",
.exit_latency = 100,
.target_residency = 300,
.flags = CPUIDLE_FLAG_TIMER_STOP,
.enter = my_idle_enter,
},
},
.state_count = 3,
.safe_state_index = 1,
};
static int __init my_idle_init(void)
{
return cpuidle_register(&my_idle_driver, NULL);
}
static void __exit my_idle_exit(void)
{
cpuidle_unregister(&my_idle_driver);
}
module_init(my_idle_init);
module_exit(my_idle_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example CPUIdle driver");
드라이버 작성 체크리스트
| 항목 | 필수 | 설명 |
|---|---|---|
| POLL state (index 0) | 권장 | polling 폴백 상태, exit_latency=0 |
| exit_latency 정확성 | 필수 | 하드웨어 측정 기반 값 사용. 부정확하면 governor 오판 |
| target_residency | 필수 | exit_latency의 2~5배가 일반적 (break-even) |
| TIMER_STOP 플래그 | 조건부 | 로컬 타이머가 정지되는 C-state에 설정 |
| TLB_FLUSHED 플래그 | 조건부 | TLB가 무효화되는 C-state에 설정 |
| enter_s2idle 콜백 | 선택 | s2idle 지원 시 구현 (가장 깊은 상태) |
| enter_dead 콜백 | 선택 | CPU offline 시 호출되는 복귀 불가 진입 |
| cpumask 설정 | 선택 | per-CPU 드라이버 시 CPU 마스크 지정 |
exit_latency 측정 팁
정확한 exit_latency 값은 드라이버 품질의 핵심입니다. 측정 방법:
- IPI 기반: 타겟 CPU를 idle에 진입시킨 후, 다른 CPU에서 IPI를 보내 응답 시간 측정
- GPIO 토글: idle 진입 직전 GPIO 핀을 낮추고, 인터럽트 핸들러에서 높여 오실로스코프로 측정
- 하드웨어 타이머: CPU 외부 타이머(예: SoC 타이머)로 idle 기간 측정
- 측정 값에 20~50% 마진을 추가하여 안전 값으로 설정하는 것이 관례
s2idle 내부 동작 심화
s2idle(Suspend-to-Idle)이 실제로 진입하는 과정은 cpuidle_enter_s2idle() 경로를 통해 진행됩니다. 일반적인 CPUIdle 경로와 달리, s2idle은 모든 디바이스 드라이버를 먼저 suspend한 뒤 각 CPU가 가장 깊은 C-state로 진입하며, 이때 드라이버의 enter_s2idle 콜백이 별도로 호출됩니다.
/* drivers/cpuidle/cpuidle.c - s2idle 전용 진입 경로 */
int cpuidle_enter_s2idle(struct cpuidle_driver *drv,
struct cpuidle_device *dev)
{
int index;
/*
* s2idle에서는 governor 선택을 우회하고
* 드라이버가 제공하는 가장 깊은 상태를 직접 사용.
* enter_s2idle 콜백이 정의된 가장 깊은 state 선택
*/
for (index = drv->state_count - 1; index >= 0; index--) {
if (drv->states[index].enter_s2idle)
break;
}
if (index < 0)
return -ENODEV;
/* 일반 enter() 대신 enter_s2idle() 콜백 호출 */
drv->states[index].enter_s2idle(dev, drv, index);
return 0;
}
Intel 플랫폼에서 intel_idle 드라이버는 enter_s2idle 콜백에서 MWAIT에 특수 힌트를 전달하여 패키지 전체를 S0ix 상태로 진입시킵니다. 이 과정에서 플랫폼 펌웨어(PMC)와 협력하여 전력 게이팅을 수행합니다.
/* drivers/idle/intel_idle.c - s2idle용 MWAIT 힌트 */
static void intel_idle_s2idle(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
unsigned long ecx = 1; /* MWAIT 인터럽트 break 플래그 */
unsigned long eax = flg2MWAIT(drv->states[index].flags);
/*
* s2idle 경로에서의 MWAIT:
* - 인터럽트 비활성화 상태에서 호출
* - PMC가 모든 코어의 MWAIT 상태를 확인
* - 모든 코어가 deep C-state에 있으면 S0ix 진입
*/
mwait_idle_with_hints(eax, ecx);
}
s2idle 진입 실패 원인
s2idle이 예상 전력 수준에 도달하지 못하는 일반적인 원인:
- EC (Embedded Controller): EC가 SCI를 지속적으로 발생시켜 CPU가 깨어남
- ACPI GPE: General Purpose Event가 주기적으로 트리거됨 —
/sys/firmware/acpi/interrupts/에서 확인 - 네트워크 디바이스: WoL(Wake-on-LAN) 설정이 D3 진입을 방해
- 오디오 코덱: HD Audio 컨트롤러가 D0 상태에 고정되면 패키지 C-state 진입 불가
# s2idle 전력 상태 검증 (Intel 플랫폼)
# SLP_S0 카운터: PMC가 S0ix에 실제 진입했는지 확인
cat /sys/kernel/debug/pmc_core/slp_s0_residency_usec
# s2idle 전후 비교
echo s2idle > /sys/power/mem_sleep
BEFORE=$(cat /sys/kernel/debug/pmc_core/slp_s0_residency_usec)
rtcwake -m freeze -s 10 # 10초 후 자동 웨이크업
AFTER=$(cat /sys/kernel/debug/pmc_core/slp_s0_residency_usec)
echo "S0ix residency: $((AFTER - BEFORE)) us"
# 0이면 S0ix에 전혀 진입하지 못한 것
# PMC 서브시스템 블록 원인 확인
cat /sys/kernel/debug/pmc_core/substate_requirements
PM QoS 고급 제약 패턴
PM QoS는 전역(global) 제약 외에도 per-device 및 per-CPU 수준의 세밀한 제어를 지원합니다. 커널 6.x에서는 dev_pm_qos 프레임워크를 통해 디바이스별 resume latency 제약을 설정할 수 있습니다.
/* per-device PM QoS: 특정 디바이스의 resume latency 제한 */
#include <linux/pm_qos.h>
static void set_device_latency(struct device *dev)
{
/*
* 디바이스의 resume latency를 100us로 제한
* 이 제약은 해당 디바이스의 부모 도메인(power domain)에도 전파됨
* → genpd(Generic Power Domain) 레벨에서 C-state 선택에 영향
*/
dev_pm_qos_add_request(dev, &dev->power.qos->resume_latency_req,
DEV_PM_QOS_RESUME_LATENCY, 100);
}
/* sysfs를 통한 per-device 제약 (사용자 공간) */
/* echo 100 > /sys/devices/.../power/pm_qos_resume_latency_us */
| PM QoS 유형 | 인터페이스 | 영향 범위 | 사용 예 |
|---|---|---|---|
| CPU Latency (전역) | /dev/cpu_dma_latency | 시스템 전체 C-state | 실시간 오디오, HFT 트레이딩 |
| Per-CPU Latency | cpu_latency_qos_* | 특정 CPU의 C-state | IRQ affinity 코어 보호 |
| Device Resume Latency | dev_pm_qos_* | 디바이스 power domain | NVMe SSD 응답 보장 |
| Device Latency Tolerance | dev_pm_qos_* | 디바이스 자체 지연 허용 | 네트워크 카드 패킷 지연 |
# PM QoS 현재 상태 전체 확인
# 전역 CPU latency 요청 목록
cat /sys/kernel/debug/pm_qos/cpu_dma_latency/requested_value
# per-device resume latency 확인 (NVMe 예시)
find /sys/devices -name pm_qos_resume_latency_us -exec sh -c \
'echo "$1: $(cat $1)"' _ {} \;
# PM QoS 제약이 C-state에 미치는 영향 실시간 관찰
bpftrace -e '
tracepoint:power:pm_qos_update {
printf("PM QoS update: action=%d value=%d\n",
args->action, args->value);
}'
PM QoS와 실시간 워크로드
실시간 워크로드에서는 /dev/cpu_dma_latency에 0을 기록하여 모든 C-state를 비활성화(POLL 모드 강제)하는 것이 일반적입니다. 그러나 이 방법은 전력 소비가 급격히 증가하므로, 가능하면 적절한 지연값(예: 10~50us)을 설정하여 C1/C1E 수준의 절전은 허용하는 것이 권장됩니다.
커널 빌드 고급 설정
CPUIdle 관련 커널 빌드 시 일반적인 Kconfig 외에도 디버그 옵션과 플랫폼별 최적화 옵션이 있습니다.
# 디버그용 추가 설정
CONFIG_PM_DEBUG=y # 전원 관리 디버그 메시지
CONFIG_PM_ADVANCED_DEBUG=y # 고급 PM 디버그 sysfs
CONFIG_PM_SLEEP_DEBUG=y # 슬립/웨이크업 디버깅
CONFIG_PM_TRACE=y # 전원 상태 트레이싱
CONFIG_FTRACE=y # 함수 트레이싱 (power 이벤트)
CONFIG_TRACING=y # 트레이싱 프레임워크
# ARM/ARM64 플랫폼 CPUIdle 드라이버
CONFIG_ARM_CPUIDLE=y # ARM 일반 CPUIdle 드라이버
CONFIG_ARM_PSCI_CPUIDLE=y # PSCI 기반 (ARMv8+)
CONFIG_ARM_BIG_LITTLE_CPUIDLE=y # big.LITTLE 유휴 관리
# RISC-V 플랫폼
CONFIG_RISCV_SBI_CPUIDLE=y # SBI 기반 CPUIdle
# 가상화 환경
CONFIG_HALTPOLL_CPUIDLE=y # KVM 게스트 haltpoll 드라이버
CONFIG_KVM_GUEST=y # haltpoll 의존성
# 현재 커널의 CPUIdle 관련 설정 확인
zcat /proc/config.gz 2>/dev/null | grep -i "cpu_idle\|cpuidle\|intel_idle\|no_hz\|pm_qos" \
|| grep -i "cpu_idle\|cpuidle\|intel_idle\|no_hz\|pm_qos" /boot/config-$(uname -r)
# 특정 옵션 포함 여부 확인
scripts/config --file .config --state CPU_IDLE_GOV_TEO
# y, m, n, 또는 undef 중 하나 출력
# 디버그 빌드 일괄 활성화
scripts/config --file .config \
--enable PM_DEBUG \
--enable PM_ADVANCED_DEBUG \
--enable PM_SLEEP_DEBUG \
--enable FTRACE
빌드 시 주의사항
CONFIG_INTEL_IDLE=y와CONFIG_ACPI_PROCESSOR_IDLE=y가 모두 활성화된 경우,intel_idle이 우선 로드됩니다.intel_idle.max_cstate=0부팅 파라미터로 비활성화하면acpi_idle로 폴백합니다.CONFIG_NO_HZ_FULL=y를 사용하면 CPUIdle governor는menu가 강제 선택됩니다.ladder는 tick 기반이므로 tickless 환경에서 정상 동작하지 않습니다.- 디버그 옵션 활성화 시 성능 오버헤드가 있으므로, 프로덕션 커널에서는 비활성화하는 것이 좋습니다.