Notes on "The Linux Device Model"
As part of The Eudyptula Challenge, Little tasked me to read Chapter 14 of
Linux Device Drivers, titled The Linux Device Model. Since the introduced topics are
pretty advanced, I took some notes along the way. These skip many details, so if you find the topic
interesting, please go read the whole chapter. Note that this book is a bit out of date since it
was written for kernel version 2.6.10
.
The Linux device model can be roughly described as a data structure used to represent the hierarchy
and relationships between buses, drivers, and devices in a Linux system. This model is exposed and
can be interacted with, using the sysfs pseudo filesystem, which is usually mounted at /sys
.
🧱 Kobjects
Kobjects are the basic building blocks of the Linux device model. They can be used for:
- reference counting (embedded inside other data structures)
- sysfs representation
- device model representation
- hotplug event handing
We won’t go into detail about reference counting, but kobject’s reference counter can be modified
using the kobject_get()
and kobject_put()
functions. Once the counter reaches 0
, the kobject
is freed.
All kobjects, if exposed, show up in /sys
as directories. Inside these directories reside the
kobject’s attributes as files. Each kobject is assigned a ktype, which defines its default
attributes, along with show()
and store()
functions (through sysfs_ops
). These verify and
implement the read/write operations on the attributes (files). If you wish to modify non-default
attributes to a kobject, you can use the sysfs_{create,remove}_file()
functions.
Ksets
While relationships between kobjects can be defined using the parent
field, they are usually
grouped into ksets (using a linked list). These act as a container for other kobjects and ksets,
and always appear under /sys
, through an embedded kobject. If a kobject is part of a kset, it uses
the parent kset’s ktype. Kset membership can be modified using the kobject_{add,del}()
functions.
Kobjects without a parent will appear in the sysfs root, but this is rarely desired. On the
other hand, ksets must belong to a subsystem. A subsystem is a representation of a part of the
device model (for example the device subsystem under /sys/devices
or the PCI subsystem under
/sys/bus/pci
). The subsystem
data structure contains a single kset and a semaphore to control
concurrent access during kset traversal.
While the sysfs filesystem has a tree structure, additional relationships between kobjects are
represented using symbolic links. For example, the device a driver is attached to is simply a
symbolic link to /sys/devices/<device>
.
Each time a kobject is created or removed, a hotplug event is triggered, which allows the userspace
to react appropriately and load the necessary drivers. This used to be handled by the
/sbin/hotplug
script. The events bubble up the device model, and each layer can add the
corresponding environment variables or suppress the event if necessary.
Here’s an example of a simple sysfs hierarchy:
$ tree /sys
/sys
├── kobject
├── kset
│ ├── kobject1
│ │ ├── attribute1
│ │ └── attribute2
│ ├── kobject2 -> ../subsystem/kobject
│ └── kobject3
└── subsystem
└── kobject
🏠 Devices and drivers
Using kobjects as a base, higher-level data structures are fairly similar. They each use an embedded
kobject and represent a small part of the Linux device model. Whenever your functions are passed a
lower-level object, which is embedded in a custom data structure, you can use the container_of()
macro to get the containing data structure defined in your kernel module.
Similar to kobjects, these data structures all have their own functions for initialization and
attribute handling. For ease of development, convenience macros like DEVICE_ATTR()
are defined.
Buses
Buses are used as a channel between the CPU and multiple devices. While not necessarily representing
a physical bus, all devices must be connected via a bus. Virtual buses are called “platform” buses.
Note that each bus is its own subsystem and is located under /sys/bus
. Each bus contains a
devices
and a drivers
kset. As mentioned before, devices are symbolic links to /sys/devices
.
Whenever a new device or a driver for a specific bus is added, the match()
function is called,
which tries to assign the device with a driver. After the function finds a driver, the kernel calls
its probe()
method, which performs additional checks and initialized the device. If probe()
returns an error, match()
continues with other drivers.
Devices
Every device in a Linux system is represented by a device
structure. It contains all the fields
needed to identify a device and its location within the device tree, while most other fields are
initialized by the specified subsystem, which assigns the bus and a driver. These subsystems
usually wrap the device
structure to track additional information. As mentioned, all devices are
located in /sys/devices
.
Classes are an even higher-level abstraction over devices, based on what a device does (for example
storage or input devices). They have pretty a complex interface, so we won’t go into detail.
Classes are usually located in /sys/class
, which mostly contain symbolic links to devices. Class
membership is handled by higher-level subsystem code.
Drivers
Drivers, usually located under /sys/bus/<name>/drivers
, implement the logic to interact with a
device. Once it’s registered with a subsystem, it can be matched with devices connected to a system.
The structure of a driver
object is similar to device
, but it also contains various methods to
initialize and shutdown devices. One of these methods is probe()
, mentioned above.
This concludes my notes about the various data structures in the Linux device model. For an example of how this all works in practice in the PCI subsystem, please check the Putting It All Together section in the book. The explanation there is far better than what I could summarize here.