udev ACTION=add vs change — 규칙이 두 번 도는 이유

USB·시리얼·블록 장치를 꽂으면 udev 규칙의 RUN·PROGRAM이 두 번 도는 경우가 있습니다. 원인은 대체로 ACTION을 좁히지 않은 채 add와 change(또는 bind)에 모두 매칭되기 때문입니다. 이 글은 add와 change 차이 · 중복 실행 막기 · udevadm monitor로 확인만 다룹니다.

근거는 udev(7)의 ACTION 매치 키와 udevadm(8)의 이벤트 액션 목록(add / remove / change / move / online / offline / bind / unbind)입니다. 제휴·C++ 예제는 없습니다.

add와 change 차이는?

한 줄 답: ACTION은 그 이벤트의 액션 이름과 매칭합니다. add는 장치가 나타날 때, change는 장치 상태가 바뀌었을 때(드라이버·커널이 올리는 갱신, udevadm trigger 기본값 등)입니다. 둘은 서로 다른 이벤트입니다.

udev(7)은 커널이 장치를 추가·제거하거나 상태가 바뀔 때 uevent를 받고, 규칙의 매치 키로 걸러 적용한다고 설명합니다. ACTION 키 설명은 짧게 「Match the name of the event action.」 입니다.

udevadm(8)이 나열하는 대표 액션:

ACTION실무에서 자주 보는 의미
add장치가 시스템에 나타남(/dev 노드 생성 흐름과 함께 오는 경우가 많음)
remove장치가 사라짐
change장치 상태 갱신. 드라이버가 올리는 경우가 많고, udevadm trigger 기본 액션도 change
bind / unbind드라이버가 장치에 붙거나 떨어짐
move / online / offline이름 변경·핫플러그 잠금/해제 등 특수 케이스

임베디드에서 흔한 패턴:

  • 케이블을 꽂으면 add 가 먼저(또는 함께) 옵니다.
  • 펌웨어·속성·권한이 갱신되거나, 누군가가 udevadm trigger를 돌리면 change 가 또 옵니다.
  • 물리 장치에서는 bind/unbind 도 이어질 수 있습니다.

규칙을 SUBSYSTEM=="usb", ATTR{idVendor}=="…"처럼만 쓰면 액션이 달라도 매번 매칭됩니다. SYMLINK·MODE처럼 멱등한 할당은 여러 번 돌아도 보통 괜찮지만, RUN+=로 스크립트를 돌리거나 로그·카운터·외부 서비스를 건드리면 “두 번 실행”으로 보입니다.

udevadm test의 기본 시뮬레이션 액션은 add, udevadm trigger의 기본은 change 입니다. 테스트와 실제 트리거가 다른 액션을 쓰는 점도 혼동 지점입니다.

중복 실행을 막으려면?

한 줄 답: 부작용이 있는 RUN/PROGRAM은 ACTION=="add"(또는 필요한 액션만)로 좁힙니다. 속성·태그·심볼릭 링크처럼 이벤트마다 다시 쌓아야 하는 규칙은 add만으로 가두지 말고, 제거 시에만 건너뛰는 쪽이 systemd 쪽 권고에 가깝습니다.

1) 한 번만 돌릴 작업 → ACTION=="add"

# /etc/udev/rules.d/99-my-usb-once.rules
# 꽂을 때(add)만 스크립트 실행 — change/bind에서 재실행 방지
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", \
  RUN+="/usr/local/bin/on-usb-plug.sh"

remove 정리 스크립트가 필요하면 별도 줄로 ACTION=="remove"를 둡니다. add|change에 한꺼번에 RUN을 걸면 의도한 “한 번”이 깨집니다.

2) 속성·MODE·TAG는 매 이벤트에 다시 적용

권한·ENV·TAG는 change·bind에서도 다시 맞춰 주는 편이 안전합니다. systemd 247 전후 안내에서도, 예전 헤더 가드 ACTION!="add|change", GOTO="…_end" 대신 ACTION=="remove", GOTO="…_end" 로 바꿔 bind/unbind에서도 속성이 쌓이게 하라고 권합니다. add만 허용하면 bind 때 태그가 빠질 수 있습니다.

# 속성/권한: remove만 스킵 (예시 골격)
ACTION=="remove", GOTO="mydev_end"

SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", \
  MODE="0660", GROUP="dialout", TAG+="uaccess"

LABEL="mydev_end"

3) 역할 분리 체크리스트

규칙 종류ACTION 전략
RUN+= 스크립트·외부 알림보통 ACTION=="add" (필요 시 remove 별도)
SYMLINK / MODE / OWNER / ENV / TAGremove만 제외하거나 액션 제한 없이, 멱등하게
udevadm trigger로 재적용 검증기본이 change이므로, add-only RUN은 trigger만으로는 안 돌 수 있음

4) 재로드

sudo udevadm control --reload-rules
sudo udevadm trigger   # 기본 ACTION=change — add-only RUN은 기대하지 말 것

trigger로 “스크립트가 또 도는지”를 보면, add-only 규칙에서는 의도적으로 안 도는 것이 정상입니다. 재현은 실제 플러그 또는 udevadm test --action=add로 확인합니다.

팁: “규칙이 두 번”인지 “커널이 add+change(+bind)를 연쇄로 올리는지”를 먼저 나눕니다. 후자면 규칙을 좁히는 것이 답이고, 전자면 파일 중복·IMPORT/PROGRAM 재진입을 의심합니다.

udevadm monitor로 확인은?

한 줄 답: 장치를 꽂고 빼는 동안 udevadm monitor --environment --udev(필요하면 --kernel)로 같은 DEVPATH에 ACTION=add와 ACTION=change가 잇따르는지 봅니다.

# 터미널 1 — udev 처리 결과(규칙 적용 후) 이벤트
sudo udevadm monitor --environment --udev

# 터미널 2 — 대상 장치 연결/해제, 또는
# sudo udevadm trigger -c add /sys/…   # 특정 액션으로 재현

볼 포인트:

  1. 동일 DEVPATH(또는 DEVNAME)에 ACTION만 다른 줄이 연속되는지.
  2. SEQNUM이 증가하는지(별개 이벤트인지).
  3. SUBSYSTEM / ID_VENDOR_ID / ID_MODEL_ID 등 규칙 매치에 쓰는 키가 기대한 값인지.
  4. --kernel과 --udev를 나누어 보면, 커널 uevent와 udev 처리 결과를 구분하기 쉽습니다.

디버그용 한 줄 시뮬레이션:

sudo udevadm test --action=add /sys/devices/…/tty/ttyUSB0
sudo udevadm test --action=change /sys/devices/…/tty/ttyUSB0

test는 실제로 RUN을 실행하지 않고 어떤 규칙이 매칭될지를 보여 줍니다. add와 change에서 RUN 후보가 둘 다 나오면, 규칙에 ACTION==이 빠졌을 가능성이 큽니다.

주의: OPTIONS+="watch"가 켜진 노드를 쓰기 모드로 열었다가 닫으면 change uevent가 합성될 수 있습니다(udev(7) watch). “아무도 trigger 안 했는데 change가 또 옴”일 때 후보입니다.

자주 묻는 질문

ACTION=="add|change"로 묶으면 안 되나?
속성·심볼릭 링크처럼 멱등한 할당에는 쓸 수 있습니다. 다만 RUN 중복을 막으려는 목적에는 보통 반대입니다. bind까지 커버하려면 remove-only 스킵 가드가 더 맞습니다.

udevadm trigger 후에도 스크립트가 안 돈다.
trigger 기본 액션은 change 입니다. 규칙이 ACTION=="add"이면 정상적으로 스킵됩니다. --action=add를 주거나 실제 플러그를 사용합니다.

add만 허용하면 권한이 빠질 때가 있다.
bind 이후에야 드라이버·속성이 완전해지는 장치가 있습니다. MODE/TAG는 remove만 제외하고, RUN만 add로 제한하는 식의 역할 분리가 안전합니다.

규칙 파일을 고쳤는데 반영이 안 된다.
udevadm control --reload-rules 후, add-only는 플러그 또는 test --action=add로 확인합니다. 파일 확장자는 반드시 .rules입니다.

정리

ACTION은 이벤트 종류 이름입니다. add는 등장, change는 상태 갱신(그리고 trigger 기본값)입니다. 부작용 있는 RUN은 ACTION=="add"로 한 번만, 속성·태그는 remove만 건너뛰고 여러 액션에서 멱등 적용. 재현·증명은 udevadm monitor --environment 로 같은 장치에 add/change가 연속되는지 보는 것입니다. 근거: udev(7), udevadm(8). 제휴 없음.

공식 문서는 어디인가?

  • udev(7) — ACTION 매치 키, RUN, OPTIONS=watch
  • udevadm(8) — monitor, trigger --action=, test --action= (add/remove/change/move/online/offline/bind/unbind)
  • udev — ArchWiki — 액션 유형 요약(보조 설명)