Skip to main content

Command Palette

Search for a command to run...

mmap — Virtual Memory Mapping, Shared Memory & MMIO

Updated
13 min readView as Markdown
mmap — Virtual Memory Mapping, Shared Memory & MMIO

It can be used for various types of memory mapping into the virtual address space of a process

mmap(<0 or NULL or specific addr>, <length>, <Read/Write Permissions>, <Flag deciding mapping type>, <fileDescriptor>, <offset>);


1st arg

  • 0/NULL -> tells kernel "I don't care where you map in virtual addr space"

    • Kernel chooses a suitable free virtual address

    • This is the normal/recommended usage

  • Specific Value 0x7fff0000

    • Without MAP_FIXED -> this is only a hint to the kernel

    • Kernel may map at a different virtual address if required

    • With MAP_FIXED -> kernel tries to map at exactly this virtual address

      • Existing mapping at that address may be replaced

      • Dangerous because it can destroy an existing mapping

    • MAP_FIXED_NOREPLACE -> requires the address to be unused; fails instead of replacing an existing mapping


2nd arg

  • Length of memory to map into the process virtual address space

  • It does NOT have to be page-size aligned

  • Kernel internally manages mappings at page granularity

  • We should provide the actual size required by our data

    • Hardware peripheral

      • Mapping length should cover the required registers

      • offset passed to mmap must satisfy the alignment requirements of the mapping

      • If the required physical address is not page aligned, map from the containing page boundary and add the intra-page offset

        Eg : 0x3F200032 (not page aligned)

        • MAP_SIZE is 4096 (0x1000)

        • MAP_MASK is MAP_SIZE - 1, which is 4095 (0x0FFF)

        • ~MAP_MASK gives 0xFFFFF000

        • PERIPHERAL_PHY_BASE & ~MAP_MASK

          • removes the lower 12 bits

          • gives the beginning of the page containing the peripheral address

    • Anonymous memory in RAM or shared memory

      • We can request any size

      • Kernel manages the mapping using page-sized units internally

      • We should provide the exact size of the data structure/buffer we need

      • Example:

        • Structure -> sizeof(structure)

        • Array -> number_of_elements * sizeof(element)

    • Scenario A: shared memory

      typedef struct {
          int process_id;
          char status[32];
          float telemetry_data;
      } SharedData;
      
      SharedData *data = (SharedData *)mmap(
          NULL,
          sizeof(SharedData),
          PROT_READ | PROT_WRITE,
          MAP_SHARED | MAP_ANONYMOUS,
          -1,
          0
      );
      
    • Scenario B : Allocating a Dynamic Buffer (e.g., Arrays)

      size_t num_elements = 500;
      size_t allocation_size = num_elements * sizeof(int);
      
      int *my_array = (int *)mmap(
          NULL,
          allocation_size,
          PROT_READ | PROT_WRITE,
          MAP_PRIVATE | MAP_ANONYMOUS,
          -1,
          0
      );
      

3rd arg (permissions)

  • PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE

  • Defines what operations the process is allowed to perform on the mapped memory

  • Example:

    • PROT_READ -> read allowed

    • PROT_WRITE -> write allowed

    • PROT_READ | PROT_WRITE -> read + write


4th arg (flag)

  • MAP_PRIVATE

    • Changes are private to the process

    • For file-backed mappings, writes use copy-on-write

  • MAP_SHARED

    • Changes are visible through other mappings of the same backing object

    • Commonly used for shared memory and device mappings

  • MAP_ANONYMOUS

    • Mapping has no file backing

    • fd is normally -1

    • Can be combined with MAP_PRIVATE or MAP_SHARED

  • MAP_FIXED

    • Forces mapping at the exact virtual address supplied in 1st argument

    • May replace an existing mapping

  • MAP_FIXED_NOREPLACE

    • Requires the requested virtual address range to be unused

    • Fails instead of replacing an existing mapping


5th arg (file descriptor)

  • fd identifies the object backing the mapping

  • It is NOT necessarily a physical address

  • Depending on the mapping type:

    • Hardware/device mapping

      • fd can be a device file descriptor

      • Example: /dev/...

    • Regular file mapping

      • fd is the file descriptor returned by open()
    • POSIX shared memory

      • fd is returned by shm_open()
    • Anonymous memory

      • fd = -1

      • No file backing


6th arg

  • offset (type off_t)

  • Tells the kernel which portion of the backing object should be mapped

  • For file-backed mappings, it is normally page aligned

  • Anonymous memory

    • fd = -1

    • offset = 0

  • POSIX shared memory

    • Usually offset = 0

    • Can use a different page-aligned offset for a large shared-memory object

  • Hardware peripheral

    • Device-driver-specific mmap semantics

    • If mapping physical memory directly, the offset is normally the page-aligned physical address

      Let's assume physical address is 0x13E8

      • Page size = 4096 = 0x1000

      • Page boundaries:

        • 0x0000

        • 0x1000

        • 0x2000

        • ...

      • 0x13E8 is inside the page starting at 0x1000

      phy_base_addr = 0x13E8 & 0xFFFFF000;
      offset        = 0x13E8 & 0x00000FFF;
      

      Results:

      phy_base_addr = 0x1000
      offset        = 0x03E8
      

      Then:

      void *mapped_base = mmap(
          NULL,
          MAP_SIZE,
          PROT_READ | PROT_WRITE,
          MAP_SHARED,
          fd,
          phy_base_addr
      );
      
      volatile uint32_t *my_register =
          (volatile uint32_t *)((char *)mapped_base + offset);
      

      This resolves conceptually into:

      physical page base = 0x1000
      in-page offset     = 0x03E8
      
      mapped_base + 0x03E8
      
      • mapped_base is a virtual address

      • 0x03E8 is an offset inside that mapped page

      • Do NOT treat mapped_base + offset as a physical address

      Pointer arithmetic:

      (char *)mapped_base + offset
      
      • char * increments in 1-byte units

      • Then cast the resulting address to the required register pointer type



Hardware peripheral :


#define PERIPHERAL_PHY_BASE 0x3F200000
#define MAP_SIZE            4096UL
#define MAP_MASK            (MAP_SIZE - 1)

int fd = open("/dev/memoryCreated", O_RDWR | O_SYNC);

/* Align physical address to page boundary */
void *mapped_base = mmap(
    NULL,
    MAP_SIZE,
    PROT_READ | PROT_WRITE,
    MAP_SHARED,
    fd,
    PERIPHERAL_PHY_BASE & ~MAP_MASK
);

/* Offset to the required register within the page */
volatile uint32_t *my_register =
    (volatile uint32_t *)((char *)mapped_base +
                          (PERIPHERAL_PHY_BASE & MAP_MASK));

/* Read and write hardware register */
uint32_t val = *my_register;

*my_register = val | 0x01;

/* Cleanup */
munmap(mapped_base, MAP_SIZE);
  • mapped_base is a virtual address returned by mmap()

  • PERIPHERAL_PHY_BASE & ~MAP_MASK

    • gives page-aligned physical/device offset
  • PERIPHERAL_PHY_BASE & MAP_MASK

    • gives offset inside that page
  • volatile

    • tells compiler each register access is observable

    • useful for MMIO registers

    • does NOT provide locking or atomicity

  • In production Linux, hardware MMIO should normally be exposed through a proper device driver rather than directly using /dev/mem


Coding Snippets for each of the case:

Anonymous memory in RAM:


#include <sys/mman.h>
#include <stdint.h>

#define BUFFER_SIZE 1000

int main()
{
    uint32_t *ptr = mmap(
        NULL,
        BUFFER_SIZE,
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS,
        -1,
        0
    );

    if (ptr == MAP_FAILED)
        return -1;

    uint32_t res = 1;

    for (int i = 1; i < 100; i++) {
        res = res * i;
        ptr[0] = res;
    }

    munmap(ptr, BUFFER_SIZE);

    return 0;
}
  • MAP_ANONYMOUS -> no file backing

  • fd = -1

  • offset = 0

  • BUFFER_SIZE means 1000 bytes

  • sizeof(BUFFER_SIZE) would NOT mean 1000 bytes

    • because BUFFER_SIZE is a macro replaced by 1000

    • sizeof(1000) is normally sizeof(int)

  • volatile is not required for normal RAM

  • For inter-process shared anonymous memory:

    • use MAP_SHARED | MAP_ANONYMOUS

    • typically between related processes after fork()



RAW shared memory in RAM:


Reader

#define SHARED_MEMORY "/shm1"
#define SEMAPHOR_NAME "/sem1"
#define BUFFER_SIZE 64

typedef struct {
    int sensor_id;
    float sensor_value;
    char manufacturer[BUFFER_SIZE];
} sensor;

int main()
{
    int shm_fd = shm_open(SHARED_MEMORY, O_RDWR);

    sensor *mapped_virt_addr = mmap(
        NULL,
        sizeof(sensor),
        PROT_READ | PROT_WRITE,
        MAP_SHARED,
        shm_fd,
        0
    );

    sem_t *sem = sem_open(SEMAPHOR_NAME, 0);

    while (1) {
        sem_wait(sem);

        printf("sensor_id:%d, sensor_value:%f, manufacturer:%s\n",
               mapped_virt_addr->sensor_id,
               mapped_virt_addr->sensor_value,
               mapped_virt_addr->manufacturer);

        sem_post(sem);
    }

    munmap(mapped_virt_addr, sizeof(sensor));
    close(shm_fd);
}
  • mapped_virt_addr points to the shared mapped memory

  • Do NOT create a separate local sensor_data and read from it

  • Both processes must access the data through the mmap() returned address

  • sem_wait() / sem_post() provide synchronization

  • Shared memory itself does NOT provide synchronization


Writer

#define SHARED_MEMORY "/shm1"
#define SEMAPHOR_NAME "/sem1"
#define BUFFER_SIZE 64

typedef struct {
    int sensor_id;
    float sensor_value;
    char manufacturer[BUFFER_SIZE];
} sensor;

int main()
{
    int shm_fd = shm_open(
        SHARED_MEMORY,
        O_CREAT | O_RDWR,
        0666
    );

    ftruncate(shm_fd, sizeof(sensor));

    sensor *mapped_virt_addr = mmap(
        NULL,
        sizeof(sensor),
        PROT_READ | PROT_WRITE,
        MAP_SHARED,
        shm_fd,
        0
    );

    sem_t *sem = sem_open(
        SEMAPHOR_NAME,
        O_CREAT,
        0666,
        1
    );

    while (1) {
        sem_wait(sem);

        mapped_virt_addr->sensor_id = 10;
        mapped_virt_addr->sensor_value = 14.566;

        snprintf(
            mapped_virt_addr->manufacturer,
            BUFFER_SIZE,
            "%s",
            "BOSCH-ADC-TEMPERATURE"
        );

        sem_post(sem);
    }

    munmap(mapped_virt_addr, sizeof(sensor));
    close(shm_fd);
}
  • O_CREAT -> correct flag

  • ftruncate() -> sets the size of the POSIX shared-memory object

  • mmap() -> maps that object into process virtual address space

  • Writer writes directly through the mapped pointer

  • Reader maps the same shared-memory object

  • Both processes can have DIFFERENT virtual addresses

  • They still access the same underlying shared-memory pages

  • shm_unlink() is used when the shared-memory object should be removed

  • sem_close() / sem_unlink() are used for semaphore cleanup


Correct mmap Combinations

Goal / Target Memory Flag 1 Flag 2 Flag 3 fd Argument offset Argument Behavior & Use Case
Anonymous RAM (Private) MAP_PRIVATE MAP_ANONYMOUS None -1 0 Private anonymous memory
Anonymous RAM (Shared) MAP_SHARED MAP_ANONYMOUS None -1 0 Shared anonymous memory, commonly between parent/child after fork()
POSIX Shared Memory MAP_SHARED None None shm_fd Usually 0 Shared memory between unrelated processes
Hardware Peripheral MAP_SHARED None None Device fd Driver/device-defined; commonly page-aligned MMIO/device mapping
Regular File MAP_SHARED or MAP_PRIVATE None None file_fd Page-aligned file offset File-backed mapping
Specific Virtual Address Any valid combination Any valid combination MAP_FIXED As required As required Forces requested virtual address; dangerous because existing mappings may be replaced


mmap() creates a virtual memory mapping.

It does NOT simply mean "allocate physical RAM and return its address."

The backing memory can be:

  • Anonymous RAM

  • Shared memory

  • A regular file

  • Device/MMIO memory

The process receives a virtual address.


Understanding Process Memory Expansion & Page Allocation

To clear up the doubt, separate:

  • Virtual Address Space

  • Physical RAM

  • Device/Physical Memory

1. How the Virtual Memory Space Changes

Assume a process already has several virtual-memory mappings.

When mmap() is called:

  • Kernel finds a suitable unused virtual-address range

  • Creates a new mapping/VMA

  • Returns the starting virtual address

  • Existing mappings are not automatically moved or extended

Example:

PROCESS VIRTUAL ADDRESS SPACE

[ Existing mapping ]
[ Existing mapping ]
[ Existing mapping ]

        ↓ mmap()

[ Existing mapping ]
[ Existing mapping ]
[ Existing mapping ]
[ Anonymous mapping ]

The new mapping does not necessarily appear immediately after the old mapping.

It can be somewhere else in the process virtual address space.


2. What happens in Physical RAM?

Virtual pages do NOT have to correspond to physically adjacent pages.

Example:

PROCESS VIRTUAL ADDRESS SPACE

Virtual Page 1  ───────────────> Physical Page X
Virtual Page 2  ───────────────> Physical Page A
Virtual Page 3  ───────────────> Physical Page Z
Virtual Page 4  ───────────────> Physical Page B

Anonymous mapping

Virtual Page 10 ──────────────> Physical Page M
Virtual Page 11 ──────────────> Physical Page Q
  • Virtual addresses appear contiguous

  • Physical pages can be anywhere in RAM

  • MMU/page tables maintain the translation


3. Anonymous Memory

When:

mmap(NULL, size,
     PROT_READ | PROT_WRITE,
     MAP_PRIVATE | MAP_ANONYMOUS,
     -1, 0);
  • Kernel creates a virtual mapping

  • Physical pages may NOT be allocated immediately

  • Pages can be allocated on demand when accessed

  • This is called demand paging

  • Physical RAM is managed independently from virtual address placement


4. Shared Memory

When two processes map the same shared-memory object:

PROCESS A                         PROCESS B

Virtual Address A                Virtual Address B
       |                                |
       v                                v
   [ Page X ]                        [ Page Y ]
       |                                |
       +------------+  +---------------+
                    |  |
                    v  v
              Same backing pages
              / shared object

Important:

  • Virtual addresses do NOT need to be the same

  • Both mappings refer to the same underlying shared-memory object/pages

  • Changes made through one mapping can be observed through the other

  • Synchronization is still required


5. Where does the I2C Peripheral fit?

The I2C controller is a hardware device, not normal RAM.

Conceptually:

Process Virtual Address
        |
        v
      MMU
        |
        v
 Page table / kernel mapping
        |
        v
Device physical/bus address
        |
        v
I2C Controller Registers
        |
        v
I2C Bus
  • The process receives a virtual address

  • CPU accesses that virtual address

  • MMU translates it according to the mapping

  • The access reaches the device register space

  • It does NOT mean the register is stored in normal physical RAM

  • On Linux, a device driver normally controls this mapping and hardware access


Summary of What Happens

When you call mmap():

1. Kernel creates a virtual-memory mapping for the requested range.
2. Kernel returns the starting virtual address.
3. The mapping can refer to anonymous memory, shared memory, a file, or device memory.
4. Anonymous memory may obtain physical pages lazily when accessed.
5. Shared memory allows multiple processes to map the same backing object.
6. Different processes can use different virtual addresses for the same shared memory.
7. Device mappings refer to device/MMIO address space rather than normal RAM.
8. `mmap()` itself is a mapping operation; it is not simply a physical-memory allocation API.

Quick Mental Model

                    mmap()
                       |
                       v
              PROCESS VIRTUAL ADDRESS
                       |
          +------------+-------------+
          |            |             |
          v            v             v
     Anonymous      File-backed     Device/MMIO
       Memory         Memory          Memory
          |            |             |
          v            v             v
     Physical RAM    File data      Device registers

Most Important Interview Points

VA != PA

mmap() != malloc()

MAP_ANONYMOUS != "cannot be shared"

MAP_SHARED != synchronization

Different processes
        ↓
different virtual addresses
        ↓
same shared backing pages

mmap() Flow

User Process
     |
     | mmap()
     v
Kernel
     |
     +---- Choose Virtual Address
     |
     +---- Create Virtual Memory Mapping
     |
     +---- Identify Backing Object
     |       |
     |       +-- Anonymous
     |       +-- File
     |       +-- Shared Memory
     |       +-- Device
     |
     v
Return Virtual Address
     |
     v
Process accesses memory
     |
     v
MMU / Page Tables
     |
     +----> Physical RAM
     |
     +----> File-backed pages
     |
     +----> Device/MMIO

Page Alignment Mental Model

Physical / Device Address
        |
        v
   0x13E8
        |
        +--------------------+
        |                    |
        v                    v
Page Base                 Offset
0x1000                    0x3E8

        |
        v

mmap(..., offset = 0x1000)

        |
        v

mapped_base
     +
   0x3E8
     |
     v
Target register

Embedded Systems

Part 1 of 1

Deep dive into Embedded concepts