Port I/O vs Memory Mapped I/O-Free Linux Device Drivers Tutorial

PREV_LEC  |  NEXT_LEC

Port I/O vs Memory Mapped I/O

Free Linux Kernel Development Course — Kernel Memory Management, Lecture 13

Chapter: Kernel Memory Management
Level: Intermediate
Kernel: 6.x mainline

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 embedded systems course
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

PIO vs MMIO fundamentals
The x86 port address space
request_region() / release_region()
devm_request_region() managed variant
inb()/outb() family
/proc/ioports

Prerequisites

Completed the Copy-on-Write lecture
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:

PIO vs MMIO at a glance

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 manual request_region()/release_region() pair whenever you have a struct device available.
  • 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.

PREV_LEC  |  NEXT_LEC

Leave a Reply

Your email address will not be published. Required fields are marked *