哈喽,各位好!今天咱们聊聊C++里那些让你感觉神秘又强大的虚拟内存管理工具:mmap
、munmap
和 mprotect
。别怕,咱们不搞深奥的理论,直接用代码说话,保证你听完能上手,能装逼。
啥是虚拟内存? 你需要它吗?
想象一下,你的电脑就像一个豪华公寓,但你的程序就像一群熊孩子,每个都想霸占所有房间。虚拟内存就像一个超级管家,给每个熊孩子都分配了一个“虚拟”的房间号,让他们以为自己拥有整个公寓。实际上,管家会悄悄地把这些虚拟房间号映射到实际的物理房间,必要时还会把一些不常用的房间(数据)暂时放到储藏室(硬盘)里。
为什么要用虚拟内存?
- 更大的空间: 你的程序可以拥有比实际物理内存更大的地址空间。
- 内存保护: 不同的程序不会互相干扰,即使一个熊孩子把自己的房间搞得一团糟,也不会影响到其他孩子。
- 更有效的内存利用: 只有真正需要的内存才会被加载到物理内存中。
主角登场:mmap
、munmap
、mprotect
这三个家伙就是C++里操作虚拟内存的利器。它们不是C++标准库的一部分,而是POSIX标准提供的,所以在Linux、macOS等类Unix系统上可以直接使用。Windows上也有类似的API,但咱们今天重点关注POSIX。
1. mmap
:内存映射,变废为宝的魔法
mmap
是memory mapping的缩写,它的作用是将一个文件或者设备映射到进程的地址空间。简单来说,就是让你可以像访问内存一样访问文件或者设备。这可比传统的 read
和 write
效率高多了!
函数原型:
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;
}
代码解释:
- 打开文件:
open(filename, O_RDONLY)
以只读方式打开文件。 - 获取文件大小:
fstat(fd, &sb)
获取文件状态信息,包括文件大小。 mmap
:mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, fd, 0)
将文件映射到内存。PROT_READ
:只读权限。MAP_PRIVATE
:私有映射,对映射区域的修改不会影响到原始文件。offset = 0
:从文件开头开始映射。
- 访问数据:
std::cout << "File content: " << mapped_data << std::endl;
直接像访问字符串一样访问映射区域。 munmap
:munmap(mapped_data, filesize)
解除内存映射。- 关闭文件:
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;
}
代码解释:
- 分配匿名内存:
mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)
分配一段匿名内存,可读可写。MAP_ANONYMOUS
:匿名映射,不与任何文件关联。fd = -1, offset = 0
:对于匿名映射,这两个参数会被忽略。
- 禁止写入:
mprotect(memory, page_size, PROT_READ)
将内存区域设置为只读。 - 尝试写入:
strcpy(memory, "This will crash!");
尝试写入只读内存,会导致程序崩溃,抛出Segmentation Fault (SIGSEGV)
信号。
mprotect
的注意事项:
addr
和len
必须是系统页大小的整数倍。你可以使用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;
}
代码解释:
- 创建共享内存对象:
shm_open(shared_memory_name, O_CREAT | O_RDWR, 0666)
创建一个共享内存对象。shm_open
不是POSIX标准的一部分,但通常在类Unix系统上可用。O_CREAT
:如果共享内存对象不存在,则创建它。O_RDWR
:以读写方式打开共享内存对象。0666
:设置权限为可读可写。
- 设置共享内存对象的大小:
ftruncate(fd, shared_memory_size)
设置共享内存对象的大小。 - 映射共享内存对象:
mmap(NULL, shared_memory_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
将共享内存对象映射到进程地址空间。MAP_SHARED
:共享映射,允许进程间共享内存。
- 创建子进程:
fork()
创建一个子进程。 - 子进程修改共享内存:子进程修改共享内存中的数据。
- 父进程读取共享内存:父进程等待子进程结束,然后读取共享内存中的数据,可以看到子进程的修改。
- 清理共享内存:
munmap
解除映射,shm_unlink
删除共享内存对象。
总结:
mmap
、munmap
和 mprotect
是C++里强大的虚拟内存管理工具,可以让你更有效地利用内存,提高程序的性能,并增强程序的安全性。掌握它们,你就能像一个真正的内存大师一样,掌控程序的命运!
一些建议:
- 在使用
mmap
时,一定要注意文件大小和映射长度,避免越界访问。 - 在使用
mprotect
时,要谨慎设置内存保护属性,避免影响程序的正常运行。 - 在使用共享内存时,要注意同步问题,避免多个进程同时修改共享数据。
希望今天的讲座能让你对C++的虚拟内存管理有更深入的了解。下次再遇到内存问题,不要慌,拿出 mmap
、munmap
和 mprotect
,让它们帮你搞定! Bye Bye!