mmap
创建:2023-12-06 17:06
更新:2025-04-19 23:05

mmap是一种UNIX系统调用,它将一个文件或者其他对象映射进内存。通过这种方式,文件可以像访问普通内存一样被访问,而不需要像readwrite这样的系统调用。这种方法有一些显著的优点,但也有一些潜在的缺点和注意事项。

mmap的速度

mmap的速度相比传统的文件I/O操作(如readwrite)有一些优势:

  1. 减少拷贝: mmap通过内存映射的方式,避免了数据在用户空间和内核空间之间的多次拷贝,因为它直接在内存中进行操作。
  2. 页面缓存: 映射的文件可以利用操作系统的页面缓存(page cache),这意味着频繁访问的数据可以快速从缓存中获取,而不是每次都从磁盘读取。
  3. 按需加载: mmap通常是惰性的,它只会在实际访问某个内存区域时才加载那部分数据到内存中,这可以节省内存,并且对于大文件而言,可以提高效率。

mmap的缺点

尽管mmap有许多优点,但也有一些缺点:

  1. 文件大小限制: 对于映射的文件,其大小通常受限于虚拟内存的大小,这对于非常大的文件可能是一个问题。
  2. 碎片化: 如果频繁地映射和解除映射小文件或不连续的文件区域,可能会导致地址空间的碎片化。
  3. 复杂性: 使用mmap可能会增加程序的复杂性,因为需要处理更多与内存管理相关的问题,比如页面错误(page faults)和同步问题。
  4. 同步问题: 对于需要写入的映射,确保数据正确同步回文件需要额外的调用(如msync),否则可能会丢失数据。

mmap后指针变化问题

当使用mmap映射文件到内存之后,会得到一个指向内存中文件数据起始位置的指针。如果解除映射(通过munmap),然后重新映射同一个文件,可能会得到一个不同的指针。这是因为操作系统可能会将文件映射到进程的不同地址空间部分。因此,不能依赖于映射之间的指针保持不变,每次映射后都应该使用返回的新指针。

持久化

使用 mmap 创建的内存映射通常与底层文件系统的页面缓存(page cache)相关联。这意味着,当对映射的内存区域进行写操作时,这些更改最终会反映到映射的文件上。但是,这个过程是由操作系统控制的,具体来说,是由操作系统的虚拟内存管理器和页面写回策略决定的。

当程序正常运行时,对映射内存的更改可能会留在内存中一段时间(在页面缓存中),并且不会立即写回到磁盘。操作系统会在适当的时候,根据需要将更改的页面写回磁盘。这个过程称为“延迟写入”(delayed write)或“惰性写入”(lazy write)。

如果程序崩溃,操作系统的页面缓存仍然保留了对应的更改。因为页面缓存是由操作系统管理的,所以即便是程序崩溃,操作系统仍然会在之后的某个时刻将脏页面(已修改的页面)写回到磁盘。这意味着,理论上,即使程序崩溃,通过 mmap 进行的写入也会最终被保存到磁盘。

然而,这里有几个注意点:

  1. 同步调用: 如果想确保数据即时写入磁盘,可以在程序中使用 msync 系统调用来手动触发映射区域的同步。这对于需要数据持久性保证的应用程序来说是非常重要的。
  2. 系统崩溃: 如果整个系统崩溃,比如由于电源故障,那么在系统崩溃的那一刻尚未写回磁盘的更改可能会丢失。
  3. 文件系统的一致性: 某些类型的文件系统可能有自己的缓存机制,这可能会影响数据何时实际写入磁盘。
  4. 崩溃一致性: 如果的应用程序需要在崩溃后保持数据一致性,可能需要使用事务日志或其他形式的恢复机制来确保即使在崩溃后也能恢复到一个一致的状态。

因此,尽管操作系统会尝试在程序崩溃后将更改写入磁盘,但依赖于这种行为来保证数据一致性和持久性通常不是一个好的做法。如果数据的完整性对的应用程序来说非常重要,应该使用适当的同步机制来确保数据在必要时被写入磁盘。

简单使用例子

在Linux下,mmap 系统调用可以将一个文件或者其它对象映射进内存。这样,文件的内容就可以像访问普通内存一样来访问,这通常比传统的read和write系统调用更加高效。

以下是一个使用mmap的简单例子,这个例子中,我们将打开一个文件,将其映射到内存中,并将这块内存的前面几个字节写为一个字符串。然后解除映射,并关闭文件。

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>

int main() {
    const char *filepath = "/tmp/mmap_example.txt";
    const char *text = "Hello, mmap!";
    int length = strlen(text) + 1; // +1 for the null terminator

    // 打开文件
    int fd = open(filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("Error opening file for writing");
        return 1;
    }

    // 调整文件大小
    if (lseek(fd, length-1, SEEK_SET) == -1) {
        close(fd);
        perror("Error calling lseek() to 'stretch' the file");
        return 1;
    }

    // 写入一个空字节,确保文件大小
    if (write(fd, "", 1) == -1) {
        close(fd);
        perror("Error writing last byte of the file");
        return 1;
    }

    // 执行映射
    void *map = mmap(0, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        close(fd);
        perror("Error mmapping the file");
        return 1;
    }

    // 写入数据到映射区域
    memcpy(map, text, length);

    // 同步映射区域到文件
    if (msync(map, length, MS_SYNC) == -1) {
        perror("Could not sync the file to disk");
    }

    // 解除映射
    if (munmap(map, length) == -1) {
        close(fd);
        perror("Error un-mmapping the file");
        return 1;
    }

    // 关闭文件
    close(fd);
    return 0;
}

这个程序首先打开(或创建)一个文件,然后设置其大小以适应我们想要写入的数据。接下来,它创建了一个内存映射,我们可以像使用普通内存一样对其进行读写。最后,它将数据同步回文件,解除映射,并关闭文件。

注意,这个例子没有做任何错误处理,除了检查系统调用是否失败。在实际应用中,应该更仔细地处理可能的错误情况。

还要注意,使用mmap时,需要确保文件的大小至少与想要映射的大小一样大。在这个例子中,我们通过写入一个空字节到文件的末尾来“拉伸”文件至所需的大小。此外,open调用中的O_CREAT标志意味着如果文件不存在,它将被创建,S_IRUSR | S_IWUSR设置了文件的权限,使得用户可以读写文件。

在执行这段代码之前,确保/tmp目录是存在的,并且有足够的权限在那里创建和写入文件。

go版本

package main

import (
    "fmt"
    "os"
    "syscall"
    "unsafe"
)

// LargeData 定义一个大结构体
type LargeData struct {
    Field1 int32
    Field2 int64
    Field3 float64
    Field4 [10]int16
}

func main() {
    // 打开文件,如果文件不存在则创建
    file, err := os.OpenFile("large_data.bin", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println("Failed to open file:", err)
        return
    }
    defer file.Close()

    // 获取结构体的大小
    structSize := int(unsafe.Sizeof(LargeData{}))

    // 调整文件大小以匹配结构体大小
    err = file.Truncate(int64(structSize))
    if err != nil {
        fmt.Println("Failed to truncate file:", err)
        return
    }

    // 进行内存映射
    data, err := syscall.Mmap(int(file.Fd()), 0, structSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
    if err != nil {
        fmt.Println("Failed to mmap:", err)
        return
    }
    defer syscall.Munmap(data)

    // 将映射的内存转换为结构体指针
    largeData := (*LargeData)(unsafe.Pointer(&data[0]))

    // 读取结构体字段的值
    fmt.Printf("Field1: %d, Field2: %d, Field3: %f\n", largeData.Field1, largeData.Field2, largeData.Field3)
    fmt.Printf("Field4: %v\n", largeData.Field4)

    // 修改结构体的值
    largeData.Field1 = 100
    largeData.Field2 = 200
    largeData.Field3 = 300.5
    for i := range largeData.Field4 {
        largeData.Field4[i] = int16(i)
    }

    // 同步内存映射区域到文件
    err = syscall.Msync(data, syscall.MS_SYNC)
    if err != nil {
        fmt.Println("Failed to msync:", err)
        return
    }
}

代码解释

  1. 结构体定义:定义了一个名为LargeData的大结构体,包含不同类型的字段,如int32int64float64和数组。
  2. 文件操作:使用os.OpenFile打开文件,若文件不存在则创建。使用file.Truncate调整文件大小,使其与结构体大小一致。
  3. 内存映射:使用syscall.Mmap将文件映射到内存中,设置为可读可写且共享模式。
  4. 结构体映射:通过unsafe.Pointer将映射的内存首地址转换为LargeData结构体的指针,这样就可以直接通过该指针访问和修改结构体的字段。
  5. 数据读写:可以像操作普通结构体一样读取和修改largeData指针指向的结构体内容。
  6. 同步操作:使用syscall.Msync将内存中的修改同步到文件中。

注意事项

  • unsafe包的使用unsafe包绕过了Go语言的类型系统和内存安全检查,使用时要格外小心,因为错误的使用可能会导致程序崩溃或产生难以调试的问题。
  • 文件大小:确保文件大小与结构体大小一致,否则可能会导致内存访问越界。
  • 内存对齐:不同架构下,结构体的内存对齐方式可能不同,要确保文件内容的布局和结构体的内存布局一致。
  • 错误处理:在文件操作、内存映射和同步过程中,要做好错误处理,保证程序的健壮性。

系统控制

Linux 缓存管理参数
在 Linux 系统中,可以通过调整以下内核参数来控制缓存的写回频率:

# 查看当前的缓存管理参数
cat /proc/sys/vm/dirty_background_ratio  #表示脏页占总内存的比例达到多少时,内核会启动后台写回进程(pdflush)。默认值通常是 10%。
cat /proc/sys/vm/dirty_ratio #表示脏页占总内存的比例达到多少时,所有进程都会被阻塞,直到脏页被写回磁盘。默认值通常是 20%。
cat /proc/sys/vm/dirty_expire_centisecs #表示脏页在内存中可以存在的最大时间(以 1/100 秒为单位)。默认值通常是 3000(即 30 秒)。
cat /proc/sys/vm/dirty_writeback_centisecs  #表示内核每多少时间(以 1/100 秒为单位)检查一次脏页并启动写回进程。默认值通常是 500(即 5 秒)。
示例

# 调整参数(需要 root 权限)
sudo sysctl vm.dirty_background_ratio=5
sudo sysctl vm.dirty_ratio=10
sudo sysctl vm.dirty_expire_centisecs=15000
sudo sysctl vm.dirty_writeback_centisecs=1000