好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 msync 函数,它用于将通过 mmap 映射的内存区域中被修改的页面同步(或刷新)回关联的文件中。


1. 函数介绍 链接到标题

msync 是一个 Linux 系统调用,用于控制和监视通过 mmap 创建的文件映射区域与底层文件之间的数据一致性

当你使用 mmap 将一个文件映射到内存后,对映射区域的读写操作实际上是在操作内存中的页面。内核会通过页面缓存(Page Cache)来管理这些页面,并在适当时机将被修改的“脏页”(Dirty Pages)写回到磁盘上的文件中。这个过程通常是异步的,由内核的回写机制(write-back daemon)在后台完成。

msync 允许程序显式地控制这个同步过程:

  • 强制同步: 要求内核立即将指定范围内的脏页写入磁盘。
  • 等待完成: 可以选择同步调用(等待写入完成)或异步调用(发起写入请求后立即返回)。

这对于需要确保数据持久化(即使在系统崩溃后也能恢复)的应用场景至关重要。

你可以把它想象成一个“保存”按钮。虽然你一直在文档(内存映射区域)中编辑内容,但更改可能只保存在内存中。点击“保存”(调用 msync)会确保你的更改被写入硬盘(底层文件)。


2. 函数原型 链接到标题

#include <sys/mman.h> // 必需

int msync(void *addr, size_t length, int flags);

3. 功能 链接到标题

  • 同步内存到文件: 将从地址 addr 开始、长度为 length 字节的内存映射区域中被修改的页面刷新到关联的文件。
  • 控制同步行为: 通过 flags 参数控制同步是同步(阻塞等待完成)还是异步(发起后立即返回),以及是否使缓存失效。
  • 范围操作: 只同步指定的内存范围,而不是整个映射区域。

4. 参数 链接到标题

  • void *addr: 指向要同步的内存映射区域的起始地址。
    • 重要: 虽然 POSIX 允许 addr 不是页对齐的,但为了可移植性和效率,强烈建议 addr 是系统页大小的整数倍。可以使用 getpagesize() 获取页大小。
  • size_t length: 要同步的内存区域的长度(以字节为单位)。
    • 同样,为了效率,length 最好也是页大小的整数倍。如果不足一页,内核通常会向上舍入到最近的页边界。
  • int flags: 指定同步操作的行为。这是一个位掩码,必须包含以下互斥值之一:
    • MS_SYNC: 同步执行同步操作。调用会阻塞,直到所有指定页面都被写入到底层文件(或发生错误)。
    • MS_ASYNC: 异步执行同步操作。调用会立即返回,内核会在后台启动将页面写回文件的过程。 此外,还可以按位或上以下标志:
    • MS_INVALIDATE: 使缓存中指定页面的其他映射(例如,其他进程对该文件的映射,或同一进程的其他映射)失效。下次访问这些失效的页面时,会从文件重新加载。这个标志在某些场景下有用,但不是总能保证完全失效。

5. 返回值 链接到标题

  • 成功时: 返回 0。
    • 对于 MS_ASYNC,返回 0 仅表示同步请求已成功发起,不保证数据已写入磁盘。
    • 对于 MS_SYNC,返回 0 表示数据已(应该)被写入磁盘。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL addr 无效或未映射,ENOMEM 地址范围超出映射区域等)。

6. 相似函数,或关联函数 链接到标题

  • fsync: 将指定文件描述符对应文件的所有缓冲区数据(包括元数据)刷新到磁盘。它作用于文件描述符,而不是内存地址。
  • fdatasync: 类似于 fsync,但只刷新文件数据部分,而不一定刷新所有元数据(如访问时间 atime),因此可能更快。
  • mmap: 创建内存映射,是 msync 操作的目标。
  • munmap: 在调用 munmap 解除映射之前,如果需要确保修改已写入文件,应该先调用 msync

7. 示例代码 链接到标题

示例 1:使用 msync 确保数据写入文件 链接到标题

这个例子演示了如何使用 mmap 映射一个文件,修改映射区域的内容,然后使用 msync 确保修改被写入磁盘。

#define _GNU_SOURCE // 可能需要,取决于系统
#include <sys/mman.h> // mmap, munmap, msync
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open, O_RDWR, O_CREAT
#include <unistd.h>   // close, fstat, getpagesize
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <string.h>   // strcpy, strlen
#include <time.h>     // time

int main() {
    const char *filename = "msync_example.txt";
    int fd;
    struct stat sb;
    char *mapped;
    size_t length;
    const char *new_data = "Data written at: ";
    char timestamp[100];
    time_t now;

    // 1. 以读写模式创建/打开文件
    fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 2. 写入一些初始数据以确保文件有一定大小
    // 或者使用 ftruncate 来设置文件大小
    const char *initial_content = "Initial content.\n";
    if (write(fd, initial_content, strlen(initial_content)) == -1) {
        perror("write initial content");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 3. 获取文件大小
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }
    length = sb.st_size;
    printf("File '%s' initial size: %zu bytes\n", filename, length);

    // 4. 确保文件至少有一页大小,方便演示 (可选)
    long pagesize = getpagesize();
    if (length < (size_t)pagesize) {
        if (ftruncate(fd, pagesize) == -1) {
            perror("ftruncate to page size");
            close(fd);
            exit(EXIT_FAILURE);
        }
        length = pagesize;
        printf("File truncated to one page size: %ld bytes\n", pagesize);
    }

    // 5. 创建内存映射 (MAP_SHARED 是关键,修改才会写回文件)
    mapped = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File mapped successfully. Address: %p\n", (void*)mapped);

    // 6. 修改映射区域的内容
    now = time(NULL);
    snprintf(timestamp, sizeof(timestamp), "%s%ld\n", new_data, (long)now);
    size_t data_len = strlen(timestamp);

    if (data_len <= length) {
        strcpy(mapped, timestamp); // 写入到映射区域的开头
        printf("Modified memory: %s", mapped); // 打印修改后的内容
    } else {
        fprintf(stderr, "Not enough space in mapped region for new data.\n");
    }

    printf("\n--- Before msync: Check file content (it might not be updated yet) ---\n");
    printf("Run 'cat %s' in another terminal to see the file content.\n", filename);
    printf("Press ENTER to continue and call msync...");
    getchar(); // 等待用户输入

    // 7. 调用 msync 强制将修改同步到文件
    // 使用 MS_SYNC 等待写入完成
    if (msync(mapped, length, MS_SYNC) == -1) {
        perror("msync MS_SYNC");
        // 清理
        munmap(mapped, length);
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("msync(MS_SYNC) completed successfully.\n");
    printf("--- After msync: File content should now be updated. ---\n");
    printf("Run 'cat %s' again to verify.\n", filename);

    // 8. 清理
    if (munmap(mapped, length) == -1) {
        perror("munmap");
    }

    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("Memory unmapped and file closed.\n");
    return 0;
}

代码解释:

  1. 以读写和创建模式打开(或创建)一个文件 msync_example.txt
  2. 写入一些初始内容,并使用 ftruncate 确保文件至少有一页大小。
  3. 调用 fstat 获取文件大小。
  4. 使用 mmap 创建一个 MAP_SHARED 的内存映射。MAP_SHARED 是关键,因为它使得对映射区域的修改会反映到文件中。
  5. 修改映射区域的内容(在开头写入带时间戳的数据)。
  6. 暂停: 提示用户可以在另一个终端检查文件内容,此时可能还未更新。
  7. 调用 msync(mapped, length, MS_SYNC)
    • mapped: 映射区域的起始地址。
    • length: 同步的长度。
    • MS_SYNC: 同步标志,使调用阻塞直到数据写入磁盘。
  8. 检查 msync 的返回值。
  9. 提示用户再次检查文件,确认内容已更新。
  10. 使用 munmapclose 进行清理。

示例 2:比较 MS_SYNCMS_ASYNC 链接到标题

这个例子演示 MS_SYNCMS_ASYNC 的区别。

#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

void test_msync_mode(const char *filename, int sync_flag, const char *flag_name) {
    int fd;
    struct stat sb;
    char *mapped;
    size_t length;
    char data[100];
    time_t now;

    printf("\n--- Testing %s ---\n", flag_name);

    fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return;
    }

    // 确保文件有内容
    write(fd, "Initial", 7);
    if (fstat(fd, &sb) == -1) { perror("fstat"); close(fd); return; }
    length = sb.st_size;

    mapped = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) { perror("mmap"); close(fd); return; }

    // 修改内存
    now = time(NULL);
    snprintf(data, sizeof(data), "Written with %s at %ld\n", flag_name, (long)now);
    strncpy(mapped, data, length > sizeof(data) ? sizeof(data) : length);

    printf("Memory modified. Calling msync with %s...\n", flag_name);

    clock_t start = clock();
    if (msync(mapped, length, sync_flag) == -1) {
        perror("msync");
    } else {
        clock_t end = clock();
        double cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
        printf("msync(%s) returned. CPU time used: %f seconds\n", flag_name, cpu_time_used);
        if (sync_flag == MS_ASYNC) {
            printf("  (MS_ASYNC returns quickly, actual write happens in background)\n");
        } else if (sync_flag == MS_SYNC) {
            printf("  (MS_SYNC waited for the write to complete)\n");
        }
    }

    // 简单等待一下,让异步写入可能完成 (但这不保证)
    if (sync_flag == MS_ASYNC) {
        printf("Sleeping 2 seconds to give async write a chance...\n");
        sleep(2);
    }

    munmap(mapped, length);
    close(fd);
}

int main() {
    // 测试 MS_ASYNC
    test_msync_mode("test_async.txt", MS_ASYNC, "MS_ASYNC");

    // 测试 MS_SYNC
    test_msync_mode("test_sync.txt", MS_SYNC, "MS_SYNC");

    printf("\nTests completed. Check the .txt files.\n");
    return 0;
}

代码解释:

  1. 定义了一个 test_msync_mode 函数,它接受文件名、msync 标志和标志名称作为参数。
  2. 在函数内部,它执行创建文件、映射、修改、调用 msync 并测量 msync 调用所用的 CPU 时间。
  3. main 函数分别使用 MS_ASYNCMS_SYNC 调用 test_msync_mode
  4. MS_ASYNC: msync 调用会非常快地返回(CPU 时间接近 0),因为它只是向内核提交了写回请求。
  5. MS_SYNC: msync 调用会阻塞,直到内核确认数据已写入磁盘(或发生错误),因此会消耗一些 CPU 时间(主要是等待时间)。
  6. 代码中为 MS_ASYNC 添加了 sleep(2),以示意异步操作在后台进行,但这并不保证数据已写入。

重要提示与注意事项:

  1. MAP_SHARED: msync 通常只对 MAP_SHARED 的映射有意义。对于 MAP_PRIVATE 的映射,修改是写时复制的,不会影响原始文件。
  2. 性能: MS_SYNC 会阻塞,可能显著影响程序性能,尤其是在写入大量数据或存储速度较慢时。MS_ASYNC 不会阻塞调用线程,但不能保证数据立即持久化。
  3. 数据持久性: 如果应用程序需要强数据持久性保证(例如数据库、日志文件),通常需要使用 MS_SYNC 或在 mmap 后使用 fsync/fdatasync。仅仅调用 msync 可能还不够,因为文件元数据(如大小)的更新可能也需要同步。
  4. 错误处理: msync 失败(返回 -1)通常表示严重问题(如磁盘空间不足、硬件错误、无效地址),需要认真处理。
  5. 范围: msync 只同步指定的内存范围。如果只修改了映射区域的一小部分,可以只同步那一部分,以提高效率。

总结:

msync 是管理 mmap 内存映射区域与文件数据一致性的重要函数。它允许程序在需要时显式地控制数据从内存刷新到磁盘的时机和方式。理解 MS_SYNCMS_ASYNC 的区别对于编写高性能且数据安全的应用程序至关重要。在需要确保数据持久化的场景中,正确使用 msync 是必不可少的。