Linux的虚拟内存是什么?

2021-06-21 17:44 来源:电子说

前不久,群里还有一个我期待的分享:“Linux的虚拟内存”。有天晚上加班,讨论虚拟内存的概念时,领导发现有几个同事对虚拟内存不太清楚,特意给这位同学选了主题(笑)。

之前学过一些操作系统的概念,主要是对自己毕业后四年大学荒废感到懊恼,对自己的计算机专业背景感到惋惜。于是我在业余时间抽空看了一下哈工大网易云班的操作系统公开课,还看了一本浅操作系统《Linux内核设计与实现》的书。去年我用C写一个简单的服务器的时候,也学到了更多关于系统的底层知识。得益于这些知识,我对应用层的知识有了更好的掌控感,也帮助我解决了上次的问题。

前几天又有同事来问另一个关于虚拟内存的问题,发现对虚拟内存理解不够深入,有些概念自相矛盾。于是我翻看了一下资料,重新整理了一下知识,希望下次用起来更流畅。

起源

虚拟内存

在现代操作系统中,多任务是标准的。多任务并行大大提高了CPU利用率,但却导致多个进程的内存操作冲突。虚拟内存的概念就是为了解决这个问题而提出的。

49bc6f36-d12c-11eb-9e57-12bb97331649.png

上图是虚拟内存最简单直观的解释。

操作系统有一个物理内存(中间部分),有两个进程(实际上会有更多),P1和P2。操作系统分别偷偷告诉P1和P2,我的整个内存都是你的,随便用用,管理一下。但其实操作系统只是给他们画了个大饼。据说这些记忆是给P1和P2的,但实际上它们只被赋予了一个序列号。只有当P1和P2真正开始使用这些记忆时,系统才开始使用洗牌,并为这个过程拼凑出各种块。P2以为他在使用一个记忆,但实际上它被系统悄悄地重定向到了真实的b。即使P1和P2共享c记忆,他们也不知道。

这种欺骗操作系统进程的手段就是虚拟内存。对于P1和P2这样的进程,他们都认为自己占据了整个内存,但是他们不知道也不需要关心自己使用的是物理内存的哪个地址。

分页和页表

虚拟内存是操作系统中的一个概念。对于操作系统,虚拟内存是一个交叉引用表。当P1在A内存中获取数据时,它应该去物理内存的A地址,而在B内存中搜索数据时,它应该去物理内存的C地址。

我们知道,系统中的基本单位是字节。如果每个虚拟内存的字节对应于物理内存的地址,每个条目至少需要8个字节(32位虚拟地址-32位物理地址)。在4G内存的情况下,需要32GB的空间来存储查找表,所以这个表对于真实的物理地址来说太大了,所以操作系统引入了Page的概念。

当系统启动时,操作系统将整个物理内存划分为以4K为单位的页面。之后在分配内存时,物理内存页对应的虚拟内存页映射表大大减少。4G内存,只需要8M的映射表。有些进程不使用虚拟内存,也不需要保存映射关系。而且Linux针对大内存设计了多级页表,输入一页就可以减少内存消耗。操作系统的虚拟内存到物理内存的映射表称为页表。

内存寻址和分配

我们知道,通过虚拟内存机制,每个进程都认为自己占据了所有的内存。当一个进程访问内存时,操作系统会将该进程提供的虚拟内存地址转换为物理地址,然后前往对应的物理地址获取数据。CPU里有一种硬件,MMU(内存管理单元)专门用来翻译虚拟内存地址。CPU还为页表寻址设置缓存策略。由于程序的局部性,其缓存命中率可以达到98%。

以上情况是页表中存在虚拟地址到物理地址的映射。如果进程访问的物理地址尚未分配,系统将产生缺页中断。当处理中断时,系统切换到内核状态,为进程的虚拟地址分配物理地址。

功能

虚拟内存不仅通过内存地址转换解决了多个进程访问内存冲突的问题,还带来了更多的好处。

进程内存管理

它有助于进程内存管理,主要体现在:

内存完整性:因为虚拟内存欺骗进程,每个进程都认为自己获取的内存是一个连续的地址。我们写应用的时候,不需要考虑大块地址的分配,总以为系统有足够大的内存块。

安全性:由于进程访问内存,所以必须通过页表寻址,操作系统可以通过在页表的每一项添加各种访问权限标识位来实现内存权限控制。

数据共享

通过虚拟内存更容易共享内存和数据。

当一个进程加载一个系统库时,它总是首先分配一个内存块,并将磁盘中的库文件加载到这个内存中。直接使用物理内存时,由于物理内存地址唯一,即使系统发现同一个库在系统中加载了两次,每个进程指定的加载内存也不一样,系统也无能为力。

当使用虚拟内存时,系统只需要将进程的虚拟内存地址指向库文件所在的物理内存地址。如上图所示,进程P1和P2的B地址都指向物理地址c

通过使用虚拟内存

共享内存也很简单,系统只需要将各个进程的虚拟内存地址指向系统分配的共享内存地址即可。

SWAP

虚拟内存可以让帮进程”扩充”内存。

我们前文提到了虚拟内存通过缺页中断为进程分配物理内存,内存总是有限的,如果所有的物理内存都被占用了怎么办呢?

Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分区,在分配物理内存,但可用内存不足时,将暂时不用的内存数据先放到磁盘上,让有需要的进程先使用,等进程再需要使用这些数据时,再将这些数据加载到内存中,通过这种”交换”技术,Linux 可以让进程使用更多的内存。

常见问题

在了解虚拟内存时,我也有过很多的问题。

32位和64位

最常见的就是 32位和64位的问题了。

CPU 通过物理总线访问内存,那么访问地址的范围就受限于机器总线的数量,在32位机器上,有32条总线,每条总线有高低两种电位分别代表 bit 的 1 和 0,那么可访问的最大地址就是 2^32bit = 4GB,所以说 32 位机器上插入大于 4G 的内存是无效的,CPU 访问不到多于 4G 的内存。

但 64位机器并没有 64位总线,而且其最大内存还要受限于操作系统,Linux 目前支持最大 256G 内存。

根据虚拟内存的概念,在 32 位系统上运行 64 位软件也并无不可,但由于系统对虚拟内存地址的结构设计,64位的虚拟地址在32位系统内并不能使用。

直接操作物理内存

操作系统使用了虚拟内存,我们想要直接操作内存该怎么办呢?

Linux 会将各个设备都映射到/dev/目录下的文件,我们可以通过这些设备文件直接操作硬件,内存也不例外。在 Linux 中,内存设置被映射为/dev/mem,root 用户通过对这个文件读写,可以直接操作内存。

JVM 进程占用虚拟内存过多

使用 TOP 查看系统性能时,我们会发现在 VIRT 这一列,Java 进程会占用大量的虚拟内存。

49ddbcea-d12c-11eb-9e57-12bb97331649.png

导致这种问题的原因是 Java 使用 Glibc 的 Arena 内存池分配了大量的虚拟内存并没有使用。此外,Java 读取的文件也会被映射为虚拟内存,在虚拟机默认配置下 Java 每个线程栈会占用 1M 的虚拟内存。具体可以查看为什么linux下多线程程序如此消耗虚拟内存。

而真实占用的物理内存要看RES(resident) 列,这一列的值才是真正被映射到物理内存的大小。

常用管理命令

我们也可以自己来管理 Linux 的虚拟内存。

查看系统内存状态

查看系统内存情况的方式有很多,free、vmstat等命令都可输出当前系统的内存状态,需要注意的是可用内存并不只是 free 这一列,由于操作系统的 lazy 特性,大量的 buffer/cache 在进程不再使用后,不会被立即清理,如果之前使用它们的进程再次运行还可以继续使用,它们在必要时也是可以被利用的。

此外,通过cat /proc/meminfo可以查看系统内存被使用的详细情况,包括脏页状态等。详情可参见:/PROC/MEMINFO之谜。

pmap

如果想单独查看某一进程的虚拟内存分布情况,可以使用pmap pid命令,它会把虚拟内存各段的占用情况从低地址到高地址都列出来。

可以添加-XX参数来输出更详细的信息。

修改内存配置

我们也可以修改 Linux 的系统配置,使用sysctl vm [-options] CONFIG或 直接读写/proc/sys/vm/目录下的文件来查看和修改配置。

SWAP 操作

虚拟内存的 SWAP 特性并不总是有益,放任进程不停地将数据在内存与磁盘之间大量交换会极大地占用 CPU,降低系统运行效率,所以有时候我们并不希望使用 swap。

我们可以修改vm.swappiness=0来设置内存尽量少使用 swap,或者干脆使用swapoff命令禁用掉 SWAP。

小结

虚拟内存的概念非常容易理解,但是它会衍生出来的一系列非常复杂的知识。本文只讲了些基本原理,略过了很多细节,比如虚拟内存寻址中段寄存器的使用,操作系统使用虚拟内存增强缓存、缓冲区的应用等,有机会单独拿出来说。

责任编辑:lq6

延伸 · 阅读