Implementing mmap In Linux Kernel- Free Linux Device Drivers Tutorial

 

← PREV_LEC  |  NEXT_LEC →

Implementing mmap In Linux Kernel

A step-by-step guide to writing the mmap file operation in a character device driver on modern Linux kernels

Lecture 17
Kernel Memory Management
Kernel 6.x Ready

This is lecture 17 of the free Linux kernel development course from EmbeddedPathashala, part of our free embedded Linux course covering kernel internals from first principles. If you are searching for a free linux device drivers course that explains implementing mmap in linux kernel drivers with real code instead of theory alone, this lecture is written for you.

What You Will Learn

  • Why user space cannot touch kernel memory directly, and what mmap() actually does about it
  • The five-step algorithm every mmap() file operation follows
  • How to validate offset and size before mapping memory to user space
  • How VM flags such as VM_IO, VM_DONTEXPAND, and VM_DONTDUMP protect a mapping
  • An original ep_mmap_algo_demo character driver that maps a simulated sensor FIFO buffer into user space
  • How to build, load, and test the driver with an original user space program and read the expected output

Prerequisites

  • Completion of the earlier lectures in this Kernel Memory Management chapter, especially the page allocator and remap_pfn_range lectures
  • Comfort writing a basic character device driver (open, release, file_operations)
  • A Linux machine or VM running kernel 6.x with headers installed
implementing mmap in linux kernel
free linux kernel development course
free linux device drivers course
free embedded systems course
remap_pfn_range

Why mmap Needs a Careful Algorithm

User space processes cannot read or write kernel memory directly, and the kernel cannot let user space poke at arbitrary physical addresses either. The mmap() file operation exists to bridge this gap safely. When a process calls the mmap() system call on an open device file, the kernel eventually calls your driver’s .mmap callback, which is responsible for building page table entries that point the process’s virtual address range at the physical memory you choose, with permissions the kernel controls rather than the caller.

Doing this correctly is not a single line of code. It is a five-step algorithm, and skipping any step is a common source of driver bugs, from silent data corruption to full kernel crashes triggered by a hostile or buggy user space program.

mmap Implementation Flow

1. Validate offset
2. Validate size
3. Resolve PFN
4. Set VMA flags
5. remap_pfn_range()

Step 1 and 2: Validate the Mapping Request

A user space process asks to map some region of your device’s file starting at a byte offset, computed from vma->vm_pgoff (the offset is always expressed in pages, so it must be shifted left by PAGE_SHIFT to get a byte offset). Your driver must reject any request whose offset or size falls outside the buffer it actually owns; otherwise a malicious or buggy caller could map memory that does not belong to it.

static int ep_mmap_validate(struct vm_area_struct *vma, size_t buf_size)
{
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long size   = vma->vm_end - vma->vm_start;

    if (offset >= buf_size)
        return -EINVAL;

    if (size > (buf_size - offset))
        return -EINVAL;

    return 0;
}

Step 3: Resolve the Page Frame Number

Once the request is validated, the driver must find the physical page frame number (PFN) that corresponds to the requested offset inside its buffer. For memory obtained with kmalloc() or a similar allocator, virt_to_phys() combined with a right shift by PAGE_SHIFT gives the PFN. For memory obtained as struct page pointers, page_to_pfn() is the correct call instead.

unsigned long pfn = virt_to_phys(dev->buffer + offset) >> PAGE_SHIFT;

Step 4: Set the Correct VMA Flags

The vm_area_struct that describes the mapping needs a few flags set correctly so the memory manager treats it the way your driver intends:

Flag or call Purpose
pgprot_noncached(vma->vm_page_prot) Disables CPU caching for memory-mapped I/O regions
VM_IO Marks the VMA as an I/O region rather than ordinary memory
VM_DONTEXPAND Prevents the mapping from being resized with mremap()
VM_DONTDUMP Excludes the region from core dumps

Modernization note: older references mention a single combined VM_RESERVED flag for kernels before 3.7. That flag was removed long ago; current drivers should set VM_IO, VM_DONTEXPAND, and VM_DONTDUMP explicitly, as shown above.

Step 5: Call remap_pfn_range()

The final step hands the validated PFN, size, and protection flags to remap_pfn_range(), which builds the actual page table entries:

if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
    return -EAGAIN;

return 0;

Original Driver: ep_mmap_algo_demo

To see the full algorithm working together, here is an original character device driver that simulates a small sensor FIFO buffer and exposes it to user space through mmap(), instead of copying data through read().

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

#define EP_FIFO_SIZE (4 * PAGE_SIZE)

static char *ep_fifo_buf;

static int ep_mmap_algo_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long size   = vma->vm_end - vma->vm_start;
    unsigned long pfn;

    if (offset >= EP_FIFO_SIZE)
        return -EINVAL;
    if (size > (EP_FIFO_SIZE - offset))
        return -EINVAL;

    pfn = virt_to_phys(ep_fifo_buf + offset) >> PAGE_SHIFT;

    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;

    pr_info("ep_mmap_algo_demo: mapped %lu bytes at offset %lu\n", size, offset);
    return 0;
}

static const struct file_operations ep_mmap_algo_fops = {
    .owner = THIS_MODULE,
    .mmap  = ep_mmap_algo_mmap,
};

static struct miscdevice ep_mmap_algo_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "ep_mmap_algo_demo",
    .fops  = &ep_mmap_algo_fops,
};

static int __init ep_mmap_algo_init(void)
{
    int ret;

    ep_fifo_buf = kzalloc(EP_FIFO_SIZE, GFP_KERNEL);
    if (!ep_fifo_buf)
        return -ENOMEM;

    memset(ep_fifo_buf, 0xAB, EP_FIFO_SIZE);

    ret = misc_register(&ep_mmap_algo_dev);
    if (ret) {
        kfree(ep_fifo_buf);
        return ret;
    }

    pr_info("ep_mmap_algo_demo: loaded, buffer size %d bytes\n", EP_FIFO_SIZE);
    return 0;
}

static void __exit ep_mmap_algo_exit(void)
{
    misc_deregister(&ep_mmap_algo_dev);
    kfree(ep_fifo_buf);
    pr_info("ep_mmap_algo_demo: unloaded\n");
}

module_init(ep_mmap_algo_init);
module_exit(ep_mmap_algo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Original mmap() implementation demo for EmbeddedPathashala");

Original User Space Test: ep_mmap_algo_test.c

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

#define MAP_SIZE (4 * 4096)

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

    unsigned char *mapped = mmap(NULL, MAP_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    printf("First byte read from mapped region: 0x%02x\n", mapped[0]);
    printf("Byte at offset 4095: 0x%02x\n", mapped[4095]);

    munmap(mapped, MAP_SIZE);
    close(fd);
    return 0;
}

Build and Run Steps

# Build the module (Makefile omitted for brevity, standard kbuild style)
make

sudo insmod ep_mmap_algo_demo.ko
dmesg | tail -n 3

gcc ep_mmap_algo_test.c -o ep_mmap_algo_test
./ep_mmap_algo_test

Expected Output

ep_mmap_algo_demo: loaded, buffer size 16384 bytes
ep_mmap_algo_demo: mapped 16384 bytes at offset 0

First byte read from mapped region: 0xab
Byte at offset 4095: 0xab

Every byte reads back as 0xab because the driver filled the buffer with that pattern at load time. No copy_to_user() ever ran; the process read kernel-owned physical memory directly through its own page tables, which is the entire point of implementing mmap in a Linux kernel driver.

Common Mistakes

Mistake Consequence
Skipping the offset/size check User space can map memory outside your buffer, corrupting unrelated kernel data
Forgetting VM_IO on device memory The kernel may treat the region as swappable or dumpable, causing crashes
Using virt_to_phys() on vmalloc’d memory vmalloc pages are not physically contiguous; this returns garbage addresses
Not returning -EAGAIN on remap_pfn_range() failure User space believes the mapping succeeded when it did not

Best Practices

  • Always validate offset and size before touching any pointer arithmetic
  • Use page_to_pfn() for page-based buffers and virt_to_phys() only for kmalloc-style contiguous memory
  • Set VM_DONTEXPAND and VM_DONTDUMP on any hardware-backed mapping
  • Prefer a misc device or the modern mmap_prepare() path (kernel 6.17+) for new drivers where available, while keeping classic .mmap support for wider kernel compatibility

Performance and Security Considerations

Mapping memory avoids a copy on every access, which matters a great deal for high-throughput devices such as frame grabbers or DMA ring buffers. The security trade-off is that the driver is now responsible for every boundary check that the VFS layer would normally perform on a regular read/write path, so the validation in steps 1 and 2 is not optional.

Real World Use Cases

  • Frame buffer drivers exposing video memory directly to a compositor
  • DMA ring buffers shared between a driver and a user space networking stack
  • Shared memory regions between a kernel driver and a real-time user space control loop

Summary and Key Takeaways

  • Implementing mmap in a Linux kernel driver always follows the same five-step algorithm: validate offset, validate size, resolve the PFN, set VMA flags, call remap_pfn_range()
  • VM_RESERVED is obsolete; use VM_IO, VM_DONTEXPAND, and VM_DONTDUMP instead
  • The ep_mmap_algo_demo driver shows the pattern end to end on a simulated FIFO buffer

Frequently Asked Questions

What is the difference between mmap() and read()/write()?

read() and write() copy data between kernel and user buffers on every call. mmap() sets up page tables once so the process can access the memory directly afterward, with no further copying.

Can I use remap_pfn_range() with vmalloc’d memory?

Not directly, because vmalloc memory is not physically contiguous. Each page must be mapped individually, typically with vm_insert_page() in a loop instead of a single remap_pfn_range() call.

Why does my mmap() call fail with -EINVAL?

This usually means the requested offset or size falls outside the buffer your driver actually owns, which is exactly what the validation step in this lecture is designed to catch.

Is VM_RESERVED still needed on modern kernels?

No. VM_RESERVED was removed years ago. Use VM_IO combined with VM_DONTEXPAND and VM_DONTDUMP instead.

Do I need mmap_prepare() instead of the classic .mmap callback?

Not yet for most drivers. mmap_prepare() is an emerging kernel 6.17+ hook; the classic .mmap file operation shown here remains the dominant, fully supported interface.

What happens if I forget to set VM_DONTDUMP?

The mapped region can end up included in core dumps, which is wasteful for large hardware buffers and can leak device memory contents into crash dump files.

Can multiple processes mmap() the same device buffer?

Yes, if the driver allows it. Each process gets its own VMA pointing at the same physical pages, so writes from one process become visible to the others immediately.

 

Continue the Free Linux Kernel Development Course

Next up: how the Linux page cache and CPU caches work together to speed up every read and write in the kernel.

← PREV_LEC  |  NEXT_LEC →

 

Leave a Reply

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