Device Attributes and sysfs Files
A device attribute is the device-model’s typed wrapper around a raw sysfs file: a
struct device_attributebundles a filename and permission bits (an embeddedstruct attribute) with ashow/storecallback pair whose signatures take astruct device *directly, so a driver author writesstatic ssize_t speed_show(struct device *dev, struct device_attribute *attr, char *buf)instead of fishing the device out of a genericstruct kobjectby hand (device.h, v6.12). TheDEVICE_ATTR_RO/RW/WOmacros declare these in one line; attribute groups (struct attribute_group, materialized byATTRIBUTE_GROUPS) bundle many attributes so the driver core creates and destroys them atomically alongside the device, the driver, the bus, or the class that owns them. This note covers the device-model-specific attribute layer. The generic sysfs/kobject/kernfs plumbing and the page-buffershow/storecontract live in sysfs and the Kernel Object Hierarchy — read that first; this note assumes it and extends it downward intostruct device. Pinned to the 6.12 Long-Term-Support (LTS) kernel (released 2024-11-17).
The Generic Contract, in One Paragraph
Everything in /sys is a kobject (kernel object) projected as a directory, and every file in that directory is a struct attribute — a two-field struct holding a name and umode_t mode (permission bits). A read or write on the file is routed by the Virtual File System through the kobject’s type (kobj_type), whose sysfs_ops supplies a generic show(struct kobject *, struct attribute *, char *) / store(...) pair. sysfs hands show() a one-page (PAGE_SIZE, 4096 bytes on x86) buffer and calls it exactly once per read; the method must fill the entire value in one shot, and should format it with the bounds-checked sysfs_emit(). The cardinal rule is one value per file (sysfs.rst). All of that — kernfs, the page-buffer protocol, sysfs_emit, __ATTR_RO/RW/WO, the kobject/kset/ktype triad — is explained in sysfs and the Kernel Object Hierarchy and is not re-derived here. What follows is the layer the device model adds on top: a device_attribute whose callbacks already carry a struct device *, and groups that are wired to the device’s groups, the driver’s dev_groups, the bus’s dev_groups, and the class’s dev_groups so files appear and vanish at exactly the right lifecycle moment.
Mental Model
Think of a device attribute as a down-cast of the generic attribute machinery, performed once, in the driver core, so every driver doesn’t have to. The kernel registers one generic sysfs_ops for all devices (dev_sysfs_ops, whose show is dev_attr_show). That single function uses container_of() to recover the struct device_attribute from the generic struct attribute, and kobj_to_dev() to recover the struct device from the generic struct kobject, then calls your attribute’s show(dev, dev_attr, buf). So the driver author never touches a struct kobject — by the time their callback runs, the generic types have already been translated to the device types. Groups are the second idea: rather than registering attributes one at a time (which is racy and leaks on error paths), you declare an array of attributes, wrap it in a struct attribute_group, and point one of the four *_groups fields at it. The driver core then walks the group at the right lifecycle event and creates every file in it as a unit — and removes them as a unit when the device unregisters, the driver unbinds, or the bus/class goes away.
flowchart TB RD["userspace: cat /sys/.../speed"] --> VFS["VFS read()"] VFS --> KOBJ["kobject sysfs_ops.show<br/>= dev_attr_show()"] KOBJ -->|"container_of(attr,<br/>device_attribute, attr)"| DA["struct device_attribute"] KOBJ -->|"kobj_to_dev(kobj)"| DEV["struct device *dev"] DA -->|"dev_attr->show(dev, dev_attr, buf)"| CB["your speed_show(dev, attr, buf)"] CB -->|"sysfs_emit(buf, ...)"| RD subgraph GRP["Where a device's files come from (lifecycle)"] CLSG["class->dev_groups<br/>(at device_add)"] TYPG["dev->type->groups<br/>(at device_add)"] DEVG["dev->groups<br/>(at device_add)"] BUSG["bus->dev_groups<br/>(when joining bus)"] DRVG["driver->dev_groups<br/>(at bind/probe)"] end GRP -.->|"each materialized as files"| DEV
The two halves of a device attribute. What it shows (top): a cat on a sysfs file is dispatched through the one generic dev_attr_show, which container_ofs up to your device_attribute and kobj_to_devs up to the struct device, then calls your typed show — the down-cast you never write yourself. (bottom): the five sources of a device’s attribute files and when each is created — class/type/device groups at device_add(), the bus’s device groups when the device joins the bus, and the driver’s dev_groups only at bind time. The insight: which *_groups field you choose decides the lifetime of the files, not just their content.
struct device_attribute: Why the Signature Differs
The base struct attribute carries no behavior — just name and mode. The device model’s wrapper, from include/linux/device.h, embeds it and adds the two callbacks:
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};Contrast this with the raw kobject sysfs_ops signature, show(struct kobject *, struct attribute *, char *). The device-model show/store take a struct device * and a struct device_attribute * instead of the generic kobject and attribute pointers. That difference is the whole point of the layer. The translation happens in one place — dev_attr_show in drivers/base/core.c:
#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)
static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,
char *buf)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;
if (dev_attr->show)
ret = dev_attr->show(dev, dev_attr, buf);
if (ret >= (ssize_t)PAGE_SIZE) {
printk("dev_attr_show: %pS returned bad count\n",
dev_attr->show);
}
return ret;
}
static const struct sysfs_ops dev_sysfs_ops = {
.show = dev_attr_show,
.store = dev_attr_store,
};Walk it: to_dev_attr(attr) is container_of(attr, struct device_attribute, attr) — given the address of the embedded attr field, it computes the address of the enclosing device_attribute by subtracting the field’s offset. kobj_to_dev(kobj) does the same trick to recover the struct device whose kobj member was passed in. Then it calls your show(dev, dev_attr, buf). The if (ret >= PAGE_SIZE) check catches the classic bug of a show that overran its one-page buffer (see sysfs and the Kernel Object Hierarchy for the page-buffer contract). dev_sysfs_ops is registered once, on the device_ktype that every struct device shares — so this dispatch is the single chokepoint for all device attribute reads in the kernel.
Declaring Attributes: DEVICE_ATTR and Its Shorthands
Hand-initializing a device_attribute is verbose, so device.h provides macros. The fully explicit one:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)DEVICE_ATTR(foo, 0644, foo_show, foo_store) expands (per the doc comment) to a struct device_attribute dev_attr_foo with .attr = { .name = "foo", .mode = 0644 }, .show = foo_show, .store = foo_store. The dev_attr_ prefix on the symbol name is a convention the macros enforce — group arrays and device_create_file() both expect to find dev_attr_foo. The convenience trio, which is what almost all modern drivers use, hard-codes the mode and infers the callback names:
#define DEVICE_ATTR_RW(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RW(_name) /* 0644, foo_show + foo_store */
#define DEVICE_ATTR_RO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RO(_name) /* 0444, foo_show only */
#define DEVICE_ATTR_WO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_WO(_name) /* 0200, foo_store only */The mode bits are not arbitrary: __ATTR_RO forces 0444 (world-readable, no write), __ATTR_WO forces 0200 (root write only, no read), and __ATTR_RW forces 0644 (world-read, root-write) (sysfs.h). So DEVICE_ATTR_RW(speed) requires you to have defined speed_show and speed_store with the device-model signatures; the macro stitches them together and names the result dev_attr_speed. There are also DEVICE_ATTR_ADMIN_RO/ADMIN_RW (mode 0400/0600, root-only even for read) for sensitive values, and self-writing wrappers DEVICE_ULONG_ATTR/DEVICE_INT_ATTR/DEVICE_BOOL_ATTR/DEVICE_STRING_ATTR_RO that bind the file directly to a variable via a struct dev_ext_attribute (which carries a void *var) and the prebuilt device_show_ulong/device_store_ulong/etc. handlers — no custom callback needed at all for a simple scalar knob.
The sibling subsystems get parallel macro families with the same embedding trick but their own object pointer in the callback: BUS_ATTR_RO/RW/WO define a struct bus_attribute (callback takes a struct bus_type *), DRIVER_ATTR_RO/RW/WO a struct driver_attribute (takes struct device_driver *), and CLASS_ATTR_RO/RW/WO a struct class_attribute (takes struct class *) (bus.h, driver.h, class.h). Same pattern, four levels of the object hierarchy.
Attribute Groups: Atomic Create and Remove
Registering attributes one-by-one with device_create_file() is discouraged: each call is a separate syscall-like operation, the error-unwinding is tedious, and there is a window where some files exist and others don’t. The modern idiom is the group, from sysfs.h:
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *, struct attribute *, int);
umode_t (*is_bin_visible)(struct kobject *, struct bin_attribute *, int);
struct attribute **attrs;
struct bin_attribute **bin_attrs;
};attrs is a NULL-terminated array of plain struct attribute *. If name is set, the group’s files go in a subdirectory of that name (e.g. power/); if name is NULL, they land directly in the device’s directory. The ATTRIBUTE_GROUPS(foo) macro is the boilerplate-killer:
#define ATTRIBUTE_GROUPS(_name) \
static const struct attribute_group _name##_group = { \
.attrs = _name##_attrs, \
}; \
__ATTRIBUTE_GROUPS(_name) /* -> static const struct attribute_group *_name##_groups[] = { &_name##_group, NULL }; */So given an array static struct attribute *foo_attrs[] = { &dev_attr_speed.attr, &dev_attr_duplex.attr, NULL };, the single line ATTRIBUTE_GROUPS(foo) produces both foo_group (the group) and foo_groups (the NULL-terminated array of group pointers) — exactly the type the *_groups fields below expect.
The Four *_groups Fields and Their Lifetimes
This is the device-model-specific heart of the matter: which field you attach a group to determines when its files exist. The driver core assembles a device’s attributes from several sources at device_add() time, in drivers/base/core.c:
static int device_add_attrs(struct device *dev)
{
const struct class *class = dev->class;
const struct device_type *type = dev->type;
int error;
if (class) {
error = device_add_groups(dev, class->dev_groups); /* class-wide device attrs */
...
}
if (type) {
error = device_add_groups(dev, type->groups); /* device_type attrs */
...
}
error = device_add_groups(dev, dev->groups); /* this instance's attrs */
...
}So three of the four sources fire when the device is added: dev->class->dev_groups (attributes every device of this class gets — e.g. every net device gets address, mtu), dev->type->groups (attributes for this device_type), and dev->groups (attributes this specific instance was given). All three are torn down symmetrically in device_remove_attrs() when the device unregisters.
The bus’s device attributes are added separately, when the device joins the bus, in drivers/base/bus.c: device_add_groups(dev, sp->bus->dev_groups). Every device on, say, the PCI bus gets the PCI bus’s dev_groups (e.g. vendor, device, config).
The driver’s device attributes are the interesting case: driver->dev_groups is added only at bind time, inside really_probe() in drivers/base/dd.c:
ret = device_add_groups(dev, drv->dev_groups);
if (ret) {
dev_err(dev, "device_add_groups() failed\n");
goto dev_groups_failed;
}and removed at unbind (device_remove_groups(dev, dev->driver->dev_groups)). The doc comment on struct device_driver says it plainly: dev_groups are “Additional attributes attached to device instance once it is bound to the driver” (driver.h). This is why dev_groups on a driver auto-creates and auto-removes files at bind/unbind — the files physically appear in the device’s sysfs directory the instant .probe() succeeds and vanish the instant the driver is unbound (e.g. echo <dev> > /sys/bus/.../driver/unbind). Note the distinction from the driver’s own groups field, which creates attributes on the driver’s directory (/sys/bus/.../drivers/<drv>/), not the device’s. The full per-device attribute set is therefore the union: class device groups ∪ type groups ∪ instance groups ∪ bus device groups ∪ (bound) driver device groups.
Conditional Visibility: is_visible
Sometimes an attribute should exist only on some devices of a class — a temperature sensor’s crit file only if the hardware has a critical threshold. Rather than building different groups per device, you keep one group and supply an is_visible callback. For each attribute in the group, the core calls is_visible(kobj, attr, n) (where n is the attribute’s index in the array) and uses the returned umode_t as the file’s mode — returning 0 omits the file entirely (sysfs.h). The doc: “Must return 0 if an attribute is not visible. The returned value will replace static permissions defined in struct attribute.” A sketch:
static umode_t sensor_is_visible(struct kobject *kobj, struct attribute *a, int n)
{
struct device *dev = kobj_to_dev(kobj);
struct sensor *s = dev_get_drvdata(dev);
if (a == &dev_attr_crit.attr && !s->has_crit_threshold)
return 0; /* hide crit on hardware that lacks it */
return a->mode; /* otherwise use the declared mode */
}
static const struct attribute_group sensor_group = {
.attrs = sensor_attrs,
.is_visible = sensor_is_visible,
};is_bin_visible is the equivalent hook for binary attributes. For named groups, is_visible/is_bin_visible can additionally return SYSFS_GROUP_INVISIBLE to hide the entire subdirectory; the DEFINE_SYSFS_GROUP_VISIBLE / SYSFS_GROUP_VISIBLE helper macros wire up separate group-level and attribute-level predicates (sysfs.h).
Binary Attributes
Text-with-one-value is the rule, but some data is inherently binary and larger than a page — a device’s raw EEPROM, a PCI device’s config space, firmware blobs. For these, the device model uses struct bin_attribute (sysfs.h):
struct bin_attribute {
struct attribute attr;
size_t size;
void *private;
struct address_space *(*f_mapping)(void);
ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
loff_t (*llseek)(struct file *, struct kobject *, struct bin_attribute *, loff_t, int);
int (*mmap)(struct file *, struct kobject *, struct bin_attribute *, struct vm_area_struct *);
};The key differences from a text attribute: read/write take an explicit loff_t offset and a size_t count, so the file is seekable and chunked rather than the one-shot page-buffer model — this is how /sys/bus/pci/devices/.../config lets you pread() 4 bytes at offset 0x10 to read a Base Address Register. size advertises the total length; an mmap hook can even map the data directly. Declared with BIN_ATTR_RO(name, size) / BIN_ATTR_RW(name, size), grouped with bin_attrs in an attribute_group (or the BIN_ATTRIBUTE_GROUPS macro), and attached to a device with device_create_bin_file() (device.h). Binary attributes deliberately escape the one-value-per-file discipline because their content is opaque bytes, not a human-parseable value.
A Complete Worked Example
A minimal driver exposing one read-only and one read-write device attribute, grouped:
/* show: read the current value out of driver state */
static ssize_t speed_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct mydev *m = dev_get_drvdata(dev);
return sysfs_emit(buf, "%u\n", m->link_speed); /* one value, bounds-checked */
}
static DEVICE_ATTR_RO(speed); /* -> dev_attr_speed, mode 0444 */
/* store: parse one value back into driver state */
static ssize_t mtu_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct mydev *m = dev_get_drvdata(dev);
unsigned int v;
int ret = kstrtouint(buf, 10, &v); /* parse the whole buffer */
if (ret)
return ret; /* bad input -> error to userspace */
m->mtu = v;
return count; /* consumed all bytes */
}
static ssize_t mtu_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct mydev *m = dev_get_drvdata(dev);
return sysfs_emit(buf, "%u\n", m->mtu);
}
static DEVICE_ATTR_RW(mtu); /* -> dev_attr_mtu, mode 0644 */
static struct attribute *mydev_attrs[] = {
&dev_attr_speed.attr,
&dev_attr_mtu.attr,
NULL, /* NULL terminator is mandatory */
};
ATTRIBUTE_GROUPS(mydev); /* defines mydev_group + mydev_groups */
/* attach to *every device this driver binds*, created at probe, removed at unbind: */
static struct platform_driver mydev_driver = {
.driver = {
.name = "mydev",
.dev_groups = mydev_groups, /* <-- bind-time attribute set */
},
.probe = mydev_probe,
.remove = mydev_remove,
};Commentary: speed_show and mtu_show use sysfs_emit() — the bounds-checked formatter mandated for new code (see sysfs and the Kernel Object Hierarchy). mtu_store parses the entire buffer with kstrtouint and returns count to signal it consumed everything; returning a short count or an error has defined meanings. The two attributes are gathered into mydev_attrs (NULL-terminated), wrapped by ATTRIBUTE_GROUPS(mydev), and the resulting mydev_groups is attached to the driver’s dev_groups. The payoff: /sys/devices/.../speed and /sys/devices/.../mtu materialize the moment mydev_probe succeeds and disappear the moment the driver unbinds — no manual device_create_file/device_remove_file bookkeeping, no error-path leaks. Had the same mydev_groups been attached to dev->groups instead, the files would instead live for the device’s whole lifetime regardless of which driver (if any) is bound.
Failure Modes and Common Misunderstandings
- Wrong callback signature. Copying a raw-kobject
show(struct kobject *, struct attribute *, char *)into a place expecting adevice_attributeshow(struct device *, struct device_attribute *, char *)compiles only if the function-pointer types happen to match by luck and otherwise fails — the two are distinct. TheDEVICE_ATTR_RO/RW/WOmacros prevent the mismatch by requiringname_show/name_storewith the device-model signatures; use them rather than hand-rolling. - Forgetting the NULL terminator in the
attrs[]array. The core walks the array until it sees NULL; a missing terminator reads off the end and corrupts memory or creates garbage files.ATTRIBUTE_GROUPSdoes not add it for you — you must end the array withNULL. - Attaching to the wrong
*_groupsfield. Putting a group ondev->groupswhen you wanted bind-time lifetime (it should have been the driver’sdev_groups) means the files persist after unbind and reappear stale; putting it on the driver’sgroups(driver directory) when you meantdev_groups(device directory) puts the files in the wrong place entirely. The four fields look similar and are a frequent mix-up. is_visiblereturning the wrong type. It returns aumode_tmode, not a bool. Returning1makes a file with mode0001(world-execute only — unreadable garbage); you must returnattr->mode(or0to hide). This is a classic bug.- Racing attribute creation against probe. Creating attributes manually inside
.probe()withdevice_create_file()races with userspace that is already watching the [[Uevents and the Kernel-Userspace Netlink Channel|KOBJ_ADDuevent]] — udev may read the device before the files exist. Usingdev_groupsavoids this because the core creates the group before it emits the bind-completion signal. This is a real reason groups are preferred over manual creation. - Exceeding one page in a binary-ish text attribute. If a value genuinely needs more than ~4 KB or arbitrary offsets, it is a
bin_attribute, not a text attribute. Forcing large structured data through a textshow()truncates atPAGE_SIZE(thedev_attr_showprintkwarning above fires).
Alternatives and When to Choose Them
- Manual
device_create_file()/device_remove_file()— still exists for the rare case where an attribute’s existence depends on runtime state discovered afterdevice_add()andis_visibleis awkward. The cost is manual lifetime management and the probe race above; prefer groups. debugfsfiles — for throwaway debugging knobs with no Application Binary Interface (ABI) promise. Device attributes in/sysare ABI: once shipped, tools parse the exact path and single-value format, and the kernel cannot rename or restructure them without breaking userspace (every new sysfs attribute must be documented underDocumentation/ABI, per sysfs-rules.rst). If a value is debug-only, it belongs indebugfs, not as a device attribute.ioctlon the device’s/devnode — for complex, structured, or high-frequency control that does not fit the one-value-per-file model. Device attributes are for simple scalar knobs and status; anioctlis for transactions. See file_operations and the Character Driver Interface.- A
bin_attribute— when the data is genuinely binary and/or larger than a page (config space, EEPROM, firmware). Covered above.
Production Notes
The device-attribute layer is the everyday face of Linux hardware management. ethtool and ip read /sys/class/net/<iface>/speed, mtu, address — class dev_groups on the net class. lspci -v and libpci read PCI bus dev_groups (vendor, device, class) and the config binary attribute. CPU frequency governors write /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed — a DEVICE_ATTR-style store. Hardware-monitoring (hwmon/lm-sensors) is built almost entirely on conditionally-visible groups: a chip driver declares one big group of tempN_input, tempN_crit, fanN_input, … and an is_visible callback that exposes only the channels the specific chip actually has — this is the canonical real-world use of is_visible, and reading the drivers/hwmon/ tree is the best way to see groups and conditional visibility used at scale. Because these are ABI, the stability matters operationally: a monitoring agent that parses /sys/class/hwmon/hwmon0/temp1_input can rely on that path and its single-integer (millidegree) format across kernel versions.
See Also
- sysfs and the Kernel Object Hierarchy — the generic kobject/kernfs/sysfs machinery and the page-buffer
show/storecontract this note builds on (read first) - Uevents and the Kernel-Userspace Netlink Channel — the push-notification channel that fires when groups appear (
KOBJ_ADD) so udev can read the new attributes - struct device — the object these attributes hang off
- Device Classes —
class->dev_groupsis where class-wide device attributes come from - struct bus_type and Bus Registration —
bus->dev_groups/bus_groups/drv_groups - struct device_driver — the
dev_groupsvsgroupsdistinction lives here - Driver Binding and the Probe Flow — where the driver’s
dev_groupsare created (bind) and destroyed (unbind) - udev and Device Management — the userspace consumer that reads these files
- MOC: Linux Device Drivers and Device Model MOC