
서비스 · 프로세스 관리 07 / 50 · Part 1. 프로세스 기초
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
ps가 "한 장의 사진"이라면 top은 실시간 영상 이다. 서버가 느려졌다는 연락을 받았을 때, 또는 CPU 사용률 경보가 울렸을 때 가장 먼저 여는 도구가 top이다. 하지만 화면에 숫자가 너무 많아 load average와 %Cpu, 메모리의 free와 avail을 헷갈리기 쉽다.
이번 글에서는 top 화면을 위에서 아래로 읽는 순서 를 정리하고, 관제 기록을 위한 배치 모드(-b), 정렬·사용자 필터·스레드 보기를 실습한다.
「리눅스 시스템 기초 33편」에서 top의 기본 화면을 소개했다. 이번 글은 수치 해석과 배치 모드 활용에 집중한다.
| 줄 | 항목 | 해석 |
|---|---|---|
| 1 | load average: 0.80, 0.23, 0.08 | 최근 1·5·15분 동안 R + D 상태 프로세스 수의 평균. 코어 수와 비교한다 |
| 2 | Tasks: ... zombie | 상태별 개수. zombie가 늘면 08편 |
| 3 | %Cpu(s) | CPU 시간 분류 (아래 표) |
| 4~5 | MiB Mem / Swap | 메모리. avail Mem 이 실제 쓸 수 있는 양 |
| 항목 | 의미 | 높을 때 의심 |
|---|---|---|
us | 사용자 공간 코드 실행 | 계산 작업, 크립토마이너 |
sy | 커널 코드 실행 | 과도한 시스템 콜, 네트워크 처리 |
ni | nice 값이 양수인 사용자 코드 | 우선순위 낮춘 배치 작업 (09편) |
id | 유휴 | — |
wa | I/O 완료 대기 중 유휴 | 디스크·NFS 병목 |
hi / si | 하드웨어/소프트웨어 인터럽트 | 대량 네트워크 트래픽 (DDoS 등) |
st | 하이퍼바이저가 가져간 시간 (steal) | 클라우드 VM 자원 경합 |
| 열 | 의미 |
|---|---|
PR / NI | 스케줄링 우선순위 / nice 값 |
VIRT | 가상 메모리 크기 (예약만 한 영역 포함) |
RES | 실제 물리 메모리 사용량 |
SHR | 공유 메모리 (라이브러리 등) |
S | 상태 (04편) |
%CPU | 직전 갱신 주기 동안 의 CPU 사용률. 코어 1개 = 100% |
TIME+ | 누적 CPU 시간 |

top은 갱신 주기(기본 3초)마다 /proc/stat(CPU), /proc/meminfo(메모리), /proc/loadavg, 그리고 모든 /proc/PID/stat을 읽는다. %CPU는 두 시점의 누적 CPU 시간 차이 로 계산한다.
t1: /proc/267/stat → utime+stime = 100 ticks
t2 (3초 후): utime+stime = 400 ticks
%CPU = (400 − 100) / (3초 × 100 ticks/초) = 100% ← 코어 1개를 꽉 채움
그래서 ps의 %CPU(수명 평균)와 top의 %CPU(최근 구간)는 다르게 나온다. 또 배치 모드의 첫 번째 화면은 비교할 이전 값이 짧아 부정확할 수 있으므로, 정확한 값이 필요하면 -n 2로 두 번 찍고 두 번째를 본다.
# 1) 환경 확인과 부하 만들기
nproc; uptime
timeout 30 sh -c 'while :; do :; done' & # CPU 부하
python3 -c 'b=bytearray(300*1024*1024); import time; time.sleep(30)' & # 메모리 300MB
# 2) 배치 모드로 한 화면 기록 (-b 배치, -n 횟수, -w 폭)
top -b -n 1 -w 110 | head -14
# 3) 메모리 순 정렬 + 사용자 필터
top -b -n 1 -w 110 -o %MEM -u analyst | sed -n '7,12p'
cat /proc/loadavg
# 4) (Ubuntu) 스레드별 보기(-H)와 2회 샘플링
top -b -H -n 1 -w 110 -p $P
top -b -n 2 -d 1 -w 110 | grep -E '^(%Cpu|MiB Mem)'
대화형 top에서는
P(CPU순),M(메모리순),u(사용자),H(스레드),c(전체 명령),1(코어별),k(kill)를 쓴다.k로 바로 종료하기 전에 반드시 증거를 먼저 수집한다.



텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ nproc; uptime
2
11:48:42 up 0 min, 1 user, load average: 0.78, 0.22, 0.07
[analyst@rocky9-lab ~]$ timeout 30 sh -c 'while :; do :; done' &
[analyst@rocky9-lab ~]$ python3 -c 'b=bytearray(300*1024*1024); import time; time.sleep(30)' &
[analyst@rocky9-lab ~]$ sleep 3; top -b -n 1 -w 110 | head -14
top - 11:48:45 up 1 min, 1 user, load average: 0.80, 0.23, 0.08
Tasks: 22 total, 2 running, 20 sleeping, 0 stopped, 0 zombie
%Cpu(s): 51.7 us, 0.0 sy, 0.0 ni, 48.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 8032.1 total, 6600.8 free, 977.1 used, 693.5 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 7054.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
267 analyst 20 0 4604 3236 2972 R 100.0 0.0 0:03.38 sh
1 root 20 0 24388 13964 10344 S 0.0 0.2 0:00.18 systemd
21 root 20 0 28512 11056 9804 S 0.0 0.1 0:00.04 systemd-journal
31 root 20 0 11788 7940 6696 S 0.0 0.1 0:00.01 sshd
32 root 20 0 20904 9704 8392 S 0.0 0.1 0:00.04 systemd-logind
37 root 20 0 4728 2708 2524 S 0.0 0.0 0:00.00 atd
38 root 20 0 6072 3564 2704 S 0.0 0.0 0:00.00 crond
[analyst@rocky9-lab ~]$ timeout 30 sh -c 'while :; do :; done' &
[analyst@rocky9-lab ~]$ python3 -c 'b=bytearray(300*1024*1024); import time; time.sleep(30)' &
[analyst@rocky9-lab ~]$ sleep 3; top -b -n 1 -w 110 -o %MEM -u analyst | sed -n '7,12p'
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
310 analyst 20 0 316996 314456 4768 S 0.0 3.8 0:00.14 python3
68 analyst 20 0 22460 12020 10236 S 0.0 0.1 0:00.02 systemd
284 analyst 20 0 18036 7616 5300 S 0.0 0.1 0:00.00 sshd-session
70 analyst 20 0 24436 4560 1916 S 0.0 0.1 0:00.00 (sd-pam)
312 analyst 20 0 7704 3856 3312 R 0.0 0.0 0:00.00 top
[analyst@rocky9-lab ~]$ cat /proc/loadavg
0.89 0.26 0.09 3/181 314
[analyst@rocky9-lab ~]$ kill $(jobs -p) 2>/dev/null; true
analyst@ubuntu-lab:~$ python3 -c 'import threading
> def spin():
> while True: pass
> [threading.Thread(target=spin,daemon=True).start() for _ in range(2)]
> import time; time.sleep(20)' &
analyst@ubuntu-lab:~$ sleep 2; P=$!; top -b -H -n 1 -w 110 -p $P | sed -n '7,12p'
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
307 analyst 20 0 162768 10224 6608 S 50.0 0.1 0:01.16 python3
308 analyst 20 0 162768 10224 6608 R 50.0 0.1 0:01.14 python3
305 analyst 20 0 162768 10224 6608 S 0.0 0.1 0:00.01 python3
analyst@ubuntu-lab:~$ top -b -n 2 -d 1 -w 110 | grep -E '^(%Cpu|MiB Mem)'
%Cpu(s): 95.2 us, 4.8 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 8032.1 total, 6872.1 free, 705.1 used, 694.2 buff/cache
%Cpu(s): 99.1 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.9 st
MiB Mem : 8032.1 total, 6872.1 free, 705.1 used, 694.2 buff/cache
analyst@ubuntu-lab:~$ kill $P
| 관찰 | 의미 |
|---|---|
nproc = 2 | 코어 2개. load 2.0이면 포화 상태다 |
%Cpu(s): 51.7 us, 48.3 id | 코어 2개 중 1개가 사용자 코드로 꽉 찼다 → 전체의 약 50% |
sh 프로세스 R 100.0 | 한 프로세스가 코어 하나를 100% 사용. top의 %CPU는 코어 단위라 멀티코어에서 100%를 넘을 수 있다 |
| load 0.80 (1분) | 부하를 막 걸어서 1분 평균이 1.0을 향해 올라가는 중이다. 15분 값(0.08)과의 차이로 최근에 시작된 부하 임을 알 수 있다 |
-o %MEM 정렬 1위 python3 RES 314456 (3.8%) | 300MB 배열이 실제 물리 메모리를 차지했다. VIRT(316996)와 RES가 거의 같은 이유는 bytearray가 0으로 채워져 실제로 접근된 메모리이기 때문 |
avail Mem 7054.9 vs free 6600.8 | buff/cache 중 회수 가능한 부분을 더한 값이 avail이다. 메모리 여유는 avail로 판단한다 |
/proc/loadavg = 0.89 0.26 0.09 3/181 314 | 앞의 3개는 load, 3/181은 실행 가능/전체 스레드 수, 314는 마지막 할당 PID |
Ubuntu -H: python3 스레드 307·308 각 50% | 스레드 두 개가 CPU를 나눠 쓴다. Python GIL 때문에 합쳐도 약 100%다 |
| 2회 샘플링: 95.2 us → 99.1 us | 첫 화면과 두 번째 화면의 값이 다르다. 정확한 값은 두 번째 화면 |
0.9 st | 가상화 호스트가 CPU를 잠시 가져간 시간. 실습 환경이 VM 위에 있다는 흔적이다 |
| 주제 | 내용 |
|---|---|
| 크립토마이너 | us가 장시간 높고, 모르는 프로세스가 코어 수만큼 %CPU를 쓰면 채굴을 의심한다. 일부는 top 실행을 감지하면 잠시 멈추는 기능이 있어 배치 모드 기록과 모니터링 추이가 필요하다 (47편) |
| 이름 위장 | top의 COMMAND는 comm(15자)이다. kworker, kthreadd 같은 커널 스레드 이름으로 위장한 사용자 프로세스는 c 키나 /proc/PID/exe로 확인한다 |
| si/hi 급증 | 소프트 인터럽트가 급증하고 네트워크 처리가 몰리면 DDoS나 대량 스캔을 의심한다 |
| 가용성 | wa·load 급증은 공격이 아니어도 서비스 장애다. 관제는 보안 이벤트와 가용성 이벤트를 함께 본다 |
[Detection] 모니터링 경보: web01 CPU 95% 30분 지속, 평소 20%
↓
[기록] top -b -n 2 -d 5 -w 200 > top_$(hostname)_$(date +%F_%H%M).txt
↓
[분석] %Cpu us 높음 / CPU 상위 프로세스 = "kdevtmpfsi" (UID www-data)
ps -o pid,ppid,lstart,args -ww -p <PID> ; ls -l /proc/<PID>/exe
↓
[판단] 커널 스레드 이름인데 PPID 1, 사용자 www-data, exe=/tmp/... → 채굴 악성코드
↓
[Response] 증거 수집 → 프로세스 정지 → 지속성(cron·systemd) 확인 → 침투 경로(웹) 조사
| 관점 | 확인 |
|---|---|
| 추이 | 순간값보다 모니터링 그래프의 시작 시점이 중요하다. 부하 시작 시각 = 침해 시각 후보 |
| 기록 | 대화형 화면은 남지 않는다. -b로 파일을 남겨 보고서에 첨부한다 |
| 기준 | load는 코어 수로 나누어 판단한다. 2코어의 load 2.0과 32코어의 load 2.0은 다르다 |
| 실수 | 결과 | 예방 |
|---|---|---|
| free 메모리가 적다고 장애 판단 | 캐시를 사용 중일 뿐인 정상 상태를 오판 | avail Mem 으로 판단 |
| load 값을 코어 수와 비교하지 않음 | 과대·과소 평가 | nproc과 함께 본다 |
| 배치 모드 첫 화면 수치를 그대로 보고 | 부정확한 %CPU | -n 2로 두 번째 값 사용 |
| %CPU 합이 100%를 넘어 이상하다고 판단 | 멀티코어에서는 정상 | 코어 1개 = 100% 기준 이해 |
top에서 바로 k로 종료 | 증거 소실 | 기록·수집 후 종료 |
[ ] load average 를 코어 수(nproc)와 비교해 해석했다
[ ] %Cpu 의 us, sy, wa, st 의미를 구분했다
[ ] free 와 avail Mem 의 차이를 설명할 수 있다
[ ] top -b -n 1 로 결과를 파일로 남길 수 있다
[ ] -o %MEM, -u USER 로 정렬·필터를 적용했다
[ ] -H 로 스레드별 CPU 사용량을 확인했다
top -b -n 2로 파일에 남긴다. 대화형 화면은 증거가 되지 않는다.다음 글 「08. 좀비 프로세스와 고아 프로세스」 에서는 04편에서 잠깐 본 <defunct>를 본격적으로 다룬다. 좀비는 왜 kill -9로 사라지지 않는지, 부모가 먼저 죽은 고아 프로세스는 누가 입양하는지 직접 확인한다.