-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #175 from Consensys/156-mmap-cherrypick
feat: Initial Support for Memory Mapped Files
- Loading branch information
Showing
6 changed files
with
252 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package mmap | ||
|
||
import ( | ||
"errors" | ||
"io" | ||
"runtime/debug" | ||
"syscall" | ||
|
||
pkgErrors "github.com/pkg/errors" | ||
"golang.org/x/sys/unix" | ||
) | ||
|
||
// BlockDevice represents a mmap block device holding a reference to a file descriptor. | ||
type BlockDevice struct { | ||
FileDescriptor int | ||
Data []byte | ||
} | ||
|
||
// NewBlockDevice creates a BlockDevice from a file | ||
// descriptor referring either to a regular file or UNIX device node. To | ||
// speed up reads, a memory map is used. | ||
func NewBlockDevice(fileDescriptor, sizeBytes int) (*BlockDevice, error) { | ||
data, err := unix.Mmap(fileDescriptor, 0, sizeBytes, syscall.PROT_READ, syscall.MAP_SHARED) | ||
if err != nil { | ||
return nil, pkgErrors.Wrap(err, "failed to memory map block device") | ||
} | ||
|
||
return &BlockDevice{ | ||
FileDescriptor: fileDescriptor, | ||
Data: data, | ||
}, nil | ||
} | ||
|
||
// ReadAt reads through the memory map at a given offset. | ||
func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) { | ||
// Let read actions go through the memory map to prevent system | ||
// call overhead for commonly requested objects. | ||
if off < 0 { | ||
return 0, syscall.EINVAL | ||
} | ||
|
||
if off > int64(len(bd.Data)) { | ||
return 0, io.EOF | ||
} | ||
// Install a page fault handler, so that I/O errors against the | ||
// memory map (e.g., due to disk failure) don't cause us to | ||
// crash. | ||
old := debug.SetPanicOnFault(true) | ||
defer func() { | ||
debug.SetPanicOnFault(old) | ||
|
||
if recover() != nil { | ||
err = errors.New("page fault occurred while reading from memory map") | ||
} | ||
}() | ||
|
||
n = copy(p, bd.Data[off:]) | ||
if n < len(p) { | ||
err = io.EOF | ||
} | ||
|
||
return | ||
} | ||
|
||
// WriteAt writes at a given offset. | ||
func (bd *BlockDevice) WriteAt(p []byte, off int64) (int, error) { | ||
// Let write actions go through the file descriptor. Doing so | ||
// yields better performance, as writes through a memory map | ||
// would trigger a page fault that causes data to be read. | ||
// | ||
// The pwrite() system call cannot return a size and error at | ||
// the same time. If an error occurs after one or more bytes are | ||
// written, it returns the size without an error (a "short | ||
// write"). As WriteAt() must return an error in those cases, we | ||
// must invoke pwrite() repeatedly. | ||
// | ||
// TODO: Maybe it makes sense to let unaligned writes that would | ||
// trigger reads anyway to go through the memory map? | ||
nTotal := 0 | ||
|
||
for len(p) > 0 { | ||
n, err := unix.Pwrite(bd.FileDescriptor, p, off) | ||
nTotal += n | ||
|
||
if err != nil { | ||
return nTotal, err | ||
} | ||
|
||
p = p[n:] | ||
off += int64(n) | ||
} | ||
|
||
return nTotal, nil | ||
} | ||
|
||
// Sync synchronizes a file's in-core state with storage device. | ||
func (bd *BlockDevice) Sync() error { | ||
return unix.Fsync(bd.FileDescriptor) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package mmap | ||
|
||
import ( | ||
pkgErrors "github.com/pkg/errors" | ||
"golang.org/x/sys/unix" | ||
) | ||
|
||
// File represents a memory-mapped file. | ||
type File struct { | ||
BlockDevice *BlockDevice | ||
SectorSizeBytes int | ||
SectorCount int64 | ||
} | ||
|
||
// NewFile constructs a new instance of File. | ||
func NewFile(path string, minimumSizeBytes int) (*File, error) { | ||
fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDWR|unix.O_APPEND, 0666) | ||
if err != nil { | ||
return nil, pkgErrors.Wrapf(err, "failed to open file %#v", path) | ||
} | ||
|
||
// Use the block size returned by fstat() to determine the | ||
// sector size and the number of sectors needed to store the | ||
// desired amount of space. | ||
var stat unix.Stat_t | ||
if err := unix.Fstat(fd, &stat); err != nil { | ||
return nil, pkgErrors.Wrapf(err, "failed to obtain size of file %#v", path) | ||
} | ||
|
||
sectorSizeBytes := int(stat.Blksize) | ||
sectorCount := int64((uint64(minimumSizeBytes) + uint64(stat.Blksize) - 1) / uint64(stat.Blksize)) | ||
sizeBytes := int64(sectorSizeBytes) * sectorCount | ||
|
||
if err := unix.Ftruncate(fd, sizeBytes); err != nil { | ||
return nil, pkgErrors.Wrapf(err, "failed to truncate file %#v to %d bytes", path, sizeBytes) | ||
} | ||
|
||
bd, err := NewBlockDevice(fd, int(sizeBytes)) | ||
|
||
if err != nil { | ||
return nil, err | ||
} else if err := unix.Close(fd); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &File{ | ||
BlockDevice: bd, | ||
SectorSizeBytes: sectorSizeBytes, | ||
SectorCount: sectorCount, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package testA | ||
package test | ||
|
||
import ( | ||
"bufio" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package test | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"runtime/debug" | ||
"testing" | ||
|
||
"github.com/consensys/go-corset/pkg/mmap" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Ignored_TestNewBlockDeviceFromFile(t *testing.T) { | ||
minSizeBytes := 123456 | ||
blockDevicePath := filepath.Join(t.TempDir(), "test_blockdevice") | ||
|
||
println(blockDevicePath) | ||
|
||
mmapFile, err := mmap.NewFile(blockDevicePath, minSizeBytes) | ||
require.NoError(t, err) | ||
|
||
sectorSizeBytes := mmapFile.SectorSizeBytes | ||
sectorCount := mmapFile.SectorCount | ||
blockDevice := mmapFile.BlockDevice | ||
// The sector size should be a power of two, and the number of | ||
// sectors should be sufficient to hold the required space. | ||
require.LessOrEqual(t, 512, sectorSizeBytes) | ||
require.Equal(t, 0, sectorSizeBytes&(sectorSizeBytes-1)) | ||
require.Equal(t, int64((minSizeBytes+sectorSizeBytes-1)/sectorSizeBytes), sectorCount) | ||
|
||
// The file on disk should have a size that corresponds to the | ||
// sector size and count. | ||
fileInfo, err := os.Stat(blockDevicePath) | ||
require.NoError(t, err) | ||
require.Equal(t, int64(sectorSizeBytes)*sectorCount, fileInfo.Size()) | ||
|
||
// Test read, write and sync operations. | ||
n, err := blockDevice.WriteAt([]byte("Hello"), 12345) | ||
require.Equal(t, 5, n) | ||
require.NoError(t, err) | ||
|
||
var b [16]byte | ||
n, err = blockDevice.ReadAt(b[:], 12340) | ||
require.Equal(t, 16, n) | ||
require.NoError(t, err) | ||
require.Equal(t, []byte("\x00\x00\x00\x00\x00Hello\x00\x00\x00\x00\x00\x00"), b[:]) | ||
|
||
require.NoError(t, mmapFile.BlockDevice.Sync()) | ||
|
||
// Truncating the file will cause future read access to the | ||
// memory map underneath the BlockDevice to raise SIGBUS. This | ||
// may also occur in case of actual I/O errors. These page | ||
// faults should be caught properly. | ||
// | ||
// To be able to implement this, ReadAt() temporary enables the | ||
// debug.SetPanicOnFault() option. Test that the original value | ||
// of this option is restored upon completion. | ||
require.NoError(t, os.Truncate(blockDevicePath, 0)) | ||
|
||
debug.SetPanicOnFault(false) | ||
|
||
n, err = blockDevice.ReadAt(b[:], 12340) | ||
require.NoError(t, err) | ||
|
||
require.False(t, debug.SetPanicOnFault(false)) | ||
require.Equal(t, 0, n) | ||
debug.SetPanicOnFault(true) | ||
|
||
n, err = blockDevice.ReadAt(b[:], 12340) | ||
|
||
require.True(t, debug.SetPanicOnFault(false)) | ||
require.Equal(t, 0, n) | ||
require.Error(t, err, "page fault occurred while reading from memory map") | ||
} |