| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- springsecurity
- string
- 디스크i/o
- SpringLegacy
- 전전긍긍
- MVC
- index
- SEQUENCE
- MVC요청플로우
- 문제선택근거
- 클린아키텍처
- 이벤트핸들러this
- Transaction
- 이벤트핸들러등록
- 단위테스트
- DDD
- JDBC
- model
- 공통규약
- 사면초가
- 공통기술
- TDD
- 커뮤니티서버
- Ajax
- AOP
- 설정세팅
- 냄새라도
- OOP
- 로그백
- JPA
- Today
- Total
개발이군고구마
[리눅스 심화 ①] 프로세스는 어떻게 태어나고, 격리되고, 컨테이너가 되는가 본문
0. 들어가며: 왜 프로세스를 더 깊이 봐야 하는가
앞 글에서 프로세스를 "부모가 자식을 낳는 나무 구조"로 이해함. 이 글은 그 나무의 뿌리(생성 원리)와 울타리(격리·제한)를 파고듦.
인프라 엔지니어에게 프로세스 심화가 중요한 이유는 명확함. Docker, Kubernetes 같은 컨테이너 기술이 새로운 마법이 아니라, 사실은 리눅스 프로세스의 두 가지 기능(namespace + cgroup)을 조합한 것이기 때문임. 이 원리를 알면 "컨테이너가 왜 격리되는가", "메모리 제한이 어떻게 걸리는가", "OOM Killer는 왜 뜨는가"가 전부 하나로 설명됨.

1. 프로세스는 어떻게 태어나는가: fork()와 exec()
1.1 리눅스에는 "그냥 실행"이 없다
가장 먼저 깨야 할 오해임. 리눅스에서 새 프로그램을 실행할 때, 무(無)에서 프로세스가 뿅 하고 생기지 않음. 반드시 기존 프로세스가 자신을 복제한 뒤, 그 복제본을 다른 프로그램으로 덮어쓰는 2단계를 거침. 이 두 단계가 fork()와 exec()임.
1.2 fork(): 자기 자신을 복제함
fork()는 현재 프로세스를 통째로 복제해서 자식 프로세스를 만드는 시스템 콜임.
- 부모의 메모리, 열린 파일(FD 테이블 포함!), 환경 변수를 그대로 물려받음.
- 복제 직후 부모와 자식은 거의 동일하지만, PID는 서로 다름.
fork()의 반환값으로 부모와 자식이 자신을 구분함: 부모는 자식의 PID를, 자식은 0을 받음.
💡 앞 글에서 "자식은 부모의 FD 0/1/2를 물려받는다"고 했던 것이 바로 이 fork() 때문임. 그래서 셸에서 실행한 명령어가 별도 설정 없이도 화면(터미널)에 출력됨.
성능 팁 — Copy-on-Write(COW): 복제할 때 메모리 전체를 실제로 복사하면 무거움. 그래서 리눅스는 처음엔 부모·자식이 같은 물리 메모리를 공유하다가, 둘 중 하나가 값을 쓰려는 순간에만 그 부분을 복사함. 이 덕분에 fork()가 가벼움.
1.3 exec(): 자신을 다른 프로그램으로 덮어씀
exec()는 현재 프로세스의 메모리 내용을 새 프로그램으로 완전히 교체하는 시스템 콜임. 새 프로세스를 만드는 게 아니라, PID는 그대로 유지한 채 알맹이만 갈아치우는 것이 핵심임.
1.4 fork + exec: 셸이 명령어를 실행하는 방식
우리가 셸에서 ls를 치면 내부적으로 이런 일이 일어남.

이 fork → exec → wait 3박자가 리눅스에서 프로그램이 실행되는 근본 원리임. systemd가 서비스를 띄우는 것도, Docker가 컨테이너 프로세스를 띄우는 것도 결국 이 패턴의 변형임.
2. 프로세스의 생애주기: 상태와 좀비의 정체
프로세스는 태어나서 죽을 때까지 여러 상태(state)를 오감. ps aux의 STAT 열이나 top에서 이 상태를 볼 수 있음.
2.1 프로세스의 5가지 핵심 상태
| 상태 | 기호 | 의미 | 특징 |
| Running | R |
실행 중 또는 실행 대기(runnable) | CPU를 쓰고 있거나 곧 쓸 예정 |
| Sleeping | S |
대기(interruptible) | 이벤트(입력, 응답)를 기다리는 정상 상태 |
| Uninterruptible | D |
대기(uninterruptible) | 주로 디스크 I/O 중. kill -9로도 안 죽음 |
| Stopped | T |
정지 | Ctrl+Z(SIGSTOP)로 멈춘 상태 |
| Zombie | Z |
좀비 | 죽었지만 아직 수거 안 된 상태 (아래 상세) |

⚠️ D 상태(Uninterruptible Sleep) 는 실무에서 중요함. 이 상태의 프로세스는 커널 레벨에서 I/O를 기다리는 중이라 kill -9(SIGKILL)로도 죽지 않음. D 상태 프로세스가 쌓이면 대개 스토리지(디스크·NFS) 장애의 신호임. 프로세스를 억지로 죽이려 하기보다 I/O 병목의 원인을 찾아야 함.
2.2 좀비 프로세스(Zombie): 오해와 진실
좀비는 이름과 달리 위험하게 날뛰는 프로세스가 아님. 정반대로, 이미 죽어서 아무 일도 안 하는 프로세스임.
동작 원리는 이러함.
- 자식 프로세스가 종료됨 → 실행은 끝났지만, 자신의 종료 코드(exit code)를 부모가 읽어갈 때까지 프로세스 테이블에 흔적을 남김.
- 부모가
wait()를 호출해 그 종료 코드를 수거하면 → 흔적이 완전히 사라짐(정상). - 그런데 부모가
wait()를 호출하지 않으면 → 자식은 종료 코드를 든 채Z상태로 계속 남음. 이것이 좀비임.
좀비 자체는 메모리·CPU를 쓰지 않음. 다만 프로세스 테이블의 한 칸(PID 슬롯)을 계속 점유함. 좀비가 대량으로 쌓이면 PID가 고갈되어 새 프로세스를 못 만드는 상황이 올 수 있음.
💡 핵심: 좀비가 많이 보이면 그건 부모 프로세스(애플리케이션)가 자식을 제대로 수거하지 않는 버그의 증상임. 좀비를 직접 죽일 순 없음(이미 죽었으므로). 대신 부모 프로세스를 고치거나 재시작해야 함.
2.3 고아 프로세스(Orphan)와 systemd의 입양
반대 상황도 있음. 자식보다 부모가 먼저 죽는 경우임. 이때 부모를 잃은 자식을 고아 프로세스라 함.
리눅스는 고아를 방치하지 않음. 부모 잃은 프로세스는 자동으로 PID 1(systemd)에게 입양(re-parenting)됨. 그리고 systemd가 이 입양한 자식들이 죽을 때 wait()로 종료 코드를 수거해줌. 이 "입양 후 수거" 역할을 reaping이라 부름.
💡 앞 글에서 systemd를 "PID 1, 모든 프로세스의 최상위 조상"이라 했던 것이 여기서 실질적 의미를 가짐. systemd는 단순 서비스 관리자가 아니라, 고아를 입양해 좀비가 시스템에 쌓이지 않게 하는 청소부 역할도 함. (Docker 컨테이너에서 init 프로세스를 넣어주는 이유도 이 reaping 때문임.)
3. 프로세스의 메모리 구조와 우선순위
3.1 프로세스 메모리 레이아웃 (간단히)
프로세스 하나가 차지하는 메모리는 용도별로 구획이 나뉘어 있음. 트러블슈팅 시 "메모리 누수가 어디서 나는가"를 판단하는 기초가 됨.
| 영역 | 저장 대상 | 특징 |
| Text (Code) | 실행할 기계어 코드 | 읽기 전용 |
| Data / BSS | 전역 변수, static 변수 | 프로그램 시작 시 크기 확정 |
| Heap | malloc 등 동적 할당 메모리 |
위로 자람. 메모리 누수의 주범 |
| Stack | 함수 호출·지역 변수 | 아래로 자람. 너무 깊으면 stack overflow |
3.2 우선순위: nice 값
CPU가 부족할 때 어떤 프로세스를 먼저 실행할지는 우선순위로 결정됨. 사용자가 조정할 수 있는 값이 nice 값(-20 ~ 19)임.
- 값이 낮을수록 우선순위 높음(-20이 최고 우선순위).
- "nice(친절하다)"는 이름은 "양보한다"는 의미임. nice 값이 높을수록 다른 프로세스에게 CPU를 양보함.
nice -n 10 ./batch.sh # 낮은 우선순위로 배치 작업 실행 (양보)
renice -n -5 -p 1234 # 실행 중인 프로세스(1234)의 우선순위를 높임
💡 대용량 배치 작업을 nice로 낮은 우선순위로 돌리면, 실시간 서비스 응답에 주는 영향을 줄일 수 있음. 트래픽 많은 커머스 환경에서 유용한 기법임.
4. namespace: 프로세스가 "볼 수 있는 세계"를 격리함
여기서부터가 컨테이너의 뿌리임. 핵심 질문은 이것임.
"한 서버에서 도는 프로세스인데, 어떻게 서로를 못 보게 만들 수 있는가?"
답이 namespace(네임스페이스)임. namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 격리하는 커널 기능임. 한마디로 "시야 차단막"임.
4.1 namespace의 종류
리눅스는 자원 종류별로 여러 namespace를 제공함. 프로세스마다 각 자원에 대해 "어느 namespace에 속하는지"가 정해짐.
| namespace | 격리하는 대상 | 격리 효과 |
| PID | 프로세스 ID | 컨테이너 안에서는 자기 프로세스가 PID 1로 보임 |
| NET | 네트워크 | 독립된 IP, 포트, 네트워크 인터페이스를 가짐 |
| MNT | 마운트(파일시스템) | 자기만의 파일시스템 루트(/)를 봄 |
| UTS | 호스트명 | 독립된 hostname을 가질 수 있음 |
| IPC | 프로세스 간 통신 | 공유 메모리·세마포어를 격리 |
| USER | 사용자/그룹 ID | 컨테이너 안의 root ≠ 호스트의 root |

4.2 이게 왜 강력한가
PID namespace 덕분에 컨테이너 안의 프로세스는 자기가 PID 1(그 세계의 systemd 자리)이라고 믿음. 호스트에서 보면 그냥 PID 30521 같은 평범한 프로세스지만, 격리된 시야 안에서는 독립된 하나의 시스템처럼 동작함.
NET namespace 덕분에 컨테이너마다 80 포트를 각자 독립적으로 쓸 수 있음. 호스트에서는 하나뿐인 80 포트를, 컨테이너 A도 80, 컨테이너 B도 80으로 쓸 수 있는 이유가 이것임.
4.3 실무에서 namespace 들여다보기
lsns # 현재 시스템의 namespace 목록
ls -l /proc/<PID>/ns/ # 특정 프로세스가 속한 namespace들
nsenter -t <PID> -n ip addr # 특정 프로세스의 network namespace 안으로 들어가 명령 실행
5. cgroup: 프로세스가 "쓸 수 있는 자원의 양"을 제한함
namespace가 시야(무엇을 볼 수 있는가)를 격리한다면, cgroup은 자원(얼마나 쓸 수 있는가)을 제한함. 이 둘의 역할 구분이 컨테이너 이해의 핵심임.
5.1 cgroup이란
cgroup(Control Group)은 프로세스 그룹이 사용하는 자원(CPU, 메모리, 디스크 I/O 등)을 측정하고 제한하는 커널 기능임. "이 프로세스 그룹은 메모리 512MB, CPU 0.5코어까지만 써라"를 강제할 수 있음.
| 제한 대상 | 예시 |
| CPU | "CPU 사용량을 최대 50%로 제한" |
| Memory | "메모리를 512MB까지만 허용" |
| I/O | "디스크 읽기/쓰기 대역폭 제한" |
| PIDs | "이 그룹은 최대 100개 프로세스까지만" |

5.2 OOM Killer와의 연결
cgroup의 메모리 제한은 OOM(Out Of Memory) Killer와 직결됨. 프로세스가 자신에게 할당된 메모리 한도를 넘어서려 하면, 커널은 OOM Killer를 발동해 그 프로세스를 강제 종료함.
💡 컨테이너가 갑자기 죽으면서 로그에 OOMKilled가 찍히는 상황이 바로 이것임. 애플리케이션이 cgroup으로 설정된 메모리 한도를 초과했다는 뜻임. JVM 힙 설정(-Xmx)을 컨테이너 메모리 제한보다 작게 잡아야 하는 이유도 여기 있음. JVM이 힙 외에 메타스페이스·스레드 스택도 쓰기 때문에, -Xmx를 cgroup 한도에 딱 맞추면 초과되어 OOMKilled 당함.
5.3 실무에서 cgroup·OOM 확인하기
systemd-cgtop # cgroup별 CPU·메모리 사용량 실시간 확인
cat /sys/fs/cgroup/memory.max # (cgroup v2) 메모리 한도 확인
dmesg | grep -i "killed process" # OOM Killer가 죽인 프로세스 기록 확인
journalctl -k | grep -i oom # 커널 로그에서 OOM 이벤트 조회
6. 결론: 컨테이너는 마법이 아니라 프로세스다
이제 앞의 모든 조각이 하나로 맞춰짐. Docker 컨테이너는 새로운 종류의 무언가가 아니라, 그냥 리눅스 프로세스임. 다만 아래 세 가지 기능으로 포장된 프로세스임.
graph LR
A["일반 프로세스"] --> B["+ namespace<br/>(시야 격리)"]
B --> C["+ cgroup<br/>(자원 제한)"]
C --> D["+ 이미지 파일시스템<br/>(격리된 루트)"]
D --> E["= 컨테이너"]
| 컨테이너의 특징 | 실제로는 무엇인가 |
| 격리된 것처럼 보임 | namespace로 시야를 차단한 프로세스 |
| 자원이 제한됨 | cgroup으로 CPU·메모리를 묶은 프로세스 |
| 독립된 파일시스템을 가짐 | 이미지 레이어(union filesystem)로 만든 격리된 루트(/) |
| 컨테이너 안 root | USER namespace로 매핑된, 호스트에서는 일반 사용자 |
한 줄 정리: namespace(무엇을 볼 수 있는가) + cgroup(얼마나 쓸 수 있는가) + 파일시스템(어디에 쓸 수 있는가) = 컨테이너.
VM(가상머신)이 하드웨어 위에 별도 OS를 통째로 올리는 무거운 방식이라면, 컨테이너는 호스트 커널을 공유하면서 프로세스만 격리하는 가벼운 방식임. 그래서 빠르고 가벼움. 이 차이의 근원이 바로 "컨테이너는 결국 프로세스"라는 사실임.
7. 실무 트러블슈팅 체크리스트
이 글의 개념들이 실제 운영에서 어떻게 쓰이는지 정리함.
| 증상 | 확인 명령어 | 원인 |
좀비(Z)가 쌓임 |
ps aux | grep defunct |
부모 프로세스가 자식을 수거 안 함 (앱 버그) |
프로세스가 kill -9로도 안 죽음 |
ps -eo pid,stat,cmd | grep ' D' |
D 상태 — 스토리지 I/O 장애 의심 |
| 컨테이너가 갑자기 종료됨 | dmesg | grep -i oom |
cgroup 메모리 한도 초과 (OOMKilled) |
| 특정 서비스가 CPU 독점 | systemd-cgtop |
cgroup CPU 제한 미설정 |
| 컨테이너 네트워크 문제 | nsenter -t <PID> -n ip addr |
NET namespace 내부 상태 확인 필요 |
'SERVER > CS' 카테고리의 다른 글
| 리눅스 운영체제 (0) | 2026.07.12 |
|---|---|
| EC2 리눅스 OS 서버 데이터 저장 구조 (1) | 2026.07.12 |
| 로그 (0) | 2026.06.07 |
| PuTTY로 SSH 접속하면 서버에서 무슨 일이 일어날까 (0) | 2026.03.27 |
| OS와 CPU의 작동 원리, 그리고 CPU 100% 문제 해결 (0) | 2026.03.27 |