Memory-Mapped I/O

Exercise: Memory-Mapped I/O Operations

Difficulty - Advanced

Estimated Time - 6-8 hours

Learning Objectives

  • Understand memory-mapped I/O and virtual memory concepts
  • Implement efficient file operations using mmap
  • Master shared memory for inter-process communication
  • Build memory-mapped data structures
  • Handle page faults and memory protection
  • Optimize I/O performance with memory mapping

Problem Statement

Build a high-performance file-based data storage system using memory-mapped I/O. Implement memory-mapped files, shared memory between processes, and persistent data structures that operate directly on memory-mapped regions. Compare performance with traditional file I/O approaches.

Memory-mapped I/O is fundamental to high-performance systems like databases, message queues, and memory-mapped files used by compilers and game engines for asset loading.

Real-World Scenario:

 1// Traditional file I/O - slow for random access
 2file, _ := os.Open("data.bin")
 3buffer := make([]byte, 4096)
 4file.ReadAt(buffer, offset)  // System call overhead
 5
 6// Memory-mapped I/O - direct memory access
 7mmap, _ := NewMemoryMap("data.bin", 1024*1024)
 8data := mmap.Read(offset, 4096)  // No system call, just memory read!
 9
10// Write directly to memory
11mmap.Write(offset, []byte("hello"))
12mmap.Sync()  // Flush to disk
13
14// Result: 10-100x faster for random access patterns!

Requirements

Functional Requirements

  1. Memory-Mapped Files: Map files into virtual address space
  2. Shared Memory: Inter-process communication via shared regions
  3. Memory-Mapped Data Structures: B-tree and hash table on mmap
  4. Page Management: Handle page faults and alignment
  5. Synchronization: Flush dirty pages to disk
  6. Protection: Set read/write/execute permissions
  7. Resizing: Dynamically grow/shrink mapped regions
  8. Performance Metrics: Compare mmap vs traditional I/O

Non-Functional Requirements

  • Support files from 4KB to 16GB
  • Handle concurrent access from multiple processes
  • Efficient memory usage
  • Graceful handling of disk full conditions
  • Cross-platform compatibility

Background and Theory

Memory-Mapped I/O Fundamentals

Memory-mapped I/O maps a file or device into the virtual address space of a process. Instead of using read()/write() system calls, the program accesses the file as if it were memory.

How It Works:

Virtual Memory:
[Code] [Data] [Heap] [Memory-Mapped Region] [Stack]
                       ↓
                    Page Table
                       ↓
              [File on Disk]

Benefits:

  1. No system call overhead: Direct memory access
  2. Shared memory: Multiple processes map same file
  3. Lazy loading: Pages loaded on first access
  4. Simplified code: Use pointers instead of offsets
  5. OS optimizations: Kernel manages caching automatically

Drawbacks:

  1. Address space limited: 32-bit systems limited to 4GB
  2. Page faults: First access to page causes page fault
  3. Synchronization complexity: Multiple processes need coordination
  4. Crash handling: Dirty pages may not be written

mmap System Call

On Unix-like systems, mmap() creates mappings:

1void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
2
3// Example:
4int fd = open("data.bin", O_RDWR);
5void *ptr = mmap(NULL, 1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
6// Now ptr[0..1MB-1] is the file contents

Protection Flags:

  • PROT_READ: Pages may be read
  • PROT_WRITE: Pages may be written
  • PROT_EXEC: Pages may be executed
  • PROT_NONE: Pages may not be accessed

Mapping Types:

  • MAP_SHARED: Changes visible to other processes, written to file
  • MAP_PRIVATE: Copy-on-write, changes not written to file
  • MAP_ANONYMOUS: Not backed by file
  • MAP_FIXED: Map at exact address

Page Faults and Demand Paging

When accessing a memory-mapped region for the first time:

  1. CPU generates page fault exception
  2. OS kernel traps to page fault handler
  3. Kernel loads page from disk into physical memory
  4. Kernel updates page table entry
  5. CPU restarts the faulting instruction

Major vs Minor Page Faults:

  • Minor: Page in memory but not in page table
  • Major: Page must be loaded from disk

Memory-Mapped Data Structures

B-Tree on mmap:

File Layout:
[Header][Node1][Node2][Node3]...

struct Header {
    magic: uint32
    root_offset: uint64
    num_nodes: uint64
}

struct BTreeNode {
    is_leaf: bool
    num_keys: uint16
    keys: [ORDER]uint64
    values: [ORDER]uint64  // or offsets
    children: [ORDER+1]uint64  // offsets to child nodes
}

Access pattern:

1header :=(unsafe.Pointer(&mmap[0]))
2rootNode :=(unsafe.Pointer(&mmap[header.root_offset]))

Use Cases

1. Databases

  • PostgreSQL: Shared buffers using mmap
  • LMDB: Lightning Memory-Mapped Database
  • LevelDB/RocksDB: SSTable files

2. Message Queues

  • Kafka: Memory-mapped log segments
  • Chronicle Queue: Low-latency messaging

3. Caching Systems

  • Redis: Persistence with memory snapshots
  • Memcached: Shared memory slabs

4. Game Engines

  • Asset loading: Textures, meshes, audio
  • Level data: Streaming from memory-mapped files

Implementation Challenges

Challenge 1: Platform-Specific APIs

Issue: mmap API differs between Unix and Windows.

Solution: Abstract platform differences:

 1//go:build unix
 2
 3func mmap(fd int, size int, prot int, flags int) {
 4    data, err := syscall.Mmap(fd, 0, size, prot, flags)
 5    return data, err
 6}
 7
 8//go:build windows
 9
10func mmap(handle syscall.Handle, size int, prot uint32) {
11    mapping, err := syscall.CreateFileMapping(handle, nil, prot, 0, uint32(size), nil)
12    // ...
13    return data, err
14}

Challenge 2: Alignment and Padding

Issue: mmap requires page-aligned offsets.

Solution: Always align to page size:

1const PageSize = 4096
2
3func alignSize(size int) int {
4    return & ^(PageSize - 1)
5}
6
7func alignOffset(offset int64) int64 {
8    return * PageSize
9}

Challenge 3: Concurrent Access

Issue: Multiple processes mapping same file need synchronization.

Solution: Use file locks or atomic operations:

1// Advisory lock
2syscall.Flock(fd, syscall.LOCK_EX)
3// ... critical section ...
4syscall.Flock(fd, syscall.LOCK_UN)
5
6// Or use atomic operations on shared memory
7atomic.AddInt64((*int64)(unsafe.Pointer(&mmap[0])), 1)

Challenge 4: Synchronization and Persistence

Issue: Changes in memory may not be written to disk immediately.

Solution: Use msync() to force write-back:

1func Sync() error {
2    _, _, err := syscall.Syscall(
3        syscall.SYS_MSYNC,
4        uintptr(unsafe.Pointer(&m.Data[0])),
5        uintptr(len(m.Data)),
6        syscall.MS_SYNC,  // Synchronous write
7    )
8    return err
9}

Hints

Hint 1: Start with Simple Read-Only Mapping

Create a basic memory-mapped file reader:

 1func MapFile(filename string) {
 2    file, err := os.Open(filename)
 3    if err != nil {
 4        return nil, err
 5    }
 6
 7    fi, err := file.Stat()
 8    if err != nil {
 9        return nil, err
10    }
11
12    data, err := syscall.Mmap(
13        int(file.Fd()),
14        0,
15        int(fi.Size()),
16        syscall.PROT_READ,
17        syscall.MAP_SHARED,
18    )
19
20    return data, err
21}
Hint 2: Use unsafe.Pointer for Structs

Access structured data in memory-mapped region:

 1type Header struct {
 2    Magic   uint32
 3    Version uint32
 4    Size    uint64
 5}
 6
 7func GetHeader() *Header {
 8    return(unsafe.Pointer(&m.Data[0]))
 9}
10
11func GetRecord(offset int) *Record {
12    return(unsafe.Pointer(&m.Data[offset]))
13}
Hint 3: Implement Safe Read/Write Methods

Add bounds checking to prevent segfaults:

 1func ReadAt(offset int, size int) {
 2    if offset < 0 || offset+size > len(m.Data) {
 3        return nil, fmt.Errorf("out of bounds")
 4    }
 5    return m.Data[offset : offset+size], nil
 6}
 7
 8func WriteAt(offset int, data []byte) error {
 9    if offset < 0 || offset+len(data) > len(m.Data) {
10        return fmt.Errorf("out of bounds")
11    }
12    copy(m.Data[offset:], data)
13    return nil
14}
Hint 4: Handle Dynamic Resizing

Grow memory-mapped file when needed:

 1func Resize(newSize int) error {
 2    // Unmap old region
 3    syscall.Munmap(m.Data)
 4
 5    // Resize file
 6    if err := m.File.Truncate(int64(newSize)); err != nil {
 7        return err
 8    }
 9
10    // Remap with new size
11    data, err := syscall.Mmap(
12        int(m.File.Fd()),
13        0,
14        newSize,
15        syscall.PROT_READ|syscall.PROT_WRITE,
16        syscall.MAP_SHARED,
17    )
18
19    m.Data = data
20    return err
21}
Hint 5: Create Shared Memory for IPC

Use anonymous mapping for inter-process shared memory:

 1func CreateSharedMemory(size int) {
 2    data, err := syscall.Mmap(
 3        -1,  // No file backing
 4        0,
 5        size,
 6        syscall.PROT_READ|syscall.PROT_WRITE,
 7        syscall.MAP_SHARED|syscall.MAP_ANONYMOUS,
 8    )
 9    return data, err
10}

Solution

Show Complete Solution

Approach

We'll implement:

  1. Memory-Mapped File Manager - Core mmap functionality
  2. Memory-Mapped B-Tree - Persistent B-tree on disk
  3. Shared Memory Ring Buffer - IPC between processes
  4. Performance Benchmarks - Compare mmap vs traditional I/O

Implementation

  1package mmap
  2
  3import (
  4    "encoding/binary"
  5    "fmt"
  6    "os"
  7    "sync"
  8    "syscall"
  9    "unsafe"
 10)
 11
 12// ===== Memory-Mapped File =====
 13
 14const PageSize = 4096
 15
 16// MemoryMap represents a memory-mapped file
 17type MemoryMap struct {
 18    File     *os.File
 19    Data     []byte
 20    Size     int64
 21    Writable bool
 22    mu       sync.RWMutex
 23}
 24
 25// NewMemoryMap creates a new memory-mapped file
 26func NewMemoryMap(filename string, size int64, writable bool) {
 27    flags := os.O_RDONLY
 28    prot := syscall.PROT_READ
 29    mapFlags := syscall.MAP_SHARED
 30
 31    if writable {
 32        flags = os.O_RDWR | os.O_CREATE
 33        prot = syscall.PROT_READ | syscall.PROT_WRITE
 34    }
 35
 36    file, err := os.OpenFile(filename, flags, 0644)
 37    if err != nil {
 38        return nil, err
 39    }
 40
 41    // Ensure file is large enough
 42    if writable {
 43        if err := file.Truncate(size); err != nil {
 44            file.Close()
 45            return nil, err
 46        }
 47    }
 48
 49    // Get actual file size
 50    fi, err := file.Stat()
 51    if err != nil {
 52        file.Close()
 53        return nil, err
 54    }
 55
 56    actualSize := fi.Size()
 57    if actualSize == 0 {
 58        return nil, fmt.Errorf("cannot mmap empty file")
 59    }
 60
 61    // Memory map the file
 62    data, err := syscall.Mmap(
 63        int(file.Fd()),
 64        0,
 65        int(actualSize),
 66        prot,
 67        mapFlags,
 68    )
 69
 70    if err != nil {
 71        file.Close()
 72        return nil, err
 73    }
 74
 75    return &MemoryMap{
 76        File:     file,
 77        Data:     data,
 78        Size:     actualSize,
 79        Writable: writable,
 80    }, nil
 81}
 82
 83// Close unmaps and closes the file
 84func Close() error {
 85    m.mu.Lock()
 86    defer m.mu.Unlock()
 87
 88    if err := syscall.Munmap(m.Data); err != nil {
 89        return err
 90    }
 91
 92    return m.File.Close()
 93}
 94
 95// ReadAt reads data at offset
 96func ReadAt(offset int64, size int) {
 97    m.mu.RLock()
 98    defer m.mu.RUnlock()
 99
100    if offset < 0 || offset+int64(size) > m.Size {
101        return nil, fmt.Errorf("read out of bounds")
102    }
103
104    result := make([]byte, size)
105    copy(result, m.Data[offset:offset+int64(size)])
106    return result, nil
107}
108
109// WriteAt writes data at offset
110func WriteAt(offset int64, data []byte) error {
111    m.mu.Lock()
112    defer m.mu.Unlock()
113
114    if !m.Writable {
115        return fmt.Errorf("memory map is read-only")
116    }
117
118    if offset < 0 || offset+int64(len(data)) > m.Size {
119        return fmt.Errorf("write out of bounds")
120    }
121
122    copy(m.Data[offset:], data)
123    return nil
124}
125
126// Sync flushes changes to disk
127func Sync() error {
128    m.mu.RLock()
129    defer m.mu.RUnlock()
130
131    _, _, errno := syscall.Syscall(
132        syscall.SYS_MSYNC,
133        uintptr(unsafe.Pointer(&m.Data[0])),
134        uintptr(len(m.Data)),
135        syscall.MS_SYNC,
136    )
137
138    if errno != 0 {
139        return errno
140    }
141
142    return nil
143}
144
145// Resize changes the size of the memory map
146func Resize(newSize int64) error {
147    m.mu.Lock()
148    defer m.mu.Unlock()
149
150    if !m.Writable {
151        return fmt.Errorf("cannot resize read-only map")
152    }
153
154    // Unmap current region
155    if err := syscall.Munmap(m.Data); err != nil {
156        return err
157    }
158
159    // Resize file
160    if err := m.File.Truncate(newSize); err != nil {
161        return err
162    }
163
164    // Remap with new size
165    data, err := syscall.Mmap(
166        int(m.File.Fd()),
167        0,
168        int(newSize),
169        syscall.PROT_READ|syscall.PROT_WRITE,
170        syscall.MAP_SHARED,
171    )
172
173    if err != nil {
174        return err
175    }
176
177    m.Data = data
178    m.Size = newSize
179
180    return nil
181}
182
183// ===== Memory-Mapped B-Tree =====
184
185const (
186    BTreeOrder    = 16
187    BTreeNodeSize = 1024
188)
189
190// BTreeHeader is the file header
191type BTreeHeader struct {
192    Magic      uint32
193    Version    uint32
194    RootOffset int64
195    NumNodes   int64
196    NextOffset int64
197}
198
199// BTreeNode is a B-tree node
200type BTreeNode struct {
201    IsLeaf   bool
202    NumKeys  int16
203    _        [5]byte // Padding
204    Keys     [BTreeOrder - 1]int64
205    Values   [BTreeOrder - 1]int64
206    Children [BTreeOrder]int64 // Offsets to child nodes
207}
208
209// MemoryMappedBTree implements a persistent B-tree
210type MemoryMappedBTree struct {
211    mmap   *MemoryMap
212    header *BTreeHeader
213    mu     sync.RWMutex
214}
215
216// NewMemoryMappedBTree creates a new memory-mapped B-tree
217func NewMemoryMappedBTree(filename string) {
218    initialSize := int64(PageSize * 256) // 1 MB initial size
219
220    mmap, err := NewMemoryMap(filename, initialSize, true)
221    if err != nil {
222        return nil, err
223    }
224
225    bt := &MemoryMappedBTree{
226        mmap:   mmap,
227        header:(unsafe.Pointer(&mmap.Data[0])),
228    }
229
230    // Initialize header if new file
231    if bt.header.Magic == 0 {
232        bt.header.Magic = 0xB7EE0001
233        bt.header.Version = 1
234        bt.header.RootOffset = int64(PageSize)
235        bt.header.NumNodes = 1
236        bt.header.NextOffset = int64(PageSize + BTreeNodeSize)
237
238        // Initialize root node
239        root := bt.getNode(bt.header.RootOffset)
240        root.IsLeaf = true
241        root.NumKeys = 0
242    }
243
244    return bt, nil
245}
246
247// getNode returns a pointer to a node at offset
248func getNode(offset int64) *BTreeNode {
249    if offset < 0 || offset >= bt.mmap.Size {
250        return nil
251    }
252    return(unsafe.Pointer(&bt.mmap.Data[offset]))
253}
254
255// allocateNode allocates a new node
256func allocateNode() {
257    offset := bt.header.NextOffset
258
259    // Check if we need to resize
260    if offset+BTreeNodeSize >= bt.mmap.Size {
261        newSize := bt.mmap.Size * 2
262        bt.mmap.Resize(newSize)
263        bt.header =(unsafe.Pointer(&bt.mmap.Data[0]))
264    }
265
266    bt.header.NextOffset += BTreeNodeSize
267    bt.header.NumNodes++
268
269    node := bt.getNode(offset)
270    node.NumKeys = 0
271    node.IsLeaf = true
272
273    return offset, node
274}
275
276// Insert inserts a key-value pair
277func Insert(key, value int64) error {
278    bt.mu.Lock()
279    defer bt.mu.Unlock()
280
281    root := bt.getNode(bt.header.RootOffset)
282
283    // If root is full, split it
284    if root.NumKeys == BTreeOrder-1 {
285        newRootOffset, newRoot := bt.allocateNode()
286        newRoot.IsLeaf = false
287        newRoot.Children[0] = bt.header.RootOffset
288
289        bt.splitChild(newRoot, 0)
290        bt.header.RootOffset = newRootOffset
291
292        root = newRoot
293    }
294
295    bt.insertNonFull(root, key, value)
296    return nil
297}
298
299// insertNonFull inserts into a non-full node
300func insertNonFull(node *BTreeNode, key, value int64) {
301    i := int(node.NumKeys) - 1
302
303    if node.IsLeaf {
304        // Shift keys to make room
305        for i >= 0 && key < node.Keys[i] {
306            node.Keys[i+1] = node.Keys[i]
307            node.Values[i+1] = node.Values[i]
308            i--
309        }
310
311        node.Keys[i+1] = key
312        node.Values[i+1] = value
313        node.NumKeys++
314    } else {
315        // Find child to insert into
316        for i >= 0 && key < node.Keys[i] {
317            i--
318        }
319        i++
320
321        child := bt.getNode(node.Children[i])
322
323        if child.NumKeys == BTreeOrder-1 {
324            bt.splitChild(node, int16(i))
325            if key > node.Keys[i] {
326                i++
327            }
328            child = bt.getNode(node.Children[i])
329        }
330
331        bt.insertNonFull(child, key, value)
332    }
333}
334
335// splitChild splits a full child
336func splitChild(parent *BTreeNode, index int16) {
337    fullChild := bt.getNode(parent.Children[index])
338    newOffset, newChild := bt.allocateNode()
339    newChild.IsLeaf = fullChild.IsLeaf
340
341    mid := / 2
342
343    // Copy upper half to new child
344    newChild.NumKeys = int16(BTreeOrder - 1 - mid - 1)
345    for j := 0; j < int(newChild.NumKeys); j++ {
346        newChild.Keys[j] = fullChild.Keys[mid+1+j]
347        newChild.Values[j] = fullChild.Values[mid+1+j]
348    }
349
350    if !fullChild.IsLeaf {
351        for j := 0; j < int(newChild.NumKeys)+1; j++ {
352            newChild.Children[j] = fullChild.Children[mid+1+j]
353        }
354    }
355
356    fullChild.NumKeys = int16(mid)
357
358    // Shift parent's keys/children to make room
359    for j := int(parent.NumKeys); j > int(index); j-- {
360        parent.Children[j+1] = parent.Children[j]
361    }
362    parent.Children[index+1] = newOffset
363
364    for j := int(parent.NumKeys) - 1; j >= int(index); j-- {
365        parent.Keys[j+1] = parent.Keys[j]
366        parent.Values[j+1] = parent.Values[j]
367    }
368
369    parent.Keys[index] = fullChild.Keys[mid]
370    parent.Values[index] = fullChild.Values[mid]
371    parent.NumKeys++
372}
373
374// Search searches for a key
375func Search(key int64) {
376    bt.mu.RLock()
377    defer bt.mu.RUnlock()
378
379    return bt.searchNode(bt.getNode(bt.header.RootOffset), key)
380}
381
382// searchNode searches within a node
383func searchNode(node *BTreeNode, key int64) {
384    i := 0
385    for i < int(node.NumKeys) && key > node.Keys[i] {
386        i++
387    }
388
389    if i < int(node.NumKeys) && key == node.Keys[i] {
390        return node.Values[i], true
391    }
392
393    if node.IsLeaf {
394        return 0, false
395    }
396
397    child := bt.getNode(node.Children[i])
398    return bt.searchNode(child, key)
399}
400
401// Close closes the B-tree
402func Close() error {
403    bt.mmap.Sync()
404    return bt.mmap.Close()
405}
406
407// ===== Shared Memory Ring Buffer =====
408
409// RingBufferHeader for shared memory
410type RingBufferHeader struct {
411    WritePos int64
412    ReadPos  int64
413    Size     int64
414    _        [40]byte // Padding to cache line
415}
416
417// SharedRingBuffer implements IPC ring buffer
418type SharedRingBuffer struct {
419    mmap   *MemoryMap
420    header *RingBufferHeader
421    data   []byte
422}
423
424// NewSharedRingBuffer creates a shared memory ring buffer
425func NewSharedRingBuffer(filename string, size int64) {
426    totalSize := int64(unsafe.Sizeof(RingBufferHeader{})) + size
427
428    mmap, err := NewMemoryMap(filename, totalSize, true)
429    if err != nil {
430        return nil, err
431    }
432
433    rb := &SharedRingBuffer{
434        mmap:   mmap,
435        header:(unsafe.Pointer(&mmap.Data[0])),
436        data:   mmap.Data[unsafe.Sizeof(RingBufferHeader{}):],
437    }
438
439    // Initialize header if new
440    if rb.header.Size == 0 {
441        rb.header.Size = size
442    }
443
444    return rb, nil
445}
446
447// Write writes data to ring buffer
448func Write(data []byte) error {
449    for {
450        writePos := rb.header.WritePos
451        readPos := rb.header.ReadPos
452
453        available := rb.header.Size -
454        if int64(len(data)) > available {
455            return fmt.Errorf("buffer full")
456        }
457
458        pos := writePos % rb.header.Size
459        n := copy(rb.data[pos:], data)
460
461        if n < len(data) {
462            // Wrap around
463            copy(rb.data[0:], data[n:])
464        }
465
466        // Try to update write position atomically
467        if atomic.CompareAndSwapInt64(&rb.header.WritePos, writePos, writePos+int64(len(data))) {
468            return nil
469        }
470    }
471}
472
473// Read reads data from ring buffer
474func Read(buf []byte) {
475    for {
476        writePos := rb.header.WritePos
477        readPos := rb.header.ReadPos
478
479        available := writePos - readPos
480        if available == 0 {
481            return 0, fmt.Errorf("buffer empty")
482        }
483
484        toRead := int64(len(buf))
485        if toRead > available {
486            toRead = available
487        }
488
489        pos := readPos % rb.header.Size
490        n := copy(buf, rb.data[pos:pos+toRead])
491
492        if int64(n) < toRead {
493            // Wrap around
494            copy(buf[n:], rb.data[0:toRead-int64(n)])
495        }
496
497        // Try to update read position atomically
498        if atomic.CompareAndSwapInt64(&rb.header.ReadPos, readPos, readPos+toRead) {
499            return int(toRead), nil
500        }
501    }
502}
503
504// Close closes the ring buffer
505func Close() error {
506    return rb.mmap.Close()
507}
508
509// ===== Utilities =====
510
511import "sync/atomic"
512
513// AlignSize aligns size to page boundary
514func AlignSize(size int) int {
515    return & ^(PageSize - 1)
516}
517
518// AlignOffset aligns offset to page boundary
519func AlignOffset(offset int64) int64 {
520    return * PageSize
521}

Testing

  1package mmap
  2
  3import (
  4    "io/ioutil"
  5    "os"
  6    "testing"
  7)
  8
  9func TestMemoryMap_ReadWrite(t *testing.T) {
 10    tmpfile, _ := ioutil.TempFile("", "mmap_test")
 11    defer os.Remove(tmpfile.Name())
 12
 13    mmap, err := NewMemoryMap(tmpfile.Name(), 4096, true)
 14    if err != nil {
 15        t.Fatalf("NewMemoryMap failed: %v", err)
 16    }
 17    defer mmap.Close()
 18
 19    // Write data
 20    data := []byte("Hello, mmap!")
 21    if err := mmap.WriteAt(0, data); err != nil {
 22        t.Fatalf("WriteAt failed: %v", err)
 23    }
 24
 25    // Read back
 26    read, err := mmap.ReadAt(0, len(data))
 27    if err != nil {
 28        t.Fatalf("ReadAt failed: %v", err)
 29    }
 30
 31    if string(read) != string(data) {
 32        t.Errorf("Expected %s, got %s", data, read)
 33    }
 34}
 35
 36func TestMemoryMap_Sync(t *testing.T) {
 37    tmpfile, _ := ioutil.TempFile("", "mmap_sync")
 38    defer os.Remove(tmpfile.Name())
 39
 40    mmap, _ := NewMemoryMap(tmpfile.Name(), 4096, true)
 41
 42    mmap.WriteAt(0, []byte("test"))
 43    if err := mmap.Sync(); err != nil {
 44        t.Errorf("Sync failed: %v", err)
 45    }
 46
 47    mmap.Close()
 48
 49    // Verify data persisted
 50    mmap2, _ := NewMemoryMap(tmpfile.Name(), 4096, false)
 51    defer mmap2.Close()
 52
 53    data, _ := mmap2.ReadAt(0, 4)
 54    if string(data) != "test" {
 55        t.Error("Data not persisted")
 56    }
 57}
 58
 59func TestBTree_InsertSearch(t *testing.T) {
 60    tmpfile, _ := ioutil.TempFile("", "btree_test")
 61    defer os.Remove(tmpfile.Name())
 62
 63    bt, err := NewMemoryMappedBTree(tmpfile.Name())
 64    if err != nil {
 65        t.Fatalf("NewMemoryMappedBTree failed: %v", err)
 66    }
 67    defer bt.Close()
 68
 69    // Insert keys
 70    for i := int64(1); i <= 100; i++ {
 71        bt.Insert(i, i*10)
 72    }
 73
 74    // Search
 75    for i := int64(1); i <= 100; i++ {
 76        val, found := bt.Search(i)
 77        if !found || val != i*10 {
 78            t.Errorf("Search(%d) = %d, want %d", i, val, i*10)
 79        }
 80    }
 81
 82    // Search non-existent
 83    if _, found := bt.Search(999); found {
 84        t.Error("Found non-existent key")
 85    }
 86}
 87
 88func TestSharedRingBuffer(t *testing.T) {
 89    tmpfile, _ := ioutil.TempFile("", "ringbuf_test")
 90    defer os.Remove(tmpfile.Name())
 91
 92    rb, err := NewSharedRingBuffer(tmpfile.Name(), 1024)
 93    if err != nil {
 94        t.Fatalf("NewSharedRingBuffer failed: %v", err)
 95    }
 96    defer rb.Close()
 97
 98    // Write
 99    msg := []byte("Hello, IPC!")
100    if err := rb.Write(msg); err != nil {
101        t.Fatalf("Write failed: %v", err)
102    }
103
104    // Read
105    buf := make([]byte, 1024)
106    n, err := rb.Read(buf)
107    if err != nil {
108        t.Fatalf("Read failed: %v", err)
109    }
110
111    if string(buf[:n]) != string(msg) {
112        t.Errorf("Expected %s, got %s", msg, buf[:n])
113    }
114}
115
116func BenchmarkMmap_RandomRead(b *testing.B) {
117    tmpfile, _ := ioutil.TempFile("", "bench_mmap")
118    defer os.Remove(tmpfile.Name())
119
120    size := int64(10 * 1024 * 1024) // 10 MB
121    mmap, _ := NewMemoryMap(tmpfile.Name(), size, true)
122    defer mmap.Close()
123
124    b.ResetTimer()
125    for i := 0; i < b.N; i++ {
126        offset := int64(i%10000) * 1024
127        mmap.ReadAt(offset, 128)
128    }
129}
130
131func BenchmarkFile_RandomRead(b *testing.B) {
132    tmpfile, _ := ioutil.TempFile("", "bench_file")
133    defer os.Remove(tmpfile.Name())
134
135    size := int64(10 * 1024 * 1024)
136    tmpfile.Truncate(size)
137
138    buf := make([]byte, 128)
139
140    b.ResetTimer()
141    for i := 0; i < b.N; i++ {
142        offset := int64(i%10000) * 1024
143        tmpfile.ReadAt(buf, offset)
144    }
145}

Usage Example

 1package main
 2
 3import (
 4    "fmt"
 5    "mmap"
 6)
 7
 8func main() {
 9    fmt.Println("=== Memory-Mapped File ===")
10
11    // Create memory-mapped file
12    m, _ := mmap.NewMemoryMap("data.bin", 1024*1024, true)
13    defer m.Close()
14
15    // Write data
16    m.WriteAt(0, []byte("Hello, mmap!"))
17
18    // Read data
19    data, _ := m.ReadAt(0, 12)
20    fmt.Printf("Read: %s\n", data)
21
22    // Sync to disk
23    m.Sync()
24
25    fmt.Println("\n=== Memory-Mapped B-Tree ===")
26
27    // Create persistent B-tree
28    bt, _ := mmap.NewMemoryMappedBTree("btree.db")
29    defer bt.Close()
30
31    // Insert 1000 key-value pairs
32    for i := int64(1); i <= 1000; i++ {
33        bt.Insert(i, i*100)
34    }
35
36    // Search
37    if val, found := bt.Search(500); found {
38        fmt.Printf("Found: 500 -> %d\n", val)
39    }
40
41    fmt.Println("\n=== Shared Memory IPC ===")
42
43    // Create shared ring buffer
44    rb, _ := mmap.NewSharedRingBuffer("ringbuf.dat", 4096)
45    defer rb.Close()
46
47    // Producer process writes
48    rb.Write([]byte("Message from producer"))
49
50    // Consumer process reads
51    buf := make([]byte, 1024)
52    n, _ := rb.Read(buf)
53    fmt.Printf("Received: %s\n", buf[:n])
54}

Testing Your Solution

Test these scenarios:

  1. Basic I/O: Read and write to memory-mapped file
  2. Persistence: Data survives program restart
  3. Boundaries: Proper handling of out-of-bounds access
  4. Resizing: Dynamic growth of mapped region
  5. Synchronization: msync flushes to disk
  6. Concurrent Access: Multiple processes accessing same file
  7. Performance: Compare mmap vs traditional file I/O
  8. B-Tree: Correctness of persistent B-tree operations

Verification Checklist:

  • Memory mapping succeeds for various file sizes
  • Read/write operations are correct
  • Out-of-bounds access returns errors
  • Data persists after sync and close
  • Resizing works correctly
  • B-tree maintains sorted order
  • Shared memory enables IPC
  • Performance better than traditional I/O for random access

Bonus Challenges

  1. Memory-Mapped Hash Table: Implement persistent hash table
  2. Transaction Log: Write-ahead log with mmap
  3. Copy-on-Write: Use MAP_PRIVATE for versioning
  4. Huge Pages: Use huge pages for better TLB performance
  5. Memory Advisor: Use madvise() for performance hints
  6. Lock-Free Structures: Combine mmap with lock-free algorithms
  7. Compression: Compress data in memory-mapped region
  8. Encryption: Transparent encryption layer
  9. Versioning: Multi-version concurrency control
  10. Remote Memory: Access remote memory via RDMA

Key Takeaways

  • Memory-mapped I/O eliminates system call overhead for file access
  • Demand paging loads data lazily on first access
  • Shared memory enables efficient IPC between processes
  • Page alignment is critical for mmap operations
  • msync is required for durability - changes may not persist otherwise
  • mmap is fastest for random access but traditional I/O can be better for sequential
  • Virtual address space limits mmap size on 32-bit systems
  • Memory-mapped data structures must be platform-aware

Memory-mapped I/O is a powerful technique for high-performance systems, but requires careful handling of synchronization, persistence, and platform differences.

References