The Useful Lie Behind Linux Plug-and-Play
Plug-and-play in Linux wasn’t a hardware breakthrough.It was an abstraction decision.
Most people think plug-and-play is a hardware miracle.
Insert a device. It works.
No IRQs. No jumpers. No configuration screens.
What almost no one sees is that this simplicity was not achieved by understanding hardware better - but by deliberately misrepresenting it.
Linux didn’t eliminate hardware diversity. It disciplined it.
This article examines a critical decision inside the kernel: the choice to model every device pathway as a bus - even when no bus exists. That decision reshaped how operating systems scale, evolve, and survive hardware chaos.
Before Plug-and-Play Was Normal
In the early 1990s, installing hardware was a nightmare.
You opened the case. Inserted a card.
You manually configured:
- Interrupt lines
- DMA channels
- I/O port addresses
If two devices wanted the same interrupt, neither worked. The system didn’t explain. It just failed.
This wasn’t user error. It was architectural.
Operating systems didn’t discover hardware. They assumed it.
Drivers were written with hardcoded expectations:
- Where a device lived
- How it announced itself
- What resources it consumed
Change the hardware layout, and the software broke.
Worse, physical differences dictated software structure. Two sound cards that did the same job required different drivers if they used different connection standards.
The deeper problem wasn’t bad tooling. It was a missing model.
Hardware was treated as a pile of exceptions instead of instances of a pattern.
What a Bus Really Provides
Physically, a bus is simple.
It’s a shared data pathway. Multiple devices. One communication fabric.
But logically, a bus does something far more important.
It allows enumeration.
At boot, the system can ask:
- Who’s here?
- What are you?
- What do you need?
PCI perfected this idea.
Each device exposes identification registers. The kernel scans the address space. Devices announce themselves. Drivers bind automatically.
No guessing. No humans in the loop.
USB extended this further. Devices could appear and disappear at runtime. The same pattern held:
- Detect
- Identify
- Match
- Initialize
Linux developers noticed something subtle.
This wasn’t about wires. It was about lifecycle.
Every device - regardless of function - follows the same story:
- It appears
- It identifies itself
- It binds to code
- It gets initialized
- It eventually disappears
That realization changed everything.
The Lie That Made Everything Work
Inside the kernel lives a structure called struct bus_type.
It defines a contract.
// The contract: what a bus must do, not what it is.
struct bus_type {
const char *name;
// Decide whether a driver can handle a device
int (*match)(struct device *dev, struct device_driver *drv);
// Called when a match succeeds
int (*probe)(struct device *dev);
// Called when a device or driver is removed
void (*remove)(struct device *dev);
};A bus, in Linux, is anything that can:
- Match devices to drivers
- Initialize them
- Tear them down
That’s it.
Just a behavior.
PCI implements this contract. USB implements it. I2C does too.
From the kernel’s perspective, they are indistinguishable.
Then Linux went further.
It applied this model to things that were not buses at all.
Some devices don’t sit on shared pathways.
They’re soldered directly onto the board.
- GPIO controllers
- Timers
- Sensors
- Interrupt controllers
There is no discovery protocol. No scanning. No negotiation.
Traditionally, these were handled with special-case code.
Linux invented a fiction.
The platform bus.
// A driver for a device that is not on any physical bus
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-sensor",
// Matches against "compatible" strings in Device Tree
.of_match_table = of_match_ptr(my_sensor_of_match),
},
};
// Registers this driver with the platform bus
module_platform_driver(my_driver); It has no wires. No physical presence. It exists only as an idea.
Hardware descriptions come from device trees or ACPI tables. The kernel reads them. Creates platform devices. Registers them on the platform bus.
From that point on, the illusion is complete.
These devices behave exactly like PCI or USB devices:
- They bind to drivers
- They appear in sysfs
- They participate in power management
- They follow the same lifecycle
The lie works because it’s consistent.
Matching Without Knowing the Hardware
Every bus defines a match function.
PCI matching:
// Match using PCI vendor and device IDs
static int pci_bus_match(struct device *dev,
struct device_driver *drv)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct pci_driver *pdrv = to_pci_driver(drv);
return pci_match_id(pdrv->id_table, pdev) != NULL;
}Platform matching:
// Match using Device Tree
static int platform_bus_match(struct device *dev,
struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
return of_device_is_compatible(
pdev->dev.of_node,
drv->of_match_table
);
}That function answers one question:
Can this driver handle this device?
PCI checks vendor and device IDs. USB checks class and interface descriptors. Platform devices check compatibility strings.
Different logic. Same pattern.
This uniformity enables something powerful.
Drivers and devices can appear in any order.
- Load the driver first? Devices bind later.
- Insert the device first? The driver binds when it loads.
The kernel doesn’t care how the match happens. Only that it can.
That separation - matching vs initialization - is what enables:
- Hot plug
- Driver replacement
- Failure recovery
- Live updates
It turns driver binding into a marketplace instead of a hardcoded decision.
Why Virtual Buses Matter
Once everything speaks the same language, software-only constructs gain superpowers.
RAID volumes. Encrypted disks. Virtual network interfaces.
They have no hardware. Yet they behave like devices.
They appear in /dev. They expose metadata. They integrate with user space tools.
Why?
Because they register on virtual buses.
The kernel doesn’t distinguish between “real” and “fake.” Only between buses, devices, and drivers.
This enables composition.
Virtual devices can depend on physical ones. Physical devices can expose virtual sub-devices.
Security frameworks hook once and cover everything. Power management walks one tree and suspends the world cleanly.
Uniformity unlocks scale.
The Cost of Pretending
This abstraction is not free.
Some hardware resists the model.
Hot plug on PCI Express is awkward. USB hub recursion creates edge cases. Platform devices get abused for things that aren’t devices at all.
Performance suffers from indirection. Debugging requires tracing multiple layers.
New interconnects blur boundaries between memory, devices, and processors.
The abstraction leaks.
But the alternative is worse.
Without the bus model:
- Every device needs custom infrastructure
- Every new standard fractures the kernel
- Plug-and-play collapses under special cases
Imperfect uniformity beats perfect fragmentation.
What This Teaches Us About Abstraction
The bus abstraction succeeded because it ignored surface differences.
It focused on behavior, not form.
Devices look different. They connect differently. They serve different purposes.
But they all need:
- Discovery
- Matching
- Initialization
- Cleanup
That lifecycle is the abstraction.
This pattern repeats across computing.
Virtual memory lies about RAM. File systems lie about disks. Networks lie about reliability.
These lies work because they:
- Isolate complexity
- Define clear contracts
- Stay small enough to reason about
The bus model doesn’t try to do everything. It captures only what all devices truly share.
That restraint is why it lasts.
Hardware diversity is unavoidable.
Linux didn’t fight it. It contained it.
By modeling every device pathway as a bus - even when no bus exists - the kernel created a uniform device lifecycle. Discovery became systematic. Driver binding became automatic. Plug-and-play became inevitable.
The abstraction is invisible to users and unnoticed by most developers. Yet it shapes everything from phones to supercomputers.
This is how complex systems survive reality.
Not by modeling it perfectly. But by choosing a useful lie — and enforcing it relentlessly.
Everything is a bus, if you choose to see it that way.