리눅스 프로세스: ps, top, htop, kill, 시그널과 nice 정리
리눅스 프로세스는 ps aux(한 번의 스냅숏), top과 htop(실시간 부하 목록)으로 확인합니다. kill PID는 기본적으로 정상 종료 요청인 SIGTERM을 보내며, kill -9(SIGKILL)는 프로세스가 반응하지 않을 때만 씁니다. 우선순위는 실행 시 nice로, 이미 실행 중인 프로세스는 renice로 바꾸며 값 범위는 -20부터 19까지입니다.
리눅스에서 실행 중인 모든 프로그램은 고유한 PID, 소유자, 상태, 우선순위를 가진 프로세스입니다. 프로세스는 ps, top, htop으로 볼 수 있고, kill로 종료하며, nice와 renice로 우선순위를 조정합니다. 아래에서는 이 명령들의 출력을 읽는 법, 상황에 맞는 시그널, 그리고 kill -9를 첫 번째 선택지로 쓰면 안 되는 이유를 설명합니다.
프로세스와 PID
프로세스는 실행 중인 프로그램입니다. 코드, 메모리, 열린 파일, 여러 속성을 함께 가집니다. 커널은 프로세스마다 고유한 식별자인 PID를 부여하고, 그 프로세스를 실행한 부모의 번호인 PPID도 기록합니다. PID 1은 보통 systemd이며, 다른 모든 프로세스의 조상이 됩니다.
새 프로세스는 두 단계로 생깁니다. 먼저 부모가 fork()를 호출해 자신의 복사본을 만들고, 복사본이 execve()를 호출해 자신의 코드를 새 프로그램의 코드로 바꿉니다. 터미널에 ls를 입력하면 bash가 정확히 이렇게 하고, wait()로 자식이 끝나기를 기다립니다.
$ echo $$ # 현재 셸의 PID
4182
$ ps -o pid,ppid,cmd -p $$
PID PPID CMD
4182 4170 -bash
ps로 프로세스 보기
ps는 한 번의 스냅숏을 보여 주고 바로 끝납니다. 인자 없이 실행하면 현재 터미널의 프로세스만 나오므로, 실무에서는 보통 다음 두 가지 중 하나를 씁니다.
$ ps aux | head -5
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 168540 12780 ? Ss 09:12 0:03 /sbin/init
root 2 0.0 0.0 0 0 ? S 09:12 0:00 [kthreadd]
www-data 1532 1.2 2.1 412308 86420 ? S 09:15 1:47 php-fpm: pool www
postgres 1610 0.4 3.8 221004 154312 ? Ss 09:15 0:31 postgres: checkpointer
여기서 a는 모든 사용자의 프로세스, u는 소유자를 포함한 자세한 형식, x는 터미널이 없는 프로세스(데몬)까지 포함한다는 뜻입니다. System V 스타일 문법인 ps -ef도 있습니다. 이 형식은 PPID를 보여 주므로 누가 누구를 실행했는지 알고 싶을 때 편합니다.
| 열 | 의미 |
|---|---|
PID | 프로세스 식별자 |
%CPU, %MEM | CPU 사용 비율과 물리 메모리 사용 비율 |
VSZ | 가상 메모리 크기(KB), 물리 메모리에 올라가지 않은 부분 포함 |
RSS | 실제로 점유 중인 물리 메모리(KB) |
STAT | 상태(R, S, D, Z, T)와 추가 표시 |
TIME | 누적 CPU 시간이며, 실행된 경과 시간이 아님 |
ps 활용 팁
# 메모리를 많이 쓰는 상위 10개
ps aux --sort=-%mem | head -11
# CPU를 많이 쓰는 상위 10개
ps aux --sort=-%cpu | head -11
# 원하는 열만 출력
ps -eo pid,ppid,user,ni,stat,etime,cmd
# 프로세스 트리
ps -ef --forest
pstree -p
# 이름으로 PID 찾기
pgrep -a nginx
pidof nginx
흔한 실수는 ps aux | grep nginx로 찾다가 출력에 grep 자신이 섞이는 것입니다. pgrep을 쓰거나 grep [n]ginx 트릭을 쓰십시오. 이 패턴은 grep 자신의 명령줄과 일치하지 않습니다.
프로세스 상태: R, S, D, Z, T
STAT 열의 글자는 장식이 아니라 프로세스의 실제 상태입니다. 부하를 어떻게 해석할지, 그리고 kill이 효과가 있을지가 여기에 달려 있습니다.
| 코드 | 상태 | 실무에서의 의미 |
|---|---|---|
R | Running | 실행 중이거나 CPU를 기다리는 중 |
S | Interruptible sleep | 이벤트(네트워크, 입력, 타이머)를 기다림. 대부분의 프로세스가 평소 이 상태 |
D | Uninterruptible sleep | 커널 안에서 I/O를 기다림. 시그널을 받지 않음 |
Z | Zombie | 종료되었지만 부모가 종료 코드를 수거하지 않음 |
T | Stopped | SIGSTOP이나 Ctrl+Z로 일시 정지됨 |
상태 글자 뒤에 붙는 기호도 도움이 됩니다. s는 세션 리더, +는 터미널의 포그라운드 프로세스, <는 높은 우선순위, N은 낮은 우선순위, l은 멀티스레드를 뜻합니다.
D 상태의 프로세스
D 상태의 프로세스는 시스템 호출 안에서 디스크 응답이나 멈춘 NFS를 기다리고 있습니다. SIGKILL을 포함해 어떤 시그널도 전달되지 않습니다. 따라서 kill -9는 소용이 없습니다. 대기의 원인을 찾아야 합니다. iostat, dmesg, 네트워크 스토리지 상태를 확인하십시오.
좀비 프로세스
좀비는 이미 죽은 프로세스입니다. CPU와 메모리는 쓰지 않고 프로세스 테이블의 한 줄만 차지합니다. 이미 끝난 상태이므로 죽일 수도 없습니다. 부모가 wait()를 호출하거나, 부모 자체가 끝나서 좀비를 PID 1이 넘겨받으면 사라집니다.
# 좀비와 그 부모 찾기
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/'
좀비가 한두 개라면 해롭지 않습니다. 수백 개가 있고 계속 늘어난다면 부모 프로그램에 버그가 있는 것이므로, 고치거나 부모를 재시작해야 합니다.
top: 실시간 프로세스와 부하
top은 같은 목록을 몇 초마다 갱신하면서 시스템 요약도 보여 줍니다. "서버가 느려졌다"는 신고를 받으면 가장 먼저 실행하는 명령입니다.
top - 14:21:07 up 12 days, 3:02, 2 users, load average: 1.84, 1.20, 0.95
Tasks: 213 total, 2 running, 211 sleeping, 0 stopped, 0 zombie
%Cpu(s): 23.1 us, 4.2 sy, 0.0 ni, 70.3 id, 1.9 wa, 0.0 hi, 0.5 si, 0.0 st
MiB Mem : 7821.4 total, 412.6 free, 3105.8 used, 4303.0 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 4398.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1532 www-data 20 0 412308 86420 14212 R 38.2 1.1 1:47.31 php-fpm
1610 postgres 20 0 221004 154312 148900 S 6.0 1.9 0:31.08 postgres
가장 중요한 줄은 %Cpu(s)입니다. us는 사용자 코드, sy는 커널, id는 유휴, wa는 디스크 대기입니다. CPU는 한가한데 wa가 높다면 병목이 입출력에 있다는 확실한 신호입니다.
top의 단축키
| 키 | 동작 |
|---|---|
P / M | CPU 기준 / 메모리 기준 정렬 |
1 | 코어를 하나씩 따로 표시 |
c | 전체 명령줄 표시 |
u | 사용자로 필터링 |
k | 프로세스에 시그널 보내기 |
r | 프로세스의 nice 값 변경 |
H | 스레드 표시 |
q | 종료 |
스크립트나 로그에서는 배치 모드가 유용합니다. top -b -n 1 | head -20은 대화형 입력 없이 스냅숏 한 장을 출력합니다. 특정 프로세스만 보려면 top -p 1532를 쓰십시오.
Load Average는 비율이 아니라 대기열입니다
load average의 세 숫자는 각각 최근 1분, 5분, 15분 동안의 평균 실행 대기열 길이입니다. 리눅스에서는 R과 D 상태의 프로세스가 여기에 들어가며, 일반적인 S 대기는 들어가지 않습니다. 값은 코어 수(nproc)와 비교해서 읽어야 합니다. 코어가 4개일 때 값이 4면 완전히 찬 상태이고, 12면 처리 능력의 세 배에 달하는 대기열이 쌓여 있다는 뜻입니다.
Load average는 높은데 CPU가 놀고 있다면 D 상태의 프로세스를 찾아보십시오: ps -eo stat,pid,cmd | grep '^D'. 거의 항상 느린 디스크나 네트워크 스토리지가 원인입니다.
htop: top의 편리한 대체
htop은 같은 정보를 코어별 색상 막대, 스크롤, 마우스 지원, 프로세스 트리와 함께 보여 줍니다. 보통 설치해야 합니다: sudo apt install htop 또는 sudo dnf install htop.
F3또는/— 이름으로 프로세스 검색F4— 일치하는 프로세스만 남기는 필터F5또는t— "부모-자식" 트리 보기F6— 정렬 열 선택F7/F8— nice 값 낮추기 / 높이기F9— 선택한 프로세스에 보낼 시그널 메뉴Space— 여러 프로세스를 표시해 한꺼번에 처리
설치할 수 없는 서버라면 top으로도 충분합니다. 여기 나온 핵심 기능은 모두 top에도 있습니다.
kill로 프로세스 종료하기: 시그널
kill은 프로세스를 "죽이는" 것이 아니라 시그널을 보냅니다. 그다음에 무슨 일이 일어날지는 시그널의 종류와 프로그램이 그 시그널을 어떻게 처리하는지에 달려 있습니다.
| 시그널 | 번호 | 용도 |
|---|---|---|
SIGHUP | 1 | 터미널 연결 끊김. 많은 데몬이 설정을 다시 읽는 신호로 씀 |
SIGINT | 2 | 중단. Ctrl+C와 같음 |
SIGKILL | 9 | 커널이 강제로 종료. 잡거나 무시할 수 없음 |
SIGTERM | 15 | 정상 종료 요청. kill의 기본값 |
SIGSTOP | 19 | 일시 정지. 잡거나 무시할 수 없음 |
SIGCONT | 18 | 정지된 프로세스 재개 |
SIGTSTP | 20 | 터미널에서의 일시 정지. Ctrl+Z |
번호는 x86과 ARM 기준입니다. 전체 목록은 kill -l로 볼 수 있습니다. 번호보다 이름으로 지정하는 편이 안전합니다.
kill 1532 # SIGTERM
kill -TERM 1532 # 같은 동작을 명시적으로
kill -HUP $(pidof nginx) # 설정 다시 읽기
kill -STOP 1532; kill -CONT 1532
kill -0 1532 && echo "프로세스가 살아 있습니다"
시그널 0은 아무것도 보내지 않고, 프로세스가 존재하는지와 그 프로세스에 시그널을 보낼 권한이 있는지만 확인합니다. 스크립트에서 유용합니다.
왜 kill -9부터 쓰면 안 되는가
SIGTERM을 받은 프로그램은 파일을 닫고, 버퍼를 비우고, 잠금 파일을 지우고, 트랜잭션을 정상적으로 마칠 기회를 얻습니다. SIGKILL은 모든 것을 즉시 끊습니다. 쓰이던 데이터는 사라지고, 임시 파일이 남으며, 데이터베이스는 다음 시작 때 복구 과정을 거칠 수 있습니다.
올바른 순서는 다음과 같습니다.
kill PID로 SIGTERM을 보냅니다.- 5~30초 기다리며
kill -0 PID로 확인합니다. - 프로세스가 아직 살아 있고
D상태가 아닐 때만kill -9 PID를 씁니다.
systemd 서비스라면 PID를 직접 다루지 않는 편이 좋습니다. systemctl stop이 SIGTERM을 보내고, 시간 제한을 기다린 뒤 SIGKILL로 마무리해 줍니다.
pkill과 killall: 이름으로 종료
pkill -f "python worker.py" # 전체 명령줄 기준
pkill -u alice # 사용자의 모든 프로세스
killall -TERM node # 이름이 node인 모든 프로세스
pgrep -l -f worker # 패턴에 걸리는 대상을 먼저 확인
대량 pkill 전에는 같은 패턴으로 pgrep을 먼저 실행하십시오. -f는 명령줄의 어느 부분과도 일치하므로 엉뚱한 프로세스까지 잡기 쉽습니다.
자신의 프로세스에만 시그널을 보낼 수 있습니다. 다른 사용자의 프로세스에는 sudo가 필요합니다. Operation not permitted 오류는 바로 이 뜻입니다.
우선순위: nice와 renice
nice 값은 CPU를 누구에게 양보할지 스케줄러에 알려 주는 힌트입니다. 범위는 -20(가장 높은 우선순위)부터 19(모두에게 양보)까지이며, 기본값은 0입니다. top에서는 NI 열로 보이며, PR은 커널 내부의 파생 우선순위입니다.
# 무거운 작업을 낮은 우선순위로 실행
nice -n 10 tar czf backup.tar.gz /var/www
# 실행 중인 프로세스의 우선순위 낮추기
renice -n 15 -p 1532
# 우선순위 높이기 (root만 가능)
sudo renice -n -5 -p 1610
# 사용자의 모든 프로세스
sudo renice -n 10 -u alice
일반 사용자는 자신의 프로세스 우선순위를 낮출 수만 있고, 음수 값은 root만 설정할 수 있습니다. nice는 CPU에만 영향을 준다는 점도 기억하십시오. 작업이 디스크를 독점해서 시스템이 느려진다면 ionice -c3 명령을 쓰십시오. idle 클래스는 디스크를 다른 프로세스가 쓰지 않을 때만 사용합니다.
백그라운드 프로세스와 셸 작업
명령 끝에 &를 붙이면 백그라운드에서 실행되고, Ctrl+Z로 일시 정지할 수도 있습니다. 셸은 작업 목록을 관리하며, 이를 내장 명령으로 다룹니다.
$ long_task &
[1] 5120
$ jobs
[1]+ Running long_task &
$ fg %1 # 포그라운드로 가져오기
^Z # 일시 정지 (SIGTSTP)
$ bg %1 # 백그라운드에서 재개
터미널을 닫으면 셸은 작업에 SIGHUP을 보내고, 작업은 종료됩니다. 터미널이 닫혀도 살아 있으려면 nohup 명령 &로 실행하거나, disown으로 셸의 작업 목록에서 분리하거나, tmux를 쓰십시오. 계속 돌아야 하는 작업이라면 systemd 서비스로 만드는 것이 가장 바람직합니다.
자주 쓰는 진단 시나리오
CPU를 누가 쓰고 있나
top -o %CPU # 또는 top 안에서 P
ps -eo pid,user,%cpu,etime,cmd --sort=-%cpu | head
쓸모없이 CPU를 먹는 프로세스가 있다면, 먼저 renice로 우선순위를 낮추십시오. 안전하고, 원인을 파악할 시간을 줍니다.
포트를 누가 점유하고 있나
sudo ss -ltnp | grep :8080
sudo lsof -i :8080
출력에 PID가 나오면 /proc/PID/에서 실행 파일(exe), 작업 디렉터리(cwd), 인자(cmdline)를 찾을 수 있습니다.
프로세스가 갑자기 사라졌다
흔한 원인은 메모리가 부족할 때 프로세스를 죽이는 OOM Killer입니다. 확인하려면 dmesg -T | grep -i oom 또는 journalctl -k | grep -i "killed process"를 실행하십시오.
정리
리눅스 프로세스를 다루는 데는 몇 가지 명령이면 충분합니다. 스냅숏은 ps aux, 관찰은 top과 htop, 종료는 올바른 시그널을 쓴 kill, 우선순위는 nice와 renice입니다. 행동하기 전에 프로세스의 상태를 먼저 읽으십시오. 좀비와 D 상태 프로세스는 시그널로 해결되지 않습니다. 그리고 SIGTERM부터 시작하고, kill -9는 최후의 수단으로 남겨 두십시오. 프로세스, 메모리, load average, 시스템 호출은 강의 실습에서 자세히 다룹니다.
자주 묻는 질문
ps aux와 ps -ef는 무엇이 다른가요?
같은 명령의 두 가지 문법입니다. aux는 %CPU, %MEM, STAT 열을 보여 주는 BSD 스타일이고, -ef는 PPID 열을 보여 주는 System V 스타일입니다. 두 경우 모두 시스템의 모든 프로세스가 나옵니다.
시스템에서 실행 중인 프로세스 수는 어떻게 확인하나요?
top의 Tasks 줄을 보거나 ps -e --no-headers | wc -l을 실행하십시오. 스레드 수는 ps -eLf --no-headers | wc -l로 셀 수 있습니다.
kill -9를 써도 프로세스가 끝나지 않는 이유는 무엇인가요?
프로세스가 D 상태여서 커널에서 I/O를 기다리거나, 이미 끝난 좀비일 수 있습니다. 전자라면 디스크나 네트워크 스토리지 문제를, 후자라면 부모 프로그램을 고치거나 재시작해야 합니다.
PID 1을 종료하면 어떻게 되나요?
커널은 init을 보호합니다. init이 처리하지 않는 일반 시그널, 사용자가 보낸 SIGKILL도 전달되지 않습니다. 그래도 init이 끝나면 시스템은 커널 패닉에 빠집니다.
프로세스의 메모리 사용량을 제한하려면 어떻게 하나요?
systemd 서비스라면 유닛 파일에 MemoryMax=를 지정하고, 일회성 실행이라면 systemd-run --scope -p MemoryMax=500M 명령을 쓰십시오. cgroups를 사용하므로 오래된 ulimit -v보다 확실합니다.