[2026] Linux 프로세스·스케줄러 심화 — 태스크 상태 머신, CFS·런큐, NUMA와 프로덕션 튜닝
이 글의 핵심
리눅스에서 “프로세스”는 스케줄링 단위인 태스크(task_struct)의 집합이고, CPU는 CFS 등 스케줄링 클래스 규칙에 따라 런큐에서 고릅니다. 상태 머신·스케줄러·cgroup·NUMA를 한데 엮어야 지연·기아·스틸 시간 같은 운영 증상을 원인까지 설명할 수 있습니다.
들어가며
운영체제 관점에서 리눅스의 실행 단위는 흔히 말하는 “프로세스”라는 용어보다 태스크(task)라고 보는 편이 커널 내부와 맞습니다. task_struct는 스레드 하나에 대응하고, thread_group으로 묶인 태스크들이 전통적인 프로세스에 해당합니다. CPU는 한 코어에서 한 순간에 하나의 태스크만 실행하지만, 스케줄러는 상태 머신과 런큐를 통해 “누가, 얼마나, 어떤 제약 아래” CPU를 쓸지를 결정합니다.
이 글은 Linux 완전 가이드에서 다룬 CFS 개요를 넘어, 태스크 상태 전이, CFS·런큐·로드 밸런싱, NUMA·가상화 스틸, 프로덕션 튜닝 포인트를 한층 깊게 연결하는 것을 목표로 합니다. 메모리·페이지 폴트·할당기와 맞물리는 부분은 Linux 메모리·가상 메모리 심화와 함께 보시면 전체 그림이 완성됩니다.
태스크 상태 머신: TASK_*가 의미하는 것
커널은 각 태스크를 스케줄 가능 여부와 대기 이유로 나눕니다. 사용자가 ps나 /proc/PID/status에서 보는 R/S/D/T/Z 같은 표기는 이 내부 상태를 단순화한 것입니다.
실행과 준비: TASK_RUNNING
RUNNING은 “지금 CPU에서 실행 중이거나, 실행될 수 있도록 런큐에 올라간 상태”를 포괄합니다. 즉, 스케줄러가 고를 수 있는 후보입니다. 멀티코어에서는 여러 태스크가 동시에 RUNNING일 수 있습니다.
끊을 수 있는 대기: TASK_INTERRUPTIBLE
디스크가 아닌 많은 블로킹 경로(소켓 대기, futex 대기, 일부 락 대기 등)는 인터럽트 가능한 대기로 들어갑니다. 이 상태에서는 시그널이 오면 깨어나 -EINTR로 돌아가는 패턴이 흔합니다. 운영에서 “요청이 잠깐 멈췄다가 시그널로 살아난다”는 현상은 종종 이 경로와 연결됩니다.
끊기 어려운 대기: TASK_UNINTERRUPTIBLE (“D 상태”)
UNINTERRUPTIBLE은 I/O 경로처럼 중간에 끊으면 안 되는 구간에서 선택될 수 있습니다. 이 상태는 시그널로 즉시 깨우기 어렵고, 스토리지·NFS·특정 드라이버 이슈로 길게 머무르면 전체 서비스가 “프로세스는 있는데 진행이 없다”처럼 보입니다. kill -9는 보통 시그널을 보내지만, 태스크가 이 대기에서 빠져나오지 못하면 체감상 종료가 지연될 수 있습니다. 이때는 CPU가 아니라 블록 계층·스토리지 지연을 의심해야 합니다.
멈춤·추적: TASK_STOPPED, TASK_TRACED
SIGSTOP 계열로 멈춘 상태와 ptrace로 추적된 상태는 스케줄러 관점에서 실행 후보에서 제외됩니다. 디버거·점검 도구가 붙은 환경에서만 보통 문제가 됩니다.
종료 경로: EXIT_ZOMBIE, EXIT_DEAD
자식이 exit했지만 부모가 아직 wait하지 않으면 좀비로 남습니다. task_struct는 PID·종료 코드 등 최소 메타데이터만 유지하며, 부모가 회수할 때까지 사라지지 않습니다. 대량으로 쌓이면 PID·커널 객체 압박이 될 수 있어, 워커 프로세스 패턴에서는 SIGCHLD 처리·waitpid 루프가 운영 이슈로 직결됩니다.
상태 전이를 한눈에(운영자 관점)
문어체로 엄밀한 전이도를 모두 적기보다, 운영에서 자주 쓰는 골격은 다음과 같습니다.
TASK_RUNNING ──스케줄 아웃──► Runnable 큐 밖으로 / 또는 블로킹 경로 진입
▲ │
│ ▼
└── wake_up / 스케줄 인 ── TASK_INTERRUPTIBLE 또는 UNINTERRUPTIBLE
RUNNING에서 벗어날 때 인터럽트 가능/불가능 중 어디로 가느냐가 ps의 S/D 체감과 직결됩니다.
상태 전이와 대기 큐(wait queue)
태스크는 “조건이 충족될 때까지” 특정 대기 큐에 걸립니다. 조건이 만족되면 워커(인터럽트 하반부, 다른 CPU의 태스크 등)가 wake_up 계열로 깨웁니다. 여기서 잘못된 wake-up 누락이나 과도한 wake-up은 스케줄 오버헤드나 기아로 이어질 수 있습니다. 이는 “애플리케이션 코드는 멀쩡한데 커널 쪽 대기가 이상하다”는 난해한 장애의 원인이 되기도 합니다.
스케줄러 개요: 클래스, 엔티티, 선점
리눅스 커널은 스케줄링 클래스를 플러그인처럼 쌓아 올립니다. 일반 프로세스는 CFS(완전 공정 스케줄러)가 기본이고, 실시간·데드라인·idle 클래스는 다른 규칙을 가집니다.
CFS의 핵심 단위: 스케줄 엔티티와 vruntime
CFS는 “CPU 시간을 공정하게” 분배하려고 가상 실행 시간(vruntime)을 누적합니다. 우선순위·nice·cgroup 가중치는 이 증가 속도에 영향을 주어, 같은 실제 시간을 써도 vruntime이 덜 늘어나는 태스크가 더 자주 선택되는 경향을 만듭니다.
이 모델은 처리량·장기 공정성에는 강하지만, 짧은 데드라인 보장은 목표로 하지 않습니다. 따라서 “p99 지연이 튄다”는 문제를 CFS 파라미터만으로 해결하려 하면 실패하기 쉽고, 락 경합·I/O·캐시·syscall 폭증 같은 다른 층을 같이 봐야 합니다.
per-CPU 런큐와 cfs_rq
각 논리 CPU는 런큐 rq를 가지며, CFS는 여기에 cfs_rq를 둡니다. 실행 가능한 태스크는 vruntime 순서를 빠르게 찾기 위해 레드-블랙 트리 등으로 정렬되는 것이 일반적인 구현입니다(세부는 커널 버전에 따라 발전).
시간 슬라이스와 스케줄 지연
CFS는 고정 양자만 반복하기보다, sched_latency_ns, sched_min_granularity_ns 같은 파라미터로 “한 주기에 태스크를 얼마나 자주 바꿀지”를 조정합니다. 태스크 수가 매우 많으면 이론상 양자가 지나치게 잘게 쪼개져 문맥 전환 비용이 지배적이 될 수 있습니다. 이때는 perf로 스케줄 이벤트·문맥 전환 비율을 확인하는 것이 1차입니다.
선점(preemption)과 need_resched
커널은 타이머 틱·wake-up 경로 등에서 다시 스케줄할 필요가 있는지를 표시합니다. 사용자 공간으로 돌아가기 직전·시스템 콜 복귀 직전 등 안전한 지점에서 선점이 일어납니다. 따라서 긴 커널 구간(특정 락 보유, IRQ 비활성 구간)은 스케줄 지연을 키울 수 있습니다.
로드 밸런싱·마이그레이션·NUMA
멀티코어에서의 밸런싱
태스크는 한 번 특정 CPU에 붙어 실행되다가, 로드 불균형이나 affinity 정책에 따라 다른 CPU로 이동(migration)할 수 있습니다. 이때 캐시 콜드가 되어 성능이 떨어지거나, 반대로 한 코어에 몰려 런큐 대기가 길어질 수 있습니다.
NUMA 환경의 함정
NUMA에서는 메모리 접근 비용이 소켓 간으로 커집니다. 스케줄러와 메모리 정책(numactl, 커널의 폴리시)이 엇갈리면 “CPU는 충분한데 느리다”는 패턴이 납니다. 프로덕션에서는 프로세스/스레드 배치, 메모리 바인딩, 투명 대형 페이지(THP)와의 상호작용을 함께 봅니다.
가상화와 스틸(steal) 시간
게스트에서 스틸 시간이 크면, 게스트 OS의 스케줄러가 보기에 CPU가 있어도 하이퍼바이저가 빼앗아 간 시간이 큰 것입니다. 이때 애플리케이션 튜닝보다 인스턴스 크기·인접 배치·크레딧·노이즈 이웃이 원인인 경우가 많습니다.
cgroups·systemd·Kubernetes와의 접점
CPU cgroup의 cpu.weight, cpu.max는 CFS가 태스크에 허용하는 상대/절대 한도와 맞물립니다. 상위 cgroup에서 이미 죽어 있는 한도는 하위 프로세스가 아무리 “공정하게” 스케줄되려 해도 넘을 수 없습니다.
Kubernetes에서는 CPU limit이 cgroup v2의 cpu.max 등으로 매핑되어, throttle 카운터가 증가하는지가 핵심 관측 포인트가 됩니다. “노드는 멀쩡한데 파드만 느리다”는 증상은 종종 limit·request·QoS 클래스 문제입니다.
프로덕션 튜닝과 운영 관측
튜닝을 시작하기 전에
스케줄러 튜닝은 워크로드 특성·커널 버전·하드웨어에 민감합니다. 문서에 나온 값을 복사해 넣기보다, 지연 분포·스케줄 지연·문맥 전환·throttle을 먼저 측정하는 것이 안전합니다.
자주 언급되는 knob들(개념 수준)
kernel.sched_*계열: CFS의 지연·최소 양자 등에 영향을 줍니다. 변경은 A/B 테스트와 롤백 계획 없이 하기 어렵습니다.isolcpus,nohz_full: 지연에 민감한 워크로드를 특정 코어에 묶거나 타이머 틱을 줄이려는 패턴이 있으나, 시스템 전체 상호작용을 이해하지 않으면 오히려 처리량이 나빠질 수 있습니다.SCHED_FIFO/SCHED_DEADLINE: 잘못 쓰면 일반 태스크 기아가 납니다. 실시간 요구가 명확할 때만, 운영 정책·모니터링과 함께 검토합니다.
관측 도구
perf sched, 스케줄 tracepoint: 지연 스파이크가 스케줄러 쪽인지 분리합니다.- eBPF(
runqlat등): 런큐 대기 시간 분포를 직접 봅니다. vmstat,pidstat -w: 문맥 전환 폭증을 빠르게 잡습니다.
트러블슈팅 매핑
| 증상 | 내부적으로 의심할 지점 | 다음 관측 |
|---|---|---|
| 로드는 낮은데 응답만 느림 | throttle, 런큐 대기, 스틸 | cgroup CPU, runqlat, steal |
| 특정 프로세스만 “멈춘 것 같음” | D 상태, I/O 병목 | ps -o stat, 블록 I/O, NFS |
| 스레드 수 폭증 | 문맥 전환·락 경합 | perf, 앱 프로파일 |
| 컨테이너만 느림 | cgroup 한도 | cpu.stat, kube limits |
내부 동작과 핵심 메커니즘
이 글의 주제는 「[2026] Linux 프로세스·스케줄러 심화 — 태스크 상태 머신, CFS·런큐, NUMA와 프로덕션 튜닝」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 시스템·런타임 경계(스케줄링, I/O, 메모리, 동시성)를 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
리눅스에서 프로세스는 태스크의 집합이고, 그 생명주기는 TASK_* 상태 머신으로 표현됩니다. CPU는 CFS가 vruntime과 런큐를 통해 분배하지만, 지연 보장은 기본 목표가 아니며 I/O·NUMA·가상화·cgroup과 강하게 얽입니다. 프로덕션에서는 “스케줄러만”이 아니라 한도·배치·관측 지표를 함께 보아야 장애 시간을 줄일 수 있습니다.
메모리 쪽 내부(페이지 테이블·페이지 폴트·영역·할당기)는 Linux 메모리·가상 메모리 심화에서 이어집니다.