- build
make- load order
monitor the udev output using
udevadm monitor
sudo insmod mybus.ko
sudo insmod mydevice.ko
sudo insmod mydriver.ko- remove order
sudo rmmod mydriver
sudo rmmod mydevice
sudo rmmod mybus +------------------+
| mybus.ko |
| (bus_type) |
+------------------+
|
--------------------------------
| |
v v
+------------------+ +------------------+
| mydevice.ko | | mydriver.ko |
| registers device | | registers driver |
| mydev0 | | name = mydev0 |
+------------------+ +------------------+
| |
------------ match() ----------
|
v
probe()
sudo insmod mybus.koKernel log:
mybus: init
mybus: registered
This means:
bus_register()created/sys/bus/mybusdevice_register()created/sys/devices/mybus0
ls /sys/bus/mybus/Output:
devices drivers drivers_autoprobe drivers_probe uevent version
| Entry | Description |
|---|---|
| devices | devices attached to this bus |
| drivers | drivers registered to this bus |
| drivers_autoprobe | automatic probing control |
| drivers_probe | manual probe trigger |
| uevent | udev event interface |
| version | custom sysfs attribute |
cat /sys/bus/mybus/version output:
1.0
This is a custom bus attribute exported through sysfs.
ls /sys/bus/mybus/devices/
No output yet.
Reason : no devices registered yet
ls /sys/bus/mybus/drivers/
No output yet.
Reason: no drivers registered yet
ls /sys/devices/mybus0/
Output:
power uevent
Explanation: mybus0 is the parent device created by:
static struct device my_bus_device
All devices on this custom bus become children of this parent device.
KERNEL add /bus/mybus
KERNEL add /module/mybus
UDEV add /bus/mybus
UDEV add /module/mybus
| Event | Meaning |
|---|---|
| add /bus/mybus | new bus registered |
| add /module/mybus | kernel module loaded |
sudo insmod mydevice.koKernel log:
mydevice: init
mydevice: device registered
This registers:
mydev0
onto:
mybus0
ls /sys/bus/mybus/devices/Output:
mydev0
The device is now registered to the bus.
cat /sys/bus/mybus/devices/mydev0/typeOutput:
type : mydev0
This attribute comes from:
DEVICE_ATTR(type, 0444, my_show_type, NULL);ls /sys/devices/mybus0/Output:
mydev0 power uevent
mydev0 becomes a child device under:
/sys/devices/mybus0/
because:
mydev->dev.parent = &my_bus_device;ls /sys/bus/mybus/drivers/
Still empty.
Reason:
- no driver loaded yet
- device exists without a driver
This is a very important Linux concept:
devices can exist before drivers
The kernel keeps the device registered and waits for a matching driver.
KERNEL add /devices/mybus0/mydev0
UDEV add /devices/mybus0/mydev0
| Event | Meaning |
|---|---|
| add /devices/mybus0/mydev0 | device created in sysfs |
sudo insmod mydriver.koKernel log:
mydriver: init
mybus: match called
mydriver: probe called for mydev0
When the driver registers:
driver_register()the kernel automatically:
- scans devices on the bus
- calls the bus match() function
- finds matching device
- binds driver to device
- calls probe()
driver_register()
↓
bus match()
↓
match success
↓
probe()
ls /sys/bus/mybus/drivers/Output:
mydev0
The driver is now attached to the device.
Notice this log order:
mybus: match called
mydriver: probe called for mydev0
This proves:
probe() is NOT called directly by the driver
Instead:
the Linux driver core calls probe()
only after a successful match.
This is one of the most important Linux Device Model concepts.
KERNEL bind /devices/mybus0/mydev0
UDEV bind /devices/mybus0/mydev0
The driver and device are now connected.
sudo rmmod mydriver
sudo rmmod mydevice
sudo rmmod mybusCorrect removal order:
driver → device → bus
Reason:
- driver depends on device/bus
- device depends on bus
kobject
↓
bus
↓
device
↓
driver
↓
probe/remove
↓
sysfs
This example implements exactly that architecture.
Creating the Bus
A bus is the communication channel between
devicesand theprocessor.
In this example:
struct bus_type my_bus_type = {
.name = "mybus",
.match = my_match,
};This creates a custom Linux bus.
The bus core internally creates:
/sys/bus/mybus/
when this runs:
bus_register(&my_bus_type);In mybus.h we declare:
extern struct bus_type my_bus_type;because:
- the real object is defined in
mybus.c - other files need access to it
Especially:
mydevice.cmydriver.c
need to attach themselves to this bus.
This is standard Linux kernel modular design.
.match()is called whenever a new device or driver appears on the bus.
Here:
static int my_match(struct device *dev,
struct device_driver *drv)
{
return !strcmp(dev_name(dev), drv->name);
}This means:
If device name == driver name
→ match successful
→ call probe()
This part is extremely important conceptually.
static struct device my_bus_device = {
.init_name = "mybus0",
.release = my_bus_release,
};Every device exists in a hierarchy.
The bus itself is represented as a parent device.
This becomes:
/sys/devices/mybus0/
Then child devices appear under it.
This is how Linux builds the device tree hierarchy internally.
struct deviceis the core representation of a device.
here:
struct my_device {
char *name;
struct device dev;
};This demonstrates the most important Linux pattern:
embed generic kernel object
inside subsystem-specific object
Linux uses composition instead of inheritance.
By embedding:
struct device dev;our object automatically gains:
- sysfs support
- reference counting
- uevents
- parent hierarchy
- driver binding support
because struct device already contains:
- kobject
- bus pointer
- driver pointer
- release callback
In mydevice.c :
my_register_device(&mydev);we eventually call:
device_register(&mydev->dev);Before registering:
mydev->dev.bus = &my_bus_type;This attaches the device to our bus.
And:
mydev->dev.parent = &my_bus_device;creates the hierarchy:
mybus0
└── mydev0
visible in:
/sys/devices/
in mybus.c:
dev_set_name(&mydev->dev, mydev->name);sets the sysfs directory name.
So Linux creates:
/sys/devices/mybus0/mydev0/
This directly comes from the embedded kobject.
In mydriver.c:
static struct my_driver mydrv = {
.driver = {
.name = "mydev0",
},
};Again:
- embed generic object
- extend with bus-specific wrapper
struct device_driverthis is the generic driver representation.
When this runs:
drv->driver.bus = &my_bus_type;the driver becomes part of:
/sys/bus/mybus/drivers/
through call:
driver_register()Now the important Linux Device Model moment happens.
Kernel internally does:
new driver registered
↓
scan all devices on this bus
↓
call my_match()
↓
match successful
↓
call probe()
This is the real Linux driver lifecycle.
Not:
module_init() directly controls hardware
but:
kernel binding engine controls driver lifecycle
This runs automatically:
static int my_probe(struct device *dev)because:
- device exists
- driver exists
- match succeeded
The kernel found our hardware and matched it with this driver.
That is exactly what happened here.
The kernel passes:
struct device *devbecause the Linux device model is generic.
The bus core does not know:
- our custom structs
- our hardware
- our driver internals
It only understands:
generic device objects
Suppose we do:
struct my_device *mydev;
mydev = container_of(dev, struct my_device, dev);This walks backward from:
embedded struct device
to:
outer struct my_device
This is one of the core Linux kernel programming patterns.
After everything loads:
/sys/
├── bus/
│ └── mybus/
│ ├── devices/
│ │ └── mydev0
│ └── drivers/
│ └── mydev0
│
└── devices/
└── mybus0/
└── mydev0/
This directly visualizes:
- bus
- device
- driver
- hierarchy
- binding
when:
sudo rmmod mydriverhappens:
Kernel does:
unbind driver
↓
call remove()
↓
detach from device
The kernel is detaching from hardware. Leave cleanly.
This example teaches the core Linux truth:
Drivers do not own devices.
The kernel owns devices.
Drivers attach to them dynamically.
That is the real Linux Device Model philosophy.
In this tutorial, matching is done manually by comparing:
device name == driver name
inside our custom .match() function.
static int my_match(struct device *dev,
struct device_driver *drv)
{
return !strcmp(dev_name(dev), drv->name);
}But in real Linux systems, matching is usually performed automatically using
identifiersprovided by the bus subsystem.
For embedded systems, the most common mechanism is the Device Tree compatible string.
Watch the full Linux SPI explanation video here
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_spi_driver",
.of_match_table = my_spi_of_match,
},
};Device Tree node:
spi_device@0 {
compatible = "vendor,my-spi-device";
};The SPI bus core compares:
DT compatible string ⇔ driver's of_match_table
If matched:
SPI bus ➡ calls probe()
Watch the full Linux I2C explanation video here
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "vendor,my-i2c-device" },
{ }
};
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
.of_match_table = my_i2c_of_match,
},
};real subsystems like:
- SPI
- I2C
- Platform bus
- PCI
- USB
usually provide their own matching mechanisms internally.
So our custom bus example simplifies the matching process to help explain