《Linux是怎样工作的》 读书笔记(二)
创始人
2025-05-31 11:12:10
0

内存管理

Linux通过内核中名为内存管理系统的功能来管理系统上搭载的所有内存。除了各种进程以外,内核本身也需要使用内存。

 

内存相关的统计信息

可以通过free命令获取系统搭载的内存总量和已消耗的内存量。

 

这里对Mem: 这一行中的重要字段进行说明。需要注意的是,上面的所有数值的单位都为千字节(KB)。

total字段:系统搭载的物理内存总量。在上面的例子中约为17GB

free字段:表面上的可用内存量

buff/cache字段:缓冲区缓存与页面缓存占用的内存。当系统的可用内存量(free字段的值)减少时,可通过内核将它们释放出来

available字段:实际的可用内存量。本字段的值为free字段的值加上当内存不足时内核中可释放的内存量。“可释放的内存”指缓冲区缓存与页面缓存中的大部分内存,以及内核中除此以外的用于其他地方的部分内存.

 

内存不足

随着内存使用量增加,可用内存变得越来越少。当内存不足时,内存管理系统将回收内核中可释放的内存。如果内存使用量继续增加,系统就会陷入做什么都缺乏足够的内存,以至于无法运行的内存不足(Out Of Memory,OOM)状态。在进入OOM状态后,内存管理系统会运行被称为 OOM killer 的可怕功能,该功能会选出合适的进程并将其强制终止(kill掉),以释放出更多内存。

如果是个人计算机,这可能并非什么大问题;但如果是商用服务器,则完全不知道结束的是哪一个进程,这种状态非常令人困扰。虽然有办法令特定进程排除在OOM killer的选择范围之外,但是要在业务用的进程中筛选出允许强制结束的进程是非常困难的。因此,也有将服务器上的sysctl的vm.panic_on_oom参数从默认的0(在发生OOM时运行OOM killer)变更为1(在发生OOM时强制关闭系统)这样的做法。

虚拟内存

简而言之,虚拟内存使进程无法直接访问系统上搭载的内存,取而代之的是通过虚拟地址间接访问。进程可以看见的是虚拟地址,系统上搭载的内存的实际地址称为物理地址。此外,可以通过地址访问的范围称为地址空间。进程无法直接访问真实的内存,也就是说不存在直接访问物理地址的方法。

 

页表

通过保存在内核使用的内存中的页表,可以完成从虚拟地址到物理地址的转换。在虚拟内存中,所有内存以页为单位划分并进行管理,地址转换也以页为单位进行。在页表中,一个页面对应的数据条目称为页表项。页表项记录着虚拟地址与物理地址的对应关系。页面大小取决于CPU架构。在x86_64架构中,页面大小为4KB。

 

如果进程访问0 ~ 300的虚拟地址,CPU将自动参考页表的内容,将其转换为对相应的物理地址的访问,而无须经过内核的处理。

如果进程访问地址300 ~ 500,即页表不存在的地址映射,则在CPU上会发生缺页中断。缺页中断可以中止正在执行的命令,并启动内核中的缺页中断机构的处理。内核的缺页中断机构检测到非法访问,向进程发送SIGSEGV信号。接收到该信号的进程通常会被强制结束运行。

为进程分配内存的流程

【1】首先读取程序的可执行文件,计算运行程序所需的内存大小为,假设计算结果为300

【2】在物理内存上划分出大小为300的区域,将其分配给进程,并把代码和数据复制过去。Linux的物理内存分配使用的是更复杂的请求分页方法。

 

【4】在复制完成后,创建进程的页表,并把虚拟地址映射到物理地址。

 

【5】最后,从指定的地址开始运行即可。

【6】如果进程请求更多内存,内核将为其分配新的内存,创建相应的页表,然后把与新分配的内存(的物理地址)对应的虚拟地址返回给进程。mmap() 函数会通过系统调用向Linux内核请求新的内存。

 

利用上层进行内存分配

C语言标准库中存在一个名为malloc() 的函数,用于获取内存。在Linux中,这个函数的底层调用了mmap() 函数。

 

mmap() 函数是以页为单位获取内存的,而malloc() 函数是以字节为单位获取内存的。为了以字节为单位获取内存,glibc事先通过系统调用mmap() 向内核请求一大块内存区域作为内存池,当程序调用malloc() 函数时,从内存池中根据申请的内存量划分出相应大小(以字节为单位)的内存并返回给程序。在内存池中的内存消耗完后,glibc会再次调用mmap() 以申请新的内存区域。

这是运行在用户模式下的OS功能(glibc的malloc() 函数)为普通程序提供的一个典型功能。

虚拟地址解决的问题

内存碎片化

如图所示,假如能巧妙地设定进程的页表,就能将物理内存上的碎片整合成虚拟地址空间上的一片连续的内存区域。这样一来,碎片化的问题也就解决了。

 

访问用于其他用途的内存区域

虚拟地址空间是每个进程独有的。相应地,页表也是每个进程独有的。如图所示,进程A和进程B各自拥有独立的虚拟地址空间。得益于虚拟内存,进程根本无法访问其他进程的内存。

 

虚拟内存的应用

●文件映射●请求分页●利用写时复制快速创建进程●Swap●多级页表●标准大页

文件映射

进程在访问文件时,通常会在打开文件后使用read()、write() 以及lseek() 等系统调用。此外,Linux还提供了将文件区域映射到虚拟地址空间的功能。按照指定方式调用mmap() 函数,即可将文件的内容读取到内存中,然后把这个内存区域映射到虚拟地址空间。

请求分页

场景问题分析

对于创建进程时的内存分配,或者在创建进程后通过mmap() 系统调用进行的动态内存分配,我们是这样描述它们的流程的。

【1】内核直接从物理内存中获取需要的区域。

【2】内核设置页表,并关联虚拟地址空间与物理地址空间。但是,这种分配方式会导致内存的浪费。因为在获取的内存中,有一部分内存在获取后,甚至直到进程运行结束都不会使用

例如:●用于大规模程序中的、程序运行时未使用的功能的代码段和数据段●由glibc保留的内存池中未被用户利用的部分

为了解决这个问题,Linux利用请求分页机制来为进程分配内存。在请求分页机制中,对于虚拟地址空间内的各个页面,只有在进程初次访问页面时,才会为这个页面分配物理内存。页面的状态除了前面提到过的“未分配给进程”与“已分配给进程且已分配物理内存”这两种以外,还存在“已分配给进程但尚未分配物理内存”这种状态。

虚拟内存不足与物理内存不足

我们都知道,在进程运行时,如果获取内存失败,进程就会异常终止。但不知大家是否知道,获取内存失败也分为虚拟内存不足与物理内存不足两种情况。

当进程把虚拟地址空间的范围内的虚拟内存全部获取完毕后,就会导致虚拟内存不足。举个例子,在虚拟地址空间的大小只有500字节的情况下,图中的情况就会引发虚拟内存不足。由于已经使用完了全部虚拟地址空间,所以即使尚有300字节的可用物理内存,也会引发虚拟内存不足。

 

虚拟内存不足与剩余多少物理内存无关。如果不清楚虚拟内存的机制,可能难以想象这到底是一种什么样的情景。在x86架构上,虚拟地址空间仅有4GB,因此数据库之类的大型程序经常会引发虚拟内存不足;但是在x86_64架构上,由于虚拟地址空间扩充到了128TB,所以虚拟内存不足变得非常罕见。但是,随着程序对虚拟内存的需求不断增加,我们可能会再次迎来容易引发虚拟内存不足的一天。与虚拟内存不足相对的物理内存不足指的是系统上搭载的物理内存被耗尽的状态。

物理内存不足与进程的虚拟内存剩余多少无关。与虚拟内存不足相比,物理内存不足的情形应该更容易想象。

写时复制

用于创建进程的fork() 系统调用,利用虚拟内存机制,可以提高fork() 的执行速度。

在发起fork() 系统调用时,并非把父进程的所有内存数据复制给子进程,而是仅复制父进程的页表。虽然在父进程和子进程双方的页表项内都存在表示写入权限的字段,但此时双方的写入权限都将失效(即变得无法进行写入)。在这之后,假如只进行读取操作,那么父进程和子进程双方都能访问共享的物理页面。但是,当其中一方打算更改任意页面的数据时,则将按照下述流程解除共享。

【1】 由于没有写入权限,所以在尝试写入时,CPU将引发缺页中断。

【2】 CPU转换到内核模式,缺页中断机构开始运行。

【3】对于被访问的页面,缺页中断机构将复制一份放到别的地方,然后将其分配给尝试写入的进程,并根据请求更新其中的内容。

【4】为父进程和子进程双方更新与已解除共享的页面对应的页表项。

●对于执行写入操作的一方,将其页表项重新连接到新分配的物理页面,并赋予写入权限

●对于另一方,也只需对其页表项重新赋予写入权限即可

交换分区Swap

当物理内存耗尽时,系统就会进入OOM状态。但实际上,Linux提供了针对OOM状态的补救措施,即Swap这一利用了虚拟内存机制的功能。通过这个功能,我们可以将外部存储器的一部分容量暂时当作内存使用。具体来说,在系统物理内存不足的情况下,当出现获取物理内存的申请时,物理内存中的一部分页面将被保存到外部存储器中,从而空出充足的可用内存。这里用于保存页面的区域称为交换分区(Swap分区)。交换分区由系统管理员在构建系统时进行设置。

假设系统处于物理内存不足的状态下,且这时需要使用更多的物理内存。在图中,进程B向尚未关联物理内存的虚拟地址100发起访问,这引发了缺页中断。

 

此时,由于已经没有空闲的物理内存了,所以内核会将正在使用的物理内存中的一部分页面保存到交换分区。这个处理称为换出。在下图中,与进程A的虚拟地址100 ~ 200对应的物理地址600 ~ 700的区域会被换出到交换分区。

 

虽然在图中,被换出的页面在交换分区上的地址信息记录在页表项中,但实际上是记录在内核中专门用于管理交换分区的区域上的。在通过换出处理空出一块可用内存后,内核将这部分内存分配给进程B。

假设在经过一段时间后,系统得以空出部分可用内存。在这样的状态下,如果进程A对先前保存到交换分区的页面发起访问,此时,内核会从交换分区中将先前换出的页面重新拿回到物理内存,这个处理称为换入。

换出与换入这两个处理统称为交换。在Linux中,由于交换是以页为单位进行的,所以也称为分页。同时,换入与换出也分别称为页面调入与页面调出。

Swap乍看之下是一个能够使可用内存量扩充为“实际搭载的内存量 + 交换分区的容量”的美好机制,但这里其实存在一个非常大的陷阱。那就是,相比对内存的访问速度,对普通的外部存储器的访问速度慢了几个数量级。当系统长期处于内存不足时,访问内存的操作将导致页面不断地被换入和换出,从而导致系统陷入系统抖动(颠簸)状态。大家或许经历过这样的情形:在使用笔记本时,明明没有进行读写文件的操作,但外部存储器的访问指示灯却亮着。这种情形的原因大多在于系统抖动。一旦发生了抖动,系统就会暂时无法响应,然后一直保持那样的状态,最后宕机或者引发OOM。如果系统频繁发生交换处理,就必须重新审视一下其设计是否存在问题。会引发系统抖动的系统不应当部署到服务器上。要调整设计,可以考虑降低系统负载以降低系统内存使用量,或者单纯地为系统增添内存等。

可以通过swapon --show命令查看系统交换分区的信息。

 

如上所示,当前计算机/dev/dm-1被用作交换分区,大小为2g,交换分区的大小可以通过free命令查看。

类似于交换这类需要访问外部存储器的缺页中断称为硬性页缺失。与此相对,无须访问外部存储器的缺页中断称为软性页缺失。虽然无论发生硬性页缺失还是软性页缺失,都需要内核进行处理,进而影响性能,但硬性页缺失所产生的影响要更大。

多级页表

在x86_64架构上,虚拟地址空间大小为128TB,页面大小为4KB,页表项的大小为8字节。通过上面的信息可以算出,一个进程的页表就需要占用256GB的内存(= 8B×128TB / 4KB)。以笔者的计算机为例,由于只有32GB的内存,所以一个进程也无法创建。

为了避免这样的情况,x86_64架构的页表采用多级结构,而非上面描述的单层结构。这样就能节约大量的内存。在现实中,x86_64架构的页表结构非常复杂,因此在比较单层页表与多级页表的不同时,我们将利用比实际结构简单的模型。假设一个页面大小为100字节,虚拟地址空间的大小为1600字节。当虚拟内存使用量增加到一定程度时,多级页表的内存使用量就会超过单层页表。但这种情况非常罕见,所以从系统整体来看,也是多级页表的内存使用量更少。

 

标准大页

随着进程的虚拟内存使用量增加,进程页表使用的物理内存量也会增加。此时,除了内存使用量增加的问题之外,还存在fork() 系统调用的执行速度变慢的问题,这是因为fork() 是通过写时复制创建进程的,这虽然不需要复制物理内存的数据,但是需要为子进程复制一份与父进程同样大小的页表。为了解决这个问题,Linux提供了标准大页机制。顾名思义,标准大页是比普通的页面更大的页。利用这种页面,能有效减少进程页表所需的内存量。

存储层次

 

高速缓存

【计算机的运作流程】

  • 根据指令,将数据从内存读取到寄存器。
  • 基于寄存器上的数据进行运算。
  • 把运算结果写入内存。

就近期的硬件而言,与在寄存器上执行运算所耗费的平均时间相比,访问内存会消耗更多的时间,产生更长的延迟。前者执行一次的时间还不到1纳秒,但后者执行一次的时间能达到几十纳秒。对于计算机系统来说,无论流程②的处理速度有多快,流程①和流程③都会成为性能瓶颈,因此整体的处理速度将受限于内存的访问延迟。

而高速缓存的存在,正是为了抹平寄存器与内存之间的性能差距。从高速缓存到寄存器的访问速度比从内存到寄存器的访问速度快了几倍甚至几十倍,利用这一点,即可提高流程①和流程③的处理速度。高速缓存通常内置于CPU内,但也存在位于CPU外的类型。

在从内存往寄存器读取数据时,数据先被送往高速缓存,再被送往寄存器。所读取的数据的大小取决于缓存块大小(cache line size)的值,该值由各个CPU规定。

假设缓存块的大小为10字节,高速缓存的容量为50字节,并且存在两个长度为10字节的寄存器(R0与R1)。在这样的运行环境下,把内存地址300上的数据读取到R0时的情形如图所示。

 

修改数据场景

当需要将寄存器上的数据重新写回到地址300上时,首先会把改写后的数据写入高速缓存,如图所示。此时依然以缓存块大小为单位写入数据。然后,为这些缓存块添加一个标记,以表明这部分从内存读取的数据被改写了。通常我们会称这些被标记的缓存块“脏了”。

 

这些被标记的数据会在写入高速缓存后的某个指定时间点,通过后台处理写入内存。随之,这些缓存块就不再脏了。在这样的状态下,当CPU仅访问位于高速缓存上的数据时,访问速度比没有高速缓存时快得多,因为数据访问速度达到了高速缓存的读写速度。

高速缓存不足时

在高速缓存不足时,如果要读写高速缓存中尚不存在的数据,就要销毁一个现有的缓存块。需要先销毁其中一个缓存块的数据,再把该地址上的数据复制到空出来的缓存块上。

当需要销毁的缓存块脏了的时候,数据将在被销毁前被同步到内存中。如果在高速缓存不足,且所有缓存块都脏了的时候向内存发起访问,那么将因高速缓存频繁执行读写处理而发生系统抖动,与此同时性能也会大幅降低。

多级缓存

在最近的x86_64架构的CPU中,高速缓存都采用分层结构。各层级在容量、延迟以及“由哪些逻辑CPU共享”等方面各不相同。构成分层结构的各高速缓存分别名为L1、L2、L3(L为Level的首字母)。不同规格的CPU中的缓存层级数量也不同。在各高速缓存中,最靠近寄存器、容量最小且速度最快的是L1缓存。层级的数字越大,离寄存器越远,速度越慢,但容量越大。

参考:

 

页面缓存

与CPU访问内存的速度比起来,访问外部存储器的速度慢了几个数量级。内核中用于填补这一速度差的机构称为页面缓存。下面将介绍页面缓存的机制及其注意事项。页面缓存和高速缓存非常相似。高速缓存是把内存上的数据缓存到高速缓存上,而页面缓存则是将外部存储器上的文件数据缓存到内存上。高速缓存以缓存块为单位处理数据,而页面缓存则以页为单位处理数据。

当进程读取文件的数据时,内核并不会直接把文件数据复制到进程的内存中,而是先把数据复制到位于内核的内存上的页面缓存区域,然后再把这些数据复制到进程的内存中。在内核自身的内存中有一个管理区域,该区域中保存着页面缓存所缓存的文件以及这些文件的范围信息等。在这之后,如果再次读取已经位于页面缓存的数据,内核将直接返回页面缓存中的数据。与直接从外部存储器读取相比,从页面缓存读取的速度更快。而且,由于页面缓存是由全部进程共享的资源,所以发起读取的进程也可以不是最初访问该文件数据的进程。

在进程向文件写入数据后,内核会把数据写入页面缓存中,如图所示。这时,管理区域中与这部分数据对应的条目会被添加一个标记,以表明“这些是脏数据,其内容比外部存储器中的数据新”。这些被标记的页面称为脏页。

 

如果各个进程想要访问的文件数据都已存在于页面缓存中,那么系统的文件访问速度将能超越外部存储器的访问速度,接近内存的访问速度,因此可以期待系统整体运行速度的提升。需要注意的是,只要系统上还存在可用内存,则每当各个进程访问那些尚未读取到页面缓存中的文件时,页面缓存的大小就会随之增大。而当系统内存不足时,内核将释放页面缓存以空出可用内存。此时,首先丢弃脏页以外的页面。如果还是无法空出足够内存,就对脏页执行回写,然后继续释放页面。当需要释放脏页时,由于需要访问外部存储器,所以恐怕会导致系统性能下降。尤其是当系统上存在大量文件写入操作而导致出现大量脏页时,系统负载往往会变得非常大。内存不足引发大量脏页的回写处理,进而导致系统性能下降的情况非常常见。

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...