USB 디스크를 두 개 꽂으면 /dev/sdb와 /dev/sdc가 부팅마다 뒤바뀐다. 특정 시리얼 장치에만 권한을 열어주고 싶은데 chmod는 재부팅하면 원래대로 돌아간다. 둘 다 udev 규칙으로 푸는 문제다. udev는 커널이 올린 uevent를 받아 /dev 아래 노드를 만들고, 규칙 파일에 적힌 대로 심볼릭 링크·권한·환경 변수를 붙이고 필요하면 명령까지 실행한다.
이 글에서는 Ubuntu 24.04(systemd 255) 환경에서 loop 장치와 dummy 네트워크 인터페이스를 대상으로 규칙을 직접 작성하고, 규칙이 안 먹을 때 어디를 봐야 하는지까지 정리한다. 실제 장치를 뽑았다 꽂지 않아도 되므로 그대로 따라 할 수 있다.
실습용 장치 만들기
# 32MB 이미지 파일을 loop 장치로 붙인다
sudo mkdir -p /root/udevwork
sudo dd if=/dev/zero of=/root/udevwork/disk.img bs=1M count=32 status=none
sudo losetup -f --show /root/udevwork/disk.img/dev/loop4
규칙을 쓰기 전에 이 장치가 어떤 키로 매칭 가능한지부터 확인한다.
sudo udevadm info -q property -n /dev/loop4DEVPATH=/devices/virtual/block/loop4
DEVNAME=/dev/loop4
DEVTYPE=disk
DISKSEQ=18
MAJOR=7
MINOR=4
SUBSYSTEM=block
USEC_INITIALIZED=1455170
SYSTEMD_READY=0
DEVLINKS=/dev/disk/by-diskseq/16
TAGS=:systemd:
CURRENT_TAGS=:systemd:
udevadm info -a는 부모 장치까지 거슬러 올라가며 ATTR{} 키를 전부 보여준다. 규칙은 대상 장치 자신의 속성과 부모 장치 하나의 속성만 조합할 수 있다는 제약이 여기 안내문에 적혀 있다.
looking at device '/devices/virtual/block/loop4':
KERNEL=="loop4"
SUBSYSTEM=="block"
DRIVER==""
ATTR{alignment_offset}=="0"
ATTR{capability}=="0"
ATTR{diskseq}=="18"
ATTR{events}=="media_change"
ATTR{ext_range}=="256"
ATTR{hidden}=="0"
ATTR{loop/autoclear}=="0"
ATTR{loop/dio}=="0"
ATTR{loop/offset}=="0"
파일시스템을 만들면 blkid 기반 속성이 추가로 붙는다. 실무에서 쓰는 매칭 키는 대부분 이쪽이다.
sudo mkfs.ext4 -q -L udevdemo /dev/loop4
sudo udevadm settle
sudo udevadm info -q property -n /dev/loop4 | grep -E "^ID_FS|^DEVLINKS"ID_FS_LABEL=udevdemo
ID_FS_LABEL_ENC=udevdemo
ID_FS_UUID=57cf3aae-4099-40f4-b683-973560eef110
ID_FS_UUID_ENC=57cf3aae-4099-40f4-b683-973560eef110
ID_FS_VERSION=1.0
ID_FS_BLOCKSIZE=4096
ID_FS_LASTBLOCK=8192
ID_FS_SIZE=33554432
ID_FS_TYPE=ext4
ID_FS_USAGE=filesystem
DEVLINKS=/dev/disk/by-diskseq/18 /dev/disk/by-label/udevdemo /dev/disk/by-loop-ref/\x2froot\x2fudevwork\x2fdisk.img /dev/disk/by-loop-inode/8:2-1572873 /dev/disk/by-uuid/57cf3aae-4099-40f4-b683-973560eef110
/dev/disk/by-label/와 by-uuid/ 링크는 배포판 기본 규칙(60-persistent-storage.rules)이 이미 붙여준 것이다.
첫 규칙 — 심볼릭 링크와 권한
SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="udevdemo", \
SYMLINK+="udevdemo", GROUP="disk", MODE="0660", \
RUN+="/usr/bin/logger -t udevdemo device %k uuid=%E{ID_FS_UUID}"| 문법 | 의미 |
|---|---|
== | 매칭 조건. 하나라도 안 맞으면 그 줄은 건너뛴다 |
= | 값 대입 (MODE, GROUP, NAME 등) |
+= | 목록에 추가 (SYMLINK, RUN, TAG 등) |
%k | 커널이 붙인 장치 이름 (loop4) |
%E{키} | udev 속성 값. ENV{키}와 같은 값을 명령줄에서 참조할 때 쓴다 |
파일 이름 앞의 숫자는 실행 순서다. 배포판 기본 규칙은 /usr/lib/udev/rules.d/에 있고, /etc/udev/rules.d/에 같은 이름을 두면 덮어쓴다. 기본 규칙이 붙인 속성을 참조하려면 그보다 뒤 번호를 써야 한다.
sudo udevadm control --reload
sudo udevadm trigger --action=change --sysname-match=loop4
sudo udevadm settle
ls -l /dev/udevdemo /dev/loop4
journalctl -t udevdemo -n 1 --no-pagerlrwxrwxrwx 1 root root 5 Aug 23 16:50 /dev/udevdemo -> loop4
brw-rw---- 1 root disk 7, 4 Aug 23 16:50 /dev/loop4
Aug 23 16:50:01 noble udevdemo[26462]: device loop4 uuid=57cf3aae-4099-40f4-b683-973560eef110
규칙 파일을 고친 뒤에는 udevadm control --reload로 규칙을 다시 읽히고, 이미 존재하는 장치에는 trigger로 이벤트를 다시 쏴야 적용된다. 새로 꽂는 장치라면 이 두 단계가 필요 없다.
규칙이 안 먹을 때
udevadm test는 실제 이벤트 없이 규칙 매칭 과정을 그대로 재현한다. 어느 파일 몇 번째 줄이 무엇을 했는지가 전부 찍힌다.
sudo udevadm test /sys/block/loop4 2>&1 | grep -nE "udevdemo"151:Reading rules file: /etc/udev/rules.d/99-udevdemo.rules
159:loop4: /usr/lib/udev/rules.d/60-persistent-storage.rules:141 Added SYMLINK 'disk/by-label/udevdemo'
167:loop4: /etc/udev/rules.d/99-udevdemo.rules:3 GROUP 6
168:loop4: /etc/udev/rules.d/99-udevdemo.rules:3 MODE 0660
169:loop4: /etc/udev/rules.d/99-udevdemo.rules:3 RUN '/usr/bin/logger -t udevdemo device %k uuid=%E{ID_FS_UUID}'
203:DEVLINKS=/dev/disk/by-loop-ref/\x2froot\x2fudevwork\x2fdisk.img /dev/disk/by-uuid/57cf3aae-4099-40f4-b683-973560eef110 /dev/udevdemo /dev/disk/by-loop-inode/8:2-1572873 /dev/disk/by-diskseq/18 /dev/disk/by-label/udevdemo
206:run: '/usr/bin/logger -t udevdemo device loop4 uuid=57cf3aae-4099-40f4-b683-973560eef110'
키 이름에 오타(ID_FS_LABEEL)를 내고 같은 절차를 밟으면 매칭 라인이 사라지고 심볼릭 링크도 생기지 않는다. 문법 오류가 아니라 “존재하지 않는 속성과 비교”이므로 에러 없이 조용히 실패한다는 점이 함정이다.
sudo sed -i "s/ID_FS_LABEL/ID_FS_LABEEL/" /etc/udev/rules.d/99-udevdemo.rules
sudo udevadm control --reload
sudo udevadm trigger --action=change --sysname-match=loop4
sudo udevadm settle
ls -l /dev/udevdemols: cannot access '/dev/udevdemo': No such file or directory
한 가지 더. udevadm test의 마지막 줄에 run: 이 찍혀도 그 명령이 실제로 실행되지는 않는다. 위 logger 규칙으로 확인한 결과다.
### journal 마지막 udevtest 항목 (test 실행 전)
Aug 23 16:53:28 noble udevtest[27030]: hello-from-run
### udevadm test 실행 후
Aug 23 16:53:28 noble udevtest[27030]: hello-from-run
### udevadm trigger 실행 후
Aug 23 16:53:28 noble udevtest[27030]: hello-from-run
Aug 23 16:53:32 noble udevtest[27043]: hello-from-run
이벤트 흐름 관찰하기
udevadm monitor는 커널 uevent와 udev가 규칙 처리를 끝낸 뒤 내보내는 이벤트를 나란히 보여준다.
sudo udevadm monitor --subsystem-match=net &
sudo ip link add dummy0 type dummymonitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent
KERNEL[9338.897293] add /devices/virtual/net/dummy0 (net)
UDEV [9338.910846] add /devices/virtual/net/dummy0 (net)
두 줄 사이의 13ms가 udev가 규칙을 돌린 시간이다. UDEV 줄이 안 나오면 규칙 처리 중 멈춘 것이므로 systemd-udevd 로그를 봐야 한다.
네트워크 인터페이스 이름 고정
SUBSYSTEM=="net", ACTION=="add", ENV{ID_NET_DRIVER}=="dummy", NAME="lab0"sudo udevadm control --reload
sudo ip link del dummy0
sudo ip link add dummy0 type dummy
ip -br link show lab0lab0 DOWN 96:bd:15:2b:ad:2d <BROADCAST,NOARP>
NAME=은 네트워크 장치에만 쓸 수 있고, 블록·문자 장치의 이름을 바꾸려는 시도는 무시된다. 실제 NIC라면 드라이버 대신 ATTR{address}(MAC)나 KERNELS==(PCI 경로)로 매칭하는 쪽이 안전하다.
주의사항
| 항목 | 내용 |
|---|---|
| 장치 이름 변경 | NAME=은 네트워크 인터페이스 전용이다. 블록 장치는 이름을 바꾸지 말고 SYMLINK+=로 별칭을 만든다. |
RUN+=의 제약 | udev 이벤트 처리 안에서 동기적으로 실행되므로 오래 걸리는 작업을 넣으면 이벤트 처리가 밀린다. 서비스 기동 같은 작업은 TAG+="systemd" 후 유닛의 BindsTo=나 systemd-run으로 분리한다. |
udevadm test | 규칙 매칭만 재현하고 RUN 명령은 실행하지 않는다. 실제 동작 확인은 trigger로 해야 한다. |
| 조용한 실패 | 속성 이름 오타는 에러가 아니라 “매칭 실패”로 처리된다. 규칙이 안 먹으면 문법부터 의심하지 말고 udevadm test로 매칭 여부를 먼저 본다. |
| 규칙 적용 시점 | 이미 열려 있는 장치의 노드 권한은 trigger 뒤에도 이미 열린 fd에는 반영되지 않는다. 데몬이 물고 있는 장치라면 재시작이 필요하다. |
| 부모 속성 | 하나의 규칙에서 참조할 수 있는 부모 장치는 하나뿐이다. 여러 계층의 ATTRS{}를 섞어 쓰면 매칭되지 않는다. |
마무리
udev 규칙은 문법이 단순한 대신 실패가 조용하다. 그래서 udevadm info로 매칭 키를 확인하고, udevadm test로 매칭을 검증하고, trigger로 실제 실행을 확인하는 세 단계를 습관으로 굳혀두는 편이 빠르다. loop 장치와 dummy 인터페이스는 실제 하드웨어 없이 이 과정을 반복 연습하기에 좋은 대상이다.
실습에 쓴 리소스는 아래로 정리한다.
sudo rm -f /etc/udev/rules.d/99-udevdemo.rules /etc/udev/rules.d/99-netdemo.rules
sudo udevadm control --reload
sudo ip link del lab0
sudo losetup -d /dev/loop4
sudo rm -rf /root/udevwork