Udev tutorial 1
Published:
udev rule for my USB driver on Raspberry Pi 4
udev Rules
I am planning to write a udev rule for my USB driver so that when I plug in my USB drive, the udev rules will be able to identify it and insert a kernel loadable module that enables userspace access to the Performance Monitors Cycle Count Register (PMCCNTR_EL0).
ARMv8 Performance Counters
Accessing PMCCNTR requires kernel-level privileges. However, there is a register, PMUSERENR_EL0, that enables user-mode access to cycle counters. By setting bit 0 and bit 2 of PMUSERENR_EL0 to 1 ARM Docs, user-level EL0 can access and read the performance counter. The source code for this kernel module can be found here. ARMv8 Performance Counters management module
Shell script
This shell script will be invoked by udev when a device is detected and matched by the rule
#!/bin/bash
/bin/date >> /tmp/udev.log
echo "Script triggered by udev" >> /tmp/udev.log
# Check if the module file exists
MODULE_PATH="/home/lizeren/Desktop/armv8_pmu_cycle_counter_el0/pmu_el0_cycle_counter.ko"
if [ ! -f "$MODULE_PATH" ]; then
echo "Error: Module $MODULE_PATH not found." >> /tmp/udev.log
exit 1
fi
# Load the kernel module using sudo insmod
echo "Loading kernel module $MODULE_PATH..." >> /tmp/udev.log
sudo insmod "$MODULE_PATH"
# Check if the module was loaded successfully
if lsmod | grep -q "pmu_el0_cycle_counter"; then
echo "Module loaded successfully!" >> /tmp/udev.log
else
echo "Failed to load module." >> /tmp/udev.log
fi
Place this in /usr/local/bin or some such place in the default executable path. Call it trigger.sh and, of course, make it executable with chmod +x.
$ sudo mv trigger.sh /usr/local/bin
$ sudo chmod +x /usr/local/bin/trigger.sh
Notice that the debugging prints are redirected to /tmp/udev.log, so the script logs when it has been successfully triggered. The usual location for log files is the /var directory, but that is primarily the root user’s domain. For testing purposes, use /tmp, which is accessible to normal users and is typically cleaned out upon reboot.
Define udev rules
Udev identifies devices by serial numbers, manufacturers, vendor ID and product ID numbers etc. In our case, my usb is Kingston DataTraveler 3.0 so i use lsusb for displaying information about USB buses in the system and the devices connected to them.
$ lsusb
sda 8:0 1 28.9G 0 disk
└─sda1 8:1 1 28.9G 0 part /media/lizeren/ESD-USB
Or, unplug and plug your thumb drive back in, then immediately issue this command:
$ sudo dmesg | tail
[16036.796284] usb-storage 2-7:1.0: USB Mass Storage device detected
[16036.796712] scsi host4: usb-storage 2-7:1.0
[16037.825918] scsi 4:0:0:0: Direct-Access Kingston DataTraveler 3.0 PMAP PQ: 0 ANSI: 6
[16037.826560] sd 4:0:0:0: Attached scsi generic sg0 type 0
[16037.827049] sd 4:0:0:0: [sda] 60555264 512-byte logical blocks: (31.0 GB/28.9 GiB)
[16037.827309] sd 4:0:0:0: [sda] Write Protect is off
[16037.827313] sd 4:0:0:0: [sda] Mode Sense: 45 00 00 00
[16037.827497] sd 4:0:0:0: [sda] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[16038.111915] sda: sda1
[16038.125991] sd 4:0:0:0: [sda] Attached SCSI removable disk
you know the kernel has assigned your thumb drive the sda label. More specifically, sda1 Now, it is time to use udevadm info to pick out parts of udev’s report about a device that are most unique to that device, then tell udev to trigger your script when those unique attributes are detected.
$ udevadm info -a -n /dev/sda1
or
$ udevadm info --attribute-walk --name=/dev/sda1
The udevadm info process reports on a device (specified by the device path), then “walks” up the chain of parent devices. For every device found, it prints all possible attributes using a key-value format. You can compose a rule to match according to the attributes of a device plus attributes from one single parent device.
looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0/4:0:0:0/block/sda/sda1':
KERNEL=="sda1"
SUBSYSTEM=="block"
DRIVER==""
ATTR{discard_alignment}=="0"
ATTR{ro}=="0"
ATTR{alignment_offset}=="0"
ATTR{size}=="60553216"
ATTR{partition}=="1"
A udev rule must contain one attribute from one single parent device.
Parent attributes are things that describe a device from the most basic level, such as it’s something that has been plugged into a physical port or it is something with a size or this is a removable device. This is sda1’s parent attributes. you notice any difference?
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0/4:0:0:0/block/sda':
KERNELS=="sda"
SUBSYSTEMS=="block"
DRIVERS==""
ATTRS{ext_range}=="256"
ATTRS{capability}=="51"
ATTRS{alignment_offset}=="0"
ATTRS{events}=="media_change"
ATTRS{inflight}==" 0 0"
ATTRS{size}=="60555264"
ATTRS{discard_alignment}=="0"
ATTRS{hidden}=="0"
ATTRS{removable}=="1"
ATTRS{ro}=="0"
ATTRS{range}=="16"
ATTRS{events_async}==""
ATTRS{events_poll_msecs}=="-1"
Te key of Parent attributes are all plural. Below is how udevadm info -a -n /dev/sda1 outputs the hierarchy
looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0/4:0:0:0/block/sda/sda1':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0/4:0:0:0/block/sda':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0/4:0:0:0':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4/target4:0:0':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0/host4':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7/2-7:1.0':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-7':
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2':
looking at parent device '/devices/pci0000:00/0000:00:14.0':
looking at parent device '/devices/pci0000:00':
Now the rule for identifying my Kingston DataTraveler 3.0 is simple. Open a file called 80-local.rules in /etc/udev/rules.d and enter this code:
SUBSYSTEM=="block",KERNEL == "sda1" ,ATTRS{vendor}=="Kingston",ACTION=="add", RUN+="/usr/local/bin/trigger.sh"
Theoretically, you need sudo udevadm control –reload-rules to activate the rule. But I found out in my system, the rules are automatically applied.
Testing
The easiest way to test is by unplugging and plugging your USB device back in, then checking the log file.
$ cat /tmp/udev.log
Wed Sep 11 12:05:29 EDT 2024
Script triggered by udev
Module loaded successfully!
If you are lazy and don’t want to unplug-plug, you can do
sudo udevadm trigger --action=add --subsystem-match=block --property-match=DEVNAME=/dev/sda1
This command manually triggers a udev event for a specific block device (/dev/sda1
) by simulating the addition of that device. Here’s a breakdown of the options used:
sudo
: Runs the command with superuser privileges, which is necessary since udev-related tasks typically require elevated permissions.udevadm trigger
: This command is used to manually trigger events that the udev system handles automatically when hardware changes occur (such as adding or removing devices). Here, we are using it to simulate the addition of a device.--action=add
: Specifies the action to simulate. In this case,add
simulates the event as if the block device/dev/sda1
was just plugged in.--subsystem-match=block
: Filters the event so that it only applies to devices in theblock
subsystem, which includes block devices such as hard drives, USB drives, and partitions.--property-match=DEVNAME=/dev/sda1
: Specifies the exact device to match based on its device name. In this case, it targets the block device located at/dev/sda1
, which might represent a specific partition of a hard drive or a USB device.
Debuging Tips
When I wrote and tested this rule, sometimes the rule would be triggered twice. To find more detailed information, you can increase the verbosity of the udev logs temporarily by setting the log level to:
sudo udevadm control --log-priority=debug
Then check the system logs (usually /var/log/syslog
or journalctl
) for detailed output:
sudo journalctl -f
Here is my log Link to GitHub. You can search for RUN '/usr/local/bin/trigger.sh'
in the log, and you’ll find two results. The /usr/local/bin/trigger.sh
script is being called twice because it is triggered by two different devices: sda
and sda1
. These are two distinct device nodes:
sda
refers to the entire USB drive (the physical device).sda1
refers to the first partition on that USB drive.
This is why the rule uses KERNEL=="sda1"
to exclude matching sda
.
After you’re done, remember to reset the log level:
sudo udevadm control --log-priority=info
Reference
https://opensource.com/article/18/11/udev https://linux.die.net/man/8/udevadm