使用Go发现和探索mmap

2021-01-11 11:22:51

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

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

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

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

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

在某些情况下,程序员发现此方法比通常的系统调用更方便:打开,读取,写入,查找和关闭。

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

包mainimport(" os"" fmt"" github.com/edsrzf/mmap-go")func main(){f,_:= os.OpenFile( " ./ file&#34 ;, os.O_RDWR,0644)延迟f.Close()mmap,_:= mmap.Map(f,mmap.RDWR,0)延迟mmap.Unmap()fmt.Println( string(mmap))mmap [0] =' X' mmap.Flush()}

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

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

让我们先看一下prot。使用prot参数可以指定映射的保护级别:RDONLY,RDWR,EXEC是为mmap-go提供的选项。这些级别非常简单,RDONLY表示您只能从映射中读取,RDWR表示您也可以编写,EXEC表示您可以在该映射上执行代码。这是Linux专家对prot的描述:

prot参数描述了所需的映射内存保护(并且不得与文件的打开模式冲突)。它可以是PROT_NONE或以下一个或多个标志的按位或:可以执行PROT_EXEC页面。可以读取PROT_READ页面。可以写入PROT_WRITE页面。不能访问PROT_NONE页面。

我对EXEC标志很感兴趣,并想看一个例子。我曾经用过Google,找不到任何示例。因此,我尝试通过PROT_EXEC在Github中进行搜索,并在C:MMapExecDemo中找到了一个很好的示例。我使用mmap-go在Go中复制了此示例。

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

使用go工具compile -S -N inc.go对其进行编译,然后通过调用go工具objdump -S inc.o对其进行汇编。

func inc(n int)int {0x22b 48c744241000000000 MOVQ $ 0x0,0x10(SP)返回n + 1 0x234 488b442408 MOVQ 0x8(SP),AX 0x239 48ffc0 INCQ AX 0x23c 4889442410 MOVQ AX,0x10(SP)0x241 c3 RET

代码:= [] byte {0x48、0xc7、0x44、0x24、0x10、0x00、0x00、0x00、0x00、0x48、0x8b,0x44、0x24、0x08、0x48、0xff,0xc0、0x48、0x89、0x44、0x24、0x24、0x10 ,0xc3,}

在此调用中,我们使用的是称为MapRegion的更完整的函数,该函数可让您指定要分配的内存量(Map分配基础文件的大小)和文件的偏移量。

首先,我们说过mmap的主要目的是在文件和内存之间创建映射。但是在此调用中,我们不指示任何文件。通过将nil设置为* os.File参数并将mmap.ANON设置为flags参数,可以将mmap用作常规内存分配器。我们将讨论更多的mmap.ANON。由于我们没有映射任何文件,因此偏移量为0。

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

我们在内存中有inc函数的代码。为了执行它,我们必须将该内存地址强制转换为签名与我们已编译的inc签名匹配的函数。

当我们调用inc时,我们正在执行存储在内存中的代码。由于标志mmap.EXEC,这只能工作。如果未设置该标志,则会发生分段冲突。

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

可能出现的一个问题是:但是代码已经在代码变量中了,我们不能执行它吗?否,因为分配给代码的静态内存是不可执行的。我们可以使其可执行吗?我曾尝试在其上使用mprotect,但仍然遇到分段违规问题。

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

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

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

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

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

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

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

即使mmap可以用来解决我们一开始就提到的数据库问题,并且许多现代数据库都在使用它,但Andy Pavlo还是反对使用它,并且在关于如何使用数据库而不使用mmap来管理数据的三个演讲中。

如果您喜欢这种内容,请在Twitter上关注我。您可能在那里找到更多相关的东西。