How does Discovering Devices with sysfs work in Linux-Free Linux Device Drivers Course

PREV_LEC | NEXT_LEC

Discovering Devices with sysfs
A free Linux kernel development course lecture — how Linux exposes every device and driver in /sys, and how to add your own sysfs attribute
Chapter 8 · Lecture 3
Device Drivers
~20 min read

Before you can control a device from a Linux kernel driver, you first have to find out what the kernel already knows about it. This lecture is part of our free Linux kernel development course and continues the device driver chapter of our free Linux device drivers course. We finish the quick command-line tools for spotting devices, then go deep into sysfs — the single most useful place to look when you’re bringing up a new embedded board on the latest stable Linux kernel.

What You Will Learn

Major/minor numbers in /proc/devices ip link, lsusb, lspci for quick discovery The sysfs kernel-object model /sys/devices, /sys/class, /sys/block Writing your own sysfs attribute

Prerequisites

You should already be comfortable with major and minor numbers and character device nodes, covered in the previous lecture of this free embedded Linux course. A Linux VM or single-board computer running a recent kernel (6.x) and basic C are enough to follow the hands-on part.

Recap: Quick Ways to Spot a Device

Before reaching for sysfs, a few command-line tools give you a fast first look at what the kernel has registered:

  • cat /proc/devices — lists every registered driver by its major number and base name (for example a line like 8 sd tells you major 8 belongs to the SCSI/SATA disk driver, without telling you how many actual disks are attached).
  • ip link show — lists network interfaces the kernel currently sees, including loopback, Ethernet, and any USB network gadgets.
  • lsusb and lspci — enumerate devices sitting on the USB and PCI buses respectively, pulling vendor and product IDs straight from the bus.

All of these are useful for a first pass, but none of them tell you the full picture: how a device connects to its parent, which driver bound to it, or what runtime attributes that driver exposes. For that, you need sysfs.

What sysfs Actually Is

Since the unified device driver model landed in the 2.6 kernel series, every device and every driver in the system is represented internally as a kernel object (kobject). sysfs is simply that internal object graph exported as a filesystem, mounted at /sys:

  • A directory in sysfs corresponds to one kernel object — typically one device or one driver.
  • A file inside that directory is an attribute — a single readable, and sometimes writable, property of that object.
  • A symbolic link represents a relationship between two objects, most commonly a device pointing to the driver bound to it, or vice versa.

Because sysfs mirrors live kernel state, nothing you see there is stale — reading an attribute calls straight into the driver’s show() callback, and writing to a writable attribute calls its store() callback.

sysfs as a mirror of the kernel object graph
Kernel object graph sysfs on disk ┌────────────────────┐ ┌───────────────────────────┐ │ struct device │ ──────► │ /sys/devices/…/ep_dev0/ │ │ – name: ep_dev0 │ │ ├── uevent │ │ – attributes[] │ │ ├── ep_value (file) │ │ – parent bus/device │ │ ├── power/ │ └────────────────────┘ │ └── subsystem -> symlink │ └───────────────────────────┘ directory = one kobject | file = one attribute | symlink = one relationship

Top-Level Layout of /sys

Listing the root of sysfs on any modern Linux system shows a small, fixed set of directories:

# ls /sys
block  bus  class  dev  devices  firmware  fs  kernel  module  power

Of these, three matter most when you’re hunting for a specific device or driver: devices, class, and block. Each one presents the same underlying kernel objects from a different angle.

/sys/devices — the Hardware View

/sys/devices is the kernel’s record of every device discovered since boot, arranged by the physical or logical bus it hangs off. What you see here depends heavily on the board, but three directories are present on essentially every system:

  • system/ — devices at the core of the machine, such as CPUs and system clocks.
  • virtual/ — purely software, memory-backed devices. This is where /dev/null, /dev/zero, and the loopback network interface lo live, under virtual/mem and virtual/net.
  • platform/ — a catch-all for devices not attached through a discoverable hardware bus. On an embedded SoC, most on-chip peripherals (UARTs, I2C controllers, GPIO banks) show up here, because they’re described in the device tree rather than probed off a bus.

Anything else appears under a directory named after the real bus it sits on — a PCI root complex shows up as something like pci0000:00, a USB controller as usb1, and so on. The path from a bus root down to a specific device can get long, which is exactly why the other two views exist.

/sys/class — the Driver/Software View

/sys/class groups devices by the type of driver that manages them rather than by physical topology — a software-centric view. Serial ports appear under tty, network interfaces under net, input devices such as keyboards and touchscreens under input, and so on. Each entry inside a class directory is a symlink back to the device’s real location under /sys/devices.

Take a serial port as an example. On a typical x86 or ARM board with a hardware UART exposed as ttyS0:

# cd /sys/class/tty/ttyS0
# ls
close_delay  flags       line   uartclk
closing_wait io_type     port   uevent
custom_divisor iomem_base power  xmit_fifo_size
dev          iomem_reg_shift subsystem
device       irq         type

The device symlink points back to the hardware node under /sys/devices, and subsystem points back up to /sys/class/tty — that pair of symlinks is the “relationship” part of the kobject model. Everything else is a plain attribute: some, like xmit_fifo_size, are UART-specific; others, like irq and dev, are common to many device classes.

The dev attribute deserves a closer look:

# cat /sys/class/tty/ttyS0/dev
4:64

That’s the major:minor pair for this device node. It’s the driver that creates this attribute when it registers the interface, and it’s exactly what udev or mdev read to decide what device node to create in /dev when you aren’t relying on devtmpfs to do it automatically.

/sys/block — the Block Device View

/sys/block is the equivalent view for block devices, with one subdirectory per block device the kernel knows about:

# ls /sys/block
loop0  loop1  loop2  loop3  nvme0n1  sda  sr0

Drilling into a disk, such as sda, shows the device’s attributes alongside its partitions:

# cd /sys/block/sda
# ls
alignment_offset  device       queue     sda1
bdi               discard_alignment  range   sda2
capability        ext_range    removable size
dev               holders      slaves    stat
device -> ../../devices/pci0000:00/.../ata1/host0/target0:0:0/0:0:0:0

Between /sys/devices, /sys/class, and /sys/block, you can build a complete picture of what a Linux system has found, which driver is bound to it, and what runtime attributes that driver is willing to let you read or tune — no source code required.

Hands-On: Exposing Your Own sysfs Attribute

Reading sysfs is useful, but a device driver becomes far more debuggable once you control what shows up there. The cleanest way to expose a custom, driver-defined value is a DEVICE_ATTR-based sysfs attribute attached to a misc device. Here’s a small original demo driver, ep_sysfs_demo, that registers one misc device and one read/write attribute called ep_value.

// ep_sysfs_demo.c
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/kstrtox.h>

static int ep_value;

static ssize_t ep_value_show(struct device *dev,
                              struct device_attribute *attr, char *buf)
{
    return sysfs_emit(buf, "%d\n", ep_value);
}

static ssize_t ep_value_store(struct device *dev,
                               struct device_attribute *attr,
                               const char *buf, size_t count)
{
    int ret = kstrtoint(buf, 10, &ep_value);

    if (ret)
        return ret;

    return count;
}

static DEVICE_ATTR_RW(ep_value);

static struct attribute *ep_sysfs_attrs[] = {
    &dev_attr_ep_value.attr,
    NULL,
};
ATTRIBUTE_GROUPS(ep_sysfs);

static const struct file_operations ep_fops = {
    .owner = THIS_MODULE,
};

static struct miscdevice ep_sysfs_demo_dev = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "ep_sysfs_demo",
    .fops   = &ep_fops,
    .groups = ep_sysfs_groups,
};

static int __init ep_sysfs_init(void)
{
    return misc_register(&ep_sysfs_demo_dev);
}

static void __exit ep_sysfs_exit(void)
{
    misc_deregister(&ep_sysfs_demo_dev);
}

module_init(ep_sysfs_init);
module_exit(ep_sysfs_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala sysfs attribute demo");

A matching Makefile for out-of-tree builds:

obj-m += ep_sysfs_demo.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Build and load it, then watch the attribute appear under /sys/class/misc — the same class view we just explored:

$ make
$ sudo insmod ep_sysfs_demo.ko

$ ls /sys/class/misc/ep_sysfs_demo/
dev  device  ep_value  power  subsystem  uevent

$ cat /sys/class/misc/ep_sysfs_demo/dev
10:126

$ cat /sys/class/misc/ep_sysfs_demo/ep_value
0

$ echo 42 | sudo tee /sys/class/misc/ep_sysfs_demo/ep_value
42

$ cat /sys/class/misc/ep_sysfs_demo/ep_value
42

$ sudo rmmod ep_sysfs_demo

Every misc device shares major number 10, which is exactly what the dev attribute confirms. The ep_value file you see is a direct product of DEVICE_ATTR_RW — reading it calls ep_value_show(), writing it calls ep_value_store(), and no other code had to change to make the attribute show up under /sys/class/misc/ep_sysfs_demo/.

Comparing the Three Views

ViewOrganized byBest for
/sys/devicesPhysical/logical bus topologyUnderstanding how a device is wired into the system
/sys/classDriver subsystem (tty, net, input, misc…)Finding a device quickly when you know its type
/sys/blockBlock deviceInspecting disks, partitions, and storage attributes

Common Mistakes

  • Assuming a symlink is a copy. device and subsystem under a class entry are symlinks into /sys/devices — following them with cd -P or realpath shows the true location.
  • Forgetting attributes are callbacks, not files on disk. A read or write to a sysfs attribute runs driver code synchronously; a buggy show()/store() can block or crash the reading process.
  • Exposing more than one value per attribute file. The sysfs convention is one value per file — cramming multiple fields into one attribute breaks tooling that expects to parse a single value.
  • Not checking the return value of misc_register() or the store callback’s parsing function — both can fail, and silently ignoring that leads to confusing behavior later.

Best Practices

  • Prefer DEVICE_ATTR_RO / DEVICE_ATTR_RW and attribute groups over manually calling device_create_file() in every code path — groups are created and removed atomically with the device.
  • Always use sysfs_emit() rather than sprintf() in a show() callback; it enforces the PAGE_SIZE buffer limit sysfs guarantees.
  • Validate every write in a store() callback — sysfs input comes from user space and must be treated as untrusted.
  • Keep one logical value per attribute file so scripts and standard tools like udevadm can parse it predictably.

Summary

sysfs turns the kernel’s internal device-driver model into a plain filesystem: directories are kernel objects, files are attributes backed by driver callbacks, and symlinks capture the relationships between objects. /sys/devices shows you hardware topology, /sys/class shows you the software/driver view, and /sys/block gives you the same view specifically for storage. Once you can read this tree confidently, you can also add to it — as the ep_sysfs_demo driver shows, a single DEVICE_ATTR_RW declaration is enough to give your own driver a live, readable and writable knob in sysfs.

FAQ

What is the difference between /proc and /sys?

/proc is a much older, general-purpose interface originally meant for process information and later used for miscellaneous kernel settings. /sys was introduced specifically to represent the unified device driver model in a structured, one-value-per-file way, and is the preferred place for new device attributes.

Can I write to any file under /sys/class?

No — only files backed by a writable attribute (registered with something like DEVICE_ATTR_RW or DEVICE_ATTR_WO) accept writes. Read-only attributes reject write attempts with a permission error at the filesystem level.

Why does my custom device show up under /sys/class/misc instead of its own class?

misc_register() intentionally places every misc device under the shared misc class using major number 10. If you need your own class name, you would create a full character device with class_create() and cdev_add() instead.

Is sysfs available on every embedded Linux build?

Yes, as long as CONFIG_SYSFS is enabled, which it is by default in virtually every mainstream embedded Linux configuration, including Buildroot and Yocto default configs.

How is sysfs different from device tree?

Device tree describes hardware statically before boot so the kernel knows what to probe. sysfs is the live, dynamic result after probing — it reflects what the kernel actually found and bound drivers to, not what was merely declared.

What happens to a sysfs attribute if my module unloads?

Calling misc_deregister() (or class_destroy()/device_unregister() for a full class-based driver) removes the device and, with it, every attribute group registered against that device — nothing is left behind in /sys.

Continue the Free Linux Kernel Development Course

More device driver lectures, including probing, the driver/device match process, and platform drivers, are coming next in this free embedded Linux course.

Next Lecture Course Index

PREV_LEC | NEXT_LEC