
서비스 · 프로세스 관리 24 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
관제 대시보드에 "서비스 이상" 경보가 떴다. systemctl status를 열면 loaded, active (exited), failed (Result: exit-code), status=203/EXEC 같은 값이 한 화면에 쏟아진다. 이 값들을 정확히 읽을 수 있어야 "서비스가 설정 오류로 못 뜬 것인지, 실행됐다가 죽은 것인지, 누가 죽인 것인지" 를 몇 초 안에 판단할 수 있다.
이번 글에서는 서비스 상태를 구성하는 네 가지 값을 정리하고, 일부러 실패하는 서비스를 만들어 실패 코드와 로그를 해석 하는 연습을 한다.
| 속성 | 질문 | 주요 값 |
|---|---|---|
LoadState | unit 파일을 제대로 읽었나? | 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 |
| 조합 | 의미 | 예 |
|---|---|---|
active (running) | 프로세스가 실행 중 | sshd, crond |
active (exited) | 프로세스는 끝났지만 성공 상태로 유지 | Type=oneshot + RemainAfterExit=yes (방화벽 규칙 적용 등) |
active (waiting) | 타이머가 다음 시각을 기다림 | *.timer |
inactive (dead) | 정상적으로 멈춰 있음 | 중지한 서비스 |
failed | 실패로 끝남 (재시작도 포기) | 설정 오류, 크래시 |
| Result | 의미 |
|---|---|
exit-code | 0이 아닌 종료 코드 |
signal | 시그널로 종료 (11편) |
core-dump | 크래시 + 코어덤프 |
timeout | 시작·중지 시간 초과 (13편) |
start-limit-hit | 너무 자주 재시작해 포기 (29편) |
oom-kill | OOM Killer (20편) |
systemd 자체가 실행 준비 중에 실패하면 200번대 전용 코드 를 쓴다. 대표적으로 203/EXEC(실행 파일 없음·권한 없음), 217/USER(User= 계정 없음), 200/CHDIR(WorkingDirectory 없음), 226/NAMESPACE(샌드박스 설정 실패, 38편)다.

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는 프로그램이 실행된 뒤 스스로 실패한 것 이다. 원인 조사 방향이 완전히 달라진다.
# 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


텍스트 원본(실제 출력):
[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
| 관찰 | 의미 |
|---|---|
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개 | 실패 기록만 지운 것이다. 원인은 해결되지 않았다 |
| 주제 | 내용 |
|---|---|
| 보안 서비스 실패 | auditd·EDR·로그 전송기가 failed면 그 시점부터 관제 공백 이다. is-system-running이 degraded면 즉시 원인 확인 |
| 203/EXEC의 의미 | 정상 서비스가 갑자기 203이면 실행 파일이 삭제·교체 되었을 수 있다. 공격자의 파일 삭제, 잘못된 패키지 제거를 확인한다 |
| 217/USER | 서비스 계정이 삭제되었다는 뜻. 계정 정리 사고 또는 공격 흔적 |
| core-dump | 반복되는 status=11/SEGV는 버그일 수도 있지만 메모리 공격(익스플로잇) 시도 일 수도 있다. 네트워크 서비스라면 요청 로그를 대조한다 |
| reset-failed 남용 | 경보를 없애려고 reset-failed만 하면 원인과 증거가 묻힌다 |
[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-running | running / degraded 판정 |
| 실수 | 결과 | 예방 |
|---|---|---|
inactive를 보고 "중지됨"으로 판단 | 실제로는 unit 이름 오타(not-found) | LoadState 먼저 확인 |
active (exited)를 "죽었다"로 오해 | oneshot 서비스는 정상 상태 | Type과 RemainAfterExit 확인 |
| 203/EXEC에서 앱 로그를 찾음 | 앱은 실행된 적이 없다 | ExecStart 경로·권한·SELinux 확인 |
| status의 마지막 몇 줄만 봄 | 앞선 원인 로그를 놓침 | journalctl -u UNIT -n 50 |
| reset-failed로 경보 정리 | 원인 방치 | 원인 기록 후 초기화 |
[ ] LoadState, ActiveState, SubState, UnitFileState 를 각각 확인했다
[ ] static, waiting, not-found 의 의미를 구분했다
[ ] 실행 파일이 없는 서비스의 203/EXEC 를 확인했다
[ ] 프로그램이 exit 2 로 끝난 서비스의 로그와 종료 코드를 확인했다
[ ] Result, ExecMainCode, ExecMainStatus 를 스크립트 형태로 조회했다
[ ] reset-failed 가 기록만 지운다는 것을 이해했다
active (exited)는 oneshot의 정상 상태, not-found는 unit 자체가 없다는 뜻이다.failed는 관제 공백이다. degraded 상태를 즉시 확인한다.다음 글 「25. 서비스 Unit 파일 직접 작성하기」 에서는 Python으로 만든 간단한 웹 앱을 전용 계정으로 실행되는 systemd 서비스 로 등록한다. Type, User, WorkingDirectory, Environment, Restart, WantedBy를 하나씩 설명하고, systemd-analyze verify로 문법을 검사한다.