Linux/AndroidOS中进程间的通信&线程间的同步 - 内存映射

前言

如何使用 mmap()系统调用来创建内存映射。内存映射可用于 IPC 以及其他很多方面。

1 概述

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

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

一个进程的映射中的内存可以与其他进程中的映射共享(即各个进程的页表条目指向RAM 中相同分页)。这种行为会在两种情况下发生。

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

当两个或更多个进程共享相同分页时,每个进程都有可能会看到其他进程对分页内容做出的变更,当然这要取决于映射是私有的还是共享的。

  • 私有映射(MAP_PRIVATE):在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会在底层文件上进行。尽管一个私有映射的分页在上面介绍的情况中初始时是共享的,但对映射内容所做出的变更对各个进程来讲则是私有的。内核使用了写时复制(copy-on-write)技术完成了这个任务。这意味着每当一个进程试图修改一个分页的内容时,内核首先会为该进程创建一个新分页并将需修改的分页中的内容复制到新分页中(以及调整进程的页表)。正因为这个原因,MAP_PRIVATE 映射有时候会被称为私有、写时复制映射。
  • 共享映射(MAP_SHARED):在映射内容上发生的变更对所有共享同一个映射的其他进程都可见,对于文件映射来讲,变更将会发生在底层的文件上。

上面介绍的两个映射特性(文件与匿名以及私有和共享)可以以四种不同的方式加以组合,下表对此进行了总结。

这四种不同的内存映射的创建和使用方式如下所述。

  • 私有文件映射 :映射的内容被初始化为一个文件区域中的内容。多个映射同一个文件的进程初始时会共享同样的内存物理分页,但系统使用写时复制技术使得一个进程对映射所做出的变更对其他进程不可见。这种映射的主要用途是使用一个文件的内容来初始化一块内存区域。一些常见的例子包括根据二进制可执行文件或共享库文件的相应部分来初始化一个进程的文本和数据段。
  • 私有匿名映射 :每次调用 mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的(即不会共享物理分页)。尽管子进程会继承其父进程的映射,但写时复制语义确保在 fork()之后父进程和子进程不会看到其他进程对映射所做出的变更。私有匿名映射的主要用途是为一个进程分配新(用零填充)内存(如在分配大块内存时 malloc()会为此而使用mmap())
  • 共享文件映射 :所有映射一个文件的同一区域的进程会共享同样的内存物理分页,这些分页的内容将被初始化为该文件区域。对映射内容的修改将直接在文件中进行。这种映射主要用于两个用途。第一种用途 ,它允许内存映射 I/O,这表示一个文件会被加载到进程的虚拟内存中的一个区域中并且对该块内容的变更会自动被写入到这个文件中。因此,内存映射 I/O 为使用 read()和 write()来执行文件 I/O 这种做法提供了一种替代方案。第二种用途是允许无关进程共享一块内容以便以一种共享内存段的方式来执行(快速)IPC。
  • 共享匿名映射:与私有匿名映射一样,每次调用 mmap()创建一个共享匿名映射时都会产生一个新的、与任何其他映射不共享分页的截然不同的映射。这里的差别在于映射的分页不会被写时复制。这意味着当一个子进程在 fork()之后继承映射时,父进程和子进程共享同样的 RAM 分页,并且一个进程对映射内容所做出的变更会对其他进程可见。共享匿名映射允许以一种类似于 System V 共享内存段的方式来进行 IPC,但只有相关进程之间才能这么做。

2 创建一个映射:mmap()

2.1 函数参数

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 必须是分页对齐的。

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

  • length 参数指定了映射的字节数。尽管 length 无需是一个系统分页大小的倍数,但内核会以分页大小为单位来创建映射,因此实际上 length 会被向上提升为分页大小的下一个倍数。

  • prot 参数是一个位掩码,它指定了施加于映射之上的保护信息,其取值要么是PROT_NONE,要么是下表中列出的其他三个标记的组合(取 OR)。

  • flags 参数是一个控制映射操作各个方面的选项的位掩码。这个掩码必须只包含下列值中一个。
    MAP_PRIVATE 创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的,对于文件映射来讲,所发生的变更将不会反应在底层文件上。
    MAP_SHARED 创建一个共享映射。区域中内容上所发生的变更对使用 MAP_SHARED 特性映射同一区域的进程是可见的,对于文件映射来讲,所发生的变更将直接反应在底层文件上。对文件的更新将无法确保立即生效,具体可参加 49.5 节中对 msync()系统调用的介绍。

    除了 MAP_PRIVATE 和 MAP_SHARED 之外,在 flags 中还可以有选择地对其他标记取OR。在第6 和 10 节中将会对这些标记进行介绍。

  • 剩余的参数 fd 和 offset 是用于文件映射的(匿名映射将忽略它们)。fd 参数是一个标识被映射的文件的文件描述符。offset 参数指定了映射在文件中的起点,它必须是系统分页大小的倍数。要映射整个文件就需要将 offset 指定为 0 并且将 length 指定为文件大小。在第5节中将会介绍更多有关文件映射的内容。

2.2 示例程序

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

c 复制代码
/*
mmap/mmcat.c
   Use mmap() plus write() to display the contents of a file (specified
   as a command-line argument) on standard output.
*/

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"

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

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

    fd = open(argv[1], O_RDONLY);
    if (fd == -1)
        errExit("open");

    /* Obtain the size of the file and use it to specify the size of
       the mapping and the size of the buffer to be written */

    if (fstat(fd, &sb) == -1)
        errExit("fstat");

    /* Handle zero-length file specially, since specifying a size of
       zero to mmap() will fail with the error EINVAL */

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

    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size)
        fatal("partial/failed write");
    exit(EXIT_SUCCESS);
}

3 解除映射区域:munmap()

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

c 复制代码
#include <sys/mman.h>
int munmap(void *addr, size_t length);	//return 0 on success, or -1 on error.
  • addr 参数是待解除映射的地址范围的起始地址,它必须与一个分页边界对齐。
  • length 参数是一个非负整数,它指定了待解除映射区域的大小(字节数)。范围为系统分页大小的下一个倍数的地址空间将会被解除映射。

一般来讲通常会解除整个映射。因此可以将 addr 指定为上一个 mmap()调用返回的地址,并且 length 的值与 mmap()调用中使用的 length 的值一样。下面是一个例子。

c 复制代码
addr = map(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");
  1. 也可以解除一个映射中的部分映射,这样原来的映射要么会收缩,要么会被分成两个,这取决于在何处开始解除映射。还可以指定一个跨越多个映射的地址范围,这样的话所有在范围内的映射都会被解除。
  2. 如果在由 addr 和 length 指定的地址范围中不存在映射,那么 munmap()将不起任何作用并返回 0(表示成功)。
  3. 在解除映射期间,内核会删除进程持有的在指定地址范围内的所有内存锁。(内存锁是通过 mlock()或 mlockall()来建立的)
  4. 当一个进程终止或执行了一个 exec()之后进程中所有的映射会自动被解除。
  5. 为确保一个共享文件映射的内容会被写入到底层文件中,在使用 munmap()解除一个映射之前需要调用 msync()

4 文件映射

要创建一个文件映射需要执行下面的步骤。

  1. 获取文件的一个描述符,通常通过调用 open()来完成。
  2. 将文件描述符作为 fd 参数传入 mmap()调用。

执行上述步骤之后 mmap()会将打开的文件的内容映射到调用进程的地址空间中。一旦mmap()被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响

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

在打开描述符 fd 引用的文件时必须要具备与 prot 和 flags 参数值匹配的权限。特别地,文件必须总是被打开以允许读取,并且如果在 flags 中指定了 PROT_WRITE 和 MAP_SHARED,那么文件必须总是被打开以允许读取和写入。

offset 参数指定了从文件区域中的哪个字节开始映射,它必须是系统分页大小的倍数 。将offset 指定为 0 会导致从文件的起始位置开始映射。length 参数指定了映射的字节数。offset和 length 参数一起确定了文件的哪个区域会被映射进内存,如下图所示。

4.1 私有文件映射

私有文件映射最常见的两个用途如下所述。

  • 允许多个执行同一个程序或使用同一个共享库的进程共享同样的(只读的)文本段,它是从底层可执行文件或库文件的相应部分映射而来的。
  • 映射一个可执行文件或共享库的初始化数据段。这种映射会被处理成私有使得对映射数据段内容的变更不会发生在底层文件上。

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

4.2 共享文件映射

当多个进程创建了同一个文件区域的共享映射时,它们会共享同样的内存物理分页。此外,对映射内容的变更将会反应到文件上。实际上,这个文件被当成了该块内存区域的分页存储,如下图所示。(这幅图是简化过的,它并没有指出映射分页在物理内存中通常是不连续的这样一个事实。)

共享文件映射存在两个用途:内存映射 I/O 和 IPC。下面将分别介绍这两种用途。

4.2.1 内存映射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()相比,mmap()带来的性能上的提升就非常有限或者说根本就没有带来性能上的提升。性能提升的幅度之所以非常有限的原因是不管使用何种技术,整个文件的内容在磁盘和内存之间只传输一次,效率的提高主要得益于减少了用户空间和内核空间之间的一次数据传输,并且与磁盘 I/O 所需的时间相比,内存使用量的降低通常是可以忽略的。

内存映射 I/O 也有一些缺点。对于小数据量 I/O 来讲,内存映射 I/O 的开销(即映射、分页故障、解除映射以及更新硬件内存管理单元的超前转换缓冲器)实际上要比简单的read()或 write()大。此外,有些时候内核难以高效地处理可写入映射的回写(在这种情况下,使用 msync()或 sync_file_range()有助于提高效率)。

4.2.2 使用共享文件映射的 IPC

由于所有使用同样文件区域的共享映射的进程共享同样的内存物理分页,因此共享文件映射的第二个用途是作为一种(快速的)IPC 方法。这种特性对那些需要共享内存内容在应用程序或系统重启时能够持久化的应用程序来讲是非常有用的。

示例程序

程序清单 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.00764544 s, 134 kB/s

然后使用程序映射这个文件并将一个字符串复制进映射区域中。

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

程序在打印当前字符串时不会显示任何内容,因为映射文件的初始值是以 null 字节打头的(即长度为零的字符串)。

接着再次使用程序映射这个文件并复制一个新字符串到映射区域中。

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

这个简单的程序没有使用任何机制来同步多个进程对映射文件的访问。但现实世界中的应用程序通常需要同步对共享映射的访问。这可以通过使用各种技术来完成,包括信号量和文件加锁。

c 复制代码
/*
mmap/t_mmap.c
Demonstrate the use of mmap() to create a shared file mapping.
*/
#include <sys/mman.h>
#include <fcntl.h>
#include "tlpi_hdr.h"

#define MEM_SIZE 10

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

    if (argc < 2 || strcmp(argv[1], "--help") == 0)
        usageErr("%s file [new-value]\n", argv[0]);

    fd = open(argv[1], O_RDWR);
    if (fd == -1)
        errExit("open");

    addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (close(fd) == -1)                /* No longer need 'fd' */
        errExit("close");

    printf("Current string=%.*s\n", MEM_SIZE, addr);
                        /* Secure practice: output at most MEM_SIZE bytes */

    if (argc > 2) {                     /* Update contents of region */
        if (strlen(argv[2]) >= MEM_SIZE)
            cmdLineErr("'new-value' too large\n");

        memset(addr, 0, MEM_SIZE);      /* Zero out region */
        strncpy(addr, argv[2], MEM_SIZE - 1);
        if (msync(addr, MEM_SIZE, MS_SYNC) == -1)
            errExit("msync");

        printf("Copied \"%s\" to shared memory\n", argv[2]);
    }

    exit(EXIT_SUCCESS);
}

4.2.3 边界情况

在很多情况下,一个映射的大小是系统分页大小的整数倍,并且映射会完全落入映射文件的范围之内。下面来看一下当这些条件不满足时会发生什么事情。

下图描绘了映射完全落入映射文件的范围之内但区域的大小并不是系统分页大小的一个整数倍的情况(在这个讨论中假设分页大小为 4KB)。

由于映射的大小不是系统分页大小的整数倍,因此它会被向上舍入到系统分页大小的下一个整数倍。由于文件的大小要大于这个被向上舍入的大小,因此文件中对应字节会像图中那样被映射。

试图访问映射结尾之外的字节将会导致 SIGSEGV 信号的产生(假设在该位置处不存在其他映射)。这个信号的默认动作是终止进程并打印出一个 core dump。

当映射扩充过了底层文件的结尾处时(参见下图)情况就变得更加复杂了。与之前一样,由于映射的大小不是系统分页大小的整数倍,因此它会被向上舍入。但在这种情况下,虽然在向上舍入区域(即图中 2200 字节和 4095 字节)中的字节是可访问的,但它们不会被映射到底层文件上(由于在文件中不存在对应的字节),并且它们会被初始化为 0 。当然,这些字节也不会与映射同一个文件的其他进程共享,即使它们指定了足够大的 length 参数 。对这些字节做出的变更不会被写入到文件中。

如果映射中包含了超出向上舍入区域中(即上图中 4096 以及之后的字节)的分页,那么试图访问这些分页中的地址将会导致 SIGBUS 信号量的产生,即警告进程文件中没有区域与这些地址对应。与之前一样,试图访问超过映射结尾处的地址将会导致 SIGSEGV 信号的产生。

从上面的描述中可以看出,创建一个大小超过底层文件大小的映射可能是无意义的 。但通过扩展文件的大小(如使用 ftruncate()或 write()),可以使得这种映射中之前不可访问的部分变得可用。

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

到目前为止还没有详细解释的一点是通过 mmap() prot 参数指定的内存保护与映射文件被打开的模式之间的交互。从一般原则来讲,PROT_READ 和 PROT_EXEC 保护要求被映射的文件使用 O_RDONLY 或 O_RDWR 打开,而 PROT_WRITE 保护要求被映射的文件使用O_WRONLY 或 O_RDWR 打开。

然而,由于一些硬件架构提供的内存保护粒度有限,因此情况会变得复杂起来。对于这种架构,下列结论是适用的。

  • 所有内存保护组合与使用 O_RDWR 标记打开文件是兼容的。
  • 没有内存保护组合------哪怕仅仅是 PROT_WRITE------与使用 O_WRONLY 标记打开的文件是兼容的(导致 EACCES 错误的发生)。这与一些硬件架构不允许对一个分页的只写访问这样一个事实是一致的。
  • 使用 O_RDONLY 标记打开一个文件的结果依赖于在调用 mmap()时是否指定了MAP_PRIVATE 或 MAP_SHARED。对于一个 MAP_PRIVATE 映射来讲,在 mmap()中可以指定任意的内存保护组合------因为对MAP_PRIVATE分页内容做出的变更不会被写入到文件中,因此无法写入文件不会成为问题。对于一个 MAP_SHARED 映射来讲,唯一与 O_RDONLY 兼容的内存保护是 PROT_REA 和 (PROT_READ | PROT_EXEC)。这是符合逻辑的,因为一个 PROT_WRITE, MAP_SHARED 映射允许更新被映射的文件。

5 同步映射区域:msync()

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

msync()系统调用让应用程序能够显式地控制何时完成共享映射与映射文件之间的同步。同步一个映射与底层文件在多种情况下都是非常有用的。如,为确保数据完整性,一个数据库应用程序可能会调用 msync()强制将数据写入到磁盘上。调用 msync() 还允许一个应用程序确保在可写入映射上发生的更新会对在该文件上执行 read()的其他进程可见。

c 复制代码
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);

传给 msync()的 addr 和 length 参数指定了需同步的内存区域的起始地址和大小。在 addr中指定的地址必须是分页对齐的,length 会被向上舍入到系统分页大小的下一个整数倍。

flags 参数的可取值为下列值中的一个:

  • MS_SYNC
    执行一个同步的文件写入。这个调用会阻塞直到内存区域中所有被修改过的分页被写入到底盘为止。
  • MS_ASYNC
    执行一个异步的文件写入。内存区域中被修改过的分页会在后面某个时刻被写入磁盘并立即对在相应文件区域中执行 read()的其他进程可见。
    另一种区分这两个值的方式可以表述为在 MS_SYNC 操作之后,内存区域会与磁盘同步,而在 MS_ASYNC 操作之后,内存区域仅仅是与内核高速缓冲区同步。
  • MS_INVALIDATE
    使映射数据的缓存副本失效。当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底层文件不一致的分页会被标记为无效。当下次引用这些分页时会从文件的相应位置处复制相应的分页内容,其结果是其他进程对文件做出的所有更新将会在内存区域中可见。
    与很多其他现代 UNIX 实现一样,Linux 提供了一个所谓的同一虚拟内存系统。这表示内存映射和高速缓冲区块会尽可能地共享同样的物理内存分页。因此通过映射获取的文件视图与通过 I/O 系统调用(read()、write()等)获得的文件视图总是一致的,而 msync()的唯一用途就是强制将一个映射区域中的内容写入到磁盘。

6 其他mmap()标记

除了 MAP_PRIVATE 和 MAP_SHARED 之外,Linux 允许在 mmap() flags 参数中包含其他一些值(取 OR)。表 49-3 对这些值进行了总结。除了 MAP_PRIVATE 和 MAP_SHARED 之外,在 SUSv3 中仅规定了 MAP_FIXED 标记。

7 匿名映射

匿名映射是没有对应文件的一种映射。本节将介绍如何创建匿名映射以及私有和共享匿名映射的用途。

7.1 MAP_ANONYMOUS 和/dev/zero

在 Linux 上,使用 mmap()创建匿名映射存在两种不同但等价的方法。

  • 在 flags 中指定 MAP_ANONYMOUS 并将 fd 指定为−1。(在 Linux 上,当指定了MAP_ANONYMOUS 之后会忽略 fd 的值。)
  • 打开/dev/zero 设备文件并将得到的文件描述符传递给 mmap()。
    /dev/zero 是一个虚拟设备,当从中读取数据时它总是会返回 0,而写入到这个设备中的数据总会被丢弃。/dev/zero 的一个常见用途是使用 0 来组装一个文件。

不管是使用 MAP_ANONYMOUS 还是使用/dev/zero 技术,得到的映射中的字节会被初始化为 0。在两种技术中,offset 参数都会被忽略(因为没有底层文件,所以也无从指定偏移量)。

7.2 MAP_PRIVATE 匿名映射

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, o);
if(addr == MAP_FAILED)
	errExit("mmap");

7.3 MAP_SHARED 匿名映射

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

下面的代码使用 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()创建的子进程会继承映射,两个进程就会共享内存区域。

7.4 示例程序

程序演示了如何使用 MAP_ANONYMOUS 或/dev/zero 技术来在父进程和子进程之间共享一个映射区域。至于到底该选择何种技术则由在编译程序时是否定义了USE_MAP_ANON 来确定。父进程在调用 fork()之前将共享区域中的一个整数初始化为 1。然后子进程递增这个共享整数并退出,而父进程则等待子进程退出,然后打印出该整数的值。运行这个程序之后能看到下面这样的输出。

bash 复制代码
$ ./anon_mmap
Child started, value = 1
In parent, value = 2
c 复制代码
/*在父进程和子进程之间共享一个匿名映射
mmap/anon_mmap.c
   Demonstrate how to share a region of mapped memory between a parent and
   child process without having to create a mapped file, either through the
   creation of an anonymous memory mapping or through the mapping of /dev/zero.
*/
#ifdef USE_MAP_ANON
#define _BSD_SOURCE             /* Get MAP_ANONYMOUS definition */
#endif
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    int *addr;                  /* Pointer to shared memory region */

    /* Parent creates mapped region prior to calling fork() */

#ifdef USE_MAP_ANON             /* Use MAP_ANONYMOUS */
    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

#else                           /* Map /dev/zero */
    int fd;

    fd = open("/dev/zero", O_RDWR);
    if (fd == -1)
        errExit("open");

    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (close(fd) == -1)        /* No longer needed */
        errExit("close");
#endif

    *addr = 1;                  /* Initialize integer in mapped region */

    switch (fork()) {           /* Parent and child share mapping */
    case -1:
        errExit("fork");

    case 0:                     /* Child: increment shared integer and exit */
        printf("Child started, value = %d\n", *addr);
        (*addr)++;
        if (munmap(addr, sizeof(int)) == -1)
            errExit("munmap");
        exit(EXIT_SUCCESS);

    default:                    /* Parent: wait for child to terminate */
        if (wait(NULL) == -1)
            errExit("wait");
        printf("In parent, value = %d\n", *addr);
        if (munmap(addr, sizeof(int)) == -1)
            errExit("munmap");
        exit(EXIT_SUCCESS);
    }
}

8 重新映射一个映射区域: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, ...);

old_address 和 old_size 参数指定了需扩展或收缩的既有映射的位置和大小。在 old_address中指定的地址必须是分页对齐的,并且通常是一个由之前的 mmap()调用返回的值。映射预期的新大小会通过 new_size 参数指定。在 old_size 和 new_size 中指定的值都会被向上舍入到系统分页大小的下一个整数倍。

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

  • MREMAP_MAYMOVE
    如果指定了这个标记,那么根据空间要求的指令,内核可能会为映射在进程的虚拟地址空间中重新指定一个位置。如果没有指定这个标记,并且在当前位置处没有足够的空间来扩展这个映射,那么就返回 ENOMEM 错误。
  • MREMAP_FIXED
    这个标记只能与 MREMAP_MAYMOVE 一起使用。它在 mremap()中所起的作用与MAP_FIXED 在 mmap()(49.10 节)中所起的作用类似。如果指定了这个标记,那么 mremap()会接收一个额外的参数 void *new_address,该参数指定了一个分页对齐的地址,并且映射将会被迁移至该地址处。所有之前在由 new_address 和 new_size 确定的地址范围之内的映射将会被解除映射。
相关推荐
☆凡尘清心☆1 天前
CentOS Stream 9 源码编译搭建 Zabbix7.0.29 完整部署方案(部署成功)
linux·运维·centos
Escalating_xu1 天前
【Linux】进程间通信:从匿名管道、FIFO 到共享内存、消息队列与信号量
java·linux·面试
予昊1 天前
Linux 资源控制实战分析:用 LXC 亲手造一个 “迷你虚拟机“
linux·运维·服务器
allforgood1 天前
管理网络安全
linux
灵晔君1 天前
【Linux】进程控制(一)——进程创建、进程终止与进程等待
linux·c语言·算法
wsad05321 天前
CentOS 10 上使用 Docker Compose 部署 Discuz X5.0 论坛完整教程
linux·docker·centos·discuz
张宇Joaquin1 天前
openEuler编译和更换内核的方法
java·linux·服务器
小此方1 天前
Re:Linux系统篇(五十六)线程篇 · 九:基于阻塞队列的生产者消费者模型与条件变量
linux·运维·服务器
迷途之人不知返1 天前
自动化工程的构建
linux·自动化
Escalating_xu1 天前
【Linux】库制作与链接原理:从静态库、动态库到 ELF、重定位、GOT 与 PLT
linux·运维·服务器