Linux Kernel Mmap Device Memory-Free Linux Device Drivers Tutorial

« PREV_LEC  NEXT_LEC »

Linux Kernel Mmap Device Memory

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

Lecture 16 of 20+
Kernel 6.x Verified
Beginner Friendly

Keywords Covered In This Free Linux Kernel Development Course Lecture

linux kernel mmap device memory
remap_pfn_range
free linux device drivers course
free linux kernel development course
free embedded systems course

Every time an application memory-maps a device file instead of calling read()/write() in a loop, a kernel driver is doing linux kernel mmap device memory work behind the scenes. This free linux kernel development course lecture walks through remap_pfn_range(), its I/O-memory sibling io_remap_pfn_range(), and the file_operations.mmap hook that ties them together — with an original driver you can build and test today.

What You Will Learn

  • Why mapping memory into user space beats copying it with read/write
  • How remap_pfn_range() installs page table entries into a process’s address space
  • The difference between remap_pfn_range() and io_remap_pfn_range()
  • How the file_operations.mmap callback fits into the mmap(2) system call path
  • How to write, build, and test an original character device driver that supports mmap()

Prerequisites

  • Completion of the previous lecture on the linux kmap function
  • Basic character device driver experience (open/read/write/release)
  • A Linux VM or board running kernel 6.x with headers installed

Why Map Memory Instead Of Copying It

A normal read() or write() call on a device file crosses the user/kernel boundary and copies data byte-for-byte between a user buffer and a kernel buffer. That copy costs CPU time and, for large or frequently accessed regions, adds up quickly. Memory mapping takes a different approach: instead of copying, the kernel updates the process’s own page table entries so that a range of the process’s virtual address space points straight at the underlying physical memory. After that, the application reads and writes through ordinary pointer dereferences, with no further kernel involvement and no per-access copy.

User Space Mapped Directly To Physical Memory
User process VMA
virtual address range from mmap()
Process page table entries
installed by remap_pfn_range()
Physical memory / device
no per-access copy needed

The mmap(2) System Call And file_operations.mmap

When user space calls mmap() on an open device file descriptor, the kernel eventually invokes the driver’s own mmap hook from struct file_operations:

int (*mmap) (struct file *filp, struct vm_area_struct *vma);

The kernel has already done most of the bookkeeping by this point: it picked a virtual address range, built a fresh struct vm_area_struct describing that range, and filled in fields such as vma->vm_start, vma->vm_end, vma->vm_pgoff (the offset into the mapped object, in pages) and vma->vm_page_prot (the requested page protection bits). The driver’s job inside mmap is simply to connect that VMA to the correct physical pages.

VMA Field Meaning
vm_start / vm_end Start and end of the mapped virtual range, always page-aligned
vm_pgoff Offset into the mapped object, expressed in pages
vm_page_prot Base protection bits the driver ORs its own flags into
vm_flags Mapping behavior flags such as VM_IO, VM_DONTCOPY, VM_DONTEXPAND, VM_DONTDUMP

remap_pfn_range(): Mapping RAM Into User Space

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
                     unsigned long pfn, unsigned long size,
                     pgprot_t prot);
Parameter Meaning
vma The VMA handed to the driver’s mmap() callback
addr User virtual address where the mapping should start, normally vma->vm_start
pfn Page frame number of the physical memory to map (physical address right-shifted by PAGE_SHIFT)
size Size of the region in bytes
prot Protection flags, normally derived from vma->vm_page_prot

It returns 0 on success and a negative error code on failure. Most drivers call it exactly once, from inside their mmap callback, covering the entire requested range in one shot.

io_remap_pfn_range(): Mapping I/O Memory Into User Space

When the memory being mapped is device I/O memory rather than ordinary RAM, use io_remap_pfn_range() instead. Its signature takes the same arguments as remap_pfn_range():

int io_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
                        unsigned long pfn, unsigned long size,
                        pgprot_t prot);

Modernization note: older references to this function sometimes show the pre-2005 name io_remap_page_range() with a physical address argument instead of a PFN. That name was retired well over a decade ago in favor of the current, portable io_remap_pfn_range() shown above, which behaves identically across architectures. Never use ioremap() here — ioremap() creates a mapping for the kernel’s own use, while io_remap_pfn_range() exposes physical I/O memory directly into a user process’s address space.

Function Use When
remap_pfn_range() The PFN refers to ordinary RAM
io_remap_pfn_range() The PFN refers to device I/O memory (MMIO)

Modernization Note: mmap_prepare() On The Horizon

Recent mainline kernel development (from kernel 6.17 onward) has begun introducing a newer hook, file_operations.mmap_prepare(), which runs earlier in the mapping process and hands the driver a struct vm_area_desc instead of a live VMA, so a failure is cheaper to unwind. This transition is still in progress across the kernel tree at the time of writing, and the classic file_operations.mmap hook shown in this lecture remains fully supported and is what you will encounter in the overwhelming majority of existing drivers. Treat mmap_prepare() as a direction to watch, not something you need to adopt today.

Original Driver: ep_mmap_demo

This original character device driver allocates a kernel buffer with kzalloc(), exposes it through mmap() using remap_pfn_range(), and lets a user-space program write into the buffer and read the same bytes straight back — no read()/write() syscalls involved after the mapping is set up.

// ep_mmap_demo.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>

#define EP_BUF_SIZE PAGE_SIZE

static char *ep_kbuf;

static int ep_mmap_demo_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn = virt_to_phys(ep_kbuf) >> PAGE_SHIFT;

    if (size > EP_BUF_SIZE) {
        pr_err("ep_mmap_demo: requested size too large\n");
        return -EINVAL;
    }

    if (remap_pfn_range(vma, vma->vm_start, pfn, size,
                         vma->vm_page_prot)) {
        pr_err("ep_mmap_demo: remap_pfn_range failed\n");
        return -EAGAIN;
    }

    pr_info("ep_mmap_demo: buffer mapped into user space, size=%lu\n", size);
    return 0;
}

static const struct file_operations ep_mmap_demo_fops = {
    .owner = THIS_MODULE,
    .mmap  = ep_mmap_demo_mmap,
};

static struct miscdevice ep_mmap_demo_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "ep_mmap_demo",
    .fops  = &ep_mmap_demo_fops,
};

static int __init ep_mmap_demo_init(void)
{
    int ret;

    ep_kbuf = kzalloc(EP_BUF_SIZE, GFP_KERNEL);
    if (!ep_kbuf)
        return -ENOMEM;

    ret = misc_register(&ep_mmap_demo_dev);
    if (ret) {
        kfree(ep_kbuf);
        return ret;
    }

    pr_info("ep_mmap_demo: loaded, /dev/ep_mmap_demo ready\n");
    return 0;
}

static void __exit ep_mmap_demo_exit(void)
{
    misc_deregister(&ep_mmap_demo_dev);
    kfree(ep_kbuf);
    pr_info("ep_mmap_demo: unloaded\n");
}

module_init(ep_mmap_demo_init);
module_exit(ep_mmap_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("remap_pfn_range mmap demo for free linux kernel development course");

User-Space Test Program: ep_mmap_test.c

// ep_mmap_test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

#define BUF_SIZE 4096

int main(void)
{
    int fd = open("/dev/ep_mmap_demo", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    char *addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE,
                       MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    strcpy(addr, "hello from user space via mmap");
    printf("Read back: %s\n", addr);

    munmap(addr, BUF_SIZE);
    close(fd);
    return 0;
}

Build And Run Steps

# Makefile for the kernel module
obj-m += ep_mmap_demo.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
$ make
$ sudo insmod ep_mmap_demo.ko
$ dmesg | tail -2
$ gcc -o ep_mmap_test ep_mmap_test.c
$ ./ep_mmap_test
$ dmesg | tail -1
$ sudo rmmod ep_mmap_demo

Expected Output

[ 512.001100] ep_mmap_demo: loaded, /dev/ep_mmap_demo ready
[ 520.221310] ep_mmap_demo: buffer mapped into user space, size=4096

Read back: hello from user space via mmap

Real-World Use Cases

  • Frame buffer drivers exposing display memory directly to a graphics application
  • High-throughput DMA buffers shared between kernel and user space (V4L2, audio drivers)
  • Zero-copy shared memory regions between a driver and a user-space daemon

Common Mistakes

Mistake Why It Hurts
Using remap_pfn_range() on I/O memory Use io_remap_pfn_range() for MMIO instead
Ignoring vma->vm_pgoff Breaks mappings that request a non-zero offset into the buffer
Mapping a size larger than the backing buffer Lets user space read or corrupt memory outside the intended region
Forgetting to check the remap_pfn_range() return value A partially failed mapping can be silently ignored

Best Practices

  • Always validate the requested mapping size against the actual buffer size
  • Round buffer allocations up to PAGE_SIZE, since mappings are always page-granular
  • Set VM_DONTEXPAND and VM_DONTDUMP on vma->vm_flags where appropriate for device memory

Performance And Security Considerations

Because a mapped region avoids a copy on every access, it is the right tool whenever an application touches a buffer frequently or the buffer is large. Security-wise, a driver’s mmap callback must never let user space map more memory, or a different physical range, than what it explicitly requested and is entitled to — always re-derive the PFN and size from trusted kernel-side state rather than user-supplied values.

Summary

  • Memory mapping avoids the per-access copy cost of read()/write()
  • remap_pfn_range() installs page table entries for RAM into a process’s VMA
  • io_remap_pfn_range() does the same for device I/O memory
  • The driver’s file_operations.mmap callback ties both into the mmap(2) system call

Conclusion

Linux kernel mmap device memory support is what turns a device file from something you laboriously read and write into something an application can touch directly, at native memory speed. With remap_pfn_range(), io_remap_pfn_range(), and the mmap file operation covered here, this free linux kernel development course closes out the kernel memory management chapter’s coverage of user-space memory mapping.

FAQ

What does remap_pfn_range() actually do?

It installs page table entries in a process’s virtual memory area so that a range of user virtual addresses points directly at specific physical pages.

When should I use io_remap_pfn_range() instead of remap_pfn_range()?

Use io_remap_pfn_range() when the PFN refers to device I/O memory rather than ordinary RAM.

Do I need to call ioremap() before io_remap_pfn_range()?

No. ioremap() maps memory for the kernel’s own use; io_remap_pfn_range() maps physical I/O memory directly into a user process, so the two are not combined.

What is vma->vm_pgoff used for?

It carries the offset, in pages, into the mapped object where the mapping should begin, taken from the offset argument passed to mmap(2) in user space.

Is the classic file_operations.mmap hook still valid in kernel 6.x?

Yes. It remains the standard interface used by almost all drivers, even as a newer mmap_prepare() hook is gradually introduced in mainline development.

Why must mapped memory sizes be page-aligned?

The MMU manages memory in fixed-size pages, so the kernel always rounds a VMA’s size up to a multiple of PAGE_SIZE.

What happens if remap_pfn_range() fails?

It returns a negative error code, and the driver’s mmap callback should propagate that error back to the mmap(2) caller in user space.

Can remap_pfn_range() be used to share memory between two processes?

Yes, indirectly: each process opens the device and calls mmap() with MAP_SHARED, and both mappings point at the same underlying physical buffer.

Continue The Free Linux Kernel Development Course

You have completed the kernel memory management chapter’s coverage of user-space mapping.

« PREV_LEC  |  NEXT_LEC »

Leave a Reply

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