`munmap`系统调用及示例

这次我们介绍 munmap 函数,它是 mmap 的“搭档”,用于释放之前通过 mmap 创建的内存映射区域。


1. 函数介绍

munmap 是一个 Linux 系统调用,其主要功能是解除(或取消映射)之前通过 mmap 系统调用在进程地址空间中创建的内存映射区域。调用 munmap 会通知内核,进程不再需要访问由 mmap 建立的从 addr 开始、长度为 length 的那块虚拟内存区域。

你可以把它想象成从墙上撕下之前贴上的书页(由 mmap 贴上)。撕下来后,这块“墙上的区域”(进程的地址空间)就空出来了,可以被系统重新分配给其他用途,同时与之关联的文件(如果有的话)也不再通过这块内存访问。


2. 函数原型

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

int munmap(void *addr, size_t length);

3. 功能

  • 解除映射: 告诉内核解除从地址 addr 开始、长度为 length 字节的内存区域的映射关系。
  • 释放资源: 内核会释放与该映射区域相关的内核数据结构,并可能将该区域的虚拟地址空间标记为未使用。
  • 数据同步: 对于 MAP_SHARED 映射,内核可能会将映射区域中被修改的页面刷新回底层文件(虽然不保证立即完成,msync 可以强制同步)。
  • 内存回收: 进程不再能通过 addr 指针访问这块内存。如果这块内存是通过 mmap 映射文件得到的,那么对该区域的访问将导致段错误(Segmentation fault)。

4. 参数

  • void *addr: 这是之前调用 mmap 成功返回的地址。它标识了要解除映射的内存区域的起始地址。
    • 重要: addr 必须是 mmap 返回的确切地址。如果传递一个 mmap 返回的地址加上某个偏移量,行为是未定义的,通常会导致 munmap 失败 (EINVAL)。
  • size_t length: 这是要解除映射的内存区域的长度(以字节为单位)。
    • 重要: 这个 length 应该与当初调用 mmap 时指定的 length 相匹配,或者至少覆盖你想解除映射的部分。如果 length 为 0,munmap 调用无效。

5. 返回值

  • 成功时: 返回 0。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL addr 不是有效的映射地址或 length 为 0 且 addr 有效,ENOMEM 指定的地址范围不完全在已映射区域内等)。

重要提示: 必须检查 munmap 的返回值! 虽然很多示例代码忽略了,但 munmap 是可能失败的。忽略错误可能导致资源泄漏(虽然操作系统在进程退出时会清理所有映射,但在长时间运行的程序中,不清理可能导致问题)。


6. 相似函数,或关联函数

  • mmap: 与 munmap 相对应,用于创建内存映射。
  • msync: 在调用 munmap 之前,如果需要确保 MAP_SHARED 映射的修改已写入文件,可以先调用 msync
  • mprotect: 修改映射区域的保护属性,但这不涉及解除映射。
  • free: 用于释放通过 malloc 分配的堆内存,与 munmap 释放 mmap 的内存相对应(尽管 mmap 有时也被 malloc 内部用于分配大块内存)。

7. 示例代码

示例 1:基本的 mmapmunmap 使用

这个例子结合了 mmapmunmap,展示了它们的标准使用流程。

#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open, O_RDONLY
#include <unistd.h>   // close, fstat
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit

int main(int argc, char *argv[]) {
    int fd;
    struct stat sb;
    char *mapped_memory;
    size_t file_length;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 1. 打开文件
    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 2. 获取文件大小
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }
    file_length = sb.st_size;

    if (file_length == 0) {
        printf("File is empty.\n");
        close(fd);
        exit(EXIT_SUCCESS);
    }

    // 3. 创建内存映射
    mapped_memory = mmap(NULL, file_length, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped_memory == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File '%s' mapped successfully. Address: %p, Length: %zu bytes\n",
           argv[1], (void*)mapped_memory, file_length);

    // 4. 使用映射的内存 (这里只是简单打印第一个字符)
    printf("First character of the file: '%c'\n", mapped_memory[0]);

    // ... 这里可以进行更多对 mapped_memory 的读/写操作 ...

    // 5. 关键步骤:解除内存映射 - 释放资源
    if (munmap(mapped_memory, file_length) == -1) {
        // munmap 失败!这是一个需要处理的错误
        perror("CRITICAL ERROR: munmap failed");
        // 即使 munmap 失败,也应该尝试关闭文件
        close(fd);
        exit(EXIT_FAILURE); // 或根据应用逻辑决定如何处理
    }
    printf("Memory unmapped successfully.\n");

    // 6. 关闭文件描述符 (可以在 munmap 之前或之后)
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("File closed.\n");
    printf("Program finished successfully.\n");
    return 0;
}

代码解释:

  1. 打开文件并获取其大小。
  2. 调用 mmap 将整个文件映射到内存。
  3. 检查 mmap 是否成功。
  4. 使用映射的内存(示例中只打印了第一个字符)。
  5. 关键: 调用 munmap(mapped_memory, file_length) 来解除映射。这是释放 mmap 资源的关键步骤。
  6. 最重要: 检查 munmap 的返回值。如果返回 -1,打印错误信息并退出(或按应用逻辑处理)。
  7. 最后关闭文件描述符。
示例 2:处理 MAP_SHARED 映射的清理

这个例子强调在解除 MAP_SHARED 映射前使用 msync 确保数据写入。

#include <sys/mman.h> // mmap, munmap, msync
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open
#include <unistd.h>   // close, fstat
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit
#include <string.h>   // memset

int main(int argc, char *argv[]) {
    int fd;
    struct stat sb;
    char *mapped_memory;
    size_t file_length = 4096; // 至少一页大小
    const char *data_to_write = "Data written by process using MAP_SHARED and msync.";

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 1. 打开或创建文件 (为了确保文件足够大,这里先创建/截断)
    fd = open(argv[1], O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 确保文件至少有一页大小
    if (ftruncate(fd, file_length) == -1) {
        perror("ftruncate");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 2. 获取文件大小 (确认)
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }
    file_length = sb.st_size;
    printf("File size is %zu bytes.\n", file_length);

    // 3. 创建共享内存映射
    mapped_memory = mmap(NULL, file_length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped_memory == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File '%s' mapped for shared read/write. Address: %p\n", argv[1], (void*)mapped_memory);

    // 4. 修改映射的内存
    // 清零前几字节
    memset(mapped_memory, 0, 100);
    // 写入我们的数据
    size_t data_len = strlen(data_to_write);
    if (data_len < file_length) {
        for (size_t i = 0; i < data_len; ++i) {
            mapped_memory[i] = data_to_write[i];
        }
        // 或 memcpy(mapped_memory, data_to_write, data_len);
    }
    printf("Modified memory via mmap.\n");

    // 5. (重要) 强制同步修改到文件
    // 对于 MAP_SHARED 映射,这是一个好习惯,尤其是在 munmap 之前
    if (msync(mapped_memory, data_len, MS_SYNC) == -1) {
        perror("msync");
        // 即使 msync 失败,也尝试清理
    } else {
        printf("Changes synced to file using msync.\n");
    }

    // 6. 解除内存映射
    if (munmap(mapped_memory, file_length) == -1) {
        perror("munmap");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("Memory unmapped.\n");

    // 7. 关闭文件描述符
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("File closed.\n");
    printf("Process finished. Check the file content.\n");
    return 0;
}

代码解释:

  1. 以读写模式打开或创建文件,并使用 ftruncate 确保文件至少有一页大小。
  2. 调用 mmap 创建 MAP_SHARED 映射。
  3. 修改映射的内存。
  4. 关键: 在 munmap 之前调用 msync(mapped_memory, data_len, MS_SYNC)。这确保了对共享映射区域的修改被强制写回到文件中。MS_SYNC 会等待写入操作完成。
  5. 调用 munmap 解除映射。
  6. 检查 munmapmsync 的返回值。
  7. 关闭文件描述符。
示例 3:错误处理和多次映射/解除映射

这个例子展示了在一个程序中进行多次 mmap/munmap 操作,并强调了正确的错误处理。

#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open
#include <unistd.h>   // close, fstat
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit

// 函数:安全地映射文件的一部分
int safe_mmap_part(const char *filename, size_t offset, size_t length, char **mapped_ptr) {
    int fd = -1;
    struct stat sb;
    char *mapped = MAP_FAILED;

    fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("open in safe_mmap_part");
        goto error;
    }

    if (fstat(fd, &sb) == -1) {
        perror("fstat in safe_mmap_part");
        goto error;
    }

    // 检查偏移和长度是否有效
    if (offset >= (size_t)sb.st_size || length == 0 || offset + length > (size_t)sb.st_size) {
        fprintf(stderr, "Invalid offset or length for file '%s'\n", filename);
        goto error;
    }

    // 确保 offset 是页对齐的 (虽然 mmap 通常能处理,但最好自己保证)
    long pagesize = sysconf(_SC_PAGESIZE);
    if (offset % pagesize != 0) {
        fprintf(stderr, "Offset %zu is not page-aligned (%ld)\n", offset, pagesize);
        goto error;
    }

    mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
    if (mapped == MAP_FAILED) {
        perror("mmap in safe_mmap_part");
        goto error;
    }

    // 成功,设置输出参数
    *mapped_ptr = mapped;
    close(fd); // mmap 成功后可以关闭 fd
    return 0; // Success

error:
    if (mapped != MAP_FAILED) {
        munmap(mapped, length); // 尝试清理,尽管可能失败
    }
    if (fd != -1) {
        close(fd);
    }
    *mapped_ptr = MAP_FAILED;
    return -1; // Failure
}

int main(int argc, char *argv[]) {
    char *map1 = MAP_FAILED, *map2 = MAP_FAILED;
    int result1, result2;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 映射文件的前 1KB
    result1 = safe_mmap_part(argv[1], 0, 1024, &map1);
    if (result1 == 0 && map1 != MAP_FAILED) {
        printf("Successfully mapped first 1KB of '%s' at %p\n", argv[1], (void*)map1);
        printf("First char of part 1: '%c'\n", map1[0]);
    } else {
        printf("Failed to map first part.\n");
    }

    // 尝试映射文件的 1KB-2KB 部分 (偏移 1024)
    result2 = safe_mmap_part(argv[1], 1024, 1024, &map2);
    if (result2 == 0 && map2 != MAP_FAILED) {
        printf("Successfully mapped 1KB starting at offset 1024 of '%s' at %p\n", argv[1], (void*)map2);
        // 打印这部分的第一个字符
        printf("First char of part 2: '%c'\n", map2[0]);
    } else {
        printf("Failed to map second part.\n");
    }

    // --- 清理阶段 ---

    // 解除第一个映射
    if (map1 != MAP_FAILED) {
        if (munmap(map1, 1024) == -1) {
            perror("munmap part 1");
            // 根据应用决定是否 exit
        } else {
            printf("Unmapped first part successfully.\n");
        }
        map1 = MAP_FAILED; // 防止重复解除映射
    }

    // 解除第二个映射
    if (map2 != MAP_FAILED) {
        if (munmap(map2, 1024) == -1) {
            perror("munmap part 2");
        } else {
            printf("Unmapped second part successfully.\n");
        }
        map2 = MAP_FAILED;
    }

    printf("All mappings cleaned up.\n");
    return 0;
}

代码解释:

  1. 定义了一个 safe_mmap_part 函数,它封装了打开文件、检查大小、调用 mmap 以及错误处理的逻辑。它返回 0 表示成功,-1 表示失败,并通过指针参数 mapped_ptr 返回映射地址。
  2. main 函数中,调用 safe_mmap_part 两次,分别映射文件的不同部分。
  3. 清理阶段: 遍历所有可能有效的映射指针(通过检查是否不等于 MAP_FAILED)并调用 munmap
  4. 关键: 对每一次 munmap 调用都检查了返回值。如果失败,会打印错误信息。
  5. 解除映射后,将指针设置回 MAP_FAILED,这是一种防止重复解除映射的好习惯。

总结来说,munmap 是管理 mmap 资源的关键函数。养成始终检查 munmap 返回值的习惯,并在适当的时候(尤其是 MAP_SHARED 映射)结合 msync 使用,对于编写健壮和高效的 Linux 程序至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值