mmap 基础

Posted by Max on October 17, 2018

1. 背景

在优化一个文本处理工具,处理线程的增多,并不能线性增加总体处理速度。使用 vtune 分析函数时间占用,发现读文件线程的 fgets 占比很高。考虑对这一过程进行优化。

2. 概述

系统调用 read() 从读取文件时,首先在地址空间查找要请求的文件页是否已经缓存在页缓存中,如果不存在则通过文件索引节点定位到文件磁盘地址,将数据从磁盘复制到页缓存中。页缓存处于内核空间,不能被用户进程直接寻址。所以还需要将页缓存中的数据页再次拷贝到进程对应的用户空间完成读文件操作。类似的,用户空间中待写入的 buffer 在内核空间中也不能直接访问,必须先拷贝到内核空间对应的主存,再回写到磁盘中,也是两次拷贝完成。

mmap 可以将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的映射关系。此时,进程可以采用指针的方式操作这一块内存,而系统会自动回写脏页面到对应的文件磁盘上,跳过 read、write 等系统调用函数完成对文件的读写操作。

网上找到一张 linux 2.6 之前版本内核的进程标准地址空间布局图:

linux_address_space

使用 mmap 映射的区域位于上图中用户空间的 Memory Mapping Segment。系统的进程地址空间大小为 2^BIT_PER_LONG,在 PAGE_OFFSET 处划分为随每个进程变化的用户空间和保持不变的内核空间。x86 架构下 32 位系统的用户空间地址仅为 3G,64 位系统目前可以不考虑限制。

3. 机制

mmap 内存映射的实现过程,可分为三个阶段:

  1. 进程在用户空间调用 mmap 启动映射过程,并在虚拟空间中为映射创建虚拟映射区域
  2. 调用内核空间的系统调用函数 mmap,实现文件的物理地址和进程的虚拟地址的映射关系
  3. 进程发起对这片映射空间的访问,产生缺页异常,实现文件内容到物理内存的拷贝

相较于传统读文件方式,mmap 跨过了页缓存,减少了数据的拷贝操作;实现了用户空间和内核空间的高效交互方式;提供了进程间共享内存以及相互通信的方式。

但是,正是由于调用 mmap 没有进行从用户态到内核态的切换,这样采用 LRU 更新策略的内核页面没法让这个被访问的页面的热度增加,使得 LRU 很容易地将其更新,从而降低 cache 命中率。

此外,内核在不命中 cache 的情况下从磁盘读数据时会加一把 mm 级别的锁,在线程模型下会存在严重的互斥现象。若有一种阻塞的方式让内核预读磁盘,保证 mmap 时要读的数据在内核中,可以尽可能的避免这把锁。

4. 函数原型

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
  • addr : 映射区起始地址,若为 null,则有内核决定创建的起始地址;否则(linux)自动选择靠近 addr 的页边界作为开始
  • length : 映射区长度
  • prot : 期望的内存保护标志,不能与文件打开模式冲突

    • PROT_EXEC 可执行
    • PROT_READ 可读
    • PROT_WRITE 可写
    • PROT_NONE 不可访问
  • flags : 指定映射对象的类型

    • MAP_FIXED 使用指定区域创建映射,会覆盖指定区域已有的映射
    • MAP_PRIVATE 创建一份写时拷贝的私有映射
    • MAP_SHARED 与其他进程共享映射空间
    • 更多类型可查询 man
  • fd : 待映射的文件描述符
  • offset : 被映射内容在指定文件中的偏移量,须是页的整数倍

执行成功,mmap() 返回映射区指针;否则返回 MAP_FAILED (即(void *) -1)。munmap() 成功返回 0, 失败返回 -1. 二者都可以查看 errno 确认失败原因。

此外,这两个函数是多线程安全的。

需要注意是,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位的,因此 mmap 从磁盘到虚拟地址空间的映射也必须是页。MAP_FIXED 模式下 映射的起始地址 addr 必须要页对齐。映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。

5. 示例

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

#define BUF_SIZE 100

int main(int argc, char **argv)
{
    int fd, nread, i;
    struct stat sb;
    char *mapped, buf[BUF_SIZE];

    for (i = 0; i < BUF_SIZE; i++) {
        buf[i] = '#';
    }

    /* 打开文件 */
    if ((fd = open(argv[1], O_RDWR)) < 0) {
        perror("open");
    }

    /* 获取文件的属性 */
    if ((fstat(fd, &sb)) == -1) {
        perror("fstat");
    }

    /* 将文件映射至进程的地址空间 */
    if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
        perror("mmap");
    }

    /* 映射完后, 关闭文件也可以操纵内存 */
    close(fd);

    printf("%s", mapped);

    /* 修改一个字符,同步到磁盘文件 */
    mapped[20] = '9';
    if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) {
        perror("msync");
    }

    /* 释放存储映射区 */
    if ((munmap((void *)mapped, sb.st_size)) == -1) {
        perror("munmap");
    }

    return 0;
}

参考

  1. 认真分析mmap:是什么 为什么 怎么用
  2. mmap详解
  3. 从fread和mmap谈C++读文件的性能
  4. Linux的IO系统常用系统调用及分析