最近,在观看有关数据库存储的Andy Pavlo数据库系统入门课程的讲座时,我已经了解了内存映射文件的概念。数据库存储引擎必须解决的主要问题之一是如何处理大于可用内存的磁盘中的数据。在更高级别上,面向磁盘的存储引擎的主要目的是操纵磁盘中的数据文件。但是,如果我们假设磁盘中的数据最终将变得大于可用内存,则我们不能简单地将整个数据文件加载到内存中,进行更改并将其写回到磁盘中。

这不是计算机科学中的新问题。在1960年代初期开发操作系统时,面临着一个类似的问题:我们如何运行存储在磁盘中的大于可用内存的程序?1961年,曼彻斯特的一个小组提出了一个解决方案,该方案在Atlas计算机上实现。它被称为虚拟内存。将虚拟内存给正在运行的程序的错觉,它有足够大的内存,尽管计算机没有足够的。

我们不会深入探讨虚拟内存的工作方式。请记住,程序正在访问内存时,它正在访问虚拟内存。也许程序正在尝试访问的数据实际上不在内存中,但这无关紧要。操作系统会假装将其放入磁盘,然后将其放置在其中,然后替换将不使用的旧内存块。

因此,数据库存储引擎解决大于内存的问题的一种方法是利用虚拟内存和内存映射文件的概念。

在Linux中,我们可以通过使用系统调用mmap来实现此目的,该函数使您可以将文件(无论大小)直接映射到内存中。如果您的程序需要操纵文件,那么它所需要的就是操纵内存。操作系统为您处理磁盘写操作。

在某些情况下,程序员发现此方法比通常的系统调用更方便:open,read,write,lseek和close。

一个简单的示范
这是一个小示例,说明如何使用mmap-go软件包在Go中利用此功能:

package main

import (
"os"
"fmt"
"github.com/edsrzf/mmap-go"
)

func main() {
f, _ := os.OpenFile("./file", os.O_RDWR, 0644)
defer f.Close()

mmap, _ := mmap.Map(f, mmap.RDWR, 0 )
defer mmap.Unmap()
fmt.Println(string(mmap))

mmap[0] = 'X'
mmap.Flush()

}

令人高兴的是,我们可以拥有更大的文件,并且该解决方案仍然有效。我们不必担心管理内存以避免内存被填满。

详解mmap功能
我们将从mmap-go提供的API的角度探索更多mmap功能。本机syscall可能提供了该库未实现的更多功能。

该prot参数
这是mmap.Map签名

func Map(f *os.File, prot, flags int) (MMap, error)
让我们prot先来看。该prot参数可以指定你映射的保护级别:RDONLY,RDWR,EXEC是规定的选项mmap-go。这些级别非常简单,RDONLY意味着您只能从映射中读取,RDWR意味着还可以编写以及EXEC可以在该映射上执行代码。这是prot来自Linux的描述man:

The prot argument describes the desired memory protection of the
mapping (and must not conflict with the open mode of the file).
It is either PROT_NONE or the bitwise OR of one or more of the
following flags:

PROT_EXEC
Pages may be executed.

PROT_READ
Pages may be read.

PROT_WRITE
Pages may be written.

PROT_NONE
Pages may not be accessed.
在UNIX包,这些标志是:unix.PROT_EXEC,unix.PROT_READ,unix.PROT_WRITE和unix.PROT_NONE。

试验PROT_EXEC标志
我对EXEC标志很感兴趣,并想看一个有关其工作原理的例子。我有Google,找不到任何示例。于是,我就在Github上搜索PROT_EXEC,发现一个很好的例子C:MMapExecDemo。我在复制这个例子中Go使用mmap-go。

第一步是创建一个我想通过mmap分配放入内存的函数,对其进行编译并获取其汇编操作码。

我inc在inc.go文件中创建了函数

package inc

func inc(n int) int {
return n + 1
}

用编译它go tool compile -S -N inc.go,然后通过调用获得它的程序集go tool objdump -S inc.o。

func inc(n int) int {
0x22b 48c744241000000000 MOVQ $0x0, 0x10(SP)
return n + 1
0x234 488b442408 MOVQ 0x8(SP), AX
0x239 48ffc0 INCQ AX
0x23c 4889442410 MOVQ AX, 0x10(SP)
0x241 c3 RET
这样,我们可以在代码中以字节为单位构建表示我们的函数的功能

code := []byte{
0x48, 0xc7, 0x44, 0x24, 0x10, 0x00, 0x00, 0x00, 0x00,
0x48, 0x8b, 0x44, 0x24, 0x08,
0x48, 0xff, 0xc0,
0x48, 0x89, 0x44, 0x24, 0x10,
0xc3,
}
我们用分配内存mmap。

memory, err := mmap.MapRegion(nil, len(code), mmap.EXEC|mmap.RDWR, mmap.ANON, 0)
if err != nil {
panic(err)
}
在此调用中,我们使用了一个更完整的函数MapRegion,该函数可让您指定要分配的内存量(Map分配基础文件的大小)和文件的偏移量。

在一开始,我们说过的主要目的mmap是在文件和内存之间创建映射。但是在此调用中,我们不指示任何文件。mmap可以通过设置使用只是一个普通的内存allocaternil的*os.File参数,并mmap.ANON给flags说法。我们将讨论更多mmap.ANON。由于我们没有映射任何文件,因此偏移量为0。

因此,我们为内存分配了与代码相同的大小len(code)。既然设置了标志mmap.RDWR,我们就可以将其复制code到memory。

copy(memory, code)
我们inc在内存中有函数的代码。为了执行它,我们必须将该内存地址转换为具有与我们的configure的签名匹配的签名的函数inc。

memory_ptr := &memory
ptr := unsafe.Pointer(&memory_ptr)
inc := (func(int) int)(ptr)
当我们调用时inc,我们正在执行存储在内存中的代码。那仅因为标志而起作用mmap.EXEC。如果未设置该标志,segmentation violation则将发生。

fmt.Println(inc(10)) // Prints 11
我不知道这是否是真正的用例。我只是想了解执行存储在内存中的代码的含义。并且可能还有其他方法可以通过常规的内存分配和对mprotect的调用来实现这一点。

可能出现的一个问题是:但是代码已经在code变量中了,我们不能仅仅执行它吗?否,因为分配给静态的内存code是不可执行的。我们可以使其可执行吗?我试图在上面使用mprotect,但还是得到了segmentation violation。

这是充分发挥作用的要点。

该flags参数
我们可以有许多映射相同内存区域的进程。该参数使我们可以决定映射中发生的更新的可见性。标志很多,您可以在mmap上检查它们。其中重要的是unix.MAP_SHARED,unix.MAP_PRIVATE和unix.MAP_ANON。

MAP_SHARED 意味着对映射的更改对于所有进程都是可见的,并且也将在基础映射文件中发生,尽管我们无法控制何时进行。

MAP_PRIVATE意味着更改是私有的,其他进程将看不到它们。而且,它们不会传递到基础文件。

MAP_ANON表示将不会有映射文件。对于与共享内存的子流程通信很有用。

我对mmap-go库的实现感到困惑。它仅提供mmap.ANON在上面的示例中使用的标志。如果希望映射是私有的,则可以将mmap.COPY标志设置为prot参数。无论如何,您始终可以使用unix包实现提供的标志。

锁定和冲洗
的API提供了另外两种不错的方法Lock和。该方法调用mlock系统调用,以防止将映射分页到磁盘。该方法调用msync系统调用,该系统调用强制将内存中的数据写入磁盘。这是尝试更好地控制将数据刷新到磁盘的方式和时间的好方法。Flushmmap-goLockFlush

包起来
mmap这么久以后,我感到有点愚蠢。我不记得是在我的大学课堂上带来的。由于某种原因,我对它及其功能感到惊讶,因此决定更深入地研究。我喜欢数据库,我的目标是更好地掌握它们。这意味着mmap我的学习不能被忽视。在以后的文章中,我将尝试介绍using的优缺点mmap,哪个项目使用它,以及它适合什么样的问题。

尽管mmap可以使用它来解决我们在开始时提到的数据库问题,并且许多现代数据库都在使用它,但是Andy Pavlo反对使用它,并在不使用mmap,管理数据的数据库方面进行了三场演讲。