[CSAPP] 9.8 内存映射 | 虚拟内存 | 页表 | 物理内存 | 写时拷贝机制
目录
进程地址空间的管理
虚拟地址与父子进程
Linux下的进程地址空间管理
原书笔记存档
习题
mmapcopy.c
去年暑假的笔记可以看这里,里面有进行过详细的代码验证【Linux详解】进程地址空间,习题答案可以直接看文末
进程地址空间的管理
当我们讨论进程地址空间时,一个关键点在于我们操作的并不是真实的物理地址,而是虚拟地址。
那么,如何在使用虚拟地址的同时能够对物理内存进行数据存储呢?
- 答案是操作系统中存在一种叫做 页表 的机制。页表维护了虚拟地址与物理地址之间的映射关系。
- 当进程试图通过虚拟地址在自己的地址空间中查找数据时,实际上是拿着这个虚拟地址去查询页表,找到对应的物理地址,从而实现对实际数据的访问。
虚拟地址与父子进程
在探讨虚拟地址时,有一个问题浮现出来:为什么同一个虚拟地址在父进程和子进程中可以存储不同的数据?
当创建子进程时,它会继承父进程中的大量数据,包括页表。
因此,在初始状态下,子进程的页表包含了与父进程相同的虚拟地址到物理地址的映射关系。
例如,如果父进程和子进程都对变量val进行了定义,它们在开始时对val的虚拟地址是一样的。
- 但是,当子进程尝试修改val(比如将val设为5)时,写时拷贝(Copy-On-Write, COW)机制就会被触发。
- 此时,系统会把原先父子进程共享的val值复制一份给子进程专用,并允许子进程单独对其进行修改。
- 这一过程中,val的虚拟地址保持不变,但其对应的物理地址发生了变化。
- 也就是说,虽然父子进程对于val的虚拟地址相同,但由于它们的页表不同,导致最终访问到的物理地址不同
这就是为何我们可以看到同一虚拟地址对应两个不同值的现象。
Linux下的进程地址空间管理
Linux操作系统采用了一套复杂的机制来管理系统中的进程地址空间。
其中,页表 扮演了至关重要的角色,不仅解决了虚拟地址到物理地址的转换问题,还通过如写时拷贝等机制优化了内存使用效率,使得多个进程(特别是父子进程之间)能够高效、安全地共享和独立使用内存资源。
- 这个地方涉及到了一个 可以中间再加一层的 映射转化 思想
- 就是例如,我们有很多的数据要对其进行存储,我们可以通过其进行标识,然后对其的标识位进行管理
- 要用的时候再通过标识去找到该数据,这样就不用统一管理非常多/大的数据了
- (类似于昨天protobuf对于传输效率提升,所使用到的Varint编码用到的MSB最高位 标记 机制)
原书笔记存档
习题
mmapcopy.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "使用方法: %s <文件名>\n", argv[0]);
exit(1);
}
// 打开输入文件
int fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("无法打开文件");
exit(1);
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("无法获取文件状态");
close(fd);
exit(1);
}
// 如果文件大小为0,直接退出
if (sb.st_size == 0) {
printf("文件为空\n");
close(fd);
exit(0);
}
// 映射文件到内存
char *mapped_file = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped_file == MAP_FAILED) {
perror("mmap失败");
close(fd);
exit(1);
}
// 将映射的内容写入stdout
size_t bytes_written = write(STDOUT_FILENO, mapped_file, sb.st_size);
if (bytes_written != sb.st_size) {
perror("写入失败");
munmap(mapped_file, sb.st_size);
close(fd);
exit(1);
}
// 清理
if (munmap(mapped_file, sb.st_size) == -1) {
perror("munmap失败");
close(fd);
exit(1);
}
close(fd);
return 0;
}
测试
我们可以用 man 再来回看一下这些接口,就会感觉到很清晰了