리눅스와 셸
서버와 컨테이너, 클라우드의 대부분이 리눅스 위에서 돈다. 커널과 배포판의 관계, 디렉터리 구조와 권한, 프로세스와 신호, 셸이 명령 한 줄을 어떻게 읽어 실행하는지, 파이프와 리디렉션, 서비스·로그·예약 작업, SSH 키까지를 실제 명령과 그림으로 정리한다.
목차 (17)
셸(shell)은 사람이 친 글자를 읽어 프로그램을 찾아 실행하고 결과를 돌려주는 프로그램이다. 셸 자체도 커널 위에서 도는 평범한 프로세스라서, 셸이 하는 일은 결국 커널에 시스템 콜로 부탁하는 일이다. 커널 모드와 시스템 콜, 프로세스와 가상 메모리의 원리는 운영체제 노트에 있고, 여기서는 그 원리가 리눅스 명령줄에서 어떻게 보이는지를 다룬다. 예시의 사용자 이름은 user, 홈은 /home/user로 쓴다.
리눅스, 배포판, 커널과 사용자 영역
엄밀히 리눅스는 커널의 이름이다(kernel.org). 커널은 하드웨어를 직접 다루며 프로세스, 메모리, 파일 시스템, 드라이버, 네트워크를 관리한다. 그 위의 셸, C 라이브러리, 서비스 관리자, 응용 프로그램은 사용자 영역(user space)에 있고, 하드웨어나 다른 프로세스에 닿으려면 반드시 시스템 콜을 거친다. 권한 검사도 이 경계에서 일어난다.
- 배포판(distribution)은 커널에 사용자 영역 도구, 패키지 관리자, 기본 설정과 보안 정책을 묶어 내놓은 것이다. Debian·Ubuntu는
.deb패키지와 apt, Fedora·RHEL 계열은.rpm패키지와 dnf를 쓴다. - 고를 때 볼 것: 패키지 형식, 출시 주기(정해진 판을 오래 고치며 쓰는 방식인지, 계속 최신으로 굴러가는 방식인지), 보안 업데이트를 주는 기간이다. 서버에는 오래 지원하는 판을 쓰고, 컨테이너 이미지는 작은 배포판을 고르는 일이 많다.
- 명령은 거의 같다: 배포판이 달라도 셸, 파일 시스템 구조, 권한, 신호는 같다. 크게 다른 곳은 패키지 관리와 일부 설정 파일 위치다.
파일 시스템 계층과 ‘모든 것은 파일’
리눅스에는 드라이브 문자가 없다. 모든 것이 루트 / 아래 한 나무에 붙고, 디스크나 USB는 그 나무의 한 디렉터리에 마운트된다. 어느 디렉터리에 무엇을 두는지는 FHS(Filesystem Hierarchy Standard) 3.0이 정하고, hier(7)가 그 요약이다.
유닉스 계열의 오래된 설계 원칙이 ‘모든 것은 파일’이다. 일반 파일뿐 아니라 장치(/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)에 있고, 디렉터리는 ‘이름 → 아이노드 번호’를 적은 표일 뿐이다. 이름과 실체가 떨어져 있기 때문에 링크가 가능하다.
파일을 만들면 데이터와 속성은 아이노드 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진수 한 자리가 되므로 권한 전체가 세 자리 숫자로 적힌다. 커널은 접근하는 사람이 소유자면 소유자 칸만, 아니고 그룹원이면 그룹 칸만 본다.
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)).
- 평소에는 일반 계정: root로 일하면 오타 하나가 시스템 전체를 지운다. 관리 작업만
sudo 명령으로 한다. - sudoers: 누가 어떤 명령을 누구로 실행할 수 있는지 정한다.
visudo로 고치면 저장 전에 문법을 검사해, 깨진 설정으로 sudo 자체를 못 쓰게 되는 일을 막는다. 인증한 뒤 기본 5분 동안은 비밀번호를 다시 묻지 않고, 성공·실패한 시도를 syslog에 남긴다(sudoers(5)). - 서비스 계정: 웹 서버나 DB는 로그인할 수 없는 전용 계정으로 돌리고, 그 계정이 쓸 디렉터리에만 권한을 준다. 하나가 뚫려도 피해가 그 계정의 권한에서 멈춘다.
프로세스와 신호
실행 중인 프로그램이 프로세스이고, 저마다 PID와 부모의 PID(PPID)가 있다. ps aux는 지금의 목록을, top은 실시간 순위를 보여 준다. 셸이 명령을 실행하는 방법은 새 프로그램을 바로 띄우는 것이 아니라, 자기를 복제하고 그 복제본이 다른 프로그램으로 바뀌는 두 단계다.
셸(bash)이 ls를 읽었다. 아직 프로세스는 셸 하나다.
신호(signal)는 프로세스에 보내는 짧은 알림이다. 대부분은 프로그램이 받아서(catch) 원하는 동작을 할 수 있지만, SIGKILL과 SIGSTOP은 잡거나 막거나 무시할 수 없다(signal(7)).
| 신호 | 번호(x86) | 기본 동작 | 잡을 수 있나 | 쓰임 |
|---|---|---|---|---|
SIGHUP | 1 | 종료 | 예 | 터미널이 끊김. 데몬은 흔히 설정 다시 읽기로 쓴다 |
SIGINT | 2 | 종료 | 예 | Ctrl+C |
SIGKILL | 9 | 종료 | 아니오 | kill -9. 커널이 바로 없앤다 |
SIGTERM | 15 | 종료 | 예 | kill의 기본값. 정리하고 끝내라는 요청 |
SIGCHLD | 17 | 무시 | 예 | 자식이 끝났음을 부모에게 알림 |
SIGCONT | 18 | 계속 | 예 | 멈춘 프로세스를 다시 돌림(fg, bg) |
SIGSTOP | 19 | 멈춤 | 아니오 | 잡을 수 없는 멈춤 |
SIGTSTP | 20 | 멈춤 | 예 | 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 매뉴얼이 정한 순서는 중괄호 확장 → 물결(~)·변수·산술·명령 치환(왼쪽부터) → 단어 분할 → 파일 이름 확장(글롭) → 따옴표 제거다. 이 순서가 셸 스크립트 버그의 대부분을 설명한다.
셸이 한 줄을 읽는다. 아직은 글자일 뿐이다.
- 프로그램은 결과만 본다:
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의 표준 입력에 잇는다. 표준 오류는 파이프로 가지 않고 화면에 남으므로, 진행 메시지는 오류 쪽에 쓰는 것이 관례다.
그래서 로그 파일에 오류까지 모으려면 명령 >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 -- |
curl | HTTP 요청. -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/* |
top | CPU·메모리를 많이 쓰는 프로세스 실시간 순위 | top · htop |
man | 명령의 설명서. 막히면 먼저 본다 | man 7 signal · ls --help |
- 예제 결과: 로그 10줄 중 오류는 6줄이고, 가장 많은 것은 401(3번)이다. 로그인 실패가 몰렸다는 뜻이니 다음에는 그 시각과 IP를 본다.
- 공백이 든 파일 이름:
find … | xargs rm은 공백에서 이름을 가른다.-print0과xargs -0을 짝으로 쓰고,--로 인자가 옵션으로 읽히지 않게 한다. - df와 du가 다를 때: 지운 로그를 어떤 프로세스가 아직 열고 있으면 이름은 없어도 공간은 풀리지 않는다. 그 프로세스를 재시작하거나 파일을 지우는 대신 비운다.
환경 변수와 PATH
환경 변수는 프로세스가 물려받는 ‘이름=값’ 목록이다. 셸 변수에 export를 붙여야 자식 프로세스에 넘어가고, 자식이 바꾼 값은 부모에게 돌아오지 않는다(fork가 복사하기 때문이다). 설정 파일 위치, 실행 환경(APP_ENV), 비밀값을 프로그램에 넘기는 데 쓴다. 비밀값은 저장소에 두지 않고 실행할 때 환경으로 넣는다.
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로 바로 실행하는 것보다 추적과 업데이트가 쉽다.
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가 부팅 때 순서대로 띄우고, 죽으면 다시 띄우고, 출력을 저널에 모은다. 터미널에서 &로 띄운 프로세스와 달리 로그아웃해도 살아 있고, 재부팅해도 다시 뜬다.
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)).
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-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다.
#!/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 | 셸 스크립트의 따옴표 누락 같은 흔한 버그를 찾아 주는 정적 분석 도구. |
- The Linux Command Line (William Shotts)
- Bash 매뉴얼: Shell Expansions
- bash(1) 매뉴얼 페이지
- hier(7)
- signal(7)
- systemd.service(5)
- sshd_config(5)
- ShellCheck
- The Linux Kernel Archives
- inode(7): 링크 수와 특수 권한 비트
- umask(2)
- credentials(7): 실제·유효 사용자 ID
- wait(2): 좀비와 고아
- sudoers(5)
- apt(8)
- DNF 명령 참조
- journalctl(1)
- crontab(5)
- ssh(1)
- ssh-keygen(1)