실행 중인 프로그램과 그에 딸린 자원의 묶음.
주소 공간의 구조
높은 주소 ┌───────────────┐
│ 스택 │ 지역 변수 · 함수 호출 프레임 (아래로 자란다)
├───────────────┤
│ ↓ │
│ │
│ ↑ │
├───────────────┤
│ 힙 │ 동적 할당 (위로 자란다)
├───────────────┤
│ BSS │ 초기화 안 된 전역 변수 (0으로 채워진다)
├───────────────┤
│ 데이터 │ 초기화된 전역 변수
├───────────────┤
낮은 주소 │ 코드(텍스트) │ 기계어 (읽기 전용)
└───────────────┘
이건 가상 주소다. 프로세스마다 자기만의 이 그림을 갖고, 실제 물리 메모리 어디에 있는지는 OS가 관리한다.
PCB — OS가 프로세스에 대해 아는 것
PID · 상태 · 프로그램 카운터 · 레지스터 값 메모리 정보(페이지 테이블 위치) · 열린 파일 목록 · 우선순위 · 부모 PID
컨텍스트 스위치는 결국 '이 PCB 를 저장하고 다른 PCB 를 복원' 하는 일이다
어떻게 태어나나 — fork + exec
pid_t pid = fork(); // 자기 자신을 복제한다
if (pid == 0) {
execvp("ls", args); // 자식의 내용을 다른 프로그램으로 갈아 끼운다
} else {
waitpid(pid, &status, 0);// 부모는 자식을 기다린다
}
-
fork — 주소 공간을 통째로 복제한다 — 실제로는 copy-on-write 로 미룬다
- 반환값이 둘이다: 부모에게는 자식 PID, 자식에게는 0
-
exec — 현재 프로세스의 내용을 새 프로그램으로 교체한다. PID 는 유지된다
-
왜 나눴나: fork 와 exec 사이에서 자식만의 준비를 할 수 있다
- (파일 디스크립터 리다이렉션 · 권한 낮추기 — 셸의 파이프가 이걸로 만들어진다)
좀비와 고아
-
좀비 — 자식이 끝났는데 부모가 wait 을 안 했다
- 종료 코드를 담은 PCB 항목이 남는다 → PID 고갈로 이어질 수 있다
-
고아 — 부모가 먼저 죽었다 → init(PID 1)이 입양해 대신 거둬 준다
-
컨테이너에서 앱을 PID 1 로 띄우면 좀비를 거둘 주체가 없다
- tini 같은 최소 init 을 넣거나 --init 을 쓴다
상태
생성 → 준비(ready) ⇄ 실행(running) → 종료
↑ ↓
└── 대기(blocked) ←┘ I/O 를 기다린다
면접 함정
- ❌ "fork는 메모리를 전부 복사한다" → copy-on-write로 미룬다. 쓸 때만 실제 복사가 일어난다.
- ❌ "좀비 프로세스가 메모리를 먹는다" → 메모리는 이미 해제됐다. PID와 PCB 항목만 남는다.
관측
ps -eo pid,ppid,stat,rss,vsz,comm --sort=-rss | head
# STAT R 실행 · S 대기 · D 중단불가대기(I/O) · Z 좀비 · T 정지
# RSS 실제 물리 메모리 · VSZ 가상 메모리(예약 포함, 훨씬 크다)
pstree -p <pid> # 부모-자식 관계
cat /proc/<pid>/status # 상태 · 스레드 수 · 메모리 상세
cat /proc/<pid>/cmdline | tr '\0' ' '
D 상태는 kill -9로도 안 죽는다 — 커널에서 I/O를 기다리는 중이라 시그널을 받을 수 없다. NFS나 디스크 장애에서 흔히 본다.
시그널
-
SIGTERM(15) — "정리하고 종료해" — 잡아서 graceful shutdown 가능 (기본 kill)
-
SIGKILL(9) — "즉시 종료" — 잡을 수 없다. 정리 코드가 안 돈다
-
SIGINT(2) — Ctrl+C
-
SIGHUP(1) — 터미널 종료 — 데몬은 관례적으로 설정 재읽기로 쓴다
-
컨테이너 종료는 SIGTERM → (grace period) → SIGKILL 순이다
- 앱이 SIGTERM 을 처리하지 않으면 매번 강제 종료된다 (요청이 잘린다)