자주 쓰는 명령어를 하나씩 — 설명, 사용법, 실제 결과로 정리한다. 현대 도구는 대체하는 고전 명령을 같이 적었다. 아래 출력은 전부 WSL에서 직접 실행한 것이고, 현대 도구는 정적 바이너리 하나라 릴리스에서 받아 ~/.local/bin에 두면 된다.

고전 명령을 대체하는 현대 도구

eza — ls 대체

디렉터리를 트리로 보여주고 색·git 상태까지 붙는다. 옛 exa는 유지보수가 끊겨(README 첫 줄이 “unmaintained, use eza”) 커뮤니티 fork인 eza로 넘어왔다. 명령은 거의 호환된다.

$ eza --tree --level=2 demo
demo
├── big
│   └── f
├── logs
│   ├── app.log
│   └── old.log
└── src
    └── main.rs

ripgrep (rg) — grep 대체

기본으로 .gitignore를 존중해 node_modules·빌드 산출물을 알아서 건너뛰고, 재귀·줄번호가 기본이라 grep -rn을 칠 필요가 없다.

$ rg -n 'TODO' demo
demo/src/main.rs:2:    // TODO: 검증

fd — find 대체

find보다 문법이 단순하고 빠르다. .gitignore를 존중하고, -e로 확장자를 거른다.

$ fd -e log . demo
demo/logs/app.log
demo/logs/old.log

우분투/데비안에선 이미 다른 패키지가 fd를 써서 실행 파일이 fdfind로 깔린다(패키지명은 fd-find). ln -s $(which fdfind) ~/.local/bin/fd로 통일한다.

bat — cat 대체

문법 강조, 줄번호, git 변경 표시, 페이저가 붙는다. 긴 코드·설정을 훑을 때.

$ bat --style=numbers,header demo/src/main.rs
     File: demo/src/main.rs
   1 fn main() {
   2     // TODO: 검증
   3     let x = 1;
   4 }

터미널에선 여기에 문법 색까지 붙는다. 구버전 우분투/데비안에선 실행 파일이 batcat으로 깔리니 ln -s /usr/bin/batcat ~/.local/bin/bat.

jq — JSON 다루기

셸에서 JSON을 grep·sed로 씨름하지 말고 jq로 자른다. .a.b로 파고들고 []로 배열을 펼친다.

$ echo '{"user":{"name":"kim","roles":["dev","admin"]}}' | jq -r '.user.name'
kim
$ echo '{"user":{"name":"kim","roles":["dev","admin"]}}' | jq -r '.user.roles[]'
dev
admin

length·select() 같은 필터가 붙어, API 응답이나 설정 JSON을 훑을 때 사실상 필수다.

dust — du 대체

어느 디렉터리가 용량을 먹는지 트리로 보여준다. du -sh * | sort -h를 손으로 조합할 필요가 없다.

$ dust -d1 -b demo
 8.0K   ┌── src
 12K   ├── logs
6.0M   ├── big
6.0M ┌─┴ demo

-d1은 한 단계 깊이만, -b는 사용률 막대를 끈다(안 끄면 각 줄에 색 막대가 붙는다).

sd — sed 대체

찾아 바꾸기가 훨씬 직관적이다. 정규식이 기본이고, 캡처 그룹은 ${1}처럼 쓴다.

$ echo 'hello world' | sd 'world' 'sd!'
hello sd!
$ echo '2026-08-11' | sd '(\d+)-(\d+)-(\d+)' '${3}/${2}/${1}'
11/08/2026

sed의 s/.../.../g와 이스케이프 지옥 없이 읽힌다. 다만 스크립트에는 여전히 sed를 쓴다(아래 원칙 참고).

duf — df 대체

디스크 사용량을 표로 깔끔하게. --only local을 붙이면 네트워크·가상 마운트를 뺀다.

$ duf /
╭────────────────────────────────────────────────────────────────────────────╮
│ 1 local device                                                             │
├────────────┬─────────┬───────┬────────┬──────────────────┬──────┬────────────┤
│ MOUNTED ON │    SIZE │  USED │  AVAIL │       USE%       │ TYPE │ FILESYSTEM │
├────────────┼─────────┼───────┼────────┼──────────────────┼──────┼────────────┤
│ /          │ 1006.9G │ 15.4G │ 940.3G │             1.5% │ ext4 │ /dev/sdd   │
╰────────────┴─────────┴───────┴────────┴──────────────────┴──────┴────────────╯

USE% 칸은 터미널에서 색 막대로 그려진다.

choose — cut/awk의 열 뽑기

0부터 세는 인덱스로 열을 고른다. -f로 구분자를 바꾸고, 뒤에서부터 세는 음수 인덱스도 된다.

$ echo 'alpha beta gamma delta' | choose 1 3
beta delta
$ echo 'root:x:0:0:root:/root:/bin/bash' | choose -f ':' 0 6
root /bin/bash

awk '{print $2,$4}'보다 짧다. 열을 넘어 합계·조건 계산이 필요하면 그땐 awk.

고전 명령 — find로 오래된 로그 지우기

find는 어디에나 있어 스크립트에도 쓴다. 기본 사용법과 결과부터:

$ find demo/logs -name '*.log' -mtime +10 -type f
demo/logs/old.log

다만 예전 메모의 이 한 줄엔 함정이 셋 있었다.

find -name '*.log' -mtime +10 | xargs rm

1) 공백이 있는 파일명에서 깨진다. xargs는 공백을 구분자로 본다. b with space.log 하나가 인자 세 개로 쪼개진다.

$ find . -name '*.log' -mtime +10 -type f | xargs sh -c 'echo "got $# args:"; printf "  [%s]\n" "$@"' _
got 3 args:
  [./b]
  [with]
  [space.log]

-print0 / xargs -0으로 널 구분자를 쓰거나 find의 -delete를 쓴다.

find . -name '*.log' -mtime +10 -type f -print0 | xargs -0 rm
find . -name '*.log' -mtime +10 -type f -delete   # 더 간단

2) -type f가 없으면 디렉터리도 걸린다. *.log라는 이름의 디렉터리까지 걸리고, rm -rf였다면 디렉터리 전체가 조용히 사라진다.

$ find . -name '*.log' -mtime +10        # -type f 없음 → 디렉터리도
./b with space.log
./c.log
$ find . -name '*.log' -mtime +10 -type f  # 파일만
./b with space.log

3) -mtime +10은 “10일 초과”가 아니다. GNU findutils 매뉴얼에 따르면 파일 나이(초)를 86400으로 나눈 뒤 소수점을 버린 정수로 비교하고, +10은 그 정수가 10보다 큰 것이다. 10.9일 파일은 정수 10으로 절삭돼 안 걸리고, 만 11일이 되어야 걸린다.

지우기 전엔 -delete를 -print로 바꿔 뭐가 걸리는지 먼저 본다. 상시 운영 서비스라면 매번 손으로 지우기보다 logrotate(/etc/logrotate.d/)에 회전·압축·보관 기간을 맡기는 쪽이 맞다.

프로세스 모니터링 — top 읽는 법

top은 누구나 쓰지만, 헤더 숫자의 기준을 잘못 알고 엉뚱한 결론을 내리기 쉽다. “부하가 0.43인데 왜 느리지”, “메모리가 free 12GB인데 부족하다는데” 같은 오해가 대부분 여기서 나온다.

헤더 다섯 줄이 말하는 것

top - 10:21:27 up 0 min,  1 user,  load average: 0.16, 0.03, 0.01
Tasks:  96 total,   1 running,  95 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.2 us,  0.0 sy,  0.0 ni, 99.6 id,  0.0 wa,  0.0 hi,  0.2 si,  0.0 st
MiB Mem :  15857.7 total,  12354.2 free,   1719.5 used,   1784.1 buff/cache
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.  13933.7 avail Mem
  • load average는 CPU 사용률이 아니다. 1·5·15분 평균이고, 세는 대상은 실행 중이거나 실행을 기다리는 프로세스 + 디스크 I/O를 기다리는(D 상태) 프로세스다. 그래서 코어 수와 함께 봐야 뜻이 생긴다. 위 기계는 32코어라 0.43은 거의 노는 상태지만, 2코어 기계에서 같은 값이면 절반쯤 찬 것이다.
  • %Cpu(s) 줄에서 실제로 볼 것은 wa와 st다. us(사용자)와 sy(커널)가 높은 건 일을 하고 있다는 뜻이지만, wa가 높으면 디스크를 기다리느라 못 하고 있는 것이고, st(steal)가 높으면 하이퍼바이저가 CPU를 다른 VM에 주고 있다는 뜻이다. 클라우드에서 “CPU는 안 바쁜데 느리다”의 범인이 대개 이 둘이다.
  • free가 작다고 메모리가 부족한 게 아니다. buff/cache는 필요하면 곧바로 회수되는 캐시다. 봐야 할 숫자는 마지막의 avail Mem 이다. 위 출력에서 free는 12.3GB지만 실제로 쓸 수 있는 건 13.9GB로 더 크다.

프로세스 목록의 열

헤더 아래부터가 프로세스 목록이다. 기본 열은 열두 개다.

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    263 jenkins   20   0   15.3g 830148  27356 S   0.0   5.1   0:15.42 java
    937 ewanshin  20   0   65900  60172   5736 S   0.0   0.4   0:00.02 python3
열뜻볼 때 주의할 것
PID프로세스 번호-H를 켜면 스레드 번호(TID)가 들어온다
USER실행 사용자서비스가 root로 돌고 있지 않은지 확인하는 자리
PR커널이 쓰는 우선순위일반 프로세스는 20 + NI. 실시간 스케줄링이면 rt
NInice 값(−20~19)클수록 양보한다. renice로 바꾼다
VIRT프로세스가 요청한 주소 공간 전체실제 사용량이 아니다
RES실제 물리 메모리(RSS)공유 페이지가 중복 계산된다 — 아래 참고
SHR그중 다른 프로세스와 공유 중인 양라이브러리 코드가 대부분이다
S상태R/S/D/T/Z — 아래 표
%CPU갱신 간격 동안 쓴 CPU코어를 합산한다(100% 초과 가능)
%MEMRES ÷ 전체 메모리RES 기반이라 합계가 100%를 넘을 수 있다
TIME+누적 CPU 사용 시간(1/100초까지)지금 바쁜지가 아니라 그동안 얼마나 썼는지
COMMAND명령 이름c를 누르면 전체 명령줄로 바뀐다

PR은 NI를 따라 움직인다. nice -n 10으로 띄우면 그대로 드러난다.

$ nice -n 10 sleep 30 &
$ top -b -n 1 -p 993 | tail -1
    993 ewanshin  30  10    3220   2036   1924 S   0.0   0.0   0:00.00 sleep

NI가 10이니 PR이 30이다.

VIRT·RES·SHR — 어느 숫자를 믿을 것인가

위 예에서 java는 VIRT가 15.3GB인데 RES는 830MB다. VIRT는 “잡아 둔 주소 공간”이라 실제 메모리 사용량과 거의 무관하다. JVM이나 Go 런타임처럼 큰 영역을 미리 예약하는 프로그램에서 특히 벌어진다. 메모리를 얼마나 쓰는지 알고 싶으면 RES를 본다.

그런데 RES에도 함정이 있다. 여러 프로세스가 공유하는 페이지가 각각의 RES에 전부 계산된다. 같은 파이썬 인터프리터로 두 프로세스를 띄우고 /proc의 실제 수치와 맞춰 봤다.

$ top -b -n 1 -p 937,938 | tail -2
    937 ewanshin  20   0   65900  60172   5736 S   0.0   0.4   0:00.02 python3
    938 ewanshin  20   0   14696   8956   5728 S   0.0   0.1   0:00.00 python3
 
$ # /proc/<pid>/smaps_rollup 에서 본 같은 순간의 내역
  PID 937   RSS  60172 kB = 공유 5692 + 사설 54456   |  PSS 55458 kB
  PID 938   RSS   8956 kB = 공유 5728 + 사설  3228   |  PSS  4221 kB

두 프로세스가 같은 5.7MB를 각자의 RES에 넣고 있다. 그래서 프로세스별 RES를 더하면 실제 사용량보다 커진다. 정확히 나누고 싶으면 PSS(공유 페이지를 공유하는 프로세스 수로 나눈 값)를 본다.

awk '/^Pss:/{print $2" kB"}' /proc/1234/smaps_rollup   # 내 프로세스는 그냥 읽힌다

남의 프로세스는 권한이 필요하다(sudo). “컨테이너 메모리 합이 안 맞는다”는 이야기의 상당수가 RES를 더한 결과다.

상태(S) 코드

네 가지를 실제로 만들어 확인했다.

    953 ewanshin  ... R 100.0   0.0   0:01.14 yes        ← 실행 중
    947 ewanshin  ... S   0.0   0.1   0:00.00 python3    ← 자는 중(대부분 여기)
    946 ewanshin  ... T   0.0   0.0   0:00.00 sleep      ← kill -STOP 으로 멈춤
    949 ewanshin  ... Z   0.0   0.0   0:00.00 python3    ← 좀비
코드뜻
R실행 중이거나 실행 대기열에 있음
S인터럽트 가능한 대기(이벤트·입력을 기다림). 평상시 대부분
D인터럽트 불가능한 대기 — 보통 디스크 I/O. 여기 오래 머물면 스토리지를 의심한다
T정지됨(SIGSTOP, 또는 디버거가 붙음)
Z좀비 — 끝났는데 부모가 종료 상태를 안 거둠

좀비 행을 보면 VIRT·RES·SHR가 전부 0이다. 이미 죽어서 메모리는 없고 프로세스 테이블 항목만 남은 상태라 그렇다. 좀비가 쌓이면 메모리가 아니라 PID 고갈이 문제가 된다. 범인은 좀비 자신이 아니라 wait()를 안 하는 부모다.

D가 여러 개 보이면서 헤더의 wa가 높다면 CPU가 아니라 디스크가 병목이다 — 위 헤더 절과 이어지는 이야기다.

%CPU는 코어를 합쳐서 센다

스레드 4개가 각각 바쁜 프로그램을 32코어 기계에서 돌려 봤다.

$ top -b -n 2 -d 1 -p 952
    PID USER      PR  NI    VIRT    RES  SHR S  %CPU  %MEM     TIME+ COMMAND
    952 ewanshin  20   0   35568   1392 1260 S 366.3   0.0   0:15.42 burn4

366.3% 다. 오류가 아니라 정의다. man top도 멀티스레드 프로세스는 100%를 넘을 수 있다고 적어 두었다. -H를 붙이면 스레드 단위로 펼쳐진다.

$ top -b -n 2 -d 1 -H -p 952
    954 ewanshin  ... R  93.1   0.0   0:04.95 burn4
    955 ewanshin  ... R  93.1   0.0   0:04.95 burn4
    956 ewanshin  ... R  93.1   0.0   0:04.95 burn4
    953 ewanshin  ... R  92.2   0.0   0:04.94 burn4
    952 ewanshin  ... S   0.0   0.0   0:00.00 burn4   ← 일 안 하는 주 스레드

“CPU 하나를 다 쓰는가”를 보려면 코어 수로 나눠야 한다. 366%는 32코어 중 3.7개를 쓰는 중이라는 뜻이다.

top의 %CPU와 ps의 %CPU는 다른 값이다

4초 동안 CPU를 태우고 그다음엔 노는 프로세스를 두고 둘을 같이 재 봤다.

$ top -b -n 1 -p 977 | tail -1
    977 ewanshin  20   0   13676   8956  5724 S   0.0   0.1   0:03.98 python3
 
$ ps -o pid,etime,time,%cpu -p 977 --no-headers
    977       00:07 00:00:03 56.8

같은 순간에 top은 0.0%, ps는 56.8% 라고 한다. 둘 다 맞다.

  • top은 갱신 간격 동안 쓴 비율을 본다 → 지금 노니까 0
  • ps의 %cpu는 프로세스가 살아 있는 전체 시간 대비 평균이다 → 7초 중 3초를 썼으니 56.8

“어떤 프로세스가 CPU를 먹고 있나”를 지금 알고 싶으면 top, “이 프로세스가 그동안 얼마나 썼나”는 ps나 TIME+ 열을 본다.

필요한 것만 보기

화면을 다 띄우지 않고 원하는 것만 뽑는 옵션들이다.

top -p 1234,5678        # 특정 PID만
top -u myapp            # 특정 사용자만
top -H -p 1234          # 스레드 단위로
top -o %MEM             # 메모리 순 정렬 (앞에 -를 붙이면 오름차순: -o -%MEM)
top -b -n 1             # 배치 모드: 대화형 없이 한 번 찍고 종료
top -b -n 1 -w 512      # 명령줄이 잘리지 않게 넓게

정렬은 -o 뒤에 열 이름을 그대로 쓴다(%CPU, %MEM, TIME+, PID). 대화형에서는 f를 눌러 필드 관리 화면에서 s로 정렬 필드를 지정한다.

스크립트와 로그에 쓰기

-b(배치)와 -n(횟수)이면 파일로 남길 수 있다.

# CPU 상위 5개를 10초마다 로그에 쌓기
while :; do
  date -Is
  top -b -n 1 -o %CPU | sed -n '7,12p'
  sleep 10
done >> /var/log/top-snapshot.log

인터넷 예제가 -n 2를 쓰는 걸 자주 보는데, 이 환경(procps-ng 3.3.17)에서는 -n 1과 두 번째 스냅샷의 값이 같았다. 차이가 나는 쪽은 위에서 본 ps다.

대화형에서 자주 쓰는 키

man top에서 확인한 것들만 적는다.

키하는 일
H프로세스 ↔ 스레드 보기 전환
V부모-자식 트리(forest) 보기
c명령 이름 ↔ 전체 명령줄
i노는 프로세스 숨기기
f필드 관리 화면(표시 열 선택, s로 정렬 필드 지정)
k / r프로세스 종료 / 우선순위 변경
W지금 화면 구성을 설정 파일로 저장

W로 저장해 두면 다음부터 그 화면으로 뜬다. 매번 열을 다시 고르고 있다면 이 키를 안 쓰고 있는 것이다.

더 편한 대체 도구

이 문서 앞부분과 같은 이야기다. top에도 현대적 대체가 있다.

sudo apt install htop      # 색·마우스·트리·검색이 기본, 조작이 직관적
sudo apt install btop      # 그래프 중심 대시보드

다만 어디에나 있는 건 top이다. 컨테이너 안이나 최소 설치 서버에서는 htop이 없는 경우가 흔하고, 그때 위의 헤더 읽는 법과 -b 옵션이 그대로 쓰인다.

두 가지 원칙

  • 스크립트엔 고전 도구. 대화형 쉘에서 편하다고 스크립트에 eza·fd·bat·sd를 넣지 않는다. 남의 서버·CI 컨테이너엔 기본으로 없다. 스크립트는 ls·find·cat·sed처럼 어디에나 있는 고전 도구로 짜고, 새 도구는 터미널에서만 쓴다.
  • 직접 배포 .deb는 이름이 그대로. 위의 batcat/fdfind 개명은 배포판 패키지 사정이다. 각 프로젝트가 직접 내는 .deb 릴리스는 실행 파일명이 bat/fd 그대로다.

참고

  • Linux top command — top이 제공하는 기능의 범위(정렬·필터·표시 전환·배치 모드)를 훑는 데 참고했다. 이 문서의 출력과 수치는 별도로 실행해 얻은 것이다.

  • man top (procps-ng 3.3.17) — 멀티스레드 프로세스의 %CPU가 100%를 넘을 수 있다는 서술, -o로 정렬 필드를 지정하는 방법, 필드 관리 화면에서 s가 정렬 필드를 정한다는 점을 확인했다.

  • ogham/exa · eza-community/eza — exa 유지보수 중단과 eza fork로의 이관을 확인했다.

  • BurntSushi/ripgrep — grep 대체. rg -n 실행 확인(15.2.0).

  • sharkdp/fd — find 대체, 패키지 fd-find/실행 파일 fdfind 확인(10.4.2).

  • sharkdp/bat — cat 대체, 구버전의 batcat 이름 확인(0.26.1).

  • jqlang/jq — JSON 필터. .user.name·[] 실행 확인(1.8.2).

  • bootandy/dust — du 대체. dust -d1 -b 실행 확인(1.2.4).

  • chmln/sd — sed 대체. 캡처 그룹 치환 실행 확인(1.0.0).

  • muesli/duf — df 대체. duf / 실행 확인(0.9.1).

  • theryangeary/choose — cut/awk의 열 선택. 인덱스·-f 확인(1.3.7).

  • GNU findutils 매뉴얼 — Comparing Timestamps — -mtime이 나이를 86400초로 나눈 뒤 정수로 절삭해 비교한다는 동작을 확인했다.