mmap
是一种UNIX系统调用,它将一个文件或者其他对象映射进内存。通过这种方式,文件可以像访问普通内存一样被访问,而不需要像read
或write
这样的系统调用。这种方法有一些显著的优点,但也有一些潜在的缺点和注意事项。
mmap
的速度相比传统的文件I/O操作(如read
和write
)有一些优势:
mmap
通过内存映射的方式,避免了数据在用户空间和内核空间之间的多次拷贝,因为它直接在内存中进行操作。mmap
通常是惰性的,它只会在实际访问某个内存区域时才加载那部分数据到内存中,这可以节省内存,并且对于大文件而言,可以提高效率。尽管mmap
有许多优点,但也有一些缺点:
mmap
可能会增加程序的复杂性,因为需要处理更多与内存管理相关的问题,比如页面错误(page faults)和同步问题。msync
),否则可能会丢失数据。当使用mmap
映射文件到内存之后,会得到一个指向内存中文件数据起始位置的指针。如果解除映射(通过munmap
),然后重新映射同一个文件,可能会得到一个不同的指针。这是因为操作系统可能会将文件映射到进程的不同地址空间部分。因此,不能依赖于映射之间的指针保持不变,每次映射后都应该使用返回的新指针。
使用 mmap 创建的内存映射通常与底层文件系统的页面缓存(page cache)相关联。这意味着,当对映射的内存区域进行写操作时,这些更改最终会反映到映射的文件上。但是,这个过程是由操作系统控制的,具体来说,是由操作系统的虚拟内存管理器和页面写回策略决定的。
当程序正常运行时,对映射内存的更改可能会留在内存中一段时间(在页面缓存中),并且不会立即写回到磁盘。操作系统会在适当的时候,根据需要将更改的页面写回磁盘。这个过程称为“延迟写入”(delayed write)或“惰性写入”(lazy write)。
如果程序崩溃,操作系统的页面缓存仍然保留了对应的更改。因为页面缓存是由操作系统管理的,所以即便是程序崩溃,操作系统仍然会在之后的某个时刻将脏页面(已修改的页面)写回到磁盘。这意味着,理论上,即使程序崩溃,通过 mmap 进行的写入也会最终被保存到磁盘。
然而,这里有几个注意点:
因此,尽管操作系统会尝试在程序崩溃后将更改写入磁盘,但依赖于这种行为来保证数据一致性和持久性通常不是一个好的做法。如果数据的完整性对的应用程序来说非常重要,应该使用适当的同步机制来确保数据在必要时被写入磁盘。
在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
目录是存在的,并且有足够的权限在那里创建和写入文件。
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
}
}
代码解释
LargeData
的大结构体,包含不同类型的字段,如int32
、int64
、float64
和数组。os.OpenFile
打开文件,若文件不存在则创建。使用file.Truncate
调整文件大小,使其与结构体大小一致。syscall.Mmap
将文件映射到内存中,设置为可读可写且共享模式。unsafe.Pointer
将映射的内存首地址转换为LargeData
结构体的指针,这样就可以直接通过该指针访问和修改结构体的字段。largeData
指针指向的结构体内容。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