5. 内存管理

Linux通过内核中名为内存管理系统的功能来管理系统上搭载的所有内存。

内存相关的统计

  1. 通过free命令获取搭载的内存总量和已消耗的内存量。
  • 所有的数值单位都是KB
  • total字段:系统搭载的物理内存总量
  • free字段:表面上的可用内存量
  • buff/cache字段:缓冲区缓存与页面缓存占用的内存。当系统的可用内存量(free字段的值)减少时,可通过内核将它们释放出来。
  • available字段:实际的可用内存量。本字段的值为free字段的值加上当内存不足时内核中可释放的内存量。"可释放的内存量"指缓冲区缓存与页面缓存中的大部分内存,以及内核中除此以外的用于其他地方的部分内存。
  1. 通过sar -r 1 1命令进行内存相关信息的采集
free命令的字段 sar -r命令的字段
total NA
free kbmemfree
buff/cache kbbuffers + kbcached
available NA

内存不足

随着内存使用量增加,可用内存变得越来越少。
如果内存使用量继续增加,系统就会陷入做什么都缺乏足够的内存,以至于无法运行的内存不足状态。

在进入内存不足状态(OOM)后,内存管理系统会运行被称为OOM killer的可怕功能,该功能会选出合适的进程将其强制终止,以释放出更多内存。

通过将服务器上的sysctl的vm.panic_on_oom参数从默认的0变更为1,可以关闭OOM killer的行为。

简单的内存分配

内核为进程分配内存的时机大体上分为以下两种:

  • 在创建进程时
  • 在创建完进程后,动态分配内存时

在进程被创建后,如果还需要申请更多内存,那么进程将向内核发出用于获取内存的系统调用,提出分配内存的请求。内核在收到分配内存的请求时,会按照请求量在可用内存中分出相应大小的内存,并把这部分内存的起始地址返回给提出请求的进程。

这种分配方式会引起下列问题:

  • 内存碎片化
    每个可用的100字节共300字节,但是由于碎片化,导致大于100字节的内存都无法获得。

  • 访问用于其他用途的内存区域
    简单机制中,进程均可以通过内存地址来访问内核或其他进程所使用的内存。
    这样就会存在数据被损毁或泄露的风险。

  • 难以执行多任务
    当多个进程同时运行时,同一个进程运行两个时,会存在内存映射到相同的地址上的情况,导致异常。

虚拟内存

基于简单内存分配机制中的问题,引入了虚拟内存技术。

虚拟内存使进程无法直接访问系统上搭载的内存,取而代之的是通过虚拟地址间接访问。

进程可以看见的是虚拟地址,系统上搭载的内存的实际地址称为物理地址。通过地址访问的范围称为地址空间

readelf命令或cat /proc/{pid}/maps输出的地址也是虚拟地址,进程无法直接访问真实的内存。

页表

通过保持在内核使用的内存中的页表,可以完成从虚拟地址到物理地址的转换。

在虚拟内存中,所有内存以页为单位划分并进行管理,地址转换也以页为单位进行。

在页表中,一个页面对应的数据条目称为页表项。页表项记录着虚拟地址与物理地址的对应关系。

虚拟地址空间的大小是固定的,并且页表项中存在一个表示页面是否关联着物理内存的数据。如果访问没有关联物理内存的虚拟内存,CPU上会发生缺页中断。缺页中断可以中止正在执行的命令,并启动内核中的缺页中断机构的处理。

内核的缺页中断机构检测到非法访问,向程序发送SIGSEGV信号。接收到该信号的进程通常会被强制结束运行。

为进程分配内存

  • 在创建进程时
    运行程序所需的内存大小 = 代码段的大小 + 数据段的大小

因此,在物理内存上划分出所需的内存大小的区域,将其分配给进程,并把代码和数据复制过去。

在现实中,Linux的物理内存分配使用的是更复杂的请求分页方法。

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

最后,从指定的地址开始运行进程。

  • 在动态分配内存时

如果进程请求更多内存,内核将为其分配新的内存,创建相应的页表,然后把新分配的内存对应的虚拟地址返回给进程。

利用上层进行内存分配

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

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

虚拟内存是如何解决简单内存分配带来的问题的

  • 内存碎片化
    巧妙地设定进程地页表,就能将物理内存上地碎片整合成虚拟地址空间上的一片连续的内存区域。

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

虚拟地址空间是每个进程独有的。相应的,页表也是每个进程独有的。
所以进程根本无法访问其他进程的内存。

出于实现上的方便,内核的内存区域被映射到了所有进程的虚拟地址空间中。但是,与内核的内存对应的页表项上都注有"内核模式专用"的信息,表明仅允许在CPU运行在内核模式下时访问,因此这部分内存也不可能被运行在用户模式下的进程窥探或损毁。

  • 难以执行多任务
    每个进程拥有独立的虚拟地址空间。因此,我们可以编写运行于专用地址空间的程序,而不用担心干扰其他程序的运行,同时也不用担心自身的内存在哪个物理地址上。

虚拟内存的应用

  • 文件映射

按照指定方式调用mmap()函数,即可将文件的内容读取到内存中,然后把这个内存区域映射到虚拟地址空间。

这样就可以按照访问内存的方式来访问被映射的文件了。
被访问的区域会在规定的时间点写入外部存储器上的文件。

  • 请求分页
    有问题的内存分配流程:
    内核直接从物理内存中获取需要的区域。
    内核设置页表,并关联虚拟地址空间与物理地址空间。

这种分配方式会导致内存的浪费。因为在获取的内存中,有一部分内存在获取后,甚至直到进程运行结束都不会使用。
例如:用于大规模程序中,程序运行时未使用的功能的代码段和数据段;由glibc保留的内存池中未被用户利用的部分。

为了解决这个问题,Linux利用请求分页机制来为进程分配内存。

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

  • 刚创建完进程时(未分配物理内存)

  • 在开始运行时,为入口点所属的页面分配内存

    1. 进程访问入口点
    2. CPU参照页表,筛选出入口点所属的页面中哪些虚拟地址未关联物理地址
    3. 在CPU中引发缺页中断
    4. 内核中的缺页中断机构未步骤1中访问的页面分配物理内存,并更新其页表
    5. 回到用户模式,继续运行进程
      另外,进程并不会感知到自身在运行时曾发生过缺页中断。此后,每当访问新的区域时,都如上述流程所示,先触发缺页中断,然后分配物理内存,并更新对应的页表。
  • 虚拟内存不足与物理内存不足
    当进程把虚拟地址空间的范围内的虚拟内存全部获取完毕后,就会导致虚拟内存不足。

与虚拟内存不足相对的物理内存不足指的是系统上搭载的物理内存被耗尽的状态。

  • 利用写时复制快速创建进程
    在发起fork()系统调用时,并非把父进程的所有内存数据复制给子进程,而是仅复制父进程的页表。

    虽然在父进程和子进程双方的页表项内都存在表示写入权限的字段,但此时双方的写入权限都将失效。

    1. 由于没有写入权限,所以在尝试写入时,CPU将引发缺页中断
    2. CPU转换到内核模式,缺页中断机构开始运行
    3. 对于被访问的页面,缺页中断机构将复制一份放到别的地方,然后将其分配给尝试写入的进程,并根据请求更新其中的内容
    4. 为父进程和子进程双发更新与已解除共享的页面对应的页表项
      对于执行写入操作的一方,将其页表项重新连接到新分配的物理页面,并赋予写入权限。
      对于另一方,也只需对齐页表项重新赋予写入权限即可。

      因为物理内存并非在发起fork()系统调用时进行复制,而是在尝试写入时才进行复制,所以这个机制被称为写时复制
  • Swap

当物理内存耗尽时,系统就会进入OOM状态。但实际上,Linux提供了针对OOM状态的补救措施,即Swap这一利用了虚拟内存机制的功能。

通过这个功能,我么可以将外部存储器的一部分容量暂时当作内存使用。

在系统物理内存不足的情况下,当出现获取内存的申请时,物理内存中的一部分页面将被保存到外部存储器中,从而空出充足的可用内存。这里用于保存页面的区域称为交换分区swap分区

换出操作:由于已经没有空闲的物理内存了,所以内核会将正在使用的物理内存中的一部分页面保存到交换分区

内核基于预设的算法选出,一般是短时间内应该用不上的区域。

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

Swap咋看之下是一个能够使可用内存量扩充了,但是实际上由于外部存储器的访问速度慢,当系统长期处于内存不足时,访问内存的操作将导致页面不断地被换入或换出,从而导致系统陷入系统抖动状态。

通过sar -S命令,则当发生交换处理时,就可以确认该交换处理到底是暂时性地还是毁灭性的。

kbswpused字段地值表示交换分区使用量,如果这个值不断增加就非常危险了。

硬性页缺失: 类似于交换这类需要访问外部存储器地缺页中断
软性页缺失: 无须访问外部存储器地缺页中断

  • 多级页表

当虚拟内存使用量增加到一定程度时,多级页表地内存使用量就会超过单层页表。但这种情况非常罕见。

可以通过sar -r ALL 1 1命令中的kbpgtbl字段查看页表所使用的物理内存量。

除了分配过多物理内存给进程之外,"创建太多进程"以及"进程使用太多虚拟内存而导致页表占用的区域增加"等情况都会使系统陷入内存不足。

  • 标准大页

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

标准大页是比普通的页面更大的页。利用这种页面,能有效减少进程页表所需的内存量。

普通页表

标准大页表

通过将普通页面置换成标准大页,不但能降低页表的内存使用量,还能提供fork()系统调用的执行速度。

posted @ 2024-07-11 14:38  Python习者  阅读(5)  评论(0)    收藏  举报