
서비스 · 프로세스 관리 03 / 50 · Part 1. 프로세스 기초
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
02편에서 모든 프로세스는 fork로 태어난다는 것을 확인했다. 그렇다면 모든 프로세스에는 부모가 있고, 부모의 부모를 계속 따라가면 하나의 뿌리 에 도달한다. 그 뿌리가 PID 1, 현대 Linux에서는 systemd 다.
이번 글에서는 PID와 PPID의 의미를 정리하고, SSH로 접속한 셸에서 PID 1까지 조상 체인을 직접 따라 올라가 본다. 관제에서 "이 프로세스는 누가 실행했나?"라는 질문의 답이 바로 이 트리에 있다.
| 용어 | 의미 | 특징 |
|---|---|---|
| PID (Process ID) | 프로세스 고유 번호 | 커널이 순서대로 할당, 종료 후 재사용 된다 |
| PPID (Parent PID) | 나를 fork한 부모의 PID | 부모가 먼저 죽으면 새 부모(PID 1 등)로 바뀐다 |
| PID 1 | 커널이 부팅 후 처음 실행한 사용자 공간 프로세스 | systemd. 고아 프로세스 입양, 좀비 회수 담당 |
| PID 0 | 커널 내부의 idle 태스크 | ps의 PID 1 행에서 PPID가 0으로 보인다 |
| PID 2 | kthreadd | 커널 스레드([kworker/...] 등)의 부모 |
| pid_max | 할당 가능한 최대 PID + 1 | /proc/sys/kernel/pid_max |
| 명령어 | 방향 | 용도 |
|---|---|---|
pstree -p | 위 → 아래 | 전체 트리를 PID와 함께 |
pstree -ps PID | 아래 → 위 | 특정 프로세스의 조상 체인만 |
ps -ef --forest | 위 → 아래 | 사용자·시각과 함께 들여쓰기 트리 |
pgrep -P PID -a | 아래 | 특정 부모의 직계 자식 목록 |
/proc/PID/status의 PPid: | 한 단계 위 | 스크립트로 조상을 따라갈 때 |

SSH 접속 한 번에 프로세스가 여러 단계로 만들어지는 이유는 권한 분리(privilege separation) 때문이다.
systemd (PID 1)
└─ sshd [listener] ← 22번 포트에서 접속 대기 (root)
└─ sshd-session: analyst [priv] ← 새 접속마다 fork, 인증 처리 (root)
└─ sshd-session: analyst@pts/0 ← 인증 후 analyst 권한으로 낮춰 동작
└─ bash ← 로그인 셸
└─ 내가 입력한 명령들
네트워크 데이터를 다루는 프로세스를 일반 사용자 권한으로 분리해 두면, 그 단계에서 취약점이 터져도 root 권한을 바로 얻지 못한다.
Rocky Linux 9.8의 OpenSSH는 세션 처리를
sshd-session이라는 별도 실행 파일 로 분리해 두었다. Ubuntu 24.04에서는 같은 역할이sshd: analyst [priv],sshd: analyst@pts/0처럼 sshd 이름으로 보인다. 버전 차이일 뿐 구조는 같다.
# 1) PID 기본 정보
cat /proc/sys/kernel/pid_max
ps -o pid,ppid,user,cmd -p 1
echo "내 셸 PID=$$ PPID=$PPID" # $$ = 현재 셸 PID, $PPID = 부모 PID
# 2) 전체 트리
pstree -p | head -12
# 3) 내 셸에서 PID 1까지 거슬러 올라가기 (스크립트 방식)
p=$$; while [ "$p" -ne 0 ]; do
printf '%-6s %s\n' "$p" "$(tr '\0' ' ' < /proc/$p/cmdline | cut -c1-60)"
p=$(awk '/^PPid/{print $2}' /proc/$p/status)
done
# 4) (Ubuntu) forest 출력, 조상 체인, 직계 자식
sleep 200 & bash -c 'sleep 200' &
ps -ef --forest | tail -8
pstree -ps $(pgrep -n sleep)
pgrep -P $$ -a


텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ cat /proc/sys/kernel/pid_max
4194304
[analyst@rocky9-lab ~]$ ps -o pid,ppid,user,cmd -p 1
PID PPID USER CMD
1 0 root /usr/sbin/init
[analyst@rocky9-lab ~]$ echo "내 셸 PID=$$ PPID=$PPID"
내 셸 PID=309 PPID=308
[analyst@rocky9-lab ~]$ pstree -p | head -12
systemd(1)-+-agetty(55)
|-atd(53)
|-crond(54)
|-dbus-broker-lau(56)---dbus-broker(57)
|-sshd(64)---sshd-session(305)---sshd-session(308)---bash(309)-+-head(335)
| `-pstree(334)
|-systemd(159)---(sd-pam)(161)
|-systemd-journal(22)
`-systemd-logind(45)
[analyst@rocky9-lab ~]$ p=$$; while [ "$p" -ne 0 ]; do printf '%-6s %s\n' "$p" "$(tr '\0' ' ' < /proc/$p/cmdline | cut -c1-60)"; p=$(awk '/^PPid/{print $2}' /proc/$p/status); done
309 bash /tmp/run_03_1.sh
308 sshd-session: analyst@pts/0
305 sshd-session: analyst [priv]
64 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
1 /usr/sbin/init
analyst@ubuntu-lab:~$ sleep 200 & bash -c 'sleep 200' &
analyst@ubuntu-lab:~$ sleep 0.3; ps -ef --forest | grep -v -e 'ps -ef' -e grep | tail -8
root 258 96 1 09:38 ? 00:00:00 \_ sshd: analyst [priv]
analyst 269 258 0 09:38 ? 00:00:00 \_ sshd: analyst@pts/0
analyst 270 269 0 09:38 pts/0 00:00:00 \_ bash /tmp/run_03_2.sh
analyst 271 270 0 09:38 pts/0 00:00:00 \_ sleep 200
analyst 272 270 0 09:38 pts/0 00:00:00 \_ sleep 200
analyst 276 270 0 09:38 pts/0 00:00:00 \_ tail -8
analyst 191 1 0 09:38 ? 00:00:00 /usr/lib/systemd/systemd --user
analyst 192 191 0 09:38 ? 00:00:00 \_ (sd-pam)
analyst@ubuntu-lab:~$ pstree -ps $(pgrep -n sleep)
systemd(1)───sshd(96)───sshd(258)───sshd(269)───bash(270)───sleep(272)
analyst@ubuntu-lab:~$ pgrep -P $$ -a
271 sleep 200
272 sleep 200
analyst@ubuntu-lab:~$ kill $(jobs -p) 2>/dev/null; pkill -u analyst -x sleep; true
| 관찰 | 의미 |
|---|---|
pid_max = 4194304 | 64비트 시스템에서 systemd가 설정하는 값(2²²). 예전 기본값 32768보다 훨씬 커서 PID 재사용 주기가 길다 |
| PID 1의 PPID는 0 | PID 1은 커널이 직접 만든다. 그 위에는 사용자 공간 부모가 없다 |
pstree -p에서 bash(309)가 sshd(64) 아래에 | 내 셸이 SSH 데몬의 후손이라는 것이 한눈에 보인다 |
systemd(159)---(sd-pam)(161) | 사용자가 로그인하면 사용자별 systemd --user 인스턴스가 따로 뜬다 |
| 조상 추적 결과 309 → 308 → 305 → 64 → 1 | /proc/PID/status의 PPid만으로 트리 전체를 복원할 수 있다 |
Ubuntu ps -ef --forest의 \_ 들여쓰기 | 부모 아래에 자식이 들여쓰기되어, 사용자·시작 시각과 함께 트리를 읽을 수 있다 |
bash -c 'sleep 200'인데 트리에는 sleep만 | 02편의 마지막 명령 exec 최적화 다. bash -c가 sleep으로 교체되어 중간 bash가 없다 |
pstree -ps → systemd(1)───sshd(96)───sshd(258)───sshd(269)───bash(270)───sleep(272) | 특정 프로세스 하나의 조상만 한 줄로 보여 준다. 관제 보고서에 붙이기 좋은 형식이다 |
pgrep -P $$ -a → 271, 272 | 현재 셸의 직계 자식만 골라낸다 |
| 주제 | 내용 |
|---|---|
| 실행 주체 추적 | "누가 실행했나"는 UID만으로 부족하다. 같은 root라도 sshd 세션의 셸 에서 왔는지, cron 에서 왔는지, 웹 서버 에서 왔는지는 조상 체인이 알려 준다 |
| PPID 1의 의미 | 사용자 명령인데 PPID가 1이면 부모가 이미 종료된 고아(데몬화된) 프로세스 다. 공격 도구는 nohup, setsid, 이중 fork로 일부러 PPID 1이 되어 트리에서 출처를 숨긴다 (19편) |
| PID 재사용 | 오래전 로그의 PID와 지금의 PID는 다른 프로세스일 수 있다. 시작 시각(ps -o lstart)을 함께 확인한다 |
| 네임스페이스 | 컨테이너 안의 PID 1은 호스트에서는 다른 PID다. 컨테이너 탈출 분석 시 어느 PID 네임스페이스의 번호인지 를 구분한다 |
[Detection] 의심 프로세스: /tmp/.x/kdevtmp (PID 4127, UID www-data)
↓
[조상 추적] pstree -ps 4127
systemd(1)───php-fpm(812)───php-fpm(2031)───sh(4120)───kdevtmp(4127)
↓
[판단] 웹 애플리케이션 프로세스(php-fpm)가 셸을 만들고 /tmp 실행 파일을 실행 → 웹 취약점 경유 침해
↓
[Evidence] php-fpm 2031 의 처리 요청 시각 ↔ 웹 접근 로그, sh 4120 의 cmdline
↓
[Response] 웹 요청 출발지 차단, 취약 파라미터 확인, 프로세스·파일 수집 후 종료
| 정상 조상 체인 (예) | 조사 대상 조상 체인 (예) |
|---|---|
systemd → sshd → sshd → bash → vim | systemd → nginx → sh → curl |
systemd → crond → crond → sh → backup.sh | systemd → crond → sh → /dev/shm/x |
systemd → systemd --user → gnome-terminal → bash | systemd(1) → ./a.out (사용자 명령인데 PPID 1) |
| 실수 | 결과 | 예방 |
|---|---|---|
| 의심 프로세스를 PID만 보고 바로 kill | 부모(실제 원인)가 다시 자식을 만든다 | 먼저 pstree -ps로 부모를 확인하고 원인부터 제거 |
pstree가 없는 최소 설치 서버에서 당황 | 트리 확인 불가 | ps -ef --forest 또는 /proc/PID/status 루프로 대체 (psmisc 패키지에 pstree 포함) |
| PID만 보고서에 기록 | 나중에 재사용된 PID와 혼동 | PID + lstart + cmdline + 조상 체인 |
$PPID를 실시간 부모로 믿음 | 셸 변수 $PPID는 셸 시작 시점 값이다 | 현재 부모는 /proc/$$/status에서 확인 |
[ ] pid_max 값과 PID 1 의 PPID(0)를 확인했다
[ ] pstree -p 로 전체 트리에서 내 셸의 위치를 찾았다
[ ] /proc/PID/status 의 PPid 로 PID 1 까지 거슬러 올라갔다
[ ] sshd 권한 분리 구조(listener → priv → user)를 트리에서 확인했다
[ ] ps -ef --forest, pstree -ps, pgrep -P 의 차이를 이해했다
[ ] bash -c 마지막 명령이 exec 되어 중간 bash 가 없는 것을 확인했다
pstree -ps PID는 조상 체인을, pgrep -P PID는 직계 자식을 빠르게 보여 준다.다음 글 「04. 프로세스 상태 (R·S·D·T·Z)」 에서는 ps의 STAT 열을 해석한다. 실행(R), 대기(S), 정지(T), 좀비(Z) 상태를 직접 만들어 보고, s, +, <, N 같은 보조 플래그가 무엇을 뜻하는지 확인한다.