TLPI 第49章 读书笔记:Memory Mapping

笔记和练习博客总目录见:开始读TLPI。

本章讨论了使用 mmap() 系统调用来创建内存映射。内存映射可以用于进程间通信(IPC),也可以用于其他各种目的。我们先概述一些基本概念,然后再深入讨论 mmap()。

49.1 Overview

mmap() 系统调用会在调用进程的虚拟地址空间中创建一个新的内存映射。映射可以有两种类型:

  • 文件映射:文件映射会将文件的某一部分直接映射到调用进程的虚拟内存中。一旦文件被映射,它的内容就可以通过对相应内存区域字节的操作来访问。映射的页面会根据需要(自动)从文件中加载。这种类型的映射也被称为基于文件的映射或内存映射文件。
  • 匿名映射:匿名映射没有对应的文件。相反,映射的页面会被初始化为 0。
    另一种理解匿名映射的方式(也比较接近实际情况)是,它是一个虚拟文件的映射,其内容总是初始化为零。

一个进程映射中的内存可能会与其他进程的映射共享(也就是说,每个进程的页表项指向相同的内存页)。这种情况有两种方式发生:

  • 当两个进程映射同一个文件区域时,它们共享相同的物理内存页。
  • 由 fork() 创建的子进程会继承父进程映射的副本,这些映射指向与父进程中对应映射相同的物理内存页。

当两个或更多进程共享相同的页面时,每个进程都有可能看到其他进程对页面内容所做的修改,这取决于映射是私有的还是共享的:

  • 私有映射(MAP_PRIVATE):对映射内容的修改对其他进程不可见,对于文件映射,也不会写入到底层文件。虽然私有映射的页面在上述情况下最初是共享的,但对映射内容的修改仍然是每个进程私有的。内核通过写时复制技术(第24.2.2节)实现这一点。这意味着每当进程尝试修改页面内容时,内核会首先为该进程创建该页面的新副本(并调整进程的页表)。因此,MAP_PRIVATE映射有时也被称为私有的写时复制映射。

  • 共享映射(MAP_SHARED):对映射内容的修改对共享同一映射的其他进程可见,且对于文件映射,会写入到底层文件。

上述两种映射属性(文件与匿名,以及私有与共享)可以组合出四种不同方式,如表49-1所示。

Table 49-1: Purposes of various types of memory mappings

Visibility of modifications File Mapping Anonymous Mapping
Private Initializing memory from contents of file Memory allocation
Shared Memory‑mapped I/O; sharing memory between processes (IPC) Sharing memory between processes (IPC)

创建和使用四种不同类型的内存映射方式如下:

  • 私有文件映射:映射的内容是从文件区域初始化的。多个进程映射同一个文件时,最初会共享相同的物理内存页,但会使用写时复制(copy-on-write)技术,因此一个进程对映射的更改对其他进程是不可见的。这种映射的主要用途是从文件内容初始化一块内存区域。一些常见的例子包括从二进制可执行文件或共享库文件的相应部分初始化进程的文本段和已初始化的数据段。
  • 私有匿名映射:每次调用 mmap() 创建一个私有匿名映射时,都会生成一个新的映射,这个映射与其他匿名映射是独立的(即不共享物理页),无论这些映射是由同一个还是不同的进程创建的。虽然子进程会继承父进程的映射,但写时复制(copy-on-write)机制保证了 fork() 之后,父进程和子进程不会看到对方对映射所做的更改。私有匿名映射的主要用途是为进程分配新的(初始化为零的)内存(例如,大块内存分配时,malloc() 就会为了这个目的调用 mmap())。
  • 共享文件映射:所有映射同一文件区域的进程共享相同的物理内存页,这些内存页是从文件区域初始化的。对映射内容的修改会同步到文件中。这种类型的映射有两个目的。首先,它允许内存映射 I/O。这意味着一个文件会被加载到进程虚拟内存的一个区域中,对该内存的修改会自动写入文件。因此,内存映射 I/O 提供了一种替代使用 read() 和 write() 来进行文件 I/O 的方法。第二个目的是允许不相关的进程共享一个内存区域,从而实现类似于 System V 共享内存段(第 48 章)的(快速)进程间通信。
  • 共享匿名映射:就像私有匿名映射一样,每次调用 mmap() 创建共享匿名映射时,都会产生一个新的、独立的映射,不会与任何其他映射共享页面。区别在于,这种映射的页面不会写时复制。这意味着当子进程在 fork() 后继承映射时,父进程和子进程共享相同的内存页,一个进程对映射内容的修改对另一个进程是可见的。共享匿名映射允许进程间通信(IPC),方式类似于 System V 共享内存段,但仅限于相关进程之间。

在本章的剩余部分,我们会更详细地讨论这些类型的映射。

当一个进程执行 exec() 时,映射会丢失,但在 fork() 的子进程中会继承。映射类型(MAP_PRIVATE 或 MAP_SHARED)也会被继承。

一个进程的所有映射信息可以在 Linux 特有的 /proc/PID/maps 文件中查看,我们在第 48.5 节中介绍过这个文件。

mmap() 的另一个用途是 POSIX 共享内存对象,它允许在不创建相关磁盘文件的情况下(共享文件映射需要创建磁盘文件)在无关进程之间共享一块内存区域。我们在第 54 章描述了 POSIX 共享内存对象。

49.2 Creating a Mapping: mmap()

mmap() 系统调用会在调用进程的虚拟地址空间中创建一个新的映射。

c 复制代码
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
				Returns starting address of mapping on success, or MAP_FAILED on error

addr 参数表示映射要被放置的虚拟地址。如果我们将 addr 指定为 NULL,内核会为映射选择一个合适的地址。这是创建映射的首选方式。或者,我们也可以在 addr 中指定一个非 NULL 的值,内核会把它当作映射应该放置的地址的一个提示。实际上,内核至少会将地址四舍五入到附近的页边界。无论哪种情况,内核都会选择一个不与任何现有映射冲突的地址。(如果 flags 中包含了 MAP_FIXED 值,那么 addr 必须是页对齐的。这个标志会在第 49.10 节中描述。)

成功时,mmap() 会返回新映射的起始地址。出错时,mmap() 会返回 MAP_FAILED。

在 Linux(以及大多数其他 UNIX 实现)中,MAP_FAILED 常量等价于 ((void *) --1)。不过,SUSv3 指定了这个常量,因为 C 标准不能保证 ((void *) --1) 与成功的 mmap() 返回值不同。

length 参数指定映射的大小,单位是字节。虽然 length 不需要是系统页大小(通过 sysconf(_SC_PAGESIZE) 获取)的整数倍,但内核是以这个单位创建映射的,因此 length 实际上会向上取整到下一个页大小的倍数。

prot 参数是一个位掩码,用来指定映射的保护措施。它可以是 PROT_NONE,也可以是表 49-2 中列出的其他三个标志的任意组合(按位或)。

Table 49-2: Memory protection values

Value Description
PROT_NONE The region may not be accessed
PROT_READ The contents of the region can be read
PROT_WRITE The contents of the region can be modified
PROT_EXEC The contents of the region can be executed

flags 参数是一个选项位掩码,用于控制映射操作的各个方面。这个掩码中必须包含以下值之一:

  • MAP_PRIVATE
    创建一个私有映射。对该区域内容的修改对使用相同映射的其他进程不可见,并且在文件映射的情况下,不会反映到底层文件中。
  • MAP_SHARED
    创建一个共享映射。对该区域内容的修改对使用 MAP_SHARED 属性映射同一区域的其他进程可见,并且在文件映射的情况下,会反映到底层文件中。文件的更新不保证是立即生效的;参见第 49.5 节中关于 msync() 系统调用的讨论。

除了 MAP_PRIVATE 和 MAP_SHARED 之外,其他标志值也可以可选地通过 OR 运算加入 flags。我们在第 49.6 和 49.10 节中讨论这些标志。

其余的参数 fd 和 offset 是用于文件映射的(对于匿名映射会被忽略)。fd 参数是一个文件描述符,用来标识要映射的文件。offset 参数指定映射在文件中的起始点,必须是系统页大小的整数倍。要映射整个文件,我们可以将 offset 指定为 0,length 指定为文件的大小。我们在第 49.5 节中会更多地介绍文件映射。

Memory protection in more detail

如上所述,mmap() 的 prot 参数指定了新内存映射的保护方式。它可以包含 PROT_NONE 的值,或者 PROT_READ、PROT_WRITE 和 PROT_EXEC 标志中的一个或多个的组合。如果一个进程试图以违反该区域保护的方式访问内存区域,内核就会向进程发送 SIGSEGV 信号。

虽然 SUSv3 规定应该使用 SIGSEGV 来表示内存保护违规,但在某些实现中,使用的是 SIGBUS。

将内存页标记为 PROT_NONE 的一个用途是在进程分配的内存区域的开始或结束处设置保护页。如果进程不小心进入了标记为 PROT_NONE 的页,内核就会通过生成 SIGSEGV 信号来告知它。

内存保护信息存储在进程私有的虚拟内存表中。因此,不同的进程可以对同一内存区域映射不同的保护方式。

内存保护可以使用 mprotect() 系统调用进行修改(见第 50.1 节)。

在一些 UNIX 实现中,实际施加在映射页面上的保护可能并不完全等于 prot 中指定的保护。特别是,底层硬件的保护粒度限制(例如,较老的 x86-32 架构)意味着在许多 UNIX 实现中,PROT_READ 会暗示 PROT_EXEC,反之亦然;并且在某些实现中,指定 PROT_WRITE 会暗示 PROT_READ。然而,应用程序不应该依赖这种行为;prot 应该始终精确指定所需的内存保护。

现代 x86-32 架构提供了将页面表标记为 NX(不可执行)的硬件支持,并且从 2.6.8 版内核开始,Linux 利用这一特性在 Linux/x86-32 上正确区分 PROT_READ 和 PROT_EXEC 权限。

Alignment restrictions specified in standards for offset and addr

SUSv3 规定 mmap() 的 offset 参数必须是页对齐的,如果指定了 MAP_FIXED,addr 参数也必须是页对齐的。Linux 符合这些要求。不过,后来有人注意到 SUSv3 的要求和早期标准有所不同,早期标准对这些参数的要求更宽松。SUSv3 的表述导致一些以前符合标准的实现变得不再符合标准。SUSv4 则回到了更宽松的要求:

  • 实现可以要求 offset 是系统页大小的倍数。
  • 如果指定了 MAP_FIXED,实施可以要求 addr 是页对齐的。
  • 如果指定了 MAP_FIXED,并且 addr 不为零,那么 addr 和 offset 对系统页大小取模后的余数应相同。

类似的情况也出现在 mprotect()、msync() 和 munmap() 的 addr 参数上。SUSv3 指定该参数必须是页对齐的,而 SUSv4 表示实现可以要求该参数是页对齐的。

Example program

清单 49-1 演示了如何使用 mmap() 创建一个私有文件映射。这个程序是 cat(1) 的简化版。它映射命令行参数中指定的(整个)文件,然后将映射的内容写到标准输出。

Listing 49-1: Using mmap() to create a private file mapping

c 复制代码
// mmap/mmcat.c
// 代码略。

49.3 Unmapping a Mapped Region: munmap()

munmap() 系统调用执行与 mmap() 相反的操作,从调用进程的虚拟地址空间中移除一个映射。

c 复制代码
#include <sys/mman.h>

int munmap(void *addr, size_t length);
				Returns 0 on success, or --1 on error

addr 参数是要取消映射的地址范围的起始地址。它必须对齐到页边界。(SUSv3 指出 addr 必须是页对齐的。SUSv4 则表示实现可能要求这个参数是页对齐的。)

length 参数是一个非负整数,指定要取消映射区域的大小(以字节为单位)。将会取消映射到下一个系统页大小的倍数的地址范围。

通常,我们会取消映射整个映射区域。因此,我们将 addr 指定为之前调用 mmap() 返回的地址,并且指定与 mmap() 调用时相同的 length 值。举个例子:

c 复制代码
addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
	errExit("mmap");
	
/* Code for working with mapped region */

if (munmap(addr, length) == -1)
	errExit("munmap");

或者,我们可以取消映射的一部分,在这种情况下,映射要么缩小,要么被切成两部分,这取决于取消映射的位置。也可以指定一个跨越多个映射的地址范围,在这种情况下,所有的映射都会被取消。

如果在 addr 和 length 指定的地址范围内没有映射,那么 munmap() 不会有任何效果,并返回 0(表示成功)。

在取消映射期间,内核会移除进程在指定地址范围内持有的任何内存锁。(内存锁是通过 mlock() 或 mlockall() 建立的,如第 50.2 节所述。)

当进程终止或执行 exec() 时,所有进程的映射都会自动被取消。

为了确保共享文件映射的内容写入到底层文件,在用 munmap() 取消映射之前应调用 msync()(第 49.5 节)。

49.4 File Mappings

要创建文件映射,我们执行以下步骤:

  1. 获取文件的描述符,通常通过调用 open() 实现。
  2. 在调用 mmap() 时,将该文件描述符作为 fd 参数传入。

通过这些步骤,mmap() 将打开文件的内容映射到调用进程的地址空间中。一旦调用了 mmap(),我们可以关闭文件描述符,而不会影响映射。不过,在某些情况下,保留这个文件描述符可能会有用------例如,参见示例 49-1 或第 54 章。

除了普通的磁盘文件,也可以使用 mmap() 来映射各种真实和虚拟设备的内容,如硬盘、光盘以及 /dev/mem。

由描述符 fd 引用的文件必须以与 prot 和 flags 中指定的值相匹配的权限打开。特别是,文件必须始终以可读方式打开,如果在 flags 中指定了 PROT_WRITE 和 MAP_SHARED,则文件必须以可读写方式打开。

offset 参数指定要从文件映射的区域的起始字节,并且必须是系统页面大小的倍数。将 offset 指定为 0 会导致文件从开头开始映射。length 参数指定要映射的字节数。offset 和 length 参数一起确定要映射到内存的文件区域,如图 49-1 所示。

在 Linux 上,文件映射的页面是在第一次访问时映射的。这意味着,如果在 mmap() 调用之后但在映射的相应部分(即页面)被访问之前对文件区域进行了修改,那么如果该页面尚未被加载到内存中,这些更改可能对进程可见。这种行为依赖于实现;可移植的应用程序应该避免依赖内核在这种情况下的特定行为。

49.4.1 Private File Mappings

私有文件映射最常见的两种用途如下:

  • 允许多个执行相同程序或使用相同共享库的进程共享相同的(只读)文本段,该文本段从底层可执行文件或库文件的相应部分进行映射。

    虽然可执行文本段通常被保护为只允许读取和执行访问(PROT_READ | PROT_EXEC),但它是使用 MAP_PRIVATE 而非 MAP_SHARED 映射的,因为调试器或自修改程序可以修改程序文本(先改变内存保护),而这些修改不应影响到底层文件或其他进程。

  • 映射可执行文件或共享库的已初始化数据段。这类映射是私有的,因此对映射数据段内容的修改不会传递到底层文件。

这两种 mmap() 的用法通常对程序是看不见的,因为这些映射是由程序加载器和动态链接器创建的。在第 48.5 节显示的 /proc/PID/maps 输出中可以看到这两种映射的例子。

另一种不太常见的私有文件映射用法是简化程序的文件输入逻辑。这有点类似于共享文件映射用于内存映射 I/O(在下一节会讲到),但是只允许文件输入。

Figure 49-1: Overview of memory-mapped file

49.4.2 Shared File Mappings

当多个进程创建同一文件区域的共享映射时,它们都会共享相同的物理内存页。此外,对映射内容的修改也会反映到文件中。实际上,文件被当作这个内存区域的分页存储,就像图49-2所示。(在这张图中,我们简化处理,没有显示映射的页面在物理内存中通常并不连续。)

共享文件映射有两个用途:内存映射I/O和进程间通信(IPC)。下面我们会分别讲到这两种用途。

Figure 49-2: Two processes with a shared mapping of the same region of a file

Memory-mapped I/O

由于共享文件映射的内容是从文件初始化的,并且对映射内容的任何修改都会自动反映到文件中,我们可以通过访问内存中的字节来进行文件 I/O,而依赖内核确保内存中的更改被传递到映射文件中。(通常,一个程序会定义一个与磁盘文件内容对应的结构化数据类型,然后使用该数据类型去转换映射的内容。)这种技术被称为内存映射 I/O,它是使用 read() 和 write() 访问文件内容的替代方法。

内存映射 I/O 有两个潜在优点:

  • 通过用内存访问替换 read() 和 write() 系统调用,可以简化某些应用程序的逻辑。
  • 在某些情况下,它可以比使用传统 I/O 系统调用进行的文件 I/O 提供更好的性能。

内存映射 I/O 可以提供性能优势的原因如下:

  • 普通的 read() 或 write() 涉及两次传输:一次是在文件和内核缓冲区缓存之间,另一次是在缓冲区缓存和用户空间缓冲区之间。使用 mmap() 可以消除第二次传输。对于输入,一旦内核将对应的文件块映射到内存中,数据就可以立即供用户进程使用。对于输出,用户进程只需要修改内存中的内容,然后可以依靠内核内存管理器自动更新底层文件。
  • 除了节省内核空间和用户空间之间的一次传输外,mmap() 还可以通过降低内存需求来提高性能。使用 read() 或 write() 时,数据会在两个缓冲区中保存:一个在用户空间,另一个在内核空间。使用 mmap() 时,内核空间和用户空间共享一个缓冲区。此外,如果多个进程对同一个文件进行 I/O,那么使用 mmap() 时,它们都可以共享同一个内核缓冲区,从而进一步节省内存。

使用内存映射 I/O 的性能优势最可能在对大文件进行重复随机访问时实现。如果我们执行的是文件的顺序访问,那么 mmap() 可能几乎不会比 read() 和 write() 提供更多的好处,前提是我们使用足够大的缓冲区进行 I/O,以避免产生大量的 I/O 系统调用。之所以性能提升不大,是因为无论使用哪种技术,整个文件内容都会在磁盘和内存之间传输一次,而且消除用户空间和内核空间之间的数据传输和减少内存使用所带来的效率提升,通常与磁盘 I/O 所需的时间相比可以忽略不计。

内存映射 I/O 也有一些缺点。对于小规模的 I/O 来说,内存映射 I/O 的开销(即映射、页面错误、解除映射以及更新硬件内存管理单元的转换旁路缓冲区)实际上可能比简单的 read() 或 write() 更高。此外,在可写映射的写回处理中,内核有时可能难以高效处理(此时使用 msync() 或 sync_file_range() 可以帮助提高效率)。

IPC using a shared file mapping

由于所有使用相同文件区域共享映射的进程共享相同的物理内存页,所以共享文件映射的第二个用途是作为一种(快速)进程间通信方式。区别于 System V 共享内存对象(第 48 章)的特性在于,对该共享内存区域内容的修改会反映到底层映射的文件中。这个特性在需要共享内存内容在应用或系统重启后仍能保留的应用中非常有用。

Example program

清单 49-2 提供了一个使用 mmap() 创建共享文件映射的简单示例。这个程序首先映射它第一个命令行参数指定的文件。然后,它打印映射区域起始位置的字符串值。最后,如果提供了第二个命令行参数,该字符串会被复制到共享内存区域中。

下面的 shell 会话日志演示了这个程序的使用。我们首先创建了一个大小为 1024 字节的文件,并用零填充它:

bash 复制代码
$ dd if=/dev/zero of=s.txt bs=1 count=1024
1024+0 records in
1024+0 records out
1024 bytes (1.0 kB, 1.0 KiB) copied, 0.00289296 s, 354 kB/s

然后我们使用我们的程序映射文件,并将一个字符串复制到映射区域:

bash 复制代码
$ ./t_mmap s.txt hello
Current string=
Copied "hello" to shared memory

程序没有显示当前字符串的任何内容,因为映射文件的初始值以空字节(即零长度字符串)开始。

接下来,我们再次使用程序映射文件,并将一个新字符串复制到映射区域:

bash 复制代码
$ ./t_mmap s.txt goodbye
Current string=hello
Copied "goodbye" to shared memory

最后,我们每行输出 8 个字符的文件内容,以便验证其内容:

bash 复制代码
$ od -c -w8 s.txt
0000000   g   o   o   d   b   y   e  \0
0000010  \0  \0  \0  \0  \0  \0  \0  \0
*
0002000

我们这个简单的小程序没有使用任何机制来同步多个进程对映射文件的访问。不过,实际应用通常需要同步对共享映射的访问。这可以通过多种方式实现,包括信号量(第47章和53章)和文件锁(第55章)。

我们在第49.5节中讲解了在清单49-2中使用的 msync() 系统调用。

Listing 49-2: Using mmap() to create a shared file mapping

c 复制代码
// mmap/t_mmap.c
// 代码略。

💡 ipcs只能显式System V的IPC对象,POSIX没有完全对应的目录,lsipc可以显式部分信息。

49.4.3 Boundary Cases

在很多情况下,映射的大小是系统页大小的倍数,并且映射完全位于被映射文件的范围内。不过,这并不一定总是如此,现在我们来看看当这些条件不满足时会发生什么。

图49-3展示了这样一种情况:映射完全位于被映射文件的范围内,但该区域的大小不是系统页大小的倍数(在本讨论中我们假设系统页大小为4096字节)。

Figure 49-3: Memory mapping whose length is not a multiple of the system page size

由于映射的大小不是系统页面大小的倍数,所以它会被向上舍入到下一个系统页面大小的倍数。因为文件比这个向上舍入后的大小还大,所以文件的相应字节就像图49-3中显示的那样被映射了。

尝试访问映射末尾之外的字节会导致生成SIGSEGV信号(假设该位置没有其他映射)。这个信号的默认操作是终止进程并生成核心转储。

当映射超出底层文件的末尾时(见图49-4),情况就更复杂了。和以前一样,因为映射的大小不是系统页大小的倍数,所以会向上取整。不过在这种情况下,虽然取整出来的区域的字节(图中2200到4095字节)可以访问,但它们并没有映射到底层文件上(因为文件里没有对应的字节)。相反,它们会被初始化为0(SUSv3要求如此)。如果其他进程映射同一个文件并且指定了足够大的长度参数,这些字节仍然会被共享。对这些字节的修改不会写回文件。

如果映射包含了超出四舍五入区域的页面(例如图49-4中第4096字节及之后的部分),那么尝试访问这些页面的地址会触发SIGBUS信号,这会提醒进程这些地址没有对应的文件区域。和之前一样,尝试访问映射末尾之外的地址会触发SIGSEGV信号。

从上面的描述来看,创建一个大小超过底层文件的映射似乎没有意义。不过,通过扩展文件的大小(例如,使用 ftruncate() 或 write()),我们可以使以前无法访问的映射部分变得可用。

Figure 49-4: Memory mapping extending beyond end of mapped file

49.4.4 Memory Protection and File Access Mode Interactions

到目前为止,我们还没有详细解释的一点是 mmap() 的 prot 参数指定的内存保护与映射文件打开模式之间的关系。一般来说,我们可以说 PROT_READ 和 PROT_EXEC 保护要求映射的文件以 O_RDONLY 或 O_RDWR 模式打开,而 PROT_WRITE 保护要求映射的文件以 O_WRONLY 或 O_RDWR 模式打开。

然而,这种情况因为一些硬件架构提供的内存保护粒度有限而变得复杂(见第49.2节)。对于这些架构,我们有以下观察:

  • 所有内存保护的组合都可以兼容使用O_RDWR标志打开文件。

  • 没有任何内存保护组合------甚至单单是PROT_WRITE------可以兼容以O_WRONLY打开的文件(会导致EACCES错误)。这与某些硬件架构不允许我们对页进行仅写访问的事实一致。正如第49.2节所述,在这些架构上,PROT_WRITE意味着PROT_READ,这也就是说,如果该页可以写入,那么也可以读取。读取操作与O_WRONLY不兼容,因为O_WRONLY不能显示文件的原始内容。

  • 当使用 O_RDONLY 标志打开文件时,结果取决于我们在调用 mmap() 时指定的是 MAP_PRIVATE 还是 MAP_SHARED。对于 MAP_PRIVATE 映射,我们可以在 mmap() 中指定任何组合的内存保护------因为对 MAP_PRIVATE 页面内容的修改从不会写回文件,所以无法写入文件并不成问题。而对于 MAP_SHARED 映射,唯一与 O_RDONLY 兼容的内存保护是 PROT_READ 和 (PROT_READ | PROT_EXEC)。这很合理,因为一个 PROT_WRITE、MAP_SHARED 映射允许对映射的文件进行更新。

49.5 Synchronizing a Mapped Region: msync()

内核会自动将 MAP_SHARED 映射内容的修改同步到底层文件,但默认情况下,并不保证这种同步何时发生。(SUSv3 并不要求实现必须提供这种保证。)

msync() 系统调用允许应用程序明确控制共享映射何时与映射的文件同步。将映射与底层文件同步在各种场景下都很有用。例如,为了确保数据完整性,数据库应用可能会调用 msync() 强制将数据写入磁盘。调用 msync() 还让应用程序可以确保对可写映射的更新对执行 read() 的其他进程可见。

c 复制代码
#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);
				Returns 0 on success, or --1 on error

msync() 的 addr 和 length 参数指定要同步的内存区域的起始地址和大小。addr 指定的地址必须按页对齐,len 会向上取整到系统页大小的下一个倍数。(SUSv3 规定 addr 必须按页对齐。SUSv4 说明实现可能要求这个参数按页对齐。)

flags 参数的可能取值包括以下之一:

  • MS_SYNC
    执行同步文件写入。调用会阻塞,直到内存区域中所有被修改的页面都写入磁盘。
  • MS_ASYNC
    执行异步文件写入。内存区域中被修改的页面会在稍后的时间写入磁盘,并立即对其他对相应文件区域执行 read() 的进程可见。

区分这两个值的另一种方法是说,在执行 MS_SYNC 操作后,内存区域会与磁盘同步;而在执行 MS_ASYNC 操作后,内存区域只是与内核缓冲区缓存同步。

如果在 MS_ASYNC 操作后不采取进一步措施,那么内存区域中被修改的页面最终会作为 pdflush 内核线程(Linux 2.4 及之前版本中的 kupdated)执行的自动缓冲刷新的一部分被写出。在 Linux 上,有两种(非标准的)方法可以更快地启动输出。我们可以在调用 msync() 后,调用与映射对应的文件描述符的 fsync()(或 fdatasync())。这个调用会阻塞,直到缓冲区缓存与磁盘同步。或者,我们也可以使用 posix_fadvise() 的 POSIX_FADV_DONTNEED 操作来异步写出页面。(这两种情况下的 Linux 特定细节在 SUSv3 中没有规定。)

还可以为标志指定另一个值:

  • MS_INVALIDATE
    使映射数据的缓存副本失效。在内存区域中任何被修改的页面与文件同步后,内存区域中与底层文件数据不一致的所有页面都会被标记为无效。下一次访问这些页面时,其内容将从文件的相应位置复制。因此,其他进程对文件所做的任何更新都会在内存区域中可见。

像许多现代 UNIX 实现一样,Linux 提供了所谓的统一虚拟内存系统。这意味着,在可能的情况下,内存映射和缓冲区缓存块共享相同的物理内存页面。因此,通过映射和通过 I/O 系统调用(read()、write() 等)获得的文件视图总是一致的,msync() 唯一的用途是强制将映射区域的内容刷新到磁盘。

然而,SUSv3 并不要求必须有统一的虚拟内存系统,并且并非所有 UNIX 实现都采用这种系统。在这样的系统上,如果希望对映射区域的内容所做的修改对其他读取该文件的进程可见,就需要调用 msync(),而如果想让其他进程对文件的写操作在映射区域中可见,则需要使用 MS_INVALIDATE 标志。使用 mmap() 和 I/O 系统调用同时操作同一个文件的多进程应用程序,如果希望在没有统一虚拟内存系统的系统上也能正常运行,就应该合理使用 msync()。

49.6 Additional mmap() Flags

除了 MAP_PRIVATE 和 MAP_SHARED 之外,Linux 还允许在 mmap() 的 flags 参数中包含(按位或)其他一些值。表 49-3 对这些值进行了总结。除了 MAP_PRIVATE 和 MAP_SHARED 之外,SUSv3 只指定了 MAP_FIXED 标志。

Table 49-3: Bit-mask values for the mmap() flags argument

Value Description SUSv3
MAP_ANONYMOUS Create an anonymous mapping
MAP_FIXED Interpret addr argument exactly (Section 49.10) •
MAP_LOCKED Lock mapped pages into memory (since Linux 2.6)
MAP_HUGETLB Create a mapping that uses huge pages (since Linux 2.6.32)
MAP_NORESERVE Control reservation of swap space (Section 49.9)
MAP_PRIVATE Modifications to mapped data are private •
MAP_POPULATE Populate the pages of a mapping (since Linux 2.6)
MAP_SHARED Modifications to mapped data are visible to other processes and propagated to underlying file (converse of MAP_PRIVATE) •
MAP_UNINITIALIZED Don't clear an anonymous mapping (since Linux 2.6.33)

以下列表提供了表 49-3 中列出的标志值的更多细节(MAP_PRIVATE 和 MAP_SHARED 已经讨论过了):

MAP_ANONYMOUS

创建一个匿名映射------也就是说,这个映射不是由文件支持的。我们将在第 49.7 节进一步介绍这个标志。

MAP_FIXED

我们将在第 49.10 节介绍这个标志。

MAP_HUGETLB(自 Linux 2.6.32 起)

这个标志对 mmap() 的作用类似于 SHM_HUGETLB 标志对 System V 共享内存段的作用。参见第 48.2 节。

MAP_LOCKED(自 Linux 2.6 起)

将映射的页面预加载并锁定到内存中,就像 mlock() 一样。我们将在第 50.2 节介绍使用这个标志所需的权限以及其操作的限制。

MAP_NORESERVE

这个标志用于控制是否提前为映射保留交换空间。详情请参见第 49.9 节。

MAP_POPULATE(自 Linux 2.6 起)

填充映射的页面。对于文件映射,这会执行文件的预读。这意味着稍后访问映射的内容时不会因页面缺失而被阻塞(前提是内存压力没有在此期间将页面交换出去)。

MAP_UNINITIALIZED(自 Linux 2.6.33 起)

指定这个标志可以防止匿名映射的页面被清零。它可以提升性能,但也存在安全风险,因为分配的页面可能包含之前进程留下的敏感信息。因此,这个标志主要用于嵌入式系统,在这些系统中性能可能非常关键,而且整个系统都由嵌入式应用程序控制。只有在内核配置了 CONFIG_MMAP_ALLOW_UNINITIALIZED 选项时,这个标志才会生效。

49.7 Anonymous Mappings

匿名映射是指没有对应文件的映射。在本节中,我们将展示如何创建匿名映射,并看看私有和共享匿名映射的用途。

MAP_ANONYMOUS and /dev/zero

在 Linux 上,有两种不同但等效的方法可以使用 mmap() 创建匿名映射:

  • 在 flags 中指定 MAP_ANONYMOUS,并将 fd 指定为 -1。(在 Linux 上,当指定 MAP_ANONYMOUS 时,fd 的值会被忽略。然而,一些 UNIX 实现要求在使用 MAP_ANONYMOUS 时 fd 必须为 -1,因此可移植的应用程序应该确保这样做。)

    要从 <sys/mman.h> 中获取 MAP_ANONYMOUS 的定义,我们必须定义 _BSD_SOURCE 或 _SVID_SOURCE 特性测试宏。Linux 还提供了常量 MAP_ANON 作为 MAP_ANONYMOUS 的别名,以兼容使用这个替代名称的其他一些 UNIX 实现。

  • 打开 /dev/zero 设备文件,并将得到的文件描述符传给 mmap()。

    /dev/zero 是一个虚拟设备,当我们从中读取时总是返回零。对这个设备的写操作总是被丢弃。/dev/zero 的常见用途是用零填充文件(例如,使用 dd(1) 命令)。

无论使用 MAP_ANONYMOUS 还是 /dev/zero 方法,结果映射的字节都会初始化为 0。对于这两种方法,offset 参数都会被忽略(因为没有底层文件可以指定偏移量)。稍后我们会展示每种方法的示例。

MAP_ANONYMOUS 和 /dev/zero 技术在 SUSv3 中没有规定,尽管大多数 UNIX 实现支持其中一种或两种。这两种不同技术存在但语义相同的原因是,一种(MAP_ANONYMOUS)源自 BSD,而另一种(/dev/zero)源自 System V。

MAP_PRIVATE anonymous mappings

MAP_PRIVATE 匿名映射用来分配初始化为 0 的进程私有内存块。我们可以使用 /dev/zero 技术来创建一个 MAP_PRIVATE 匿名映射,如下所示:

c 复制代码
fd = open("/dev/zero", O_RDWR);
if (fd == -1)
	errExit("open");
addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
	errExit("mmap");

glibc 对 malloc() 的实现使用 MAP_PRIVATE 的匿名映射来分配大于 MMAP_THRESHOLD 字节的内存块。这使得如果这些内存块后来被 free() 释放,就可以高效地通过 munmap() 进行释放。(它还减少了在反复分配和释放大内存块时内存碎片化的可能性。)MMAP_THRESHOLD 默认为 128 kB,但可以通过 mallopt() 库函数进行调整。

💡 上一章已经说过,公共内存是一块专门的区域,不来自于栈或堆。小于128kB的malloc会从堆中分配,大于128kB的malloc会从虚拟内存中分配。

MAP_SHARED anonymous mappings

一个 MAP_SHARED 匿名映射允许相关进程(例如父进程和子进程)共享一块内存区域,而不需要对应的映射文件。

MAP_SHARED 匿名映射仅在 Linux 2.4 及更高版本可用。

我们可以使用 MAP_ANONYMOUS 技术创建一个 MAP_SHARED 匿名映射,方法如下:

c 复制代码
addr = mmap(NULL, length, PROT_READ | PROT_WRITE,
			MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
	errExit("mmap");

如果上述代码后面跟着调用 fork(),那么因为 fork() 产生的子进程会继承映射,两个进程就会共享这块内存区域。

Example program

清单49-3中的程序演示了如何使用MAP_ANONYMOUS或/dev/zero在父子进程之间共享映射区域。使用哪种技术取决于在编译程序时是否定义了USE_MAP_ANON。父进程在调用fork()之前会将共享区域中的整数初始化为1。子进程随后将共享整数加1然后退出,而父进程则等待子进程退出后再打印该整数的值。当我们运行这个程序时,会看到如下结果:

bash 复制代码
$ ./anon_mmap
Child started, value = 1
In parent, value = 2

💡 USE_MAP_ANON是一个自定义宏,你可以添加#define USE_MAP_ANON来测试另一种方法。两种方法都是支持的。

Listing 49-3: Sharing an anonymous mapping between parent and child processes

c 复制代码
// mmap/anon_mmap.c
// 代码略。

49.8 Remapping a Mapped Region: mremap()

在大多数 UNIX 实现中,一旦创建了映射,它的位置和大小就不能更改。不过,Linux 提供了(不可移植的)mremap() 系统调用,它允许进行这样的更改。

c 复制代码
#define _GNU_SOURCE
#include <sys/mman.h>

void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...);
				Returns starting address of remapped region on success,
				or MAP_FAILED on error

old_address 和 old_size 参数指定了我们希望扩展或缩小的现有映射的位置和大小。old_address 指定的地址必须是页对齐的,并且通常是之前调用 mmap() 返回的值。映射的期望新大小由 new_size 指定。old_size 和 new_size 指定的值都会向上取整到系统页面大小的下一个倍数。

在执行重新映射时,内核可能会在进程的虚拟地址空间中重新定位映射。是否允许这样做由 flags 参数控制,该参数是一个位掩码,可以为 0 或包含以下值:

  • MREMAP_MAYMOVE
    如果指定了这个标志,那么根据空间需求,内核可以在进程的虚拟地址空间中重新定位映射。如果未指定该标志,并且当前地址位置的空间不足以扩展映射,则会出现 ENOMEM 错误。
  • MREMAP_FIXED(自 Linux 2.4 起)
    该标志只能与 MREMAP_MAYMOVE 一起使用。它在 mremap() 中的作用类似于 mmap() 的 MAP_FIXED(第 49.10 节)。如果指定了该标志,mremap() 将多一个参数 void *new_address,用于指定映射应移动到的页对齐地址。new_address 和 new_size 指定的地址范围内的任何先前映射都会被取消映射。

成功时,mremap() 会返回映射的起始地址。由于(如果指定了 MREMAP_MAYMOVE 标志)这个地址可能与之前的起始地址不同,所以指向该区域的指针可能不再有效。因此,使用 mremap() 的应用程序在引用映射区域的地址时应只使用偏移量(而不是绝对指针)(参见第 48.6 节)。

在 Linux 上,realloc() 函数使用 mremap() 来高效地重新分配之前由 malloc() 使用 mmap() MAP_ANONYMOUS 分配的大块内存。(我们在第 49.7 节提到过 glibc malloc() 实现的这个特性。)使用 mremap() 来完成这个任务可以在重新分配时避免复制字节。

49.9 MAP NORESERVE and Swap Space Overcommitting

有些应用程序会创建很大的(通常是私有匿名的)映射,但只使用映射区域的一小部分。例如,某些类型的科学应用会分配一个非常大的数组,但只操作数组中几处分散的元素(所谓的稀疏数组)。

如果内核总是为这些映射分配(或保留)足够的交换空间,那么可能会浪费大量交换空间。相反,内核可以只在实际需要时(即当应用程序访问某个页面时)为映射的页面保留交换空间。这种方法叫做延迟交换空间保留,它的优点是应用程序使用的总虚拟内存可以超过 RAM 加交换空间的总大小。

换句话说,延迟交换空间保留允许交换空间过度分配。只要所有进程不尝试访问映射的整个范围,这种方式是可以的。但是,如果所有应用都尝试访问映射的完整范围,那么 RAM 和交换空间就会耗尽。在这种情况下,内核会通过终止系统中的一个或多个进程来减轻内存压力。理想情况下,内核会尝试选择引起内存问题的进程(见下文的 OOM 杀手讨论),但这不能保证。因此,我们可能会选择阻止延迟交换空间保留,而是强制系统在创建映射时分配所有必要的交换空间。

内核如何处理交换空间的保留由调用 mmap() 时使用 MAP_NORESERVE 标志控制,并通过 /proc 接口来影响系统范围内的交换空间超额分配操作。这些因素总结在表 49-4 中。

Table 49-4: Handling of swap space reservation during mmap()

Linux特有的 /proc/sys/vm/overcommit_memory 文件包含一个整数值,用来控制内核如何处理交换空间超分配。在2.6之前的Linux版本中,这个文件只区分两个值:0,表示拒绝明显的超分配(但会受 MAP_NORESERVE 标志的影响);大于0,表示在所有情况下都允许超分配。

拒绝明显的过度提交意味着允许新映射的大小不超过当前可用空闲内存的数量。现有的分配可能会被过度提交(因为它们可能并未使用其映射的所有页面)。

从 Linux 2.6 开始,值为 1 的含义与早期内核中的正值相同,但值为 2(或更大)会启用严格的过度提交。在这种情况下,内核对所有 mmap() 分配进行严格的计算,并将系统范围内所有此类分配的总量限制为小于或等于:

bash 复制代码
[swap size] + [RAM size] * overcommit_ratio / 100

overcommit_ratio 的值是一个整数------表示百分比------保存在 Linux 特有的 /proc/sys/vm/overcommit_ratio 文件中。该文件中的默认值是 50,这意味着内核可以最多超分配系统内存大小的 50%,只要不是所有进程都尝试使用它们的全部分配,这将是可行的。

请注意,过度承诺监控只在以下类型的映射中起作用:

  • 私有可写映射(包括文件映射和匿名映射),对于这些映射,每个使用该映射的进程的交换空间"成本"等于映射的大小;
  • 共享匿名映射,对于这些映射,交换空间"成本"就是映射的大小(因为所有进程共享这个映射)。

为只读私有映射预留交换空间是没有必要的:因为映射的内容不能被修改,所以不需要使用交换空间。

共享文件映射也不需要交换空间,因为映射的文件本身就充当了映射的交换空间。

当子进程通过 fork() 继承映射时,它会继承映射的 MAP_NORESERVE 设置。MAP_NORESERVE 标志在 SUSv3 中没有规定,但在一些其他 UNIX 实现中是被支持的。

在本节中,我们讨论了调用 mmap() 为什么可能无法增加进程的地址空间,这是因为系统对 RAM 和交换空间的限制。调用 mmap() 也可能失败,因为它遇到了每个进程的 RLIMIT_AS 资源限制(在第 36.3 节中描述),这个限制对调用进程的地址空间大小设定了上限。

The OOM killer

如上所述,当我们使用惰性交换保留时,如果应用程序尝试使用其映射的整个范围,内存可能会被耗尽。在这种情况下,内核会通过终止进程来缓解内存耗尽。内核中负责在内存耗尽时选择要终止进程的代码通常被称为内存不足 (OOM) 杀手。OOM 杀手会尝试选择最合适的进程来终止,以缓解内存耗尽,"最合适"是通过多种因素来确定的。例如,占用内存越多的进程,被 OOM 杀手选中的可能性就越大。其他增加进程被选择可能性的因素包括:通过 fork 创建许多子进程,以及拥有较低的 nice 值(即大于 0 的值)。内核不倾向于终止以下进程:

  • 有特权的进程,因为它们可能正在执行重要任务;
  • 正在进行原始设备访问的进程,因为终止它们可能会使设备变得不可用;
  • 已运行较长时间或消耗大量 CPU 的进程,因为终止它们会导致大量"工作"丢失。

为了终止选定的进程,OOM killer 会发送一个 SIGKILL 信号。

自内核 2.6.11 以来,Linux 特有的 /proc/PID/oom_score 文件显示了内核在必要时调用 OOM killer 时对进程的权重。文件中的值越大,如果需要被 OOM killer 选择,进程被选中的可能性就越高。Linux 特有的 /proc/PID/oom_adj 文件,同样自内核 2.6.11 以来可用,可以用来影响进程的 oom_score。这个文件可以设置在 --16 到 15 的任意值,其中负值会降低 oom_score,正值会增加。特殊值 --17 会将进程完全从 OOM killer 的候选列表中移除。更多细节,请参阅 proc(5) 手册页面。

49.10 The MAP_FIXED Flag

在 mmap() 的 flags 参数中指定 MAP_FIXED 会强制内核按 addr 指定的地址来解释,而不是把它当作一个提示。如果我们指定了 MAP_FIXED,那么 addr 必须是页对齐的。一般来说,一个可移植的应用程序应该避免使用 MAP_FIXED,并将 addr 指定为 NULL,这样系统就可以选择放置映射的地址。原因和我们在第 48.3 节中解释的类似------当使用 shmat() 附加 System V 共享内存段时,通常最好将 shmaddr 指定为 NULL。

不过,有一种情况可移植应用可能会使用 MAP_FIXED。如果在调用 mmap() 时指定了 MAP_FIXED,并且从 addr 开始、长度为 length 的内存区域与之前的映射页重叠,那么重叠的页面会被新的映射替换。我们可以使用这个特性在内存中连续区域可移植地映射文件(或文件的多个部分),方法如下:

  1. 使用 mmap() 创建一个匿名映射(见第 49.7 节)。在 mmap() 调用中,将 addr 指定为 NULL,并且不指定 MAP_FIXED,这样内核就可以选择映射的地址。
  2. 使用一系列指定 MAP_FIXED 的 mmap() 调用,将文件区域映射(覆盖)到上一步创建的映射的不同部分。

虽然我们可以跳过第一步,直接用一系列 mmap() MAP_FIXED 操作在应用程序选定的地址范围内创建一组连续映射,但这种方法比执行这两步的可移植性要差。如上所述,可移植的应用程序应该避免在固定地址创建新映射。第一步可以避免可移植性问题,因为我们让内核选择一个连续地址范围,然后在该范围内创建新的映射。

从 Linux 2.6 开始,我们将在下一节介绍的 remap_file_pages() 系统调用也可以实现同样的效果。不过,相比于 remap_file_pages()(这是 Linux 特有的),使用 MAP_FIXED 更具可移植性。

49.11 Nonlinear Mappings: remap_file_pages()

使用 mmap() 创建的文件映射是线性的:映射文件的每一页都与内存区域的每一页一一对应。对于大多数应用来说,线性映射就足够了。不过,有些应用需要创建大量非线性映射------映射中,文件的页面在连续内存中的顺序不同。我们在图 49-5 中展示了一个非线性映射的例子。

在上一节中,我们描述了一种创建非线性映射的方法:使用带有 MAP_FIXED 标志的多次 mmap() 调用。然而,这种方法扩展性不佳。问题在于,每次 mmap() 调用都会创建一个单独的内核虚拟内存区域(VMA)数据结构。每个 VMA 都需要时间来设置,并且会消耗一些不可交换的内核内存。此外,大量 VMA 的存在可能会降低虚拟内存管理器的性能;特别是,当有成千上万个 VMA 时,处理每个页面错误所需的时间可能会显著增加。(这对一些在数据库文件中维护多个不同视图的大型数据库管理系统来说是个问题。)

/proc/PID/maps 文件(第 48.5 节)中的每一行表示一个 VMA。

从内核 2.6 开始,Linux 提供了 remap_file_pages() 系统调用,可以在不创建多个 VMA 的情况下创建非线性映射。我们可以这样做:

  1. 使用 mmap() 创建一个映射。
  2. 调用一次或多次 remap_file_pages() 来重新排列内存页和文件页之间的对应关系。(remap_file_pages() 做的只是操作进程的页表。)

可以使用 remap_file_pages() 将文件的同一页映射到映射区域内的多个位置。

c 复制代码
#define _GNU_SOURCE
#include <sys/mman.h>

int remap_file_pages(void *addr, size_t size, int prot, size_t pgoff, int flags);
				Returns 0 on success, or --1 on error

pgoff 和 size 参数用于确定要更改内存位置的文件区域。pgoff 参数以系统页大小(由 sysconf(_SC_PAGESIZE) 返回)为单位指定文件区域的起始位置。size 参数以字节为单位指定文件区域的长度。addr 参数有两个作用:

  • 它标识我们希望重新排列其中页面的现有映射。换句话说,addr 必须是一个落在之前使用 mmap() 映射的区域中的地址。
  • 它指定 pgoff 和 size 标识的文件页面在内存中的位置。

addr 和 size 都应该以系统页大小的整数倍来指定。如果不是,则会向下取整到最接近的页大小倍数。

假设我们使用以下 mmap() 调用来映射由描述符 fd 指向的打开文件的三页,且调用返回的地址 0x4001a000 被分配给 addr:

c 复制代码
ps = sysconf(_SC_PAGESIZE); /* Obtain system page size */
addr = mmap(0, 3 * ps, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

以下调用将创建图 49-5 所示的非线性映射:

c 复制代码
remap_file_pages(addr, ps, 0, 2, 0);
/* Maps page 0 of file into page 2 of region */
remap_file_pages(addr + 2 * ps, ps, 0, 0, 0);
/* Maps page 2 of file into page 0 of region */

Figure 49-5: A nonlinear file mapping

还有两个我们还没介绍的 remap_file_pages() 参数:

  • prot 参数会被忽略,必须指定为 0。将来可能可以使用这个参数来改变 remap_file_pages() 影响的内存区域的保护属性。但在当前实现中,保护属性仍然和整个 VMA 一样。
    虚拟机和垃圾回收器是其他使用多个 VMA 的应用程序。这些应用程序中的一些需要能够对单独的页进行写保护。原本的想法是 remap_file_pages() 可以允许改变 VMA 内单独页的权限,但到目前为止这个功能还没有实现。
  • flags 参数目前未使用。
    目前的实现中,remap_file_pages() 只能应用于共享 (MAP_SHARED) 映射。
    remap_file_pages() 系统调用是 Linux 特有的;在 SUSv3 中没有规范,也不能在其他 UNIX 系统上使用。

49.12 Summary

mmap() 系统调用在调用进程的虚拟地址空间中创建一个新的内存映射。munmap() 系统调用则执行相反的操作,从进程的地址空间中移除一个映射。

映射可以有两种类型:基于文件的或匿名的。文件映射会将文件区域的内容映射到进程的虚拟地址空间中。匿名映射(通过使用 MAP_ANONYMOUS 标志或者映射 /dev/zero 创建)没有对应的文件区域;映射的字节会被初始化为 0。

映射可以是私有的(MAP_PRIVATE)或共享的(MAP_SHARED)。这个区分决定了对共享内存所做更改的可见性,并且在文件映射的情况下,决定内核是否将映射内容的更改传递到底层文件。当进程以 MAP_PRIVATE 标志映射文件时,它对映射内容的任何修改对其他进程不可见,也不会写回到映射的文件中。MAP_SHARED 文件映射则相反------对映射的更改对其他进程可见,并且会写回到底层文件中。

虽然内核会自动将 MAP_SHARED 映射的内容更改传播到底层文件,但它并不保证具体什么时候完成。应用程序可以使用 msync() 系统调用来明确控制映射内容与映射文件同步的时间。

内存映射有多种用途,包括:

  • 分配进程私有内存(私有匿名映射);
  • 初始化进程的文本段和已初始化数据段的内容(私有文件映射);
  • 在通过 fork() 关联的进程之间共享内存(共享匿名映射);以及
  • 执行内存映射的 I/O,可选择同时在无关进程之间共享内存(共享文件映射)。

在访问映射内容时,可能会出现两个信号。如果我们以违反映射保护的方式访问映射(或者访问任何当前未映射的地址),会产生 SIGSEGV。对于基于文件的映射,如果我们访问映射中在文件中没有对应区域的部分(也就是说,映射比底层文件大),则会产生 SIGBUS。

交换空间过度分配允许系统分配比实际 RAM 和交换空间更多的内存给进程。过度分配之所以可能,是因为通常每个进程不会完全使用其分配的内存。过度分配可以通过每个 mmap() 使用 MAP_NORESERVE 标志来控制,也可以通过 /proc 文件在系统范围内控制。

mremap() 系统调用允许重新调整现有映射的大小。remap_file_pages() 系统调用允许创建非线性文件映射。

Further information

关于 Linux 上 mmap() 实现的信息可以参考 Bovet \& Cesati, 2005。关于其他 UNIX 系统上 mmap() 实现的信息可以参考 McKusick et al., 1996(BSD)、Goodheart \& Cox, 1994(System V Release 4)以及 Vahalia, 1996(System V Release 4)。

相关推荐
EVEN-09131 小时前
Linux-进程(2)
linux·运维·服务器
u0109053591 小时前
(永久免费)树莓派 / 香橙派等开发板,安装神卓互联巴比达内网穿透安装教程(Linux ARM 版 V9.4.1)
linux·运维·arm开发
Linux_cy1 小时前
服务器管理方案选型:面板 / SSH 客户端 / 桌面型客户端怎么选
java·linux·运维·服务器·ssh·可视化·gmssh
可乐_不渴1 小时前
【Linux】 进程虚拟地址空间
java·linux·javascript
byte轻骑兵2 小时前
hcitool 工具:HCI 层指令实操 + 源码中指令的封装逻辑
linux·aiot·bluez·电脑蓝牙·嵌入式蓝牙
qeen872 小时前
【Linux】磁盘读写文件原理及文件系统基本概念介绍
linux·运维·服务器·笔记·学习
Rabitebla2 小时前
【Linux 底层原理】工具篇 (三):gcc/g++ 编译工具、动静态库与编译原理
linux·运维·服务器·笔记·学习
进击的荆棘2 小时前
Linux系统——进程信号
linux·运维·服务器·信号
知已难寻2 小时前
第 5 篇 · 云原生生态与实践:微服务、DevOps、可观测性与 Serverless
大数据·linux·服务器·微服务·云原生·云计算·devops