systemd path unit 사용법, 파일 감시로 서비스를 띄우려면
systemd path unit은 달력이 아니라 파일·디렉터리 조건이 맞을 때 짝 서비스를 켭니다. 감시 자체는 man systemd.path에 적힌 대로 inotify(7) 위에 올라가며, 기본으로 **같은 이름의 .service**를 활성화합니다. 이 글은 timer OnCalendar 재탕 없이 갈라쓰기·최소 .path+.service·journal 점검만 정리합니다. 제휴·가격·현장 후기는 없습니다.
timer와 path는 언제 갈라쓰나?
한 줄 답: 시각·주기면 .timer(예: OnCalendar=), 경로의 존재·변경·비어 있지 않음이면 .path입니다. 둘 다 “짝 서비스를 대신 시작”하는 활성화 유닛이지만, 트리거 축이 다릅니다.
| 축 | .timer | .path |
|---|---|---|
| 트리거 | 벽시계·모노토닉 일정(systemd.timer) | PathExists= / PathChanged= 등(systemd.path) |
| 대표 질문 | “매일 03:00에?” | “이 파일이 생겼을 때 / 닫힌 뒤 내용이 바뀌었을 때?” |
| 내부 | 타이머fd·캘린더 | inotify — 원격 NFS처럼 inotify가 안 먹는 경로는 문서상 한계 |
| 설치 타깃(기본) | timers.target | paths.target |
짧게만 대비하면, 이전에 다룬 OnCalendar 표현·Persistent= 같은 timer 본문은 여기로 가져오지 않습니다. “언제”가 달력이면 timer, “어디에 무엇이 있나”면 path입니다. 같은 작업이라도 드롭 디렉터리에 파일이 떨어지는 순간이 기준이면 path가 맞고, 파일이 없어도 정해진 시각에 돌아야 하면 timer입니다.
.path+.service 최소 예시는?
한 줄 답: foo.path의 [Path]에 절대 경로 조건 하나 이상, 짝 foo.service는 보통 Type=oneshot으로 두고, **enable은 .path**만 합니다. Unit=을 안 쓰면 접미사만 바꾼 동명 서비스가 기본 대상입니다(man systemd.path).
예시: /var/spool/drop/ready가 존재하면 oneshot을 한 번 돌립니다.
# /etc/systemd/system/drop-ingest.service
[Unit]
Description=Ingest when drop ready file exists
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ingest-drop.sh
# /etc/systemd/system/drop-ingest.path
[Unit]
Description=Watch for /var/spool/drop/ready
[Path]
PathExists=/var/spool/drop/ready
[Install]
WantedBy=paths.target
적용:
sudo systemctl daemon-reload
sudo systemctl enable --now drop-ingest.path
systemctl status drop-ingest.path
조건 고르기(man 요약):
| 지시어 | 의미 |
|---|---|
PathExists= | 파일·디렉터리가 있으면 활성화. 유닛을 켤 때 이미 있으면 즉시도 발동 |
PathExistsGlob= | 글로브에 맞는 항목이 하나라도 있으면 |
PathChanged= | 감시 대상이 바뀌면. 쓰기마다가 아니라, 쓰기 위해 열었던 파일이 닫힐 때 |
PathModified= | PathChanged=와 비슷하되 단순 write에도 |
DirectoryNotEmpty= | 디렉터리에 항목이 하나 이상 있으면(켤 때 이미 비어 있지 않으면 즉시) |
실무 메모:
- 인자는 절대 경로여야 합니다.
MakeDirectory=true면 감시 디렉터리를 먼저 만들 수 있으나,PathExists=에는 무시됩니다(DirectoryMode=기본 0755).- 이름 앞이
.인 숨김 파일은 대체로 무시됩니다. - 서비스가 끝나면 경로를 바로 다시 검사해 재시작할 수 있으므로, busy loop 방지는 서비스 쪽
StartLimitIntervalSec=/StartLimitBurst=와 path 쪽TriggerLimitIntervalSec=/TriggerLimitBurst=(기본 2s / 200, v250+)를 문서대로 둡니다. - 변경 감시면
PathExists대신PathChanged=(또는 write마다면PathModified=)를 고릅니다. “파일 생김”만이면PathExists=가 최소입니다.
안 뜨면 journal에서 뭘 보나?
한 줄 답: path 유닛과 service 유닛을 같이 보고, “감시가 안 됨 / 트리거만 되고 Exec 실패 / rate limit으로 path까지 failed”를 가릅니다.
systemctl status drop-ingest.path drop-ingest.service
journalctl -u drop-ingest.path -u drop-ingest.service -b --no-pager
systemctl list-units --type=path --all
systemctl show drop-ingest.path -p ActiveState -p SubState -p TriggerLimitIntervalUSec -p TriggerLimitBurst
점검 순서:
- path가 active(waiting)인가?
enable --now한 대상이.service가 아니라.path인지,WantedBy=paths.target인지 확인합니다. - 경로·권한: 절대 경로인지, 중간 디렉터리 접근이 막혀 있지 않은지. man은 접근 불가면 권한 변화를 감시하다 조건이 되면 알아챈다고 합니다. 그래도 잘못된 경로면 조건이 영원히 안 맞습니다.
- 조건 종류:
PathExists=인데 파일을 만든 뒤 지웠다면 존재 조건은 더 이상 참이 아닙니다.PathChanged=는 이미 켜질 때 있던 파일의 “과거 변경”을 소급하지 않습니다(존재계열과 다름). - 서비스 로그: path는 떴는데
ExecStart=실패·권한·인터프리터 없음이면 service journal에 남습니다. - 루프·한도: 서비스가 끝나자마자 조건이 또 참이면 재기동이 이어질 수 있습니다. start rate limit 히트는 path 유닛 실패로 전파되어 감시가 멈출 수 있습니다(man systemd.path).
TriggerLimit*에 걸리면 path가 failure로 가며, 재시작 전까지 더 이상 감시하지 않습니다. - inotify 한계: 다른 머신이 올린 NFS 변경 등은 man이 명시한 대로 기대한 대로 안 올 수 있습니다. 로컬 디스크·로컬 tmpfs부터 검증합니다.
트리거된 유닛에는 서비스 매니저가 환경 변수를 넘길 수 있습니다. 세부 이름은 systemd.exec(5)의 “Environment Variables Set or Propagated by the Service Manager”를 봅니다.
자주 묻는 질문
Q. service도 enable 해야 하나요?
일반적으로 path만 enable합니다. timer와 같이, 활성화 유닛이 짝 서비스를 시작합니다. 수동 start 테스트는 systemctl start drop-ingest.service로 분리해 볼 수 있습니다.
Q. PathExists와 PathChanged를 같이 써도 되나요?
man은 동일·이종 지시어를 여러 개 합쳐 여러 경로를 볼 수 있다고 합니다. 빈 문자열을 할당하면 해당 옵션 목록이 리셋됩니다.
Q. 임베디드에서 cron 대신 path만 쓰면 되나요?
주기 배치면 여전히 timer가 맞습니다. 드롭 디렉터리·플래그 파일·소켓 옆 상태 파일처럼 경로 사건이 경계일 때만 path가 단순합니다.
정리
- 달력 → timer, 경로 조건 → path. OnCalendar 본문은 재탕하지 않고 축만 가릅니다.
- 최소 쌍:
PathExists=(또는 Changed/Modified) +Type=oneshot동명 service,enable --now *.path. - 안 뜨면
journalctl -u …path -u …service, 절대 경로·조건 종류·StartLimit/TriggerLimit·inotify 한계 순으로 봅니다.
근거: systemd.path(5), systemd.service(5), systemd.timer(5)(대비만), inotify(7).