MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言使用mmap()实现大文件快速读写

2023-10-293.3k 阅读

一、mmap() 函数简介

在Linux系统下,mmap() 函数是一个强大的系统调用,它允许进程将文件或设备映射到内存地址空间中。通过这种映射,进程可以像访问内存一样直接访问文件内容,而不需要传统的read()write() 系统调用。这种方式在处理大文件时具有显著的性能优势,因为它减少了数据从内核空间到用户空间的拷贝次数,同时利用了操作系统的虚拟内存管理机制。

mmap() 函数的原型如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:指向映射区域的起始地址。通常设为 NULL,让内核自动选择合适的地址。
  • length:映射区域的长度,以字节为单位。
  • prot:映射区域的保护权限,常用的取值有:
    • PROT_READ:映射区域可读。
    • PROT_WRITE:映射区域可写。
    • PROT_EXEC:映射区域可执行。
    • PROT_NONE:映射区域不可访问。
  • flags:映射的标志,常用的取值有:
    • MAP_SHARED:对映射区域的写入会反映到文件中,并且其他映射该文件的进程也能看到这些改变。
    • MAP_PRIVATE:对映射区域的写入不会反映到文件中,而是产生一个写时复制(copy - on - write)的副本。
    • MAP_ANONYMOUS:创建一个匿名映射,不与任何文件关联,通常用于进程间共享内存。
  • fd:要映射的文件描述符。
  • offset:文件偏移量,必须是系统内存页大小的整数倍。

mmap() 函数成功时返回映射区域的起始地址,失败时返回 MAP_FAILED(通常为 (void *) - 1)。

二、传统文件读写与 mmap() 的性能差异

2.1 传统文件读写方式

传统的文件读写使用 read()write() 系统调用。例如,读取文件的代码如下:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
        // 处理读取到的数据
    }

    if (bytes_read == -1) {
        perror("read");
    }

    close(fd);
    return 0;
}

在这种方式下,数据从磁盘读取到内核缓冲区,然后再从内核缓冲区拷贝到用户空间的缓冲区。写入操作则相反,数据从用户空间缓冲区拷贝到内核缓冲区,然后再写入磁盘。这种数据在内核空间和用户空间之间的多次拷贝会带来额外的性能开销。

2.2 mmap() 方式

使用 mmap() 时,文件内容直接映射到进程的地址空间,进程可以直接访问文件内容,避免了数据的多次拷贝。例如:

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

int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }

    void *map_start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map_start == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 直接访问映射区域的数据
    char *data = (char *)map_start;
    for (off_t i = 0; i < sb.st_size; i++) {
        // 处理数据
    }

    if (munmap(map_start, sb.st_size) == -1) {
        perror("munmap");
    }
    close(fd);
    return 0;
}

通过 mmap(),文件内容被映射到内存,进程可以像访问普通内存一样访问文件,减少了数据拷贝的开销,从而在处理大文件时能够显著提高性能。

三、使用 mmap() 实现大文件读写的详细步骤

3.1 打开文件

首先,使用 open() 函数打开要读写的文件。例如:

int fd = open("big_file.txt", O_RDWR);
if (fd == -1) {
    perror("open");
    return 1;
}

这里使用 O_RDWR 标志表示以读写方式打开文件。如果文件不存在,可能需要根据需求添加 O_CREAT 等标志并指定文件权限。

3.2 获取文件大小

使用 fstat() 函数获取文件的大小,以便确定映射区域的长度。

struct stat sb;
if (fstat(fd, &sb) == -1) {
    perror("fstat");
    close(fd);
    return 1;
}
off_t file_size = sb.st_size;

fstat() 函数将文件的相关信息填充到 struct stat 结构体中,我们主要关注 st_size 字段,它表示文件的大小。

3.3 映射文件到内存

使用 mmap() 函数将文件映射到内存。

void *map_start = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_start == MAP_FAILED) {
    perror("mmap");
    close(fd);
    return 1;
}

这里将文件映射为可读可写(PROT_READ | PROT_WRITE)且共享(MAP_SHARED)的内存区域。如果是只读操作,可以只设置 PROT_READ 权限。

3.4 读写映射区域

映射成功后,就可以像访问普通内存一样对映射区域进行读写操作。例如,将字符串写入映射区域:

char *data = (char *)map_start;
const char *str = "Hello, mmap!";
for (size_t i = 0; i < strlen(str); i++) {
    data[i] = str[i];
}

读取操作类似,直接从映射区域读取数据即可。

3.5 解除映射并关闭文件

完成读写操作后,需要使用 munmap() 函数解除映射,并使用 close() 函数关闭文件。

if (munmap(map_start, file_size) == -1) {
    perror("munmap");
}
close(fd);

munmap() 函数将映射区域从进程的地址空间中移除,close() 函数关闭文件描述符,释放相关资源。

四、mmap() 使用中的注意事项

4.1 权限设置

在设置 mmap()prot 参数时,要确保权限设置合理。如果设置的权限与文件打开时的权限不匹配,可能会导致映射失败。例如,如果文件以只读方式打开(O_RDONLY),却在 mmap() 中设置了 PROT_WRITE 权限,就会出现问题。

4.2 内存管理

虽然 mmap() 提供了方便的文件映射方式,但也要注意内存管理。映射的内存区域大小要根据实际需求合理设置,过大的映射区域可能会消耗过多的系统内存,导致系统性能下降。另外,在解除映射(munmap())之前,确保所有对映射区域的操作已经完成,否则可能会导致数据丢失或内存错误。

4.3 同步问题

当使用 MAP_SHARED 标志时,对映射区域的修改会反映到文件中。但在多进程或多线程环境下,需要注意同步问题,以避免数据竞争。可以使用互斥锁(pthread_mutex_t)等同步机制来确保数据的一致性。

4.4 文件偏移量

mmap()offset 参数必须是系统内存页大小的整数倍。在某些情况下,需要根据文件的实际结构和需求来设置合适的偏移量。例如,如果文件有特定的头部结构,可能需要跳过头部后再进行映射。

五、完整代码示例

下面是一个完整的使用 mmap() 进行大文件读写的示例代码,包括写入和读取操作。

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

#define FILE_NAME "big_file.txt"

int main() {
    // 以读写方式打开文件,如果文件不存在则创建
    int fd = open(FILE_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 设置文件大小为 1024 字节
    if (lseek(fd, 1023, SEEK_SET) == -1) {
        perror("lseek");
        close(fd);
        return 1;
    }
    if (write(fd, "", 1) == -1) {
        perror("write");
        close(fd);
        return 1;
    }

    // 获取文件大小
    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }
    off_t file_size = sb.st_size;

    // 映射文件到内存
    void *map_start = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map_start == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 写入数据到映射区域
    char *data = (char *)map_start;
    const char *write_str = "This is a test string written by mmap.";
    for (size_t i = 0; i < strlen(write_str); i++) {
        data[i] = write_str[i];
    }

    // 从映射区域读取数据
    char read_buffer[1024];
    for (size_t i = 0; i < strlen(write_str); i++) {
        read_buffer[i] = data[i];
    }
    read_buffer[strlen(write_str)] = '\0';
    printf("Read data: %s\n", read_buffer);

    // 解除映射
    if (munmap(map_start, file_size) == -1) {
        perror("munmap");
    }
    close(fd);

    return 0;
}

这个示例代码首先创建一个大小为 1024 字节的文件,然后将其映射到内存,向映射区域写入字符串,再从映射区域读取数据并打印,最后解除映射并关闭文件。

六、性能测试与分析

为了更直观地了解 mmap() 在大文件读写方面的性能优势,我们可以进行一个简单的性能测试。分别使用传统的 read()write() 以及 mmap() 来读写一个大文件,记录它们的运行时间。

6.1 传统读写方式的性能测试代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>

#define BUFFER_SIZE 1024
#define FILE_NAME "big_file.txt"

double get_time() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec / 1000000.0;
}

int main() {
    int fd = open(FILE_NAME, O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[BUFFER_SIZE];
    double start_time = get_time();

    // 读取文件
    lseek(fd, 0, SEEK_SET);
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
        // 不进行实际处理,仅模拟读取
    }
    if (bytes_read == -1) {
        perror("read");
    }

    // 写入文件
    lseek(fd, 0, SEEK_SET);
    const char *write_str = "A" ;
    size_t write_len = strlen(write_str);
    for (size_t i = 0; i < 10000; i++) {
        if (write(fd, write_str, write_len) != write_len) {
            perror("write");
        }
    }

    double end_time = get_time();
    printf("Traditional read - write time: %f seconds\n", end_time - start_time);

    close(fd);
    return 0;
}

6.2 mmap() 方式的性能测试代码

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

#define FILE_NAME "big_file.txt"

double get_time() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec / 1000000.0;
}

int main() {
    int fd = open(FILE_NAME, O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }
    off_t file_size = sb.st_size;

    void *map_start = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map_start == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    double start_time = get_time();

    // 读取映射区域
    char *data = (char *)map_start;
    for (off_t i = 0; i < file_size; i++) {
        // 不进行实际处理,仅模拟读取
    }

    // 写入映射区域
    const char *write_str = "A";
    size_t write_len = strlen(write_str);
    for (size_t i = 0; i < 10000; i++) {
        for (size_t j = 0; j < write_len; j++) {
            data[(i * write_len)+j] = write_str[j];
        }
    }

    double end_time = get_time();
    printf("mmap read - write time: %f seconds\n", end_time - start_time);

    if (munmap(map_start, file_size) == -1) {
        perror("munmap");
    }
    close(fd);
    return 0;
}

通过多次运行这两个测试代码,并对结果进行统计分析,可以发现,在处理大文件时,mmap() 方式通常比传统的 read()write() 方式要快很多。这主要是因为 mmap() 减少了数据在内核空间和用户空间之间的拷贝次数,利用了操作系统的虚拟内存管理机制,从而提高了文件读写的效率。

七、mmap() 在实际项目中的应用场景

7.1 大数据处理

在大数据处理领域,经常需要处理非常大的数据集。例如,日志分析系统可能需要处理几十GB甚至更大的日志文件。使用 mmap() 可以快速地将这些大文件映射到内存,然后进行高效的分析和处理,避免了频繁的数据拷贝带来的性能开销。

7.2 数据库系统

数据库系统中,数据文件通常非常大。mmap() 可以用于将数据库文件映射到内存,使得数据库引擎可以直接在内存中操作数据,提高数据的读写效率。同时,结合 MAP_SHARED 标志,多个数据库进程可以共享这些映射区域,实现数据的共享和同步。

7.3 文件备份与恢复

在文件备份和恢复工具中,mmap() 可以用于快速读取源文件并写入备份文件。由于其高性能的读写特性,可以大大缩短备份和恢复的时间,提高系统的可用性。

八、总结 mmap() 的优势与局限

8.1 优势

  • 高性能:减少数据拷贝次数,直接在内存中操作文件内容,显著提高大文件读写性能。
  • 简单易用:通过映射,将文件操作转化为内存操作,代码实现相对简单,逻辑清晰。
  • 内存共享:使用 MAP_SHARED 标志可以实现多进程间的内存共享,方便进程间通信和数据共享。

8.2 局限

  • 内存限制:映射的内存区域大小受系统内存限制,如果映射过大的文件,可能会导致系统内存不足,影响系统性能。
  • 同步问题:在多进程或多线程环境下,需要额外的同步机制来保证数据的一致性,增加了编程的复杂性。
  • 文件系统依赖mmap() 的性能和行为可能会受到底层文件系统的影响,不同的文件系统在支持和优化 mmap() 方面可能存在差异。

尽管存在一些局限性,但在处理大文件读写以及需要高效内存共享的场景中,mmap() 仍然是一个非常强大和实用的工具,在Linux C语言开发中具有重要的地位。通过合理地使用 mmap(),可以显著提升程序的性能和效率。