Listening to devices with libudev - usairb devlog #1

This is a development log of usairb, a project I’m building to learn embedded Linux. The goal is to transform any embedded Linux device with access to the Internet into a multiplexing transmitter for USB hubs.

See other posts here:


First development day. Added an empty CHANGELOG and a README, and started this development log. See the code at the time of writing if you’d like to follow along.

Introduction #

I wrote this wishful piece for the README:

The goal of usairb (Universal Serial Air-Bus) is to transform any embedded Linux device with access to the Internet into a multiplexing transmitter for USB hubs: connect gadgets to it and use them remotely from your desktop.

A quick tech overview:

To achieve this, usairb uses USB/IP. USB/IP follows a server-client architecture where the server or host is the device broadcasting its USB gadgets, and the client can connect to them. USB/IP is available as a native Kernel module on Linux for the host, and has multi-platform client programs.

And a justification:

While all planned features of usairb are achievable using just USB/IP, usairb aims to provide a no-frills experience, potentially offering both a client graphical user interface as well as very simple interface for the host device.

I guess you can call it a no-frills experience if there’s no product for you to use!

Planning features of the host device #

To build a PoC that broadcasts devices automatically, and that can be used from a client alongside the usbip command-line, interface, the host needs the following features:

Telling apart leaf USB devices from USB hubs #

Running lsusb with the --tree option returns this:

--- log-without-hub     2022-02-05 12:26:40.311210251 +0100
+++ log-with-hub        2022-02-05 12:26:35.234559166 +0100
@@ -1,11 +1,15 @@
 /:  Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/2p, 10000M
     ID 1d6b:0003 Linux Foundation 3.0 root hub
+    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 5000M
+        ID 05e3:0620 Genesys Logic, Inc. GL3523 Hub
 /:  Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/2p, 480M
     ID 1d6b:0002 Linux Foundation 2.0 root hub
 /:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/10p, 10000M
     ID 1d6b:0003 Linux Foundation 3.0 root hub
 /:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/16p, 480M
     ID 1d6b:0002 Linux Foundation 2.0 root hub
+    |__ Port 4: Dev 5, If 0, Class=Hub, Driver=hub/4p, 480M
+        ID 05e3:0610 Genesys Logic, Inc. Hub
     |__ Port 8: Dev 2, If 3, Class=Video, Driver=uvcvideo, 480M
         ID 04f2:b6be Chicony Electronics Co., Ltd
     |__ Port 8: Dev 2, If 1, Class=Video, Driver=uvcvideo, 480M

The Genesys USB hub shows up twice, under two different trees: first the 3.0 root hub, and then the 2.0 root hub. If I connect a USB 3.0 device to the hub, it doesn’t matter which port I connect it to, it always shows up under the 3.0 tree of the root hub in my laptop:

--- log-with-hub        2022-02-05 12:26:35.234559166 +0100
+++ log-with-device     2022-02-05 12:37:10.480333271 +0100
@@ -1,14 +1,16 @@
 /:  Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/2p, 10000M
     ID 1d6b:0003 Linux Foundation 3.0 root hub
-    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 5000M
+    |__ Port 1: Dev 3, If 0, Class=Hub, Driver=hub/4p, 5000M
         ID 05e3:0620 Genesys Logic, Inc. GL3523 Hub
+        |__ Port 2: Dev 7, If 0, Class=Mass Storage, Driver=usb-storage, 5000M
+            ID 090c:1000 Silicon Motion, Inc. - Taiwan (formerly Feiya Technology Corp.) Flash Drive
 /:  Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/2p, 480M
     ID 1d6b:0002 Linux Foundation 2.0 root hub
 /:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/10p, 10000M
     ID 1d6b:0003 Linux Foundation 3.0 root hub
 /:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/16p, 480M
     ID 1d6b:0002 Linux Foundation 2.0 root hub
-    |__ Port 4: Dev 5, If 0, Class=Hub, Driver=hub/4p, 480M
+    |__ Port 4: Dev 6, If 0, Class=Hub, Driver=hub/4p, 480M
         ID 05e3:0610 Genesys Logic, Inc. Hub
     |__ Port 8: Dev 2, If 3, Class=Video, Driver=uvcvideo, 480M
         ID 04f2:b6be Chicony Electronics Co., Ltd

It looks like lsusb has a way to tell apart hubs from leaf devices. I can probably simply look at the device Class and check to see if it’s root_hub or Hub. This could enable letting the user pick whether to bind a USB hub or their root hub (any ports on the host device itself).

I’m still not certain as to whether I can get this Class information directly from libudev or I’ll need to include some other dependency.

Listening to USB devices as they connect and disconnect #

I found this helpful post: libudev and Sysfs Tutorial. I won’t be following it so much, but it seems like a good example. Running man libudev also seems helpful.

As a first step, I’m going to implement a busy loop that listens to connection and disconnection events and just logs any interesting information to the console.

From man libudev:

To monitor the local system for hotplugged or unplugged devices, a monitor can be created via udev_monitor_new_from_netlink(3).

Looks like udev_monitor_new_from_netlink takes a pointer to udev and a name. To get a udev context object, I can call udev_new.

Aside: to set up a bit of a quicker feedback loop, I’m running watchexec: watchexec -c -w src -e c 'make && ./target/usairb'.

After adding -ludev to my Makefile, I wrote this:

#include <libudev.h>
#include <stdio.h>
#include <stdlib.h>

const char *UDEV_MONITOR_NAME = "usairb-udev-monitor";

int main(void) {
  struct udev *udev = udev_new();
  struct udev_monitor *udev_monitor =
      udev_monitor_new_from_netlink(udev, UDEV_MONITOR_NAME);

  if (!udev_monitor) {
    fprintf(stderr, "udev_monitor_new_from_netlink returned NULL\n");
    exit(EXIT_FAILURE);
  }
}

My error message came to fruition instantly. man libudev says “on failure, NULL is returned”, but doesn’t mentions what the reasons for failure are.

I’m wondering if it’s udev_new that’s failing:

if (!udev) {
  fprintf(stderr, "udev_new returned NULL\n");
  exit(EXIT_FAILURE);
}

No, it’s our other friend:

udev_monitor_new_from_netlink returned NULL
[[Command exited with 1]]

I solved it out of sheer luck: I changed the value passed to name to "udev", and now it’s working just fine. The takeaway: the name param is the name of a known Netlink. A Netlink is similar to a domain socket and is used for IPC. It looks like there’s one for "udev", and you need to specify that.

Now I’m looking into what I can do with udev_monitor. The manual only includes entries for functions (such as udev_new) but not for structs. Maybe the monitor is meant to be passed around to other functions. How can I find those? My copy of The Linux Programming Interface by Michael Kerrisk only introduces udev at a high level but doesn’t go into detail. I guess it’s time to read examples on the Internet, unfortunately. This makes me miss the Rust ecosystem. Documentation generated by rustdoc would make cross-referencing searches very easy.

One practical solution I’ve found (besides reading existing work on the Internet) is to let my shell help me: if I type udev_monitor_ and ask for an autocompletion with tab, I get this:

 ~/Development/usairb => man udev_monitor_
udev_monitor_enable_receiving                    udev_monitor_filter_update                       udev_monitor_receive_device
udev_monitor_filter_add_match_subsystem_devtype  udev_monitor_get_fd                              udev_monitor_ref
udev_monitor_filter_add_match_tag                udev_monitor_get_udev                            udev_monitor_set_receive_buffer_size
udev_monitor_filter_remove                       udev_monitor_new_from_netlink                    udev_monitor_unref

That’s good enough. I can also use LunarVim’s LSP hints. Two functions look interesting: udev_monitor_receive_device and udev_monitor_enable_receiving. The latter sounds like a prerequisite, but I’m going to go without it at first and see what happens.

struct udev_device *device = udev_monitor_receive_device(udev_monitor);
printf("Prints if `udev_monitor_receive_device` is not blocking.");

It printed. I added a call to udev_monitor_enable_receiving, but the behavior didn’t change. Looks like my assumption that it’s blocking isn’t holding up!

After some sleuthing in systemd source code, I found this comment that confirms that the call is non-blocking, and suggests two possible paths forward:

I don’t know how to do either of these things, so it’s time for some more reading.

TLPI to the rescue! I can use fcntl to modify flags on the file descriptor returned by udev_monitor_get_fd.

Got it to work thanks to some bitwise-fu from a StackOverflow answer:

// udev_monitor_receive_device is non-blocking. To make it blocking,
// get the monitor file descriptor and unset its O_NONBLOCK flag.
int udev_monitor_fd = udev_monitor_get_fd(udev_monitor);
int udev_monitor_fd_flags = fcntl(udev_monitor_fd, F_GETFL);
fcntl(udev_monitor_fd, F_SETFL, udev_monitor_fd_flags & ~O_NONBLOCK);

I wrote a busy loop:

while (1) {
  printf("Listening for new devices...\n");

  struct udev_device *device = udev_monitor_receive_device(udev_monitor);

  if (device) {
    printf("Found a device!\n");
  }
}

Aside: I added -r to my watchexec command so that it restarts the process every time, otherwise the busy loop never exits.

The behavior is not quite what I expected: my program logs "Found a device!" but it seems really excited about it and does it many times over on every connection. Additionally, it does it when I disconnect a device. It feels like a race condition.

To find out, I added a sleep(1); right after printf("Found a device!\n");. To my surprise, my program still “finds the device” a bunch of times. Without sleep(1);, it prints roughly the same amount of times.

After reading the documentation a bit more, I found some new clarifications:

If I run udevadm monitor --udev, I get exactly the same output (well, in a different format):

 ~/Development/usairb => udevadm monitor -u
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing

UDEV  [8858.969668] add      /devices/virtual/workqueue/scsi_tmf_0 (workqueue)
UDEV  [8858.971981] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6 (usb)
UDEV  [8858.973386] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0 (usb)
UDEV  [8858.974132] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0 (scsi)
UDEV  [8858.974950] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/scsi_host/host0 (scsi_host)
UDEV  [8858.975633] bind     /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0 (usb)
UDEV  [8858.978815] bind     /devices/pci0000:00/0000:00:14.0/usb2/2-6 (usb)
UDEV  [8860.074537] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0 (scsi)
UDEV  [8860.075845] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0 (scsi)
UDEV  [8860.075899] add      /devices/virtual/bdi/8:0 (bdi)
UDEV  [8860.077712] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/scsi_device/0:0:0:0 (scsi_device)
UDEV  [8860.078456] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/bsg/0:0:0:0 (bsg)
UDEV  [8860.078522] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/scsi_disk/0:0:0:0 (scsi_disk)
UDEV  [8860.131761] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/block/sda (block)
UDEV  [8860.244986] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda2 (block)
UDEV  [8860.258575] add      /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1 (block)
UDEV  [8860.259622] bind     /devices/pci0000:00/0000:00:14.0/usb2/2-6/2-6:1.0/host0/target0:0:0/0:0:0:0 (scsi)

Looks like new devices are added for different things: the partitions in my USB flash drive, some accessors for protocols I don’t understand (SCSI), etc.

Since they all point to the same thing in /sys/devices, I think I can just filter for events for which udev_device_get_devtype returns "usb_device".

const char *device_type = udev_device_get_devtype(device);
if (!device_type || strcmp(device_type, "usb_device") != 0) {
  continue;
}

printf("Found a USB device:\n");
// ...

Now, I get only two events for each physical action: add and bind (in that order) when plugging in the device and remove and unbind when unplugging it. For now, I’m only going to care about add and remove, and we’ll see about this later!

Currently, the output looks like this when I plug a USB flash drive in and out:

Found a USB device:
   Node: /dev/bus/usb/002/026
   Subsystem: usb
   Devtype: usb_device
   Action: add
Found a USB device:
   Node: /dev/bus/usb/002/026
   Subsystem: usb
   Devtype: usb_device
   Action: remove

When I do the same with my USB hub, I get two distinct devices:

Found a USB device:
   Node: /dev/bus/usb/001/026
   Subsystem: usb
   Devtype: usb_device
   Action: add
Found a USB device:
   Node: /dev/bus/usb/004/015
   Subsystem: usb
   Devtype: usb_device
   Action: add
Found a USB device:
   Node: /dev/bus/usb/001/026
   Subsystem: usb
   Devtype: usb_device
   Action: remove
Found a USB device:
   Node: /dev/bus/usb/004/015
   Subsystem: usb
   Devtype: usb_device
   Action: remove

This is because, like we saw earlier, my hub registers once as a USB 2.0 device and once again as a USB 3.0 device.

I’m calling this one a success! Next time I get a chance to work on this, I’ll be working on binding these devices automatically using libusbip. Thanks for reading!