跳至正文

探索 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<<i {
            return 1 << i, nil
        }
    }

    // Verify the requested size is not above the maximum allowed.
    if size > 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
}
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<<i {
            return 1 << i, nil
        }
    }

    // Verify the requested size is not above the maximum allowed.
    if size > 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).

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))
}

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注