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
0x7fff0000Without
MAP_FIXED-> this is only a hint to the kernelKernel may map at a different virtual address if required
With
MAP_FIXED-> kernel tries to map at exactly this virtual addressExisting 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
offsetpassed to mmap must satisfy the alignment requirements of the mappingIf 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_SIZEis 4096 (0x1000)MAP_MASKisMAP_SIZE - 1, which is 4095 (0x0FFF)~MAP_MASKgives0xFFFFF000PERIPHERAL_PHY_BASE & ~MAP_MASKremoves 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_NONEDefines what operations the process is allowed to perform on the mapped memory
Example:
PROT_READ-> read allowedPROT_WRITE-> write allowedPROT_READ | PROT_WRITE-> read + write
4th arg (flag)
MAP_PRIVATEChanges are private to the process
For file-backed mappings, writes use copy-on-write
MAP_SHAREDChanges are visible through other mappings of the same backing object
Commonly used for shared memory and device mappings
MAP_ANONYMOUSMapping has no file backing
fdis normally-1Can be combined with
MAP_PRIVATEorMAP_SHARED
MAP_FIXEDForces mapping at the exact virtual address supplied in 1st argument
May replace an existing mapping
MAP_FIXED_NOREPLACERequires the requested virtual address range to be unused
Fails instead of replacing an existing mapping
5th arg (file descriptor)
fdidentifies the object backing the mappingIt is NOT necessarily a physical address
Depending on the mapping type:
Hardware/device mapping
fdcan be a device file descriptorExample:
/dev/...
Regular file mapping
fdis the file descriptor returned byopen()
POSIX shared memory
fdis returned byshm_open()
Anonymous memory
fd = -1No file backing
6th arg
offset(typeoff_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 = -1offset = 0
POSIX shared memory
Usually
offset = 0Can 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
0x13E8Page size = 4096 =
0x1000Page boundaries:
0x00000x10000x2000...
0x13E8is inside the page starting at0x1000
phy_base_addr = 0x13E8 & 0xFFFFF000; offset = 0x13E8 & 0x00000FFF;Results:
phy_base_addr = 0x1000 offset = 0x03E8Then:
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 + 0x03E8mapped_baseis a virtual address0x03E8is an offset inside that mapped pageDo NOT treat
mapped_base + offsetas a physical address
Pointer arithmetic:
(char *)mapped_base + offsetchar *increments in 1-byte unitsThen 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_baseis a virtual address returned bymmap()PERIPHERAL_PHY_BASE & ~MAP_MASK- gives page-aligned physical/device offset
PERIPHERAL_PHY_BASE & MAP_MASK- gives offset inside that page
volatiletells 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 backingfd = -1offset = 0BUFFER_SIZEmeans 1000 bytessizeof(BUFFER_SIZE)would NOT mean 1000 bytesbecause
BUFFER_SIZEis a macro replaced by1000sizeof(1000)is normallysizeof(int)
volatileis not required for normal RAMFor inter-process shared anonymous memory:
use
MAP_SHARED | MAP_ANONYMOUStypically 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_addrpoints to the shared mapped memoryDo NOT create a separate local
sensor_dataand read from itBoth processes must access the data through the
mmap()returned addresssem_wait()/sem_post()provide synchronizationShared 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 flagftruncate()-> sets the size of the POSIX shared-memory objectmmap()-> maps that object into process virtual address spaceWriter 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 removedsem_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