cover_image

一文吃透Linux虚拟内存:原理、机制与优化

往事敬秋风 深度Linux
2025年04月08日 07:30
图片

无论是日常办公处理文档,还是运行大型游戏享受沉浸式娱乐体验,亦或是企业服务器高效处理海量数据,计算机都需要迅速且稳定地对数据进行存取。而在这背后,Linux 虚拟内存技术扮演着至关重要的角色。它就像一位神奇的管家,巧妙地协调着物理内存与硬盘空间,为计算机系统的高效运转提供坚实保障。

你是否好奇,为何计算机能同时运行多个看似内存需求巨大的程序,却不会因内存不足而频繁卡顿?Linux 虚拟内存究竟是如何在幕后默默施展它的 “魔法”,实现内存的高效管理和利用的?接下来,就让我们一同深入 Linux 虚拟内存的奇妙世界,从原理到机制,再到优化策略,全方位吃透这一关键技术,探寻它为计算机性能提升带来的无限可能 。

一、虚拟内存技术

1.1为什么需要使用虚拟内存

进程需要使用的代码和数据都放在内存中,比放在外存中要快很多。问题是内存空间太小了,不能满足进程的需求,而且现在都是多进程,情况更加糟糕。所以提出了虚拟内存,使得每个进程用于3G的独立用户内存空间和共享的1G内核内存空间。(每个进程都有自己的页表,才使得3G用户空间的独立)这样进程运行的速度必然很快了。而且虚拟内存机制还解决了内存碎片和内存不连续的问题。为什么可以在有限的物理内存上达到这样的效果呢?

1.2虚拟内存概述

在深入探讨 Linux 虚拟内存之前,先来明晰虚拟内存的基本概念。虚拟内存,简单来说,是一种内存管理技术,它为每个进程提供了一个独立的、连续的地址空间,让进程误以为自己拥有一块完整且足够大的内存空间 ,而无需关心实际物理内存的具体布局和大小限制。这就好比你拥有一个超大的虚拟仓库,你可以随意规划货物的摆放位置,而不用担心仓库空间不够。

虚拟内存的主要作用之一是实现内存地址转换。在 Linux 系统中,每个进程都有自己的虚拟地址空间,这个空间通过页表(Page Table)与物理内存进行映射。页表就像是一本地址翻译字典,负责将进程使用的虚拟地址翻译成实际的物理地址。

当程序运行时,它所访问的内存地址都是虚拟地址。例如,当程序需要读取某个变量的值时,它会给出一个虚拟地址。CPU 首先会根据这个虚拟地址中的页号(Page Number)在页表中查找对应的物理页框号(Page Frame Number)。如果页表中存在这个映射关系(即页表项有效),CPU 就可以通过物理页框号和虚拟地址中的页内偏移(Offset)计算出实际的物理地址,从而访问到物理内存中的数据。

但如果页表中没有找到对应的映射关系(即发生缺页异常,Page Fault),系统会认为这个虚拟页还没有被加载到物理内存中。此时,操作系统会介入,从磁盘的交换区(Swap Area)或者文件系统中找到对应的物理页,并将其加载到物理内存中,同时更新页表,建立虚拟地址与物理地址的映射关系。之后,程序就可以通过新建立的映射关系访问到数据了。

为了更直观地理解,我们可以把虚拟内存想象成一个图书馆的目录系统。每个进程就像是一个读者,拥有自己的目录(虚拟地址空间)。当读者想要查找某本书(访问数据)时,会先在自己的目录中找到对应的条目(虚拟地址),然后通过这个条目去书架(物理内存)上找到实际的书。如果书架上没有这本书(缺页异常),图书馆管理员(操作系统)就会从仓库(磁盘)中把书取出来放到书架上,并更新目录(页表),以便下次读者能更快地找到这本书。

例如:对于程序计数器位数为32位的处理器来说,他的地址发生器所能发出的地址数目为2^32=4G个,于是这个处理器所能访问的最大内存空间就是4G。在计算机技术中,这个值就叫做处理器的寻址空间或寻址能力。

照理说,为了充分利用处理器的寻址空间,就应按照处理器的最大寻址来为其分配系统的内存。如果处理器具有32位程序计数器,那么就应该按照下图的方式,为其配备4G的内存:

图片

这样,处理器所发出的每一个地址都会有一个真实的物理存储单元与之对应;同时,每一个物理存储单元都有唯一的地址与之对应。这显然是一种最理想的情况。

但遗憾的是,实际上计算机所配置内存的实际空间常常小于处理器的寻址范围,这是就会因处理器的一部分寻址空间没有对应的物理存储单元,从而导致处理器寻址能力的浪费。例如:如下图的系统中,具有32位寻址能力的处理器只配置了256M的内存储器,这就会造成大量的浪费:

图片

另外,还有一些处理器因外部地址线的根数小于处理器程序计数器的位数,而使地址总线的根数不满足处理器的寻址范围,从而处理器的其余寻址能力也就被浪费了。例如:Intel8086处理器的程序计数器位32位,而处理器芯片的外部地址总线只有20根,所以它所能配置的最大内存为1MB:

图片

在实际的应用中,如果需要运行的应用程序比较小,所需内存容量小于计算机实际所配置的内存空间,自然不会出什么问题。但是,目前很多的应用程序都比较大,计算机实际所配置的内存空间无法满足。

实践和研究都证明:一个应用程序总是逐段被运行的,而且在一段时间内会稳定运行在某一段程序里。

这也就出现了一个方法:如下图所示,把要运行的那一段程序自辅存复制到内存中来运行,而其他暂时不运行的程序段就让它仍然留在辅存。

图片

当需要执行另一端尚未在内存的程序段(如程序段2),如下图所示,就可以把内存中程序段1的副本复制回辅存,在内存腾出必要的空间后,再把辅存中的程序段2复制到内存空间来执行即可:

图片

在计算机技术中,把内存中的程序段复制回辅存的做法叫做“换出”,而把辅存中程序段映射到内存的做法叫做“换入”。经过不断有目的的换入和换出,处理器就可以运行一个大于实际物理内存的应用程序了。或者说,处理器似乎是拥有了一个大于实际物理内存的内存空间。于是,这个存储空间叫做虚拟内存空间,而把真正的内存叫做实际物理内存,或简称为物理内存。

那么对于一台真实的计算机来说,它的虚拟内存空间又有多大呢?计算机虚拟内存空间的大小是由程序计数器的寻址能力来决定的。例如:在程序计数器的位数为32的处理器中,它的虚拟内存空间就为4GB。

可见,如果一个系统采用了虚拟内存技术,那么它就存在着两个内存空间:虚拟内存空间和物理内存空间。虚拟内存空间中的地址叫做“虚拟地址”;而实际物理内存空间中的地址叫做“实际物理地址”或“物理地址”。处理器运算器和应用程序设计人员看到的只是虚拟内存空间和虚拟地址,而处理器片外的地址总线看到的只是物理地址空间和物理地址。

由于存在两个内存地址,因此一个应用程序从编写到被执行,需要进行两次映射。第一次是映射到虚拟内存空间,第二次时映射到物理内存空间。在计算机系统中,第两次映射的工作是由硬件和软件共同来完成的。承担这个任务的硬件部分叫做存储管理单元MMU,软件部分就是操作系统的内存管理模块了。

在映射工作中,为了记录程序段占用物理内存的情况,操作系统的内存管理模块需要建立一个表格,该表格以虚拟地址为索引,记录了程序段所占用的物理内存的物理地址。这个虚拟地址/物理地址记录表便是存储管理单元MMU把虚拟地址转化为实际物理地址的依据,记录表与存储管理单元MMU的作用如下图所示:

图片

综上所述,虚拟内存技术的实现,是建立在应用程序可以分成段,并且具有“在任何时候正在使用的信息总是所有存储信息的一小部分”的局部特性基础上的。它是通过用辅存空间模拟RAM来实现的一种使机器的作业地址空间大于实际内存的技术。

从处理器运算装置和程序设计人员的角度来看,它面对的是一个用MMU、映射记录表和物理内存封装起来的一个虚拟内存空间,这个存储空间的大小取决于处理器程序计数器的寻址空间。

可见,程序映射表是实现虚拟内存的技术关键,它可给系统带来如下特点:

  • 系统中每一个程序各自都有一个大小与处理器寻址空间相等的虚拟内存空间;
  • 在一个具体时刻,处理器只能使用其中一个程序的映射记录表,因此它只看到多个程序虚存空间中的一个,这样就保证了各个程序的虚存空间时互不相扰、各自独立的;
  • 使用程序映射表可方便地实现物理内存的共享。
  • 二、Linux 虚拟内存工作机制

2.1内存映射机制

以存储单元为单位来管理显然不现实,因此Linux把虚存空间分成若干个大小相等的存储分区,Linux把这样的分区叫做页。为了换入、换出的方便,物理内存也就按也得大小分成若干个块。由于物理内存中的块空间是用来容纳虚存页的容器,所以物理内存中的块叫做页框。页与页框是Linux实现虚拟内存技术的基础。

虚拟内存的页、物理内存的页框及页表

在Linux中,页与页框的大小一般为4KB。当然,根据系统和应用的不同,页与页框的大小也可有所变化。

物理内存和虚拟内存被分成了页框与页之后,其存储单元原来的地址都被自然地分成了两段,并且这两段各自代表着不同的意义:高位段分别叫做页框码和页码,它们是识别页框和页的编码;低位段分别叫做页框偏移量和页内偏移量,它们是存储单元在页框和页内的地址编码。下图就是两段虚拟内存和物理内存分页之后的情况:

图片

为了使系统可以正确的访问虚存页在对应页框中的映像,在把一个页映射到某个页框上的同时,就必须把页码和存放该页映像的页框码填入一个叫做页表的表项中。这个页表就是之前提到的映射记录表。一个页表的示意图如下所示:

图片

页模式下,虚拟地址、物理地址转换关系的示意图如下所示:

图片

也就是说:处理器遇到的地址都是虚拟地址。虚拟地址和物理地址都分成页码(页框码)和偏移值两部分。在由虚拟地址转化成物理地址的过程中,偏移值不变。而页码和页框码之间的映射就在一个映射记录表——页表中

话说回来,内存映射是 Linux 中一种重要的内存管理技术,它允许将一个文件或者其他对象映射到进程的虚拟地址空间中,使得进程可以像访问内存一样直接访问文件 。这种技术的核心优势在于提高了文件访问的效率,减少了内核和用户空间之间的数据拷贝。在 Linux 中,内存映射主要通过mmap()系统调用实现。

mmap()函数将文件或其他对象映射到虚拟地址空间的一个连续区域,返回一个指向映射区域开始地址的指针 。对该指针进行读写操作,实际上就是在访问文件内容。使用munmap()函数可以解除内存映射。例如,在 C 语言中,可以这样使用mmap()函数:

#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int fd;
    char *map_start;
    off_t file_size;

    // 打开文件
    fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 获取文件大小
    file_size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);

    // 创建内存映射
    map_start = (char *)mmap(0, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map_start == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 关闭文件描述符,映射依然有效
    close(fd);

    // 访问映射内存,就像访问文件一样
    printf("Content of file: %s\n", map_start);

    // 修改映射内存中的内容
    sprintf(map_start, "This is a new content");

    // 解除内存映射
    if (munmap(map_start, file_size) == -1) {
        perror("munmap");
        return 1;
    }

    return 0;
}

在这个例子中,首先打开一个文件,然后使用mmap()函数将文件映射到进程的虚拟地址空间。通过返回的指针map_start,可以像访问普通内存一样访问文件内容。修改map_start指向的内存区域,实际上就是在修改文件内容。最后,使用munmap()函数解除内存映射。

内存映射在实际应用中有很多场景。比如在共享库的加载中,多个进程可以映射同一个共享库文件,实现代码和数据的共享,减少内存占用 。在文件 I/O 操作中,对于大文件的读写,内存映射可以避免频繁的系统调用和数据拷贝,提高读写效率。例如,数据库系统通常会使用内存映射来处理数据文件,加快数据的读取和写入速度。

2.2请页与交换

虚存页面到物理页框的映射叫做页面的加载

当处理器试图访问一个虚存页面时,首先到页表中去查询该页是否已映射到物理页框中,并记录在页表中。如果在,则MMU会把页码转换成页框码,并加上虚拟地址提供的页内偏移量形成物理地址后去访问物理内存;如果不在,则意味着该虚存页面还没有被载入内存,这时MMU就会通知操作系统:发生了一个页面访问错误(页面错误),接下来系统会启动所谓的“请页”机制,即调用相应的系统操作函数,判断该虚拟地址是否为有效地址。

如果是有效的地址,就从虚拟内存中将该地址指向的页面读入到内存中的一个空闲页框中,并在页表中添加上相对应的表项,最后处理器将从发生页面错误的地方重新开始运行;如果是无效的地址,则表明进程在试图访问一个不存在的虚拟地址,此时操作系统将终止此次访问。

当然,也存在这样的情况:在请页成功之后,内存中已没有空闲物理页框了。这是,系统必须启动所谓地“交换”机制,即调用相应的内核操作函数,在物理页框中寻找一个当前不再使用或者近期可能不会用到的页面所占据的页框。找到后,就把其中的页移出,以装载新的页面。对移出页面根据两种情况来处理:如果该页未被修改过,则删除它;如果该页曾经被修改过,则系统必须将该页写回辅存。

系统请页的处理过程如下所示:

为了公平地选择将要从系统中抛弃的页面,Linux系统使用最近最少使用(LRU)页面的衰老算法。这种策略根据系统中每个页面被访问的频率,为物理页框中的页面设置了一个叫做年龄的属性。页面被访问的次数越多,则页面的年龄最小;相反,则越大。而年龄较大的页面就是待换出页面的最佳候选者。

图片

2.3快表

在系统每次访问虚存页时,都要在内存的所有页表中寻找该页的页框,这是一个很费时间的工作。但是,人们发现,系统一旦访问了某一个页,那么系统就会在一段时间内稳定地工作在这个页上。所以,为了提高访问页表的速度,系统还配备了一组正好能容纳一个页表的硬件寄存器,这样当系统再访问虚存时,就首先到这组硬件寄存器中去访问,系统速度就快多了。这组存放当前页表的寄存器叫做快表。

总之,使用虚拟存储技术时,处理器必须配备一些硬件来承担内存管理的一部分任务。承担内存管理任务的硬件部分叫做存储管理单元MMU。存储管理单元MMU的工作过程如下图所示:

图片

(1)页的共享

在多程序系统中,常常有多个程序需要共享同一段代码或数据的情况。在分页管理的存储器中,这个事情很好办:让多个程序共享同一个页面即可。

具体的方法是:使这些相关程序的虚拟空间的页面在页表中指向内存中的同一个页框。这样,当程序运行并访问这些相关页面时,就都是对同一个页框中的页面进行访问,而该页框中的页就被这些程序所共享。下图是3个程序共享一个页面的例子:

图片

(2)页的保护

由上可知,页表实际上是由虚拟空间转到物理空间的入口。因此,为了保护页面内容不被没有该页面访问权限的程序所破坏,就应在页表的表项中设置一些访问控制字段,用于指明对应页面中的内容允许何种操作,从而禁止非法访问。下图是页表项中存放控制信息的一种可能的形式:

图片

注意:其中的PCD位表示着是否允许高速缓存(cache)。

如果程序对一个页试图进行一个该页控制字段所不允许的操作,则会引起操作系统的一次中断——非法访问中断,并拒绝这种操作,从而保护该页的内容不被破坏。

(3)多级页表

需要注意的是,页表是操作系统创建的用于内存管理的表格。因此,一个程序在运行时,其页表也要存放到内存空间。如果一个程序只需要一个页表,则不会有什么问题。但如果,程序的虚拟空间很大的话,就会出现一个比较大的问题。

比如:一个程序的虚拟空间为4GB,页表以4KB为一页,那么这个程序空间就是1M页。为了存储这1M页的页指针,那么这个页表的长度就相当大了,对内存的负担也很大了。所以,最好对页表也进行分页存储,在程序运行时只把需要的页复制到内存,而暂时不需要的页就让它留在辅存中。为了管理这些页表页,还要建立一个记录页表页首地址的页目录表,于是单级页表就变成了二级页表。二级页表的地址转换如下图所示:

图片

当然,如果程序的虚拟空间更大,那么也可以用三级页表来管理。为了具有通用性,Linux系统使用了三级页表结构:页目录(Page Directory,PGD)、中间页目录(Page Middle Directory,PMD)、页表(Page Table,PTE)。

2.4Linux的页表结构

为了通用,Linux系统使用了三级页表结构:页目录、中间页目录和页表。PGD为顶级页表,是一个pgd_t数据类型(定义在文件linux/include/page.h中)的数组,每个数组元素指向一个中间页目录;PMD为二级页表,是一个pmd_t数据结构的数组,每个数组元素指向一个页表;PTE则是页表,是一个pte_t数据类型的数组,每个元素中含有物理地址。

图片

为了应用上的灵活,Linux使用一系列的宏来掩盖各种平台的细节。用户可以在配置文件config中根据自己的需要对页表进行配置,以决定是使用三级页表还是使用二级页表。

在系统编译时,会根据配置文件config中的配置,把目录include/asm符号连接到具体CPU专用的文件目录中。例如,对于i386CPU,该目录符号会连接到include/asm-i386,并在文件pgable-2level-defs.h中定义了二级页表的基本结构,如下图:

图片

其中还定义了:

#define PGDIR_SHIFT 22                        //PGD在线性地址中的起始地址为bit22
#define PTRS_PER_PGD 1024                     //PGD共有1024个表项
#define PTRS_PER_PTE 1024                     //PTE共有1024个表项
#endif

在文件include/asm-i386/pgtable.h中定义了页目录和页表项的数据结构,如下:

typedof struct { unsigned long pte_low; } pte_t;                    //页表中的物理地址,页框码
typedof struct { unsigned long pgd; } pgd_t;                        //指向一个页表
typedof struct { unsigned long pgprot; } pgprot_t;                  //页表中的各个状态信息和访问权限

从定义可知,它们都是只有一个长整型类型(32位)的结构体。

注意:如上文的“页的保护”部分,页框码代表物理地址,只需要高20位就够了(因为页框的长度为4KB,因此页内偏移12位)。而后12位可以存放各个状态信息和访问权限。但是Linux并没有这样做,反而重新定义了一个结构体来存放,通过“或”运算来将两者结合。

2.5Swap 交换机制

首先呢,提一个概念,交换空间(swap space),这个大家应该不陌生,在重装系统的时候,会让你选择磁盘分区,就比如说一个硬盘分几个部分去管理。其中就会分一部分磁盘空间用作交换,叫做swap space。其实就是一段临时存储空间,内存不够用的时候就用它了,虽然它也在磁盘中,但省去了很多的查找时间啊。当发生进程切换的时候,内存与交换空间就要发生数据交换一满足需求。所以啊,进程的切换消耗是很大的,这也说明了为什么自旋锁比信号量效率高的原因。

那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;就是说一个函数可能只在物理内存中呆了一会,用完了就被清除出去了,虽然在虚拟地址空间还在。(不过虚拟地址空间不是事实上的存储,所以只能说这个函数占据了一段虚拟地址空间,当你访问这段地址时,就会产生缺页处理,从交换区把对应的代码搬到物理内存上来)

Swap 交换机制是 Linux 虚拟内存管理的另一个重要组成部分。简单来说,Swap 是磁盘上的一块区域,当物理内存不足时,系统会将一部分暂时不用的内存页面(Page)交换到 Swap 空间中,腾出物理内存给更需要的进程使用 。当被交换出去的页面再次被访问时,系统会将其从 Swap 空间换回到物理内存中。

Swap 交换机制的工作原理涉及到内存回收和页面置换算法。当系统内存紧张时,内核会启动内存回收机制,扫描内存中的页面,选择一些不常用或最近最少使用的页面进行回收。如果这些页面是匿名页面(没有关联到文件的内存页面,如进程的堆和栈空间),就会被交换到 Swap 空间中;如果是文件映射页面(关联到文件的内存页面,如共享库、文件缓存等),则会根据情况进行处理,脏页面(被修改过的页面)会被写回文件,干净页面(未被修改过的页面)可以直接释放。

Swap 交换机制对系统性能有着重要的影响。当 Swap 使用频繁时,说明物理内存不足,系统需要频繁地在物理内存和 Swap 空间之间交换页面,这会导致磁盘 I/O 增加,系统性能下降 。因为磁盘的读写速度远远低于内存,过多的 Swap 操作会使系统变得迟缓。因此,在实际应用中,需要合理配置 Swap 空间的大小,并密切关注系统的内存使用情况,避免 Swap 过度使用。

例如,可以通过调整/proc/sys/vm/swappiness参数来控制系统对 Swap 的使用倾向,swappiness的值范围是 0 - 100,表示系统将内存页面交换到 Swap 空间的倾向程度,值越大表示越倾向于使用 Swap 。一般来说,对于内存充足的系统,可以将swappiness设置为较低的值,如 10 或 20,以减少不必要的 Swap 操作;对于内存紧张的系统,可以适当提高swappiness的值,但也要注意不要过高,以免严重影响性能。

三、Linux 虚拟内存管理工具

在 Linux 系统中,有多个实用工具可以帮助我们查看和管理虚拟内存,了解系统的内存使用状态,下面介绍几个常用的命令。

3.1top 命令

top命令是一个功能强大的系统监控工具,它能够实时显示系统中各个进程的资源使用情况,包括CPU、内存等 。通过 top 命令,我们可以直观地了解到系统中哪些进程占用了较多的虚拟内存。在终端中输入 “top”,即可启动该命令,其输出结果大致如下:

top - 14:20:12 up 2 days,  1:23,  2 users,  load average: 0.00, 0.01, 0.05
Tasks: 152 total,   1 running, 151 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8161844 total,  7433924 free,   149744 used,   578176 buff/cache
KiB Swap:  2097148 total,  2097148 free,        0 used.  7663360 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root      20   0   12844   7448   4564 S   0.0  0.1   0:02.33 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd

在 top 命令的输出中,与虚拟内存相关的主要是 “VIRT” 列,它表示进程使用的虚拟内存总量,单位为 KB 。例如,上述输出中 PID 为 1 的 systemd 进程,其 VIRT 值为 12844KB,表示该进程使用了 12844KB 的虚拟内存。此外,“RES” 列表示进程使用的物理内存大小,“SHR” 列表示共享内存大小 。通过观察这些指标,我们可以了解每个进程对内存资源的占用情况,判断是否存在内存使用异常的进程。在实际应用中,如果发现某个进程的 VIRT 值持续增长且占用大量虚拟内存,可能需要进一步分析该进程的行为,看是否存在内存泄漏等问题。比如,在一个长时间运行的服务器程序中,如果其 VIRT 值不断上升,而业务量并没有明显增加,就需要检查程序代码,查看是否有未释放的内存资源。

3.2free 命令

free 命令用于显示系统内存的使用情况,包括物理内存和虚拟内存(交换空间) 。它可以帮助我们快速了解系统内存的整体使用状态,判断是否存在内存不足的情况。在终端中输入 “free”,输出结果如下:

              total        used        free      shared  buff/cache   available
Mem:        8161844      149744     7433924       34624     578176     7663360
Swap:       2097148           0     2097148

在 free 命令的输出中,“total” 表示系统内存的总量,“used” 表示已使用的内存量,“free” 表示空闲内存量,“shared” 表示共享内存量,“buff/cache” 表示缓冲区和缓存使用的内存量,“available” 表示应用程序还可以申请到的内存 。其中,与虚拟内存相关的是 “Swap” 部分,“Swap total” 表示交换空间的总量,“Swap used” 表示已使用的交换空间量,“Swap free” 表示空闲的交换空间量 。例如,上述输出中 Swap total 为 2097148KB,Swap used 为 0KB,Swap free 为 2097148KB,说明当前系统的交换空间未被使用,这通常是一个比较理想的状态,意味着系统的物理内存充足,不需要频繁地进行内存交换操作。如果 Swap used 的值较大,说明系统的物理内存可能不足,需要将一部分内存数据交换到磁盘的交换空间中,这可能会导致系统性能下降,因为磁盘的读写速度远远低于内存。在这种情况下,我们可以考虑增加物理内存,或者优化系统的内存使用,减少不必要的内存占用。

3.3vmstat 命令

vmstat(Virtual Memory Statistics)命令用于显示虚拟内存统计信息,同时也可以展示进程、CPU、I/O 等系统整体运行状态 。它提供了更详细的内存使用信息,对于深入分析系统性能非常有帮助。在终端中输入 “vmstat”,输出结果如下:

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 7433924 149744 578176    0    0     1     1    1    1  0  0 100  0  0

在 vmstat 命令的输出中,与虚拟内存相关的字段有 “swpd”“si” 和 “so” 。“swpd” 表示使用的虚拟内存量(单位为 KB),“si” 表示从磁盘交换到内存的内存量(单位为 KB/s),“so” 表示从内存交换到磁盘的内存量(单位为 KB/s) 。例如,上述输出中 swpd 为 0,表示当前系统没有使用虚拟内存;si 和 so 都为 0,表示当前没有发生内存交换操作。

当 si 和 so 的值不为 0 时,说明系统正在进行内存交换,数值越大,说明内存交换越频繁,这可能会对系统性能产生较大影响,需要进一步分析原因,采取相应的优化措施,如增加物理内存、调整应用程序的内存使用策略等。此外,vmstat 命令还可以通过指定刷新时间间隔和刷新次数来持续监控系统状态,例如 “vmstat 2 10” 表示每 2 秒刷新一次,共刷新 10 次,这样可以更直观地观察系统内存使用情况的变化趋势。

四、Linux 虚拟内存优化策略

4.1调整内核参数

在 Linux 系统中,通过调整内核参数可以对虚拟内存的性能进行优化 。其中,vm.swappiness是一个非常重要的内核参数,它的值表示系统将内存页面交换到 Swap 空间的倾向程度,取值范围是 0 - 100 。当vm.swappiness的值为 0 时,系统尽量不使用 Swap 空间,只有在物理内存完全耗尽时才会考虑交换;当值为 100 时,系统会非常积极地将内存页面交换到 Swap 空间 。

可以使用sysctl命令来查看和临时修改vm.swappiness的值。例如,要查看当前vm.swappiness的值,可以在终端中输入:

sysctl vm.swappiness

如果要将vm.swappiness的值临时修改为 10,可以使用以下命令:

sysctl vm.swappiness=10

这种修改在系统重启后会失效。如果想要永久修改vm.swappiness的值,可以编辑/etc/sysctl.conf文件,添加或修改vm.swappiness = 10这一行,然后执行sysctl -p使修改生效 。

vm.vfs_cache_pressure也是一个重要的内核参数,它控制着文件系统缓存(VFS Cache)被回收的倾向 。该参数的值越大,文件系统缓存就越容易被回收;值越小,文件系统缓存就越不容易被回收 。默认值通常为 100。如果系统中文件 I/O 操作频繁,可以适当降低vm.vfs_cache_pressure的值,以减少文件系统缓存的回收,提高文件访问性能 。同样,可以使用sysctl命令来查看和修改这个参数,例如:

sysctl vm.vfs_cache_pressure
sysctl vm.vfs_cache_pressure=50

修改内核参数对虚拟内存性能有着显著的影响。合理调整vm.swappiness可以避免系统过度依赖 Swap 空间,减少磁盘 I/O 操作,提高系统整体性能 。如果vm.swappiness设置过高,系统频繁进行内存交换,会导致磁盘 I/O 负载增加,系统响应变慢;如果设置过低,当物理内存不足时,可能会导致进程因无法获取足够内存而被终止 。而调整vm.vfs_cache_pressure则可以优化文件系统缓存的使用,提高文件读写效率,对于那些依赖文件系统的应用程序(如数据库、文件服务器等)来说,合理设置该参数能有效提升其性能 。

4.2优化应用程序

应用程序的内存使用方式对虚拟内存性能有着直接的影响 。在实际开发中,常常会出现应用程序内存使用不当的情况,从而导致虚拟内存的浪费和系统性能的下降 。比如,在一些 Web 应用程序中,可能会存在内存泄漏的问题。以使用 Python 的 Flask 框架开发的 Web 应用为例,如果在视图函数中创建了大量的对象,却没有及时释放,随着时间的推移,这些未释放的对象会占用越来越多的内存,导致虚拟内存不断增加 。假设我们有如下代码:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    data = []
    for i in range(10000):
        # 创建大量对象但未及时释放
        obj = {'key': i}
        data.append(obj)
    return 'Hello, World!'


if __name__ == '__main__':
    app.run()

在这个简单的示例中,每次访问/路由时,都会创建 10000 个字典对象并添加到data列表中,但在函数结束后,这些对象并没有被及时释放,会造成内存浪费。为了优化应用程序的内存使用,可以采用以下方法:

首先,使用内存分析工具(如 Python 中的memory_profiler、C++ 中的Valgrind等)来检测应用程序中的内存泄漏和不合理的内存使用 。通过这些工具,可以定位到具体的代码行,找出内存问题的根源 。其次,合理设计数据结构和算法,减少不必要的内存占用 。比如,在存储大量整数数据时,如果数据范围较小,可以使用uint8_t(无符号 8 位整数)类型代替int(通常为 32 位或 64 位整数)类型,这样可以节省内存空间 。

另外,对于一些频繁创建和销毁的对象,可以考虑使用对象池技术 。以数据库连接池为例,在一个 Web 应用中,如果每次处理请求都创建新的数据库连接,会消耗大量资源。使用数据库连接池后,可以预先创建一定数量的数据库连接对象,当有请求时,直接从连接池中获取连接,使用完毕后再放回连接池,避免了频繁创建和销毁连接对象带来的开销 。

优化应用程序的内存使用对虚拟内存性能有着重要的作用。通过减少内存泄漏和优化内存使用,可以降低应用程序对虚拟内存的需求,减少系统内存管理的负担,提高系统的整体性能和稳定性 。当应用程序能够高效地使用内存时系统可以将更多的内存资源分配给其他需要的进程,避免因内存不足而导致的频繁内存交换和系统性能下降 。

4.3合理配置 Swap 空间

Swap空间的大小和配置对系统性能有着显著的影响 。如果Swap空间设置得太小,当物理内存不足时,系统可能无法及时将内存页面交换到 Swap 空间中,导致进程因无法获取足够内存而出现异常,甚至系统崩溃 。相反,如果Swap空间设置得过大,会占用过多的磁盘空间,而且在系统不需要使用Swap空间时,这些空间就被浪费了 。

在 Linux 系统中,可以通过创建交换分区或交换文件的方式来设置 Swap 空间 。以创建交换文件为例,首先需要使用dd命令创建一个指定大小的文件,例如创建一个大小为 2GB 的交换文件:

sudo dd if=/dev/zero of=/swapfile bs=1M count=2048

上述命令中,if=/dev/zero表示从/dev/zero设备读取数据(/dev/zero是一个特殊的设备文件,它会不断返回 0 值字节流),of=/swapfile表示将数据写入到/swapfile文件中,bs=1M表示每次读写的数据块大小为 1MB,count=2048表示总共读写 2048 次,即创建一个 2GB 大小的文件 。

创建好文件后,需要使用mkswap命令将其格式化为交换文件:

sudo mkswap /swapfile

最后,使用swapon命令启用这个交换文件:

sudo swapon /swapfile

如果希望系统在每次启动时自动启用这个交换文件,可以将其添加到/etc/fstab文件中,在文件末尾添加一行:

/swapfile none swap sw 0 0

除了设置 Swap 空间的大小,还可以设置 Swap 空间的优先级 。在 Linux 系统中,每个 Swap 设备都有一个优先级,取值范围是 - 2^31 到 2^31 - 1 。优先级较高的 Swap 设备会优先被使用 。可以使用swapon命令的-p选项来设置 Swap 设备的优先级,例如:

sudo swapon -p 10 /swapfile

上述命令将/swapfile这个交换文件的优先级设置为 10 。如果系统中有多个 Swap 设备,可以根据实际需求为它们设置不同的优先级,以优化内存交换的性能 。比如,对于读写速度较快的 SSD 磁盘上的 Swap 分区,可以设置较高的优先级,使其在内存交换时优先被使用,减少因磁盘 I/O 速度慢而导致的性能下降 。

Linux内核 · 目录
上一篇解锁Linux内核黑科技:页面回收技术大揭秘下一篇全面解读zsmalloc:高效内存分配器的源码
修改于2025年04月25日
继续滑动看下一个
深度Linux
向上滑动看下一个