리눅스 기본 — 파일 시스템, 권한, 프로세스 관리
chmod 755는 뭘 의미할까? 좀비 프로세스는 왜 생기고, 어떻게 정리할까?
리눅스는 서버 환경의 사실상 표준입니다. 배포, 운영, 트러블슈팅 전부 리눅스 위에서 돌아가기 때문에, 파일 시스템 구조부터 권한, 프로세스 관리까지 기본기를 단단히 잡아야 합니다.
파일 시스템 구조 (FHS)
리눅스는 FHS(Filesystem Hierarchy Standard) 라는 표준을 따릅니다. 모든 것이 / (루트)에서 시작하는 트리 구조입니다. Windows처럼 C:, D: 드라이브가 따로 있는 게 아닙니다.
/
├── bin/ 실행 파일 (ls, cp, mv 등 기본 명령어)
├── sbin/ 시스템 관리 명령어 (fdisk, iptables)
├── etc/ 설정 파일 (nginx.conf, fstab, passwd)
├── home/ 사용자 홈 디렉토리 (/home/sim, /home/deploy)
├── root/ root 사용자의 홈 디렉토리
├── var/ 가변 데이터 (로그, 캐시, 메일)
│ ├── log/ 시스템·애플리케이션 로그
│ └── tmp/ 재부팅해도 유지되는 임시 파일
├── tmp/ 임시 파일 (재부팅 시 삭제)
├── usr/ 사용자 설치 프로그램 (패키지 매니저가 설치하는 곳)
│ ├── bin/ 일반 사용자 명령어
│ ├── lib/ 라이브러리
│ └── local/ 직접 컴파일해서 설치한 프로그램
├── proc/ 가상 파일시스템 (커널·프로세스 정보)
├── dev/ 디바이스 파일 (하드디스크, 터미널)
├── mnt/ 임시 마운트 포인트
├── opt/ 서드파티 패키지 설치 경로
└── boot/ 부트로더, 커널 이미지
핵심 디렉토리 정리
| 디렉토리 | 역할 | 핵심 포인트 |
|---|---|---|
/etc | 시스템 전체 설정 | /etc/passwd에 사용자 정보, /etc/shadow에 비밀번호 해시 |
/var/log | 로그 저장소 | syslog, auth.log, 애플리케이션 로그 확인 |
/proc | 프로세스 정보를 파일처럼 제공 | /proc/cpuinfo, /proc/meminfo — 디스크에 없고 커널이 동적 생성 |
/tmp | 누구나 쓸 수 있는 임시 공간 | sticky bit가 설정되어 있어 다른 사용자 파일 삭제 불가 |
/proc은 특히 중요합니다. 실제 파일이 아니라 커널이 실시간으로 생성하는 가상 파일시스템입니다. cat /proc/1/status하면 PID 1 프로세스(보통 systemd)의 상태를 볼 수 있습니다.
파일 권한
리눅스의 모든 파일에는 소유자(owner), 그룹(group), 기타(others) 세 범주에 대한 권한이 있습니다.
rwx 기본
$ ls -l
-rwxr-xr-- 1 sim developers 4096 Mar 15 12:00 deploy.sh
하나씩 뜯어보면 이렇습니다.
- rwx r-x r--
│ │ │ │
│ │ │ └── others: 읽기만 가능
│ │ └────── group: 읽기 + 실행
│ └────────── owner: 읽기 + 쓰기 + 실행
└───────────── 파일 유형 (- 일반, d 디렉토리, l 심볼릭 링크)
| 권한 | 파일에서의 의미 | 디렉토리에서의 의미 |
|---|---|---|
r (4) | 파일 내용 읽기 | 디렉토리 내 파일 목록 조회 |
w (2) | 파일 내용 수정 | 디렉토리 내 파일 생성/삭제 |
x (1) | 파일 실행 | 디렉토리 진입 (cd) |
디렉토리에서 x 권한이 없으면 cd로 들어갈 수 없습니다. r만 있으면 파일 목록은 보이지만 안에 들어갈 수는 없는 애매한 상태가 됩니다.
chmod — 권한 변경
숫자 방식 이 실제로 훨씬 많이 쓰입니다.
chmod 755 deploy.sh # rwxr-xr-x
chmod 644 config.yml # rw-r--r--
chmod 600 id_rsa # rw------- (SSH 키는 반드시 600)
숫자 계산법: r=4, w=2, x=1을 더합니다. 7 = 4+2+1 = rwx, 5 = 4+0+1 = r-x.
기호 방식 도 알아두면 좋습니다.
chmod u+x script.sh # owner에 실행 권한 추가
chmod g-w file.txt # group에서 쓰기 권한 제거
chmod o=r file.txt # others를 읽기만으로 설정
chmod a+r file.txt # 모든 사용자에 읽기 추가
chmod -R 755 /var/www # 하위 디렉토리까지 재귀 적용
chown, chgrp — 소유자/그룹 변경
chown deploy:deploy /var/www/app # 소유자와 그룹 한 번에 변경
chown -R deploy:deploy /var/www # 재귀 변경
chgrp developers project/ # 그룹만 변경
umask — 기본 권한 마스크
새 파일이 생성될 때 적용되는 기본 권한을 결정합니다. 파일 기본값은 666, 디렉토리 기본값은 777에서 umask를 빼는 식입니다.
$ umask
0022
# 파일: 666 - 022 = 644 (rw-r--r--)
# 디렉토리: 777 - 022 = 755 (rwxr-xr-x)
umask 0077로 설정하면 본인만 접근 가능한 파일이 기본으로 생깁니다. 보안이 중요한 서버에서 자주 씁니다.
특수 권한
일반 rwx 외에 세 가지 특수 권한이 있습니다. 자주 헷갈리는 부분입니다.
setuid (4000)
파일을 실행할 때, 실행자가 아닌 파일 소유자의 권한으로 실행됩니다.
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 15 12:00 /usr/bin/passwd
s가 보이죠? 일반 사용자가 passwd를 실행해도 /etc/shadow(root만 쓸 수 있는 파일)를 수정할 수 있는 이유가 바로 이겁니다. 실행 순간만 root 권한을 빌리는 겁니다.
chmod u+s executable # setuid 설정
chmod 4755 executable # 숫자로 설정
setgid (2000)
파일이면 그룹 권한으로 실행, 디렉토리면 하위 파일이 디렉토리의 그룹을 상속 합니다.
chmod g+s /shared # 디렉토리에 setgid 설정
chmod 2775 /shared
팀 프로젝트 디렉토리에서 유용합니다. 누가 파일을 만들든 같은 그룹 소유가 되니까요.
sticky bit (1000)
디렉토리에 설정하면, 파일 소유자만 자기 파일을 삭제 할 수 있습니다. /tmp에 기본 설정되어 있습니다.
$ ls -ld /tmp
drwxrwxrwt 12 root root 4096 Mar 15 12:00 /tmp
맨 끝 t가 sticky bit입니다. 모두가 쓸 수 있지만 남의 파일을 지울 수는 없습니다.
chmod +t /shared
chmod 1777 /shared
프로세스 관리
프로세스 확인
ps aux # 전체 프로세스 목록
ps -ef # 풀 포맷으로 보기
ps aux | grep nginx # nginx 프로세스만 필터링
top # 실시간 시스템 모니터링
htop # top의 개선 버전 (색상, 스크롤)
ps aux 출력의 각 컬럼이 뭘 의미하는지도 알아야 합니다.
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 16892 10240 ? Ss Mar14 0:03 /sbin/init
| 컬럼 | 의미 |
|---|---|
| VSZ | 가상 메모리 사용량 |
| RSS | 실제 물리 메모리 사용량 |
| STAT | 프로세스 상태 (S=Sleep, R=Running, Z=Zombie, D=Disk Sleep) |
프로세스 종료
kill PID # SIGTERM (15) — 정상 종료 요청
kill -9 PID # SIGKILL (9) — 강제 종료 (정리 기회 없음)
kill -HUP PID # SIGHUP (1) — 설정 재로드 (nginx에서 자주 사용)
killall nginx # 이름으로 종료
pkill -f "java.*myapp" # 패턴 매칭으로 종료
kill -9는 최후의 수단입니다. 프로세스에게 리소스 정리 기회를 주지 않기 때문에, 먼저 kill(SIGTERM)을 보내고 안 죽을 때만 -9를 씁시다.
프로세스 우선순위
nice -n 10 ./heavy-task.sh # 낮은 우선순위로 실행 (nice 값 높을수록 양보)
renice -5 -p 1234 # 실행 중인 프로세스 우선순위 변경
# nice 범위: -20 (가장 높은 우선순위) ~ 19 (가장 낮은 우선순위)
# 음수 nice 값은 root만 설정 가능
백그라운드 실행과 작업 제어
./server.sh & # 백그라운드에서 실행
jobs # 현재 세션의 백그라운드 작업 목록
fg %1 # 1번 작업을 포그라운드로
bg %1 # 정지된 작업을 백그라운드로 재개
Ctrl+Z # 포그라운드 작업 일시 정지
nohup ./server.sh & # 세션 종료 후에도 계속 실행
nohup은 HUP 시그널을 무시하게 해줍니다. SSH 접속 끊어도 프로세스가 살아있어야 할 때 씁니다. 요즘은 screen이나 tmux를 쓰는 편이 더 낫긴 합니다.
데몬과 서비스
systemd
현대 리눅스 배포판 대부분이 systemd 를 init 시스템으로 사용합니다. PID 1이 systemd입니다.
systemctl start nginx # 서비스 시작
systemctl stop nginx # 서비스 중지
systemctl restart nginx # 재시작
systemctl reload nginx # 설정만 다시 읽기 (무중단)
systemctl status nginx # 상태 확인
systemctl enable nginx # 부팅 시 자동 시작
systemctl disable nginx # 자동 시작 해제
systemctl list-units --type=service # 전체 서비스 목록
journalctl -u nginx -f # 서비스 로그 실시간 보기
서비스 등록
직접 만든 Spring Boot 앱을 서비스로 등록하는 경우를 봅시다.
# /etc/systemd/system/myapp.service
[Unit]
Description=My Spring Boot Application
After=network.target
[Service]
Type=simple
User=deploy
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
systemctl daemon-reload # 유닛 파일 변경 후 반영
systemctl enable myapp # 부팅 시 자동 시작 등록
systemctl start myapp # 시작
실제 배포 환경에서 nohup java -jar app.jar & 이렇게 띄우는 건 관리가 안 됩니다. systemd로 등록하면 자동 재시작, 로그 관리, 부팅 시 자동 기동까지 해결됩니다.
실제 필수 명령어
grep — 텍스트 검색
grep "ERROR" app.log # 파일에서 패턴 검색
grep -r "TODO" ./src # 디렉토리 재귀 검색
grep -i "warning" app.log # 대소문자 무시
grep -n "Exception" app.log # 줄 번호 표시
grep -c "ERROR" app.log # 매칭 횟수만 출력
grep -v "DEBUG" app.log # 해당 패턴 제외
grep -E "ERROR|WARN" app.log # 정규식 (여러 패턴 OR)
grep -A 3 "Exception" app.log # 매칭 후 3줄도 표시
find — 파일 검색
find /var/log -name "*.log" # 이름으로 검색
find / -type f -size +100M # 100MB 이상 파일
find . -name "*.tmp" -mtime +7 -delete # 7일 지난 tmp 파일 삭제
find . -type f -name "*.java" | xargs wc -l # 자바 파일 줄 수 세기
find /home -user sim -perm 777 # 특정 사용자의 777 파일 찾기
awk — 텍스트 처리
awk '{print $1, $4}' access.log # 1번, 4번 컬럼 출력
awk -F: '{print $1}' /etc/passwd # 구분자 지정 (: 기준)
awk '$9 == 500 {print $7}' access.log # 조건부 출력 (500 에러 URL)
awk '{sum+=$1} END {print sum}' data.txt # 합계 계산
sed — 스트림 편집
sed 's/old/new/g' file.txt # 치환 (원본 유지, 표준출력)
sed -i 's/old/new/g' file.txt # 원본 직접 수정
sed -n '10,20p' file.txt # 10~20번째 줄만 출력
sed '/^#/d' config.conf # 주석 줄 삭제
파이프와 리다이렉션
# 파이프: 앞 명령어의 stdout을 뒤 명령어의 stdin으로
ps aux | grep java | grep -v grep | awk '{print $2}'
# 리다이렉션
echo "hello" > file.txt # 덮어쓰기
echo "world" >> file.txt # 이어쓰기
command 2> error.log # stderr만 파일로
command > out.log 2>&1 # stdout과 stderr 모두 파일로
command > /dev/null 2>&1 # 출력 완전 버리기
xargs — 인자 전달
find . -name "*.log" | xargs rm # 검색 결과를 rm의 인자로
find . -name "*.java" | xargs grep "TODO" # 검색 결과에서 다시 검색
echo "1 2 3" | xargs -n 1 echo # 하나씩 처리
docker ps -q | xargs docker stop # 실행 중인 컨테이너 전부 중지
xargs는 파이프와 다릅니다. 파이프는 stdin으로 넘기지만, xargs는 명령어의 인자(argument) 로 넘깁니다. rm이나 docker stop 같이 stdin을 안 받는 명령어에 파이프 결과를 넘길 때 필수입니다.
패키지 관리
apt (Debian/Ubuntu)
apt update # 패키지 목록 업데이트
apt upgrade # 설치된 패키지 업그레이드
apt install nginx # 패키지 설치
apt remove nginx # 삭제 (설정 파일 유지)
apt purge nginx # 삭제 (설정 파일까지)
apt search keyword # 패키지 검색
yum/dnf (CentOS/RHEL/Fedora)
yum install nginx # 패키지 설치
yum remove nginx # 삭제
yum update # 전체 업데이트
dnf install nginx # dnf는 yum의 후계자 (Fedora, RHEL 8+)
소스 컴파일 설치
패키지 매니저에 원하는 버전이 없을 때 직접 컴파일합니다.
wget https://example.com/app-2.0.tar.gz
tar xzf app-2.0.tar.gz
cd app-2.0
./configure --prefix=/usr/local
make
make install
./configure로 빌드 환경을 검사하고, make로 컴파일, make install로 설치합니다. /usr/local에 설치하는 게 관례인데, 패키지 매니저가 관리하는 /usr과 충돌을 피하기 위해서입니다.
쉘 스크립트 기초
변수와 기본 문법
#!/bin/bash
# 변수 (= 양옆에 공백 없이!)
NAME="deploy"
PORT=8080
echo "User: $NAME, Port: $PORT"
# 명령어 치환
CURRENT_DATE=$(date +%Y-%m-%d)
FILE_COUNT=$(ls | wc -l)
조건문
if [ -f "/etc/nginx/nginx.conf" ]; then
echo "nginx 설정 파일 존재"
elif [ -d "/etc/nginx" ]; then
echo "디렉토리는 있지만 설정 파일 없음"
else
echo "nginx 미설치"
fi
# 파일 테스트
# -f: 파일 존재 -d: 디렉토리 존재 -r: 읽기 가능
# -w: 쓰기 가능 -x: 실행 가능 -s: 파일 크기 > 0
반복문
# for
for server in web1 web2 web3; do
echo "Deploying to $server..."
ssh deploy@$server "cd /opt/app && ./restart.sh"
done
# while
while read line; do
echo "Processing: $line"
done < servers.txt
# 무한 루프 + 조건 탈출
while true; do
health=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
if [ "$health" = "200" ]; then
echo "Server is up!"
break
fi
sleep 2
done
함수
deploy() {
local server=$1
local version=$2
echo "[$server] Deploying version $version..."
scp app-${version}.jar deploy@${server}:/opt/app/
ssh deploy@${server} "systemctl restart myapp"
}
deploy "web1" "2.1.0"
deploy "web2" "2.1.0"
crontab — 스케줄링
crontab -e # 편집
crontab -l # 현재 등록된 작업 보기
# 분 시 일 월 요일 명령어
0 3 * * * /opt/scripts/backup.sh # 매일 새벽 3시
*/5 * * * * /opt/scripts/health-check.sh # 5분마다
0 0 1 * * /opt/scripts/monthly-report.sh # 매월 1일 자정
0 9 * * 1-5 /opt/scripts/daily-alert.sh # 평일 오전 9시
cron 표현식에서 *는 "매번"이고, */5는 "5마다"입니다. 요일은 0(일)~6(토)입니다.
네트워크 명령어
curl / wget
curl http://localhost:8080/api/health # GET 요청
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"test"}' http://localhost:8080/api/users # POST 요청
curl -o file.zip https://example.com/file.zip # 파일 다운로드
curl -I https://google.com # 헤더만 확인
wget https://example.com/large-file.tar.gz # 다운로드 (이어받기 지원)
wget -c https://example.com/large-file.tar.gz # 끊긴 다운로드 이어받기
네트워크 상태 확인
# netstat (구버전)
netstat -tlnp # TCP 리스닝 포트 + PID 보기
# ss (netstat 대체, 더 빠름)
ss -tlnp # TCP 리스닝 포트 + PID
ss -s # 소켓 통계 요약
# 포트 확인
lsof -i :8080 # 8080 포트를 쓰고 있는 프로세스
netstat은 deprecated 상태이고 ss가 대체입니다. 하지만 netstat도 아직 많이 쓰이니 알아두는 게 좋습니다.
iptables 기초
iptables -L # 현재 규칙 보기
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # 80 포트 허용
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT # 특정 대역만 SSH 허용
iptables -A INPUT -j DROP # 나머지 전부 차단 (순서 중요!)
iptables 규칙은 위에서 아래로 순서대로 적용됩니다. DROP을 맨 위에 놓으면 뒤에 뭘 넣어도 소용없습니다. 최근에는 firewalld나 ufw같은 래퍼를 더 많이 씁니다.
주의할 점
"하드 링크와 심볼릭 링크의 차이는?"
ln source.txt hard_link.txt # 하드 링크
ln -s source.txt sym_link.txt # 심볼릭 링크
| 하드 링크 | 심볼릭 링크 | |
|---|---|---|
| 원리 | 같은 inode를 가리킴 | 경로를 가리키는 별도 파일 |
| 원본 삭제 시 | 접근 가능 (inode가 살아있음) | 끊어짐 (dangling link) |
| 파일시스템 넘기 | 불가능 | 가능 |
| 디렉토리 | 불가능 (순환 방지) | 가능 |
핵심은 inode 입니다. 하드 링크는 원본과 동일한 inode 번호를 공유합니다. 그래서 원본을 지워도 inode를 참조하는 링크가 남아있으면 데이터는 살아 있습니다. 심볼릭 링크는 Windows의 바로가기와 비슷하게 "경로 문자열"을 저장하는 별도 파일입니다.
$ ls -li
123456 -rw-r--r-- 2 sim sim 100 source.txt # link count가 2
123456 -rw-r--r-- 2 sim sim 100 hard_link.txt # 같은 inode!
789012 lrwxrwxrwx 1 sim sim 10 sym_link.txt -> source.txt
"/proc 파일시스템이 뭔가요?"
/proc은 커널이 실시간으로 생성하는 가상 파일시스템 입니다. 디스크에 존재하지 않습니다.
cat /proc/cpuinfo # CPU 정보
cat /proc/meminfo # 메모리 정보
cat /proc/1/status # PID 1 프로세스 상태
cat /proc/1/cmdline # PID 1 실행 명령어
ls /proc/1/fd # PID 1이 열고 있는 파일 디스크립터 목록
cat /proc/loadavg # 시스템 부하 평균
cat /proc/uptime # 가동 시간
모니터링 도구들(top, htop, Prometheus node_exporter 등)이 내부적으로 /proc를 읽어서 정보를 수집합니다. Docker 컨테이너 내부에서 /proc을 읽으면 호스트 정보가 나오는 경우가 있는데, 이건 cgroup 격리와 관련됩니다.
"좀비 프로세스와 고아 프로세스?"
좀비 프로세스: 자식 프로세스가 종료됐는데 부모가 wait()를 호출하지 않아서 프로세스 테이블에 남아있는 상태입니다.
$ ps aux | grep Z
user 1234 0.0 0.0 0 0 ? Z 12:00 0:00 [defunct]
상태가 Z(Zombie)로 표시됩니다. 메모리는 이미 해제됐지만 PID와 종료 코드가 프로세스 테이블에 남아 있습니다. PID가 유한한 자원이라서 좀비가 쌓이면 새 프로세스를 못 만들 수 있습니다.
고아 프로세스: 부모 프로세스가 자식보다 먼저 종료된 경우입니다. 이때 자식은 PID 1(init/systemd)에 입양됩니다. init이 주기적으로 wait()를 호출해서 정리해주기 때문에, 고아 프로세스 자체는 큰 문제가 되지 않습니다.
좀비: 자식이 죽었는데 부모가 수습 안 함 → 문제 될 수 있음
고아: 부모가 먼저 죽음 → init이 대신 수습 → 보통 괜찮음
좀비 프로세스를 직접 kill할 수는 없습니다. 이미 죽은 프로세스니까요. 부모 프로세스에게 SIGCHLD를 보내거나, 부모를 종료시켜서 init에게 넘기는 방법으로 정리합니다.
파생되는 개념들
-
시스템 콜 — 사용자 공간에서 커널 기능을 호출하는 인터페이스.
open(),read(),write(),fork(),exec()등. 모든 파일 I/O와 프로세스 관리는 결국 시스템 콜을 통합니다.strace명령어로 프로세스가 호출하는 시스템 콜을 추적할 수 있습니다. -
파일 시스템과 inode — 리눅스 파일시스템(ext4, xfs 등)은 inode 기반입니다. inode에는 파일 크기, 권한, 타임스탬프, 데이터 블록 포인터가 들어있고, 파일 이름은 디렉토리 엔트리에 저장됩니다.
df -i로 inode 사용량을 확인할 수 있는데, 작은 파일이 엄청 많으면 디스크 공간은 남았는데 inode가 고갈되는 경우도 있습니다. -
Docker의 namespace와 cgroup — Docker 컨테이너는 리눅스 커널의 namespace(프로세스, 네트워크, 파일시스템 격리)와 cgroup(CPU, 메모리 자원 제한)을 기반으로 동작합니다. 가상머신처럼 OS를 통째로 띄우는 게 아니라 커널 기능으로 격리하는 겁니다.
/proc에서 컨테이너 내부 cgroup 정보를 확인할 수 있습니다.