서비스 · 프로세스 관리 10 / 50 · Part 1. 프로세스 기초
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

SSH로 접속해 오래 걸리는 작업을 &로 백그라운드에 걸어 두고 로그아웃했는데, 다음 날 보니 작업이 중간에 죽어 있었다. 반대로 공격자가 남겨 둔 프로세스는 로그아웃 후에도 멀쩡히 살아 있다. 이 차이는 세션, 프로세스 그룹, SIGHUP 을 이해하면 설명된다.

이번 글에서는 jobs, bg, fg로 작업 상태를 바꿔 보고, nohup, setsid, disown 중 실제로 로그아웃에서 살아남는 방법 을 직접 로그아웃해서 확인한다.


2. 핵심 개념

2-1. 작업 제어 명령

명령동작
cmd &백그라운드로 실행
jobs -l현재 셸의 작업 목록(작업 번호, PID, 상태)
Ctrl+Z포그라운드 작업에 SIGTSTP → 정지
bg %N정지된 작업을 백그라운드에서 계속 실행 (SIGCONT)
fg %N작업을 포그라운드로 가져옴
kill %N작업 번호로 시그널 전송
%+ / %-현재 작업 / 이전 작업

2-2. 관련 용어

용어의미확인
프로세스 그룹 (PGID)파이프라인처럼 함께 제어되는 프로세스 묶음 = 셸의 "작업"ps -o pgid
세션 (SID)로그인 한 번에 생기는 프로세스 그룹들의 묶음ps -o sid
제어 터미널세션이 연결된 터미널 (pts/0)ps -o tty
TPGID터미널의 현재 포그라운드 프로세스 그룹ps -o tpgid

2-3. 로그아웃 시 SIGHUP

터미널 연결이 끊기면(hang up) 커널은 그 터미널의 포그라운드 프로세스 그룹과 세션 리더 에게 SIGHUP을 보낸다. 대화형 bash는 SIGHUP을 받으면 자신의 작업들에도 SIGHUP을 전달한다. SIGHUP의 기본 동작은 종료 다.

방법원리
nohupSIGHUP을 무시(SIG_IGN) 로 설정한 뒤 exec. 출력이 터미널이면 nohup.out으로 돌린다
setsid새 세션 을 만들어 기존 터미널과 관계를 끊는다
disownbash의 작업 목록에서만 제거 → bash가 SIGHUP을 전달하지 않을 뿐, 커널이 보내는 SIGHUP은 막지 못한다

3. 동작 원리

Job Control 상태 전이와 로그아웃 생존 비교

SSH 세션 (SID 452, 제어 터미널 pts/0)
 ├─ PGID 452 : bash, sleep 301, sleep 302(nohup), sleep 303(disown)   ← 포그라운드 그룹
 └─ SID 457  : sleep 304 (setsid, 터미널 없음)                          ← 별도 세션

로그아웃 → pts/0 hang up → 커널이 포그라운드 그룹(452)에 SIGHUP
   sleep 301 : 기본 동작(종료)
   sleep 302 : SIGHUP 무시 → 생존 → 부모 bash 종료 → PPID 1 (08편 고아)
   sleep 303 : disown 했지만 같은 그룹 → 종료
   sleep 304 : 다른 세션 → SIGHUP 대상 아님 → 생존

이 실습은 비대화형 스크립트 로 실행되어 백그라운드 작업이 셸과 같은 프로세스 그룹(452)에 들어갔다. 대화형 셸(job control 활성)에서는 작업마다 별도 그룹이 생기고, bash가 종료 시 작업에 SIGHUP을 보낼지(shopt huponexit)에 따라 결과가 달라질 수 있다. 그래서 확실한 방법은 nohup 또는 setsid 다.


4. 명령어 실습

# 1) (Rocky) job control
set -m                           # 스크립트에서 job control 활성화 (대화형 셸은 기본 활성)
sleep 100 &
sleep 200 &
jobs -l
kill -TSTP %2; jobs -l           # Ctrl+Z 와 같은 효과
bg %2; jobs -l
sleep 3 & fg %3; echo "fg 종료 코드=$?"
ps -o pid,pgid,sid,tpgid,stat,cmd -t $(tty | sed 's#/dev/##')

# 2) (Ubuntu) 네 가지 방식으로 실행 후 로그아웃
sleep 301 &
nohup sleep 302 &
sleep 303 & disown
setsid sleep 304 < /dev/null > /dev/null 2>&1 &
ps -o pid,ppid,pgid,sid,stat,cmd -C sleep
grep SigIgn /proc/$(pgrep -f 'sleep 302')/status
exit

# 3) 다시 로그인해서 확인
ps -o pid,ppid,sid,stat,tty,cmd -C sleep
ls -l ~/nohup.out

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — jobs · 정지(TSTP) · bg · fg

실제 실행 결과 — Ubuntu 24.04.5 · analyst@ubuntu-lab — 로그아웃 후 살아남는 작업 (nohup · setsid · disown)

실제 실행 결과 — Ubuntu 24.04.5 · analyst@ubuntu-lab — 다시 로그인해서 확인

텍스트 원본(실제 출력):

[analyst@rocky9-lab ~]$ set -m
[analyst@rocky9-lab ~]$ sleep 100 &
[analyst@rocky9-lab ~]$ sleep 200 &
[analyst@rocky9-lab ~]$ jobs -l
[1]-   640 Running                 sleep 100 &
[2]+   641 Running                 sleep 200 &
[analyst@rocky9-lab ~]$ kill -TSTP %2; sleep 0.2; jobs -l
[1]-   640 Running                 sleep 100 &
[2]+   641 Stopped                 sleep 200
[analyst@rocky9-lab ~]$ bg %2; jobs -l
[2]+ sleep 200 &
[1]-   640 Running                 sleep 100 &
[2]+   641 Running                 sleep 200 &
[analyst@rocky9-lab ~]$ sleep 3 & fg %3; echo "fg 종료 코드=$?"
sleep 3
fg 종료 코드=0
[analyst@rocky9-lab ~]$ ps -o pid,pgid,sid,tpgid,stat,cmd -t $(tty | sed 's#/dev/##')
    PID    PGID     SID   TPGID STAT CMD
    616     616     616     647 Ss   bash /tmp/run_10_1.sh
    640     640     616     647 S    sleep 100
    641     641     616     647 S    sleep 200
    647     647     616     647 R+   ps -o pid,pgid,sid,tpgid,stat,cmd -t pts/0
[analyst@rocky9-lab ~]$ kill %1 %2
analyst@ubuntu-lab:~$ cd ~; rm -f nohup.out
analyst@ubuntu-lab:~$ sleep 301 &
analyst@ubuntu-lab:~$ nohup sleep 302 &
analyst@ubuntu-lab:~$ sleep 303 & disown
analyst@ubuntu-lab:~$ setsid sleep 304 < /dev/null > /dev/null 2>&1 &
analyst@ubuntu-lab:~$ sleep 0.3; ps -o pid,ppid,pgid,sid,stat,cmd -C sleep
nohup: appending output to 'nohup.out'
    PID    PPID    PGID     SID STAT CMD
    454     452     452     452 S+   sleep 301
    455     452     452     452 S+   sleep 302
    456     452     452     452 S+   sleep 303
    457     452     457     457 Ss   sleep 304
analyst@ubuntu-lab:~$ grep SigIgn /proc/$(pgrep -f 'sleep 302')/status
SigIgn:	0000000000000007
analyst@ubuntu-lab:~$ ps -o pid,ppid,sid,stat,tty,cmd -C sleep
    PID    PPID     SID STAT TT       CMD
    455       1     452 S    ?        sleep 302
    457       1     457 Ss   ?        sleep 304
analyst@ubuntu-lab:~$ ls -l ~/nohup.out 2>&1
-rw------- 1 analyst analyst 0 Sep 24 11:50 /home/analyst/nohup.out
analyst@ubuntu-lab:~$ pkill -u analyst -x sleep; true

6. 결과 해석

관찰의미
[1]- 640 Running, [2]+ 641 Running작업 번호, PID, 상태. +는 현재 작업(fg/bg 기본 대상)
TSTP 후 [2]+ StoppedCtrl+Z와 같은 SIGTSTP로 정지
bg %2 → RunningSIGCONT를 받아 백그라운드에서 계속 실행
fg %3 → sleep 3 출력 후 종료 코드 0작업을 포그라운드로 가져와 끝날 때까지 기다렸다
PGID 640, 641, 647이 각각 다름job control이 켜지면 작업마다 별도 프로세스 그룹 이 생긴다
모두 SID 616같은 로그인 세션. 세션 리더는 bash(616, STAT Ss)
TPGID 647 = ps 자신의 PGID, STAT R+지금 터미널의 포그라운드 그룹은 실행 중인 ps다
Ubuntu: 301·302·303 모두 PGID 452, STAT S+비대화형 실행이라 셸과 같은 포그라운드 그룹 에 들어갔다
304는 PGID 457 SID 457 Sssetsid로 새 세션의 리더가 되었다
nohup: appending output to 'nohup.out'출력이 터미널이라 파일로 돌렸다
SigIgn: 0000000000000007비트 1·2·3 = SIGHUP·SIGINT·SIGQUIT 무시. HUP 무시는 nohup이, INT·QUIT 무시는 비대화형 셸의 백그라운드 작업 규칙이 설정했다
재로그인 후 302·304만 남음nohup과 setsid만 생존. disown(303)은 커널이 보낸 SIGHUP에 종료되었다
생존한 두 프로세스의 PPID 1, TTY ?부모 셸이 사라져 고아가 되었고, 터미널도 없다
302의 SID는 여전히 452세션은 끝났지만 옛 세션 번호가 흔적으로 남는다. 출처 추적 단서가 된다

7. 보안 관점

주제내용
지속 실행nohup ./x &, setsid ./x는 공격자가 셸 접속이 끊겨도 도구를 계속 돌리는 가장 흔한 방법이다
흔적nohup은 기본적으로 nohup.out을 남긴다. 공격자의 홈·웹 루트에서 발견되는 nohup.out은 실행 흔적이다
세션 번호로그아웃 후 남은 프로세스의 SID는 원래 로그인 세션 을 가리킨다. loginctl, 인증 로그와 연결해 누가 남겼는지 찾는다
정책systemd-logind의 KillUserProcesses=yes로 설정하면 로그아웃 시 세션 cgroup의 프로세스를 모두 정리한다. 배포판 기본값은 no다
올바른 운영운영 작업을 nohup으로 띄우면 재부팅·장애 시 관리가 안 된다. systemd 서비스·타이머 (25·35편), 필요하면 tmux를 쓴다

8. 보안관제 관점

[Detection]  사용자 로그아웃(세션 종료) 이후에도 실행 중인 해당 사용자 프로세스
     ↓
[확인]       ps -eo pid,ppid,sid,user,tty,lstart,args -ww | awk '$2==1 && $5=="?"'
             loginctl list-sessions   ← 해당 SID 세션이 이미 종료되었는가
     ↓
[추적]       cat /proc/PID/cgroup     ← session-N.scope
             journalctl -u sshd 에서 같은 시각 로그인 IP
             find / -name nohup.out -newermt '-1 day' 2>/dev/null
     ↓
[판단]       업무용 배치인가? 등록되지 않은 도구인가?
     ↓
[Response]   소유자 확인 → 불명확하면 증거 수집 후 종료, 계정 점검

9. 실무에서 자주 발생하는 실수

실수결과예방
cmd &만 하고 로그아웃작업이 SIGHUP으로 종료nohup, setsid, 또는 systemd-run/tmux
disown이면 안전하다고 믿음환경에 따라 종료됨 (실측)확실한 방법은 nohup/setsid
nohup 작업의 출력 방치nohup.out이 무한히 커짐출력 리다이렉션 지정
백그라운드 작업이 입력을 기다림SIGTTIN으로 멈춰 T 상태입력을 < /dev/null로
장기 작업을 nohup으로 운영재부팅 후 사라지고 관리·로그 없음systemd 서비스로 등록

10. 실습 체크리스트

[ ] jobs, bg, fg, kill %N 으로 작업 상태를 바꿔 봤다
[ ] job control 에서 작업마다 PGID 가 다른 것을 확인했다
[ ] SID, TPGID 로 세션과 포그라운드 그룹을 구분했다
[ ] nohup 프로세스의 SigIgn 에서 SIGHUP 무시 비트를 확인했다
[ ] 로그아웃 후 nohup·setsid 만 살아남는 것을 확인했다
[ ] 남은 프로세스의 SID 로 원래 세션을 추적할 수 있다

11. 핵심 정리

  • 작업(job)은 셸이 관리하는 프로세스 그룹이며, 터미널은 포그라운드 그룹 하나에만 입력을 준다.
  • 로그아웃 시 SIGHUP이 전달되어 일반 백그라운드 작업은 종료될 수 있다.
  • nohup(SIGHUP 무시)과 setsid(새 세션)는 살아남고, disown만으로는 보장되지 않는다.
  • 로그아웃 후 남은 사용자 프로세스는 PPID 1, TTY 없음, 옛 SID를 가진다 — 관제의 추적 단서다.
  • 운영 작업은 nohup 대신 systemd 서비스·타이머로 관리한다.

12. 다음 편 예고

Part 2의 첫 글 「11. Linux Signal의 개념과 주요 시그널」 에서는 지금까지 여러 번 등장한 SIGHUP, SIGTERM, SIGKILL, SIGSTOP을 체계적으로 정리한다. 시그널 번호와 종료 코드 128+N 의 관계, 프로세스의 시그널 처리 상태(SigIgn, SigCgt)를 해석하는 방법을 실습한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글