좀비 프로세스란?


프로세스가 종료될 때 마지막 문장의 실행을 끝내고 exit() 시스템콜을 사용하여 운영체제에게 자신의 삭제를 요청하면서 종료된다. 부모 프로세스는 wait() 시스템콜을 사용해 자식 프로세스의 종료를 기다릴 수 있다.

프로세스가 종료되면 메모리, CPU 등 사용하던 실행 리소스는 OS가 회수해간다. 하지만 프로세스의 종료 상태가 저장되는 프로세스 테이블의 해당 항목은 부모 프로세스가 wait()을 호출할 때까지 남아있게 된다. 즉, 종료는 되었지만 부모 프로세스가 아직 wait()을 호출하지 않은 프로세스를 좀비 프로세스라고 한다.

모든 프로세스는 종료 후 좀비 상태가 되지만 일반적으로 아주 짧은 시간 머무르다가 부모 프로세스의 wait() 호출에 의해 프로세스 식별자와 프로세스 테이블의 해당 항목이 OS에 반환된다. 좀비 프로세스는 메모리나 CPU 같은 실행 리소스를 소모하지 않고 프로세스 테이블 엔트리만 점유하기 때문에 소수의 좀비 프로세스는 큰 문제가 되지 않는다. 다만 좀비 프로세스가 많아지면 프로세스 식별자(PID)가 고갈될 수 있으므로 주의가 필요하다.

좀비 프로세스 확인


좀비 프로세스 필터링

ps aux | egrep "Z|defunct"
meatsby@lima-default:~$ ps aux | egrep "Z|defunct"
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        1970  0.0  0.0      0     0 ?        Z    17:50   0:00 [sshd] <defunct>
meatsby     5053  0.0  0.0   6904  1884 pts/2    S+   17:55   0:00 grep -E --color=auto Z|defunct

좀비 프로세스는 ps aux | egrep "Z|defunct" 명령어로 필터링하여 확인할 수 있다.

실습: 좀비 프로세스 생성


테스트 스크립트 작성

# zombie.py
import os
import time
 
cmd = os.popen('ps -ef --no-headers').read()
time.sleep(1000)

os.popen()은 내부적으로 쉘 프로세스를 생성하여 명령을 실행한다. .read()로 출력을 읽은 후 명시적으로 .close()를 호출하지 않으면, 쉘 프로세스가 종료된 후에도 부모 프로세스가 wait()을 호출하지 않아 좀비 상태로 남게 된다.

스크립트 실행

python3 zombie.py

좀비 프로세스 확인

meatsby@lima-default:~$ ps -ef | grep zombie
root        5133    4989  0 18:03 pts/1    00:00:00 python3 zombie.py
meatsby     5143    5000  0 18:04 pts/2    00:00:00 grep --color=auto zombie
 
meatsby@lima-default:~$ ps -ef | grep 5133
root        5133    4989  0 18:03 pts/1    00:00:00 python3 zombie.py
root        5134    5133  0 18:03 pts/1    00:00:00 [sh] <defunct>
meatsby     5145    5000  0 18:04 pts/2    00:00:00 grep --color=auto 5133
 
meatsby@lima-default:~$ ps aux | egrep "Z|defunct"
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        1970  0.0  0.0      0     0 ?        Z    17:50   0:00 [sshd] <defunct>
root        5134  0.0  0.0      0     0 pts/1    Z+   18:03   0:00 [sh] <defunct>
meatsby     5147  0.0  0.0   6904  1864 pts/2    S+   18:04   0:00 grep -E --color=auto Z|defunct

PID 5133 파이썬 부모 프로세스가 os.popen()으로 실행한 sh 프로세스(PID 5134)가 종료 후 좀비 상태로 남아있는 것을 확인할 수 있다.

시그널과 kill


프로세스에 보내는 비동기 알림 = signal. kill 명령은 “죽이는” 게 아니라 시그널을 보내는 명령. 기본 시그널 = SIGTERM (번호 15).

자주 쓰는 시그널

번호이름의미캐치·무시 가능?
1SIGHUP컨트롤 터미널 끊김. 데몬은 관례적으로 설정 reload 로 재해석.
2SIGINT인터럽트 (Ctrl-C).
9SIGKILL즉시 종료. 커널이 강제.
15SIGTERM정상 종료 요청. cleanup·flush 기회 줌.
17SIGCHLD자식 프로세스 상태 변경(종료 포함). 부모가 받아 wait() 호출.
18SIGCONT정지된 프로세스 재개.
19SIGSTOP프로세스 정지 (Ctrl-Z 비슷).

전체 = kill -l 로 확인. 번호·이름·의미는 POSIX 표준이지만 일부 번호는 아키텍처에 따라 다름 — 이름 (-TERM, -KILL) 으로 보내는 게 안전.

kill vs kill -9

kill <pid> = kill -15 <pid> = kill -TERM <pid> (기본)

  • SIGTERM(15) 보냄. “정상 종료해 줘” 요청.
  • 프로세스가 시그널 핸들러를 등록해놨다면 cleanup 코드 실행 — 열린 파일 flush, 네트워크 연결 정리, lock 해제, 진행 중 transaction commit/rollback 등.
  • 무시·캐치 가능. 잘 만든 데몬은 SIGTERM 받으면 graceful shutdown.

kill -9 <pid> = kill -KILL <pid>

  • SIGKILL(9) 보냄. 커널이 강제로 즉시 종료.
  • 프로세스가 캐치·무시 불가능. 시그널 핸들러도 안 통함.
  • cleanup 안 됨 → 임시 파일 남거나, DB 트랜잭션 깨지거나, 공유 자원 lock 안 풀리거나, 데이터 손실 가능.

기본은 kill (SIGTERM). SIGTERM 으로 안 죽으면 (hang·deadlock 등) 마지막 수단으로 kill -9. SIGKILL 먼저 보내는 건 cleanup 기회를 박탈하는 셈.

systemctl stop <service> 도 내부적으로 SIGTERM 먼저, 일정 시간(TimeoutStopSec, 기본 90s) 후에도 안 죽으면 SIGKILL 로 escalate.

좀비는 kill 로 못 죽인다

좀비 프로세스는 이미 죽었음. PID·프로세스 테이블 엔트리만 남은 시체 상태. 어떤 시그널을 보내도 무효 — 받을 프로세스가 없음.

  • kill -9 <zombie_pid> 도 효과 없음. 좀비는 죽일 게 없는 상태다.
  • 해결책 = 부모 프로세스에 SIGCHLD 보내서 wait() 시도 시키기:
    kill -CHLD <parent_pid>     # = kill -17
    단 부모 코드가 SIGCHLD 핸들러를 등록·구현해야 효과. 안 했으면 무시됨.
  • 확실한 방법 = 부모 프로세스 자체를 종료. 부모가 죽으면 좀비 자식은 init/systemd (PID 1) 가 입양(re-parent) → init 이 즉시 wait() 호출 → 좀비 reap.
    kill <parent_pid>           # SIGTERM 으로 graceful

위 실습의 zombie.py 예시 = PID 5133 (parent python) 을 종료하면 PID 5134 (defunct sh) 도 함께 사라진다.

ulimit 과 process limits


프로세스 한 개가 동시에 쓸 수 있는 자원 (열린 fd, 메모리, CPU 시간, 스레드 등) 의 상한. 시스템 한도(fs.file-max) 와 다름 — 이건 per-process.

조회

ulimit -n        # 열린 파일(fd) soft limit (현재 shell)
ulimit -Hn       # hard limit
ulimit -a        # 전체 자원의 soft limit 한꺼번에
cat /proc/<pid>/limits   # 특정 프로세스의 실제 적용값

설정 메커니즘 — PAM limits.conf

/etc/security/limits.conf/etc/security/limits.d/*.conf 에 적은 줄이 사용자 로그인 시 PAM 으로 적용된다. 포맷:

<domain>   <type>   <item>   <value>
*          soft     nofile   65530
*          hard     nofile   65530
  • <domain> = 사용자명 / @그룹명 / * (전체)
  • <type> = soft (시작 시 받는 값) / hard (setrlimit 으로 올릴 수 있는 상한). soft ≤ hard
  • <item> = nofile (fd) / nproc (프로세스) / memlock / stack / cpu
  • <value> = 정수 또는 unlimited

기본값이 낮다

대부분 배포판이 nofile soft 1024 / hard 4096. multi-connection 서버(JVM, Nginx, Node.js, DB 클라이언트) 는 금방 부족 → EMFILE: Too many open files → 새 연결 거부. 흔한 튜닝 = 65530 (16-bit 한도 65535 직전).

systemd 가 PAM 을 우회

systemd 가 띄우는 서비스(systemctl start nginx) 는 PAM login 을 안 거치므로 limits.conf 가 안 먹는다. 대신 unit 파일에:

[Service]
LimitNOFILE=65530
LimitNPROC=4096

또는 전역 default = /etc/systemd/system.confDefaultLimitNOFILE=. 위 ## 운영 명령어·## 리소스 제어 와 연결.

컨테이너 환경

  • Docker = docker run --ulimit nofile=65530:65530 ...
  • Kubernetes = 직접 pod 에 ulimit 설정 옵션이 없음. host 의 limit 이 ceiling, 컨테이너는 그 안에서 inherit. host 노드에 LimitNOFILE 을 미리 올려둬야 함.

운영 사례

EKS Worker AMI 의 OS 튜닝에서 nofile 65530 을 명시적으로 박은 사례 = 09. AL2023 EKS Worker AMI Migration > OS 튜닝 (sysctl·limits).

top


root@lima-default:~# top
top - 21:16:28 up  3:11,  1 user,  load average: 0.06, 0.03, 0.00
Tasks: 131 total,   1 running, 129 sleeping,   0 stopped,   1 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
MiB Mem :   3899.1 total,   1891.3 free,    434.1 used,   1738.1 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   3465.1 avail Mem
 
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      1 root      20   0   25504  14792  10184 S   0.0   0.4   0:03.55 systemd
      2 root      20   0       0      0      0 S   0.0   0.0   0:00.04 kthreadd
      3 root      20   0       0      0      0 S   0.0   0.0   0:00.00 pool_wo
  • 실시간 프로세스·CPU·메모리 모니터링.
  • load average = 1·5·15 분 평균 실행 큐 길이. CPU 수 대비 해석.
  • 컬럼 = VIRT 가상 메모리 / RES 실제 메모리 / S 상태(S sleep, R running, Z zombie).

sar


root@lima-default:~# sar
Linux 6.17.0-6-generic (lima-default) 	12/22/25 	_aarch64_	(4 CPU)
 
18:05:28     LINUX RESTART	(4 CPU)
 
18:10:36        CPU     %user     %nice   %system   %iowait    %steal     %idle
18:20:36        all      0.06      0.00      0.12      0.00      0.00     99.82
18:30:36        all      0.07      0.00      0.12      0.00      0.00     99.81
18:40:36        all      0.08      0.00      0.13      0.00      0.00     99.79
18:50:36        all      0.09      0.00      0.14      0.00      0.00     99.77
Average:        all      0.08      0.00      0.13      0.00      0.00     99.80
  • sysstat 패키지. cron 으로 주기적 수집되는 시스템 통계를 사후 조회.
  • top 이 “지금” 이라면 sar 는 “지난 X 분 평균” — 사후 트러블슈팅용.
  • 옵션 = -u CPU / -r 메모리 / -d 디스크 / -n DEV 네트워크 인터페이스.

systemd 란


systemd(system daemon)은 Unix 시스템 부팅 후 가장 먼저 생성된 후 다른 프로세스를 실행하는 init 역할을 하는 데몬이다. RedHat 에서 개발을 시작했고 RHEL/CentOS 와 Ubuntu 나 Arch 등 대부분의 리눅스 시스템에 공식적으로 채택되어 사용중이다.

systemd 는 PID 1번을 갖으며 부팅부터 서비스관리, 로그관리 등을 담당한다. 부팅 시 병렬로 실행되기 때문에 부팅속도 역시 빠르다.

부팅 시 필요한 작업을 systemd unit 으로 등록하여 사용할 수 있으며, 해당 파일들은 /etc/systemd/system 에 위치한다.

Unit과 구성 요소


systemd Unit 파일 구조

[Unit]
Description=Systemd Test
 
[Service]
ExecStart=/usr/local/bin/example.sh
 
[Install]
WantedBy=multi-user.target

systemd 파일의 섹션

  • Unit
    • Description: 서비스 설명
  • Service
    • ExecStart: 서비스를 시작하기 위한 실행파일
  • Timer
  • Install

Unit 타입

  • service: 데몬/서비스 관리 (.service)
  • socket: 소켓 활성화 (.socket)
  • timer: 작업 스케줄 (.timer)
  • target: 부트 목표/런레벨 유사 개념 (.target)
  • path: 파일/디렉터리 변경 감시 후 트리거 (.path)
  • mount/automount: 마운트 관리 (.mount, .automount)
  • device: udev 디바이스 이벤트 (.device)
  • swap: 스왑 관리 (.swap)
  • slice/scope: cgroups로 리소스 그룹화 (.slice, .scope)

서비스 관리


Service 주요 옵션

  • Type: simple | forking | oneshot | notify | dbus | idle
    • simple(기본): ExecStart 실행 후 곧바로 활성화로 간주
    • forking: 백그라운드로 포크하는 데몬에 사용 (PIDFile 권장)
    • oneshot: 단발성 작업(설정 스크립트) — RemainAfterExit=true와 함께 사용
    • notify: systemd-notify로 준비 완료 신호를 보냄
  • ExecStart, ExecStartPre, ExecStartPost, ExecReload, ExecStop
  • Restart: no | on-success | on-failure | on-abnormal | always 등
  • RestartSec: 재시작 지연
  • User/Group: 실행 계정 지정, WorkingDirectory
  • Environment/EnvironmentFile: 환경변수
  • LimitNOFILE, Nice, IOSchedulingClass 등 리소스 제한

의존성과 설치(Install)

  • [Unit]
    • Requires=, Wants=: 강/약 의존
    • After=, Before=: 실행 순서
  • [Install]
    • WantedBy=multi-user.target 등: enable 시 심볼릭 링크가 생성되는 대상
  • enable/disable는 부팅 시 자동 시작 여부만 제어, start/stop은 즉시 실행 제어

부팅과 Target


Target (런레벨 매핑)

  • graphical.target ≈ runlevel5, multi-user.target ≈ runlevel3, rescue.target ≈ runlevel1
  • 기본 타깃 확인/설정: systemctl get-default, systemctl set-default multi-user.target

운영 명령어


자주 쓰는 systemctl

  • 상태/제어
    • systemctl status <unit>
    • systemctl start|stop|restart <unit>
    • systemctl enable|disable <unit>
    • systemctl daemon-reload (유닛 파일 수정 후)
  • 나열/검색
    • systemctl list-units --type=service
    • systemctl list-unit-files --type=service

journalctl (로그)

  • journalctl -u <service>: 서비스 로그
  • journalctl -u <service> -f: 실시간 팔로우
  • journalctl -b: 현재 부팅 사이클 로그
  • -p info|warning|err, --since "2025-09-17 09:00"

스케줄링과 활성화


Timer vs cron

  • .timer 유닛으로 스케줄 정의, .service 실행을 트리거
  • 모드: OnCalendar= (캘린더), OnBootSec=/OnUnitActiveSec= (상대시간)
  • 장점: 유닛 의존성, 로깅(journal), 실패 시 Restart 정책과 통합

예시 (.timer):

[Unit]
Description=DB 백업 타이머
 
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
 
[Install]
WantedBy=timers.target

예시 (.service):

[Unit]
Description=DB 백업 실행
 
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

소켓/패스 활성화

  • socket: 포트/소켓 접근이 있을 때 관련 .service를 지연 기동
  • path: 파일 생성/변경 시 .service 트리거 (PathChanged=, PathExists= 등)

리소스 제어


cgroups와 리소스 제어

  • systemd는 cgroups을 통해 각 유닛 리소스를 추적/제한
  • CPUAccounting=, MemoryMax=, IPAddressDeny=/Allow= 등으로 제어 가능 (버전 의존)

트러블슈팅


  • 유닛 파일 변경 후 systemctl daemon-reload
  • 권한/경로 확인: ExecStart 경로, 실행권한, WorkingDirectory
  • 환경 변수: EnvironmentFile 경로/권한
  • SELinux/AppArmor 정책으로 인한 거부 여부 확인
  • 부팅 시 실패: systemctl --failed, journalctl -b -p err

References