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

1. 들어가며

관제 대시보드에 "서비스 이상" 경보가 떴다. systemctl status를 열면 loaded, active (exited), failed (Result: exit-code), status=203/EXEC 같은 값이 한 화면에 쏟아진다. 이 값들을 정확히 읽을 수 있어야 "서비스가 설정 오류로 못 뜬 것인지, 실행됐다가 죽은 것인지, 누가 죽인 것인지" 를 몇 초 안에 판단할 수 있다.

이번 글에서는 서비스 상태를 구성하는 네 가지 값을 정리하고, 일부러 실패하는 서비스를 만들어 실패 코드와 로그를 해석 하는 연습을 한다.


2. 핵심 개념

2-1. 상태를 구성하는 네 가지 값

속성질문주요 값
LoadStateunit 파일을 제대로 읽었나?loaded, not-found, bad-setting, masked
ActiveState지금 동작 중인가? (종류 공통)active, inactive, failed, activating, deactivating, reloading
SubState세부 상태 (종류별)service: running, exited, dead, failed / timer: waiting / socket: listening
UnitFileState부팅 시 시작하나?enabled, disabled, static, masked

2-2. 헷갈리기 쉬운 조합

조합의미예
active (running)프로세스가 실행 중sshd, crond
active (exited)프로세스는 끝났지만 성공 상태로 유지Type=oneshot + RemainAfterExit=yes (방화벽 규칙 적용 등)
active (waiting)타이머가 다음 시각을 기다림*.timer
inactive (dead)정상적으로 멈춰 있음중지한 서비스
failed실패로 끝남 (재시작도 포기)설정 오류, 크래시

2-3. 실패 원인 (Result)과 종료 코드

Result의미
exit-code0이 아닌 종료 코드
signal시그널로 종료 (11편)
core-dump크래시 + 코어덤프
timeout시작·중지 시간 초과 (13편)
start-limit-hit너무 자주 재시작해 포기 (29편)
oom-killOOM Killer (20편)

systemd 자체가 실행 준비 중에 실패하면 200번대 전용 코드 를 쓴다. 대표적으로 203/EXEC(실행 파일 없음·권한 없음), 217/USER(User= 계정 없음), 200/CHDIR(WorkingDirectory 없음), 226/NAMESPACE(샌드박스 설정 실패, 38편)다.


3. 동작 원리

systemctl status 한 화면 해부

systemctl start broken
  → PID 1 fork → 자식에서 exec 준비 (계정 전환, 디렉터리 이동, 샌드박스)
  → execve("/usr/local/bin/lab-app") 실패 (파일 없음)
  → 자식이 203 으로 종료 → "status=203/EXEC" → ActiveState=failed, Result=exit-code

systemctl start exit2
  → execve("/bin/bash") 성공 → 프로그램이 "config error" 출력 후 exit 2
  → "status=2/INVALIDARGUMENT" → failed

두 서비스 모두 Result=exit-code지만 의미는 다르다. 203은 프로그램이 한 줄도 실행되지 않은 것, 2는 프로그램이 실행된 뒤 스스로 실패한 것 이다. 원인 조사 방향이 완전히 달라진다.


4. 명령어 실습

# 1) 네 가지 값 비교
systemctl show crond -p LoadState,ActiveState,SubState,UnitFileState
systemctl show systemd-journald -p LoadState,ActiveState,SubState,UnitFileState
systemctl show systemd-tmpfiles-clean.timer -p LoadState,ActiveState,SubState,UnitFileState
systemctl show nosuch.service -p LoadState,ActiveState,SubState,UnitFileState

# 2) 실패하는 서비스 두 개
printf '[Unit]\nDescription=Lab broken service\n[Service]\nExecStart=/usr/local/bin/lab-app --port 9000\n' > /etc/systemd/system/broken.service
printf '[Unit]\nDescription=Lab exit-2 service\n[Service]\nExecStart=/bin/bash -c "echo config error >&2; exit 2"\n' > /etc/systemd/system/exit2.service
systemctl daemon-reload; systemctl start broken exit2

# 3) 실패 읽기
systemctl --failed --no-pager --no-legend
systemctl status broken --no-pager | head -7
systemctl status exit2 --no-pager | tail -4
systemctl show exit2 -p Result,ExecMainStatus,ExecMainCode

# 4) 실패 기록 초기화
systemctl reset-failed; systemctl --failed --no-legend | wc -l

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 상태를 구성하는 4개의 값

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — failed 서비스 만들고 읽기

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

[root@rocky9-lab ~]# systemctl show crond -p LoadState,ActiveState,SubState,UnitFileState
LoadState=loaded
ActiveState=active
SubState=running
UnitFileState=enabled
[root@rocky9-lab ~]# systemctl show systemd-journald -p LoadState,ActiveState,SubState,UnitFileState
LoadState=loaded
ActiveState=active
SubState=running
UnitFileState=static
[root@rocky9-lab ~]# systemctl show systemd-tmpfiles-clean.timer -p LoadState,ActiveState,SubState,UnitFileState
LoadState=loaded
ActiveState=active
SubState=waiting
UnitFileState=static
[root@rocky9-lab ~]# systemctl show nosuch.service -p LoadState,ActiveState,SubState,UnitFileState
LoadState=not-found
ActiveState=inactive
SubState=dead
UnitFileState=
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab broken service\n[Service]\nExecStart=/usr/local/bin/lab-app --port 9000\n' > /etc/systemd/system/broken.service
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab exit-2 service\n[Service]\nExecStart=/bin/bash -c "echo config error >&2; exit 2"\n' > /etc/systemd/system/exit2.service
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl start broken exit2; sleep 1
[root@rocky9-lab ~]# systemctl --failed --no-pager --no-legend
● broken.service loaded failed failed Lab broken service
● exit2.service  loaded failed failed Lab exit-2 service
[root@rocky9-lab ~]# systemctl status broken --no-pager | head -7
× broken.service - Lab broken service
     Loaded: loaded (/etc/systemd/system/broken.service; static)
     Active: failed (Result: exit-code) since Thu 2026-09-24 12:10:34 UTC; 1s ago
   Duration: 2ms
    Process: 2809 ExecStart=/usr/local/bin/lab-app --port 9000 (code=exited, status=203/EXEC)
   Main PID: 2809 (code=exited, status=203/EXEC)

[root@rocky9-lab ~]# systemctl status exit2 --no-pager | tail -4
Sep 24 12:10:34 rocky9-lab systemd[1]: Started Lab exit-2 service.
Sep 24 12:10:34 rocky9-lab bash[2810]: config error
Sep 24 12:10:34 rocky9-lab systemd[1]: exit2.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
Sep 24 12:10:34 rocky9-lab systemd[1]: exit2.service: Failed with result 'exit-code'.
[root@rocky9-lab ~]# systemctl show exit2 -p Result,ExecMainStatus,ExecMainCode
Result=exit-code
ExecMainCode=1
ExecMainStatus=2
[root@rocky9-lab ~]# systemctl reset-failed; systemctl --failed --no-legend | wc -l
0

6. 결과 해석

관찰의미
crond: active / running / enabled정상 운영 서비스의 전형
journald: UnitFileState=static[Install]이 없고 소켓·다른 unit에 의해 시작된다. enable 대상이 아니다
tmpfiles-clean.timer: SubState=waiting타이머는 active 상태로 다음 실행을 기다린다 (35편)
nosuch.service: LoadState=not-found, inactive/dead존재하지 않는 unit도 조회는 된다. not-found를 먼저 확인 해야 오타를 "서비스 중지"로 오판하지 않는다
systemctl --failed에 2개관제에서 가장 먼저 보는 목록
broken: Loaded: ...; static[Install] 섹션을 쓰지 않아 static이다
broken: Duration: 2ms, status=203/EXEC실행 파일이 없어 exec 단계에서 실패 했다. 프로그램 로그는 있을 수 없다
exit2 로그 bash[2810]: config error프로그램의 표준 에러가 journal에 남았다. 서비스 출력은 자동으로 journal에 수집 된다
status=2/INVALIDARGUMENT종료 코드 2. systemd는 LSB 관례 이름을 붙여 보여 준다
Result=exit-code, ExecMainCode=1, ExecMainStatus=2스크립트에서 쓰기 좋은 형태. ExecMainCode 1 = exited(2 = killed, 3 = dumped)
reset-failed 후 failed 0개실패 기록만 지운 것이다. 원인은 해결되지 않았다

7. 보안 관점

주제내용
보안 서비스 실패auditd·EDR·로그 전송기가 failed면 그 시점부터 관제 공백 이다. is-system-running이 degraded면 즉시 원인 확인
203/EXEC의 의미정상 서비스가 갑자기 203이면 실행 파일이 삭제·교체 되었을 수 있다. 공격자의 파일 삭제, 잘못된 패키지 제거를 확인한다
217/USER서비스 계정이 삭제되었다는 뜻. 계정 정리 사고 또는 공격 흔적
core-dump반복되는 status=11/SEGV는 버그일 수도 있지만 메모리 공격(익스플로잇) 시도 일 수도 있다. 네트워크 서비스라면 요청 로그를 대조한다
reset-failed 남용경보를 없애려고 reset-failed만 하면 원인과 증거가 묻힌다

8. 보안관제 관점

[Detection]  모니터링: host01 systemd state = degraded
     ↓
[목록]       systemctl --failed --no-legend
     ↓
[분류]       systemctl show <unit> -p Result,ExecMainCode,ExecMainStatus
             ├ 203/EXEC  → ls -l <ExecStart 경로> , rpm -V <패키지>  (파일 삭제·변조?)
             ├ 217/USER  → getent passwd <User>                     (계정 삭제?)
             ├ killed 9  → dmesg OOM? / 타임아웃? / 외부 kill? (13·20편)
             └ exit N    → journalctl -u <unit> -n 50               (프로그램 오류 내용)
     ↓
[Response]   원인 제거 → systemctl restart → reset-failed → 보고
스크립트용 한 줄용도
systemctl list-units --state=failed --no-legend --plain \| awk '{print $1}'실패 unit 이름만
systemctl show UNIT -p ActiveState --value상태 값만
systemctl is-system-runningrunning / degraded 판정

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

실수결과예방
inactive를 보고 "중지됨"으로 판단실제로는 unit 이름 오타(not-found)LoadState 먼저 확인
active (exited)를 "죽었다"로 오해oneshot 서비스는 정상 상태Type과 RemainAfterExit 확인
203/EXEC에서 앱 로그를 찾음앱은 실행된 적이 없다ExecStart 경로·권한·SELinux 확인
status의 마지막 몇 줄만 봄앞선 원인 로그를 놓침journalctl -u UNIT -n 50
reset-failed로 경보 정리원인 방치원인 기록 후 초기화

10. 실습 체크리스트

[ ] LoadState, ActiveState, SubState, UnitFileState 를 각각 확인했다
[ ] static, waiting, not-found 의 의미를 구분했다
[ ] 실행 파일이 없는 서비스의 203/EXEC 를 확인했다
[ ] 프로그램이 exit 2 로 끝난 서비스의 로그와 종료 코드를 확인했다
[ ] Result, ExecMainCode, ExecMainStatus 를 스크립트 형태로 조회했다
[ ] reset-failed 가 기록만 지운다는 것을 이해했다

11. 핵심 정리

  • 서비스 상태는 LoadState(파일) · ActiveState(동작) · SubState(세부) · UnitFileState(부팅)로 읽는다.
  • active (exited)는 oneshot의 정상 상태, not-found는 unit 자체가 없다는 뜻이다.
  • 200번대 종료 코드(203/EXEC, 217/USER 등)는 프로그램 실행 전 systemd 단계의 실패다.
  • 서비스의 표준 출력·에러는 자동으로 journal에 남는다.
  • 보안 서비스의 failed는 관제 공백이다. degraded 상태를 즉시 확인한다.

12. 다음 편 예고

다음 글 「25. 서비스 Unit 파일 직접 작성하기」 에서는 Python으로 만든 간단한 웹 앱을 전용 계정으로 실행되는 systemd 서비스 로 등록한다. Type, User, WorkingDirectory, Environment, Restart, WantedBy를 하나씩 설명하고, systemd-analyze verify로 문법을 검사한다.


참고 자료


시리즈 이동

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

0개의 댓글