작년에 지인들과 스터디로 시작했던 혼공컴운을 아직도 다 못 뗐다는 게 슬퍼지는 요즘이다. 지금은 다들 바빠져서 오프라인으로도 온라인으로도 못 모이지만, 시작한 공부를 끝까지 완주하기 위해 혼자서라도 책을 보며 정리하고 있다. (겸사겸사 이번회차 성장일지를 채우기도...^^;;)
이번 게시글에서는 11장 CPU 스케줄링에 대해 요약하고, 검색과 AI를 통해 Java 백엔드 주니어 개발자가 이와 관련해서 무엇을 더 배우고 생각할 수 있는지를 고민해보았다.
CPU 스케줄링 개요
💡 운영체제가 프로세스들에게 CPU 자원을 배분하고 실행 순서를 정하는 정책과 과정
- 다중 프로그래밍 환경에서 CPU 이용률을 극대화하고, 프로세스들을 공평하고 효율적으로 처리하기 위해 필수적인 작업임
- 문맥 교환(Context Switching)을 통해 어떤 프로세스에 CPU를 할당할지 결정함
프로세스 우선순위
💡 우선 순위가 높은 프로세스란 빨리 처리해야하는 프로세스 를 의미한다.
- 우선순위 확인법
- 유닉스/리눅스/macOS: ps -el
- 윈도우: process explorer
스케줄링의 개념과 필요성
- 프로세스는 실행 상태와 대기 상태를 반복하며 실행됨
- 하지만 프로세스 종류마다 입출력장치를 이용하는 시간과 CPU를 이용하는 시간의 양에는 차이가 있다.
- 입출력 집중 프로세스 (I/O bound process): 비디오 재생이나 워드 프로세서와 같이
입출력 작업이 많은 프로세스. 실행 상태보다 대기 상태에 머무르는 시간이 더 많음.
- CPU 집중 프로세스 (CPU bound process): 복잡한 수치 연산이나 그래픽 작업과 같이
연산 작업이 많은 프로세스임. 대기 상태보다 실행 상태에 머무르는 시간이 더 많음.
- CPU 버스트와 입출력 버스트: 각각 CPU를 이용하는 작업과 입출력장치를 기다리는 작업을 의미한다. 입출력 집중 프로세스는 입출력 버스트가 많은 프로세스, CPU 집중 프로세스는 CPU 버스트가 많은 프로세스다!
스케줄링이 필요한 이유:
모든 프로세스가 CPU를 동일하게 사용하지 않으므로, 운영체제는 각 프로세스의 특성에 맞춰 우선순위(Priority)를 부여한다! 일반적으로 입출력 집중 프로세스의 우선순위가 CPU 집중 프로세스보다 높게 설정된다.
→ 빨리 입출력을 보내고 CPU를 반환하는 것이 효율적이기 때문
스케줄링 큐 (Scheduling Queue)
💡 메모리에 적재된 프로세스들을 현재 상태(준비, 대기 등)별로 묶어 관리하는 대기열 자료구조
- 운영체제가 다음에 실행할 프로세스나 특정 입출력 완료를 기다리는 프로세스를 빠르고 효율적으로 탐색하기 위해 사용함
- 각 큐는 프로세스의 정보가 담긴 PCB(프로세스 제어 블록)들을 연결 리스트 형태로 묶어서 구현함
- 준비 큐 (Ready Queue): CPU를 할당받기 위해 기다리는 상태의 프로세스들이 줄을 섬.
- 대기 큐 (Waiting Queue): 입출력 장치 완료 등을 기다리기 위해 대기 상태로 접어든 프로세스들이 줄을 섬. (장치별로 별도의 큐가 존재함)
선점형과 비선점형 스케줄링
| 구분 | 선점형 (Preemptive) | 비선점형 (Non-preemptive) |
|---|
| 특징 | 운영체제가 강제로 CPU 점유를 뺏을 수 있음 | 프로세스가 종료되거나 스스로 대기 상태가 되기 전까지 점유 |
| 장점 | 자원 독점을 막고 모든 프로세스에 골고루 자원 배분 가능 | 문맥 교환에 의한 오버헤드가 적음 |
| 단점 | 문맥 교환(Context Switch) 오버헤드가 자주 발생함 | 하나의 프로세스가 자원을 독점하면 나머지 대기 시간이 길어짐 |
| 비유 | 급한 일이 생기면 순서를 바꿀 수 있는 병원 응급실 | 한 번 들어가면 끝날 때까지 못 나오는 1인실 화장실 |
CPU 스케줄링 알고리즘
알고리즘의 종류
FCFS 스케줄링 (First-Come, First-Served Scheduling)
- 특징:
준비 큐에 도착한 순서대로 CPU를 할당하는 선입 선처리 비선점형 방식
- 문제점: 실행 시간이 매우 긴 프로세스가 먼저 도착할 경우, 뒤의 짧은 프로세스들의 대기 시간이 비정상적으로 길어지는
호위 효과(Convoy Effect)가 발생함.
SJF 스케줄링 (Shortest Job First Scheduling)
- 특징: 준비 큐에 있는 프로세스 중
CPU 이용 시간의 길이가 가장 짧은 프로세스부터 실행
- 기본적으로
비선점형이지만 선점형으로 구현될 수도 있다! (=SRT 스케줄링)
라운드 로빈 스케줄링 (Round Robin Scheduling)
- 특징: FCFS 방식에
타임 슬라이스(Time Slice) 개념이 추가된 선점형 방식
- 타임 슬라이스: 각 프로세스가 CPU를 사용할 수 있는 정해진 시간
- 운용:
정해진 타임 슬라이스 동안만 CPU를 이용하고, 작업이 안 끝나면 큐의 맨 뒤로 이동
- 주의점: 타임 슬라이스가 너무 크면 FCFS와 다를 바 없고, 너무 작으면 문맥 교환 오버헤드 때문에 성능이 저하됨.
SRT 스케줄링 (Shortest Remaining Time Scheduling)
- 특징: 최소 잔여 시간 우선 스케줄링이라고도 함.
쉽게 생각하면, SJF의 선점형 버전.
정해진 시간만큼 실행하되, 다음 CPU를 할당할 때는 남은 작업 시간이 가장 적은 프로세스를 선택함.
우선순위 스케줄링 (Priority Scheduling)
- 특징: 프로세스에 우선 순위를 부여하고,
우선순위가 가장 높은 순서대로 실행함.
- 문제점: 우선순위가 낮은 프로세스는 우선순위가 높은 프로세스에 밀려 영원히 실행되지 못하는
기아 현상(Starvation)이 발생할 수 있음.
- 해결책: 오랫동안 대기한 프로세스의 우선순위를 점차 높이는
에이징(aging) 방식을 활용
다단계 큐 스케줄링(Multilevel Queue Scheduling)
- 특징:
우선순위 스케줄링에서 발전(?)한 방식.
우선순위별로 여러 개의 큐를 사용 (프로세스 유형 별로 우선순위를 구분하여 실행)
- 우선순위가 가장 높은 큐에 있는 프로세스들을 먼저 처리
- 큐별로 타임 슬라이스를 여러 개 지정할 수도 있고, 다른 스케줄링 알고리즘을 사용할 수도 있다.
- ex) 큐1: 타임슬라이스 5ms, SRT / 큐2: 타임슬라이스 4ms, 라운드 로빈
다단계 피드백 큐 스케줄링 (Multilevel Feedback Queue Scheduling)
- 특징: 다단계 큐 스케줄링의 발전된 형태. 현대 OS에서 가장 흔하게 사용된다.
- 기존 다단계 큐 스케줄링에서는 프로세스들이 큐 사이를 이동할 수 없기 때문에 우선순위가 낮으면 계속 연기됨 (=기아 현상)
- 다단계 피드백 큐 스케줄링은
프로세스가 큐 사이를 이동할 수 있다!
- 우선순위가 가장 높은 우선순위 큐에 삽입되었다가, 해당 큐에서 실행이 끝나지 않는 다면 다음 우선순위 큐에 삽입되어 실행
- 너무 오래 기다리고 있는 프로세스가 있다면 점차 우선순위가 높은 큐로 이동시키는
에이징 기법을 적용한다.
기타) 스케줄링 성능 척도
- 이용률(Utilization): 전체 시간 중 CPU가 일한 시간의 비율 (높을수록 좋음)
- 처리량(Throughput): 단위 시간당 완료된 프로세스의 개수 (많을수록 좋음)
- 대기 시간(Waiting Time): 프로세스가 준비 큐에서 기다린 전체 시간의 합
- 응답 시간(Response Time): 프로세스가 준비 큐에 들어온 후 처음으로 CPU를 할당 받기까지 걸린 시간.
백엔드 Java 개발자 관점의 추가 고려 사항
JVM 스레드와 OS 스케줄링의 관계
- Java의 플랫폼 스레드는 운영체제의 네이티브 스레드와 1:1로 매핑되어 실행됨.
- 운영체제의 CPU 스케줄링 알고리즘이 Java 애플리케이션의 처리량 및 응답 시간에 직접적인 영향을 미침.
문맥 교환(Context Switching) 비용 제어
- 다수의 클라이언트 요청을 처리할 때 과도한 스레드 생성은 빈번한 문맥 교환을 유발함.
- 이는 CPU 오버헤드를 증가시키므로, 서버 구성 시 적절한 스레드 풀(Thread Pool) 크기를 설정해야 함.
I/O 집중 프로세스 최적화
- 백엔드 애플리케이션은 데이터베이스 조회나 외부 API 호출 등 I/O 집중 프로세스의 비중이 큼.
- 스레드가 I/O 대기 상태일 때 발생하는 자원 낭비를 줄이기 위해 비동기 프로그래밍 활용이 권장됨.
Java 스레드 상태와 OS 상태 매핑
💡 Java 스레드는 운영체제의 네이티브 스레드에 1:1로 매핑되어 실행된다. (Java 21 가상 스레드의 경우 M:N)
JVM이 스레드 관리를 운영체제에 위임하므로, 두 상태가 완벽히 일치하지는 않는다.
상태 매핑 상세
| Java 스레드 상태 | 운영체제 프로세스 상태 | 설명 |
|---|
NEW | 생성 (Created) | 객체는 생성되었으나 start() 메서드가 아직 호출되지 않은 상태임. |
RUNNABLE | 준비 (Ready) / 실행 (Running) | CPU 할당을 대기 중이거나 현재 CPU에서 실행 중인 상태를 모두 포함함. |
BLOCKED | 대기 (Waiting/Blocked) | 락(Lock)을 획득하기 위해 모니터 진입을 기다리며 실행이 중지된 상태임. |
WAITING / TIMED_WAITING | 대기 (Waiting/Blocked) | 다른 스레드의 작업 완료를 기다리거나, 지정된 시간 동안 대기하는 상태임. |
TERMINATED | 종료 (Terminated) | 스레드의 run() 메서드 실행이 완료되어 생명주기가 끝난 상태임. |
백엔드 개발 시 주의점
- 스레드가
BLOCKED 또는 WAITING 상태로 전환되면 운영체제는 문맥 교환을 수행할 수 있다.
- 이 과정에서 CPU 자원이 소모되므로, 불필요한 동기화로 인한 병목 현상을 최소화해야 함.
Java 21 가상 스레드와 문맥 교환 최적화
가상 스레드(Virtual Thread)
💡 기존 1:1 스레드 모델의 한계를 극복하고 문맥 교환 오버헤드를 줄이는 경량 스레드
동작 원리
- JVM이 다수의 가상 스레드를 소수의 운영체제 스레드(캐리어 스레드)에 매핑하여 다대일(M:N) 관계로 실행함.
- 개발자는 기존과 동일한 스레드 코드를 작성하면서도 운영체제 스레드 자원을 아낄 수 있음.
문맥 교환 오버헤드 감소
- 가상 스레드가 I/O 작업으로 인해 대기 상태가 되면, JVM은 해당 가상 스레드를 캐리어 스레드에서 분리(unmount)함.
- 캐리어 스레드는 대기하지 않고 즉시 다른 가상 스레드의 작업을 이어받아 처리함.
- 결과적으로 운영체제 수준의 무거운 문맥 교환을 피하고 JVM 내부에서 가볍게 스위칭이 이루어짐.
백엔드 적용 이점
- 높은 동시성을 요구하는 웹 서버 환경에서 스레드 생성 비용과 메모리 사용량을 획기적으로 줄일 수 있음.