자주 쓰는 명령어를 하나씩 — 설명, 사용법, 실제 결과로 정리한다. 현대 도구는 대체하는 고전 명령을 같이 적었다. 아래 출력은 전부 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.rsripgrep (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
adminlength·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/2026sed의 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/bashawk '{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 rm1) 공백이 있는 파일명에서 깨진다. 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.log3) -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 |
NI | nice 값(−20~19) | 클수록 양보한다. renice로 바꾼다 |
VIRT | 프로세스가 요청한 주소 공간 전체 | 실제 사용량이 아니다 |
RES | 실제 물리 메모리(RSS) | 공유 페이지가 중복 계산된다 — 아래 참고 |
SHR | 그중 다른 프로세스와 공유 중인 양 | 라이브러리 코드가 대부분이다 |
S | 상태 | R/S/D/T/Z — 아래 표 |
%CPU | 갱신 간격 동안 쓴 CPU | 코어를 합산한다(100% 초과 가능) |
%MEM | RES ÷ 전체 메모리 | 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 sleepNI가 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 burn4366.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은 갱신 간격 동안 쓴 비율을 본다 → 지금 노니까 0ps의%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초로 나눈 뒤 정수로 절삭해 비교한다는 동작을 확인했다.