Modern operating systems are capable for identifying a hardware device that is attached to the system while it's running. Most obvious case is handling USB. On Unix-like systems, these hardware devices are accessed through special files called device-files or device-nodes located in the `/dev` directory. The content of the /dev directory is kept on a temporary file system (usually devtmpfs or tmpfs) and rendered at every reboot. Manually made changes won't survive.
Since the 2.6 kernel series, Linux systems began using udev to manage these devices at runtime, as a succesor of the deprecated devfs kernel subsystem. Furthermore, most *nix systems had grown other alternatives to manage /dev, besides udev.
In particular, vdev was designed by Jude C. Nelson with portability across Unix-like operating systems in mind. The Linux port of vdev aims to be a drop-in replacement for udev, and therby needs to ship with a port of libudev that can be queried without requiring the daemon of udev to be running. This library ABI-compatible is called libudev-compat.
Linux Users who prefer some other device manager besides udev or vdev but need to run software that depends on libudev can use libudev-compat instead.
The implication of having ABI compatibility is that we don't need to recompile the sources of the already existent libudev clients. As could be imagined, this is key to ensuring backwards compatibility, given the many programs linked against udev's shared library.
Just as an illustration, we're going to study some libudev usage examples before moving on to the basics of libudev-compat.
The snippets below are verbatim copies taken from the following tutorial, and therefore, we're not going into details:
For deeper information, you can read the explanations given in the provided how-to if you wish.
libudev-compat's ABI-compatibility
First of all, you need to install eudev's development files. Obviously, this step will remove vdev if it's currently installed at all:
# apt-get install libeudev1 eudev libeudev-dev
Building with libudev is as simple as including libudev.h and passing the -ludev flag to the compiler. The same goes for eudev (udev without systemd).
Example 1:
The first example gets a list of the hidraw objects connected to the machine and shows some of their attributes:
You can build it by typing:
$ gcc -Wall -g -o libudev_example libudev_example.c -ludev
When running the program, the following data is printed out in my system:
$ ./libudev_example Device Node Path: /dev/hidraw0 VID/PID: 1c4f 0002 SIGMACHIP USB Keyboard serial: (null) Device Node Path: /dev/hidraw1 VID/PID: 1c4f 0002 SIGMACHIP USB Keyboard serial: (null) Device Node Path: /dev/hidraw2 VID/PID: 046d c542 Logitech Wireless Receiver serial: (null)
Example 2:
In addition to being able to access a specific hardware device, libudev also provides a monitoring interface.
The algorithm of our second example, taken also from the aforementioned link, shows only monitoring without enumeration, just for simplicity.
Again, you can build the example as follows:
$ gcc -Wall -g -o libudev_monitor libudev_monitor.c -ludev
This program will run forever. Teminate it with Ctrl-C. As I disconnect and reconnect my HID device with the above code running, it throws:
$ ./libudev_monitor .............. select() says there should be data Got Device Node: /dev/hidraw2 Subsystem: hidraw Devtype: (null) Action: remove ............... select() says there should be data Got Device Node: /dev/hidraw2 Subsystem: hidraw Devtype: (null) Action: add ... ^C
Because we are interested in demonstrate the ABI stability regardless the queried library, we shall now proceed to restore vdev leading it to the removal of eudev:
# apt-get install libudev1-compat vdev
After that, reboot the system and run the generated binaries without recompilation. You should get similar outputs in the command line.
libudev-compat's API-compatibility
As opposed to the runtime compatibility, the implication of having API compatibility takes place at source level: code that would have compiled succesfully against libudev should compile against libudev-compat as well without issues.
Assuming that vdev is already installed, you can get the development files via:
# apt-get install libudev-compat-dev
Let us look at the official testing example of eudev: test-libudev.c . In order to build it aside from the project's tree, I've had to make some minor changes in the header of the file. These changes are trivial and have nothing to do with libudev-compat.
The result:
can be built with:
$ gcc -Wall -g -o test-libudev-compat test-libudev-compat.c `pkg-config --cflags --libs libudev`
Notice the replacement of the -ludev flag with `pkg-config --cflags --libs libudev` in the case of libudev-compat.
Just like the previous monitoring example, this program will run forever. Terminate it with Ctrl-C.
How libudev-compat works
Consider the `udevadm monitor` command:
# udevadm monitor monitor will print the received events for: UDEV - the event which udev sends out after rule processing KERNEL - the kernel uevent ... ^C
As you can see, the program makes a basic differentiation between two types of visualized events:
KERNEL - Every time a device is added to or removed from the system, this change in the device state needs to be propagated to the user space; for this purpose, the kernel sends a uevent over netlink to notify the udevd daemon of the change. These driver core uevents occur in the kernel space, whereas on the other hand device information is exported dinamically by the sysfs filesystem. It is sysfs which makes devices visible in user space; netlink only sends the notification together with a bunch of data. While all this is going on, the device manager will take an inventory of the hardware from sysfs and populate `/dev` accordingly in the userspace.
UDEV - Each uevent involving the detection/removal of a hardware device is received from a kernel netlink socket and matched against a set of rules in order to carry out some specific tasks. For instance, a rule can request and import additional data to evaluate during the creation of the device node in `/dev` directory; it also can run certain scripts after the device node has been created (for the sake of argument, to load the corresponding driver). As opposed to the kernel uevents, the event processes triggered as the device manager parses all tasks occur in the user space. Think of udev events like user space /dev events, regardless the running daemon. In the concrete case of udevd, processed task are called rules; in the case of vdevd are called actions. In either case, they allow you to customize user space device event processing, providing a way to plug-in external tools as part of kernel device handling.
This said, pay attention to the following function taken from eudev (libudev-monitor.c, lines 226-251):
/** * udev_monitor_new_from_netlink: * @udev: udev library context * @name: name of event source * * Create new udev monitor and connect to a specified event * source. Valid sources identifiers are "udev" and "kernel". **/ _public_ struct udev_monitor * udev_monitor_new_from_netlink ( struct udev *udev, const char *name ) { return udev_monitor_new_from_netlink_fd ( udev, name, -1 ); }
The two valid identifiers are "udev" and "kernel" depending on the group it belongs to (libudev-monitor.c, lines 65-69):
enum udev_monitor_netlink_group { UDEV_MONITOR_NONE, UDEV_MONITOR_KERNEL, UDEV_MONITOR_UDEV, };
However, as already explained, libudev-compat aims to provide service to libudev clients stripping away the user space daemon udevd. To this effect, it removes the netlink connection to udevd in favor of creating and watching a per-process specific directory located in the /dev filesystem directly. To archieve this, it only connects to a netlink socket if the name is "kernel", as shown below:
_public_ struct udev_monitor * udev_monitor_new_from_netlink(struct udev *udev, const char *name) { if( strcmp( name, "udev" ) == 0 ) { return udev_monitor_new_from_filesystem(udev); } else { return udev_monitor_new_from_netlink_fd(udev, name, -1); } }
The invocked function
udev_monitor_new_from_filesystem( udev );
is defined in another file written entirelly by Jude C. Nelson:
As an illustration, it might be interesting to build the example test located at the bottom of the file, just under the directive #ifdef TEST
.
After removing the forks done at the end of the loop in the main function, we get the following code:
/* * * test-libudev-fs.c * */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <stddef.h> #include <limits.h> #include <sys/epoll.h> #include <unistd.h> #include <string.h> #include <poll.h> #include <libudev.h> int main( int argc, char** argv ) { int rc = 0; char pathbuf[PATH_MAX+1]; struct udev* udev_client = NULL; struct udev_monitor* monitor = NULL; int monitor_fd = 0; struct udev_device* dev = NULL; struct pollfd pfd[1]; udev_monitor_fs_events_path( "", pathbuf, 0 ); printf("Watching '%s'\n", pathbuf ); udev_client = udev_new(); if( udev_client == NULL ) { // OOM exit(2); } monitor = udev_monitor_new_from_netlink( udev_client, "udev" ); if( monitor == NULL ) { // OOM or error udev_unref( udev_client ); exit(2); } printf("Press Ctrl-C to quit\n"); monitor_fd = udev_monitor_get_fd( monitor ); if( monitor_fd < 0 ) { rc = -errno; printf("udev_monitor_get_fd rc = %d\n", rc ); exit(3); } pfd[0].fd = monitor_fd; pfd[0].events = POLLIN; while(1) { // wait for the next device rc = poll( pfd, 1, -1 ); if( rc < 0 ) { printf("poll(%d) rc = %d\n", monitor_fd, rc ); break; } // get devices while(1) { dev = udev_monitor_receive_device( monitor ); if( dev == NULL ) { break; } int pid = getpid(); struct udev_list_entry *list_entry = NULL; printf("[%d] ACTION: '%s'\n", pid, udev_device_get_action( dev ) ); printf("[%d] SEQNUM: %llu\n", pid, udev_device_get_seqnum( dev ) ); printf("[%d] USEC: %llu\n", pid, udev_device_get_usec_since_initialized( dev ) ); printf("[%d] DEVNODE: '%s'\n", pid, udev_device_get_devnode( dev ) ); printf("[%d] DEVPATH: '%s'\n", pid, udev_device_get_devpath( dev ) ); printf("[%d] SYSNAME: '%s'\n", pid, udev_device_get_sysname( dev ) ); printf("[%d] SYSPATH: '%s'\n", pid, udev_device_get_syspath( dev ) ); printf("[%d] SUBSYSTEM: '%s'\n", pid, udev_device_get_subsystem( dev ) ); printf("[%d] DEVTYPE: '%s'\n", pid, udev_device_get_devtype( dev ) ); printf("[%d] SYSNUM: '%s'\n", pid, udev_device_get_sysnum( dev ) ); printf("[%d] DRIVER: '%s'\n", pid, udev_device_get_driver( dev ) ); printf("[%d] DEVNUM: %d:%d\n", pid, major( udev_device_get_devnum( dev ) ), minor( udev_device_get_devnum( dev ) ) ); printf("[%d] IFINDEX: '%s'\n", pid, udev_device_get_property_value( dev, "IFINDEX" ) ); printf("[%d] DEVMODE: '%s'\n", pid, udev_device_get_property_value( dev, "DEVMODE" ) ); printf("[%d] DEVUID: '%s'\n", pid, udev_device_get_property_value( dev, "DEVUID" ) ); printf("[%d] DEVGID: '%s'\n", pid, udev_device_get_property_value( dev, "DEVGID" ) ); list_entry = udev_device_get_devlinks_list_entry( dev ); udev_list_entry_foreach( list_entry, udev_list_entry_get_next( list_entry )) { printf("[%d] devlink: '%s'\n", pid, udev_list_entry_get_name( list_entry ) ); } list_entry = udev_device_get_properties_list_entry( dev ); udev_list_entry_foreach( list_entry, udev_list_entry_get_next( list_entry )) { printf("[%d] property: '%s' = '%s'\n", pid, udev_list_entry_get_name( list_entry ), udev_list_entry_get_value( list_entry ) ); } list_entry = udev_device_get_tags_list_entry( dev ); udev_list_entry_foreach( list_entry, udev_list_entry_get_next( list_entry )) { printf("[%d] tag: '%s'\n", pid, udev_list_entry_get_name( list_entry ) ); } list_entry = udev_device_get_sysattr_list_entry( dev ); udev_list_entry_foreach( list_entry, udev_list_entry_get_next( list_entry )) { printf("[%d] sysattr: '%s'\n", pid, udev_list_entry_get_name( list_entry ) ); } printf("\n"); udev_device_unref( dev ); } } udev_monitor_fs_destroy( monitor ); return 0; }
Build it with:
$ gcc -Wall -g -o test-libudevfs test-libudevfs.c `pkg-config --cflags --libs libudev`
And run the program as root:
# ./test-libudevfs Watching '/dev/metadata/udev/events/libudev-23148-0/' Press Ctrl-C to quit
A new directory has been created in /dev/metadata/udev/events. So, whenever the name of the second argument passed to the function is udev
monitor = udev_monitor_new_from_netlink( udev_client, "udev" );
libudev-compat watches the underlying directory
/dev/metadata/udev/events/libudev-$CLIENT_PID-0
for new packet events. Otherwise, it connects to a netlink socket.
libudev-compat removes the netlink connection to udevd in favor of creating and watching a `per-process` specific directory located in the /dev filesystem directly."
# tree /dev/metadata/udev/events /dev/metadata/udev/events ├── global ├── libudev-1548-0 ├── libudev-1548-1 ├── libudev-17821-0 ├── libudev-23035-0 ├── libudev-23039-0 ├── libudev-23058-0 ├── libudev-23088-0 ├── libudev-23148-0
Eventfs: specialized userspace filesystem
*** TO BE CONTINUED ***