Ubuntu 24.04에서 udev 규칙 작성하기 — 장치 심볼릭 링크부터 이벤트 실행까지

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/loop4
DEVPATH=/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-pager
lrwxrwxrwx 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/udevdemo
ls: 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 dummy
monitor 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 lab0
lab0             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

참고

답글 남기기