探索 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
}

如果你直接運行這個程序,那麼將會發生錯誤 (內存地址會不一樣)

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

運行程序,即可打印出文件內容

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://www.jianshu.com/p/964b887da04c