探索 mmap

最近工作中在研究Hyperledger Fabric区块链开源项目,其中区块链peer节点底层使用了leveldb作为State Database的存储介质,出于好奇,决定对一些常用的KV存储做一些研究。

这一次我主要对2种KV存储的源码做了分析,一个是BoltDB,这是LMDB的Go语言版本,另一个就是goleveldb。

在阅读BoltDB项目源码的过程中,我发现它将持久化文件以只读模式通过mmap映射到内存空间中,然后通过索引找到内存中key对应的value所指向的空间,然后将这段内存返回给用户。之前虽然也听说过内存映射文件技术,但一直没有实际使用过,因此这一次我决定对mmap做一些尝试,以下是这次尝试的过程。

文件写入

内存映射文件(Memory-mapped file)将一段虚拟内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主存,用于增加I/O性能。Mmap函数存在于Go's syscall package中,它接收一个文件描述符,需要映射的大小(返回的切片容量)以及需要的内存保护和映射类型。

package main

import (
    "fmt"
    "os"
    "syscall"
)

const maxMapSize = 0x8000000000
const maxMmapStep = 1 << 30 // 1GB

func main() {
    file, err := os.OpenFile("my.db", os.O_RDWR|os.O_CREATE, 0644)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    stat, err := os.Stat("my.db")
    if err != nil {
        panic(err)
    }

    size, err := mmapSize(int(stat.Size()))
    if err != nil {
        panic(err)
    }

    b, err := syscall.Mmap(int(file.Fd()), 0, size, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
    if err != nil {
        panic(err)
    }

    for index, bb := range []byte("Hello world") {
        b[index] = bb
    }

    err = syscall.Munmap(b)
    if err != nil {
        panic(err)
    }
}

func mmapSize(size int) (int, error) {
    // Double the size from 32KB until 1GB.
    for i := uint(15); i <= 30; i++ {
        if size <= 1< maxMapSize {
        return 0, fmt.Errorf("mmap too large")
    }

    // If larger than 1GB then grow by 1GB at a time.
    sz := int64(size)
    if remainder := sz % int64(maxMmapStep); remainder > 0 {
        sz += int64(maxMmapStep) - remainder
    }

    // Ensure that the mmap size is a multiple of the page size.
    // This should always be true since we're incrementing in MBs.
    pageSize := int64(os.Getpagesize())
    if (sz % pageSize) != 0 {
        sz = ((sz / pageSize) + 1) * pageSize
    }

    // If we've exceeded the max size then only grow up to the max size.
    if sz > maxMapSize {
        sz = maxMapSize
    }

    return int(sz), nil
}

如果你直接运行这个程序,那么将会发生错误(内存地址会不一样)

unexpected fault address 0x13ac000
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x13ac000 pc=0x10c1375]

SIGBUS信号意味着你在文件的地址以外写入内容,根据Linux man pages mmap(2)的描述:

SIGBUS Attempted access to a portion of the buffer that does not correspond to the file (for example, beyond the end of the file, including the case where another process has truncated the file).

在创建新文件时,它最初为空,即大小为0字节,您需要使用ftruncate调整其大小,至少足以包含写入的地址(可能四舍五入到页面大小)。修改main函数:

func main() {
    file, err := os.OpenFile("my.db", os.O_RDWR|os.O_CREATE, 0644)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    stat, err := os.Stat("my.db")
    if err != nil {
        panic(err)
    }

    size, err := mmapSize(int(stat.Size()))
    if err != nil {
        panic(err)
    }

    err = syscall.Ftruncate(int(file.Fd()), int64(size))
    if err != nil {
        panic(err)
    }

    b, err := syscall.Mmap(int(file.Fd()), 0, size, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
    if err != nil {
        panic(err)
    }

    for index, bb := range []byte("Hello world") {
        b[index] = bb
    }

    err = syscall.Munmap(b)
    if err != nil {
        panic(err)
    }
}

再次运行程序,可正常写入。

读取文件

读取文件的方式更加简单,直接以只读方式将文件映射到主存中即可:

func main() {
    file, err := os.OpenFile("my.db", os.O_RDONLY, 0600)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    stat, err := os.Stat("my.db")
    if err != nil {
        panic(err)
    }

    b, err := syscall.Mmap(int(file.Fd()), 0, int(stat.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
    if err != nil {
        panic(err)
    }
    defer syscall.Munmap(b)

    fmt.Println(string(b))
}

运行程序,即可打印出文件内容

你可能感兴趣的:(探索 mmap)