LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

《Linux/UNIX系统编程手册》第49章 内存映射

关键词:mmap()、munmap()、msync()、SIGSEGV、SIGBUS、MAP_NORESERVE、MAP_FIXED、mremap()、remap_file_pages()等等。

1. 概述

mmap()系统调用在调用进程的虚拟地址空间中创建一个新内存映射。映射分为两种:

  • 文件映射:将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中加载。
  • 匿名映射:一个匿名映射没有对应的文件,这种映射的分页会被初始化为0

 一个进程的映射中的内存可以与其他进程中的映射共享:

  • 当两个进程映射了一个文件的同一个区域时他们会共享物理内存的相同分页。
  • 通过fork()创建的子进程会继承其父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同。

关于私有映射和共享映射:

  • 私有映射(MAP_PRIVATE):在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会在底层文件上进行。初始时是共享的,但对影射内容所做出的变更对各个进程来讲则是私有的。内核使用了写时复制完成这个任务,当一个进程试图修改一个分页的内容是,内核首先会为该进程创建一个新分页并将需修改的分页中的内容复制到新分页中
  • 共享映射(MAP_SHARED):在映射内容上发生的变更对所有共享同一个映射的其他进程都可见,对文件映射来讲,变更将发生在底层的文件上

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

  • 私有文件映射:映射的内容被初始化为一个文件区域中的内容。多个映射同一个文件的的进程初始时会共享同样的内存物理分页,单系统使用写时复制使得一个进程对映射所做出的变更对其他进程不可见。主要用途是使用一个文件的内容来初始化一块内存区域。常见的例子包括根据二进制可执行文件或共享库文件的相应部分来初始化一个进程的文本和数据段。
  • 私有匿名映射:每次调用mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一或不同进程创建的其他匿名映射是不同的,既不会共享物理内存分页。私有匿名映射的主要用途是为一个进程分配新用零填充内存。
  • 共享文件映射:所有映射一个文件的同一区域的进程会共享同样的内存物理分页,这些分页的内容会被初始化为该文件区域。对映射内容的修改将直接在文件中进行。
  • 共享匿名映射:每次调用mmap()创建一个共享匿名映射时都会产生一个新的、与任何其他映射不共享分页的截然不同的映射。和私有匿名映射区别在于映射的分页不会被写时复制。for()的子进程继承映射,父子进程共享同样的RAM分页,并且一个进程对应摄内容的变更会对其他进程可见。

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

2. 创建一个映射:mmap()

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

#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,那么内核会为映射选择一个合适的地址。

length:指定了映射的字节数。内核会以分页大小为单位来创建映射,实际上length会被向上提升为分页大小的下一个倍数。

prot:是一个位掩码,指定了施加于应设置上的保护信息,其取值要么是PROT_NONE,要么是另三个标记的组合。

 flags:是一个控制映射操作各个方面选项的位掩码。

    MAP_PRIVATE:创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的,对于文件映射来讲,所发生的的变更将不会反应在底层文件上。

    MAP_SHARED:创建一个共享映射。区域中内容上所发生的变更对使用MAP_SHARED特性映射同一区域的进程是可见的,对于文件映射来讲,所发生的变更将直接反应在底层文件上。

fdoffset:用于文件映射,fd标识被映射文件的文件描述符;offset指定映射在文件中的起点,它必须是系统分页大小的倍数。

成功时mmap()会返回新映射的起始地址,错误是mmap()会返回MAP_FAILED。MAP_FAILED等同于((void *)-1)。

下面的示例通过mmap()将一个文件映射,然后将其内容输出到STDOUT。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int
main(int argc, char *argv[])
{
    char *addr;
    int fd;
    struct stat sb;

    if (argc != 2 || strcmp(argv[1], "--help") == 0)
        printf("%s file\n", argv[0]);

    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        printf("open failed");
        return -1;
    }

    if (fstat(fd, &sb) == -1) {
        printf("fstat failed");
        return -1;
    }

    if (sb.st_size == 0)
        exit(EXIT_SUCCESS);

    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);-------将文件句柄fd全部内容以Read、Private映射到addr起始的映射区域。
    if (addr == MAP_FAILED) {
        printf("mmap failed");
        return -1;
    }

    if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size) {---------将映射区域全部内容输出到STDOUT。
        printf("partial/failed write");
        return -1;
    }

    exit(EXIT_SUCCESS);
}

3. 解除映射区域:munmap()

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

#include <sys/mman.h>
int munmap(void *addr, size_t length);
    Returns 0 on success, or –1 on error

addr:是待解除映射的地址范围的起始地址。

length:是一个非负整数,指定了待解除映射区域的大小。范围为系统分区页大小的下一个倍数的地址空间将会被解除映射。

当一个进程终止或执行了一个exec()之后进程中所有的映射会自动被解除。

为确保一个共享文件映射的内容会被写入到底层文件中,在使用munmap()解除一个映射之前需要调用msync()。

4. 文件映射

创建一个文件映射的步骤:

1. 获取文件的一个描述符,通常通过open()来完成。

2. 将文件描述符作为fd参数传入mmap()调用。

即可将打开文件的内容映射到调用进程的地址空间中,即使文件被关闭,也不会对映射产生影响。

fd引用文件时必须要具备与prot和flags参数值匹配的权限。

offset参数指定了从文件区域中的哪个字节开始映射,他必须是系统分页大小的倍数。offset指定为0则从文件的起始位置开始映射。

length参数指定了映射的字节数。

4.1 私有文件映射

私有文件映射用途:

  • 允许多个执行同一个程序或使用同一个共享库的进程共享同样的文本段,它是从底层可执行文件或库文件的相应部分映射而来的。(尽管可执行文件文本段只允许读取和执行访问,但在被映射时仍然使用了MAP_PRIVATE,这是因为调试器或自修改的程序能够修改程序文本,而这样的变更不应该发生在底层文件上或影响其他进程。)
  • 应设一个可执行文件或共享库的初始化数据段。这种映射会被处理成私有是的对映射数据段内容的变更不会发生在底层文件上。

mmap()的这种用法通常对程序是不可见的,这些映射是由程序加载器和动态链接器创建的。

4.2 共享文件映射

 多个进程创建了同一个文件区域的共享映射时,它们会共享同样的物理内存分页。

共享文件映射存在两个用途:内存映射I/O和IPC。

内存映射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有什么劣势?

对于小数据量I/O来讲,内存映射I/O开销,即映射、缺页故障、讲出映射以及更新硬件内存管理单元的超前转换缓冲器,实际上比简单的read()或write()大。

有时候内核难以高效地处理可写入映射的回写,需要借助msync()或sync_file_range()有助于提高效率。

使用共享文件映射的IPC

由于所有使用同样文件区域的共享映射的进程共享同样的内存物理分页,因此共享文件映射第二个用途是作为一种IPC方法。

使用共享文件映射IPC和System V共享内存对象之间区别在于区域中内容上的变更会反应到映射文件上。

4.3 边界情况

通常情况下,一个映射的大小是系统分页大小整数倍,并且映射会完全落入映射文件的范围之内。

映射完全落入映射文件范围之内,但区域大小不是系统分页大小整数倍

假设系统分页大小为4096字节,被映射文件大小为9500字节,将文件首6000字节映射。

  • 要求映射的6000字节,会被对齐到8192字节。
  • 内存实际可以访问范围为0~8091,并且对齐修改会落实到实际文件中。
  • 文件8192~9499区域没有被映射,超出8191内存访问会产生SIGSEGV异常。

 扩充底层文件结尾映射

假设系统分页大小为4096字节,被映射文件大小为2200字节,mmap()长度为8192。

  •  要求映射的8192字节,被分为三部分:0~2199 - 映射到文件的可访问部分,2200~4095 - 没有映射到文件的可访问部分,4096~8191 - 不可访问部分。
  • 映射到文件的可访问部分,变更会更新到底层文件;没有映射到文件的可访问部分,被初始化为0,不会被映射到底层文件上,也不会与映射同一个文件的其他进程共享。
  • 4096~8191范围的不可访问部分,对齐访问会产生SIGBUS异常。
  • 对8192~之后的地址访问,同样产生SIGSEGV异常。

4.4 内存保护和文件访问模式交互

一般来讲,PROT_READ和PROT_EXEC保护要求被影射的文件使用O_RDONLY或O_RDWR打开,而PROT_WRITE保护要求被映射的文件使用O_WRONLY或O_RDWR打开。

5. 同步映射区域:msync()

内核会自动将发生在MAP_SHARED映射内容上的变更写入到底层文件中,但是不保证这种操作会在何时发生。

msync()系统调用让应用程序能够显式控制何时完成共享映射和映射文件之间的同步。

调用msync()还允许一个 应用程序确保在可写入映射上发生的更新会对在该文件上执行read()的其他进程可见。

#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
    Returns 0 on success, or –1 on error

addr指定的地址必须是分页对齐的。

length会被向上摄入到系统分页大小下一个整数倍。

flags指定msync()同步方式。

  MS_SYNC执行一个同步文件写入。这个调动会阻塞知道内存区域中所有被修改过的分页被写入到文件为止。MS_ASYNC执行一个异步文件写入。变更会在后面某个时刻被写入磁盘并立即对在相应文件区域中执行read()的其他进程可见。

  MS_SYNC操作之后,内存区域会与磁盘同步;MS_ASYNC操作之后,内存区域仅仅是与内核高速缓冲区同步。

  MS_INVALIDATE使映射数据的缓存副本失效。当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底层文件不一致的分页会被标记为无效。

6. 其他mmap()标记

7. 匿名映射

MAP_ANONYMOUS和/dev/zero

在Linux上,使用mmap()创建匿名映射存在梁红不同但等价方法:

  • flags指定MAP_ANONYMOUS并将fd指定为-1。
  • 打开/dev/zero设备文件并将得到的文件描述符传递给mmap()。

这两种映射得到的字节都会被初始化Wie0,并且offset都会被忽略。

MAP_PRIVATE匿名映射

MAP_SHARED匿名映射用来分配进程私有的内存块并将其中的内容初始化为0.

MAP_SHARED匿名映射

MAP_SHARED匿名映射允许相关进程共享一块内存区域而无需一个对应的映射文件。

8. 重新映射一个映射区域:mremap()

mreamap()系统个调用用来执行映射区域大小变更。

#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_addressold_size指定了需扩展或收缩的既有映射的位置和大小。old_address通常是一个由之前mmap()调用返回的值。

new_size是映射预期的新大小。

在执行重映射的过程中内核可能会为映射在进程的虚拟地址空间中重新指定一个位置,是否允许这种行为是由flags参数控制。flags要么是0,要么包含下列几个值。

MREMAP_MAYMOVE:如果指定了标记,内核可能会为映射在进程的虚拟地址空间中重新指定一个位置。如果没有指定,并且在当前位置处没有足够的空间来扩展这个映射,那么就返回ENOMEM错误。

MREMAP_FIXED:只能和MAP_MAYMOVE一起使用。如果指定了标记,那么mremap()会接收一个额外参数void *new_address,该参数指定了一个分页对齐的地址,并且映射将会被迁移至该地址处。

mremap()在成功时会返回映射的起始地址。由于这个地址可能有变化,从而导致指向这个区域中的指针可能会变得无效,因此使用mremap()的应用程序在引用映射区域中的地址是应该只使用偏移量。

9. MAP_NORESERVE和过度利用交换空间

内核如何处理交换空间的预留是由调用mmap()时是否使用了MAP_NORESERVE标记以及影响系统层面的交换空间过度利用操作的/proc接口来控制的。

 

 

 /proc/sys/vm/overcommit_memory包含一个整数值:0表示拒绝明显的过度利用;1表示所有情况下都允许过度利用;2表示采用严格的过度利用。

overcommit_memory为2情况下,内核会在所有mmap()分配上执行严格的几张并将系统中此类分配的总量控制在小于等于:

 

 

 overcommit_ratio是一个整数-用百分比表示-它位于/proc/sys/vm/overcommit_ratio文件中。这个文件包含默认值是50,表示内核最多可分配的空间为系统RAM总量的50%。

过度利用监控只适用于下面映射:

私有可写映射,这种映射的交换开销等于所有使用该映射的进程为该映射所分配的空间总和。

共享匿名映射,这种映射的交换开销等于映射的大小。

10. MAP_FIXED标记

mmap()的flags参数MAP_FIXED标记会强制内核原样地解释addr中的地址,addr必须是分页对齐的。

如果在调用mmap()时指定了MAP_FIXED,并且内存区域的起始位置为addr,覆盖的length字节与之前的映射的分页重叠了,那么重叠的分页会被新映射取代。使用这个特性可以可移植地将一个文件的多个部分映射进一块连续的内存区域。

  1. 使用mmap()创建一个匿名映射,在mmap()调用将addr指定为NULL并不指定MAP_FIXED标记。
  2. 使用一系列指定了MAP_FIXED标记的mmap()调用来将文件区域映射进在上一步创建的映射的不同部分中。

11. 非线性映射:remap_file_pages()

使用mmap()创建的文件映射是连续的,映射文件的分页与内存区域的分页存在一个顺序的、一对一的对应关系。

非线性映射文件分页的顺序与它们在连续内存中出现的顺序不同的映射。

 

 

 remap_file_pages()系统调用在无需创建多个vma情况下创建非线性映射:

  • 使用mmap()创建一个映射。
  • 使用一个或多个remap_file_pages()调用来调整内存分页和文件分页之间的对应关系。
#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参数指定了文件区域的长度,其单位为字节。

addr参数起两个作用,它标识了分页需要调整的既有映射。addr必须是一个位于之前通过mmap()映射的区域中的地址;指定了通过pgoff和size标识出的文件分页所处的内存地址。

addr和size都应该是系统分页大小的整数倍。

prot参数会被忽略,其值必须是0.

flags参数当前未被使用。

remap_file_pages()仅适用于共享映射。

12. 总结

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

映射可以分为两种:基于文件的映射和匿名映射。文件映射将一个文件区域中的内容映射到进程的虚拟地址空间中。匿名映射并没有对应的稳健趋于,该映射中的字节会被初始化为0.

映射既可以是私有的,也可以是共享的。对文件映射来讲,这种差别确定了内核是否会将映射内容上发生的变更传递到底层文件上。使用MAP_PRIVATE,映射内容上发生的变更对其他进程是不可见的,也不会反应到映射文件上。MAP_SHARED文件映射的做法则相反,在映射上发生的变更对其他进程可见并且会反应到映射文件上。

msync()系统调用显式地控制一个映射的内容何时与映射文件进行同步。

内存映射用途

  • 分配进程私有的内存(私有匿名内存)
  • 对一个进程的文本段和初始化数据段中的内容进行初始化(私有文件映射)
  • 通过fork()关联起来的进程之间的共享内存(共享匿名映射)
  • 执行内存映射I/O,还可以将其与无关进程之间的内存共享结合起来(共享文件映射)

两个信号:SIGSEGV和SIGBUS

如果在映射时违反了应设置上的保护规则(或访问一个当前未被映射的地址),那么就会产生一个SIGSEGV信号。

对于基于文件的映射来讲,如果访问的映射部分在文件中没有相关区域与之对应(即映射大于底层文件),那么就会产生一个SIGBUS信号。

使用MAP_NORESERVE标记可以控制每个mmap()调用的过度利用情况,而是用/proc文件则可以控制整个系统的过度利用情况。

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

posted on 2020-04-30 00:00  ArnoldLu  阅读(825)  评论(0编辑  收藏  举报

导航