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
- Memory-Mapped Files: Map files into virtual address space
- Shared Memory: Inter-process communication via shared regions
- Memory-Mapped Data Structures: B-tree and hash table on mmap
- Page Management: Handle page faults and alignment
- Synchronization: Flush dirty pages to disk
- Protection: Set read/write/execute permissions
- Resizing: Dynamically grow/shrink mapped regions
- 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:
- No system call overhead: Direct memory access
- Shared memory: Multiple processes map same file
- Lazy loading: Pages loaded on first access
- Simplified code: Use pointers instead of offsets
- OS optimizations: Kernel manages caching automatically
Drawbacks:
- Address space limited: 32-bit systems limited to 4GB
- Page faults: First access to page causes page fault
- Synchronization complexity: Multiple processes need coordination
- 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 readPROT_WRITE: Pages may be writtenPROT_EXEC: Pages may be executedPROT_NONE: Pages may not be accessed
Mapping Types:
MAP_SHARED: Changes visible to other processes, written to fileMAP_PRIVATE: Copy-on-write, changes not written to fileMAP_ANONYMOUS: Not backed by fileMAP_FIXED: Map at exact address
Page Faults and Demand Paging
When accessing a memory-mapped region for the first time:
- CPU generates page fault exception
- OS kernel traps to page fault handler
- Kernel loads page from disk into physical memory
- Kernel updates page table entry
- 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:
- Memory-Mapped File Manager - Core mmap functionality
- Memory-Mapped B-Tree - Persistent B-tree on disk
- Shared Memory Ring Buffer - IPC between processes
- 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:
- Basic I/O: Read and write to memory-mapped file
- Persistence: Data survives program restart
- Boundaries: Proper handling of out-of-bounds access
- Resizing: Dynamic growth of mapped region
- Synchronization: msync flushes to disk
- Concurrent Access: Multiple processes accessing same file
- Performance: Compare mmap vs traditional file I/O
- 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
- Memory-Mapped Hash Table: Implement persistent hash table
- Transaction Log: Write-ahead log with mmap
- Copy-on-Write: Use MAP_PRIVATE for versioning
- Huge Pages: Use huge pages for better TLB performance
- Memory Advisor: Use madvise() for performance hints
- Lock-Free Structures: Combine mmap with lock-free algorithms
- Compression: Compress data in memory-mapped region
- Encryption: Transparent encryption layer
- Versioning: Multi-version concurrency control
- 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.