物理内存
:也就是安装在计算机中的内存条,比如安装了 2GB 大小的内存条,那么物理内存地址的范围就是 0 ~ 2GB。虚拟内存
:虚拟的内存地址。由于 CPU 只能使用物理内存地址,所以需要将虚拟内存地址转换为物理内存地址才能被 CPU 使用,这个转换过程由MMU(Memory Management Unit,内存管理单元)
来完成。虚拟内存
大小不受物理内存
大小的限制,在 32 位的操作系统中,每个进程的虚拟内存空间大小为 0 ~ 4GB。在 64 位的操作系统中,每个进程的虚拟内存空间大小为 0 ~ 128TB。
程序中使用的内存地址都是虚拟内存地址,也就是说,我们通过 malloc
函数申请的内存都是虚拟内存。实际上,内核会为每个进程管理其虚拟内存空间,并且会把虚拟内存空间划分为多个区域,如图所示:
我们来分析一下这些区域的作用:
代码段
:用于存放程序的可执行代码。数据段
:用于存放程序的全局变量和静态变量。堆空间
:用于存放由malloc
申请的内存。栈空间
:用于存放函数的参数和局部变量。内核空间
:存放 Linux 内核代码和数据。
一个内存页是一段固定大小的连续的连续内存地址的总称,具体到Linux中,典型的内存页大小为4KB。所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
由于页大小都是4k,所以页内偏移都是用低12位表示,而剩下的高地址表示页号 MMU映射单位并不是字节,而是页,这个映射通过差一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制。
有时MMU在工作时,会发现页表表名某个内存页不在物理内存页不在物理内存中,此时会触发一个缺页异常,此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。
在内核中,使用一个名为 brk
的指针来表示进程的堆空间的顶部,如图所示:
malloc 函数只是移动 brk 指针,但并没有申请物理内存。前面我们介绍虚拟内存和物理内存的时候介绍过,虚拟内存地址必须映射到物理内存地址才能被使用。
如果对没有进行映射的虚拟内存地址进行读写操作,那么将会发生 缺页异常
。Linux 内核会对 缺页异常
进行修复,修复过程如下:
-
获取触发
缺页异常
的虚拟内存地址(读写哪个虚拟内存地址导致的)。 -
查看此虚拟内存地址是否被申请(是否在
brk
指针内),如果不在brk
指针内,将会导致 Segmention Fault 错误(段错误),进程将会异常退出。- 野指针会触发段错误
- 为了避免野指针引发段错误,应该始终确保指针指向有效的内存地址。
- 放了一个指针指向的内存块后,应该将指针设置为 nullptr
- 对于指针的使用,应该进行必要的有效性检查,以确保指针指向的内存仍然有效和可用。
-
如果虚拟内存地址在
brk
指针内,那么将此虚拟内存地址映射到物理内存地址上,完成缺页异常
修复过程,并且返回到触发异常的地方进行运行。
从上面的过程可以看出,不对申请的虚拟内存地址进行读写操作是不会触发申请新的物理内存。所以,这就解释了为什么申请 1GB 的内存,但实际上只使用了 404 KB 的物理内存。
在C++中进行文件读写操作时,通常会使用文件流(fstream)来处理。文件的读写操作是通过文件流对象和与之关联的文件进行的,而不是将整个文件加载到内存中。
- 文件流对象在读取或写入数据时,会按照指定的方式从文件中读取或写入一定量的数据
- 文件流对象会根据需要从文件中读取数据,并将其存储在内部的缓冲区中
- 这种逐块读取和写入的方式可以有效地处理大文件,而不需要将整个文件加载到内存中
- 当使用文件流进行读取操作时,文件仍然存储在磁盘上,而不是被加载到内存中。
- 文件流对象提供了一种从磁盘文件中逐块读取数据的机制。