
서비스 · 프로세스 관리 23 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
systemctl start, enable, restart, reload는 매일 쓰는 명령이지만, 각각이 정확히 무엇을 바꾸는지 는 헷갈리기 쉽다. "enable 했는데 서비스가 안 떠요", "restart와 reload의 차이가 뭔가요", "서비스를 확실히 못 뜨게 하려면?" 같은 질문이 모두 여기서 나온다.
이번 글에서는 systemctl 핵심 명령을 런타임(지금 실행 상태) 과 설치 상태(부팅 시 동작) 두 축으로 나누어, 각 명령 전후의 프로세스와 파일 시스템 변화를 직접 확인한다.
「리눅스 시스템 기초 37편」에서 systemctl의 기본 사용법을 다뤘다. 이번 글은 각 명령의 내부 동작 확인에 집중한다.
| 명령 | 동작 | PID |
|---|---|---|
start | 서비스 시작 (의존성 포함) | 새로 생성 |
stop | 종료 절차 (ExecStop → TERM → KILL, 13편) | 사라짐 |
restart | stop 후 start | 바뀜 |
reload | ExecReload= 실행, 설정만 다시 읽음 | 유지 |
reload-or-restart | reload 지원 시 reload, 아니면 restart | — |
try-restart | 실행 중일 때만 restart | — |
kill -s SIG | 서비스 cgroup 프로세스에 시그널 | — |
| 명령 | 동작 |
|---|---|
enable | [Install]에 따라 *.wants/ 링크 생성 → 부팅 시 시작 |
disable | 링크 제거 |
enable --now / disable --now | 링크 변경 + 즉시 start / stop |
mask | unit을 /dev/null로 링크 → 어떤 방법으로도 시작 불가 |
unmask | mask 해제 |
| 명령 | 출력 | 종료 코드 |
|---|---|---|
is-active UNIT | active / inactive / failed | active면 0 |
is-enabled UNIT | enabled / disabled / static / masked | enabled면 0 |
is-failed UNIT | failed 여부 | failed면 0 |
status UNIT | 상태 요약 + 최근 로그 | 24편 |
show UNIT -p PROP | 속성 값 (스크립트용) | — |
list-dependencies UNIT | 의존성 트리 | 26편 |

런타임과 설치 상태는 서로 독립 이다. 그래서 네 가지 조합이 모두 가능하다.
systemctl start atd → 지금 실행 (재부팅하면? 설치 상태에 따름)
systemctl enable atd → 부팅 시 실행 (지금은? 그대로)
systemctl enable --now → 둘 다
systemctl mask atd → /etc/systemd/system/atd.service → /dev/null
/etc 가 /usr/lib 보다 우선(22편)이라 원본 대신 빈 파일이 선택됨 → 시작 불가
reload는 서비스가 ExecReload=를 정의했을 때만 가능하다. 대부분 11편의 SIGHUP을 보내는 방식 으로 구현되어 있어, 접속을 끊지 않고 설정만 다시 읽는다.
# 1) 런타임: stop · start · restart
systemctl stop atd; systemctl is-active atd; pgrep -a atd || echo "atd 프로세스 없음"
systemctl start atd; systemctl is-active atd; pgrep -a atd
OLD=$(systemctl show atd -p MainPID --value); systemctl restart atd
NEW=$(systemctl show atd -p MainPID --value); echo "restart: PID $OLD → $NEW"
systemctl status atd --no-pager | head -8
# 2) 설치 상태: disable · enable · mask
systemctl is-enabled atd; systemctl disable atd; systemctl is-enabled atd
systemctl enable atd; ls -l /etc/systemd/system/multi-user.target.wants/atd.service
systemctl mask atd; ls -l /etc/systemd/system/atd.service
systemctl start atd; systemctl is-enabled atd
systemctl unmask atd; systemctl start atd; systemctl is-active atd
# 3) (Ubuntu) reload, 의존성, 여러 unit 조회
systemctl show ssh -p CanReload,ExecReload --no-pager
systemctl reload ssh && journalctl -u ssh -n 2 --no-pager -o cat
systemctl list-dependencies cron --no-pager | head -8
systemctl is-active cron nonexistent.service; echo "종료 코드=$?"



텍스트 원본(실제 출력):
[root@rocky9-lab ~]# systemctl stop atd; systemctl is-active atd; pgrep -a atd || echo "atd 프로세스 없음"
inactive
atd 프로세스 없음
[root@rocky9-lab ~]# systemctl start atd; systemctl is-active atd; pgrep -a atd
active
2631 /usr/sbin/atd -f
[root@rocky9-lab ~]# OLD=$(systemctl show atd -p MainPID --value); systemctl restart atd; NEW=$(systemctl show atd -p MainPID --value); echo "restart: PID $OLD → $NEW"
restart: PID 2631 → 2637
[root@rocky9-lab ~]# systemctl status atd --no-pager | head -8
● atd.service - Deferred execution scheduler
Loaded: loaded (/usr/lib/systemd/system/atd.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-24 12:10:32 UTC; 20ms ago
Docs: man:atd(8)
Main PID: 2637 (atd)
Tasks: 1 (limit: 51293)
Memory: 288.0K
CGroup: /docker/5a46f907edb23397a00c52e8214c9c387f1cf283dd50c13b81f6c01a58252f3b/system.slice/atd.service
[root@rocky9-lab ~]# systemctl is-enabled atd; systemctl disable atd; systemctl is-enabled atd
enabled
Removed "/etc/systemd/system/multi-user.target.wants/atd.service".
disabled
[root@rocky9-lab ~]# ls -l /etc/systemd/system/multi-user.target.wants/ | grep -c atd
0
[root@rocky9-lab ~]# systemctl enable atd; ls -l /etc/systemd/system/multi-user.target.wants/atd.service
Created symlink /etc/systemd/system/multi-user.target.wants/atd.service → /usr/lib/systemd/system/atd.service.
lrwxrwxrwx 1 root root 35 Sep 24 12:10 /etc/systemd/system/multi-user.target.wants/atd.service -> /usr/lib/systemd/system/atd.service
[root@rocky9-lab ~]# systemctl mask atd; ls -l /etc/systemd/system/atd.service
Created symlink /etc/systemd/system/atd.service → /dev/null.
lrwxrwxrwx 1 root root 9 Sep 24 12:10 /etc/systemd/system/atd.service -> /dev/null
[root@rocky9-lab ~]# systemctl start atd; systemctl is-enabled atd
Failed to start atd.service: Unit atd.service is masked.
masked
[root@rocky9-lab ~]# systemctl unmask atd; systemctl start atd; systemctl is-active atd
Removed "/etc/systemd/system/atd.service".
active
root@ubuntu-lab:~# systemctl show ssh -p CanReload,ExecReload --no-pager
ExecReload={ path=/usr/sbin/sshd ; argv[]=/usr/sbin/sshd -t ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
ExecReload={ path=/bin/kill ; argv[]=/bin/kill -HUP $MAINPID ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
CanReload=yes
root@ubuntu-lab:~# systemctl reload ssh && journalctl -u ssh -n 2 --no-pager -o cat
Reloaded ssh.service - OpenBSD Secure Shell server.
Received SIGHUP; restarting.
root@ubuntu-lab:~# systemctl list-dependencies cron --no-pager | head -8
cron.service
● ├─system.slice
● └─sysinit.target
● ├─dev-hugepages.mount
● ├─dev-mqueue.mount
○ ├─kmod-static-nodes.service
○ ├─ldconfig.service
● ├─proc-sys-fs-binfmt_misc.automount
root@ubuntu-lab:~# systemctl is-active cron nonexistent.service; echo "종료 코드=$?"
active
inactive
종료 코드=0
| 관찰 | 의미 |
|---|---|
stop 후 inactive, 프로세스 없음 | 런타임 상태만 바뀌었다. enable 상태는 그대로다 |
start 후 2631 /usr/sbin/atd -f | PID 1이 새 프로세스를 만들었다 |
restart: PID 2631 → 2637 | restart는 프로세스를 새로 만든다. 메모리 상태·연결이 모두 초기화된다 |
status의 Loaded: ... enabled; preset: enabled, Active: active (running) | 한 화면에 설치 상태와 런타임 상태가 함께 보인다 (24편) |
disable → Removed ".../multi-user.target.wants/atd.service" | disable = 링크 삭제 다. 프로세스는 계속 실행 중이다 |
enable → Created symlink ... → /usr/lib/systemd/system/atd.service | enable = 링크 생성 |
mask → atd.service → /dev/null | /etc에 빈 파일 링크를 만들어 원본을 가렸다 |
mask 상태에서 start → Unit atd.service is masked. | 수동 시작도 거부된다. 다른 unit의 의존성으로도 시작되지 않는다 |
Ubuntu ExecReload 두 개: sshd -t → kill -HUP $MAINPID | reload 전에 설정 문법을 검사 하고, 통과해야 HUP을 보낸다. 잘못된 설정으로 sshd가 죽는 것을 막는 안전장치다 |
로그 Received SIGHUP; restarting. | sshd가 HUP을 받아 설정을 다시 읽었다. 기존 SSH 세션은 유지된다 |
is-active cron nonexistent.service → active / inactive, 종료 코드 0 | 여러 unit을 주면 하나라도 active면 0 을 반환한다. 스크립트에서 한 번에 여러 개를 검사하면 오판할 수 있다 |
| 주제 | 내용 |
|---|---|
| mask로 공격 표면 축소 | 쓰지 않는 서비스(예: rpcbind, cups, avahi-daemon)는 disable만으로는 다른 unit의 의존성으로 켜질 수 있다. mask 가 확실하다 |
| 보안 서비스 mask 공격 | 반대로 공격자가 auditd, EDR 에이전트를 mask하면 재부팅 후에도 켜지지 않는다. /etc/systemd/system/*.service → /dev/null 링크를 점검한다 |
| disabled + active 함정 | 조치 후 stop만 하고 disable하지 않은 악성 unit은 재부팅 시 되살아난다. 반대로 disable만 하면 지금은 계속 돈다 |
| sudo 위임 | sudo systemctl restart nginx처럼 명령과 대상 unit을 한정 해 위임한다. systemctl 전체 위임은 root 위임과 같다 |
[Detection] EDR 하트비트 끊김
↓
[확인] systemctl is-active edr-agent ; systemctl is-enabled edr-agent
→ inactive / masked
↓
[증거] ls -l --time-style=full-iso /etc/systemd/system/edr-agent.service ← mask 링크 생성 시각
journalctl --since "<그 시각 -5min>" -u edr-agent
journalctl _COMM=sudo --since "<그 시각 -5min>"
↓
[Response] systemctl unmask --now ... 복구 → mask 한 계정·세션 조사
| 점검 명령 | 목적 |
|---|---|
systemctl list-unit-files --state=masked | mask된 unit 목록 (보안 서비스 포함 여부) |
systemctl list-unit-files --state=enabled | 부팅 시 시작 목록 (기준선 비교) |
journalctl -g 'Created symlink\|Removed' | enable/disable/mask 흔적 (systemctl 출력이 로그에 남은 경우) |
| 실수 | 결과 | 예방 |
|---|---|---|
| enable만 하고 start 안 함 | 재부팅 전까지 서비스 없음 | enable --now |
| start만 하고 enable 안 함 | 재부팅 후 서비스 사라짐 | 운영 서비스는 enable 확인 |
| 설정 변경에 무조건 restart | 연결 끊김·세션 초기화 | 지원하면 reload |
| 서비스 차단에 disable만 사용 | 의존성으로 다시 시작 | mask |
여러 unit을 is-active 한 번으로 검사 | 하나만 active여도 성공 | unit별로 개별 검사 |
[ ] stop/start 후 프로세스 유무를 확인했다
[ ] restart 전후 MainPID 가 바뀌는 것을 확인했다
[ ] disable/enable 이 *.wants 링크 삭제/생성인 것을 확인했다
[ ] mask 가 /dev/null 링크이고 start 가 거부되는 것을 확인했다
[ ] ExecReload 와 reload 시 SIGHUP 로그를 확인했다
[ ] is-active 에 여러 unit 을 줄 때의 종료 코드 함정을 확인했다
*.wants/ 링크 조작, mask는 /dev/null 링크로 시작 자체를 막는다.is-active는 unit별로 검사한다.다음 글 「24. 서비스 상태 해석 (active·enabled·failed)」 에서는 LoadState, ActiveState, SubState, UnitFileState 네 가지 값으로 서비스 상태를 정확히 읽는 법을 다룬다. 일부러 실패하는 서비스를 만들어 status=203/EXEC 같은 실패 코드를 해석 한다.