cpusets & CPU Isolation
cpusets는 태스크(Task)를 지정 CPU와 메모리 노드에 고정해 성능 예측성을 높이는 핵심 제어 수단입니다. 이 문서는 cgroup v2의 cpuset.cpus/cpuset.mems 의미, isolcpus/nohz_full/rcu_nocbs 조합, IRQ affinity와 housekeeping 분리, NUMA 배치, HPC 및 실시간(Real-time) 워크로드의 지터 최소화 튜닝과 검증 절차를 상세히 다룹니다.
핵심 요약
- cpuset.cpus — 작업이 실행 가능한 CPU 집합
- cpuset.mems — 메모리 할당 가능한 NUMA 노드 집합
- isolcpus — 부팅 시점의 정적 격리(Isolation)
- nohz_full — 주기 tick 인터럽트(Interrupt)를 줄여 지터 완화
- rcu_nocbs — RCU 콜백(Callback)을 격리 CPU 밖으로 오프로드
단계별 이해
- 대상 워크로드 선정
지연(Latency) 민감 태스크와 일반 태스크를 먼저 분류합니다. - CPU/NUMA 매핑(Mapping) 설계
코어와 메모리 노드를 짝지어 cpuset 경계를 정의합니다. - 커널 파라미터 결합
isolcpus,nohz_full,rcu_nocbs를 함께 조정해 간섭원을 제거합니다. - 성능 측정 반복
latency/throughput를 측정하며 cpuset 경계와 밸런싱 정책을 미세 조정합니다.
개요
CPU Isolation은 특정 CPU 코어를 일반 커널 작업으로부터 격리하여 중요한 워크로드가 방해받지 않도록 보장하는 기술입니다.
격리 이유
- 성능 예측성 — 스케줄러 노이즈 제거, 일정한 응답 시간. 동일 작업의 반복 실행 시간 편차(지터)를 수 µs 이내로 제한
- 캐시(Cache) 친화성 — 독점 CPU로 L1/L2/L3 캐시 오염 방지. 다른 프로세스가 캐시 라인을 교체하지 않아 캐시 히트율(Hit Rate) 유지
- 실시간 요구사항 — 금융 거래(나노초 단위), 로봇 제어(마이크로초 단위), 5G 통신 장비(TTI 내 처리 보장)
- HPC 워크로드 — 과학 시뮬레이션, 렌더링 팜, MPI 기반 병렬 계산에서 코어별 전용 할당
- 컨테이너(Container) 격리 — Kubernetes Guaranteed QoS 파드, Docker
--cpuset-cpus, VM vCPU 피닝(Pinning) - DPDK/SPDK — 커널 바이패스 네트워크/스토리지 처리에서 CPU 독점 poll 모드
격리 방법
| 방법 | 설명 | 적용 시점 |
|---|---|---|
| isolcpus | 부팅 시 CPU를 스케줄러에서 제외 | 커널 파라미터 (정적) |
| cpusets | cgroup으로 프로세스 CPU 제한 | 런타임 (동적) |
| nohz_full | 틱리스(Tickless) 모드 (타이머(Timer) 인터럽트 제거) | 커널 파라미터 (정적) |
| rcu_nocbs | RCU 콜백을 다른 CPU로 오프로드 | 커널 파라미터 (정적) |
| taskset | 프로세스 CPU affinity 설정 | 런타임 (명령줄) |
cpusets 서브시스템
개념
cpusets는 프로세스 그룹에 CPU와 메모리 노드를 할당하는 cgroup v1/v2 서브시스템입니다.
cpuset 제어 파일
| 파일 | 설명 |
|---|---|
cpuset.cpus |
허용된 CPU 목록 (예: 0-3,8-11) |
cpuset.mems |
허용된 메모리 노드 (NUMA) |
cpuset.cpu_exclusive |
독점 모드 (1 = 다른 cpuset과 CPU 중복 불가) |
cpuset.mem_exclusive |
메모리 노드 독점 |
cpuset.sched_load_balance |
스케줄러 로드 밸런싱 (0 = 비활성화) |
cpuset.memory_migrate |
메모리 마이그레이션 허용 |
cpuset.memory_pressure |
메모리 압력 통계 (읽기 전용(Read-Only)) |
cpuset 생성 (cgroup v1)
# cpuset cgroup 마운트
$ sudo mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
# 격리된 cpuset 생성
$ sudo mkdir /sys/fs/cgroup/cpuset/isolated
# CPU 4-7 할당
$ echo 4-7 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.cpus
# NUMA 노드 0 할당
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.mems
# 독점 모드 활성화
$ echo 1 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.cpu_exclusive
# 로드 밸런싱 비활성화 (지터 최소화)
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.sched_load_balance
# 프로세스 추가
$ echo $$ | sudo tee /sys/fs/cgroup/cpuset/isolated/tasks
$ ./my_rt_app
cpuset in cgroup v2
isolcpus 커널 파라미터
개요
isolcpus는 부팅 시 지정된 CPU를 기본 스케줄러 도메인에서 제외합니다. 이로 인해 일반 프로세스는 해당 CPU에 스케줄링되지 않습니다. isolcpus가 커널 부트 파라미터로 전달되면, kernel/sched/isolation.c의 housekeeping_setup()가 호출되어 내부 비트맵(housekeeping_mask)에서 해당 CPU를 제거합니다. 스케줄러는 이 비트맵을 참조하여 태스크 배치와 로드 밸런싱 대상에서 격리된 CPU를 건너뜁니다.
isolcpus 플래그 상세
| 플래그 | 커널 상수 | 설명 | 커널 버전 |
|---|---|---|---|
domain |
HK_TYPE_DOMAIN |
스케줄러 도메인에서 제외 — 로드 밸런서가 해당 CPU를 건너뜀 | 2.6.11+ |
managed_irq |
HK_TYPE_MANAGED_IRQ |
커널이 관리하는 IRQ(MSI-X 등)를 해당 CPU에 배치하지 않음 | 5.1+ |
nohz |
HK_TYPE_TICK |
nohz_full과 동일 효과 (타이머 틱 제거) | 4.15+ |
| (없음, CPU만 지정) | HK_TYPE_DOMAIN |
기본 동작 = domain 플래그와 동일 | 2.6.11+ |
isolcpus=domain,managed_irq,4-7은 스케줄러 도메인 격리와 관리형 IRQ 격리를 동시에 적용합니다. managed_irq 없이 domain만 사용하면, NVMe·네트워크 카드의 MSI-X 인터럽트가 여전히 격리 CPU에 도달할 수 있습니다.
문법
# GRUB 설정 (/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=4-7"
# 또는 도메인별 격리
GRUB_CMDLINE_LINUX="isolcpus=domain,4-7"
# 옵션
isolcpus=4-7 # CPU 4~7 격리 (기본)
isolcpus=domain,4-7 # 스케줄러 도메인에서 제외
isolcpus=managed_irq,4-7 # managed IRQ도 격리
# GRUB 업데이트
$ sudo update-grub
$ sudo reboot
격리 확인
# isolcpus 설정 확인
$ cat /proc/cmdline | grep isolcpus
# 스케줄러 도메인 확인
$ cat /proc/sched_debug | grep -A 10 "cpu#4"
# 프로세스 CPU affinity 확인
$ taskset -p $$
pid 1234's current affinity mask: f # 0-3만 허용 (4-7 제외)
nohz_full (틱리스 모드)
개념
nohz_full은 지정된 CPU에서 주기적인 타이머 틱(timer tick)을 제거하여 실시간 워크로드의 지터를 최소화합니다. 기본적으로 리눅스 커널은 1ms(HZ=1000) 또는 4ms(HZ=250) 간격으로 타이머 인터럽트를 발생시켜 스케줄링, 통계 업데이트, 타이머 처리를 수행합니다. nohz_full은 해당 CPU에서 runnable 태스크가 1개뿐일 때 이 주기적 틱을 완전히 멈추어, 수백 나노초~수 마이크로초 단위의 지터를 제거합니다.
요구사항
CONFIG_NO_HZ_FULL=y커널 빌드 옵션- 최소 1개 이상의 housekeeping CPU (nohz_full에서 제외)
- 단일 스레드(Thread) 워크로드 (CPU당 1개 프로세스)
설정
# GRUB 설정
GRUB_CMDLINE_LINUX="nohz_full=4-7"
# isolcpus와 함께 사용 (권장)
GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7"
# 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7
$ cat /sys/devices/system/cpu/isolated
4-7
동작 방식
/* nohz_full CPU에서의 상태 전이 */
/* 1. 틱 중단 조건 */
- 단일 runnable 태스크만 있을 때 → 타이머 틱 중단
tick_nohz_stop_tick() → hrtimer_cancel(&ts->sched_timer)
/* 2. 틱 재개 조건 */
- 2개 이상 runnable 태스크 → 타이머 틱 재개 (스케줄링 필요)
tick_nohz_full_kick() → IPI로 해당 CPU에 틱 재개 요청
- 시스템 콜/인터럽트 진입 시 → 일시적으로 틱 재개
context_tracking_enter(CONTEXT_KERNEL) 호출
/* 3. context_tracking 서브시스템 */
- 커널 진입(시스템 콜/인터럽트) 시 ct_kernel_enter()
- 사용자 공간 복귀 시 ct_kernel_exit()
- RCU에게 "이 CPU는 유휴 상태"를 알림 (extended quiescent state)
/* 4. 성능 카운터 업데이트 */
- 틱이 없으면 CPU 사용량 통계가 정확하지 않을 수 있음
- vtime_account_system() 으로 시스템 콜 진입 시 보정
- /proc/stat의 CPU 시간이 nohz_full CPU에서 부정확할 수 있음
nohz_full을 사용하려면 이 옵션이 필요합니다. 커널 6.x에서는 CONFIG_NO_HZ_FULL=y 설정 시 자동으로 활성화됩니다. context tracking은 매 시스템 콜 진입/복귀에 약간의 오버헤드(Overhead)를 추가하므로, nohz_full이 필요 없는 시스템에서는 비활성화하는 것이 좋습니다.
rcu_nocbs (RCU 콜백 오프로드)
개념
RCU(Read-Copy-Update) 콜백 처리를 별도 CPU로 오프로드하여 격리된 CPU의 지터를 줄입니다. RCU는 읽기 측에서 잠금 없이 데이터를 참조하고, 쓰기 측에서는 유예 기간(Grace Period) 후 이전 데이터를 해제하는 동기화 메커니즘입니다. 이 해제 작업이 "콜백(Callback)"이며, 기본적으로 해당 CPU의 softirq 컨텍스트(Context)에서 처리됩니다. rcu_nocbs를 설정하면 이 콜백을 전용 커널 스레드(rcuog/N, rcuop/N)가 housekeeping CPU에서 처리하게 됩니다.
설정
# GRUB 설정
GRUB_CMDLINE_LINUX="rcu_nocbs=4-7"
# 완전한 CPU 격리 설정
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7"
# 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7
# RCU 스레드 확인
$ ps aux | grep rcuo
root 12 0.0 0.0 0 0 ? S 12:00 0:00 [rcuo/4]
root 13 0.0 0.0 0 0 ? S 12:00 0:00 [rcuo/5]
...
taskset (CPU Affinity)
taskset은 프로세스의 CPU affinity를 설정하는 유틸리티입니다. 내부적으로 sched_setaffinity() 시스템 콜을 호출하여 태스크의 cpus_mask를 변경합니다. cpuset과 달리 cgroup을 사용하지 않고 개별 프로세스 단위로 CPU를 제한합니다.
사용법
# CPU 4에 프로세스 바인딩
$ taskset -c 4 ./my_app
# CPU 4-7에 바인딩
$ taskset -c 4-7 ./my_app
# 비트마스크 사용 (CPU 0,2,4,6)
$ taskset 0x55 ./my_app
# 실행 중인 프로세스 affinity 변경
$ taskset -cp 4 1234
pid 1234's current affinity list: 0-7
pid 1234's new affinity list: 4
# 현재 affinity 확인
$ taskset -p $$
pid 5678's current affinity mask: f0 # CPU 4-7
sched_setaffinity() 시스템 콜(System Call)
#define _GNU_SOURCE
#include <sched.h>
int main(void)
{
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(4, &cpuset); /* CPU 4 */
if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
perror("sched_setaffinity");
return 1;
}
/* 이제 CPU 4에서만 실행됨 */
while (1) {
/* compute-intensive task */
}
}
IRQ Affinity (인터럽트 격리)
인터럽트 관리
CPU를 격리해도 인터럽트가 해당 CPU로 전달되면 워크로드가 중단됩니다. 하드웨어 인터럽트(IRQ)는 CPU를 즉시 선점하여 인터럽트 핸들러(Handler)를 실행하므로, 격리 CPU에서 모든 IRQ를 제거해야 완전한 격리가 가능합니다. IRQ affinity는 /proc/irq/N/smp_affinity(비트마스크) 또는 smp_affinity_list(CPU 목록)를 통해 제어합니다.
# 모든 IRQ 확인
$ cat /proc/interrupts
# IRQ 0의 affinity 확인
$ cat /proc/irq/0/smp_affinity
ff # 모든 CPU
# IRQ 0을 CPU 0-3으로 제한 (4-7 격리)
$ echo 0f | sudo tee /proc/irq/0/smp_affinity
0f
# 모든 IRQ를 CPU 0-3으로 이동 (스크립트)
$ for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done
smp_affinity 비트마스크 계산
IRQ affinity는 16진수 비트마스크(Bitmask)로 표현됩니다. 각 비트(Bit)가 CPU 번호에 대응하며, 1이면 해당 CPU로 인터럽트를 전달할 수 있습니다.
| CPU 구성 | 비트마스크 (16진수) | 비트마스크 (2진수) | 의미 |
|---|---|---|---|
| CPU 0만 | 01 |
00000001 |
CPU 0에서만 처리 |
| CPU 0-3 | 0f |
00001111 |
housekeeping CPU에서만 처리 |
| CPU 0-7 (전체) | ff |
11111111 |
기본값 — 모든 CPU 허용 |
| CPU 4-7만 | f0 |
11110000 |
격리 CPU에서 처리 (비정상) |
| CPU 0,2 (짝수) | 05 |
00000101 |
특정 CPU 지정 |
# smp_affinity_list 사용 (CPU 목록 형태 — 더 직관적)
$ cat /proc/irq/32/smp_affinity_list
0-7
# CPU 0-3으로 제한 (목록 형태)
$ echo "0-3" | sudo tee /proc/irq/32/smp_affinity_list
# 32코어 이상 시스템에서 비트마스크 (그룹 단위, 쉼표 구분)
# CPU 0-31: 0000ffff,0000ffff (32비트씩 그룹)
$ cat /proc/irq/128/smp_affinity
00000000,0000000f # CPU 0-3만 허용
# effective_affinity: 실제 배치된 CPU 확인
$ cat /proc/irq/32/effective_affinity_list
2 # 실제로 CPU 2에서 처리 중
격리 CPU IRQ 검증
# 격리 CPU에 남아있는 IRQ 검사 스크립트
echo "===== 격리 CPU IRQ 검사 ====="
for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
affinity=$(cat /proc/irq/$irq/smp_affinity_list 2>/dev/null)
# CPU 4-7 범위에 해당하는 IRQ 찾기
if echo "$affinity" | grep -qE '[4-7]'; then
name=$(cat /proc/irq/$irq/actions 2>/dev/null || echo "unknown")
echo " [경고] IRQ $irq ($name) affinity=$affinity → 격리 CPU 포함"
fi
done
# 격리 CPU의 인터럽트 카운트 확인 (0이어야 정상)
$ awk 'NR==1{print "IRQ", $5, $6, $7, $8}
NR>1{sum=0; for(i=5;i<=8;i++) sum+=$i; if(sum>0) print $0}' /proc/interrupts
# CPU4~CPU7 컬럼에 숫자가 증가하면 격리 실패
irqbalance 관리
irqbalance 데몬은 시스템 부하에 따라 IRQ를 자동으로 분산합니다. 격리 환경에서는 irqbalance가 격리 CPU로 IRQ를 재배치할 수 있으므로 적절히 관리해야 합니다.
# 방법 1: 완전 비활성화 (소규모 시스템)
$ sudo systemctl stop irqbalance
$ sudo systemctl disable irqbalance
# 방법 2: 격리 CPU만 제외 (권장 — 대규모 시스템)
# /etc/default/irqbalance 또는 /etc/sysconfig/irqbalance
IRQBALANCE_BANNED_CPULIST="4-7" # 격리 CPU 제외
$ sudo systemctl restart irqbalance
# 방법 3: irqbalance --policyscript 사용 (고급)
# 특정 IRQ를 특정 CPU에 고정하는 정책 스크립트 작성 가능
$ sudo irqbalance --policyscript=/etc/irqbalance/policy.sh
실전 예제
HPC 워크로드 설정
#!/bin/bash
# HPC CPU 격리 스크립트
# 1. GRUB 설정 확인
grep -q "isolcpus" /proc/cmdline || {
echo "Error: isolcpus not set"
exit 1
}
# 2. cpuset 생성
sudo mkdir -p /sys/fs/cgroup/cpuset/hpc
echo 4-7 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.cpus
echo 0 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.mems
echo 1 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.cpu_exclusive
echo 0 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.sched_load_balance
# 3. IRQ affinity 설정 (CPU 0-3만)
for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done
# 4. HPC 애플리케이션 실행
echo $$ | sudo tee /sys/fs/cgroup/cpuset/hpc/tasks
exec taskset -c 4 chrt -f 99 ./hpc_simulation
실시간 애플리케이션
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
#include <stdio.h>
void setup_rt_thread(int cpu_id)
{
struct sched_param param;
cpu_set_t cpuset;
/* CPU affinity 설정 */
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
/* 실시간 우선순위 설정 */
param.sched_priority = 99;
pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);
/* 메모리 락 (페이지 폴트 방지) */
mlockall(MCL_CURRENT | MCL_FUTURE);
printf("RT thread on CPU %d\\n", cpu_id);
}
void *rt_worker(void *arg)
{
int cpu_id = *(int *)arg;
setup_rt_thread(cpu_id);
while (1) {
/* Critical real-time work */
}
}
모니터링
CPU 격리 모니터링
#!/bin/bash
# CPU 격리 상태 모니터링
echo "===== CPU Isolation Status ====="
echo "Kernel Parameters:"
grep -o 'isolcpus=[^ ]*' /proc/cmdline
grep -o 'nohz_full=[^ ]*' /proc/cmdline
grep -o 'rcu_nocbs=[^ ]*' /proc/cmdline
echo
echo "Isolated CPUs:"
cat /sys/devices/system/cpu/isolated
echo
echo "Tasks on Isolated CPUs:"
for cpu in 4 5 6 7; do
echo " CPU $cpu:"
ps -eLo psr,comm | grep "^ *$cpu" | wc -l
done
echo
echo "IRQ Distribution:"
cat /proc/interrupts | head -1
cat /proc/interrupts | grep -E "^ *[0-9]+:" | head -5
커널 설정
CONFIG_CPUSETS=y # cpuset 서브시스템
CONFIG_PROC_PID_CPUSET=y # /proc/[pid]/cpuset
CONFIG_NO_HZ_FULL=y # Full tickless mode
CONFIG_RCU_NOCB_CPU=y # RCU callback offload
CONFIG_IRQ_FORCED_THREADING=y # IRQ 스레딩
# 실시간 커널 (선택)
CONFIG_PREEMPT_RT=y # PREEMPT_RT 패치
CONFIG_HIGH_RES_TIMERS=y # 고해상도 타이머
Best Practices
- Housekeeping CPU 확보 — 최소 1~2개 CPU는 일반 작업용으로 남겨둘 것
- NUMA 고려 — 격리된 CPU와 메모리 노드를 동일하게 설정
- 하이퍼스레딩 비활성화 — SMT 비활성화로 캐시 경합(Contention) 제거
- 전원 관리(Power Management) 비활성화 — cpufreq governor를 performance로 설정
- 투명 거대 페이지(Transparent Huge Pages) 비활성화 — THP가 지터를 유발할 수 있음
참고자료
- Linux Kernel Parameters — 커널 부트 파라미터 전체 목록을 확인할 수 있습니다
- cpusets Documentation (cgroup v1) — cgroup v1 기반 cpuset 공식 문서입니다
- Control Group v2 Documentation — cgroup v2 통합 계층의 cpuset 컨트롤러 사용법을 설명합니다
- NO_HZ (Tickless Kernel) — nohz_full 파라미터와 틱리스 커널 동작 원리를 다룹니다
- Core Scheduling — SMT 환경에서 코어 스케줄링과 CPU 격리의 보안 측면을 설명합니다
- Scheduler Isolation (housekeeping) — isolcpus, nohz_full, rcu_nocbs 등 하우스키핑 CPU 분리 메커니즘의 커널 문서입니다
- RCU Expedited Grace Periods — rcu_nocbs와 관련된 RCU 콜백 오프로딩 설계를 설명합니다
- LWN: Primitives for low-latency workloads — 저지연 워크로드를 위한 CPU 격리 기법을 심층 분석합니다
- LWN: A survey of scheduler benchmarks — 스케줄러 벤치마크와 CPU 바인딩 효과를 비교합니다
- LWN: cpuset improvements for cgroup v2 — cgroup v2에서의 cpuset 개선 사항을 다룹니다
- cpuset(7) man page — cpuset의 인터페이스와 사용법을 정리한 매뉴얼 페이지입니다
- cgroups(7) man page — cgroup v1/v2 전반적인 구조와 cpuset 컨트롤러 관계를 설명합니다
- sched_setaffinity(2) man page — CPU 친화성 설정 시스템 콜과 cpuset 제약의 상호작용을 설명합니다
kernel/sched/isolation.c— CPU 격리 구현 소스입니다kernel/cgroup/cpuset.c— cpuset 서브시스템 핵심 구현부입니다
- cgroups — 리소스 제어
- Process Scheduler — 스케줄링 정책
- Real-time — 실시간 커널
- NUMA — 메모리 배치 최적화
cgroup v1 vs v2 cpuset 계층 구조
cpuset 컨트롤러는 cgroup v1과 v2에서 근본적으로 다른 계층 모델을 사용합니다. v1은 전용 마운트(Mount) 포인트(/sys/fs/cgroup/cpuset/)에 독립된 트리를 구성하는 반면, v2는 통합된 단일 계층에서 cgroup.subtree_control을 통해 컨트롤러를 활성화합니다. 마이그레이션 시 API 차이, 파일 이름 변경, 동작 의미 변화를 반드시 이해해야 합니다.
API 차이 비교
| 항목 | cgroup v1 | cgroup v2 |
|---|---|---|
| 마운트 | mount -t cgroup -o cpuset |
mount -t cgroup2 (통합) |
| 컨트롤러 활성화 | 마운트 옵션으로 자동 | echo "+cpuset" > cgroup.subtree_control |
| 프로세스 이동 | echo PID > tasks |
echo PID > cgroup.procs |
| 독점 CPU | cpuset.cpu_exclusive=1 |
cpuset.cpus.partition=root |
| 로드 밸런싱 제어 | cpuset.sched_load_balance=0 |
partition이 자동 관리 |
| 유효 CPU 확인 | cpuset.effective_cpus (일부 배포판) |
cpuset.cpus.effective |
| 스레드 모드 | 미지원 | cgroup.type=threaded |
| 내부 프로세스 규칙 | 없음 (자유) | no-internal-process (리프 노드만 태스크 보유) |
v1 → v2 마이그레이션 절차
# 1. 현재 v1 cpuset 설정 확인
$ cat /proc/mounts | grep cpuset
cpuset /sys/fs/cgroup/cpuset cgroup rw,cpuset 0 0
# 2. v1 cpuset 계층 구조 백업
$ find /sys/fs/cgroup/cpuset -name "cpuset.cpus" -exec sh -c 'echo "$1: $(cat $1)"' _ {} \;
# 3. systemd가 cgroup v2를 사용하도록 GRUB 설정
$ sudo sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1 /' /etc/default/grub
$ sudo update-grub
# 4. 재부팅 후 v2 통합 계층 확인
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# 5. cpuset 컨트롤러 활성화
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 6. v2 방식으로 cpuset 생성
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems
# 7. partition 모드로 CPU 독점 (v1의 cpu_exclusive 대체)
$ echo "root" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition
v2 스레드 모드
cgroup v2는 스레드 수준의 cpuset 제어를 지원합니다. cgroup.type=threaded로 설정하면 개별 스레드를 서로 다른 CPU 집합에 배치할 수 있습니다.
# 스레드 모드 cgroup 생성
$ sudo mkdir /sys/fs/cgroup/app
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/app/cgroup.subtree_control
# 하위 cgroup을 threaded 타입으로 전환
$ sudo mkdir /sys/fs/cgroup/app/worker-fast
$ echo "threaded" | sudo tee /sys/fs/cgroup/app/worker-fast/cgroup.type
$ sudo mkdir /sys/fs/cgroup/app/worker-slow
$ echo "threaded" | sudo tee /sys/fs/cgroup/app/worker-slow/cgroup.type
# 각 스레드 그룹에 다른 CPU 할당
$ echo "4-5" | sudo tee /sys/fs/cgroup/app/worker-fast/cpuset.cpus
$ echo "6-7" | sudo tee /sys/fs/cgroup/app/worker-slow/cpuset.cpus
# 스레드 이동 (TID 사용)
$ echo 12345 | sudo tee /sys/fs/cgroup/app/worker-fast/cgroup.threads
CPU 격리와 cpuset.cpus.partition
cgroup v2에서 cpuset.cpus.partition은 CPU 격리의 핵심 메커니즘입니다. root 파티션으로 설정하면 해당 cgroup의 CPU가 부모에서 분리되어 완전히 독립된 스케줄링 도메인(Scheduling Domain)을 형성합니다. 이는 v1의 cpuset.cpu_exclusive=1 + cpuset.sched_load_balance=0를 하나로 통합한 것입니다.
isolcpus 비권장 추세
isolcpus를 점진적으로 cpuset partition 기반으로 대체하는 것을 권장합니다. isolcpus는 부팅 시 고정되어 런타임 변경이 불가능하며, 잘못 설정하면 재부팅 없이 복구할 수 없습니다.
# 기존 방식 (isolcpus — 재부팅 필요)
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7"
# 새로운 방식 (cpuset partition — 런타임 변경 가능, 커널 5.11+)
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "0-1" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems
$ echo "isolated" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition
# 격리 해제 (재부팅 없이!)
$ echo "member" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition
# 현재 유효 CPU 확인
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4-7
Housekeeping CPU 설계
격리 환경에서 housekeeping CPU는 커널 데몬, IRQ 처리, RCU 콜백, 워크큐 등 시스템 유지보수 작업을 전담합니다. 최소 1~2개의 CPU를 housekeeping 전용으로 확보해야 하며, NUMA 토폴로지(Topology)를 고려하여 배치합니다.
| CPU 역할 | CPU 번호 (예) | 담당 작업 | 커널 파라미터 |
|---|---|---|---|
| Housekeeping | 0-1 | 커널 데몬, IRQ, RCU, 워크큐, 타이머 | (기본값, 격리 대상에서 제외) |
| 관리 워크로드 | 2-3 | 모니터링, 로깅, 관리 프로세스 | cpuset으로 제한 |
| 격리 워크로드 | 4-7 | 지연 민감 애플리케이션 | nohz_full=4-7 rcu_nocbs=4-7 |
# housekeeping CPU 마스크 확인 (nohz_full 미지정 CPU = housekeeping)
$ cat /sys/devices/system/cpu/nohz_full
4-7
# housekeeping CPU에 커널 스레드 고정
$ for pid in $(pgrep -f "\[.*\]"); do
taskset -cp 0-1 $pid 2>/dev/null
done
# irqbalance에서 격리 CPU 제외
$ echo 'IRQBALANCE_BANNED_CPULIST=4-7' | sudo tee -a /etc/default/irqbalance
$ sudo systemctl restart irqbalance
NOHZ_FULL 통합과 틱 오프로딩(Offloading)
NOHZ_FULL(전체 틱리스 모드)은 cpuset 격리와 결합하여 CPU 코어의 주기적 타이머 인터럽트를 완전히 제거합니다. 지연에 민감한 워크로드(금융 거래, 실시간 제어)에서 마이크로초 수준의 지터를 제거하는 핵심 기술입니다.
NOHZ_FULL 검증 방법
# 1. nohz_full 활성 CPU 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7
# 2. 실제 틱 중단 여부를 /proc/interrupts로 확인
# 격리 CPU의 LOC(Local timer) 카운트가 증가하지 않아야 함
$ watch -n 1 "cat /proc/interrupts | grep LOC"
# 3. ftrace로 tick_stop 이벤트 확인
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/timer/tick_stop/enable
$ cat /sys/kernel/debug/tracing/trace_pipe | head -20
# 4. perf로 인터럽트 빈도 측정
$ sudo perf stat -C 4 -e irq:irq_handler_entry -- sleep 10
지연 민감 워크로드 최적화
#define _GNU_SOURCE
#include <sched.h>
#include <time.h>
#include <stdio.h>
#include <sys/mman.h>
/* nohz_full CPU에서 최소 지터로 실행하기 위한 설정 */
void setup_nohz_full_task(int cpu)
{
cpu_set_t mask;
struct sched_param sp = { .sched_priority = 99 };
/* 1. CPU affinity — 격리 CPU에 고정 */
CPU_ZERO(&mask);
CPU_SET(cpu, &mask);
sched_setaffinity(0, sizeof(mask), &mask);
/* 2. SCHED_FIFO 최고 우선순위 — 선점 방지 */
sched_setscheduler(0, SCHED_FIFO, &sp);
/* 3. 메모리 잠금 — 페이지 폴트 제거 */
mlockall(MCL_CURRENT | MCL_FUTURE);
/* 4. 타이머 슬랙 제거 (prctl) */
prctl(PR_SET_TIMERSLACK, 1);
}
/* 지터 측정: 연속 clock_gettime 호출 간격 편차 */
void measure_jitter(int iterations)
{
struct timespec ts1, ts2;
long max_ns = 0, min_ns = 999999999;
for (int i = 0; i < iterations; i++) {
clock_gettime(CLOCK_MONOTONIC, &ts1);
/* 워크로드 시뮬레이션 */
asm volatile("pause");
clock_gettime(CLOCK_MONOTONIC, &ts2);
long diff = (ts2.tv_sec - ts1.tv_sec) * 1000000000L
+ (ts2.tv_nsec - ts1.tv_nsec);
if (diff > max_ns) max_ns = diff;
if (diff < min_ns) min_ns = diff;
}
printf("지터: min=%ldns max=%ldns range=%ldns\n",
min_ns, max_ns, max_ns - min_ns);
}
nohz_full CPU에서 시스템 콜을 호출하면 일시적으로 틱이 재개됩니다. 최소 지터를 원하면 시스템 콜 호출을 최소화하고, vDSO를 활용하여 clock_gettime() 등을 사용자 공간(User Space)에서 처리하세요.
메모리 노드 바인딩과 NUMA 친화성
cpuset.mems는 프로세스 그룹이 메모리를 할당할 수 있는 NUMA 노드를 제한합니다. CPU와 메모리 노드를 올바르게 짝지어야 원격 메모리 접근(Remote Memory Access)으로 인한 성능 저하를 방지할 수 있습니다. 잘못된 cpuset.mems 설정은 OOM(Out of Memory)을 유발할 수 있습니다.
cpuset.mems 설정과 확인
# NUMA 토폴로지 확인
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 32768 MB
node 1 cpus: 4 5 6 7
node 1 size: 32768 MB
# CPU와 NUMA 노드 매핑 확인
$ lscpu | grep NUMA
NUMA node0 CPU(s): 0-3
NUMA node1 CPU(s): 4-7
# 올바른 cpuset 설정 (CPU와 메모리 노드 일치)
$ sudo mkdir /sys/fs/cgroup/numa-aligned
$ echo "4-7" | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.mems
# 유효 메모리 노드 확인
$ cat /sys/fs/cgroup/numa-aligned/cpuset.mems.effective
1
# 메모리 마이그레이션 활성화 (cpuset.mems 변경 시 기존 페이지도 이동)
$ echo 1 | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.memory_migrate
NUMA OOM 처리와 대응
/* 커널 내부: cpuset OOM 처리 경로 */
/* mm/oom_kill.c */
static void select_bad_process(struct oom_control *oc)
{
/*
* cpuset이 설정된 경우, OOM killer는 해당 cpuset의
* mems_allowed에 속한 태스크만 종료 대상으로 선택합니다.
*
* 이는 전체 시스템이 아닌 cpuset 범위 내에서만
* OOM이 발생하는 것을 의미합니다.
*/
if (is_memcg_oom(oc))
return;
/* cpuset 제한 내에서 가장 큰 메모리 사용자 선택 */
for_each_process(p) {
if (!cpuset_mems_allowed_intersects(current, p))
continue; /* cpuset 외부 프로세스는 건너뜀 */
/* ... oom_badness 점수 계산 ... */
}
}
cpuset.mems를 너무 제한적으로 설정하면 해당 NUMA 노드의 메모리가 부족할 때 다른 노드에서 빌려올 수 없어 OOM이 발생합니다. 워크로드의 최대 메모리 사용량을 사전에 파악하고, 여유분을 확보하세요.
스케줄러 도메인과 로드 밸런싱
cpuset은 스케줄러 도메인(Scheduling Domain) 구조를 직접 변경합니다. cpuset.cpus.partition=root를 설정하면 커널은 partition_sched_domains_locked()을 호출하여 스케줄러 도메인을 재구축합니다. 이 과정에서 로드 밸런싱 경계가 재설정되며, 격리된 CPU는 다른 CPU와 태스크를 교환하지 않습니다.
sched_relax_domain_level 튜닝
sched_relax_domain_level은 cpuset 내에서 스케줄러가 로드 밸런싱을 수행하는 도메인 깊이를 제어합니다. 값이 클수록 더 넓은 범위에서 밸런싱하지만, 캐시 친화성이 저하됩니다.
| 값 | 도메인 수준 | 설명 | 사용 시나리오 |
|---|---|---|---|
| -1 | 기본값 | 커널 기본 동작 | 일반 워크로드 |
| 0 | 없음 | 밸런싱 비활성화 | 완전 격리 (지연 민감) |
| 1 | SMT | 하이퍼스레드 내에서만 | SMT 쌍 활용 |
| 2 | MC | 같은 다이 내 코어 | L2 캐시 공유 코어 |
| 3 | NUMA | 같은 NUMA 노드 | 메모리 지역성 유지 |
| 4 | Cross-NUMA | 전체 NUMA 노드 | 최대 처리량 (지역성 무시) |
# cgroup v1에서 sched_relax_domain_level 설정
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.sched_relax_domain_level
# 스케줄러 도메인 정보 확인
$ cat /proc/schedstat | head -20
# CPU별 스케줄러 도메인 구조 확인
$ find /proc/sys/kernel/sched_domain/ -name "name" -exec sh -c 'echo "$1: $(cat $1)"' _ {} \;
/proc/sys/kernel/sched_domain/cpu0/domain0/name: SMT
/proc/sys/kernel/sched_domain/cpu0/domain1/name: MC
/proc/sys/kernel/sched_domain/cpu0/domain2/name: NUMA
스케줄러 도메인 변경 확인
# CPU별 스케줄러 도메인 확인 (격리 전)
$ for cpu in 0 4; do
echo "=== CPU $cpu ==="
for dom in /proc/sys/kernel/sched_domain/cpu$cpu/domain*; do
echo " $(basename $dom): name=$(cat $dom/name) flags=$(cat $dom/flags)"
done
done
# 격리 전 출력 예시:
# === CPU 0 ===
# domain0: name=SMT flags=4143
# domain1: name=MC flags=4655
# domain2: name=NUMA flags=6199
# === CPU 4 ===
# domain0: name=SMT flags=4143
# domain1: name=MC flags=4655
# domain2: name=NUMA flags=6199
# cpuset partition=isolated 설정 후:
# === CPU 0 ===
# domain0: name=SMT flags=4143 (그대로)
# domain1: name=MC flags=4655 (그대로)
# === CPU 4 ===
# (도메인 없음 — 완전 격리, 로드 밸런싱 불가)
# sched_debug로 상세 정보 확인
$ cat /proc/sched_debug | grep -A 5 "cpu#4"
로드 밸런싱 비활성화의 영향
- migration 스레드 중지 — 격리 CPU의 migration 커널 스레드(Kernel Thread)가 동작하지 않음
- wake_affine 비활성 — 깨어나는 태스크가 격리 CPU로 이동하지 않음
- IPI 감소 — 스케줄러 리밸런싱을 위한 IPI(Inter-Processor Interrupt)가 제거됨
- 태스크 고정 —
sched_setaffinity()또는 cpuset 프로세스 이동으로만 배치 가능
컨테이너 환경에서의 cpuset 활용
Docker와 Kubernetes는 cpuset을 활용하여 컨테이너에 전용 CPU를 할당합니다. 특히 Kubernetes의 Guaranteed QoS 클래스에서는 CPU Manager가 자동으로 cpuset을 설정하여 정수 단위 CPU를 독점 할당합니다.
컨테이너 cgroup 계층 구조
컨테이너 런타임(containerd, CRI-O)은 Pod/컨테이너마다 cgroup을 생성하고, cpuset 컨트롤러를 통해 CPU를 할당합니다. systemd 기반 cgroup 드라이버를 사용하는 경우, slice 계층으로 구성됩니다.
# 시스템 cgroup 계층 구조 확인
$ systemd-cgls --no-pager | head -30
Control group /:
├─init.scope
├─system.slice
│ ├─containerd.service
│ └─kubelet.service
└─kubepods.slice
├─kubepods-guaranteed.slice
│ ├─kubepods-guaranteed-podxxxx.slice
│ │ └─cri-containerd-xxxx.scope
│ └─...
├─kubepods-burstable.slice
└─kubepods-besteffort.slice
# 특정 컨테이너의 cpuset 확인
$ find /sys/fs/cgroup -name "cri-containerd-*" -exec sh -c '
echo "Container: $(basename $1)"
echo " cpuset.cpus: $(cat $1/cpuset.cpus 2>/dev/null)"
echo " cpuset.cpus.effective: $(cat $1/cpuset.cpus.effective 2>/dev/null)"
echo " cpuset.mems: $(cat $1/cpuset.mems 2>/dev/null)"
' _ {} \;
# CRI 런타임의 cpuset 설정 흐름:
# kubelet → CRI gRPC → containerd → runc → cgroup 파일 쓰기
# Guaranteed Pod (정수 CPU) → cpuset.cpus에 특정 CPU 할당
# Burstable/BestEffort Pod → 공유 CPU 풀의 cpuset.cpus 사용
Docker cpuset 설정
# Docker에서 cpuset 지정
$ docker run -d --cpuset-cpus="4-7" --cpuset-mems="1" --name rt-app my-rt-image
# cpuset 확인
$ docker inspect rt-app --format '{{.HostConfig.CpusetCpus}}'
4-7
# 컨테이너의 cgroup cpuset 파일 직접 확인
$ cat /sys/fs/cgroup/system.slice/docker-$(docker inspect rt-app --format '{{.Id}}').scope/cpuset.cpus
4-7
# docker-compose에서 cpuset 설정
# docker-compose.yml
# services:
# rt-service:
# image: my-rt-image
# cpuset: "4-7"
# deploy:
# resources:
# limits:
# cpus: '4'
Kubernetes CPU Manager
# kubelet CPU Manager 설정 (/var/lib/kubelet/config.yaml)
# cpuManagerPolicy: "static"
# cpuManagerReconcilePeriod: "10s"
# reservedSystemCPUs: "0-1"
# topologyManagerPolicy: "single-numa-node"
# Guaranteed QoS Pod 예시 (정수 CPU 요청 → cpuset 독점)
# apiVersion: v1
# kind: Pod
# metadata:
# name: rt-workload
# spec:
# containers:
# - name: worker
# image: my-rt-image
# resources:
# requests:
# cpu: "4" # 정수 → cpuset 독점
# memory: "8Gi"
# limits:
# cpu: "4" # requests == limits → Guaranteed
# memory: "8Gi"
# CPU Manager 상태 확인
$ cat /var/lib/kubelet/cpu_manager_state
{"policyName":"static","defaultCpuSet":"0-1,6-7",
"entries":{"pod-uid-1":{"container-A":"2-3"},
"pod-uid-2":{"container-B":"4-5"}}}
# Pod 내에서 실제 cpuset 확인
$ kubectl exec rt-workload -- cat /sys/fs/cgroup/cpuset.cpus.effective
2-5
topologyManagerPolicy: single-numa-node)와 CPU Manager(cpuManagerPolicy: static)를 함께 사용하세요. 이렇게 하면 Pod의 CPU와 메모리가 동일 NUMA 노드에 배치되어 최적 성능을 보장합니다.
cpuset 커널 내부 구현
cpuset 서브시스템의 핵심 구현은 kernel/cgroup/cpuset.c에 있습니다. 주요 함수인 cpuset_attach()와 update_cpumask()의 동작을 이해하면 cpuset이 태스크 마이그레이션과 스케줄러 도메인에 미치는 영향을 깊이 파악할 수 있습니다.
핵심 자료구조
/* kernel/cgroup/cpuset.c */
struct cpuset {
struct cgroup_subsys_state css;
/* 사용자가 설정한 CPU/메모리 마스크 */
cpumask_var_t cpus_allowed; /* cpuset.cpus */
nodemask_t mems_allowed; /* cpuset.mems */
/* 실제 유효 마스크 (부모와의 교집합) */
cpumask_var_t effective_cpus; /* cpuset.cpus.effective */
nodemask_t effective_mems; /* cpuset.mems.effective */
/* 하위 cpuset에 배포된 CPU (partition root용) */
cpumask_var_t subparts_cpus;
/* 플래그 */
int flags; /* CS_CPU_EXCLUSIVE 등 */
int pn; /* partition 번호 */
/*
* partition 상태:
* PRS_MEMBER — 부모 도메인에 포함 (기본)
* PRS_ROOT — 독립 스케줄링 도메인
* PRS_ISOLATED — root + 로드 밸런싱 비활성
*/
int partition_root_state;
/* 로드 밸런싱 제어 (v1) */
int relax_domain_level;
};
/* 주요 플래그 */
#define CS_CPU_EXCLUSIVE 0x01 /* CPU 독점 */
#define CS_MEM_EXCLUSIVE 0x02 /* 메모리 독점 */
#define CS_SCHED_LOAD_BALANCE 0x04 /* 로드 밸런싱 */
#define CS_SPREAD_PAGE 0x08 /* 페이지 분산 */
#define CS_SPREAD_SLAB 0x10 /* slab 분산 */
cpuset_attach() 태스크 이동 흐름
/* kernel/cgroup/cpuset.c — 태스크를 cpuset에 연결하는 핵심 함수 */
static void cpuset_attach(struct cgroup_taskset *tset)
{
struct task_struct *task;
struct cgroup_subsys_state *css;
struct cpuset *cs = css_cs(css);
struct cpuset *oldcs;
/* 잠금 획득 — cpuset 변경은 직렬화 */
percpu_down_write(&cpuset_rwsem);
cgroup_taskset_for_each(task, css, tset) {
oldcs = task_cs(task);
/* 1. CPU affinity 갱신 */
cpuset_change_task_nodemask(task, &cs->mems_allowed);
/* 2. 태스크의 cpus_mask를 새 cpuset으로 갱신 */
set_cpus_allowed_ptr(task, cs->effective_cpus);
/*
* 3. 태스크가 현재 허용되지 않는 CPU에서 실행 중이면
* 즉시 마이그레이션 트리거
*/
if (!cpumask_test_cpu(task_cpu(task), cs->effective_cpus))
do_migrate_task(task);
/* 4. NUMA 메모리 정책 갱신 */
cpuset_update_task_spread_flags(cs, task);
}
/*
* 5. 스케줄러 도메인 재구축이 필요한 경우
* (partition root 변경 등)
*/
if (need_rebuild_sched_domains())
rebuild_sched_domains_locked();
percpu_up_write(&cpuset_rwsem);
}
update_cpumask() CPU 마스크 갱신
/* cpuset.cpus 파일 쓰기 시 호출되는 핵심 경로 */
static int update_cpumask(struct cpuset *cs,
struct cpuset *trialcs,
const char *buf)
{
int retval;
/* 1. 문자열 파싱 → cpumask 변환 */
retval = cpulist_parse(buf, trialcs->cpus_allowed);
if (retval < 0)
return retval;
/* 2. 유효성 검사: 온라인 CPU인지 확인 */
cpumask_and(trialcs->cpus_allowed,
trialcs->cpus_allowed,
cpu_active_mask);
/* 3. 독점 모드 검사: 다른 cpuset과 중복 불가 */
if (is_cpu_exclusive(trialcs)) {
retval = validate_change(cs, trialcs);
if (retval)
return retval;
}
/* 4. 계층적 유효 마스크 갱신 */
update_cpumasks_hier(cs, &trialcs->cpus_allowed, 0);
/* 5. 스케줄러 도메인 재구축 */
rebuild_sched_domains_locked();
return 0;
}
/*
* update_cpumasks_hier() — 하위 cpuset에 재귀적으로
* effective_cpus를 갱신합니다.
*
* effective_cpus = cpus_allowed ∩ parent->effective_cpus
*
* 부모의 CPU가 줄어들면 자식의 effective도 자동 축소됩니다.
*/
static void update_cpumasks_hier(struct cpuset *cs,
struct cpumask *new_cpus,
int update_partition)
{
struct cpuset *cp;
struct cgroup_subsys_state *pos_css;
css_for_each_descendant_pre(pos_css, &cs->css) {
cp = css_cs(pos_css);
/* 부모와 교집합 → 유효 마스크 */
cpumask_and(cp->effective_cpus,
cp->cpus_allowed,
parent_cs(cp)->effective_cpus);
/* 이 cpuset에 속한 태스크의 affinity 갱신 */
update_tasks_cpumask(cp);
}
}
partition 상태 머신 구현
cpuset의 partition 상태는 update_parent_subparts_cpumask()에서 관리됩니다. partition root가 되려면 부모에서 충분한 CPU를 가져올 수 있어야 하며, 부모의 유효 CPU가 0이 되면 안 됩니다.
/* kernel/cgroup/cpuset.c — partition 상태 전이 로직 */
/*
* partition 상태:
* PRS_MEMBER (0) — 부모 도메인 내 일반 멤버
* PRS_ROOT (1) — 독립 sched domain (CPU 분리)
* PRS_ISOLATED (2) — root + 로드 밸런싱 비활성
* PRS_INVALID (-1) — 유효하지 않은 partition (에러 상태)
*/
static int update_parent_subparts_cpumask(
struct cpuset *cs,
int cmd,
struct cpumask *newmask,
struct tmpmasks *tmp)
{
struct cpuset *parent = parent_cs(cs);
int old_prs, new_prs;
/*
* 검증: partition root로 전환 시
* 부모에 최소 1개 CPU가 남아야 함
*/
if (cmd == partcmd_enable) {
cpumask_andnot(tmp->new_cpus,
parent->effective_cpus,
cs->cpus_allowed);
/* 부모의 남은 CPU가 없으면 실패 */
if (cpumask_empty(tmp->new_cpus))
return PERR_NOCPUS;
}
/*
* 부모의 subparts_cpus에 이 cpuset의 CPU 추가
* → 부모의 effective_cpus에서 해당 CPU 제거
* → 이 cpuset이 독립 스케줄링 도메인 획득
*/
cpumask_or(parent->subparts_cpus,
parent->subparts_cpus,
cs->effective_cpus);
cpumask_andnot(parent->effective_cpus,
parent->effective_cpus,
cs->effective_cpus);
/* 스케줄러 도메인 재구축 요청 */
set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
notify_partition_change(cs, old_prs, new_prs);
return 0;
}
cgroup 서브시스템 콜백
/* kernel/cgroup/cpuset.c — cpuset cgroup 서브시스템 등록 */
struct cgroup_subsys cpuset_cgrp_subsys = {
.css_alloc = cpuset_css_alloc,
.css_online = cpuset_css_online,
.css_offline = cpuset_css_offline,
.css_free = cpuset_css_free,
.can_attach = cpuset_can_attach,
.cancel_attach = cpuset_cancel_attach,
.attach = cpuset_attach, /* 태스크 이동 */
.post_attach = cpuset_post_attach,
.bind = cpuset_bind,
.fork = cpuset_fork, /* 새 태스크 생성 시 */
.legacy_cftypes = cpuset1_files, /* v1 인터페이스 */
.dfl_cftypes = dfl_files, /* v2 인터페이스 */
.early_init = true,
.threaded = true, /* 스레드 모드 지원 */
};
/*
* can_attach(): 태스크를 이 cpuset으로 이동할 수 있는지 검증
* - effective_cpus가 비어있으면 ENOSPC 반환
* - 스레드 모드에서 TID 이동 검증
*/
static int cpuset_can_attach(struct cgroup_taskset *tset)
{
struct cgroup_subsys_state *css;
struct cpuset *cs = css_cs(css);
struct task_struct *task;
percpu_down_write(&cpuset_rwsem);
/* cpuset에 유효 CPU가 없으면 태스크 이동 불가 */
if (cpumask_empty(cs->effective_cpus)) {
percpu_up_write(&cpuset_rwsem);
return -ENOSPC;
}
cgroup_taskset_for_each(task, css, tset) {
/* kthread는 특정 CPU에 바인딩되었을 수 있음 */
if (task->flags & PF_NO_SETAFFINITY) {
percpu_up_write(&cpuset_rwsem);
return -EINVAL;
}
}
percpu_up_write(&cpuset_rwsem);
return 0;
}
kernel/cgroup/cpuset.c에 있습니다. partition 관련 로직은 update_parent_subparts_cpumask()와 update_partition_sd_lb()를 참조하세요. 스케줄러 도메인 재구축은 kernel/sched/topology.c의 build_sched_domains()에서 처리합니다.
ftrace/bpftrace로 cpuset 동작 추적
cpuset 설정 변경이 실제로 어떤 커널 경로를 거치는지 확인하려면 ftrace와 bpftrace를 활용합니다. 특히 태스크 마이그레이션, 스케줄러 도메인 재구축 시점, CPU mask 변경을 실시간으로 관찰할 수 있습니다.
ftrace를 사용한 cpuset 이벤트 추적
# 1. cpuset 관련 ftrace 이벤트 확인
$ ls /sys/kernel/debug/tracing/events/cgroup/
cgroup_attach_task cgroup_destroy_root cgroup_mkdir
cgroup_release cgroup_remount cgroup_rmdir
cgroup_setup_root cgroup_transfer_tasks
# 2. cpuset 태스크 연결 이벤트 추적
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/cgroup/cgroup_attach_task/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예시:
# bash-1234 [002] .... 12345.678: cgroup_attach_task:
# dst_root=1 dst_id=15 dst_level=2 dst_path=/isolated pid=5678 comm=my_app
# 3. sched_migrate_task 이벤트로 실제 마이그레이션 확인
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/sched/sched_migrate_task/enable
# 4. function_graph로 cpuset_attach 내부 호출 추적
$ echo "cpuset_attach" | sudo tee /sys/kernel/debug/tracing/set_graph_function
$ echo "function_graph" | sudo tee /sys/kernel/debug/tracing/current_tracer
$ cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예시:
# 0) | cpuset_attach() {
# 0) 0.850 us | cpuset_change_task_nodemask();
# 0) | set_cpus_allowed_ptr() {
# 0) 1.200 us | __set_cpus_allowed_ptr_locked();
# 0) 1.500 us | }
# 0) | rebuild_sched_domains_locked() {
# 0) 45.000 us | build_sched_domains();
# 0) 48.000 us | }
# 0) 52.000 us | }
# 5. 추적 정리
$ echo 0 | sudo tee /sys/kernel/debug/tracing/events/cgroup/cgroup_attach_task/enable
$ echo "nop" | sudo tee /sys/kernel/debug/tracing/current_tracer
bpftrace를 사용한 고급 추적
# 1. cpuset CPU 마스크 변경 추적
$ sudo bpftrace -e '
kprobe:update_cpumask {
printf("PID %d (%s) updating cpumask\n",
pid, comm);
}
kprobe:rebuild_sched_domains_locked {
printf("=== Sched domains rebuild at %llu ===\n",
nsecs);
print(kstack);
}
'
# 2. 태스크 마이그레이션 지연 측정
$ sudo bpftrace -e '
tracepoint:sched:sched_migrate_task {
printf("Task %s (PID %d): CPU %d -> CPU %d\n",
args->comm, args->pid,
args->orig_cpu, args->dest_cpu);
}
'
# 3. cpuset_attach 실행 시간 측정
$ sudo bpftrace -e '
kprobe:cpuset_attach { @start[tid] = nsecs; }
kretprobe:cpuset_attach {
$dur = nsecs - @start[tid];
printf("cpuset_attach took %d us\n", $dur / 1000);
@hist = hist($dur / 1000);
delete(@start[tid]);
}
END { print(@hist); }
'
# 4. cpuset cgroup 파일 쓰기 추적
$ sudo bpftrace -e '
kprobe:cpuset_write_resmask {
printf("cpuset write by %s (PID %d): %s\n",
comm, pid, str(arg1));
}
'
# 5. 격리 CPU의 인터럽트 빈도 모니터링
$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry /cpu == 4/ {
@[args->name] = count();
}
interval:s:10 { print(@); clear(@); }
'
bpftrace 고급 스크립트 예제
# cpuset 변경에 의한 sched domain 재구축 비용 분석
$ sudo bpftrace -e '
kprobe:rebuild_sched_domains_locked {
@start[tid] = nsecs;
@stack[tid] = kstack;
}
kretprobe:rebuild_sched_domains_locked {
$dur = nsecs - @start[tid];
printf("sched domain rebuild: %d us (by %s PID %d)\n",
$dur / 1000, comm, pid);
if ($dur > 100000) {
printf(" WARNING: slow rebuild!\n");
print(@stack[tid]);
}
@latency = hist($dur / 1000);
delete(@start[tid]);
delete(@stack[tid]);
}
END {
printf("\n=== Rebuild latency histogram (us) ===\n");
print(@latency);
}
'
# 격리 CPU의 잔여 간섭 소스 종합 분석
$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry /cpu >= 4 && cpu <= 7/ {
@irq[cpu, args->name] = count();
}
tracepoint:irq:softirq_entry /cpu >= 4 && cpu <= 7/ {
@softirq[cpu, args->vec] = count();
}
tracepoint:sched:sched_switch /cpu >= 4 && cpu <= 7/ {
@switches[cpu] = count();
}
tracepoint:workqueue:workqueue_execute_start /cpu >= 4 && cpu <= 7/ {
@wq[cpu] = count();
}
interval:s:30 {
printf("\n=== 30초 간 격리 CPU 간섭 보고서 ===\n");
printf("--- IRQ ---\n"); print(@irq);
printf("--- SoftIRQ ---\n"); print(@softirq);
printf("--- Context Switches ---\n"); print(@switches);
printf("--- Workqueue ---\n"); print(@wq);
clear(@irq); clear(@softirq);
clear(@switches); clear(@wq);
}
'
perf를 사용한 cpuset 이벤트 기록
# cpuset 관련 이벤트를 perf로 기록
$ sudo perf record -e 'cgroup:*' -e 'sched:sched_migrate_task' \
-a -- sleep 30
# 기록 분석
$ sudo perf script | grep -E "cgroup_attach|sched_migrate"
# 특정 CPU에서 실행된 태스크 프로파일링
$ sudo perf stat -C 4-7 -e context-switches,cpu-migrations,page-faults \
-- sleep 10
Performance counter stats for 'CPU(s) 4-7':
3 context-switches
0 cpu-migrations # 격리 시 0이어야 함
0 page-faults
성능 비교: cpuset vs taskset vs cgroup cpu.max
CPU 제어에는 여러 메커니즘이 있으며, 각각 목적과 동작 방식이 다릅니다. 격리 수준에 따른 지터(jitter) 감소 효과를 이해하면 워크로드에 적합한 방식을 선택할 수 있습니다.
| 항목 | cpuset (cgroup) | taskset / sched_setaffinity | cpu.max (cgroup v2) |
|---|---|---|---|
| 제어 대상 | 프로세스 그룹 | 개별 프로세스/스레드 | 프로세스 그룹 |
| 제어 방식 | CPU 집합 제한 (배치) | CPU affinity 비트마스크 | 시간 할당량 쓰로틀링 |
| CPU 독점 | 지원 (partition=root) | 불가 (다른 프로세스도 실행 가능) | 불가 |
| NUMA 메모리 | cpuset.mems로 제어 | 별도 numactl 필요 | 미지원 |
| 스케줄러 도메인 | 도메인 재구축 (격리) | 변경 없음 | 변경 없음 |
| 로드 밸런싱 | 비활성화 가능 | 정상 동작 | 정상 동작 |
| 지터 감소 | 높음 (도메인 격리) | 낮음 (밸런싱 간섭) | 없음 (쓰로틀링만) |
| 계층적 관리 | 지원 (cgroup 트리) | 미지원 | 지원 (cgroup 트리) |
| 런타임 변경 | 가능 (파일 쓰기) | 가능 (시스템 콜) | 가능 (파일 쓰기) |
| 컨테이너 통합 | Docker/K8s 기본 지원 | 수동 설정 필요 | Docker/K8s 기본 지원 |
| 권장 사용 시나리오 | HPC, 실시간, 컨테이너 격리 | 단일 프로세스 고정 | 멀티테넌트 공정 분배 |
성능 벤치마크 예제
#!/bin/bash
# CPU 제어 방식별 지터 측정 스크립트
ISOLATED_CPU=4
ITERATIONS=1000000
echo "===== 1. 기본 (격리 없음) ====="
$ cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1234) P:99 I:1000 C:1000000 Min: 1 Act: 3 Avg: 4 Max: 85
echo "===== 2. taskset만 사용 ====="
$ taskset -c $ISOLATED_CPU cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1235) P:99 I:1000 C:1000000 Min: 1 Act: 2 Avg: 3 Max: 42
echo "===== 3. cpuset 격리 (partition=root) ====="
sudo mkdir -p /sys/fs/cgroup/bench
echo "$ISOLATED_CPU" | sudo tee /sys/fs/cgroup/bench/cpuset.cpus
echo "0" | sudo tee /sys/fs/cgroup/bench/cpuset.mems
echo "root" | sudo tee /sys/fs/cgroup/bench/cpuset.cpus.partition
echo $$ | sudo tee /sys/fs/cgroup/bench/cgroup.procs
$ cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1236) P:99 I:1000 C:1000000 Min: 1 Act: 1 Avg: 2 Max: 12
echo "===== 4. cpuset + isolcpus + nohz_full + rcu_nocbs ====="
# (부팅 파라미터: isolcpus=4 nohz_full=4 rcu_nocbs=4)
$ cyclictest -m -n -p 99 -a $ISOLATED_CPU -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1237) P:99 I:1000 C:1000000 Min: 1 Act: 1 Avg: 1 Max: 4
벤치마크 결과 요약
| 격리 수준 | Min (us) | Avg (us) | Max (us) | 비고 |
|---|---|---|---|---|
| 격리 없음 | 1 | 4 | 85 | 스케줄러 노이즈, IRQ 간섭 |
| taskset만 | 1 | 3 | 42 | CPU 고정, 밸런싱 간섭 잔존 |
| cpuset (partition=root) | 1 | 2 | 12 | 도메인 격리, IRQ 일부 잔존 |
| cpuset + isolcpus + nohz_full | 1 | 1 | 4 | 완전 격리, 최소 지터 |
cyclictest는 rt-tests 패키지에 포함된 실시간 지연 측정 도구입니다. sudo apt install rt-tests로 설치할 수 있습니다. Max 값이 낮을수록 격리가 효과적입니다.
cpu.max (CFS 대역폭(Bandwidth)) vs cpuset 비교
cpu.max는 CFS 대역폭 제어(CFS Bandwidth Control)를 사용하여 시간 기반으로 CPU 사용을 제한합니다. cpuset과는 근본적으로 다른 접근 방식으로, 상호 보완적으로 사용할 수 있습니다.
# cpu.max 설정 (CFS 대역폭 제한)
# 형식: $MAX $PERIOD (마이크로초)
# 200000 100000 → 100ms 기간 중 200ms CPU 시간 = 2 CPU 상당
$ echo "200000 100000" | sudo tee /sys/fs/cgroup/app/cpu.max
# cpuset + cpu.max 조합 사용
# cpuset으로 CPU 4-7에 배치, cpu.max로 최대 3 CPU 사용 제한
$ echo "4-7" | sudo tee /sys/fs/cgroup/app/cpuset.cpus
$ echo "300000 100000" | sudo tee /sys/fs/cgroup/app/cpu.max
# 쓰로틀링 발생 여부 확인
$ cat /sys/fs/cgroup/app/cpu.stat
usage_usec 8765432
user_usec 7654321
system_usec 1111111
nr_periods 12345
nr_throttled 42 # 쓰로틀링 발생 횟수
throttled_usec 98765 # 쓰로틀링된 총 시간
| 특성 | cpuset | cpu.max (CFS BW) |
|---|---|---|
| 제어 방식 | CPU 배치 (어디서 실행) | 시간 할당 (얼마나 실행) |
| 격리 효과 | 높음 (물리 CPU 분리) | 없음 (같은 CPU 공유) |
| 유휴 CPU 낭비 | 다른 그룹이 사용 불가 | 다른 그룹이 사용 가능 |
| 쓰로틀링 | 없음 | 한도 초과 시 발생 |
| 지터 영향 | 감소 (독점) | 증가 가능 (쓰로틀링) |
실전 시나리오별 권장 설정
| 시나리오 | 격리 방식 | 설정 예 |
|---|---|---|
| 금융 거래 (초저지연) | cpuset + isolcpus + nohz_full + rcu_nocbs | Max 지터 < 5us |
| 5G 기지국 제어 | cpuset partition=isolated + PREEMPT_RT | 결정적 지연 보장 |
| 게임 서버 | cpuset (partition=root) | 안정적 FPS 유지 |
| 웹 서버 (멀티테넌트) | cpu.max + cpuset (공유 풀) | 공정 분배 + 격리 |
| CI/CD 빌드 | cpu.max | 자원 공정 분배 |
| 과학 시뮬레이션 (HPC) | cpuset + NUMA 정렬 | 메모리 대역폭 최적화 |
자동화 격리 스크립트
#!/bin/bash
# cpuset_isolate.sh — 완전 CPU 격리 자동 설정 스크립트
# 사용법: sudo ./cpuset_isolate.sh <CPU_LIST> <NUMA_NODE> <CGROUP_NAME>
CPU_LIST="${1:-4-7}"
NUMA_NODE="${2:-1}"
CGROUP_NAME="${3:-isolated}"
CGROUP_PATH="/sys/fs/cgroup/${CGROUP_NAME}"
set -e
echo "[1/6] cgroup v2 확인..."
if ! mount | grep -q cgroup2; then
echo "오류: cgroup v2가 마운트되지 않았습니다."
exit 1
fi
echo "[2/6] cpuset 컨트롤러 활성화..."
echo "+cpuset" | tee /sys/fs/cgroup/cgroup.subtree_control
echo "[3/6] cgroup 생성: ${CGROUP_NAME}..."
mkdir -p "${CGROUP_PATH}"
echo "${CPU_LIST}" | tee "${CGROUP_PATH}/cpuset.cpus"
echo "${NUMA_NODE}" | tee "${CGROUP_PATH}/cpuset.mems"
echo "[4/6] partition 모드 설정..."
echo "isolated" | tee "${CGROUP_PATH}/cpuset.cpus.partition"
echo "[5/6] IRQ affinity 재설정..."
# 격리 CPU에서 IRQ 제거 (housekeeping CPU로 이동)
HOUSEKEEPING_MASK=$(python3 -c "
import os
isolated = set()
for part in '${CPU_LIST}'.split(','):
if '-' in part:
a, b = part.split('-')
isolated.update(range(int(a), int(b)+1))
else:
isolated.add(int(part))
ncpus = os.cpu_count()
hk = set(range(ncpus)) - isolated
mask = sum(1 << c for c in hk)
print(f'{mask:x}')
")
for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
echo "${HOUSEKEEPING_MASK}" > /proc/irq/$irq/smp_affinity 2>/dev/null || true
done
echo "[6/6] 검증..."
echo " cpuset.cpus.effective: $(cat ${CGROUP_PATH}/cpuset.cpus.effective)"
echo " cpuset.cpus.partition: $(cat ${CGROUP_PATH}/cpuset.cpus.partition)"
echo " cpuset.mems.effective: $(cat ${CGROUP_PATH}/cpuset.mems.effective)"
echo
echo "사용법: echo \$PID > ${CGROUP_PATH}/cgroup.procs"
echo "완료!"
cpuset vs numactl 비교
numactl은 프로세스 수준의 NUMA 메모리 정책(Memory Policy)을 설정하는 도구입니다. cpuset과 함께 사용할 때의 상호작용을 이해해야 합니다.
# numactl: 프로세스 수준 NUMA 제어
$ numactl --cpunodebind=1 --membind=1 ./my_app
# cpuset: cgroup 수준 NUMA 제어 (더 강력)
$ echo "4-7" | sudo tee /sys/fs/cgroup/app/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/app/cpuset.mems
# 주의: cpuset 제한이 numactl보다 우선
# cpuset.mems=0인 cgroup에서 numactl --membind=1을 실행하면
# cpuset 제한에 의해 노드 0에서만 할당됩니다.
# 조합 확인
$ numastat -p $(pgrep my_app)
Per-node process memory usage (in MBs) for PID 5678 (my_app)
Node 0 Node 1 Total
--------------- --------------- ---------------
Huge 0.00 0.00 0.00
Heap 0.00 128.50 128.50
Stack 0.00 0.25 0.25
cpufreq/cpupower와의 통합
격리 CPU의 주파수를 고정하면 동적 주파수 스케일링(DVFS)에 의한 추가 지터를 방지할 수 있습니다.
# 격리 CPU의 governor를 performance로 설정
$ for cpu in 4 5 6 7; do
echo "performance" | sudo tee /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor
done
# C-state 비활성화 (idle 진입 방지 → 깨어나는 지연 제거)
$ for cpu in 4 5 6 7; do
echo 0 | sudo tee /sys/devices/system/cpu/cpu$cpu/cpuidle/state*/disable
echo 1 | sudo tee /sys/devices/system/cpu/cpu$cpu/cpuidle/state[1-9]*/disable
done
# 또는 커널 파라미터로 전역 설정
# processor.max_cstate=1 intel_idle.max_cstate=0
# 터보 부스트 비활성화 (주파수 변동 제거)
$ echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo
# 현재 주파수 확인
$ cpupower -c 4-7 frequency-info | grep "current CPU frequency"
performance governor와 C-state 비활성화는 전력 소비를 크게 증가시킵니다. 서버 환경에서만 적용하고, 열 관리(thermal throttling)에 주의하세요. CPUFreq, CPUIdle 문서를 참고하세요.
SMT(하이퍼스레딩) 고려사항
SMT(Simultaneous Multi-Threading) 환경에서는 하이퍼스레드 쌍이 캐시와 실행 유닛을 공유합니다. 격리 효과를 극대화하려면 SMT 쌍을 함께 격리하거나 SMT를 비활성화해야 합니다.
# SMT 토폴로지 확인
$ lscpu -e=CPU,CORE,SOCKET,NODE
CPU CORE SOCKET NODE
0 0 0 0
1 1 0 0
2 2 0 0
3 3 0 0
4 0 0 0 # CPU4 = CPU0의 하이퍼스레드 쌍
5 1 0 0 # CPU5 = CPU1의 하이퍼스레드 쌍
6 2 0 0
7 3 0 0
# 하이퍼스레드 쌍 확인
$ cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
0,4
# 권장: 물리 코어 단위로 cpuset 구성
# 코어 2,3 격리 → CPU 2,3,6,7 (SMT 쌍 포함)
$ echo "2-3,6-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
# 또는 SMT 비활성화 (최고 수준 격리)
$ echo off | sudo tee /sys/devices/system/cpu/smt/control
# 커널 파라미터로 영구 비활성화
# nosmt 또는 mitigations=auto,nosmt
문제 해결과 디버깅(Debugging)
cpuset 설정에서 자주 발생하는 문제와 해결 방법을 정리합니다. 잘못된 설정은 태스크 실행 불가, 성능 저하, OOM 등 심각한 결과를 초래할 수 있습니다.
자주 발생하는 문제와 해결
# === 문제 1: 태스크를 cgroup에 추가할 수 없음 (EINVAL) ===
$ echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs
bash: echo: write error: Invalid argument
# 원인: cpuset.cpus 또는 cpuset.mems가 비어있음
$ cat /sys/fs/cgroup/mygroup/cpuset.cpus
# (비어있음)
# 해결: CPU와 메모리 노드를 먼저 설정
$ echo "0-7" | sudo tee /sys/fs/cgroup/mygroup/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/mygroup/cpuset.mems
$ echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs # 성공
# === 문제 2: partition이 invalid로 전이됨 ===
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.partition
root (invalid)
# 원인: 부모에서 해당 CPU를 더 이상 제공할 수 없음
$ cat /sys/fs/cgroup/cpuset.cpus.effective
0-7
# 다른 형제 cpuset이 같은 CPU를 이미 partition root로 가져감
# 해결: 형제 cpuset의 CPU 배분을 확인하고 겹치지 않게 조정
$ cat /sys/fs/cgroup/sibling1/cpuset.cpus
4-7 # 이미 같은 CPU 사용 중!
# === 문제 3: 격리 CPU에서 예상치 못한 인터럽트 ===
$ cat /proc/interrupts | awk '{print $1, $6}' # CPU4 열 확인
# 원인: managed IRQ가 여전히 격리 CPU에 배정됨
# 해결: isolcpus에 managed_irq 플래그 추가
# isolcpus=domain,managed_irq,4-7
# === 문제 4: cpuset.cpus.effective가 설정값과 다름 ===
$ echo "0-7" | sudo tee /sys/fs/cgroup/child/cpuset.cpus
$ cat /sys/fs/cgroup/child/cpuset.cpus.effective
0-3 # 설정한 0-7이 아님!
# 원인: effective = cpus_allowed ∩ parent.effective_cpus
$ cat /sys/fs/cgroup/cpuset.cpus.effective
0-3 # 부모가 0-3만 가지고 있음
# 해결: 부모의 cpuset.cpus를 먼저 확장
로드 불균형 디버깅
# CPU별 실행 큐 길이 확인
$ cat /proc/schedstat | awk '/^cpu/ {print $1, "runqueue:", $3}'
cpu0 runqueue: 15423
cpu1 runqueue: 14892
cpu2 runqueue: 15001
cpu3 runqueue: 14756
cpu4 runqueue: 1 # 격리 CPU — 단일 태스크만
cpu5 runqueue: 0
cpu6 runqueue: 0
cpu7 runqueue: 0
# 각 CPU에서 실행 중인 태스크 확인
$ ps -eo psr,pid,comm --sort=psr | awk '$1 >= 4'
4 5678 my_rt_app # 의도된 태스크
4 12 rcuo/4 # ← 이 스레드가 있으면 rcu_nocbs 미적용
# 스케줄러 도메인 플래그 확인
$ cat /proc/sys/kernel/sched_domain/cpu4/domain0/flags
0 # SD_LOAD_BALANCE 없으면 정상 격리
# 격리 CPU의 컨텍스트 스위치 카운트 (최소여야 함)
$ sar -w -P 4 1 5
12:00:01 CPU cswch/s
12:00:02 4 0.00 # 컨텍스트 스위치 없음 → 정상 격리
문제 해결 체크리스트
cat /sys/fs/cgroup/<name>/cpuset.cpus.effective— 의도한 CPU가 유효한지 확인cat /sys/fs/cgroup/<name>/cpuset.cpus.partition— partition 상태가 invalid가 아닌지 확인cat /proc/interrupts— 격리 CPU의 IRQ 카운트가 증가하지 않는지 확인ps -eo psr,pid,comm— 격리 CPU에 의도하지 않은 태스크가 없는지 확인cat /proc/softirqs— 격리 CPU의 softirq 카운트 확인perf stat -C <cpu> -e context-switches— 컨텍스트 스위치가 최소인지 확인cat /proc/sys/kernel/sched_domain/cpu<N>/domain0/flags— 스케줄러 도메인 플래그 확인numactl --hardware— CPU와 NUMA 메모리 노드가 올바르게 짝지어졌는지 확인
커널 로그 분석
cpuset 관련 문제는 dmesg와 커널 로그에서 진단할 수 있습니다. 특히 partition 상태 전이와 CPU 핫플러그(Hotplug) 이벤트에 주의하세요.
# cpuset 관련 커널 메시지 확인
$ dmesg | grep -i "cpuset\|sched_domain\|isolcpus\|nohz_full"
# 일반적 메시지 예시:
# [ 0.000000] Command line: ... isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7
# [ 0.123456] sched_isolation: housekeeping mask: 0-3
# [ 0.234567] NO_HZ: Full dynticks CPUs: 4-7
# [ 0.345678] rcu: Offloading RCU callbacks from CPUs: 4-7
# CPU 핫플러그 이벤트 (cpuset에 영향)
$ dmesg | grep -i "cpu.*offline\|cpu.*online"
# partition 상태 전이 실패 시 커널 경고
# [ 100.123] cpuset: partition root ... has empty effective_cpus
# [ 100.124] cpuset: ... forced to be invalid partition
# OOM 발생 시 cpuset 범위 확인
$ dmesg | grep -A 20 "Out of memory"
# ... cpuset=isolated mems_allowed=1 ...
에러 메시지 레퍼런스
| 에러 | 발생 상황 | 해결 방법 |
|---|---|---|
EINVAL (cgroup.procs 쓰기) |
cpuset.cpus 또는 cpuset.mems 미설정 | 먼저 cpuset.cpus와 cpuset.mems를 설정 |
ENOSPC (cgroup.procs 쓰기) |
cpuset.cpus.effective가 비어있음 | 부모 cpuset의 CPU 범위 확인 |
EINVAL (partition 쓰기) |
partition=root로 전환 시 부모 CPU 부족 | 형제 cpuset의 CPU 중복 확인 |
EBUSY (cpuset.cpus 쓰기) |
독점 CPU가 다른 cpuset에서 사용 중 | 다른 cpuset의 cpu_exclusive 확인 |
EPERM (cgroup.procs 쓰기) |
권한 부족 (CAP_SYS_ADMIN 필요) | root 또는 delegation 설정 확인 |
| partition → "root invalid" | 부모에서 CPU를 제공할 수 없게 됨 | CPU 재분배 후 partition 재설정 |
| OOM within cpuset | cpuset.mems 노드의 메모리 소진 | cpuset.mems 확장 또는 메모리 제한 조정 |
systemd와 cpuset 통합
# systemd 서비스에서 cpuset 설정 (유닛 파일)
# /etc/systemd/system/my-rt-app.service
# [Service]
# Type=simple
# ExecStart=/opt/my-rt-app
# AllowedCPUs=4-7 # cpuset.cpus 설정 (systemd v244+)
# AllowedMemoryNodes=1 # cpuset.mems 설정
# CPUSchedulingPolicy=fifo
# CPUSchedulingPriority=99
# MemoryDenyWriteExecute=yes
# LockPersonality=yes
# systemd-run으로 즉시 cpuset 지정 실행
$ sudo systemd-run --scope \
-p AllowedCPUs=4-7 \
-p AllowedMemoryNodes=1 \
./my_app
# 현재 서비스의 cpuset 확인
$ systemctl show my-rt-app.service --property=AllowedCPUs
AllowedCPUs=4-7
# 런타임에 cpuset 변경
$ sudo systemctl set-property my-rt-app.service AllowedCPUs=4-5
CPU 핫플러그와 cpuset 상호작용
CPU가 오프라인되면 해당 CPU를 포함하는 cpuset의 effective_cpus가 자동으로 갱신됩니다. 모든 CPU가 오프라인되면 cpuset의 태스크가 실행 불가 상태가 될 수 있습니다.
# CPU 오프라인
$ echo 0 | sudo tee /sys/devices/system/cpu/cpu5/online
# cpuset effective에서 자동 제거됨
$ cat /sys/fs/cgroup/isolated/cpuset.cpus
4-7 # 설정값은 유지
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4,6-7 # CPU5 제외됨
# CPU 다시 온라인
$ echo 1 | sudo tee /sys/devices/system/cpu/cpu5/online
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4-7 # 자동 복구
# 위험: 모든 격리 CPU가 오프라인되면?
# → 태스크가 루트 cpuset의 CPU로 대피 (fallback)
# → 커널 로그에 경고 메시지 출력
완전 격리 레시피
# ====== 부팅 시 설정 (GRUB) ======
# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 \
nohz_full=4-7 \
rcu_nocbs=4-7 \
irqaffinity=0-3 \
processor.max_cstate=1 \
intel_idle.max_cstate=0 \
nosmt \
transparent_hugepage=never \
skew_tick=1"
# ====== 부팅 후 런타임 설정 ======
# 1. cpuset cgroup 설정
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems
$ echo "isolated" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition
# 2. CPU governor 고정
$ for c in 4 5 6 7; do
echo performance | sudo tee /sys/devices/system/cpu/cpu$c/cpufreq/scaling_governor
done
# 3. 터보 부스트 비활성화
$ echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null || \
echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost 2>/dev/null
# 4. IRQ affinity 재확인
$ for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done
# 5. irqbalance 격리 CPU 제외
$ sudo systemctl stop irqbalance
echo 'IRQBALANCE_BANNED_CPULIST=4-7' | sudo tee /etc/default/irqbalance
$ sudo systemctl start irqbalance
# 6. 커널 스레드 housekeeping CPU로 이동
$ for pid in $(pgrep -f "\[.*\]"); do
taskset -cp 0-3 $pid 2>/dev/null || true
done
# 7. 워크로드 실행
$ echo $$ | sudo tee /sys/fs/cgroup/isolated/cgroup.procs
$ exec chrt -f 99 ./my_rt_application
# 8. 격리 상태 최종 검증
$ echo "=== Isolation Status ==="
$ echo "Isolated CPUs: $(cat /sys/devices/system/cpu/isolated)"
$ echo "nohz_full CPUs: $(cat /sys/devices/system/cpu/nohz_full)"
$ echo "Effective CPUs: $(cat /sys/fs/cgroup/isolated/cpuset.cpus.effective)"
$ echo "Partition state: $(cat /sys/fs/cgroup/isolated/cpuset.cpus.partition)"
$ sudo perf stat -C 4-7 -e context-switches,cpu-migrations -- sleep 5
흔한 실수와 주의사항
cpuset.mems 미설정
echo $$ > cgroup.procs에서 ENOSPC 또는 프로세스가 OOM으로 즉시 종료됩니다.
cpuset.mems가 비어 있으면 해당 cpuset의 태스크는 어떤 NUMA 노드에서도 메모리를 할당할 수 없습니다. cgroup v2에서는 cpuset.cpus와 cpuset.mems 모두 설정해야 태스크 이동이 가능합니다.
# 잘못된 설정 — mems 누락
$ sudo mkdir /sys/fs/cgroup/myapp
$ echo "4-7" | sudo tee /sys/fs/cgroup/myapp/cpuset.cpus
$ echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs
# → write error: No space left on device
# 올바른 설정 — mems도 반드시 지정
$ echo "0" | sudo tee /sys/fs/cgroup/myapp/cpuset.mems
$ echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs # 성공
모든 CPU 격리
# 절대 금지 — CPU를 전부 격리하면 안 됨
GRUB_CMDLINE_LINUX="isolcpus=0-7" # 8코어 전부 격리 → 시스템 마비
# 반드시 housekeeping CPU 최소 1~2개 확보
GRUB_CMDLINE_LINUX="isolcpus=2-7" # CPU 0-1은 housekeeping
IRQ affinity 미설정으로 인한 지터
isolcpus + cpuset을 설정했는데도 격리 CPU에서 간헐적 지터가 발생합니다. /proc/interrupts에서 격리 CPU 컬럼의 숫자가 증가합니다.
isolcpus는 스케줄러에서만 CPU를 제외합니다. IRQ는 별도로 affinity를 설정하지 않으면 여전히 격리 CPU로 전달될 수 있습니다. 특히 managed_irq 플래그 없이 isolcpus만 사용하면 MSI-X 인터럽트가 격리 CPU에 도달합니다.
# 불완전한 격리 (IRQ 누수 가능)
GRUB_CMDLINE_LINUX="isolcpus=4-7"
# 완전한 격리 (managed IRQ도 차단)
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7"
# 런타임에서도 IRQ affinity 강제 설정
$ for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
echo "0-3" | sudo tee /proc/irq/$irq/smp_affinity_list 2>/dev/null
done
NUMA 교차 배치
perf stat에서 node-loads가 원격(Remote) 노드를 포함합니다.
cpuset.cpus에 NUMA 노드 0의 CPU를 설정하면서 cpuset.mems에 노드 1을 설정하면, 모든 메모리 접근이 인터커넥트(Interconnect)를 경유하여 지연이 2~3배 증가합니다.
# 잘못된 NUMA 배치 (교차)
$ echo "0-3" | sudo tee .../cpuset.cpus # NUMA 노드 0의 CPU
$ echo "1" | sudo tee .../cpuset.mems # NUMA 노드 1의 메모리 → 원격 접근!
# 올바른 NUMA 배치 (동일 노드)
$ echo "0-3" | sudo tee .../cpuset.cpus # NUMA 노드 0의 CPU
$ echo "0" | sudo tee .../cpuset.mems # NUMA 노드 0의 메모리 → 로컬 접근
# NUMA 토폴로지 확인
$ lscpu | grep "NUMA"
NUMA node0 CPU(s): 0-3
NUMA node1 CPU(s): 4-7
SMT 시블링(Sibling) 미격리
# SMT 시블링 확인
$ cat /sys/devices/system/cpu/cpu4/topology/thread_siblings_list
4,12 # CPU 4와 CPU 12는 동일 물리 코어
# 시블링도 함께 격리해야 캐시 간섭 제거
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7,12-15"
# 또는 SMT 자체를 비활성화 (가장 확실한 방법)
GRUB_CMDLINE_LINUX="nosmt isolcpus=domain,managed_irq,4-7"
# 런타임에서 SMT 비활성화
$ echo off | sudo tee /sys/devices/system/cpu/smt/control
cpuset.cpus.partition 상태 "invalid"
cpuset.cpus.partition에 "root" 또는 "isolated"를 쓰면 "root invalid" 또는 "isolated invalid"가 표시됩니다.
partition을 설정하려면 해당 cpuset의 CPU가 부모의 다른 자식 cpuset과 겹치지 않아야 하며, 부모의 cpuset.cpus.effective에서 해당 CPU를 빼도 빈 집합이 되지 않아야 합니다.
# partition invalid가 되는 경우
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.partition
root invalid # 부모에 다른 자식이 같은 CPU 사용 중
# 진단
$ cat /sys/fs/cgroup/cgroup.subtree_control # cpuset 활성화 확인
$ cat /sys/fs/cgroup/cpuset.cpus.effective # 부모 유효 CPU 확인
$ ls /sys/fs/cgroup/*/cpuset.cpus # 다른 자식 cpuset CPU 확인
# 해결: 부모의 CPU를 정확히 분할
$ echo "0-3" | sudo tee /sys/fs/cgroup/system/cpuset.cpus # 시스템용
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus # 격리용 (겹침 없음)
$ echo "root" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition # 성공
실전 활용 예제
DPDK 네트워크 패킷 처리
DPDK(Data Plane Development Kit)는 커널을 우회하여 사용자 공간에서 직접 NIC 패킷을 처리하며, CPU 독점 사용이 필수입니다. cpuset으로 DPDK 워커 스레드를 격리 CPU에 고정하고, 나머지 시스템 작업은 housekeeping CPU에서 처리합니다.
#!/bin/bash
# DPDK 전용 CPU 격리 설정 (16코어 듀얼 소켓)
# 부팅 파라미터 (GRUB)
# isolcpus=domain,managed_irq,2-7 nohz_full=2-7 rcu_nocbs=2-7
# default_hugepagesz=1G hugepagesz=1G hugepages=8
# iommu=pt intel_iommu=on
# 1. cpuset 구성
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ sudo mkdir /sys/fs/cgroup/dpdk
$ echo "2-7" | sudo tee /sys/fs/cgroup/dpdk/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/dpdk/cpuset.mems
$ echo "isolated" | sudo tee /sys/fs/cgroup/dpdk/cpuset.cpus.partition
# 2. Hugepage 확인
$ cat /proc/meminfo | grep HugePages
HugePages_Total: 8
HugePages_Free: 8
# 3. DPDK 애플리케이션 실행 (EAL 옵션)
$ echo $$ | sudo tee /sys/fs/cgroup/dpdk/cgroup.procs
$ ./dpdk-testpmd -l 2-7 -n 4 \
--socket-mem 4096,0 \
--file-prefix=dpdk1 \
-- -i --nb-cores=4 --rxq=4 --txq=4
금융 거래 시스템 (Ultra-Low Latency)
초저지연 금융 거래 시스템은 나노초 단위 응답이 요구됩니다. CPU 격리, 커널 바이패스, 메모리 잠금을 조합하여 결정론적(Deterministic) 실행 환경을 구축합니다.
#define _GNU_SOURCE
#include <sched.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/prctl.h>
/* 금융 거래 시스템을 위한 CPU 격리 초기화 */
static void setup_trading_thread(int cpu_id)
{
cpu_set_t cpuset;
struct sched_param param;
/* 1. CPU 격리 — 지정 코어에 고정 */
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
if (sched_setaffinity(0, sizeof(cpuset), &cpuset) < 0) {
perror("sched_setaffinity");
exit(1);
}
/* 2. SCHED_FIFO 최고 우선순위 — 선점 방지 */
param.sched_priority = sched_get_priority_max(SCHED_FIFO);
if (sched_setscheduler(0, SCHED_FIFO, ¶m) < 0) {
perror("sched_setscheduler");
exit(1);
}
/* 3. 전체 메모리 잠금 — 페이지 폴트 제거 */
if (mlockall(MCL_CURRENT | MCL_FUTURE) < 0)
perror("mlockall");
/* 4. 타이머 슬랙 제거 */
prctl(PR_SET_TIMERSLACK, 1);
/* 5. 워크로드 시작 전 캐시 워밍업(Warming Up) */
printf("거래 스레드 초기화 완료: CPU %d, 우선순위 %d\n",
cpu_id, param.sched_priority);
}
/* 거래 루프 — busy-poll 방식 */
static void trading_loop(volatile int *order_queue)
{
struct timespec ts;
while (1) {
/* 커널 개입 없는 busy-wait (시스템 콜 최소화) */
if (__atomic_load_n(order_queue, __ATOMIC_ACQUIRE)) {
clock_gettime(CLOCK_MONOTONIC, &ts); /* vDSO 경유 */
process_order(order_queue, &ts);
}
/* _mm_pause() 또는 asm volatile("pause") — CPU 전력 절약 */
asm volatile("pause");
}
}
통신 장비 (5G RAN)
5G RAN(Radio Access Network) 장비에서는 TTI(Transmission Time Interval) 내에 반드시 처리를 완료해야 합니다. cpuset과 PREEMPT_RT를 조합하여 결정론적 스케줄링을 보장합니다.
#!/bin/bash
# 5G RAN CPU 격리 설정 (32코어 서버)
# 부팅 파라미터
# isolcpus=domain,managed_irq,4-31
# nohz_full=4-31 rcu_nocbs=4-31
# nosmt irqaffinity=0-3
# processor.max_cstate=1 intel_idle.max_cstate=0
# skew_tick=1 tsc=reliable
# CPU 역할 분담
# CPU 0-1: housekeeping (커널, IRQ, RCU)
# CPU 2-3: OAM/관리 (모니터링, 로깅)
# CPU 4-15: L1 PHY 처리 (FFT, 채널 추정)
# CPU 16-27: L2 MAC/RLC 처리
# CPU 28-31: L3/Transport 처리
# cpuset 계층 구성
echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
for group in oam l1-phy l2-mac l3-transport; do
sudo mkdir -p /sys/fs/cgroup/ran/$group
done
echo "2-3" | sudo tee /sys/fs/cgroup/ran/oam/cpuset.cpus
echo "4-15" | sudo tee /sys/fs/cgroup/ran/l1-phy/cpuset.cpus
echo "16-27" | sudo tee /sys/fs/cgroup/ran/l2-mac/cpuset.cpus
echo "28-31" | sudo tee /sys/fs/cgroup/ran/l3-transport/cpuset.cpus
# 각 그룹에 NUMA 노드 매칭
for group in oam l1-phy l2-mac l3-transport; do
echo "0" | sudo tee /sys/fs/cgroup/ran/$group/cpuset.mems
done
# L1 PHY에 partition=isolated 적용 (가장 지연 민감)
echo "isolated" | sudo tee /sys/fs/cgroup/ran/l1-phy/cpuset.cpus.partition
# 지터 검증
$ sudo cyclictest -t 1 -p 99 -a 4 -i 100 -l 1000000 -q
# T: 0 ( 4) P:99 I:100 C:1000000 Min:1 Act:2 Avg:2 Max:5
# Max 5µs 이하 → 5G TTI(0.5ms) 요구사항 충족
가상 머신(VM) vCPU 피닝
KVM/QEMU 환경에서 vCPU를 물리 CPU에 1:1로 고정(Pin)하면 캐시 친화성과 NUMA 지역성을 극대화할 수 있습니다. cpuset으로 호스트 워크로드와 VM을 분리합니다.
# libvirt XML에서 vCPU 피닝 설정
# <cputune>
# <vcpupin vcpu='0' cpuset='4'/>
# <vcpupin vcpu='1' cpuset='5'/>
# <vcpupin vcpu='2' cpuset='6'/>
# <vcpupin vcpu='3' cpuset='7'/>
# <emulatorpin cpuset='0-1'/>
# </cputune>
# systemd-machined cpuset 확인
$ cat /sys/fs/cgroup/machine.slice/machine-qemu*.scope/cpuset.cpus.effective
4-7
# QEMU 명령줄에서 직접 설정
$ qemu-system-x86_64 \
-smp 4 \
-object memory-backend-memfd,id=mem,size=4G,host-nodes=1,policy=bind \
-numa node,memdev=mem \
... &
# QEMU vCPU 스레드를 cpuset에 등록
QEMU_PID=$!
for tid in $(ls /proc/$QEMU_PID/task/); do
echo $tid | sudo tee /sys/fs/cgroup/vm-isolated/cgroup.procs
done
실습: CPU 격리 단계별 구축
1단계: 현재 시스템 상태 파악
# CPU 토폴로지 확인
$ lscpu | grep -E 'CPU\(s\)|Thread|Core|Socket|NUMA'
# cgroup v2 활성화 확인
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# 현재 격리 상태 확인
$ cat /sys/devices/system/cpu/isolated
# (비어 있으면 격리 CPU 없음)
# 현재 부트 파라미터 확인
$ cat /proc/cmdline
# NUMA 토폴로지 확인
$ numactl --hardware 2>/dev/null || echo "numactl 미설치"
2단계: cpuset으로 동적 CPU 제한 (재부팅 없이)
# cpuset 컨트롤러 활성화
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 테스트 cpuset 생성
$ sudo mkdir /sys/fs/cgroup/lab-isolated
$ echo "2-3" | sudo tee /sys/fs/cgroup/lab-isolated/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/lab-isolated/cpuset.mems
# 테스트 프로세스 실행 후 cpuset에 등록
$ stress-ng --cpu 1 --timeout 60 &
STRESS_PID=$!
$ echo $STRESS_PID | sudo tee /sys/fs/cgroup/lab-isolated/cgroup.procs
# CPU affinity 확인 — CPU 2-3에서만 실행되는지 검증
$ taskset -p $STRESS_PID
pid 12345's current affinity mask: c # 0b1100 = CPU 2,3
# 실시간 모니터링
$ top -p $STRESS_PID -d 1 # 'P' CPU 컬럼에 2 또는 3만 표시
3단계: partition=isolated 적용
# 파티션을 isolated로 전환 (독립 스케줄링 도메인 생성)
$ echo "isolated" | sudo tee /sys/fs/cgroup/lab-isolated/cpuset.cpus.partition
# 상태 확인
$ cat /sys/fs/cgroup/lab-isolated/cpuset.cpus.partition
isolated # ← "isolated invalid"가 아닌지 반드시 확인
# 격리된 CPU가 시스템 격리 목록에 나타나는지 확인
$ cat /sys/devices/system/cpu/isolated
2-3 # ← cpuset partition에 의해 동적으로 격리됨
# 유효 CPU 확인
$ cat /sys/fs/cgroup/lab-isolated/cpuset.cpus.effective
2-3
4단계: 지터 측정 비교
# cyclictest 설치 (rt-tests 패키지)
$ sudo apt install rt-tests # Debian/Ubuntu
# 또는
$ sudo yum install rt-tests # RHEL/CentOS
# 비격리 CPU에서 지터 측정 (기준선)
$ sudo cyclictest -t 1 -p 99 -a 0 -i 100 -l 100000 -q
# T: 0 ( 0) P:99 I:100 C:100000 Min:1 Act:3 Avg:4 Max:42
# 격리 CPU에서 지터 측정
$ echo $$ | sudo tee /sys/fs/cgroup/lab-isolated/cgroup.procs
$ sudo cyclictest -t 1 -p 99 -a 2 -i 100 -l 100000 -q
# T: 0 ( 2) P:99 I:100 C:100000 Min:1 Act:2 Avg:2 Max:8
# Max 지터가 42µs → 8µs로 개선됨
5단계: 정리
# 격리 해제
$ echo "member" | sudo tee /sys/fs/cgroup/lab-isolated/cpuset.cpus.partition
# 프로세스를 루트 cgroup으로 복귀
$ echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs
# cpuset 삭제
$ sudo rmdir /sys/fs/cgroup/lab-isolated
# 격리 CPU 해제 확인
$ cat /sys/devices/system/cpu/isolated
# (비어 있으면 정상 복구)
관련 문서
- cgroups — cpuset의 상위 프레임워크, 리소스 제어 기본 개념
- CPU 캐시 (CPU Cache) — L1/L2/L3·TLB 상호작용, 격리 시 캐시 친화성 극대화
- CPU Topology — 소켓/코어/스레드 구조, NUMA 토폴로지 이해
- 프로세스 스케줄러 — sched_class 계층, 로드 밸런싱, 스케줄러 도메인
- EEVDF 스케줄러 — EEVDF 알고리즘과 구현
- sched_ext — BPF 확장 스케줄러 — BPF 기반 커스텀 스케줄러
- Real-Time Linux & PREEMPT_RT — RT 워크로드 CPU 격리, PREEMPT_RT 패치
- NUMA — 메모리 배치 최적화, cpuset.mems와의 연동
- IRQ Domain — 인터럽트 라우팅과 affinity
- vDSO — nohz_full 환경에서 시스템 콜 없는 시간 조회