cgroups v1/v2 (Control Groups)
Linux 커널 cgroups: CPU, memory, I/O 컨트롤러, PSI, systemd 연동.
cgroups 개요
Control Groups(cgroups)는 프로세스 그룹의 시스템 리소스 사용을 제한(limit), 계량(accounting), 격리(isolation)하는 커널 메커니즘입니다. 컨테이너 런타임, systemd 등에서 리소스 관리의 기반으로 사용됩니다.
리소스 제어 원리
cgroups의 핵심 설계 원리는 "커널의 리소스 할당 경로에 cgroup 검사를 삽입한다"는 것입니다. 프로세스가 리소스(메모리, CPU 시간, I/O 대역폭)를 요청할 때마다 커널은 해당 프로세스가 속한 cgroup의 제한을 확인합니다:
CPU 제어 원리
CPU 컨트롤러는 CFS(Completely Fair Scheduler) 스케줄러에 통합되어 동작합니다. 두 가지 메커니즘이 있습니다:
- 가중치(weight): 경쟁 상황에서 CPU 시간의 비례 배분.
cpu.weight=200인 그룹은cpu.weight=100인 그룹보다 2배의 CPU 시간을 받습니다. CPU가 여유로우면 제한 없이 사용 가능합니다. - 대역폭 제한(bandwidth throttling):
cpu.max의 quota/period 방식. period(예: 100ms) 동안 quota(예: 50ms)만큼만 실행을 허용하고, 초과하면 해당 period가 끝날 때까지 스로틀링합니다. 내부적으로 hrtimer가 period 경계를 관리합니다.
메모리 제어 원리
메모리 컨트롤러는 charge/uncharge 메커니즘으로 동작합니다:
- 프로세스가 페이지를 할당하면, 해당 페이지의 비용을 프로세스의 cgroup에 charge(부과)합니다.
- 페이지가 해제되면 uncharge(환급)합니다.
- cgroup의 누적 사용량이
memory.max에 도달하면, 커널은 해당 cgroup 내에서 페이지 회수를 시도합니다. - 회수로도 부족하면 OOM killer가 해당 cgroup 내의 프로세스를 종료합니다.
계층 구조와 리소스 누적 원리
cgroup v2의 계층 구조에서 리소스 제한은 부모가 자식을 포함합니다:
- 자식 cgroup의 리소스 사용량은 항상 부모에 누적됩니다. 부모의
memory.max가 1GB이면, 모든 자식의 합산 메모리가 1GB를 초과할 수 없습니다. - 자식은 부모보다 더 높은 제한을 설정할 수 있지만, 실질적으로는 부모의 제한이 먼저 적용됩니다.
memory.low는 리소스 보호를 위한 최소 보장으로, 글로벌 메모리 회수 시 이 값 이하의 메모리는 보호됩니다.
cgroup v2 설계 원칙: v1에서는 컨트롤러별로 독립된 계층이 존재하여 CPU cgroup과 메모리 cgroup의 트리 구조가 달라 정책 불일치가 발생했습니다. v2는 단일 통합 계층으로 모든 컨트롤러가 같은 트리를 공유하여 일관된 리소스 정책을 보장합니다.
cgroup v1 vs v2
| 특성 | cgroup v1 | cgroup 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의 핵심 원리는 "일할 수 있는데 리소스를 기다리느라 못 하는 시간의 비율"을 측정하는 것입니다:
- some: 최소 하나 이상의 태스크가 해당 리소스를 기다리는 시간 비율 (부분적 지연)
- full: 모든 태스크가 해당 리소스를 기다리는 시간 비율 (완전 정체). CPU에는 full이 없습니다.
커널은 각 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.24 | cgroup v1 도입 |
| 3.16 | cgroup v2 통합 계층 초기 구현 |
| 4.5 | cgroup v2 정식 릴리스 |
| 4.20 | PSI (Pressure Stall Information) 도입 |
| 5.0 | cgroup v2 cpuset 컨트롤러 |
| 5.2 | cgroup v2 freezer 통합 |
| 5.7 | cgroup v1의 cpuacct를 cpu에 통합 (v2) |
| 6.1 | PSI per-cgroup 트리거 개선 |
참고 자료: 커널 cgroup v2 문서, Facebook PSI 문서
cgroup 관련 주요 취약점
cgroup은 컨테이너 리소스 격리의 핵심 메커니즘이지만, 특히 v1의 설계에서 보안 취약점이 반복적으로 발견되었습니다. cgroup v2로의 전환이 이러한 문제의 근본적 해결을 위해 가속화되고 있습니다.
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) 검사 추가 */
cgroup v1의 파일 디스크립터 처리에서 cgroup_get_from_fd()가 cgroup에 대한 참조를 적절히 관리하지 않아, 해제된 cgroup 구조체에 접근하는 Use-After-Free가 발생합니다. 이를 통해 로컬 권한 상승이 가능합니다.
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 구성을 참고하라.