테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

리눅스와 셸

서버와 컨테이너, 클라우드의 대부분이 리눅스 위에서 돈다. 커널과 배포판의 관계, 디렉터리 구조와 권한, 프로세스와 신호, 셸이 명령 한 줄을 어떻게 읽어 실행하는지, 파이프와 리디렉션, 서비스·로그·예약 작업, SSH 키까지를 실제 명령과 그림으로 정리한다.

목차 (17)
  1. 리눅스, 배포판, 커널과 사용자 영역
  2. 파일 시스템 계층과 ‘모든 것은 파일’
  3. 경로와 링크
  4. 권한: rwx와 8진수
  5. 사용자, root, sudo
  6. 프로세스와 신호
  7. 셸이 명령을 읽는 순서
  8. 표준 입출력, 파이프, 리디렉션
  9. 자주 쓰는 명령
  10. 환경 변수와 PATH
  11. 패키지 관리: apt와 dnf
  12. 서비스와 로그: systemd와 journalctl
  13. 예약 작업: cron
  14. SSH 키와 known_hosts
  15. 셸 스크립트 안전장치
  16. 흔한 오해
  17. 핵심 자료

셸(shell)은 사람이 친 글자를 읽어 프로그램을 찾아 실행하고 결과를 돌려주는 프로그램이다. 셸 자체도 커널 위에서 도는 평범한 프로세스라서, 셸이 하는 일은 결국 커널에 시스템 콜로 부탁하는 일이다. 커널 모드와 시스템 콜, 프로세스와 가상 메모리의 원리는 운영체제 노트에 있고, 여기서는 그 원리가 리눅스 명령줄에서 어떻게 보이는지를 다룬다. 예시의 사용자 이름은 user, 홈은 /home/user로 쓴다.

리눅스, 배포판, 커널과 사용자 영역

엄밀히 리눅스는 커널의 이름이다(kernel.org). 커널은 하드웨어를 직접 다루며 프로세스, 메모리, 파일 시스템, 드라이버, 네트워크를 관리한다. 그 위의 셸, C 라이브러리, 서비스 관리자, 응용 프로그램은 사용자 영역(user space)에 있고, 하드웨어나 다른 프로세스에 닿으려면 반드시 시스템 콜을 거친다. 권한 검사도 이 경계에서 일어난다.

리눅스 시스템의 층: 맨 아래 하드웨어, 그 위에 커널, 시스템 콜 경계 위에 셸·C 라이브러리·systemd·응용 프로그램 같은 사용자 영역이 있다. 배포판은 커널부터 사용자 영역과 패키지 관리자까지를 묶는다사용자 영역(user space)셸bash, zshC 라이브러리glibc, musl서비스 관리systemd응용 프로그램nginx, python시스템 콜: open, read, fork, execve … 여기서 권한 검사를 한다커널(Linux)프로세스 스케줄링 · 메모리 · 파일 시스템 · 드라이버 · 네트워크하드웨어: CPU · 메모리 · 디스크 · 네트워크 카드배포판이고르고 묶는범위+ 패키지 관리자+ 기본 설정Debian·Ubuntu·Fedora·Arch는 같은 커널 계열 위에 고른 도구와 정책이 다르다
리눅스 시스템의 층. 커널만으로는 쓸 수 없고, 사용자 영역 도구와 패키지 관리자, 기본 설정을 골라 묶은 것이 배포판이다.
  • 배포판(distribution)은 커널에 사용자 영역 도구, 패키지 관리자, 기본 설정과 보안 정책을 묶어 내놓은 것이다. Debian·Ubuntu는 .deb 패키지와 apt, Fedora·RHEL 계열은 .rpm 패키지와 dnf를 쓴다.
  • 고를 때 볼 것: 패키지 형식, 출시 주기(정해진 판을 오래 고치며 쓰는 방식인지, 계속 최신으로 굴러가는 방식인지), 보안 업데이트를 주는 기간이다. 서버에는 오래 지원하는 판을 쓰고, 컨테이너 이미지는 작은 배포판을 고르는 일이 많다.
  • 명령은 거의 같다: 배포판이 달라도 셸, 파일 시스템 구조, 권한, 신호는 같다. 크게 다른 곳은 패키지 관리와 일부 설정 파일 위치다.

파일 시스템 계층과 ‘모든 것은 파일’

리눅스에는 드라이브 문자가 없다. 모든 것이 루트 / 아래 한 나무에 붙고, 디스크나 USB는 그 나무의 한 디렉터리에 마운트된다. 어느 디렉터리에 무엇을 두는지는 FHS(Filesystem Hierarchy Standard) 3.0이 정하고, hier(7)가 그 요약이다.

리눅스 디렉터리 나무: 루트 아래 /etc는 설정, /var는 로그처럼 바뀌는 데이터, /home은 사용자 홈, /usr는 프로그램, /tmp는 임시, /opt는 추가 패키지, /dev·/proc·/sys는 커널이 만들어 보여 주는 가상 파일이다//etc이 기계의 설정 파일(ssh/sshd_config, fstab)/var바뀌며 쌓이는 데이터: 로그, 캐시, 스풀/home사용자 홈(/home/user). root의 홈은 /root/usr프로그램과 라이브러리. 읽기 전용, 공유 가능/tmp임시 파일. 언제든(재부팅 때) 지워질 수 있다/opt따로 설치한 추가 패키지/dev장치 파일: /dev/sda, /dev/null/proc실행 중인 프로세스와 커널 정보: /proc/1234/sys장치·드라이버 정보(sysfs)색칠한 셋은 디스크에 없다. 읽는 순간 커널이 내용을 만들어 준다
루트 아래 주요 디렉터리. 설정은 /etc, 로그는 /var/log, 내 파일은 홈 디렉터리라는 세 가지만 기억해도 문제의 대부분을 찾아갈 수 있다.

유닉스 계열의 오래된 설계 원칙이 ‘모든 것은 파일’이다. 일반 파일뿐 아니라 장치(/dev/sda), 버리는 곳(/dev/null), 프로세스 정보(/proc), 파이프와 소켓까지 같은 open·read·write로 다룬다. 그래서 cat 하나로 커널 정보를 읽고, 리디렉션 하나로 출력을 버린다. /proc/self는 그것을 읽는 프로세스 자신을 가리킨다.

cat /proc/loadavg
cat /proc/self/status
ls -l /dev/null
echo "버려질 글" > /dev/null

경로와 링크

/로 시작하는 경로는 절대 경로, 아니면 지금 디렉터리에서 시작하는 상대 경로다. .은 지금 디렉터리, ..은 한 단계 위, ~는 홈(/home/user)이다. 파일의 내용과 권한·소유자·크기 같은 속성은 아이노드(inode)에 있고, 디렉터리는 ‘이름 → 아이노드 번호’를 적은 표일 뿐이다. 이름과 실체가 떨어져 있기 때문에 링크가 가능하다.

링크 단계 그림: app.conf를 만들고, ln으로 backup.conf를 더하면 아이노드 링크 수가 2가 되고, ln -s로 current.conf를 만들면 경로만 가리키며, app.conf를 지우면 backup.conf는 살아 있고 current.conf는 끊긴다디렉터리 항목(이름)아이노드(데이터·권한·링크 수)app.conf아이노드 1201아이노드 1201링크 수: 1내용: port=8080app.conf아이노드 1201backup.conf아이노드 1201아이노드 1201링크 수: 2내용: port=8080app.conf아이노드 1201backup.conf아이노드 1201current.conf→ 경로 "app.conf"아이노드 1201링크 수: 2내용: port=8080backup.conf아이노드 1201current.conf→ app.conf (끊김)아이노드 1201링크 수: 1내용: port=8080실선 = 하드 링크(같은 아이노드), 점선 = 심볼릭 링크(경로를 따라감)
1 / 4

파일을 만들면 데이터와 속성은 아이노드 1201에, 이름 app.conf는 디렉터리에 적힌다. 링크 수는 1이다.

왼쪽은 디렉터리에 적힌 이름, 오른쪽은 실제 데이터와 속성을 담은 아이노드다. 하드 링크는 같은 아이노드를 가리키는 이름이 하나 더 생기는 것이고, 심볼릭 링크는 다른 경로를 글자로 적어 둔 작은 파일이다.
  • 하드 링크(ln 원본 새이름): 같은 아이노드에 이름을 하나 더 붙인다. 그림에서 링크 수는 1 → 2 → 2 → 1로 바뀐다. 아이노드 번호는 파일 시스템 안에서만 유일하므로 하드 링크는 다른 파일 시스템에 걸 수 없다(inode(7)).
  • 심볼릭 링크(ln -s 대상 새이름): 대상 경로를 글자로 담은 작은 파일이다. 다른 파일 시스템이나 디렉터리도 가리킬 수 있지만, 대상 이름이 사라지면 끊긴다. 설정의 ‘현재 판’을 가리키는 current 링크처럼 바꿔 끼우는 용도로 많이 쓴다.
  • rm은 이름을 지운다: 링크 수가 0이 되어야 데이터가 풀린다. ls -li로 아이노드 번호와 링크 수를 함께 볼 수 있다.

권한: rwx와 8진수

파일마다 소유자와 그룹이 있고, 소유자·그룹·나머지 세 묶음에 각각 읽기(r)·쓰기(w)·실행(x)을 켜고 끈다. 디렉터리에서 r은 목록 보기, w는 안에 만들고 지우기, x는 들어가기다. 묶음마다 r=4, w=2, x=1을 더하면 8진수 한 자리가 되므로 권한 전체가 세 자리 숫자로 적힌다. 커널은 접근하는 사람이 소유자면 소유자 칸만, 아니고 그룹원이면 그룹 칸만 본다.

권한 비트 그림: rwxr-xr-x는 소유자 4+2+1=7, 그룹 4+1=5, 나머지 4+1=5라서 755이고, rw-r--r--는 644다. umask 022는 새 파일 666을 644로, 새 디렉터리 777을 755로 만든다종류소유자그룹나머지755-rwx4+2+1 = 7r-x4+1 = 5r-x4+1 = 5실행 파일·디렉터리644-rw-4+2 = 6r--4 = 4r--4 = 4일반 파일r=4 읽기, w=2 쓰기, x=1 실행(디렉터리에서는 들어가기). 앞의 d는 디렉터리, -는 일반 파일새 파일: 요청 666에서 umask 022의 비트를 끄면 → 644 (rw-r--r--)새 디렉터리: 요청 777에서 umask 022의 비트를 끄면 → 755 (rwxr-xr-x)새 파일(077): 요청 666에서 umask 077의 비트를 끄면 → 600 (rw-------)
755와 644를 비트로 풀어 본 것. 아래는 umask가 새로 만드는 파일과 디렉터리의 권한을 정하는 계산이다.
ls -l deploy.sh
chmod 755 deploy.sh
chmod u+x,go-w deploy.sh
sudo chown user:dev deploy.sh
umask
umask 077
  • chmod는 숫자(chmod 755)나 기호(u+x, go-w)로 권한을 바꾸고, chown은 소유자와 그룹을 바꾼다. 소유자를 남에게 넘기는 일은 root만 할 수 있다.
  • umask는 새 파일에서 끌 비트다. 프로그램이 파일을 666, 디렉터리를 777로 만들려 해도 umask 022면 결과는 644와 755가 된다(umask(2)). 나만 읽어야 하는 파일을 만드는 스크립트는 umask 077로 시작하면 새 파일이 600이 된다.
  • 특수 비트: setuid(4)가 켜진 프로그램은 실행한 사람이 아니라 파일 소유자의 권한으로 돈다(sudo와 passwd가 rwsr-xr-x인 이유다). 디렉터리의 setgid(2)는 새 파일이 디렉터리의 그룹을 물려받게 하고, sticky(1)는 남의 파일을 지우지 못하게 한다. 모두가 쓰는 /tmp가 rwxrwxrwt인 이유다.
  • setuid는 공격 표면이다: root 소유 setuid 프로그램의 버그는 곧 권한 상승이다. 시스템에 어떤 것이 있는지 주기적으로 확인하고, 직접 만든 스크립트에 setuid를 붙이지 않는다.
sudo find / -xdev -perm -4000 -type f

사용자, root, sudo

사용자는 이름이 아니라 숫자 UID로 구별되고, UID 0인 root는 권한 검사를 거의 받지 않는다. 계정 목록은 /etc/passwd, 그룹은 /etc/group에 있고, id 명령이 내 UID와 그룹을 보여 준다. 프로세스는 실행한 사람의 ID를 물려받고, 커널은 그 ID로 파일 접근을 판단한다(credentials(7)).

sudo 흐름: uid 1000인 일반 사용자가 sudo를 실행하면, setuid 비트가 켜진 sudo가 root 권한으로 시작해 sudoers 규칙을 확인하고, 본인 비밀번호를 받은 뒤 명령 하나만 uid 0으로 실행하고 기록을 남긴다일반 사용자uid 1000sudo 명령sudo 실행rwsr-xr-xsetuid rootsudoers 확인이 사용자·명령이규칙에 있나본인 비밀번호root 것이 아니다5분 동안 기억root로 실행uid 0기록: syslog규칙에 없으면 여기서 거절하고 그 시도도 기록한다su와 sudosu: root 비밀번호로 root 셸 전체비밀번호를 여럿이 나눠 알게 된다sudo: 사람마다, 명령마다 허락누가 언제 무엇을 했는지 남는다
sudo로 명령 하나를 root로 실행하는 흐름. root 셸을 열어 두는 대신, 허락된 명령만 그때그때 root로 돌리고 흔적을 남긴다.
  • 평소에는 일반 계정: root로 일하면 오타 하나가 시스템 전체를 지운다. 관리 작업만 sudo 명령으로 한다.
  • sudoers: 누가 어떤 명령을 누구로 실행할 수 있는지 정한다. visudo로 고치면 저장 전에 문법을 검사해, 깨진 설정으로 sudo 자체를 못 쓰게 되는 일을 막는다. 인증한 뒤 기본 5분 동안은 비밀번호를 다시 묻지 않고, 성공·실패한 시도를 syslog에 남긴다(sudoers(5)).
  • 서비스 계정: 웹 서버나 DB는 로그인할 수 없는 전용 계정으로 돌리고, 그 계정이 쓸 디렉터리에만 권한을 준다. 하나가 뚫려도 피해가 그 계정의 권한에서 멈춘다.

프로세스와 신호

실행 중인 프로그램이 프로세스이고, 저마다 PID와 부모의 PID(PPID)가 있다. ps aux는 지금의 목록을, top은 실시간 순위를 보여 준다. 셸이 명령을 실행하는 방법은 새 프로그램을 바로 띄우는 것이 아니라, 자기를 복제하고 그 복제본이 다른 프로그램으로 바뀌는 두 단계다.

fork, exec, wait 단계 그림: PID 100인 bash가 PID 101 자식을 복제하고, 자식이 ls로 바뀌었다가 끝나 좀비가 되고, 부모가 wait로 종료 상태를 받으면 표에서 사라진다시스템 콜셸이 ls 입력을 읽음fork()execve("/usr/bin/ls")exit(0)wait() → 상태 0PIDPPID명령상태1001bash실행1001bash대기101100bash실행1001bash대기101100ls실행1001bash대기101100ls좀비1001bash실행부모가 먼저 끝나면자식은 고아가 되어init(PID 1)이 입양하고대신 wait해 준다PPID = 부모의 PID. 좀비는 실행되지 않지만 프로세스 표의 자리를 차지한다
1 / 5

셸(bash)이 ls를 읽었다. 아직 프로세스는 셸 하나다.

프로세스 표의 변화. 새 프로그램을 실행하는 시스템 콜은 따로 없고, ‘복제(fork)’와 ‘바꿔 끼우기(exec)’ 두 단계로 나뉜다. 그 사이에 셸이 자식의 표준 입출력을 바꿔 두는 것이 리디렉션의 원리다.

신호(signal)는 프로세스에 보내는 짧은 알림이다. 대부분은 프로그램이 받아서(catch) 원하는 동작을 할 수 있지만, SIGKILL과 SIGSTOP은 잡거나 막거나 무시할 수 없다(signal(7)).

신호번호(x86)기본 동작잡을 수 있나쓰임
SIGHUP1종료예터미널이 끊김. 데몬은 흔히 설정 다시 읽기로 쓴다
SIGINT2종료예Ctrl+C
SIGKILL9종료아니오kill -9. 커널이 바로 없앤다
SIGTERM15종료예kill의 기본값. 정리하고 끝내라는 요청
SIGCHLD17무시예자식이 끝났음을 부모에게 알림
SIGCONT18계속예멈춘 프로세스를 다시 돌림(fg, bg)
SIGSTOP19멈춤아니오잡을 수 없는 멈춤
SIGTSTP20멈춤예Ctrl+Z
ps aux
pgrep -a nginx
kill 4242
kill -9 4242
./long-task.sh &
jobs
fg %1
  • kill -15 먼저, -9는 마지막: kill의 기본은 SIGTERM(15)이다. 프로그램은 이것을 받아 쓰던 파일을 닫고, 연결을 정리하고, 임시 파일을 지운 뒤 끝낸다. SIGKILL(9)은 커널이 그 자리에서 없애므로 이런 정리가 하나도 일어나지 않는다. TERM을 보내고 몇 초 기다린 뒤에도 남아 있을 때만 KILL을 쓴다.
  • 좀비와 고아: 끝났지만 부모가 아직 종료 상태를 읽어 가지 않은 자식이 좀비다. 좀비는 이미 죽어 있어서 kill로 없앨 수 없고, 부모가 wait하거나 부모가 끝나야 사라진다. 부모가 먼저 끝난 자식(고아)은 init(PID 1)이 입양해 대신 wait한다(wait(2)). 좀비가 계속 쌓인다면 부모 프로그램의 버그다.
  • 포그라운드와 백그라운드: 끝에 &를 붙이면 셸이 기다리지 않고 바로 프롬프트를 준다. 실행 중인 것을 Ctrl+Z(SIGTSTP)로 멈춘 뒤 bg로 뒤에서, fg로 앞에서 다시 돌린다. jobs가 이 셸의 작업 목록이다. 터미널을 닫아도 계속 돌아야 하는 일은 systemd 서비스나 tmux 같은 터미널 다중화기에 맡긴다.

셸이 명령을 읽는 순서

셸은 친 글자를 그대로 프로그램에 넘기지 않는다. Bash 매뉴얼이 정한 순서는 중괄호 확장 → 물결(~)·변수·산술·명령 치환(왼쪽부터) → 단어 분할 → 파일 이름 확장(글롭) → 따옴표 제거다. 이 순서가 셸 스크립트 버그의 대부분을 설명한다.

셸 확장 단계 그림: 따옴표 친 "$SRC"는 my notes.txt 한 인자로, *.log는 app.log와 db.log로, $(date +%F)는 날짜로 바뀌어 cp가 인자 네 개를 받는다1. 입력1. 입력2. 낱말 나누기2. 낱말 나누기3. 치환3. 치환4. 단어 분할4. 단어 분할5. 글롭5. 글롭6. 따옴표 제거6. 따옴표 제거cp "$SRC" *.log backup-$(date +%F)/낱말 1개cp"$SRC"*.logbackup-$(date +%F)/낱말 4개cp"my notes.txt"*.logbackup-2026-10-30/낱말 4개cp"my notes.txt"*.logbackup-2026-10-30/낱말 4개cp"my notes.txt"app.logdb.logbackup-2026-10-30/낱말 5개cpmy notes.txtapp.logdb.logbackup-2026-10-30/낱말 5개색칠한 칸 = 인자, 회색 첫 칸 = 실행할 명령
1 / 6

셸이 한 줄을 읽는다. 아직은 글자일 뿐이다.

예: cp "$SRC" *.log backup-$(date +%F)/. 변수 SRC에는 공백이 든 파일 이름이, 현재 디렉터리에는 로그 두 개가 있다. 칸 하나가 프로그램이 받는 인자 하나다.
  • 프로그램은 결과만 본다: cp는 *.log라는 글자를 받지 않는다. 셸이 이미 파일 이름으로 바꿔 넘긴다. 그래서 rm *의 위험은 rm이 아니라 셸이 만든 인자 목록에 있다. 맞는 파일이 없으면 bash는 패턴을 그대로 넘긴다(nullglob을 켜지 않았을 때).
  • 큰따옴표는 변수만 살린다: "$SRC"는 치환은 되지만 단어 분할과 글롭을 거치지 않아 한 인자로 남는다. 작은따옴표 안은 아무것도 바뀌지 않는다. 따옴표 없는 $SRC는 공백에서 갈라져 my와 notes.txt 두 파일을 찾는다.
  • 명령 치환: $(명령)은 그 명령의 출력으로 바뀐다. 결과도 따옴표 밖이면 단어 분할을 거치므로 "$(명령)"처럼 감싸는 습관을 들인다.
SRC="my notes.txt"
cp $SRC /tmp/
cp "$SRC" /tmp/
echo '$SRC는 그대로 찍힌다'

표준 입출력, 파이프, 리디렉션

프로세스는 처음부터 파일 설명자 세 개를 갖고 시작한다. 0은 표준 입력, 1은 표준 출력, 2는 표준 오류이고, 터미널에서 실행하면 셋 다 터미널을 가리킨다. 리디렉션은 fork와 exec 사이에서 셸이 이 셋이 가리키는 곳을 바꿔 두는 것이고, 프로그램은 바뀐 줄도 모른 채 0·1·2에 읽고 쓴다.

  • 명령 > 파일은 1번을 파일로 바꾸고(덮어씀), >>는 뒤에 덧붙인다. < 파일은 0번을 파일로 바꾼다.
  • 2> 파일은 오류만, 2>&1은 ‘2번을 지금 1번이 가리키는 곳으로’ 바꾼다. bash의 &> 파일은 둘 다 한 파일로 보내는 줄임이다.
  • A | B는 A의 표준 출력을 B의 표준 입력에 잇는다. 표준 오류는 파이프로 가지 않고 화면에 남으므로, 진행 메시지는 오류 쪽에 쓰는 것이 관례다.
리디렉션 순서: cmd >out.log 2>&1은 1번을 파일로 돌린 뒤 2번이 그것을 베껴 둘 다 파일로 가고, cmd 2>&1 >out.log는 2번이 먼저 터미널을 베낀 뒤 1번만 파일로 가서 오류는 화면에 남는다cmd >out.log 2>&1시작1 → 터미널2 → 터미널1. >out.log1 → out.log2 → 터미널2. 2>&11 → out.log2 → out.log출력과 오류 모두 out.log의도한 대로다cmd 2>&1 >out.log시작1 → 터미널2 → 터미널1. 2>&11 → 터미널2 → 터미널2. >out.log1 → out.log2 → 터미널오류는 여전히 화면으로2번이 옛 1번(터미널)을 베꼈다0 = 표준 입력, 1 = 표준 출력, 2 = 표준 오류. 셸은 리디렉션을 왼쪽부터 차례로 적용한다
같은 두 리디렉션을 순서만 바꿔 적용한 결과. 2>&1은 1번과 ‘연결’하는 것이 아니라, 그 순간 1번이 가리키는 곳을 베낀다. bash(1)도 ‘리디렉션의 순서가 중요하다’고 적고 같은 예를 든다.

그래서 로그 파일에 오류까지 모으려면 명령 >out.log 2>&1처럼 파일을 먼저 적는다. 반대로 적으면 오류가 화면에 남아, 크론이나 CI에서 실패 원인이 기록되지 않는다(bash(1)).

자주 쓰는 명령

유닉스 도구는 저마다 한 가지 일을 하고 글자 줄을 입력·출력으로 주고받는다. 그래서 작은 명령을 파이프로 이어 큰일을 한다. 자주 쓰는 것을 모았다. 옵션은 man과 --help로 확인한다.

명령하는 일예
grep파일에서 글자·정규식이 든 줄 찾기grep -rn "TODO" src/
sed줄마다 바꾸기·지우기(-i는 파일을 직접 고친다)sed -i 's/8080/9090/g' app.conf
awk칸으로 나눠 고르고 세기awk -F: '{print $1}' /etc/passwd
find이름·시간·크기·권한으로 파일 찾기find . -name '*.log' -mtime +7
xargs입력 줄을 다른 명령의 인자로find . -name '*.tmp' -print0 | xargs -0 rm --
curlHTTP 요청. -f는 HTTP 오류를 종료 코드로curl -fsSL https://example.com/health
tar여러 파일을 하나로 묶고 풀기tar -czf site.tar.gz site/ · tar -xzf site.tar.gz
ss열린 소켓과 리슨 중인 포트sudo ss -tlnp
df파일 시스템마다 남은 공간df -h
du디렉터리가 실제로 쓰는 공간du -sh /var/log/*
topCPU·메모리를 많이 쓰는 프로세스 실시간 순위top · htop
man명령의 설명서. 막히면 먼저 본다man 7 signal · ls --help
파이프라인 그림: 로그 10줄에서 grep이 오류 6줄을 고르고, awk가 상태 코드만 남기고, sort와 uniq -c가 코드별로 세고, sort -nr과 head가 401 세 번, 500 두 번, 404 한 번을 남긴다명령(왼쪽 출력이 오른쪽 입력)넘기는 양하는 일grep -E ' [45][0-9]{2}$' access.log10줄 중 6줄4xx·5xx로 끝나는 줄만| awk '{print $3}'6줄셋째 칸(상태 코드)만| sort6줄같은 값이 붙어 있게 정렬| uniq -c3줄붙은 같은 줄을 세어 하나로| sort -nr | head -33줄개수 큰 순서로 위 셋 3 401 2 500 1 404uniq는 붙어 있는 같은 줄만 센다.그래서 앞에 sort가 꼭 있어야 한다
접근 로그에서 오류 상태 코드를 많이 나온 순서로 세는 파이프라인. 왼쪽 명령의 출력이 오른쪽 명령의 입력이 되고, 단계마다 넘기는 줄 수가 줄어든다.
  • 예제 결과: 로그 10줄 중 오류는 6줄이고, 가장 많은 것은 401(3번)이다. 로그인 실패가 몰렸다는 뜻이니 다음에는 그 시각과 IP를 본다.
  • 공백이 든 파일 이름: find … | xargs rm은 공백에서 이름을 가른다. -print0과 xargs -0을 짝으로 쓰고, --로 인자가 옵션으로 읽히지 않게 한다.
  • df와 du가 다를 때: 지운 로그를 어떤 프로세스가 아직 열고 있으면 이름은 없어도 공간은 풀리지 않는다. 그 프로세스를 재시작하거나 파일을 지우는 대신 비운다.

환경 변수와 PATH

환경 변수는 프로세스가 물려받는 ‘이름=값’ 목록이다. 셸 변수에 export를 붙여야 자식 프로세스에 넘어가고, 자식이 바꾼 값은 부모에게 돌아오지 않는다(fork가 복사하기 때문이다). 설정 파일 위치, 실행 환경(APP_ENV), 비밀값을 프로그램에 넘기는 데 쓴다. 비밀값은 저장소에 두지 않고 실행할 때 환경으로 넣는다.

PATH 검색 표: PATH가 /home/user/.local/bin, /usr/local/bin, /usr/bin, /bin 순서일 때 python3은 /usr/local/bin에서 찾고 /usr/bin의 것은 가려지며, ls는 /usr/bin에서, deploy는 맨 앞 디렉터리에서 찾는다PATH=/home/user/.local/bin:/usr/local/bin:/usr/bin:/bin순서 · 디렉터리python3lsdeploy1. /home/user/.local/bin없음없음여기서 찾음2. /usr/local/bin여기서 찾음없음보지 않음3. /usr/bin있지만 가려짐여기서 찾음보지 않음4. /bin보지 않음있지만 가려짐보지 않음python3이 둘이면 앞의 것이 이긴다. 어느 것이 쓰이는지는 command -v python3으로 확인한다현재 디렉터리(.)를 PATH 앞에 두면 같은 이름의 가짜 프로그램이 먼저 실행될 수 있다
PATH의 디렉터리를 앞에서부터 찾는 모습. 같은 이름의 프로그램이 둘 있으면 앞의 것이 이기고, 뒤의 것은 없는 것과 같다.

PATH는 셸이 명령을 찾을 디렉터리 목록이다. 예제에서 python3은 2번째 디렉터리인 /usr/local/bin/python3에서 찾고 멈춘다. ‘설치했는데 옛 판이 실행된다’는 문제의 대부분이 이 순서 때문이고, command -v로 실제로 무엇이 실행되는지 확인한다.

export APP_ENV=production
echo "$PATH"
command -v python3
export PATH="$HOME/.local/bin:$PATH"

패키지 관리: apt와 dnf

배포판은 검증한 소프트웨어를 패키지로 묶어 저장소에 둔다. 패키지 관리자는 의존성을 풀어 필요한 것을 함께 설치하고, 어떤 파일이 어느 패키지 것인지 기억해 깨끗이 지우고 올린다. 인터넷에서 받은 설치 스크립트를 root로 바로 실행하는 것보다 추적과 업데이트가 쉽다.

패키지 관리 흐름: 저장소의 서명된 목록을 apt update로 받아 두고, apt install이 의존성을 풀어 필요한 패키지를 함께 설치하며 파일을 /usr/bin, /etc 등에 놓는다. 아래는 apt와 dnf의 대응 명령이다저장소배포판이 서명한패키지와 목록apt update목록만 내려받아로컬에 저장apt install web의존성 풀기:web → libA, libB파일 배치/usr/bin, /etc,systemd 단위update는 목록만 새로 받는다. 실제로 판을 올리는 것은 upgrade다할 일Debian·Ubuntu (apt)Fedora·RHEL (dnf)업그레이드apt upgradednf upgrade설치 / 제거apt install / apt removednf install / dnf remove찾기apt searchdnf search되돌리기(직접 판을 지정)dnf history undo
apt의 흐름. 목록을 받는 일과 실제로 설치·업그레이드하는 일이 나뉜다. 아래는 Debian 계열 apt와 RPM 계열 dnf의 대응이다.
sudo apt update
sudo apt upgrade
sudo apt install nginx
apt show nginx
sudo apt purge nginx
sudo apt autoremove

sudo dnf install nginx
sudo dnf history undo 42
  • update와 upgrade: apt update는 저장소의 패키지 목록만 받는다. apt upgrade는 설치된 것을 올리되 다른 패키지를 지워야 하는 업그레이드는 건너뛰고, full-upgrade는 필요하면 지우면서 올린다(apt(8)).
  • remove와 purge: remove는 설정 파일을 남겨 다시 설치하면 되살아나고, purge는 그것까지 지운다. autoremove는 의존성으로만 들어왔다가 더는 필요 없는 패키지를 지운다.
  • 스크립트에는 apt-get: apt 명령의 화면 출력과 동작은 판마다 바뀔 수 있어, apt(8)은 스크립트에서 apt-get·apt-cache를 쓰라고 권한다.
  • dnf: RPM 계열 배포판의 패키지 관리자로 YUM의 다음 판이다. dnf history undo로 지난 트랜잭션을 거꾸로 되돌릴 수 있다(DNF 명령 참조).

서비스와 로그: systemd와 journalctl

많은 배포판에서 PID 1은 systemd다. 서비스 하나를 단위 파일(unit)로 적어 두면 systemd가 부팅 때 순서대로 띄우고, 죽으면 다시 띄우고, 출력을 저널에 모은다. 터미널에서 &로 띄운 프로세스와 달리 로그아웃해도 살아 있고, 재부팅해도 다시 뜬다.

systemd 그림: 왼쪽은 app.service 단위 파일, 오른쪽은 systemctl start로 inactive에서 active가 되고, 프로세스가 0이 아닌 값으로 끝나면 failed를 거쳐 Restart=on-failure로 다시 시작하는 흐름과, 표준 출력이 저널에 모여 journalctl로 읽히는 길이다/etc/systemd/system/app.service[Unit]Description=Example web app[Service]Type=execUser=appExecStart=/usr/bin/app --port 8080Restart=on-failure[Install]WantedBy=multi-user.targetinactiveactivefailedsystemctl startexit 1Restart=on-failureapp의 stdout·stderr저널journalctl -u app -f이 서비스의 로그를 따라 읽기
서비스 단위 파일과 상태. Restart=on-failure는 0이 아닌 종료, 신호로 인한 죽음, 시간 초과에서 다시 띄우고, 깨끗한 종료나 systemctl stop에서는 띄우지 않는다.
sudo systemctl daemon-reload
sudo systemctl enable --now app
systemctl status app
sudo systemctl restart app
journalctl -u app -f
journalctl -u app --since today -p err
journalctl -b -1 -e
  • Type=exec: 기본값인 simple은 fork 직후를 ‘시작됨’으로 보므로, 실행 파일 경로가 틀려도 시작에 성공한 것처럼 보일 수 있다. exec은 실행 파일이 실제로 실행된 뒤를 시작으로 봐서 이런 실패를 제대로 알린다. 오래 도는 서비스에는 exec과 Restart=on-failure를 권한다(systemd.service(5)).
  • User=: 서비스를 전용 계정으로 돌린다. 단위 파일에 비밀값을 바로 적기보다 권한을 좁힌 별도 파일에서 읽게 한다.
  • journalctl: -u는 단위 하나, -f는 따라 읽기, -b는 부팅 하나(-b -1은 지난 부팅), --since는 시작 시각, -p err는 그 수준 이상만, -e는 맨 끝에서 연다(journalctl(1)). 서비스가 왜 죽었는지는 systemctl status의 마지막 몇 줄과 지난 부팅의 저널에서 찾는다.

예약 작업: cron

cron은 정해진 시각에 명령을 실행한다. crontab -e로 사용자마다 표를 고치고, 한 줄은 다섯 칸의 시각(분, 시, 일, 월, 요일)과 명령이다. 칸에는 *(전부), 범위(1-5), 목록(1,15), 걸음(*/15)을 쓴다(crontab(5)).

cron 식 30 2 * * 1-5는 분 30, 시 2, 매일, 매월, 월요일부터 금요일이라 평일 새벽 2시 30분에 돈다. 2026년 10월 30일 금요일 정오 기준 다음 실행은 11월 2일 월요일부터다. */15 * * * *는 15분마다 돈다분300–5930시20–232일*1–31모두월*1–12모두요일1-50–7(0·7=일)1,2,3,4,5기준 시각 2026-10-30 (금) 12:00 이후 다음 세 번30 2 * * 1-52026-11-02 (월) 02:302026-11-03 (화) 02:302026-11-04 (수) 02:30*/15 * * * *2026-10-30 (금) 12:152026-10-30 (금) 12:302026-10-30 (금) 12:45cron은 SHELL=/bin/sh로 돌고 환경 변수가 로그인 셸과 다르다. 경로는 절대 경로로 적는다
cron 식의 다섯 칸과, 그 식이 실제로 다음에 실행되는 시각. 시각은 기계의 시간대를 따르며 그림은 UTC로 계산했다.
crontab -e
30 2 * * 1-5 /home/user/bin/backup.sh >> /home/user/backup.log 2>&1
0 * * * * date +\%H >> /home/user/hours.log
  • 환경이 다르다: cron은 기본으로 /bin/sh로 명령을 돌리고, HOME과 LOGNAME 정도만 정해 준다. 로그인 셸에서 되던 명령이 cron에서 ‘찾을 수 없음’으로 실패하면 대개 PATH 문제다. 명령과 파일은 절대 경로로 적는다.
  • %는 줄바꿈: 명령 안의 %는 줄바꿈으로 바뀌고 그 뒤는 표준 입력으로 간다. date +%H처럼 쓰려면 \%로 막는다.
  • 출력을 남긴다: 결과를 어디에도 남기지 않으면 실패해도 모른다. 위 예처럼 로그 파일로 표준 출력과 오류를 함께 보낸다. 리디렉션 순서는 앞 절의 함정 그대로다.
  • systemd 타이머: 놓친 실행을 부팅 뒤 따라잡거나 저널에 로그를 모으고 싶으면 systemd 타이머 단위를 쓰는 방법도 있다.

SSH 키와 known_hosts

SSH는 두 방향으로 신분을 확인한다. 서버는 호스트 키로 ‘내가 그 서버가 맞다’를, 사용자는 개인 키로 ‘내가 그 사람이 맞다’를 증명한다. 비밀번호와 달리 개인 키 자체는 네트워크로 가지 않고 서명만 간다. 원리는 암호학 노트의 공개 키 서명과 같다.

SSH 키 그림: 클라이언트에는 개인 키 id_ed25519와 known_hosts가, 서버에는 호스트 키와 authorized_keys가 있다. 먼저 서버가 호스트 키로 서명하면 클라이언트가 known_hosts의 지문과 대조하고, 이어서 클라이언트가 개인 키로 서명하면 서버가 authorized_keys의 공개 키로 확인한다내 컴퓨터(클라이언트)~/.ssh/id_ed25519개인 키: 밖으로 보내지 않는다~/.ssh/id_ed25519.pub공개 키: 서버에 등록한다~/.ssh/known_hosts믿기로 한 서버 키 목록서버/etc/ssh/ssh_host_ed25519_key호스트 키: 서버의 신분~/.ssh/authorized_keys로그인을 허락할 공개 키/etc/ssh/sshd_config서버 설정① 호스트 키로 서명known_hosts와 대조② 개인 키로 서명공개 키로 확인처음 접속: 지문을 보여 주고 묻는다 → 서버 관리자에게 받은 지문과 맞을 때만 yes키가 바뀌었다는 경고: 서버를 다시 설치했는지 먼저 확인한다. 아니면 가로채기일 수 있다PasswordAuthentication no키가 등록된 것을 확인한 뒤 비밀번호 로그인을 끈다PermitRootLogin noroot로 바로 들어오지 못하게 하고 sudo를 쓴다
SSH의 키 네 종류와 확인 순서. known_hosts는 ‘이 서버의 키는 이것’이라는 내 기록이고, authorized_keys는 ‘이 공개 키를 가진 사람은 들여보낸다’는 서버의 기록이다.
ssh-keygen -t ed25519 -C "work laptop"
ssh-keygen -l -f ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/id_ed25519
ssh-keygen -R server.example.com
  • 키 만들기: ssh-keygen은 인자 없이 실행하면 Ed25519 키를 만든다. 개인 키에는 암호(passphrase)를 걸어, 노트북을 잃어버려도 키 파일만으로는 쓰지 못하게 한다. 암호를 잊으면 되찾을 수 없으니 새 키를 만들어 다시 등록한다(ssh-keygen(1)).
  • 키 관리: 기기마다 다른 키를 쓰면 하나를 잃어도 그 키만 서버의 authorized_keys에서 지우면 된다. 개인 키는 600 권한으로 두고, 저장소나 채팅에 올리지 않는다. 더 쓰지 않는 키는 서버에서 바로 지운다.
  • known_hosts: 처음 접속하면 서버 키의 지문을 보여 주고 묻는다. 서버 관리자가 알려 준 지문과 맞을 때만 받아들인다. 나중에 키가 바뀌었다는 경고가 뜨면 ssh는 비밀번호 인증을 막는다(ssh(1)). 서버를 다시 설치한 것이 확인될 때만 ssh-keygen -R로 옛 기록을 지운다.
  • 비밀번호 로그인 끄기: PasswordAuthentication과 KbdInteractiveAuthentication의 기본값은 yes이고, PermitRootLogin의 기본값은 비밀번호만 막는 prohibit-password다(sshd_config(5)). 키 로그인이 되는 것을 다른 창에서 확인한 뒤 아래처럼 바꾸고 sshd를 다시 읽힌다. 확인 전에 끄면 스스로를 잠근다.
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

셸 스크립트 안전장치

bash는 기본으로 실패에 관대하다. 중간 명령이 실패해도 다음 줄로 넘어가고, 없는 변수는 빈 글자가 되고, 파이프라인의 성공 여부는 마지막 명령만 본다. 스크립트 맨 위의 set -euo pipefail이 이 세 가지를 바꾼다.

  • -e: 명령이 실패(0이 아닌 종료 상태)하면 스크립트를 멈춘다. 단 if·while의 조건, &&·|| 목록의 마지막이 아닌 명령, !로 뒤집은 명령은 예외라서, ‘실패해도 되는 명령’을 표현하는 길이 남는다(bash(1)).
  • -u: 설정되지 않은 변수를 쓰면 오류로 멈춘다. 변수 이름 오타가 빈 경로가 되어 엉뚱한 곳을 지우는 사고를 막는다.
  • -o pipefail: 파이프라인의 종료 상태를 ‘0이 아닌 값으로 끝난 가장 오른쪽 명령의 값’으로 바꾼다. 아래 첫 줄은 내려받기가 실패해도 기본으로는 0, pipefail이면 22다.
pipefail 표: 앞 명령이 실패하고 뒤 명령이 성공하는 파이프라인은 기본으로 종료 상태 0이라 set -e가 멈추지 않지만, pipefail을 켜면 실패 코드가 남아 그 줄에서 멈춘다. 아래는 set -u가 비어 있는 변수로 엉뚱한 경로를 지우는 일을 막는 예다파이프라인각 명령의 종료 코드기본 $?pipefail $?curl -fsS $URL | tar xz22 | 0내려받기 실패022 → 멈춤grep ERROR app.log | wc -l2 | 0로그 파일 없음02 → 멈춤make | tee build.log2 | 0빌드 실패02 → 멈춤echo ok | grep -q ok0 | 0모두 성공00빨간 0: 실패를 성공으로 잘못 알고 다음 줄로 넘어간다rm -rf "$BUILD_DIR/cache"BUILD_DIR에 오타가 있어 비어 있으면set -u 없음rm -rf "/cache"set -u 있음설정 안 된 변수라 그 자리에서 멈춘다
pipefail을 켜기 전후의 종료 상태와, set -u가 막는 사고. 실패를 성공으로 읽은 스크립트는 망가진 결과물을 다음 단계로 넘긴다.
#!/usr/bin/env bash
set -euo pipefail

readonly backup_dir="/home/user/backup"
mkdir -p -- "$backup_dir"
for file in ./*.log; do
  [ -e "$file" ] || continue
  gzip -c -- "$file" > "$backup_dir/$(basename -- "$file").gz"
done
  • 변수는 언제나 큰따옴표로: "$file". 공백·별표가 든 이름에서 깨지지 않는다. 앞 절의 확장 순서가 이유다.
  • 옵션처럼 생긴 이름: -rf라는 파일 이름도 있을 수 있다. -- 뒤의 인자는 옵션으로 읽지 않는다.
  • ShellCheck: 따옴표 누락, 쓰지 않는 변수, 잘못된 비교 같은 실수를 정적으로 찾는다(ShellCheck). CI에 넣으면 리뷰에서 놓치는 것이 줄어든다. 스크립트가 수백 줄을 넘거나 자료 구조가 필요해지면 Python 같은 언어로 옮기는 것이 낫다.

흔한 오해

  • ‘kill -9가 확실한 종료다’: 확실히 없애기는 하지만 정리할 기회를 주지 않는다. 반쯤 쓴 파일과 남은 잠금 파일이 다음 실행을 깨뜨린다. 또 좀비는 이미 끝난 프로세스라 kill -9로도 사라지지 않는다.
  • ‘권한 오류는 chmod 777로 푼다’: 모든 사용자에게 쓰기와 실행을 여는 것이다. 웹 서버 디렉터리라면 누구든 코드를 바꿔 넣을 수 있게 된다. 필요한 것은 소유자와 그룹을 맞추고 필요한 비트만 켜는 일이다.
  • ‘sudo는 root 비밀번호를 묻는다’: 기본 설정의 sudo는 내 비밀번호를 묻는다. root 비밀번호를 여럿이 나눠 알 필요가 없다는 것이 sudo의 요점이다.
  • ‘rm으로 지운 것은 휴지통에 있다’: 명령줄의 rm에는 휴지통이 없다. 되살리기는 백업이 있을 때만 확실하고, 그 밖의 복원은 디지털 포렌식 노트의 영역이다.
  • ‘따옴표는 취향이다’: 변수의 따옴표 유무는 인자 개수를 바꾼다. 공백이 든 파일 이름 하나가 스크립트를 엉뚱한 파일에 작동시킨다.
  • ‘리눅스는 안전하니 신경 쓸 것이 없다’: 실제 침해는 비밀번호 로그인이 열린 SSH, 너무 넓은 권한, 업데이트하지 않은 패키지, 오염된 의존성으로 들어온다. 방어 원리는 보안 노트에 있다.
  • ‘/tmp는 내 작업 공간이다’: 모든 사용자가 함께 쓰는 곳이다. sticky 비트가 남의 파일 삭제를 막을 뿐이다. 이름을 예측할 수 있는 임시 파일 대신 mktemp를 쓰고 비밀은 두지 않는다.

핵심 자료

자료한 줄 요지
The Linux Command Line (William Shotts)셸 입문부터 스크립트까지를 다룬 책. CC BY-NC-ND로 PDF를 무료로 받을 수 있다.
Bash 매뉴얼: Shell Expansions셸이 한 줄을 실행하기 전 거치는 확장의 순서와 각 단계의 규칙.
bash(1) 매뉴얼 페이지리디렉션 순서, set -e의 예외, pipefail의 종료 상태 규칙까지 한 문서에 있다.
hier(7)디렉터리 계층의 공식 요약. FHS 3.0을 따른다.
signal(7)신호 번호와 기본 동작, 잡을 수 없는 SIGKILL·SIGSTOP.
systemd.service(5)서비스 단위의 Type=, ExecStart=, Restart= 같은 설정의 정확한 뜻.
sshd_config(5)SSH 서버 설정 항목과 기본값. 비밀번호 로그인과 root 로그인 설정이 여기 있다.
ShellCheck셸 스크립트의 따옴표 누락 같은 흔한 버그를 찾아 주는 정적 분석 도구.