本文总结内存映射相关接口的用法和注意事项。

1.1 内存映射IO的实现方法

内存映射IO(memory-mapped I/O)将一个磁盘中的文件映射到内存的一个缓冲区中,从而可以通过操作内存中的缓冲区达到操作文件的目的。例如读取映射在内存中的缓冲区,就等同于读取对应的磁盘文件。

1.1.1 内存映射IO的优缺点

优点:

  1. 读写文件直接编程操作缓冲区,更加简单;
  2. 对于两个文件之间的读写操作而言,直接进行文件读写操作的话,read需要先将内核缓冲区中的内容读到应用缓冲区(read_buffer),然后write再将应用缓冲区(write_buffer)的数据拷贝到内核缓冲区。若使用内存映射IO,则直接将数据从内核的一个缓冲区拷贝到另外一个缓冲区即可,开销很小(开销来自于可能发生缺页中断)。

1.2 内存映射IO相关的接口

1.2.1 内存映射IO相关的函数声明

1
2
3
4
5
#include <sys/mman.h>
void* mmap(void* addr, size_t length, int prot, int flag, int fd, off_t offset);
int mprotect(void *addr, size_t len, int prot);
int msync(void *addr, size_t length, int flags);
int munmap(void *addr, size_t length);

1.2.2 mmap函数

mmap函数参数说明:

  1. addr参数用于指定映射内存区的起始地址。通常设置为NULL,表示由系统选择该映射区的首地址。
  2. length参数用于指定要映射的内存区域的长度。length必须是虚拟内存页面的整数倍。
  3. prot用于指定内存映射区域的保护方式:
    • PROT_READ:映射区可读
    • PROT_WRITE:映射区可写
    • PROT_EXEC:映射区可执行
    • PROT_NONE:映射区不可访问
  1. prot可以设置为PROT_NONE,也可以设置为PROT_READ,PROT_WRITE, PROT_EXEC一个或者多个组合按位或。
  2. 对指定内存映射区的保护不能超过被映射文件打开时的访问权限。例如文件是只读打开的,那么对映射内存区就不能指定PROT_WRITE属性。
  3. 当prot设置为PROT_NONE时:这块映射的缓冲区不可读写,读写时会触发SIGSEGV。 用于实现对特定区域的保护,例如可以检测内存越界。典型应用:pthread_attr_setguardsize/pthread_attr_getguardsize.
  4. prot的属性PROT_EXEC的作用是:将内存映射区域的内容当作CPU可以执行的的机器指令进行执行。
  1. flag参数:用于设置映射区域的属性。

    • MAP_SHARED:该属性表示共享当前映射的内存区。映射区域的更新对其它映射了同一文件区域的进程可见。更新映射区域就会更新对应的映射文件。
    • MAP_PRIVATE: 该属性表示创建一个私有的映射副本。映射区域的更新对其它映射了同一文件区域的进程不可见。更新映射区域也不会更新对应的映射文件。任何修改都只影响映射文件的副本,而不影响源文件。
    1. 必须指定MAP_SHARED或者MAP_PRIVATE其中的一个,但是不能同时指定。
  2. fd参数:指定要被映射的文件描述符。注意,在进行文件映射之前,文件必须先打开。但是关闭文件描述符并不会解除映射区。

  3. offset参数:offset为被映射的文件中偏移,offset必须是虚拟内存页面的整数倍。虚拟内存页面的大小通过sysconf(_SC_PAGE_SIZE)获取。

    • 若offset或者length不是内存页面大小的整数倍,操作系统如何处理?例如文件长度96字节,系统页面大小4096字节,则系统会提供4096字节的内存映射区,其中4000字节会被设置为0。虽然可以在映射区中修改4000字节区域,但是修改不会影响到被映射的文件。

1.2.3 mprotect()函数

函数参数说明:

  1. addr必须是内存页面的整数倍。
  2. mprotect()函数用于修改调用它的进程的访问指定页面中数据的访问权限。如果调用mprotect()函数的进程尝试违反设置的保护方式访问指定的内存区域,则操作系统会给进程返回一个SIGSEGV。利用这特性,我们可以用来定位踩内存问题。
  3. 可以通过mprotect()函数来修改内存映射区的保护属性。prot参数和mmap中的参数相同。

1.2.4 msync()函数

函数参数和功能说明:

  1. addr必须是内存页面的整数倍。
  2. flags参数说明:
    • MS_ASYNC: 函数执行更新操作后立刻返回。
    • MS_SYNC: 函数需要等待写操作完成才返回。函数必须指定M_ASYNC或M_SYNC中的一个。
    • MS_INVALIDATE:

msync()函数的用途说明:

  1. 如果是用MAP_SHARED标识进行的内存映射,当映射区更新后,修改并不会立刻写会到被映射文件中。而何时将修改的脏页写会磁盘取决于操作系统内核的策略。
  2. 脏页写回的策略是:只要某个页面有一个字节被修改,整个页面都会被写回。
  3. 如果内存映射区被修改,可以调用msync()函数将脏页下刷到被映射的文件中。

1.2.5 munmap()函数

函数功能说明: munmap()用于解除内存和文件的映射关系。
相关特性说明:

  1. 当进程退出时会自动解除内存映射。
  2. 关闭内存映射时的文件描述符并不能解除和文件的内存映射关系。
  3. 调用munmap()函数并不会将内存映射区的内容更新到被映射的文件呢中。
  4. 当内存映射区解除映射后,映射时设置为MAP_PRIVATE的内存区的修改会被丢弃。

1.2.6 映射区的一些特性

  1. 与映射区相关的信号

    SIGSEGV:信号SIGSEGV通常在进程访问到它不可用的内存区时产生。例如映射区被设置为只读时,进程若尝试修改这个映射区是就会触发SIGSEGV。或者试图访问设置了PROT_NONE的映射区域。

    SIGBUS:如果映射区的某部分在访问时不存在,会触发SIGBUS信号。

  2. 子进程可以通过fork基础父进程的内存映射区。但是不能通过exec继承映射区。

1.3 内存映射IO的使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>

#define COPYINCR (4*1024)
int main(int argc, char** argv)
{
    int fdin;
    int fdout;
    void* src;
    void* dst;
    size_t copysz;
    struct stat sbuf;
    off_t fsz = 0;

    if (argc != 3) {
        printf("input argv invalid(%d).\n", argc);
        exit(1);
    }

    if ((fdin = open(argv[1], O_RDONLY)) < 0) {
        printf("open file(%s) failed, errno(%d).\n", argv[1], errno);
        exit(1);
    }

    if (fstat(fdin, &sbuf) < 0) {
        printf("get file(%s) stat failed, errno(%d).\n", argv[1], errno);
        exit(1);
    }

    if ((fdout = open(argv[2], O_CREAT | O_RDWR | O_TRUNC)) < 0) {
        printf("open file(%s) failed, errno(%d).\n", argv[2], errno);
        exit(1);
    }

    if (ftruncate(fdout, sbuf.st_size) < 0) {
        printf("change file(%s) size failed, errno(%d).\n", argv[2], errno);
        exit(1);
    }

    while (fsz < sbuf.st_size) {
        if (sbuf.st_size - fsz > COPYINCR) {
            copysz = COPYINCR; 
        } else {
            copysz = sbuf.st_size - fsz;
        }

        src = mmap(NULL, copysz, PROT_READ, MAP_PRIVATE, fdin, fsz);
        if (src == MAP_FAILED) {
            printf("mmap file(%s) offset(%ld)len(%ld) size failed, errno(%d).\n", argv[1], fsz, copysz, errno);
            exit(1);
        }

        dst = mmap(NULL, copysz, PROT_WRITE, MAP_SHARED, fdout, fsz);
        if (src == MAP_FAILED) {
            printf("mmap file(%s) offset(%ld)len(%ld) size failed, errno(%d).\n", argv[2], fsz, copysz, errno);
            exit(1);
        }

        memcpy(dst, src, copysz);

        munmap(src, copysz);
        munmap(dst, copysz);
        fsz += copysz;
    }

    return 0;
}