Port I/O vs Memory Mapped I/O
Free Linux Kernel Development Course — Kernel Memory Management, Lecture 13
Every device driver eventually needs to touch real hardware registers, and the Linux kernel gives you two different ways to do it: port io linux kernel access, also called Programmed I/O (PIO), and Memory Mapped I/O (MMIO). In this free linux kernel development course lecture, we finish the Kernel Memory Management chapter by comparing both approaches, walking through the official port I/O API, and building an original driver that safely reserves and releases an I/O port region.
free linux development course
free linux device drivers course
free linux kernel development course
port io linux kernel
memory mapped io linux
What You Will Learn
The x86 port address space
request_region() / release_region()
devm_request_region() managed variant
inb()/outb() family
/proc/ioports
Prerequisites
Basic idea of CPU registers and buses
A Linux VM or board to build/insmod modules
Two Ways to Talk to Hardware Registers
Besides moving data around in RAM, the kernel constantly needs to read and write device registers — the small pieces of memory built into a chip that control and report its state. Depending on the CPU architecture, the kernel reaches those registers in one of two ways:
Port I/O (PIO)
Registers live in a separate I/O port address space, reached only through special CPU instructions such as in/out. Common on x86.
Memory Mapped I/O (MMIO)
Registers are mapped into the normal memory address space. You access them with ordinary reads/writes to a pointer. Common on ARM and most modern SoCs.
| Property | Port I/O (PIO) | Memory Mapped I/O (MMIO) |
|---|---|---|
| Address space | Separate port space (64K ports on x86) | Shares the normal memory address space |
| Access method | Dedicated instructions (in/out on x86) | Normal load/store via a mapped pointer |
| Typical architecture | x86 | ARM, ARM64, most SoCs |
| Kernel helper APIs | request_region(), inb()/outb() | ioremap(), readl()/writel() |
| How common today | Legacy peripherals only (e.g. some serial ports) | Standard for almost all modern devices |
The Port Address Space (PIO)
On architectures that support it, there are two entirely separate address spaces: the regular memory address space we have used throughout this chapter, and a much smaller port address space, historically limited to 65,536 ports on x86. PIO is considered an old-fashioned technique today and is mostly seen on legacy peripherals; nearly all modern devices use MMIO instead, which is why the next lecture in this course moves on to ioremap(). Still, understanding PIO matters, since some drivers — legacy serial ports being the classic example — still rely on it, and the API pattern it teaches (reserve, access, release) is the same pattern used everywhere else in the kernel.
Reserving a Port Region: request_region()
Before touching any I/O ports, a well-behaved driver must first announce its intent to use them, so that two drivers never fight over the same hardware region. This is done with request_region(), declared in <linux/ioport.h> and still the standard API on current kernels:
#include <linux/ioport.h>
struct resource *request_region(unsigned long start,
unsigned long n,
const char *name);
void release_region(unsigned long start, unsigned long n);
start is the first port number, n is how many consecutive ports you need, and name is a label (usually your driver’s name) that shows up in /proc/ioports. request_region() returns NULL if the range is already taken by another driver. When you are done, call release_region() with the same start and count.
Modern kernels also offer a managed variant, devm_request_region(), which automatically releases the region when the associated device is detached — no manual cleanup path required:
struct resource *devm_request_region(struct device *dev,
unsigned long start,
unsigned long n,
const char *name);
Original Driver Example: ep_ioport_demo
The following original example reserves a small, unused legacy port range purely to demonstrate the API safely (it does not read or write any real hardware register), then releases it on module unload.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioport.h>
#define EP_PORT_BASE 0x300 /* commonly free legacy debug range */
#define EP_PORT_COUNT 4
static struct resource *ep_region;
static int __init ep_ioport_demo_init(void)
{
ep_region = request_region(EP_PORT_BASE, EP_PORT_COUNT, "ep_ioport_demo");
if (!ep_region) {
pr_err("ep_ioport_demo: ports 0x%x-0x%x already in use\n",
EP_PORT_BASE, EP_PORT_BASE + EP_PORT_COUNT - 1);
return -EBUSY;
}
pr_info("ep_ioport_demo: reserved ports 0x%x-0x%x\n",
EP_PORT_BASE, EP_PORT_BASE + EP_PORT_COUNT - 1);
return 0;
}
static void __exit ep_ioport_demo_exit(void)
{
release_region(EP_PORT_BASE, EP_PORT_COUNT);
pr_info("ep_ioport_demo: ports released\n");
}
module_init(ep_ioport_demo_init);
module_exit(ep_ioport_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala port I/O region demo");
Build and Run
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_ioport_demo.ko
$ cat /proc/ioports | grep ep_ioport_demo
$ dmesg | tail -2
$ sudo rmmod ep_ioport_demo
$ cat /proc/ioports | grep ep_ioport_demo
Expected Output
[ 890.112] ep_ioport_demo: reserved ports 0x300-0x303
$ cat /proc/ioports | grep ep_ioport_demo
0300-0303 : ep_ioport_demo
$ sudo rmmod ep_ioport_demo
[ 905.774] ep_ioport_demo: ports released
$ cat /proc/ioports | grep ep_ioport_demo
(no output — region released)
The entry disappearing from /proc/ioports after rmmod confirms release_region() worked correctly, exactly the same pattern you saw with vmallocinfo in the vmalloc lecture, just for a different resource type.
Actually Reading and Writing a Port
Once a region is reserved, the kernel provides a small family of functions for the real read/write access, declared in <asm/io.h>:
unsigned char inb(unsigned long port);
unsigned short inw(unsigned long port);
unsigned int inl(unsigned long port);
void outb(unsigned char value, unsigned long port);
void outw(unsigned short value, unsigned long port);
void outl(unsigned int value, unsigned long port);
The naming pattern is consistent: in/out tells you the direction, and the suffix b/w/l tells you the width — byte, word (16-bit), or long (32-bit). These map almost directly to the underlying in/out CPU instructions on x86 and are only meaningful on architectures that actually implement a port address space.
Why MMIO Dominates Modern Drivers
Because port I/O only exists on a handful of architectures and is capped at a small address space, almost every modern peripheral — from SPI and I2C controllers to network cards on ARM SoCs — is designed around Memory Mapped I/O instead. MMIO registers are reached with ioremap() to obtain a kernel virtual address, followed by ordinary readl()/writel() calls. We will cover that full API, along with devm_ioremap_resource(), in the next lecture, which is really the natural sequel to everything covered in this chapter so far.
Real-World Use Cases
- Legacy serial (UART) and parallel port drivers still built around PIO on x86.
- PC speaker and some early-boot diagnostic drivers.
- Virtually all modern SoC peripherals (I2C, SPI, GPIO, timers) use MMIO exclusively.
- PCI devices can expose either I/O-space BARs or memory-space BARs, so drivers sometimes need to support both.
Common Mistakes
| Mistake | Why It’s a Problem | Fix |
|---|---|---|
| Accessing ports without calling request_region() first | Two drivers can silently collide on the same hardware | Always reserve the region before touching ports |
| Forgetting release_region() on every error/exit path | Region stays reserved forever, blocking reload | Use devm_request_region() to get automatic cleanup |
| Using inb()/outb() on an architecture with no port space | Code will not compile or behave correctly | Guard with #ifdef or simply use MMIO instead |
| Mixing up port width (b/w/l) with the device’s actual register width | Reads garbage or writes to the wrong bytes | Check the datasheet for the correct register width |
Best Practices
- Prefer
devm_request_region()over the manualrequest_region()/release_region()pair whenever you have astruct deviceavailable. - Always check the return value before assuming a port region is yours.
- For any new driver targeting modern hardware, design around MMIO first; only fall back to PIO for genuinely legacy devices.
Performance Considerations
Port I/O instructions are generally slower than a plain memory read/write because they go through a dedicated, more restrictive bus path. MMIO, by contrast, benefits from the same caching and pipelining infrastructure as regular memory access (though device registers are typically mapped uncached to preserve correct ordering).
Security Considerations
Reserving a region with request_region() is a cooperative convention, not hardware-enforced protection — a misbehaving or malicious module can still perform raw inb()/outb() calls without checking. This is one reason direct port/register access from user space (e.g. via /dev/port or ioperm()) is tightly restricted and typically requires elevated privileges.
Summary / Key Takeaways
- PIO uses a separate port address space reached through dedicated CPU instructions; MMIO maps registers into normal memory address space.
- request_region()/release_region() (or the managed devm_request_region()) prevent two drivers from colliding on the same hardware.
- inb()/outb() and their w/l variants are the actual read/write functions for port I/O.
- MMIO is the dominant technique on modern hardware, especially ARM-based SoCs.
Conclusion
This lecture closes out the core memory-management portion of the Kernel Memory Management chapter by connecting everything back to real hardware: after learning how the kernel manages RAM through page allocation, kmalloc, vmalloc, page faults, and Copy-on-Write, you now know the two ways a driver actually reaches device registers. With PIO’s request/release pattern fresh in mind, the next lecture’s dive into ioremap() and MMIO will feel very familiar — it is the exact same “reserve, map, use, release” discipline applied to memory-mapped registers instead of ports.
Frequently Asked Questions
What is the difference between Port I/O and Memory Mapped I/O?
Port I/O uses a separate address space reached only through special CPU instructions, while Memory Mapped I/O maps device registers into the normal memory address space so they can be accessed with regular reads and writes.
Why must a driver call request_region() before accessing ports?
It prevents two different drivers from trying to use the same hardware port range at the same time.
What does devm_request_region() do differently from request_region()?
It automatically releases the reserved region when the device is detached, removing the need for a manual release_region() call in every exit path.
Which architectures use Port I/O today?
Mainly x86. Most other architectures, including ARM and ARM64, rely on Memory Mapped I/O for essentially all peripherals.
What do the inb(), inw(), and inl() functions do?
They read a byte, a 16-bit word, and a 32-bit long respectively from a given I/O port. outb(), outw(), and outl() are their write counterparts.
Where can I see which driver owns which I/O ports?
The /proc/ioports file lists every reserved I/O port range on the system along with the name passed to request_region().
Is Port I/O faster or slower than Memory Mapped I/O?
Port I/O instructions are generally slower since they go through a dedicated, more restrictive bus path compared to ordinary memory access used by MMIO.
Continue the Free Linux Kernel Development Course
Next up: Memory Mapped I/O and the ioremap() API.
