cgroups v1/v2 (Control Groups)

Linux 커널 cgroups: CPU, memory, I/O 컨트롤러, PSI, systemd 연동.

관련 표준: OCI Runtime Specification 1.0 (cgroups 리소스 제한 요구사항) — 컨테이너 런타임의 리소스 격리 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

cgroups 개요

Control Groups(cgroups)는 프로세스 그룹의 시스템 리소스 사용을 제한(limit), 계량(accounting), 격리(isolation)하는 커널 메커니즘입니다. 컨테이너 런타임, systemd 등에서 리소스 관리의 기반으로 사용됩니다.

리소스 제어 원리

cgroups의 핵심 설계 원리는 "커널의 리소스 할당 경로에 cgroup 검사를 삽입한다"는 것입니다. 프로세스가 리소스(메모리, CPU 시간, I/O 대역폭)를 요청할 때마다 커널은 해당 프로세스가 속한 cgroup의 제한을 확인합니다:

CPU 제어 원리

CPU 컨트롤러는 CFS(Completely Fair Scheduler) 스케줄러에 통합되어 동작합니다. 두 가지 메커니즘이 있습니다:

메모리 제어 원리

메모리 컨트롤러는 charge/uncharge 메커니즘으로 동작합니다:

  1. 프로세스가 페이지를 할당하면, 해당 페이지의 비용을 프로세스의 cgroup에 charge(부과)합니다.
  2. 페이지가 해제되면 uncharge(환급)합니다.
  3. cgroup의 누적 사용량이 memory.max에 도달하면, 커널은 해당 cgroup 내에서 페이지 회수를 시도합니다.
  4. 회수로도 부족하면 OOM killer가 해당 cgroup 내의 프로세스를 종료합니다.

계층 구조와 리소스 누적 원리

cgroup v2의 계층 구조에서 리소스 제한은 부모가 자식을 포함합니다:

💡

cgroup v2 설계 원칙: v1에서는 컨트롤러별로 독립된 계층이 존재하여 CPU cgroup과 메모리 cgroup의 트리 구조가 달라 정책 불일치가 발생했습니다. v2는 단일 통합 계층으로 모든 컨트롤러가 같은 트리를 공유하여 일관된 리소스 정책을 보장합니다.

cgroup v1 vs v2

특성cgroup v1cgroup v2
계층 구조컨트롤러별 독립 계층단일 통합 계층
스레드 지원프로세스 단위스레드 모드 지원
위임제한적안전한 위임 가능
압력 통지없음PSI (Pressure Stall Info)
마운트/sys/fs/cgroup/*/sys/fs/cgroup

주요 컨트롤러

CPU 컨트롤러

# cgroup v2: CPU 제한 (quota/period)
echo "100000 1000000" > /sys/fs/cgroup/mygroup/cpu.max
# → 1초(1000000us) 중 100ms(100000us)만 사용 = 10% CPU

# CPU weight (상대적 가중치, 1-10000)
echo 150 > /sys/fs/cgroup/mygroup/cpu.weight

Memory 컨트롤러

# 메모리 제한 (256MB)
echo 268435456 > /sys/fs/cgroup/mygroup/memory.max

# 메모리 사용량 확인
cat /sys/fs/cgroup/mygroup/memory.current

# OOM kill 카운트
cat /sys/fs/cgroup/mygroup/memory.events

I/O 컨트롤러

# I/O 대역폭 제한 (장치 8:0에 대해 읽기 10MB/s)
echo "8:0 rbps=10485760" > /sys/fs/cgroup/mygroup/io.max

# I/O weight
echo "default 100" > /sys/fs/cgroup/mygroup/io.weight

커널 cgroup API

#include <linux/cgroup.h>

/* 현재 태스크의 cgroup 접근 */
struct cgroup *cgrp;
rcu_read_lock();
cgrp = task_cgroup(current, memory_cgrp_id);
rcu_read_unlock();

/* cgroup 서브시스템 등록 (커널 컨트롤러 개발 시) */
struct cgroup_subsys my_cgrp_subsys = {
    .css_alloc  = my_css_alloc,
    .css_free   = my_css_free,
    .attach     = my_attach,
    .name       = "my_controller",
};

PSI (Pressure Stall Information)

cgroup v2에서는 PSI를 통해 CPU, 메모리, I/O의 리소스 압력을 실시간 모니터링할 수 있습니다. PSI의 핵심 원리는 "일할 수 있는데 리소스를 기다리느라 못 하는 시간의 비율"을 측정하는 것입니다:

커널은 각 CPU에서 태스크 상태 전환(running → waiting)을 추적하고, 지수 이동 평균(EMA)으로 10초/60초/300초 윈도우의 압력 비율을 계산합니다. 이 값은 0.00(압력 없음)~100.00(완전 정체) 사이입니다.

# CPU 압력 확인
cat /proc/pressure/cpu
# some avg10=0.50 avg60=0.30 avg300=0.20 total=12345678

# cgroup별 메모리 압력
cat /sys/fs/cgroup/mygroup/memory.pressure

systemd는 cgroup v2를 기본으로 사용합니다. systemd-cgtop으로 cgroup별 리소스 사용량을 실시간 모니터링할 수 있습니다.

Freezer 컨트롤러

Freezer 컨트롤러는 cgroup 내 모든 프로세스를 일시 중지(freeze)하고 재개(thaw)할 수 있습니다:

# cgroup v2 freezer
echo 1 > /sys/fs/cgroup/myapp/cgroup.freeze
# myapp 그룹의 모든 프로세스가 TASK_FROZEN 상태

echo 0 > /sys/fs/cgroup/myapp/cgroup.freeze
# 프로세스 재개

# 상태 확인
cat /sys/fs/cgroup/myapp/cgroup.events
# frozen 1

cpuset 컨트롤러

CPU와 메모리 노드를 프로세스 그룹에 할당합니다:

# cgroup v2 cpuset
echo "0-3" > /sys/fs/cgroup/myapp/cpuset.cpus      # CPU 0~3 할당
echo "0" > /sys/fs/cgroup/myapp/cpuset.mems         # NUMA 노드 0
echo "root" > /sys/fs/cgroup/myapp/cpuset.cpus.partition  # 전용 파티션

cgroup 위임 (Delegation)

# 비특권 사용자에게 cgroup 관리 위임
mkdir /sys/fs/cgroup/user_slice/user-1000
chown -R 1000:1000 /sys/fs/cgroup/user_slice/user-1000

# systemd에서 위임 설정
# /etc/systemd/system/myservice.service
# [Service]
# Delegate=cpu memory io

cgroup 커널 내부 구조

/* 커널 내부: cgroup 서브시스템 등록 */
struct cgroup_subsys memory_cgrp_subsys = {
    .css_alloc   = mem_cgroup_css_alloc,
    .css_online  = mem_cgroup_css_online,
    .css_offline = mem_cgroup_css_offline,
    .css_free    = mem_cgroup_css_free,
    .can_attach  = mem_cgroup_can_attach,
    .attach      = mem_cgroup_move_task,
    .name        = "memory",
};

/* task_struct에서 cgroup 접근 */
struct css_set *cset = task->cgroups;
/* css_set은 프로세스가 속한 모든 cgroup subsystem state를 참조 */

PSI 상세 분석

PSI(Pressure Stall Information)는 시스템 자원 압박 수준을 정량적으로 측정합니다:

# 시스템 전체 PSI 확인
cat /proc/pressure/cpu
# some avg10=0.25 avg60=0.10 avg300=0.05 total=123456
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /proc/pressure/memory
cat /proc/pressure/io

# cgroup별 PSI
cat /sys/fs/cgroup/myapp/cpu.pressure
cat /sys/fs/cgroup/myapp/memory.pressure
cat /sys/fs/cgroup/myapp/io.pressure

# PSI 트리거 (임계값 기반 알림)
# /proc/pressure/memory에 트리거 설정:
# "some 150000 1000000" → 1초 window에서 150ms 이상 some 압력 시 이벤트
/* PSI 트리거를 커널에서 사용 */
int fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
struct pollfd fds = { .fd = fd, .events = POLLPRI };

/* 트리거 설정: 1초 window에서 150ms 이상 stall */
write(fd, "some 150000 1000000", 20);

/* 이벤트 대기 */
poll(&fds, 1, -1);  /* 압력 임계값 초과 시 반환 */

커널 버전별 변경사항

버전변경
2.6.24cgroup v1 도입
3.16cgroup v2 통합 계층 초기 구현
4.5cgroup v2 정식 릴리스
4.20PSI (Pressure Stall Information) 도입
5.0cgroup v2 cpuset 컨트롤러
5.2cgroup v2 freezer 통합
5.7cgroup v1의 cpuacct를 cpu에 통합 (v2)
6.1PSI per-cgroup 트리거 개선

cgroup 관련 주요 취약점

cgroup은 컨테이너 리소스 격리의 핵심 메커니즘이지만, 특히 v1의 설계에서 보안 취약점이 반복적으로 발견되었습니다. cgroup v2로의 전환이 이러한 문제의 근본적 해결을 위해 가속화되고 있습니다.

CVE-2022-0492 — cgroup v1 release_agent 컨테이너 탈출 (CVSS 7.0):

cgroup v1의 release_agent 파일에 호스트 경로의 스크립트를 지정하면, cgroup 내 마지막 프로세스가 종료될 때 해당 스크립트가 호스트의 root 권한으로 실행됩니다. CAP_SYS_ADMIN + cgroup 마운트 권한이 있는 컨테이너에서 트리거 가능합니다.

/* cgroup v1 release_agent 탈출 vs v2의 개선 */

/* cgroup v1: release_agent는 호스트의 init cgroup ns에서 실행 */
/* → 컨테이너 내부에서 호스트 명령 실행 가능 */
echo /host/path/exploit.sh > /sys/fs/cgroup/rdma/release_agent
echo 1 > /sys/fs/cgroup/rdma/child/notify_on_release

/* cgroup v2: release_agent 메커니즘 제거 */
/* 대신 cgroup.events 파일의 "populated 0" 이벤트를 */
/* 유저스페이스 데몬(systemd 등)이 처리 */
/* → 커널이 직접 사용자 프로그램을 실행하지 않음 */

/* 커널 5.17+ 수정: cgroup namespace 내에서 release_agent 쓰기 차단 */
/* cgroup_release_agent_write()에 ns_capable(init_ns, CAP_SYS_ADMIN) 검사 추가 */
CVE-2021-4154 — cgroup v1 파일 디스크립터 UAF (CVSS 8.8):

cgroup v1의 파일 디스크립터 처리에서 cgroup_get_from_fd()가 cgroup에 대한 참조를 적절히 관리하지 않아, 해제된 cgroup 구조체에 접근하는 Use-After-Free가 발생합니다. 이를 통해 로컬 권한 상승이 가능합니다.

cgroup 리소스 제한 우회 패턴:

Memory 제한 우회: cgroup v1의 memory.limit_in_bytes는 커널 메모리(slab, page tables)를 별도로 계산하지 않아, kmem.limit_in_bytes 미설정 시 커널 메모리를 무제한 사용 가능. cgroup v2에서는 memory.max가 커널/사용자 메모리를 통합 관리
CPU 제한 우회: cpu.cfs_quota_us/cpu.cfs_period_us에서 짧은 period와 높은 quota 조합 시 버스트(burst)가 발생하여 순간적으로 제한 초과 가능. cgroup v2의 cpu.max.burst로 버스트 크기를 명시적으로 제어
I/O 제한 우회: cgroup v1의 blkio 컨트롤러는 direct I/O만 제한하고 buffered I/O(페이지 캐시 경유)는 제한하지 않음. cgroup v2의 io.max는 writeback 포함 전체 I/O를 제한

/* cgroup v1 vs v2 보안 비교 */

/*
 * cgroup v1 보안 문제:
 * 1. release_agent: 커널이 사용자 프로그램 직접 실행 → 탈출 벡터
 * 2. 다중 계층(hierarchy): 컨트롤러별 독립 트리 → 정책 불일치 가능
 * 3. 위임 모델 미비: 하위 cgroup에 대한 세밀한 권한 제어 부재
 * 4. kmem accounting 분리: 커널 메모리 제한 우회 가능
 *
 * cgroup v2 보안 개선:
 * 1. release_agent 제거: 유저스페이스 데몬으로 대체
 * 2. 단일 통합 계층: 모든 컨트롤러가 하나의 트리 → 일관된 정책
 * 3. 안전한 위임: subtree_control로 하위 cgroup 권한 제어
 * 4. 통합 메모리 accounting: 커널 + 사용자 메모리 통합 제한
 * 5. nsdelegate 마운트 옵션: cgroup NS 경계에서 위임 강제
 */

# cgroup v2 보안 권장 설정
# /etc/default/grub에 추가
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=all"

# nsdelegate로 안전한 위임 활성화
mount -t cgroup2 none /sys/fs/cgroup -o nsdelegate,memory_recursiveprot

Android cgroup 구성

Android는 cgroup을 활용하여 foreground/background 앱의 CPU, 메모리, I/O 자원을 정교하게 관리한다. Android 12부터 cgroup v2가 필수이며, task_profiles.json이 cgroup 설정을 추상화한다.

컨트롤러마운트 경로Android 용도
cpuctl/dev/cpuctl/foreground/background CPU 가중치 (cpu.weight)
cpuset/dev/cpuset/빅/리틀 코어 핀닝 (foreground→전체, background→리틀)
memory/dev/memcg/앱별 메모리 제한, lmkd(PSI 기반) 연동
freezer/sys/fs/cgroup/cgroup v2 freezer로 백그라운드 앱 동결 (배터리 절약)
# Android cgroup 계층 구조
/dev/cpuset/foreground     → cpus: 0-7   # 빅+리틀 전체 코어
/dev/cpuset/background     → cpus: 0-3   # 리틀 코어만
/dev/cpuctl/foreground     → cpu.weight: 1024
/dev/cpuctl/background     → cpu.weight: 50

# UCLAMP: 스케줄러에 utilization 힌트 (EAS 연동)
/dev/cpuctl/foreground/cpu.uclamp.min: 20
/dev/cpuctl/foreground/cpu.uclamp.max: 1024

Android init의 task_profiles 지시어, UCLAMP 스케줄링, lmkd PSI 연동 등 심화 내용은 Android 커널 — cgroup 구성을 참고하라.