이 글은 CPU 스케줄링 알고리즘과 Linux에서 프로세스와 포트를 관리하는 방법을 한 번에 정리합니다. 운영체제 이론에서 다루는 스케줄링의 목적과 대표 알고리즘을 설명하고, Linux 환경에서 프로세스를 모니터링하고 포트 충돌을 해결하는 실무 명령어까지 연결하여 정리합니다. 이를 통해 학습자는 면접 대비와 서버 운영 상황 모두에서 CPU 스케줄링과 프로세스·포트 관리 흐름을 스스로 설명할 수 있는 수준을 목표로 합니다.
운영체제는 동시에 실행하려는 프로세스가 많을 때 Ready 상태에 있는 프로세스 중 누구에게 CPU를 줄지 선택합니다.
이 선택을 수행하는 것이 CPU 스케줄링입니다.
스케줄링 알고리즘의 품질은 보통 다음 지표로 평가합니다.
| 지표 | 설명 | 목표 |
|---|---|---|
| CPU 이용률 | CPU가 일을 하는 시간의 비율 | 높게 유지합니다 |
| 처리량 | 단위 시간당 완료된 작업 수 | 많이 처리합니다 |
| 반환 시간 | 작업이 제출된 후 완료될 때까지의 총 시간 | 짧게 유지합니다 |
| 대기 시간 | Ready Queue에서 기다린 시간의 합 | 짧게 유지합니다 |
| 응답 시간 | 첫 응답이 돌아오기까지 걸린 시간 | 짧게 유지합니다 |
일반적으로 배치 처리 시스템은 처리량과 반환 시간을, 대화형 시스템은 응답 시간을 더 중시합니다.
프로세스는 실행 과정에서 CPU를 쓰는 구간과 I/O를 기다리는 구간을 반복합니다.
프로세스의 성격은 다음과 같이 나눌 수 있습니다.
| 타입 | 특징 | 예시 |
|---|---|---|
| CPU bound | CPU Burst가 길고 I/O가 적습니다 | 영상 인코딩, 과학 계산 |
| I/O bound | I/O Burst가 길고 CPU Burst는 짧습니다 | 웹 서버, 에디터, 터미널 |
스케줄링 알고리즘은 이 두 타입을 적절히 섞어서 CPU를 놀리지 않으면서 응답 시간도 확보하는 것을 목표로 합니다.
프로세스는 생명주기 동안 여러 상태를 거치게 됩니다. Ready Queue에 대기 중인 프로세스들 중에서 CPU 스케줄러가 다음에 실행할 프로세스를 선택하여 Running 상태로 전환합니다. 이 결정을 내리는 알고리즘이 바로 CPU 스케줄링 알고리즘입니다.
FCFS (First Come First Served)는 가장 단순한 비선점형 알고리즘입니다.
먼저 도착한 프로세스가 먼저 CPU를 전부 사용한 뒤 다음 프로세스가 실행합니다.
SJF (Shortest Job First)는 실행 시간이 가장 짧은 작업부터 실행합니다.
이 알고리즘은 이론적으로 평균 대기 시간을 최소화하는 최적 알고리즘입니다.
이를 완화하기 위해 Aging 기법을 사용합니다.
비선점형 스케줄링은 한 번 CPU를 할당받은 프로세스가 자발적으로 CPU를 반납할 때까지 계속 실행되는 방식입니다. 우선순위가 더 높은 프로세스가 도착하더라도 중간에 끼어들 수 없고, 컨텍스트 스위칭 횟수가 상대적으로 적습니다.
선점형 스케줄링은 운영체제가 필요하다고 판단하면 강제로 CPU를 회수하고 다른 프로세스에 할당할 수 있는 방식입니다. Time Quantum이 끝나거나 더 높은 우선순위 프로세스가 도착하면 문맥 교환이 발생하며, 대화형 환경에서 빠른 응답성을 확보하는 데 유리합니다.
우선순위 스케줄링은 각 프로세스에 우선순위 값을 부여하고, 우선순위가 높은 프로세스부터 실행합니다.
Round Robin은 시분할 시스템에서 가장 많이 사용하는 선점형 알고리즘입니다.
모든 프로세스에 동일한 시간 할당량 (Time Quantum)을 주고 돌려가며 실행합니다.
실무에서는 보통 수십 밀리초 수준의 Time Quantum을 사용합니다.
현대 운영체제는 하나의 스케줄링 알고리즘만 사용하는 대신 여러 큐와 정책을 조합해 사용하는 경우가 많습니다.
| 알고리즘 | 선점 여부 | 장점 | 단점 | 주요 사용 환경 |
|---|---|---|---|---|
| FCFS | 비선점형 | 구현이 단순하고 직관적입니다 | Convoy 현상으로 평균 대기 시간이 길어질 수 있습니다 | 배치 작업 위주의 시스템 |
| SJF SRTF | 비선점형 SJF, 선점형 SRTF | 평균 대기 시간이 이론적으로 최적입니다 | 실행 시간이 긴 작업의 Starvation이 발생할 수 있습니다 | 실행 시간 예측이 가능한 배치, 시뮬레이션 환경 |
| Priority | 비선점형과 선점형 모두 가능 | 중요도가 높은 작업을 먼저 처리할 수 있습니다 | 우선순위가 낮은 작업이 기약 없이 밀릴 수 있습니다 | 시스템 프로세스, 실시간에 가까운 작업 |
| Round Robin | 선점형 | 모든 프로세스가 공평하게 CPU를 나눠 가지며 응답성이 좋습니다 | Time Quantum 설정이 부적절하면 오버헤드 또는 응답성 저하가 발생합니다 | 터미널, GUI 환경 등 대화형 시스템 |
| Multilevel Queue | 보통 선점형 | 프로세스 성격별로 서로 다른 정책을 적용할 수 있습니다 | 큐 간 이동이 불가능하면 특정 큐가 쉽게 과부하 상태가 될 수 있습니다 | 시스템, 대화형, 배치 작업을 함께 다루는 범용 OS |
| Multilevel Feedback Queue | 선점형 | CPU bound와 I O bound를 동적으로 분리하며 Starvation을 완화합니다 | 설계와 튜닝이 복잡합니다 | 범용 데스크톱, 서버 OS에서 널리 사용되는 개념적 모델 |
예를 들어 java -jar app.jar를 실행하면
app.jar는 프로그램이고 Linux는 기본 CPU 스케줄러로 CFS Completely Fair Scheduler를 사용합니다.
이 스케줄러의 목표는 모든 프로세스가 동시에 실행되는 이상적인 멀티태스킹을 공평하게 근사하는 것입니다.
핵심 개념은 다음과 같습니다.
결과적으로 CFS는 적게 실행된 프로세스에 먼저 CPU를 주는 방식으로 공평성을 보장합니다.
Linux는 각 프로세스에 PID Process ID를 부여합니다.
PID를 통해 프로세스를 식별하고 제어합니다.
fork를 통해 자식 프로세스로 생성됩니다. ps나 top에서 자주 보게 되는 상태는 다음과 같습니다.
| 상태 | 표기 | 설명 |
|---|---|---|
| Running | R | 실행 중이거나 실행을 기다리는 상태입니다 |
| Sleeping | S | I O를 기다리며 잠든 상태입니다 인터럽트 가능합니다 |
| Uninterruptible Sleep | D | 특정 I O 작업을 기다리는 중이며 인터럽트할 수 없습니다 |
| Stopped | T | 일시 정지 상태입니다 보통 Ctrl+Z로 진입합니다 |
| Zombie | Z | 종료되었지만 부모가 상태를 회수하지 않은 상태입니다 |
Zombie 프로세스는 실제로 CPU와 메모리를 거의 사용하지 않지만,
부모 프로세스의 버그를 의미하는 경우가 많기 때문에 주의해서 확인합니다.
# 현재 터미널에 연결된 프로세스
ps
# 시스템의 모든 프로세스를 상세하게
ps aux
# 트리 형태로 확인
ps auxf
# 특정 패턴 검색
ps aux | grep java
주로 보는 컬럼은 USER, PID, %CPU, %MEM, STAT, COMMAND입니다.
CPU와 메모리를 많이 사용하는 프로세스를 빠르게 찾을 수 있습니다.
top
top은 실시간으로 프로세스를 모니터링하는 도구입니다.
자주 사용하는 키는 다음과 같습니다.
q 종료 P CPU 사용률 기준 정렬 M 메모리 사용률 기준 정렬 1 CPU 코어별 사용률 표시 k 특정 PID에 시그널 전송 프로세스를 종료할 때는 시그널 Signal을 보냅니다.
자주 사용하는 시그널은 다음과 같습니다.
| 시그널 | 번호 | 설명 |
|---|---|---|
| SIGTERM | 15 | 정상 종료 요청입니다 기본값입니다 |
| SIGKILL | 9 | 강제 종료입니다 즉시 종료합니다 |
| SIGINT | 2 | 인터럽트입니다 터미널의 Ctrl+C와 동일합니다 |
| SIGHUP | 1 | 세션 종료 또는 설정 재로딩에 사용합니다 |
실무에서는 다음 순서를 권장합니다.
# 1단계 정상 종료 요청
kill 1234 # 또는 kill -15 1234
# 잠시 기다린 뒤에도 종료되지 않으면
# 2단계 강제 종료
kill -9 1234
Spring Boot나 다른 서버 애플리케이션은 SIGTERM을 기준으로 Graceful Shutdown을 구현하는 경우가 많습니다.
따라서 가능하면 SIGTERM을 먼저 보내서 정상 종료와 리소스 정리가 이루어지도록 합니다.
포트 Port는 하나의 IP에서 여러 네트워크 서비스를 구분하기 위한 논리적인 번호입니다. IP 주소가 컴퓨터를 식별한다면 포트는 컴퓨터 내의 특정 프로세스를 식별합니다.
예를 들어 다음과 같이 이해할 수 있습니다.
192.168.0.10:22는 SSH 서버입니다. 192.168.0.10:80은 웹 서버 HTTP입니다. 192.168.0.10:5432는 PostgreSQL 데이터베이스입니다.| 범위 | 이름 | 설명 | 예시 |
|---|---|---|---|
| 0에서 1023 | Well known 포트 | 표준 서비스가 사용하는 포트입니다 | HTTP 80, HTTPS 443, SSH 22 |
| 1024에서 49151 | Registered 포트 | 등록된 서비스가 사용하는 포트입니다 | 3306 MySQL, 5432 PostgreSQL |
| 49152에서 65535 | Dynamic 포트 | 클라이언트 측에서 임시로 사용하는 포트입니다 | 웹 브라우저의 임시 소켓 |
서버를 구성할 때는 일반적으로 1024 이상 포트에서 애플리케이션 포트를 선택합니다.
TCP 기반 서버에서 자주 보이는 상태는 다음과 같습니다.
결국 실무에서는 어떤 프로세스가 어떤 포트에서 LISTEN 중인지 확인하는 것이 포트 관련 문제 해결의 핵심입니다.
# 특정 포트를 사용하는 프로세스 확인
lsof -i :8080
# 특정 PID가 가진 소켓과 파일 확인
lsof -p 1234
# 리스닝 중인 TCP 포트 전체 확인
lsof -i -sTCP:LISTEN
lsof -i :포트번호는 포트 충돌이 발생했을 때 가장 먼저 사용하는 명령어입니다.
# 리스닝 중인 TCP 포트와 프로세스
netstat -tlnp
# 특정 포트 필터링
netstat -tlnp | grep 8080
-t TCP -l LISTEN 상태 -n 숫자 주소로 표시 -p 프로세스 정보 표시 # 리스닝 포트 확인
ss -tlnp
# ESTABLISHED 상태만 확인
ss -t state established
# 특정 포트 검색
ss -tlnp | grep 8080
ss는 netstat보다 빠르게 동작하며 최신 Linux에서 권장되는 도구입니다.
웹 애플리케이션을 실행했더니 Port 8080 already in use 오류가 발생하는 상황을 예로 들겠습니다.
lsof -i :8080
출력 결과에서 PID와 COMMAND를 확인합니다.
의도하지 않은 프로세스라면 종료를 검토합니다.
# 우선 정상 종료 시도
kill 1234
# 종료되지 않을 경우 강제 종료
kill -9 1234
이후 다시 lsof -i :8080으로 포트가 비었는지 확인합니다.
kill $(lsof -t -i :8080)
이렇게 하면 포트를 사용하는 PID를 자동으로 찾아서 종료합니다.
API 서버 장애가 발생했을 때 CPU와 메모리, 네트워크 상태를 빠르게 확인하는 예시입니다.
# CPU 사용 상위 10개 프로세스
ps aux --sort=-%cpu | head -10
# 메모리 사용 상위 10개 프로세스
ps aux --sort=-%mem | head -10
# Java 프로세스만 top으로 모니터링
top -p $(pgrep java | tr '\n' ',' | sed 's/,$//')
# TCP ESTABLISHED 연결 수 확인
ss -t state established | wc -l
이 정도 패턴을 외워 두면 서버 부하가 걸렸을 때 병목 지점을 빠르게 의심하고 파고들 수 있습니다.
간단한 테스트나 임시 서버는 다음처럼 실행합니다.
# 단순 백그라운드 실행
java -jar app.jar &
터미널을 닫으면 함께 종료될 수 있기 때문에 운영 환경에는 적합하지 않습니다.
보다 안전하게 실행하려면 nohup을 사용합니다.
nohup java -jar app.jar > app.log 2>&1 &
echo $! > app.pid
app.log에 기록합니다. app.pid에 저장합니다.종료할 때는 다음과 같이 수행합니다.
kill $(cat app.pid)
운영 환경에서는 가능하면 systemd 서비스나 프로세스 매니저 supervisord 등을 사용해 관리하는 편이 더 안전합니다.
API 서버에서 특정 클라이언트 IP의 트래픽이 폭주하는지 확인하고 싶을 때는 ss와 로그를 함께 활용합니다.
# 특정 IP에서 들어온 ESTABLISHED TCP 연결 확인
ss -t state established src 203.0.113.10
# Nginx 로그에서 해당 IP가 보낸 요청 수 집계 예시
grep "203.0.113.10" /var/log/nginx/access.log | wc -l
이렇게 연결 수와 요청 수를 함께 확인하면 특정 클라이언트의 비정상적인 트래픽 패턴을 빠르게 파악할 수 있습니다.
필요하다면 Web Application Firewall 설정이나 Rate Limit 정책을 추가해 방어할 수 있습니다.
정리 차원에서 면접에서 자주 물을 수 있는 포인트를 모아 봅니다.
이 정도 내용을 자신의 말로 정리해서 설명할 수 있으면 운영체제와 Linux 기본 운영에 대한 질문에 충분히 대응할 수 있습니다.
실제 운영체제는 단일 알고리즘만 사용하는 경우가 거의 없습니다.
보통 우선순위, 인터랙티브 여부, 실시간 여부에 따라 서로 다른 알고리즘을 조합합니다.
Linux도 일반 프로세스에는 CFS를, 실시간 프로세스에는 별도의 실시간 스케줄링 정책을 사용합니다.
너무 작으면 컨텍스트 스위칭 오버헤드가 증가하고,
너무 크면 응답성이 떨어져 FCFS와 비슷하게 동작합니다.
일반적으로 컨텍스트 스위칭 시간의 여러 배 수준, 즉 수십 밀리초 정도를 기준으로 조정합니다.
실제 값은 워크로드의 특성과 사용자의 응답 요구 시간에 맞춰 실험적으로 튜닝합니다.
항상 그런 것은 아닙니다.
먼저 해당 포트가 원래 의도한 서비스가 사용하는지 확인합니다.
- 만약 원래 떠 있어야 할 서버라면 애플리케이션의 포트를 변경해서 실행합니다.
- 테스트용 서버나 이미 종료되어야 할 프로세스가 잡고 있다면,
lsof -i :포트로 PID를 확인한 뒤 종료합니다.
운영 환경에서는 어떤 프로세스를 종료하는지 확실히 파악한 뒤 kill을 실행하는 습관이 중요합니다.
TIME WAIT 상태 자체는 정상적인 TCP 종료 과정의 일부입니다.
다만 단기간에 매우 많은 연결을 생성하고 끊는 시스템에서는 TIME WAIT 소켓이 수만 개 이상 쌓일 수 있습니다.
이 경우 포트 고갈이나 커널 리소스 부족이 발생할 수 있으므로
- 커넥션 재사용
- Keep Alive 설정
- 커널 파라미터 튜닝
등을 함께 고려합니다.
개인 개발 환경이나 간단한 테스트라면
nohup command &만으로도 충분합니다.
그러나 운영 환경에서는 systemd와 같은 서비스 매니저를 사용하는 편이 훨씬 안전합니다.
- 자동 재시작 설정
- 로그 관리
- 의존성 관리
등을 표준화된 방법으로 처리할 수 있기 때문입니다.
한 번에 확인하는 방법은
lsof -i -a -p $(pgrep -f "app.jar")입니다. 먼저ps aux | grep "app.jar"로 PID를 찾은 다음lsof -i -a -p 2000으로 해당 PID의 포트를 확인할 수 있습니다. 결과에는 애플리케이션이 리스닝하는 포트(예: 8080, 8081)와 외부 연결 포트(예: MySQL 3306)가 모두 표시됩니다.
Linux는 CFS(Completely Fair Scheduler)를 사용합니다. 모든 프로세스에 공평하게 CPU 시간을 배분하며 vruntime 기반으로 선택합니다. Red-Black Tree로 O(log n) 시간에 관리하며 2007년 Linux 2.6.23부터 기본 스케줄러로 사용됩니다.
chrt -p 2000명령어로 확인하면 SCHED_OTHER로 표시되는데, 이것이 CFS 스케줄러입니다.
TIME_WAIT는 정상적인 TCP 연결 종료 과정의 일부입니다. TCP 연결 종료 시 약 1~4분간 2MSL 동안 대기하며 지연된 패킷을 처리하고 연결 종료를 확실히 합니다.
정상적인 경우는 웹 서버에서 많은 연결을 처리할 때 자연스럽게 발생하며 자동으로 정리됩니다. 문제가 되는 경우는 수만 개 이상 쌓이거나 새 연결 생성이 실패하거나 포트가 고갈될 때입니다.
해결책으로는 Connection Pool 사용, Keep-Alive 활성화, 커널 파라미터 조정이 있습니다.
sysctl -w net.ipv4.tcp_tw_reuse=1이 설정은 TIME_WAIT 소켓을 재사용하여 포트 고갈을 방지합니다. 그러나 무분별한 조정은 네트워크 문제를 야기할 수 있으므로 신중하게 적용해야 합니다.
세 가지 모두 백그라운드 실행과 관련되어 있지만 동작 방식이 다릅니다.
&는 백그라운드 실행만 수행합니다.
java -jar app.jar &터미널 종료 시 프로세스도 종료되며 간단한 테스트용으로 적합합니다.
nohup은 터미널 종료 후에도 프로세스를 계속 실행합니다.
nohup java -jar app.jar &터미널 종료 후에도 프로세스가 살아있으며 출력이 nohup.out 파일에 저장됩니다.
출력 리다이렉트와 함께 사용하는 것이 권장됩니다.
nohup java -jar app.jar > app.log 2>&1 &출력을 app.log에 저장하며 백그라운드에서 실행하고 터미널 종료 후에도 계속 실행됩니다.
disown은 실행 중인 작업을 터미널에서 분리합니다.
java -jar app.jar & disown %1이미 실행 중인 프로세스를 터미널에서 분리하여 터미널 종료 후에도 계속 실행되도록 합니다.
실무에서는 systemd나 Docker 사용을 권장합니다. systemd는 서비스 관리가 체계적이며 자동 재시작과 로그 관리가 용이합니다. Docker는 격리된 환경에서 실행되며 배포와 확장이 편리합니다.
제한적으로 가능합니다. Java에서 할 수 있는 것은 스레드 우선순위 설정, 스레드 양보, 스레드 슬립, 스레드 조인입니다.
스레드 우선순위를 설정하는 방법은 다음과 같습니다.
Thread thread = new Thread(runnable); thread.setPriority(Thread.MAX_PRIORITY);1부터 10까지 설정할 수 있습니다.
스레드를 양보하는 방법입니다.
Thread.yield();다른 스레드에게 CPU를 양보합니다.
Java에서 할 수 없는 것은 OS 레벨 스케줄링 알고리즘 변경, 다른 프로세스 우선순위 변경, CPU 코어 직접 지정입니다. 일부 JVM은 CPU 코어 지정이 가능하지만 제한적입니다.
setPriority()는 힌트일 뿐 OS가 무시할 수 있으며 JVM과 OS 스케줄러가 모두 관여합니다. OS마다 동작이 다를 수 있어 이식성 문제가 발생할 수 있습니다.
nice 값은 -20부터 19까지 범위를 가지며 -20은 가장 높은 우선순위로 root만 설정 가능하고 0은 기본값이며 19는 가장 낮은 우선순위입니다.
실행 시 우선순위를 지정하는 방법입니다.
nice -n 10 java -jar app.jar낮은 우선순위로 실행하여 백그라운드 작업에 적합합니다.
실행 중인 프로세스 우선순위를 변경하는 방법입니다.
renice -n 5 -p 2000PID 2000의 nice 값을 5로 변경합니다.
현재 nice 값을 확인하는 방법입니다.
ps -o pid,nice,comm -p 2000출력 예시입니다.
PID NI COMMAND 2000 5 java실무 예시로 배치 작업은 nice 값을 높여 낮은 우선순위로 실행합니다.
nice -n 15 java -jar batch-job.jar실시간 API는 nice 값을 낮춰 높은 우선순위로 실행하며 root 권한이 필요합니다.
sudo nice -n -10 java -jar api-server.jar일반 사용자는 nice 값을 높이기만 가능하며 낮추려면 root 권한이 필요합니다. 과도한 조정은 시스템 불안정을 초래할 수 있으므로 신중하게 적용해야 합니다.