mmap 系统调用初步

字数: 1074

概论

mmap 及其配套的 munmap 用于进行内存映射的系统调用,mmap 本身就是内存映射(memory map)的缩写。它主要用于将文件或设备映射到内存的地址空间,提高 I/O 效率。

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

对于 mmap() 函数的参数来说:

  • addr : 要映射的地址,通常填 NULL 让操作系统选择合适的内存空间。
  • length : 映射区域的长度,以字节为单位通常为页大小的倍数。
  • prot : 映射区域的访问权限比如有读写和执行,用宏来进行确定(PROT_READPROT_WRITEPROT_EXEC……)
  • flags : 映射类型与行为,也是用宏进行确定(MAP_SHAREDMAP_PRIVATEMAP_ANONYMOUS……)
  • fd : 需要映射的文件描述符
  • offset : 文件映射到起始偏移

简单文件读写示例

 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
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    // 打开文件
    int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
    if (fd == -1)
    {
        perror("open failed");
        exit(1);
    }

    int file_size = 1024;
    ftruncate(fd, file_size);

    char *addr = mmap(NULL, file_size, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap failed");
        close(fd);
        exit(1);
    }
    strcpy(addr, "Test mmap!");
    printf("读取映射内容: %s\n", addr);

    if (munmap(addr, file_size) == -1)
        perror("munmap failed");

    close(fd);
    return 0;
}

这个程序通过 mmap 进行简单的文件读写,在实际使用中,对于大文件的读写通过 mmap 可以避免频繁的使用 write read 系统调用在内核态和用户态的切换提升读写性能。

在 prot 一项中使用 : PROT_WRITE | PROT_READ 来确定该段内存空间的权限。
通过 pwndbg 的 vmmap 可以较为清楚的看到在 0x7ffff7fbc000 ~ 0x7ffff7fbd000 这段 1024 字节的区域被划分给了 test.txt 使用。权限为 rw-p 。

缺页中断(Page Fault)

mmap 只是在内存中找到合适的空闲虚拟内存区域然后进行映射,可以在上图的 vmmap 中看到一段虚拟内存被跨定好了,但其实还未与物理内存进行关联。内核直到我们需要使用这块内存的时候会触发缺页中断分配加载物理内存。
众所皆知,虚拟内存在物理内存上以页为单位进行分配,每页通常为 4KB 或更大。通过页的方式在页表中对应映射。也就是在此时,内核发现数据没有在内存,开始运行缺页中断让一切恢复秩序。

堆内存分配中的 mmap

当 mmap 不将地址空间映射到某个文件时,又称这块空间为匿名空间,匿名空间可以拿来做堆空间。 glibc 的 malloc 函数对于小于 128 kb 的请求会在现有的堆空间里安装堆分配算法为它分配一块空间并返回,对于大于 128 kb 的请求来说,它会使用 mmap() 函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间。
可以用 mmap 简单的实现 malloc() 函数:

1
2
3
4
5
6
7
8
void *malloc(size_t nbytes)
{
    void *ret = mmap(NULL, nbytes, PROT_READ | PROT_WRITE, 
                     MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    if (ret == MAP_FAILED)
        return 0;
    return ret;
}

但是 mmap 从系统虚拟空间申请的起始地址和大小都必须是系统页大小的整数倍,如果是字节数很小的请求这样做很浪费空间。

参考资料

  1. 程序员的自我修养 ——链接、装载与库 电子工业出版社
  2. 深入理解 mmap:原理、用法与实战全解析