- A+
操作系统虚拟内存
应用程序通过malloc函数申请内存的时候,实际申请的是虚拟内存,并不会分配物理内存
当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,这时会发现虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内存态,并将缺页中断交给内核的page Fault handler(缺页中断函数)处理。
缺页中断处理函数会看是否有空闲的物理内存
- 如果有,就直接分配物理内存,并建立虚拟内存和物理内存之间的映射关系
- 如果没有空闲的物理内存,内核就会开始进行回收内存的工作,如果回收内存工作结束后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发OOM(Out of Memory)机制
32位操作系统和64位操作系统的虚拟地址空间大小是不同的,在Linux操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分:
- 2位系统的内核空间占用1G,位于最高处,剩下的3G是用户空间;
- 64位系统的内核空间和用户空间都是128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
在32位操作系统上,进程最多申请3GB大小的虚拟内存空间,所以进程申请8GB内存的话,在申请虚拟内存阶段就会失败(可能错误原因是OOM)
在64位操作系统,进程可以使用128T大小的虚拟内存空间,所以进程申请8GB内存是没问题的,因为进程申请内存是申请虚拟内存,只要不读写这个虚拟内存,操作系统就不会分配物理内存。
测试方案:
先查寻当前设备物理内存
free -m
在机器上连续申请4次1GB内存,只单纯分配了虚拟内存,并没有使用此虚拟内存
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MEM_SIZE 1024 * 1024 * 1024 int main() { char* addr[4]; int i = 0; for(i = 0; i < 4; ++i) { addr[i] = (char*) malloc(MEM_SIZE); if(!addr[i]) { printf("执行 malloc 失败, 错误:%sn",strerror(errno)); return -1; } printf("主线程调用malloc后,申请1gb大小得内存,此内存起始地址:0X%xn", addr[i]); } //输入任意字符后,才结束 getchar(); return 0; }
虽然物理内存只有2GB,但是程序正常分配了4G大小的虚拟内存:
通过一下命令查看进程(test)的虚拟内存大小:
ps aux
其中,VSZ 就代表进程使用的虚拟内存大小,RSS 代表进程使用的物理内存大小。可以看到,VSZ 大小为 4198540,也就是 4GB 的虚拟内存。
Swap机制的作用
如果申请物理内存大小超过了空闲物理内存的大小,就要看操作系统有没有开启Swap机制:
- 如果没有开启Swap机制,程序就会直接OOM
- 如果有开启Swap机制,程序可以正常运行
什么是Swap机制?
当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复数据到内存中。
当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘中读入内存就可以了。
这种,将内存数据换出磁盘,又将磁盘中恢复数据到内存的过程,就是Swap机制负责的。
Swap就是把一块磁盘空间或者本地文件,当成内存来使用,包含换出和换入两个过程:
- 换出(Swap Out),是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
- 换入(Swap In),是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;
使用 Swap 机制优点是,应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比内存要低,因此这种方式无疑是经济实惠的。当然,频繁地读写硬盘,会显著降低操作系统的运行速率,这也是 Swap 的弊端。
Linux中的Swap机制会在内存不足和内存闲置的场景下触发:
- 内存不足:当系统需要的内存超过了可用的物理内存时。内核会将内存中不常使用的内存页交换到磁盘上为当前进程让出内存,保证正在执行的进程的可用性,这个内存回收的过程是强制的直接内存回收(Direct Page Reclaim),直接内存回收是同步的过程,会阻塞当前申请内存的进程。
- 内存闲置:应用程序在启动阶段使用的大量内存在启动后往往都不会使用,通过后台运行的守护进程(kSwapd),我们可以将这部分只使用一次的内存交换到磁盘上为其他内存的申请预留空间。kSwapd 是 Linux 负责页面置换(Page replacement)的守护进程,它也是负责交换闲置内存的主要进程,它会在空闲内存低于一定水位时,回收内存页中的空闲内存保证系统中的其他进程可以尽快获得申请的内存。kSwapd 是后台进程,所以回收内存的过程是异步的,不会阻塞当前申请内存的进程。
Linux 提供了两种不同的方法启用 Swap,分别是 Swap 分区(Swap Partition)和 Swap 文件(Swapfile):
- Swap 分区是硬盘上的独立区域,该区域只会用于交换分区,其他的文件不能存储在该区域上,我们可以使用 Swapon -s 命令查看当前系统上的交换分区;
- Swap 文件是文件系统中的特殊文件,它与文件系统中的其他文件也没有太多的区别
Swap换入换出的是什么类型的内存?
内核缓存的文件数据,因为都有对应的磁盘文件,所以在回收文件数据的时候, 直接写回到对应的文件就可以了。
但是像进程的堆、栈数据等,它们是没有实际载体,这部分内存被称为匿名页。而且这部分内存很可能还要再次被访问,所以不能直接释放内存,于是就需要有一个能保存匿名页的磁盘载体,这个载体就是 Swap 分区。
匿名页回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
实验测试:
我的Linux系统镜像是32位操作系统,物理内存设置2GB,带有Swap分区:
测试代码直接申请4GB虚拟内存后,通过memset函数访问
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MEM_SIZE 1024 * 1024 * 1024 int main() { char* addr[4]; int i = 0; for(i = 0; i < 4; ++i) { addr[i] = (char*) malloc(MEM_SIZE); if(!addr[i]) { printf("执行 malloc 失败, 错误:%sn",strerror(errno)); return -1; } printf("主线程调用malloc后,申请1gb大小得内存,此内存起始地址:0X%xn", addr[i]); } for(i = 0; i < 4; ++i) { printf("开始访问第 %d 块虚拟内存(每一块虚拟内存为 1 GB)n", i + 1); memset(addr[i], 0, MEM_SIZE); } //输入任意字符后,才结束 getchar(); return 0; }
运行结果:
结论:
在有Swap机制的情况下,虽然物理内存只有2GB,也是可以申请4GB虚拟内存并使用的
修改测试代码:
申请8G内存(因为物理内存加Swap区接近8G),测试结果如下:
8G内存可以正常申请,但是当访问前7G虚拟内存区域时都是正常的,当访问第8G时,被系统kill,意味着发生了OOM,查询日志文件,发现确实kernel发生了oom-kill。
总结
因为手头暂时没有现成的64位镜像,所64位结论采用参考文章的结论
-
在32位操作系统,因为进程最大只能申请3GB大小的虚拟内存,所以直接申请8G内存会申请失败
-
在64位操作系统,因为进程最大能申请128T大小的虚拟内存,即使物理内存只有4GB,申请8GB也是没有问题的,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有Swap分区:
-
如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
-
如果有 Swap 分区,即使物理内存只有 2GB,程序也能正常使用 4GB 的内存(4G物理内存,申请8G内存同理),进程可以正常运行;