Linux Multi‑Kernel Explained: From Device Drivers to Virtual Environments
September 25, 2025
When people talk about "Linux kernels," they often mean the Linux kernel: the giant open-source core that powers everything from Android phones to supercomputers. But in practice, developers often deal with multiple kernel instances—whether through virtual machines, cross-compilation, or testing drivers across different kernel versions. This is where you’ll often hear the term multi-kernel workflow. For device driver developers in particular, being able to spin up, swap, and experiment with multiple kernel environments is not just convenient—it’s practically a survival skill.
In this post, we’re going to explore what Linux multi-kernel really means in practice. We’ll focus on the hands-on side: how you can create safe sandboxes for kernel hacking, how device drivers tie into the kernel, and how tools like Multipass make juggling multiple kernels painless. We’ll also dive into the relationship between user applications and kernel modules, and we’ll walk through a small project that demonstrates the full cycle: writing a simple kernel module, compiling it, injecting it, and then interacting with it from user space.
This is a long read—around 3,300 words—so grab a coffee, settle in, and let’s talk about Linux multi-kernel development like two friends geeking out about low-level systems.
What Does “Multi-Kernel” Mean?
Let’s clear up the terminology first. “Multi-kernel” can mean different things depending on the context:
- Academic OS research: There’s a research project called Barrelfish that defines a “multikernel” as an OS architecture designed for multicore scalability. Not our focus here.
- Practical Linux development: For Linux devs, “multi-kernel” usually refers to working with multiple Linux kernel builds or instances in parallel. Developers often need to test modules across different kernel versions, or use multiple isolated environments each running its own kernel.
- Virtualized sandboxes: With virtual machines and hypervisors, you can spin up multiple Linux kernels side by side, even on a Mac or Windows host.
So in this article, when we say Linux multi-kernel, we mean the practical developer workflow of juggling multiple kernel instances for driver development, testing, and learning.
Why Multi-Kernel Development Matters
If you’ve ever tinkered with Linux device drivers, you know how easy it is to brick your system with a single buggy line of code. When your code runs in kernel space, there are no guardrails—the kernel doesn’t tolerate errors. A faulty pointer dereference or infinite loop can bring down the entire system.
That’s why multi-kernel workflows are so important:
- Safety: You don’t risk trashing your host OS. If your kernel experiment crashes, it only affects the sandbox instance.
- Reproducibility: You can test the same driver on multiple kernel versions without reinstalling your main OS.
- Reset-ability: If an instance gets messed up, just nuke it and spin up a new one.
- Isolation: You can keep your work environment clean, separating experimental kernel hacking from day-to-day development.
This is especially useful if you’re learning. Beginners can write unsafe code fearlessly without worrying about breaking their laptop.
Setting the Stage: Device Drivers and the Kernel
Before we dive deeper into multi-kernel environments, let’s recap what device drivers are and how they interact with the kernel.
A device driver is a piece of software that tells the kernel how to talk to a piece of hardware. Key points:
- Applications run in user space, with limited privileges.
- Drivers run in kernel space, with full access to hardware.
- The kernel acts as a bridge: user applications make system calls, which the kernel routes to drivers, which in turn talk to devices.
So when you plug in a USB keyboard, for example, the kernel relies on a USB input driver to translate raw signals into events that user apps can understand.
In Linux, drivers can be compiled directly into the kernel or built as loadable kernel modules (LKMs). Modules are nice because you can insert or remove them dynamically without rebooting the system.
Building a Safe Multi-Kernel Sandbox
Okay, let’s talk about building a sandbox to experiment with multiple Linux kernels. The idea is simple:
- Host OS: This is your main operating system—macOS, Windows, or even another Linux.
- Guest OS: A Linux distribution (usually Ubuntu) running inside the VM. This has its own kernel instance.
Here’s the neat part: you can spin up multiple guest instances, each running its own kernel. That’s your multi-kernel playground.
Installing Multipass on macOS
If you’re on a Mac, installing Multipass is easy with Homebrew:
brew install multipass
If you run into issues, try:
brew install --cask multipass
Once installed, test it:
multipass --help
You should see available commands.
Spinning Up a New Linux Kernel Instance
Now let’s launch an Ubuntu VM:
multipass launch --name dev-kernel --mem 2G --disk 10G --cpus 2
This creates a VM named dev-kernel with 2GB RAM, 10GB disk, and 2 CPUs.
Connect to it:
multipass shell dev-kernel
Boom—you’re inside a fresh Linux environment, running its own kernel.
Managing Multiple Kernels
Want another instance with a different Ubuntu release? Easy:
multipass launch 22.04 --name kernel-22-04
multipass launch 20.04 --name kernel-20-04
Each instance runs a different kernel version. You can test your driver across both.
List all running kernels:
multipass list
And if you break one:
multipass delete dev-kernel
multipass purge
Disposable kernels at your fingertips.
Writing Your First Kernel Module
Enough theory—let’s write a minimal kernel module to see the multi-kernel workflow in action.
Here’s a simple kernel module in C that counts how many times it’s accessed.
The Driver Code (simple_driver.c)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "simple_dev"
#define CLASS_NAME "simple"
static int majorNumber;
static int counter = 0;
static struct class* simpleClass = NULL;
static struct device* simpleDevice = NULL;
static ssize_t dev_read(struct file *f, char __user *buf, size_t len, loff_t *off);
static struct file_operations fops = {
.read = dev_read,
};
static ssize_t dev_read(struct file *f, char __user *buf, size_t len, loff_t *off) {
char message[64];
int msg_len;
counter++;
msg_len = snprintf(message, sizeof(message), "Driver called %d times\n", counter);
if (*off >= msg_len) return 0;
if (copy_to_user(buf, message, msg_len)) return -EFAULT;
*off += msg_len;
return msg_len;
}
static int __init simple_init(void) {
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
simpleClass = class_create(THIS_MODULE, CLASS_NAME);
simpleDevice = device_create(simpleClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
printk(KERN_INFO "simple_driver: loaded with major %d\n", majorNumber);
return 0;
}
static void __exit simple_exit(void) {
device_destroy(simpleClass, MKDEV(majorNumber, 0));
class_unregister(simpleClass);
class_destroy(simpleClass);
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "simple_driver: unloaded\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("InPajama");
MODULE_DESCRIPTION("A simple Linux multi-kernel demo driver");
This module registers a character device. Each time you read from it, it increments a counter and returns the number of times it’s been accessed.
Makefile
obj-m += simple_driver.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
Compile it inside your kernel instance:
make
Insert the module:
sudo insmod simple_driver.ko
Check logs:
dmesg | tail
Remove it:
sudo rmmod simple_driver
User-Space Interaction
Now let’s write a small Python app to talk to our driver. The driver exposes itself as a device file under /dev/simple_dev.
Python App (user_app.py)
with open("/dev/simple_dev", "r") as f:
data = f.read()
print("Kernel says:", data.strip())
Each time you run this app, the counter in the driver increases.
python3 user_app.py
python3 user_app.py
Output:
Kernel says: Driver called 1 times
Kernel says: Driver called 2 times
This is the classic cycle: user space → kernel module → kernel → back to user space.
Multi-Kernel Workflow in Practice
Here’s where the multi-kernel setup shines. Imagine you compiled this driver on Ubuntu 22.04’s kernel, but you also need to test it on 20.04. With Multipass, you:
- Launch both instances.
- Copy the driver source into each.
- Compile against each kernel’s headers.
- Insert and test.
That’s multi-kernel development: treating each kernel as a disposable sandbox for your driver.
Why Not Just Use Docker?
Good question. Docker containers share the host kernel. If your goal is to test kernel modules, Docker won’t help because you can’t load custom kernel modules inside a container without affecting the host kernel. You need full kernel isolation, which means VMs.
Protecting Your Host OS
One of the most valuable lessons from the InPajama device driver course is this: never hack directly on your host OS kernel. Even if you’re running Linux as your host, create a VM and do your experiments there. Why?
- If you misconfigure kernel headers, you might break your host’s build system.
- If your module has a bug, you can panic the whole system.
- If you corrupt kernel memory, you risk data loss.
With multi-kernel sandboxes, you can:
- Roll back instantly.
- Run multiple versions side by side.
- Share code from your host via SSH + VS Code integration.
VS Code + SSH: A Dream Setup
Writing kernel code in vim inside a VM is fine, but modern workflows let you keep your editor on the host and push code into the VM. Here’s how:
- Install the Remote - SSH extension in VS Code.
- SSH into your Multipass instance.
- Open the driver source files directly in VS Code.
Now you have IntelliSense, Git integration, and modern editing features, while the actual compilation happens inside the sandbox kernel instance.
Lessons Learned from Multi-Kernel Development
After going through these workflows, a few takeaways stand out:
- Isolation is freedom: Knowing you can always discard a broken kernel instance makes you bolder to experiment.
- Headers matter: Always install the kernel headers inside your instance (
sudo apt install linux-headers-$(uname -r)). Without them, you can’t compile modules. - Version drift is real: Small changes between kernel versions can break drivers. That’s why testing across multiple kernels is crucial.
- Automation pays off: Write scripts to spin up instances, install headers, and copy sources. Multi-kernel workflows get messy without automation.
Conclusion: Embrace the Multi-Kernel Mindset
Linux isn’t just one kernel—it’s a living, evolving codebase with thousands of versions out in the wild. If you’re serious about device driver development, you need to embrace a multi-kernel mindset. That means:
- Building isolated sandboxes with tools like Multipass.
- Practicing writing, inserting, and unloading kernel modules.
- Testing across multiple kernels to ensure compatibility.
- Keeping your host OS clean and safe.
With these skills, you’re not just writing code—you’re learning the language of the kernel. And once you’re comfortable juggling multiple kernels, you’ll find that debugging, testing, and even contributing to upstream Linux becomes far less intimidating.
So go ahead: spin up that sandbox, write your first driver, and watch your code dance inside the kernel. The multi-kernel world is waiting.
Takeaway: Multi-kernel development is less about abstract theory and more about practical workflows. Create disposable kernel instances, test your drivers across them, and keep your host safe. That’s how modern Linux driver developers roll.
If you enjoyed this deep dive, consider subscribing to stay updated on more long-form explorations of Linux systems and hands-on kernel development.