采用libudev探测热拔插设备

本文深入探讨了libudev库在现代Unix/Linux系统中如何简化硬件设备的管理与访问。通过阐述传统的设备文件系统(/dev)与libudev的功能对比,解释了libudev如何动态管理设备节点,并提供了基于sysfs的替代方案来解决传统方法的局限性。重点介绍了libudev的使用方法,包括如何获取设备信息、枚举设备和监听设备事件,以及如何在应用程序中集成libudev以实现更加灵活和可靠的设备管理。此外,文章还讨论了libudev的引用计数机制、监控接口及其非阻塞行为,以及与sysfs交互的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://www.signal11.us/oss/udev/



libudev and Sysfs Tutorial

Introduction and Motivation

On Unix and Unix-like systems, hardware devices are accessed throughspecial files (also called device files or nodes) located in the/dev directory. These files are read from and written to justlike normal files, but instead of writing and reading data on a disk, theycommunicate directly with a kernel driver which then communicates with thehardware. There are many online resources describing /dev files inmore detail. Traditonally, these special files were created at install timeby the distribution, using the mknod command. In recent years,Linux systems began using udev to manage these/dev files at runtime. For example, udev willcreate nodes when devices are detected and delete them when devices are removed(including hotplug devices at runtime). This way,the /dev directory contains (for the most part) only entriesfor devices which actually exist on the system at the current time, asopposed to devices which could exist.

Udev also has a powerful scripting interface (with files commonly locatedin /etc/udev/rules.d) which distributors (and end users) oftenuse to customize the way device nodes are created. Customizable properties include file permissions,location withinthe filesystem, and symbolic links. As could be imagined, this customizationcan make it difficult for application writers to locate specific devicefiles (ortypes of devices), because they could be easily moved by modifying theudevrules. For example, in recent years, the js (joystick) nodeswere moved from /dev to /dev/input. Many olderprograms explicitly opened devices in /dev (for example/dev/js0). When these older programs are run today, andtry to open /dev/js0, they will simply not worksince /dev/js0 has been moved.

Another problem is that when using multiple devices of the same type, theorder in which they appear in /dev is not guaranteed to be thesame every time. This often happens with USB devices. Some USB devices willshow up in a different order after a reboot even when plugged into the sameUSB ports. I'veobserved this directly with FTDI USB serial ports. For example, with two ofthese ports plugged in, udev will create /dev/ttyUSB0 and/dev/ttyUSB1, but the order is undefined. (This particularproblem can be worked around by creating udev rules which createsymlinks based on something like a device serial number).

Another issue is that when dealing with things like HID devices, simply knowingthat an entry such as /dev/hidraw0 exists tells you nothing aboutwhat kind of device it is. It could be any type of HID device.

The Solution - sysfs

Sysfs is a virtual filesystem exported by the kernel, similar to/proc. The files in Sysfs contain information about devices anddrivers. Some files in Sysfs are even writable, for configuration andcontrol of devices attached to the system. Sysfs is always mounted on /sys.

The directories in Sysfs contain the heirarchy of devices, as they areattached to the computer. For example, on my computer, thehidraw0 device is located under:

/sys/devices/pci0000:00/0000:00:12.2/usb1/1-5/1-5.4/1-5.4:1.0/0003:04D8:003F.0001/hidraw/hidraw0

Based on the path, the device is attached to (roughly, starting fromthe end) configuration 1 (:1.0) of the device attached to portnumber 4 of device 1-5, connected to USB controller 1 (usb1), connected tothe PCI bus. While interesting, this directory path doesn't do us very muchgood, since it's dependent on how the hardware is physically connected tothe computer.

Fortunately, Sysfs also provides a large number of symlinks, for easy accessto deviceswithout having to know which PCI and USB ports they are connected to. In/sys/class there is a directory for each different class ofdevice. My /sys/class directory looks like this:


alan@ato:/sys/class$ ls

atm        graphics       ieee1394_protocol  printer       thermal
backlight  hidraw         input              rfkill        tty
bdi        hwmon          mem                scsi_device   usb
block      i2c-adapter    misc               scsi_disk     vc
bluetooth  ide_port       net                scsi_generic  video_output
dma        ieee1394       pci_bus            scsi_host     vtconsole
dmi        ieee1394_host  power_supply       sound
firmware   ieee1394_node  ppdev              spi_master

Following our example of using hidraw, one can see that there is ahidraw directory here. Inside it is a symbolic link namedhidraw0 which points to

../../devices/pci0000:00/0000:00:12.2/usb1/1-5/1-5.4/1-5.4:1.0/0003:04D8:003F.0001/hidraw/hidraw0

This way, hidraw devices can easily be found under /sys/class/hidraw without knowing anything about their USB or PCI heirarchy. It would bea good exercise to examine the contents of the /sys directory,especially /sys/bus, /sys/class, and/sys/subsystem. Since much of /sys is symboliclinks, it may also benefit you to use the utility realpath to showphysical directory paths, as opposed to symbolic link paths. This is useful whentrying to find the actual parent directories of device directories. Forexample, to find the containing USB device entry for hidraw0,one could use realpath to do something like the following:


alan@ato:/sys$ cd /sys/class/hidraw/hidraw0/
alan@ato:/sys/class/hidraw/hidraw0$ ls
dev  device power  subsystem  uevent
alan@ato:/sys/class/hidraw/hidraw0$ cd `realpath $PWD`
alan@ato:/sys/devices/pci0000:00/0000:00:12.2/usb2/2-5/2-5.4/2-5.4:1.0/0003:04D8:003F.0001/hidraw/hidraw0$ ls
dev  device power  subsystem  uevent
alan@ato:/sys/devices/pci0000:00/0000:00:12.2/usb2/2-5/2-5.4/2-5.4:1.0/0003:04D8:003F.0001/hidraw/hidraw0$ cd ../../../../
alan@ato:/sys/devices/pci0000:00/0000:00:12.2/usb2/2-5/2-5.4$ ls
2-5.4:1.0            bDeviceSubClass     configuration  idProduct     remove
authorized           bmAttributes        descriptors    idVendor      serial
avoid_reset_quirk    bMaxPacketSize0     dev            manufacturer  speed
bcdDevice            bMaxPower           devnum         maxchild      subsystem
bConfigurationValue  bNumConfigurations  devpath        power         uevent
bDeviceClass         bNumInterfaces      driver         product       urbnum
bDeviceProtocol      busnum              ep_00          quirks
version
alan@ato:/sys/devices/pci0000:00/0000:00:12.2/usb2/2-5/2-5.4$ 

libudev

Because it's cumbersome and error-prone to walk the Sysfs tree from withinan application's code,there's a convenient library called libudev to do this task forus. Currently, the closest thing to a manual for libudev is thegtk-doc-genereated API reference located here:

http://www.kernel.org/pub/linux/utils/kernel/hotplug/libudev/

The documentation there is not really enough for the average developer to getstarted, so hopefully this guide and its example will make it a bit easier.

For the remainder of this guide, we'll be using libudev toaccess hidraw devices. Using libudev, we'll be able to inspectthe devices, including their Vendor ID (VID), Product ID (PID), serialnumber, and device strings, without ever opening the device. Further,libudev will tell us exactly where inside /dev thedevice's node is located, giving the application a robust anddistribution-independent way of accessing the device.

Building with libudev is as simple as including libudev.h andpassing -ludev to the compiler to link with thelibudev library.

The first example gets a list of the hidraw objects connectedto the system, andprints out their device node path, manufacturer strings, and serial number.To do this, a udev_enumerate object is created, and the textstring "hidraw" is specified to be used as its filter.libudev will then return a list of udev_deviceobjects which match the filter. In our example, this will be a list of allthe hidraw devices attached to the system. The example code performsthe following tasks:

  1. Initialize the library, getting handle to a struct udev.
  2. Enumerate the devices
  3. For each device:
    1. Print its node name (eg: /dev/hidraw0).
    2. Find the ancestor node which represents the actual USB device (as opposed to the device's HID interface).
    3. Print the USB device information (IDs, serial number, etc).
    4. Unreference the device object.
  4. Unreference the enumeration object.
  5. Unreference the udev object.

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

int main (void)
{
	struct udev *udev;
	struct udev_enumerate *enumerate;
	struct udev_list_entry *devices, *dev_list_entry;
	struct udev_device *dev;
	
	/* Create the udev object */
	udev = udev_new();
	if (!udev) {
		printf("Can't create udev\n");
		exit(1);
	}
	
	/* Create a list of the devices in the 'hidraw' subsystem. */
	enumerate = udev_enumerate_new(udev);
	udev_enumerate_add_match_subsystem(enumerate, "hidraw");
	udev_enumerate_scan_devices(enumerate);
	devices = udev_enumerate_get_list_entry(enumerate);
	/* For each item enumerated, print out its information.
	   udev_list_entry_foreach is a macro which expands to
	   a loop. The loop will be executed for each member in
	   devices, setting dev_list_entry to a list entry
	   which contains the device's path in /sys. */
	udev_list_entry_foreach(dev_list_entry, devices) {
		const char *path;
		
		/* Get the filename of the /sys entry for the device
		   and create a udev_device object (dev) representing it */
		path = udev_list_entry_get_name(dev_list_entry);
		dev = udev_device_new_from_syspath(udev, path);

		/* usb_device_get_devnode() returns the path to the device node
		   itself in /dev. */
		printf("Device Node Path: %s\n", udev_device_get_devnode(dev));

		/* The device pointed to by dev contains information about
		   the hidraw device. In order to get information about the
		   USB device, get the parent device with the
		   subsystem/devtype pair of "usb"/"usb_device". This will
		   be several levels up the tree, but the function will find
		   it.*/
		dev = udev_device_get_parent_with_subsystem_devtype(
		       dev,
		       "usb",
		       "usb_device");
		if (!dev) {
			printf("Unable to find parent usb device.");
			exit(1);
		}
	
		/* From here, we can call get_sysattr_value() for each file
		   in the device's /sys entry. The strings passed into these
		   functions (idProduct, idVendor, serial, etc.) correspond
		   directly to the files in the directory which represents
		   the USB device. Note that USB strings are Unicode, UCS2
		   encoded, but the strings returned from
		   udev_device_get_sysattr_value() are UTF-8 encoded. */
		printf("  VID/PID: %s %s\n",
		        udev_device_get_sysattr_value(dev,"idVendor"),
		        udev_device_get_sysattr_value(dev, "idProduct"));
		printf("  %s\n  %s\n",
		        udev_device_get_sysattr_value(dev,"manufacturer"),
		        udev_device_get_sysattr_value(dev,"product"));
		printf("  serial: %s\n",
		         udev_device_get_sysattr_value(dev, "serial"));
		udev_device_unref(dev);
	}
	/* Free the enumerator object */
	udev_enumerate_unref(enumerate);

	udev_unref(udev);

	return 0;       
}

libudev programs can be compiled using the following:

gcc -Wall -g -o udev_example udev_example.c -ludev

On my system, I have a Microchip Application Demo connected, so my output isthe following (notice how the non-ASCII, Unicode character from the USB serial number ispropagated through to userspace as UTF-8):


alan@alan-desktop:~/tmp$ ./test_udev 
Device Node Path: /dev/hidraw0
  VID/PID: 04d8 003f
  Microchip Technology Inc.
  Simple HID Device Demo
  serial: 1234Å

Some Notes on libudev

Before moving on, it seems appropriate to mention some important thingsabout libudev.

  • libudev's functions are string-based. Since the data iscoming directly from sysfs (which contains (virtual) files with text inthem), all the data which comes from libudev is in text stringformat.This means that the user will have to manually convert strings to integer typesif desired.
  • The strings passed to udev_device_get_sysattr_value()correspond to file names in the sysfs tree. In this example,idVendor corresponds to
    /sys/devices/pci0000:00/0000:00:12.2/usb1/1-5/1-5.4/idVendor
    which can also be reached as (using the sysfs symlinks)
    /sys/bus/usb/devices/1-5.4/idVendor
    In order to find out which properties are available for a device, simplyfind the device in sysfs, and look at which files exist. On my system, my/sys/bus/usb/devices/1-5.4/ directory looks like the following:
    
    1-5.4:1.0            bDeviceSubClass     configuration  idProduct     remove
    authorized           bmAttributes        descriptors    idVendor      serial
    avoid_reset_quirk    bMaxPacketSize0     dev            manufacturer  speed
    bcdDevice            bMaxPower           devnum         maxchild      subsystem
    bConfigurationValue  bNumConfigurations  devpath        power         uevent
    bDeviceClass         bNumInterfaces      driver         product       urbnum
    bDeviceProtocol      busnum              ep_00          quirks
    version
    
    Any non-directory file or link in that directory can be queried withudev_device_get_sysattr_value() to determine the properties ofthe device.
  • All the strings which come from sysfs are Unicode UTF-8. It is an errorto assume that they are ASCII.
  • libudev is reference counted. Instead of specificallyallocating and freeing objects, ref() and unref() functions (such asudev_ref() and udev_unref()) are used for keeping track of how manyreferences to an object exist. When the reference count drops to zero, theobject is freed. Functions which return a new object return it with areference count of 1, so calling its unref() function will effectively freeit. See the libudev documentation (referenced above).

libudev - Monitoring Interface

libudev also provides a monitoring interface. The monitoringinterface will report events to the application when the status of a devicechanges. This is useful for receiving notification when devices are connectedor disconnected from the system. Like the enumeration interface described above,the monitoring interface also provides a filtering mechanisn, so that anapplication can subscribe to only events with which it is concerned. Forexample, if an application added "hidraw" to the filter,only events concerning hidraw devices would be deliveredto the application. When a device changes state, theudev_monitor_receive_device() function will return a handle toa udev_device which represents the object which changed. The returned objectcan then be queried with the udev_device_get_action() functionto determine which action occurred. The actions are returned as thefollowing strings:

  • add - Device is connected to the system
  • remove - Device is disconnected from the system
  • change - Something about the device changed
  • move - Device node was moved, renamed, or re-parented

The udev_monitor_receive_device() function is blocking. That is,when called, program execution will stop until there is an event to bereturned. This use case does not seem to be very useful. Fortunately, theudev_monitor object can provide a file descriptor, suitable foruse with the select() system call. select() can beused to determine if a call to udev_monitor_receive_device()will block, providing a way to receive events in a non-blocking way.

The following code shows an example of the libudev monitorinterface. The example runs a loop which executes select() todetermine if there has been an event. If there has, it callsudev_monitor_receive_device() to receive the event and printsit out. At the end of the loop itsleep()'s for 250 milliseconds. In real life, a simple programlike this would be just fine to not use select() and just letudev_monitor_receive_device() block, but it is written this wayto show an example of how to get non-blocking behavior from thelibudev monitoring interface.



	/* Set up a monitor to monitor hidraw devices */
	mon = udev_monitor_new_from_netlink(udev, "udev");
	udev_monitor_filter_add_match_subsystem_devtype(mon, "hidraw", NULL);
	udev_monitor_enable_receiving(mon);
	/* Get the file descriptor (fd) for the monitor.
	   This fd will get passed to select() */
	fd = udev_monitor_get_fd(mon);
	
	/* This section will run continuously, calling usleep() at
	   the end of each pass. This is to demonstrate how to use
	   a udev_monitor in a non-blocking way. */
	while (1) {
		/* Set up the call to select(). In this case, select() will
		   only operate on a single file descriptor, the one
		   associated with our udev_monitor. Note that the timeval
		   object is set to 0, which will cause select() to not
		   block. */
		fd_set fds;
		struct timeval tv;
		int ret;
		
		FD_ZERO(&fds);
		FD_SET(fd, &fds);
		tv.tv_sec = 0;
		tv.tv_usec = 0;
		
		ret = select(fd+1, &fds, NULL, NULL, &tv);
		
		/* Check if our file descriptor has received data. */
		if (ret > 0 && FD_ISSET(fd, &fds)) {
			printf("\nselect() says there should be data\n");
			
			/* Make the call to receive the device.
			   select() ensured that this will not block. */
			dev = udev_monitor_receive_device(mon);
			if (dev) {
				printf("Got Device\n");
				printf("   Node: %s\n", udev_device_get_devnode(dev));
				printf("   Subsystem: %s\n", udev_device_get_subsystem(dev));
				printf("   Devtype: %s\n", udev_device_get_devtype(dev));

				printf("   Action: %s\n",udev_device_get_action(dev));
				udev_device_unref(dev);
			}
			else {
				printf("No Device from receive_device(). An error occured.\n");
			}					
		}
		usleep(250*1000);
		printf(".");
		fflush(stdout);
	}

It's important to note that when using monitoring and enumeration together,that monitoring should be enabled before enumeration. This way, anyevents (for example devices being attached to the system) which happenduring enumeration will not be lost. If enumeration is done beforemonitoring is enabled, any device attached between the time the enumerationhappens and when monitoring starts will be missed. The algorithmshould be:

  1. Set up monitoring.
  2. Enumerate devices (optionally opening desired devices).
  3. Begin checking the monitoring interface for events.

The example file (linked at the end of this document) uses enumeration andmonitoring together, and follows this algorithm.The code above shows only monitoring, for simplicity.

Output

The code above will run forever. (Terminate it with Ctrl-C). Withthe above section of code running, the following data is printed out as Idisconnect and reconnect my HID device (note that a . character is printedevery 250 milliseconds):


...........................
select() says there should be data
Got Device
   Node: /dev/hidraw0
   Subsystem: hidraw
   Devtype: (null)
   Action: remove
.............
select() says there should be data
Got Device
   Node: /dev/hidraw0
   Subsystem: hidraw
   Devtype: (null)
   Action: add
......^C

Conclusion

The libudev interface is very useful for creating robust software whichneeds to access a specific hardware device or monitor the real-time connection anddisconnection status of hot-pluggable hardware. I hope you find this documentuseful. The full source code of the demo is available through the followinglink:

udev_example.c

Alan Ott
alan@signal11.us
Signal 11 Software
2010-05-23

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值