Device Classes
A device class (
struct class) is the Linux device model’s answer to a different question than a bus answers. A bus says how a device is connected (PCI, USB, I²C); a class says what a device does — regardless of how it attaches. The kerneldoc captures it exactly: “A class is a higher-level view of a device that abstracts out low-level implementation details. Drivers may see a SCSI disk or an ATA disk, but, at the class level, they are all simply disks. Classes allow user space to work with devices based on what they do, rather than how they are connected or how they work” (class.h, v6.12). The class is what populates/sys/class/—/sys/class/net/holds every network interface whether it is a PCI NIC, a USB dongle, or a virtualtun;/sys/class/block/holds every disk;/sys/class/tty/every serial line. Each entry there is a symlink back to the device’s real home in the physical (bus-based) hierarchy under/sys/devices/. Because the grouping is functional and stable, userspace tools — above alludev— key off it to find and name devices uniformly.
This note owns struct class and the class-side helpers. The data structure it groups is in struct device; the connection-based grouping it contrasts with is in struct bus_type and Bus Registration; the model both belong to is The Linux Device Model; and the attribute files a class exposes are in Device Attributes and sysfs Files.
Uncertain
Verify: there is no
class.rstinDocumentation/driver-api/driver-model/for v6.12 — confirmed by querying the GitHub contents API for that directory at thev6.12tag (it listsbus.rst,device.rst,devres.rst,driver.rst, … but noclass.rst). The authoritative class documentation is the kerneldoc comment onstruct classinclass.h, not a standalone.rst. The task brief namedclass.rstas a source; it does not exist at this tag. Reason: source enumeration. To resolve: this is already settled against the v6.12 tree; flagging only because the brief assumed a file that is not there. uncertain
Mental Model
The cleanest way to hold class vs bus: the bus is the cable, the class is the job. A device plugs into exactly one bus (its physical attachment) and may belong to a class (its role). The physical tree under /sys/devices/ is organized by bus — deep, nested, reflecting real topology. The class views under /sys/class/ are flat indexes that cut across that tree, collecting every device of a given role into one directory of symlinks.
flowchart TB subgraph PHYS["/sys/devices/ — the physical tree (by bus)"] direction TB PCI0["pci0000:00/.../0000:01:00.0<br/>(a PCI NIC)"] USB0["usb1/1-2/1-2:1.0<br/>(a USB NIC dongle)"] VIRT["virtual/net/...<br/>(a tun interface)"] end subgraph CLASS["/sys/class/net/ — the functional index (flat)"] direction TB ETH0["eth0 ->"] WLAN["wlan0 ->"] TUN["tun0 ->"] end ETH0 -.->|"symlink"| PCI0 WLAN -.->|"symlink"| USB0 TUN -.->|"symlink"| VIRT UDEV["udev<br/>(userspace)"] -->|"iterates /sys/class/net,<br/>renames, sets perms"| CLASS
Class views cut across the bus-organized physical tree. What it shows: three network devices live in completely different parts of /sys/devices/ (PCI, USB, virtual), yet /sys/class/net/ gathers all three as symlinks pointing back to their real directories. udev walks the flat class index, not the deep physical tree, to apply uniform policy. The insight: the class directory is not where a device “lives” — it is a stable, role-based index into where the device lives, which is exactly what makes /sys/class/<role>/ the right place for userspace to enumerate “all disks” or “all network interfaces” without knowing any bus topology.
What struct class Contains
The definition is small and lives entirely in class.h (line 50 in v6.12):
struct class {
const char *name;
const struct attribute_group **class_groups;
const struct attribute_group **dev_groups;
int (*dev_uevent)(const struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(const struct device *dev, umode_t *mode);
void (*class_release)(const struct class *class);
void (*dev_release)(struct device *dev);
int (*shutdown_pre)(struct device *dev);
const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(const struct device *dev);
void (*get_ownership)(const struct device *dev, kuid_t *uid, kgid_t *gid);
const struct dev_pm_ops *pm;
};Field by field:
name— the class name, which becomes the directory name under/sys/class/."net","block","tty","leds","mem","input","hwmon"are all class names.class_groups— attribute groups attached to the class itself (files directly under/sys/class/<name>/, not per device). Rare; used for class-wide tunables.dev_groups— attribute groups applied to every device that joins this class. This is the common one: a class declaresdev_groupsonce and every member device automatically sprouts those sysfs files. The LED class uses this to give every LED abrightness,max_brightness,triggerfile. See Device Attributes and sysfs Files.dev_uevent— a callback invoked when a member device generates a uevent, letting the class add environment variables to the uevent (e.g. the input class addsKEY=,MODALIAS=). These variables are what udev rules match on.devnode— returns the name (and optionally permissionmode) for the/devnode of a member device. This is howdevtmpfsand udev learn where under/devto place the node and with what mode. Thememclass uses it to set0666on/dev/nulland friends (mem.c).class_release/dev_release— destructors:class_releasefrees the class object itself (set automatically byclass_create());dev_releasefrees a member device when its refcount hits zero (a class-level fallback for devices that do not set their ownrelease).shutdown_pre— called at system shutdown before driver shutdown, per member device.ns_type/namespace/get_ownership— network-namespace plumbing. Thenetclass is namespace-aware: a network interface is only visible in/sys/class/net/inside its own namespace.ns_typeandnamespacetell sysfs how to compute the namespace of a device, andget_ownershiplets the class report the uid/gid for the sysfs directory (so a container’s interfaces appear owned by the container). The kerneldoc notes registration requiresns_typeandnamespaceto be both-set-or-both-unset —class_register()returns-EINVALotherwise (class.c).pm— default power-management operations for member devices.
A structural subtlety worth internalizing: struct class does not embed a struct kobject anymore. Where a device carries its kobject directly, a class is associated at registration time with an internal struct subsys_private (allocated by class_register), which holds the actual kset/kobject, the device klist, the mutex, and the lock key (class.c). The driver core recovers it on demand with class_to_subsys(). This is why a modern struct class can be — and increasingly is — a static const object: it holds no mutable state itself.
Registration: class_register and class_create
There are two ways to bring a class into existence, and which you see depends on the kernel era.
class_register(const struct class *cls) is the primary, modern path. You declare a static const struct class with at least a .name and register it:
static const struct class leds_class = {
.name = "leds",
.dev_groups = led_groups,
.pm = &leds_class_dev_pm_ops,
};
...
return class_register(&leds_class); /* in module init */
...
class_unregister(&leds_class); /* in module exit */(drivers/leds/led-class.c, v6.12). Internally class_register() (class.c) allocates the subsys_private, initializes its device klist and interface list, registers a lockdep key, names the internal kobject after cls->name, hangs it under the global class_kset (which is /sys/class), kset_register()s it — this is the moment /sys/class/<name>/ appears — and creates any class_groups. The const-correctness (const struct class *) is deliberate: a registered class is meant to be immutable, with all its dynamic state held in the private structure.
class_create(const char *name) is the older convenience helper for code that does not want to declare a static class. It kzallocs a struct class, sets name and a class_release that kfrees it, then calls class_register() — returning a struct class * you later tear down with class_destroy() (class.c):
struct class *class_create(const char *name)
{
struct class *cls;
...
cls->name = name;
cls->class_release = class_create_release;
retval = class_register(cls);
...
return cls;
}The class_create signature change — verify your kernel
Uncertain
The exact release in which the parameter dropped is stated below as 6.4 (June 2023) based on diffing the
v6.3vsv6.4source blobs and reading the merge commit message. If you need this pinned to a specific-rcor commit hash beyond “the 6.4 merge window,” confirm against the kernel git log. uncertain
A common compilation trap when working across kernel versions: class_create() lost its first argument. Through Linux 6.3 the API was a macro taking a module owner:
/* v6.3 and earlier */
#define class_create(owner, name) ... __class_create(owner, name, &__key) ...In Linux 6.4 it became the single-argument function shown above:
/* v6.4 onward, including 6.12 LTS */
struct class * __must_check class_create(const char *name);The change is the commit “driver core: class: remove module * from class_create()” by Greg Kroah-Hartman (March 2023, merged for v6.4), whose changelog is blunt: “The module pointer in class_create() never actually did anything, and it shouldn’t have been required to be set as a parameter even if it did something. So just remove it” (torvalds/linux commit log for class.h). The companion commit removed struct module *owner from struct class entirely, with the same note that it “was never actually used.” So on 6.12 LTS the correct call is class_create("myname") — code written for 5.x that passes THIS_MODULE will not compile. This is the kind of silent API drift that breaks out-of-tree modules (Out-of-Tree Modules and the Kernel ABI).
device_create: a Class Device Plus a /dev Node in One Call
The reason struct class matters to character-device authors is device_create() — the helper that, in one call, allocates a struct device, attaches it to a class, sets its dev_t, runs the full device_add() flow, and (because devt is set) triggers the /dev node. Its body (core.c) routes through device_create_groups_vargs():
device_initialize(dev);
dev->devt = devt; /* makes /dev node appear */
dev->class = class; /* joins /sys/class/<name>/ */
dev->parent = parent;
dev->groups = groups;
dev->release = device_create_release; /* auto-frees on last put */
dev_set_drvdata(dev, drvdata);
retval = kobject_set_name_vargs(&dev->kobj, fmt, args);
retval = device_add(dev); /* the real registration */The signature is:
struct device *device_create(const struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);— class, parent device (or NULL), the device number, opaque driver data, and a printf-style name. Because it sets dev->release to a built-in that kfrees the device, you do not manage that memory yourself; you tear it down with device_destroy(class, devt), which finds the device by its devt and device_unregister()s it. A real, complete example from drivers/char/mem.c (v6.12):
static const struct class mem_class = {
.name = "mem",
.devnode = mem_devnode, /* sets /dev node mode */
};
static int __init chr_dev_init(void)
{
register_chrdev(MEM_MAJOR, "mem", &memory_fops);
class_register(&mem_class);
for (minor = 1; minor < ARRAY_SIZE(devlist); minor++) {
...
device_create(&mem_class, NULL, MKDEV(MEM_MAJOR, minor),
NULL, devlist[minor].name);
}
...
}After this runs, /sys/class/mem/ contains null, zero, full, random, mem, … each a symlink to the (virtual) device directory, and /dev/null, /dev/zero, … exist with the 0666 modes the devnode callback returns. That single device_create() per minor is the entire userspace-visible surface of these devices. The sibling device_create_with_groups() does the same but also attaches per-device attribute groups.
How the /sys/class Symlinks Are Actually Built
The class index is not magic — it is constructed during device_add() by device_add_class_symlinks() (core.c). For a device whose dev->class is set, it:
- Looks up the class’s internal
subsys_privatewithclass_to_subsys(dev->class). - Creates a
subsystemsymlink from the device’s own directory to the class kobject — so/sys/devices/.../mydev/subsystempoints back at/sys/class/<name>. - If the device has a parent, creates a
devicesymlink from the device dir to the parent (the “this is my physical parent” back-reference). - Creates the forward symlink
sysfs_create_link(&sp->subsys.kobj, &dev->kobj, dev_name(dev))— i.e. an entry under/sys/class/<name>/named after the device, pointing at the device’s real directory.
Step 4 is the one that materializes the /sys/class/net/eth0 -> ../../devices/.../net/eth0 link you see in the diagram. Because it is a symlink, the device has exactly one real home (the physical tree) and any number of index entries; removing the device removes both. The same device_add() later adds the device to the class’s klist_devices, which is what class_for_each_device() and class_find_device() iterate — the in-kernel counterpart to walking /sys/class/<name>/ from userspace.
Why udev Keys Off Class
The class is the contract between the kernel and udev. When a device with a class is added, the KOBJ_ADD uevent carries (among other keys) SUBSYSTEM=<class-or-bus-name> and any variables the class’s dev_uevent callback added. udev rules match on SUBSYSTEM=="net", SUBSYSTEM=="block", SUBSYSTEM=="tty" and apply policy: predictable interface naming for net, by-id/by-uuid symlinks for block, permission and /dev ownership for tty and input. Because the class grouping is stable and role-based, a udev rule written against SUBSYSTEM=="net" works for every network device ever, regardless of the bus it arrived on. This division — kernel publishes role via class + uevent, userspace applies policy via rules — is covered in udev and Device Management and udev Rules and Predictable Device Naming; the netlink transport is Uevents and the Kernel-Userspace Netlink Channel.
Class vs Bus — the Distinction, Sharpened
| Bus (struct bus_type and Bus Registration) | Class (this note) | |
|---|---|---|
| Answers | How is the device connected? | What does the device do? |
| sysfs root | /sys/bus/<name>/ | /sys/class/<name>/ |
| Owns matching/probe? | Yes — .match() + .probe() bind a driver | No — a class never binds drivers |
| Membership | Exactly one bus per device (its attachment) | Zero or one class per device (its role) |
| Examples | pci, usb, i2c, spi, platform | net, block, tty, input, leds, hwmon |
The load-bearing difference: a bus drives device-to-driver binding; a class never does. A class is a view and a publication mechanism, not part of the probe machinery. A single device is commonly both — a PCI Ethernet card is on the pci bus (where its ixgbe driver binds) and in the net class (where userspace finds it as eth0). The bus.rst doc shows the bus side of the same picture: /sys/bus/pci/devices/ holds symlinks to PCI devices and /sys/bus/pci/drivers/ holds the drivers (bus.rst, v6.12) — note the drivers/ subdirectory a class never has.
Failure Modes and Common Misunderstandings
Expecting a class to bind a driver. Newcomers sometimes try to attach probe logic to a class. There is no .match() or .probe() in struct class — only a bus does binding. If you need driver binding, you need a bus (platform is the catch-all for synthetic devices).
Passing THIS_MODULE to class_create() on a 6.4+ kernel. A compile error (too many arguments) — the owner argument is gone (see the version section). The mirror failure is back-porting 6.4+ code to a ≤6.3 kernel, where the single-arg form does not exist.
class_register() returning -EINVAL for a namespaced class. If you set ns_type without namespace (or vice-versa), registration fails with -EINVAL by the explicit check in class_register(). Both or neither.
Forgetting device_destroy() symmetry. A device_create() is undone by device_destroy(class, devt), not by device_unregister() on a pointer you stashed (though that also works if you kept the pointer). Mismatching create/destroy on a class leaks the device or, worse, double-frees.
Treating /sys/class/<name>/<dev> as a real directory. It is a symlink. Tools that readlink it reach the physical home; tools that assume it is a directory and try to create files in the class dir are usually doing the wrong thing — per-device attributes belong on the device (via dev_groups), and the symlink resolves to exactly that directory.
Alternatives and When to Use Them
- No class at all. Many devices are purely bus devices with no functional role to publish; they have
dev->class == NULLand simply do not appear under/sys/class/. That is fine — a class is optional. struct device_typeinstead of (or alongside) a class. Adevice_typeis a lighter grouping that bundles sharedgroups, arelease, adevnode, andpmops without creating a/sys/class/view. Buses use it to sub-categorize devices (PCI distinguishes a bridge type from a normal device). Use adevice_typewhen you want shared per-device behavior but not a userspace-facing functional index; use a class when you want the/sys/class/<role>/index that udev can enumerate.- The miscdevice framework uses the shared
miscclass under the hood (/sys/class/misc/) and callsdevice_create()for you. If all you want is one character device with a/devnode, miscdevice is far less boilerplate than registering your own class and callingdevice_create()by hand — reach for your own class only when you genuinely have a family of devices that share a role (all the LEDs, all the IIO sensors).
Production Notes
Real subsystems are class-shaped end to end. The LED framework (drivers/leds/led-class.c) declares one static const struct class leds_class with dev_groups = led_groups, registers it once at module init, and every LED driver that calls led_classdev_register() lands a device in /sys/class/leds/<name>/ with brightness/max_brightness/trigger files supplied by dev_groups — uniform across hundreds of wildly different LED controllers. The hwmon, input, thermal, IIO, and tty subsystems all follow the same template: one class, dev_groups for the common attribute surface, device_create*() (or a framework wrapper) per device. The payoff is the one userspace cares about: ls /sys/class/hwmon/ lists every temperature/voltage sensor in the machine as a flat set, no matter which I²C bus, PCI device, or SoC block each actually hangs off — which is precisely the abstraction the kerneldoc promised: work with devices “based on what they do, rather than how they are connected.”
See Also
- struct device — the device structure a class groups;
dev->classis the back-pointer, anddevice_create()mints one - struct bus_type and Bus Registration — the connection-based grouping a class contrasts with; the owner of driver binding
- The Linux Device Model — the unified object graph both class and bus are views of
- Device Attributes and sysfs Files — the
dev_groups/class_groupsfiles a class exposes in sysfs - The miscdevice Framework — the easy path that uses the shared
miscclass anddevice_create()for you - sysfs — the filesystem
/sys/class/is projected into - udev and Device Management — userspace consumer that keys off
SUBSYSTEM=<class> - udev Rules and Predictable Device Naming — rules matching on class to name and permission devices
- devtmpfs and the dev Directory — where the
/devnodesdevice_create()requests are materialized - Major and Minor Numbers — the
dev_tyou pass todevice_create() - Linux Device Drivers and Device Model MOC — the parent map