Udev tutorial 1

11 minute read

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 the block 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