Vấn đề thực tế: Thiết bị đổi tên sau mỗi lần reboot
Bạn đã bao giờ gặp cảnh này chưa: server có 2 card mạng, hôm nay eth0 là card WAN, reboot xong thì eth0 lại thành card LAN? Hoặc cắm USB scanner vào thì phải ngồi kiểm tra lại xem nó được gán /dev/ttyUSB0 hay /dev/ttyUSB1?
Không phải bug. Linux gán tên thiết bị theo thứ tự kernel phát hiện — không phải thứ tự vật lý, không phải thứ tự bạn cắm vào. Thứ tự đó thay đổi sau mỗi lần reboot, mỗi lần kernel update, đôi khi chỉ cần rút ra cắm lại cũng đủ xáo trộn.
udev sinh ra để fix chính xác vấn đề này. Bạn viết rules để khai báo: thiết bị nào đặt tên gì, permission thế nào, script nào chạy khi cắm vào — udev lo phần còn lại.
So sánh các cách tiếp cận
Linux có mấy cách xử lý bài toán đặt tên thiết bị. So sánh nhanh để chọn đúng tool cho đúng tình huống:
Cách 1: Đặt tên thủ công trong /etc/network/interfaces hoặc Netplan
Với Ubuntu/Debian, bạn có thể hardcode tên interface trong file cấu hình mạng. Nhưng cách này chỉ giải quyết được phần network — không áp dụng cho USB, serial port, hay các thiết bị khác. Và nếu bạn hoán đổi vị trí card vật lý, toàn bộ cấu hình lại bị sai ngay.
Cách 2: Dùng systemd-networkd với MACAddress matching
systemd-networkd hỗ trợ match thiết bị theo MAC address và đặt tên alias. Hoạt động ổn cho network interface, nhưng không xử lý được các loại thiết bị khác và cú pháp phức tạp hơn udev một chút.
Cách 3: udev rules — cách Linux làm đúng nhất
udev là layer mà kernel Linux dùng để quản lý thiết bị ngay từ đầu. Nó hoạt động ở cấp thấp hơn, áp dụng được cho mọi loại thiết bị (network, USB, block device, serial…), và có thể kích hoạt script tự động. Nói ngắn gọn: bạn đang dùng đúng chỗ nó được thiết kế ra.
| Tiêu chí | Manual config | systemd-networkd | udev rules |
|---|---|---|---|
| Loại thiết bị hỗ trợ | Network only | Network only | Tất cả |
| Chạy script tự động | Không | Hạn chế | Có |
| Ổn định sau reboot | Phụ thuộc kernel | Tốt | Rất tốt |
| Học curve | Thấp | Trung bình | Trung bình |
udev hoạt động như thế nào
Cắm thiết bị vào → kernel phát ra một uevent. systemd-udevd nhận signal đó, duyệt qua các file rules trong /etc/udev/rules.d/ và /lib/udev/rules.d/, tìm rule nào khớp, rồi chạy các action tương ứng. Toàn bộ quá trình diễn ra trong vài millisecond — bạn cắm vào là nó đã xử lý xong.
Rules được đọc theo thứ tự tên file (số thứ tự ở đầu tên). File trong /etc/udev/rules.d/ override file cùng tên trong /lib/udev/rules.d/.
Lấy thông tin thiết bị trước khi viết rules
Trước khi viết rule, bạn cần biết thuộc tính của thiết bị để match. Dùng lệnh udevadm:
# Xem thông tin thiết bị theo đường dẫn /sys
udevadm info -a -p /sys/class/net/enp3s0
# Hoặc theo /dev path
udevadm info -a -p $(udevadm info -q path -n /dev/ttyUSB0)
# Test rule mà không cần reload
udevadm test /sys/class/net/enp3s0
Output sẽ hiện các thuộc tính như ATTRS{idVendor}, ATTRS{idProduct}, KERNELS, SUBSYSTEMS… Đây là các key để viết điều kiện match trong rule.
Cấu trúc của một udev rule
Mỗi rule là một dòng, gồm các cặp KEY=="value" (điều kiện match) và KEY="value" (action):
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:dd:ee:ff", NAME="wan0"
Quy tắc đơn giản: == là so sánh, = là gán. Dùng sai là rule im lặng không chạy — không có error message nào báo cho bạn biết.
Case 1: Đặt tên interface mạng cố định theo MAC address
Mình quản lý một server Ubuntu 22.04 production với 2 card mạng. Sau một lần kernel update, thứ tự của 2 interface bị đảo, firewall rules trỏ nhầm card — mất khoảng 20 phút debug mới ra. Từ đó mình dùng udev rules để lock cứng tên.
Bước 1: Lấy MAC address của từng card:
ip link show
# hoặc
cat /sys/class/net/enp*/address
Bước 2: Tạo file rule:
sudo nano /etc/udev/rules.d/10-network-names.rules
# Card WAN (kết nối ISP)
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:11:22:33", NAME="wan0"
# Card LAN (kết nối switch nội bộ)
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:44:55:66", NAME="lan0"
Bước 3: Cập nhật cấu hình mạng để dùng tên mới (wan0, lan0 thay vì enp3s0), rồi reload udev và reboot:
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo reboot
Sau đó dù reboot bao nhiêu lần, hoán đổi slot PCIe, hay thay kernel — tên interface vẫn giữ nguyên.
Case 2: Tự động chạy script khi cắm USB
Tình huống thực tế: bạn có một USB backup drive, mỗi khi cắm vào server thì muốn tự động mount và chạy rsync. Hoặc đơn giản hơn: log lại mọi USB được cắm vào để audit bảo mật.
Xác định vendor/product ID của USB
# Cắm USB vào rồi chạy
lsusb
# Output: Bus 001 Device 003: ID 0781:5581 SanDisk Corp. Ultra
# 0781 = idVendor, 5581 = idProduct
# Hoặc dùng udevadm để xem chi tiết hơn
udevadm info -a -p $(udevadm info -q path -n /dev/sdb) | grep -E 'idVendor|idProduct|serial'
Viết rule kích hoạt script
sudo nano /etc/udev/rules.d/90-usb-backup.rules
# Khi cắm USB SanDisk cụ thể vào
ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", \
ATTRS{idVendor}=="0781", ATTRS{idProduct}=="5581", \
RUN+="/usr/local/bin/usb-backup.sh"
Script /usr/local/bin/usb-backup.sh:
#!/bin/bash
# Script chạy bởi udev — KHÔNG có interactive terminal
# udev chạy với môi trường tối giản, không có PATH đầy đủ
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LOGFILE="/var/log/usb-backup.log"
MOUNT_POINT="/mnt/usb-backup"
echo "$(date '+%Y-%m-%d %H:%M:%S') - USB backup drive detected" >> "$LOGFILE"
mkdir -p "$MOUNT_POINT"
sleep 2 # Chờ kernel hoàn tất nhận diện
mount /dev/sdb1 "$MOUNT_POINT" >> "$LOGFILE" 2>&1
if mountpoint -q "$MOUNT_POINT"; then
rsync -av --delete /home/user/important/ "$MOUNT_POINT/backup/" >> "$LOGFILE" 2>&1
echo "$(date '+%Y-%m-%d %H:%M:%S') - Backup completed" >> "$LOGFILE"
fi
sudo chmod +x /usr/local/bin/usb-backup.sh
Lưu ý quan trọng: Script chạy bởi udev không có terminal, không có PATH đầy đủ, và chạy dưới root. Quên export PATH hoặc dùng đường dẫn tương đối là script chết im lìm — không error message, không log, không gì cả.
Case 3: Đặt permission và symlink cố định cho thiết bị serial
Nếu bạn dùng Arduino, RS485 converter, hay GPS receiver qua USB — chúng thường được gán /dev/ttyUSB0 hoặc /dev/ttyUSB1 ngẫu nhiên. Rule này tạo symlink cố định để code của bạn không cần sửa mỗi lần cắm lại:
sudo nano /etc/udev/rules.d/99-usb-serial.rules
# Arduino Uno
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0043", \
SYMLINK+="arduino", MODE="0666"
# GPS Receiver
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", \
SYMLINK+="gps0", GROUP="dialout", MODE="0664"
Sau khi cắm Arduino, bạn sẽ có /dev/arduino trỏ đến đúng thiết bị, dù kernel gán nó là ttyUSB0 hay ttyUSB3.
Debug khi rule không hoạt động
udev rules hay fail vì hai lý do: nhầm = với ==, hoặc điều kiện match không khớp với thuộc tính thực của thiết bị. Đây là các lệnh cần biết:
# Xem log udev realtime
journalctl -f -u systemd-udevd
# Test rule với thiết bị cụ thể (không thực thi, chỉ simulate)
udevadm test /sys/class/net/enp3s0 2>&1 | grep -E 'IMPORT|RUN|NAME'
# Monitor event khi cắm thiết bị
udevadm monitor --environment --udev
# Force trigger lại event cho thiết bị
udevadm trigger --action=add /sys/bus/usb/devices/1-1
Reload rules sau mỗi lần chỉnh sửa:
sudo udevadm control --reload-rules && sudo udevadm trigger
Tóm lại: nên dùng udev khi nào
Dùng udev khi bạn cần: tên interface mạng cố định theo MAC, symlink ổn định cho USB/serial, hoặc script tự chạy khi cắm thiết bị. Nếu chỉ rename một network interface đơn giản trên Ubuntu 22.04+, /etc/systemd/network/*.link cũng đủ dùng — nhưng ngay khi có thêm thiết bị nào khác vào bài toán, udev là lựa chọn linh hoạt duy nhất.
Sau khi viết xong, nhớ test kỹ trước khi lên production. Dùng udevadm test để simulate, journalctl -f -u systemd-udevd để theo dõi realtime. Và quan trọng nhất: backup cấu hình mạng trước — nếu tên interface thay đổi mà bạn chưa cập nhật Netplan hoặc ifupdown, kết nối SSH sẽ đứt ngay sau reboot.

