본문 바로가기

리눅스 필수 명령어

프로세스, 신호, 메모리, 디스크, 텍스트 처리, 네트워크, systemd를 다루는 Linux 명령을 앞에서 배운 운영체제 개념과 연결해 정리합니다. ps와 top의 CPU 사용률 차이, SIGTERM 처리기, 로그에서 상태 코드를 세는 방법을 예제로 확인합니다.

서버를 다루는 개발자에게 Linux 명령은 도구 상자입니다. 장애가 났을 때 원인을 좁히고, 성능 문제를 추적하고, 배포 스크립트를 짤 때 이 명령들을 씁니다. 명령을 외우기보다 각 명령이 운영체제의 어떤 정보를 보여 주는지 알면, 처음 보는 상황에서도 무엇을 확인할지 떠올릴 수 있습니다.

이 글에서는 자주 쓰는 명령을 프로세스, 메모리, 디스크, 텍스트 처리, 네트워크, 서비스 관리의 순서로 정리하고, 앞의 장에서 배운 개념과 연결합니다. 명령의 출력은 배포판과 버전에 따라 다르므로, 출력의 각 열이 무엇을 뜻하는지에 집중합니다.

프로세스 살펴보기

ps aux                       # 모든 프로세스
ps aux --sort=-%cpu | head   # CPU 사용률 순
ps aux --sort=-%mem | head   # 메모리 사용률 순
ps -ef --forest              # 부모와 자식 관계를 들여쓰기로
pstree -p 1234               # PID 1234 아래의 프로세스 트리
top                          # 실시간으로 갱신되는 목록(htop은 더 보기 좋은 대체품)

Linux 진단 · 비율과 단위

ps의 CPU 비율은 프로세스 생애 평균이고 RSS는 상주 메모리입니다

procps-ng의 ps aux를 기준으로 비율의 분모와 메모리 단위를 확인합니다. 한 번의 출력은 조사할 대상을 고르는 단서이며 병목이나 누수를 확정하지 않습니다.

ps aux의 열을 해석하는 기준
열읽는 값해석 범위
USER프로세스의 유효 사용자(EUSER)실행 권한의 일부 정보이며 모든 권한을 요약하지는 않음
%CPU누적 CPU 시간 ÷ 실행 경과 시간 × 100ps의 생애 평균 비율. top의 최근 구간 값과 구분
%MEMRSS ÷ 물리 메모리 × 100프로세스의 상주 메모리 비율
VSZ가상 메모리 크기 · KiB예약·매핑된 주소 공간이며 RAM 사용량과 다름
RSS상주 메모리 크기 · KiB공유 페이지도 포함하므로 프로세스별 합계는 중복될 수 있음
STAT실행·대기 등의 상태와 부가 플래그R: 실행 가능, S: interruptible sleep, D: uninterruptible sleep, Z: 좀비
TIME누적 CPU 시간경과 시간이 아님. top의 TIME+도 CPU 시간이며 표시 정밀도·모드를 확인
USER
읽는 값: 프로세스의 유효 사용자(EUSER)
해석 범위: 실행 권한의 일부 정보이며 모든 권한을 요약하지는 않음
%CPU
읽는 값: 누적 CPU 시간 ÷ 실행 경과 시간 × 100
해석 범위: ps의 생애 평균 비율. top의 최근 구간 값과 구분
%MEM
읽는 값: RSS ÷ 물리 메모리 × 100
해석 범위: 프로세스의 상주 메모리 비율
VSZ
읽는 값: 가상 메모리 크기 · KiB
해석 범위: 예약·매핑된 주소 공간이며 RAM 사용량과 다름
RSS
읽는 값: 상주 메모리 크기 · KiB
해석 범위: 공유 페이지도 포함하므로 프로세스별 합계는 중복될 수 있음
STAT
읽는 값: 실행·대기 등의 상태와 부가 플래그
해석 범위: R: 실행 가능, S: interruptible sleep, D: uninterruptible sleep, Z: 좀비
TIME
읽는 값: 누적 CPU 시간
해석 범위: 경과 시간이 아님. top의 TIME+도 CPU 시간이며 표시 정밀도·모드를 확인

PID·PPID도 함께 볼 때는 ps -o pid,ppid,state,pcpu,pmem,comm을 사용할 수 있습니다. 한 번의 목록은 원인을 확정하는 증거가 아니라 대상을 고르는 단서입니다.

그림처럼 ps aux의 열은 3장에서 본 프로세스의 정보를 보여 줍니다. VSZ는 프로세스가 예약한 가상 주소 공간의 크기이고 RSS는 실제로 물리 메모리에 올라와 있는 크기이므로, 메모리를 볼 때는 RSS를 봅니다. STAT의 D는 디스크 I/O 등을 기다리며 신호로도 깨울 수 없는 대기 상태, Z는 부모가 아직 거두지 않은 좀비 프로세스입니다.

ps와 top의 %CPU는 이름은 같지만 계산하는 구간이 다릅니다. ps(1)의 %CPU는 프로세스가 태어난 뒤 쓴 CPU 시간을 지금까지의 경과 시간으로 나눈 평균이고, top은 최근 갱신 주기 동안의 값을 보여 줍니다.

# (경과 초, 누적 CPU 초): 90초 동안 한가하다가 마지막 10초 동안 CPU 하나를 꽉 채워 썼다
samples = [(0, 0.0), (90, 2.0), (100, 12.0)]

elapsed, cpu = samples[-1]
print(f"ps의 %CPU(태어난 뒤 평균): {cpu / elapsed * 100:.0f}%")
(t0, c0), (t1, c1) = samples[-2], samples[-1]
print(f"top의 %CPU(최근 {t1 - t0}초): {(c1 - c0) / (t1 - t0) * 100:.0f}%")
ps의 %CPU(태어난 뒤 평균): 12%
top의 %CPU(최근 10초): 100%

방금 바빠지기 시작한 프로세스는 ps로는 12%로 한가해 보이지만 top으로는 CPU 하나를 다 쓰고 있습니다. 지금 무엇이 CPU를 쓰는지 알고 싶다면 top이나 pidstat처럼 구간을 정해 측정하는 도구를 봐야 합니다.

신호 보내기

3장 4편에서 본 신호로 프로세스를 멈추거나 설정을 다시 읽게 합니다. 아래에서 1234는 대상 프로세스의 PID 자리이고, kill은 이름과 달리 프로세스를 죽이는 명령이 아니라 신호를 보내는 명령입니다. 먼저 -TERM으로 정리할 기회를 주고, 그래도 끝나지 않을 때만 -KILL을 씁니다. 마지막 두 줄의 nice와 renice는 신호가 아니라 스케줄링 우선순위를 바꾸는 명령으로, 값이 클수록 우선순위가 낮아집니다.

kill -TERM 1234              # 정리하고 끝내 달라는 요청(kill의 기본값)
kill -KILL 1234              # 강제 종료. 프로세스는 막을 수 없다
kill -HUP 1234               # 많은 데몬이 설정을 다시 읽는 신호로 쓴다
pkill -f 'python.*worker'    # 명령줄이 패턴과 맞는 프로세스에 SIGTERM
nice -n 10 ./batch_job       # 낮은 우선순위로 실행
renice -n 5 -p 1234          # 실행 중인 프로세스의 우선순위를 낮춘다

Linux 제어 · 기본 동작과 처리기

SIGTERM은 정리를 요청할 수 있지만 SIGKILL은 사용자 정리 코드를 실행하지 않습니다

기본 동작과 애플리케이션 처리기를 구분합니다. SIGHUP이 설정 리로드로 동작하는지는 애플리케이션 구현에 달려 있습니다.

시그널의 기본 동작과 처리 가능 여부
시그널기본 동작처리기와 용도
SIGHUP종료처리기 등록 가능. 리로드는 앱이 그렇게 구현한 경우
SIGINT종료처리기 등록 가능. 터미널의 Ctrl+C로 흔히 전달
SIGTERM종료처리기 등록 가능. 정상 정리 후 종료를 요청할 때 사용
SIGKILL종료잡거나 무시할 수 없음. 사용자 정리 코드는 실행되지 않음
SIGSTOP정지잡거나 무시할 수 없음
SIGCONT정지 상태에서 재개처리기 등록 가능. 재개 동작과 핸들러 처리를 구분
SIGHUP
기본 동작: 종료
처리기와 용도: 처리기 등록 가능. 리로드는 앱이 그렇게 구현한 경우
SIGINT
기본 동작: 종료
처리기와 용도: 처리기 등록 가능. 터미널의 Ctrl+C로 흔히 전달
SIGTERM
기본 동작: 종료
처리기와 용도: 처리기 등록 가능. 정상 정리 후 종료를 요청할 때 사용
SIGKILL
기본 동작: 종료
처리기와 용도: 잡거나 무시할 수 없음. 사용자 정리 코드는 실행되지 않음
SIGSTOP
기본 동작: 정지
처리기와 용도: 잡거나 무시할 수 없음
SIGCONT
기본 동작: 정지 상태에서 재개
처리기와 용도: 처리기 등록 가능. 재개 동작과 핸들러 처리를 구분

시그널 번호는 아키텍처에 따라 달라질 수 있으므로 이름을 우선 사용합니다. 전송 성공이 정리 완료나 프로세스 종료 완료를 뜻하지는 않습니다.

SIGTERM을 받은 프로그램은 처리기에서 하던 일을 마무리하고, 임시 파일을 지우고, 연결을 닫은 뒤 끝낼 수 있습니다. 다음 코드는 SIGTERM 처리기를 등록하고 자기 자신에게 SIGTERM을 보냅니다.

import signal

def on_term(signum, frame):
    print("SIGTERM 받음: 하던 일을 정리하고 끝낸다")
    raise SystemExit(0)

signal.signal(signal.SIGTERM, on_term)        # SIGTERM 처리기를 등록한다
print("일하는 중")
signal.raise_signal(signal.SIGTERM)           # 자기 자신에게 SIGTERM을 보낸다
print("이 줄은 실행되지 않는다")
일하는 중
SIGTERM 받음: 하던 일을 정리하고 끝낸다

SIGKILL에는 처리기를 등록할 수 없으므로, 정리할 기회가 없습니다. 그래서 프로세스를 멈출 때는 SIGTERM을 먼저 보내고, 정해진 시간을 기다려도 끝나지 않을 때만 SIGKILL을 씁니다. 처음부터 SIGKILL을 쓰면 쓰다 만 파일, 지워지지 않은 잠금 파일, 정리되지 않은 자식 프로세스가 남을 수 있습니다. nice 값은 5장의 스케줄러가 CPU를 나누는 비중을 바꾸며, 일반 사용자는 값을 높여 우선순위를 낮출 수만 있습니다.

터미널을 닫아도 계속 실행하기

./long_task.sh &                              # 백그라운드에서 실행
jobs -l                                       # 이 셸의 작업 목록
fg %1                                         # 1번 작업을 포그라운드로
nohup ./server.sh > server.log 2>&1 &         # SIGHUP을 무시하게 하고 출력은 파일로
tmux new -s work                              # 다시 붙을 수 있는 터미널 세션

터미널을 닫으면 그 터미널에서 시작한 작업들에 SIGHUP이 전달되어 보통 함께 끝납니다. nohup은 SIGHUP을 무시하도록 해서 실행하지만, 다른 신호나 로그아웃할 때 세션을 정리하는 정책까지 막지는 못합니다. 오래 도는 작업은 tmux나 screen으로 세션을 만들어 나중에 다시 붙거나, 아예 뒤에서 볼 systemd 서비스로 등록하는 편이 낫습니다.

메모리 확인하기

free -h                                               # 전체, 사용 중, 사용 가능한 메모리
grep -E 'MemTotal|MemAvailable|Cached|Swap' /proc/meminfo
vmstat 2                                              # 2초마다 메모리, 스왑, CPU, I/O 요약
pmap -x 1234                                          # 프로세스의 메모리 매핑별 크기

free에서 봐야 할 것은 free 열이 아니라 available 열입니다. 10장 4편에서 본 것처럼 Linux는 놀고 있는 메모리를 페이지 캐시로 쓰므로 free는 작은 것이 정상이고, available은 스왑을 쓰지 않고 새 프로그램에 줄 수 있는 메모리의 추정치입니다. 메모리가 정말 모자란지는 vmstat의 si, so 열로 스왑 입출력이 계속 일어나는지, 응용 프로그램이 느려졌는지를 함께 보고 판단합니다. 스왑이 계속 일어나면 9장 4편에서 본 스래싱을 의심해야 합니다.

디스크와 파일

df -h                                          # 파일 시스템별 용량
df -i                                          # 파일 시스템별 inode 사용량
du -h --max-depth=1 /var | sort -rh | head     # 디렉터리별 크기
find /var -type f -size +100M -exec ls -lh {} +   # 100MiB보다 큰 파일
find /var/log -type f -mtime -1                # 하루 안에 바뀐 파일
lsof +L1                                       # 지워졌지만 아직 열려 있는 파일
iostat -xz 2                                   # 장치별 I/O 통계

디스크가 찼을 때 무엇을 확인할지는 10장 4편에서 자세히 다루었습니다. 용량이 남았는데 파일을 만들 수 없으면 df -i로 inode를, 지웠는데 공간이 늘지 않으면 lsof +L1로 열린 참조를 확인합니다. find의 -exec ... {} +는 찾은 파일들을 모아 명령을 적게 실행하므로, 파일마다 명령을 실행하는 {} \;보다 빠릅니다.

텍스트 처리로 로그 읽기

grep -rn 'ERROR' /var/log/app/                        # 재귀로, 줄 번호와 함께
grep -v 'healthcheck' access.log                      # 패턴이 없는 줄만
tail -f /var/log/app/error.log | grep --line-buffered CRITICAL   # 새로 쌓이는 줄만 걸러 보기
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head   # IP별 요청 수
awk '$9 == 404' access.log | wc -l                    # 상태 코드가 404인 요청 수
sed -i 's/old.example.com/new.example.com/g' app.conf # 파일 안의 문자열 바꾸기
jq '.items[] | {id, status}' response.json            # JSON에서 필드 고르기

로그를 셀 때는 무엇을 세는지 정확히 해야 합니다. grep -c 404는 404라는 글자가 들어 있는 줄의 수이지, 상태 코드가 404인 응답의 수가 아닙니다.

logs = [
    '10.0.0.1 - - [31/Oct/2026:15:30:01 +0900] "GET /404-help HTTP/1.1" 200 512',
    '10.0.0.2 - - [31/Oct/2026:15:30:02 +0900] "GET /missing HTTP/1.1" 404 128',
    '10.0.0.3 - - [31/Oct/2026:15:30:03 +0900] "GET /healthcheck?tag=404 HTTP/1.1" 200 64',
]
print("404라는 글자가 든 줄:", sum("404" in line for line in logs))
print("상태 코드가 404인 줄:", sum(line.split()[8] == "404" for line in logs))
404라는 글자가 든 줄: 3
상태 코드가 404인 줄: 1

경로나 쿼리에도 404가 들어갈 수 있으므로, 상태 코드를 세려면 로그 형식에서 상태 필드의 위치를 확인하고 그 필드만 비교해야 합니다. nginx와 Apache의 기본 접근 로그 형식에서는 공백으로 나눈 아홉 번째 필드가 상태 코드여서, 위의 awk '$9 == 404'가 이를 셉니다. 집계에서 이상한 값이 나오면 원래 로그 줄로 돌아가 확인하는 습관이 좋습니다.

네트워크 진단

ss -tlnp                             # 연결을 기다리는 TCP 포트와 프로세스
ss -Htan state time-wait | wc -l     # TIME_WAIT 상태 연결 수(-H로 머리글 줄을 뺀다)
ss -s                                # 상태별 연결 수 요약
ip addr show                         # 인터페이스와 주소
ip route show                        # 라우팅 표
ping -c 4 10.0.0.1                   # 도달 가능한지, 왕복 시간
mtr 10.0.0.1                         # 경로의 구간별 손실과 지연
dig example.com                      # DNS 조회
nc -zv 10.0.0.1 5432                 # 포트에 연결할 수 있는지
sudo tcpdump -i any host 10.0.0.1 -w capture.pcap   # 패킷을 파일로 캡처
curl -o /dev/null -s -w '%{http_code} %{time_total}s\n' https://example.com   # 응답 코드와 걸린 시간

진단은 아래 계층부터 올라가며 합니다. 주소와 경로가 맞는지(ip), 상대에게 닿는지(ping, mtr), 이름이 제대로 풀리는지(dig), 포트가 열려 있는지(nc, ss), 마지막으로 응용 프로그램이 제대로 답하는지(curl)를 확인합니다. TIME_WAIT는 연결을 먼저 끊은 쪽이 마지막 ACK를 보낸 뒤 잠시 머무는 정상 종료의 마지막 단계로, 왜 필요하고 언제 쌓이는지는 네트워크 교재의 연결 수립과 종료에서 다룹니다. 이 상태의 연결이 많다면 짧은 연결을 너무 자주 맺고 끊는지 살펴보고, 연결 풀(connection pool)이나 연결 재사용(keep-alive)을 검토합니다. 방화벽 규칙은 12장 3편의 nftables나 배포판의 ufw, firewall-cmd로 확인합니다.

systemd로 서비스 관리하기

요즘 대부분의 Linux 배포판은 systemd가 PID 1로 실행되어 서비스를 시작하고, 감시하고, 로그를 모읍니다. 서비스는 systemctl로, 그 로그는 journalctl로 다룹니다. 아래에서 restart는 프로세스를 끝내고 새로 띄우므로 그사이 서비스가 잠시 끊기고, reload는 프로세스를 살려 둔 채 설정만 다시 읽게 하므로 서비스가 지원한다면 끊김이 적습니다. enable은 부팅할 때 시작하도록 등록만 하므로, 지금 바로 시작하려면 --now를 함께 붙입니다.

systemctl status nginx                # 상태, 최근 로그, 메인 PID
sudo systemctl restart nginx          # 다시 시작
sudo systemctl reload nginx           # 서비스가 지원하면 설정만 다시 읽기
sudo systemctl enable --now nginx     # 부팅할 때 자동으로 시작하고 지금도 시작
systemctl --failed                    # 실패한 서비스 목록
journalctl -u nginx -f                # 서비스 로그를 실시간으로
journalctl -u nginx --since '1 hour ago'

직접 만든 프로그램은 유닛 파일을 써서 서비스로 등록합니다. 유닛 파일은 세 구역으로 나뉩니다. [Unit]에는 설명과 다른 유닛과의 순서를, [Service]에는 어떤 사용자로 어느 디렉터리에서 무엇을 실행하고 끝나면 어떻게 할지를, [Install]에는 systemctl enable했을 때 어느 타깃에 묶일지를 적습니다.

/etc/systemd/system/myapp.service
[Unit]
Description=My Application
Wants=network-online.target
After=network-online.target

[Service]
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server
Restart=on-failure
RestartSec=5
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Restart=on-failure는 프로세스가 오류로 끝나거나 신호로 죽었을 때 5초(RestartSec) 뒤에 다시 시작하게 하고, LimitNOFILE은 이 서비스가 열 수 있는 파일 디스크립터의 수를 늘립니다. After=는 시작 순서만 정할 뿐 그 대상을 함께 시작시키지는 않으므로, 네트워크가 실제로 준비된 뒤 시작해야 한다면 Wants=와 After=에 network-online.target을 함께 적습니다. 파일을 고친 뒤에는 sudo systemctl daemon-reload로 systemd가 다시 읽게 해야 합니다.

정리

  • ps는 프로세스의 상태와 메모리를 보여 주지만 %CPU는 태어난 뒤의 평균이므로, 지금의 사용률은 top처럼 구간을 정해 측정하는 도구로 봅니다.
  • 프로세스는 SIGTERM으로 먼저 정리할 기회를 주고, 끝나지 않을 때만 SIGKILL로 멈춥니다.
  • 메모리는 free의 available과 스왑 입출력으로, 디스크는 df -h, df -i, lsof +L1로 확인합니다.
  • 로그를 셀 때는 문자열이 아니라 필요한 필드를 비교해야 하며, 네트워크 문제는 아래 계층부터 차례로 확인합니다.
  • systemd는 유닛 파일에 적은 대로 서비스를 시작하고 다시 시작하며, 로그는 journalctl로 봅니다.

연습 문제

1. 로그와 CPU 사용률 바로 읽기

접근 로그의 세 줄이 각각 경로 /404-help에 상태 200, 경로 /missing에 상태 404, 경로 /healthcheck?tag=404에 상태 200이고, 다른 필드에는 404가 없습니다. grep -c 404의 결과, grep -v healthcheck로 거른 뒤 grep -c 404의 결과, 실제 404 응답의 수를 각각 구하세요. 또 ps에서 실행된 지 100초 된 프로세스의 누적 CPU 시간이 20초라면 %CPU는 얼마이고, 이것이 지금 이 순간의 사용률인가요?

해설 보기

세 줄 모두 404라는 글자를 담고 있으므로 grep -c 404는 3, healthcheck 줄을 빼면 2이고, 상태 필드가 404인 실제 응답은 1개입니다. 한 줄에 404가 두 번 나와도 grep -c는 그 줄을 한 번만 셉니다. %CPU는 20 ÷ 100 × 100 = 20%이지만, 이는 프로세스가 태어난 뒤 100초 동안의 평균입니다. 그 20초를 처음에 몰아 썼을 수도, 지금 막 쓰고 있을 수도 있으므로 지금의 사용률은 알 수 없고, top이나 pidstat 1로 짧은 구간을 측정해야 합니다.

2. 서비스를 멈추는 순서

어떤 동료는 서비스를 멈출 때 언제나 kill -9부터 씁니다. 이 습관의 문제를 설명하고, 서비스를 안전하게 멈추는 순서를 제시하세요.

해설 보기

SIGKILL은 프로세스가 잡을 수 없으므로, 프로그램은 처리 중인 요청을 마치거나, 버퍼에 남은 데이터를 파일에 쓰거나, 잠금 파일과 임시 파일을 지우거나, 데이터베이스 연결을 닫을 기회를 얻지 못합니다. 그 결과 쓰다 만 파일이나 남은 잠금 때문에 다음 시작이 실패하거나, 클라이언트가 응답을 받지 못하는 일이 생깁니다. 먼저 SIGTERM을 보내고, 프로그램이 정리할 시간을 정해 두고 기다린 뒤, 그래도 끝나지 않을 때만 SIGKILL을 보냅니다. systemd의 systemctl stop과 docker stop도 이 순서를 따르며, 기다리는 시간은 각각 TimeoutStopSec과 --timeout(-t)으로 정합니다.

3. free가 작다는 경보

모니터링이 "free 메모리가 1GiB 아래로 내려갔다"는 경보를 보냈습니다. 같은 시각 서버의 전체 메모리는 16GiB, available은 7GiB이고, vmstat의 si와 so는 계속 0입니다. 메모리가 부족한 상황인가요? 경보를 어떻게 고치는 것이 좋을까요?

해설 보기

부족한 상황이 아닙니다. Linux는 놀고 있는 메모리를 페이지 캐시로 쓰므로 free는 작은 것이 정상이고, 필요하면 캐시를 줄여 새 프로그램에 7GiB 정도를 줄 수 있습니다. 스왑 입출력도 없으므로 메모리 압박의 신호가 없습니다. 경보는 free 대신 available이 전체의 일정 비율 아래로 떨어질 때, 그리고 스왑 입출력이 계속될 때 울리도록 바꾸는 것이 좋습니다. 커널이 제공하는 PSI(/proc/pressure/memory)로 메모리를 기다리느라 멈춘 시간의 비율을 보는 방법도 있습니다.

다음 글 시스템 모니터링과 디버깅에서는 시스템 콜을 추적하는 strace, 성능을 분석하는 perf, 그리고 USE 방법론처럼 문제를 체계적으로 좁혀 가는 방법을 살펴봅니다.