C++ 虚拟内存管理:`mmap`, `munmap`, `mprotect` 的灵活运用

哈喽,各位好!今天咱们聊聊C++里那些让你感觉神秘又强大的虚拟内存管理工具:mmapmunmapmprotect。别怕,咱们不搞深奥的理论,直接用代码说话,保证你听完能上手,能装逼。

啥是虚拟内存? 你需要它吗?

想象一下,你的电脑就像一个豪华公寓,但你的程序就像一群熊孩子,每个都想霸占所有房间。虚拟内存就像一个超级管家,给每个熊孩子都分配了一个“虚拟”的房间号,让他们以为自己拥有整个公寓。实际上,管家会悄悄地把这些虚拟房间号映射到实际的物理房间,必要时还会把一些不常用的房间(数据)暂时放到储藏室(硬盘)里。

为什么要用虚拟内存?

  • 更大的空间: 你的程序可以拥有比实际物理内存更大的地址空间。
  • 内存保护: 不同的程序不会互相干扰,即使一个熊孩子把自己的房间搞得一团糟,也不会影响到其他孩子。
  • 更有效的内存利用: 只有真正需要的内存才会被加载到物理内存中。

主角登场:mmapmunmapmprotect

这三个家伙就是C++里操作虚拟内存的利器。它们不是C++标准库的一部分,而是POSIX标准提供的,所以在Linux、macOS等类Unix系统上可以直接使用。Windows上也有类似的API,但咱们今天重点关注POSIX。

1. mmap:内存映射,变废为宝的魔法

mmap 是memory mapping的缩写,它的作用是将一个文件或者设备映射到进程的地址空间。简单来说,就是让你可以像访问内存一样访问文件或者设备。这可比传统的 readwrite 效率高多了!

函数原型:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

别被这堆参数吓到,咱们一个一个来:

  • addr: 建议的起始地址,通常设为 NULL,让系统自己选择。
  • length: 映射的长度,也就是你要映射多少字节。
  • prot: 内存保护标志,指定映射区域的访问权限。
  • flags: 映射标志,控制映射的性质。
  • fd: 文件描述符,要映射的文件或者设备的句柄。
  • offset: 文件偏移量,从文件的哪个位置开始映射。

返回值:

  • 成功时返回映射区域的起始地址。
  • 失败时返回 MAP_FAILED,并设置 errno

代码示例:读取文件

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

int main() {
    const char* filename = "example.txt";

    // 创建一个示例文件
    {
        std::ofstream outfile(filename);
        outfile << "Hello, mmap world!n";
        outfile.close();
    }

    int fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

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

    size_t filesize = sb.st_size;

    // 使用 mmap 将文件映射到内存
    char* mapped_data = (char*)mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped_data == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 现在你可以像访问内存一样访问文件内容了
    std::cout << "File content: " << mapped_data << std::endl;

    // 不要忘记解除映射和关闭文件
    if (munmap(mapped_data, filesize) == -1) {
        perror("munmap");
    }

    close(fd);

    return 0;
}

代码解释:

  1. 打开文件:open(filename, O_RDONLY) 以只读方式打开文件。
  2. 获取文件大小:fstat(fd, &sb) 获取文件状态信息,包括文件大小。
  3. mmapmmap(NULL, filesize, PROT_READ, MAP_PRIVATE, fd, 0) 将文件映射到内存。
    • PROT_READ:只读权限。
    • MAP_PRIVATE:私有映射,对映射区域的修改不会影响到原始文件。
    • offset = 0:从文件开头开始映射。
  4. 访问数据:std::cout << "File content: " << mapped_data << std::endl; 直接像访问字符串一样访问映射区域。
  5. munmapmunmap(mapped_data, filesize) 解除内存映射。
  6. 关闭文件:close(fd) 关闭文件描述符。

mmap 的 flag 参数:

Flag 含义
MAP_SHARED 共享映射,对映射区域的修改会影响到原始文件,并且其他映射到该文件的进程也能看到这些修改。
MAP_PRIVATE 私有映射,对映射区域的修改不会影响到原始文件,其他映射到该文件的进程也看不到这些修改。
MAP_FIXED 强制使用 addr 参数指定的地址。如果该地址已经被占用,mmap 会失败。慎用!
MAP_ANONYMOUS 匿名映射,不与任何文件关联。通常与 MAP_SHARED 一起使用,用于进程间共享内存。
MAP_POPULATE 在映射时预先加载所有页面到内存中。这可以减少第一次访问映射区域时的延迟。

2. munmap:卸磨杀驴,解除内存映射

munmap 的作用很简单,就是解除 mmap 创建的内存映射。

函数原型:

int munmap(void *addr, size_t length);
  • addr: mmap 返回的地址。
  • length: 映射的长度。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno

代码示例:

上面的例子里已经包含了 munmap 的用法,这里不再赘述。

3. mprotect:金钟罩铁布衫,设置内存保护

mprotect 的作用是修改内存区域的保护属性,也就是设置内存的读、写、执行权限。这可以用来防止程序意外修改关键数据,或者防止恶意代码执行。

函数原型:

int mprotect(void *addr, size_t len, int prot);
  • addr: 要修改保护属性的内存区域的起始地址。
  • len: 要修改的内存区域的长度。
  • prot: 新的保护属性。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno

prot 参数:

Flag 含义
PROT_NONE 没有权限,任何访问都会导致错误。
PROT_READ 可读权限。
PROT_WRITE 可写权限。
PROT_EXEC 可执行权限。

代码示例:禁止写入

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

int main() {
    // 分配一段内存
    size_t page_size = sysconf(_SC_PAGE_SIZE); // 获取系统页大小
    char* memory = (char*)mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (memory == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // 初始化内存
    strcpy(memory, "Hello, world!");
    std::cout << "Original content: " << memory << std::endl;

    // 禁止写入
    if (mprotect(memory, page_size, PROT_READ) == -1) {
        perror("mprotect");
        munmap(memory, page_size);
        return 1;
    }

    // 尝试写入,会触发 Segmentation Fault (SIGSEGV)
    try {
        strcpy(memory, "This will crash!"); // 尝试写入只读内存
    } catch (...) {
        std::cerr << "Caught exception: Segmentation Fault (SIGSEGV)" << std::endl;
    }

    // 解除映射
    if (munmap(memory, page_size) == -1) {
        perror("munmap");
        return 1;
    }

    return 0;
}

代码解释:

  1. 分配匿名内存:mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) 分配一段匿名内存,可读可写。
    • MAP_ANONYMOUS:匿名映射,不与任何文件关联。
    • fd = -1, offset = 0:对于匿名映射,这两个参数会被忽略。
  2. 禁止写入:mprotect(memory, page_size, PROT_READ) 将内存区域设置为只读。
  3. 尝试写入:strcpy(memory, "This will crash!"); 尝试写入只读内存,会导致程序崩溃,抛出 Segmentation Fault (SIGSEGV) 信号。

mprotect 的注意事项:

  • addrlen 必须是系统页大小的整数倍。你可以使用 sysconf(_SC_PAGE_SIZE) 获取系统页大小。
  • 如果你映射了一段文件,并且使用了 MAP_SHARED 标志,那么修改内存保护属性可能会影响到其他映射到该文件的进程。

高级用法:进程间共享内存

mmap 可以用来创建进程间共享内存,这是一种非常高效的进程间通信方式。

代码示例:

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

int main() {
    const char* shared_memory_name = "/my_shared_memory";
    size_t shared_memory_size = 4096;

    // 创建共享内存对象
    int fd = shm_open(shared_memory_name, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        return 1;
    }

    // 设置共享内存对象的大小
    if (ftruncate(fd, shared_memory_size) == -1) {
        perror("ftruncate");
        close(fd);
        shm_unlink(shared_memory_name);
        return 1;
    }

    // 映射共享内存对象到进程地址空间
    char* shared_memory = (char*)mmap(NULL, shared_memory_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_memory == MAP_FAILED) {
        perror("mmap");
        close(fd);
        shm_unlink(shared_memory_name);
        return 1;
    }

    close(fd); // 关闭文件描述符,但共享内存仍然有效

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        munmap(shared_memory, shared_memory_size);
        shm_unlink(shared_memory_name);
        return 1;
    }

    if (pid == 0) {
        // 子进程
        std::cout << "Child process: Initial content: " << shared_memory << std::endl;

        // 修改共享内存
        strcpy(shared_memory, "Hello from child!");
        std::cout << "Child process: Modified content: " << shared_memory << std::endl;

        munmap(shared_memory, shared_memory_size);
        exit(0);
    } else {
        // 父进程
        wait(NULL); // 等待子进程结束

        std::cout << "Parent process: Content after child modification: " << shared_memory << std::endl;

        // 清理共享内存
        munmap(shared_memory, shared_memory_size);
        shm_unlink(shared_memory_name);
    }

    return 0;
}

代码解释:

  1. 创建共享内存对象:shm_open(shared_memory_name, O_CREAT | O_RDWR, 0666) 创建一个共享内存对象。
    • shm_open 不是POSIX标准的一部分,但通常在类Unix系统上可用。
    • O_CREAT:如果共享内存对象不存在,则创建它。
    • O_RDWR:以读写方式打开共享内存对象。
    • 0666:设置权限为可读可写。
  2. 设置共享内存对象的大小:ftruncate(fd, shared_memory_size) 设置共享内存对象的大小。
  3. 映射共享内存对象:mmap(NULL, shared_memory_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) 将共享内存对象映射到进程地址空间。
    • MAP_SHARED:共享映射,允许进程间共享内存。
  4. 创建子进程:fork() 创建一个子进程。
  5. 子进程修改共享内存:子进程修改共享内存中的数据。
  6. 父进程读取共享内存:父进程等待子进程结束,然后读取共享内存中的数据,可以看到子进程的修改。
  7. 清理共享内存:munmap 解除映射,shm_unlink 删除共享内存对象。

总结:

mmapmunmapmprotect 是C++里强大的虚拟内存管理工具,可以让你更有效地利用内存,提高程序的性能,并增强程序的安全性。掌握它们,你就能像一个真正的内存大师一样,掌控程序的命运!

一些建议:

  • 在使用 mmap 时,一定要注意文件大小和映射长度,避免越界访问。
  • 在使用 mprotect 时,要谨慎设置内存保护属性,避免影响程序的正常运行。
  • 在使用共享内存时,要注意同步问题,避免多个进程同时修改共享数据。

希望今天的讲座能让你对C++的虚拟内存管理有更深入的了解。下次再遇到内存问题,不要慌,拿出 mmapmunmapmprotect,让它们帮你搞定! Bye Bye!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注